Files
tono/scripts/generate_demo_particles.py

80 lines
2.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Generate a synthetic nanoparticle image for the demo/ folder.
The image simulates an AFM scan of particles on a flat substrate:
- Slightly noisy background
- ~20 hemisphere-shaped particles with varying radii and heights
- Saved as both .npy (calibrated float64) and .png (visual preview)
Run from project root:
python scripts/generate_demo_particles.py
"""
import numpy as np
from pathlib import Path
DEMO_DIR = Path(__file__).resolve().parent.parent / "demo"
DEMO_DIR.mkdir(exist_ok=True)
RNG = np.random.default_rng(2024)
# --- Image parameters ---
N = 256 # pixels
SCAN_SIZE = 5e-6 # 5 µm scan
PIXEL_SIZE = SCAN_SIZE / N # metres per pixel
BG_NOISE_RMS = 0.3e-9 # 0.3 nm background noise
# --- Generate particles ---
particles = []
# Hand-placed cluster + random scatter to give a realistic spread
fixed = [
# (cx_frac, cy_frac, radius_nm, height_nm)
(0.25, 0.30, 120, 30),
(0.28, 0.34, 80, 20),
(0.70, 0.25, 150, 45),
(0.50, 0.55, 100, 25),
(0.55, 0.60, 60, 15),
(0.15, 0.75, 200, 55),
(0.80, 0.80, 90, 22),
]
for cx_f, cy_f, r_nm, h_nm in fixed:
particles.append((cx_f * N, cy_f * N, r_nm * 1e-9, h_nm * 1e-9))
# Random particles
for _ in range(15):
cx = RNG.uniform(20, N - 20)
cy = RNG.uniform(20, N - 20)
radius = RNG.uniform(30, 180) * 1e-9 # 30-180 nm
height = RNG.uniform(8, 60) * 1e-9 # 8-60 nm
particles.append((cx, cy, radius, height))
# --- Render height map ---
image = RNG.normal(0, BG_NOISE_RMS, (N, N))
yy, xx = np.mgrid[0:N, 0:N]
for cx, cy, radius_m, height_m in particles:
radius_px = radius_m / PIXEL_SIZE
dist2 = (xx - cx) ** 2 + (yy - cy) ** 2
inside = dist2 < radius_px ** 2
# Hemisphere profile: z = h * sqrt(1 - (r/R)^2)
z = np.zeros_like(image)
z[inside] = height_m * np.sqrt(1.0 - dist2[inside] / radius_px ** 2)
image = np.maximum(image, z) # particles don't subtract from each other
# --- Save .npy (float64 metres) ---
npy_path = DEMO_DIR / "nanoparticles.npy"
np.save(str(npy_path), image)
print(f"Saved {npy_path} shape={image.shape} range=[{image.min():.2e}, {image.max():.2e}] m")
# --- Save .png (8-bit grayscale for quick visual) ---
from PIL import Image
normed = (image - image.min()) / (image.max() - image.min())
uint8 = (normed * 255).astype(np.uint8)
png_path = DEMO_DIR / "nanoparticles.png"
Image.fromarray(uint8, mode="L").save(str(png_path))
print(f"Saved {png_path}")
print(f"\n{len(particles)} particles generated on a {SCAN_SIZE*1e6:.0f} µm × {SCAN_SIZE*1e6:.0f} µm scan")