Building Synthetic Microscopy Images from Simple Shapes¶
Labelling real images by hand is slow. If we draw the objects ourselves, we know exactly where they are, so every synthetic image comes with a perfect label mask.
In this notebook we draw particles with a bell-shaped (Gaussian) brightness profile and thin, wavy cracks onto a real SEM background. These are simple shape models, not a physical simulation of the sample.
JuSPICE Modules and Classes¶
juspice.io:SPICEData,load_data,save_datajuspice.shape_based:ShapeBasedAccessor(viaspice.shape_based)juspice.tracking:Tracker
[1]:
from juspice.tracking import Tracker
tracker = Tracker(include_metadata=True, notes='Physics-based image generation')
tracker.recording_start()
/Users/amir/GIT_repositories/juspice_pre_release/venvs/.venv/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
from .autonotebook import tqdm as notebook_tqdm
[2]:
# --------- Block 0: Setup ----------
# Standard library
from pathlib import Path
import sys
import os
import json
import logging
import shutil
# Third-party
import numpy as np
import matplotlib.pyplot as plt
try:
import cv2
_cv2_available = True
except ImportError:
cv2 = None
_cv2_available = False
# JuSPICE
from juspice.io import SPICEData, load_data, save_data
# Paths
try:
notebook_dir = Path(__file__).resolve().parent
except Exception:
notebook_dir = Path.cwd()
cur = notebook_dir
repo_root = None
for _ in range(6):
if (cur / 'juspice').exists() or (cur / 'pyproject.toml').exists():
repo_root = cur
break
if cur.parent == cur:
break
cur = cur.parent
if repo_root is None:
repo_root = notebook_dir
if str(repo_root) not in sys.path:
sys.path.insert(0, str(repo_root))
[3]:
tracker.recording_stop()
Load the background¶
A real image gives realistic texture and noise behind the drawn objects.
[4]:
tracker.recording_start()
[5]:
# --------- Block 1: Load background ----------
bg_image_path = os.path.join(repo_root, 'Sample_data', 'em', '1b935635dd.png')
# `bg_spice` is just a variable name for the SPICEData instance
# load_data() returns here — any name would work; we use `bg_spice`
# (background + spice, an intuitive nod to JuSPICE / SPICEData) since
# this loaded image becomes the drawing session's background canvas.
bg_spice = load_data(bg_image_path)
bg_image = bg_spice.data
print(f'Background image shape: {bg_image.shape}')
print(f'Background image dtype: {bg_image.dtype}')
Background image shape: (512, 697, 3)
Background image dtype: uint8
[6]:
tracker.recording_stop()
Start a drawing session¶
create() turns the background into a grayscale canvas with an empty mask of the same size.
[7]:
tracker.recording_start()
[8]:
# --------- Block 2: Initialize drawing session ----------
if bg_image.ndim == 3:
height, width = bg_image.shape[:2]
else:
height, width = bg_image.shape
# spice.shape_based.create() starts a drawing session, setting bg_spice.data
# to the background canvas (mutated in place, mirrors spice.preprocess).
bg_spice.shape_based.create(
width=width,
height=height,
background_img_path=bg_image_path,
noise_type=None,
n_frames=1,
)
[8]:
SPICEData(data=array([[71, 72, 74, ..., 68, 71, 72],
[73, 73, 73, ..., 69, 70, 71],
[74, 73, 71, ..., 70, 70, 70],
...,
[86, 84, 81, ..., 74, 78, 81],
[81, 79, 77, ..., 65, 68, 70],
[73, 72, 71, ..., 56, 58, 60]], shape=(512, 697), dtype=uint8), metadata={'source_path': '/Users/amir/GIT_repositories/juspice_pre_release/Sample_data/em/1b935635dd.png', 'shape': (512, 697, 3), 'dtype': 'uint8', 'mode': 'RGB', 'size': (697, 512), 'mask': array([[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0],
...,
[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0]], shape=(512, 697), dtype=uint8)}, data_type='image_png', source_path='/Users/amir/GIT_repositories/juspice_pre_release/Sample_data/em/1b935635dd.png', extra={'_shape_based_drawer': <juspice.shape_based.draw object at 0x1262d9520>}, history=DatasetHistory(source_path='/Users/amir/GIT_repositories/juspice_pre_release/Sample_data/em/1b935635dd.png', data_type='image_png', import_timestamp='2026-08-20T15:47:57', track_history=True, metadata={'source_path': '/Users/amir/GIT_repositories/juspice_pre_release/Sample_data/em/1b935635dd.png', 'shape': (512, 697, 3), 'dtype': 'uint8', 'mode': 'RGB', 'size': (697, 512)}, operations=[{'function_name': 'load_data', 'module_name': 'juspice.io', 'timestamp': '2026-08-20T15:47:57', 'args': ['/Users/amir/GIT_repositories/juspice_pre_release/Sample_data/em/1b935635dd.png'], 'kwargs': {}, 'returned': {'type': 'image_png'}, 'callable_ref': None}, {'function_name': 'apply_shape_based', 'module_name': 'juspice.shape_based', 'timestamp': '2026-08-20T15:47:57', 'args': [], 'kwargs': {'method': '__init__', 'width': 697, 'height': 512, 'n_frames': 1, 'noise_type': None, 'background_img_path': '/Users/amir/GIT_repositories/juspice_pre_release/Sample_data/em/1b935635dd.png', 'original_img_path': None, 'original_mask_path': None, 'base_scale': 1.25, 'grow_rate_per_frame': 0.2, 'init_pos_randomness': 10.0, 'movement_randomness': 0.5, 'center_boost': 1.3, 'l1_freq': 2, 'l1_amp': 0.05, 'l2_freq': 3, 'l2_amp': 0.03}, 'returned': {'type': 'SPICEData'}, 'callable_ref': None}, {'function_name': 'shape_based.create', 'module_name': 'juspice.shape_based', 'timestamp': '2026-08-20T15:47:57', 'args': [], 'kwargs': {'width': 697, 'height': 512, 'n_frames': 1, 'noise_type': None, 'background_img_path': '/Users/amir/GIT_repositories/juspice_pre_release/Sample_data/em/1b935635dd.png', 'original_img_path': None, 'original_mask_path': None, 'base_scale': 1.25, 'grow_rate_per_frame': 0.2, 'init_pos_randomness': 10.0, 'movement_randomness': 0.5, 'center_boost': 1.3, 'l1_freq': 2, 'l1_amp': 0.05, 'l2_freq': 3, 'l2_amp': 0.03}, 'returned': {'type': 'SPICEData'}, 'callable_ref': None}], version_info={'python': '3.12.8', 'juspice': '0.1.0', 'git_hash': '0a860d3'}, definitions={}, callable_instances={}, _instance_counter=0, import_function_name='load_data'), dataset_type='single_image')
[9]:
tracker.recording_stop()
Light smoothing reduces background noise so the drawn objects blend in.
[10]:
tracker.recording_start()
[11]:
# --------- Block 3: Gaussian smoothing ----------
kernel_size = (5, 5)
sigma = 1.0
bg_spice.shape_based.apply_gaussian_smoothing(kernel_size=kernel_size, sigma=sigma)
[11]:
SPICEData(data=array([[73, 73, 74, ..., 68, 70, 71],
[73, 73, 73, ..., 69, 70, 70],
[73, 72, 72, ..., 69, 70, 70],
...,
[81, 80, 78, ..., 72, 74, 75],
[79, 78, 77, ..., 67, 68, 69],
[77, 77, 75, ..., 64, 65, 66]], shape=(512, 697), dtype=uint8), metadata={'source_path': '/Users/amir/GIT_repositories/juspice_pre_release/Sample_data/em/1b935635dd.png', 'shape': (512, 697, 3), 'dtype': 'uint8', 'mode': 'RGB', 'size': (697, 512), 'mask': array([[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0],
...,
[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0]], shape=(512, 697), dtype=uint8)}, data_type='image_png', source_path='/Users/amir/GIT_repositories/juspice_pre_release/Sample_data/em/1b935635dd.png', extra={'_shape_based_drawer': <juspice.shape_based.draw object at 0x1262d9520>}, history=DatasetHistory(source_path='/Users/amir/GIT_repositories/juspice_pre_release/Sample_data/em/1b935635dd.png', data_type='image_png', import_timestamp='2026-08-20T15:47:57', track_history=True, metadata={'source_path': '/Users/amir/GIT_repositories/juspice_pre_release/Sample_data/em/1b935635dd.png', 'shape': (512, 697, 3), 'dtype': 'uint8', 'mode': 'RGB', 'size': (697, 512)}, operations=[{'function_name': 'load_data', 'module_name': 'juspice.io', 'timestamp': '2026-08-20T15:47:57', 'args': ['/Users/amir/GIT_repositories/juspice_pre_release/Sample_data/em/1b935635dd.png'], 'kwargs': {}, 'returned': {'type': 'image_png'}, 'callable_ref': None}, {'function_name': 'apply_shape_based', 'module_name': 'juspice.shape_based', 'timestamp': '2026-08-20T15:47:57', 'args': [], 'kwargs': {'method': '__init__', 'width': 697, 'height': 512, 'n_frames': 1, 'noise_type': None, 'background_img_path': '/Users/amir/GIT_repositories/juspice_pre_release/Sample_data/em/1b935635dd.png', 'original_img_path': None, 'original_mask_path': None, 'base_scale': 1.25, 'grow_rate_per_frame': 0.2, 'init_pos_randomness': 10.0, 'movement_randomness': 0.5, 'center_boost': 1.3, 'l1_freq': 2, 'l1_amp': 0.05, 'l2_freq': 3, 'l2_amp': 0.03}, 'returned': {'type': 'SPICEData'}, 'callable_ref': None}, {'function_name': 'shape_based.create', 'module_name': 'juspice.shape_based', 'timestamp': '2026-08-20T15:47:57', 'args': [], 'kwargs': {'width': 697, 'height': 512, 'n_frames': 1, 'noise_type': None, 'background_img_path': '/Users/amir/GIT_repositories/juspice_pre_release/Sample_data/em/1b935635dd.png', 'original_img_path': None, 'original_mask_path': None, 'base_scale': 1.25, 'grow_rate_per_frame': 0.2, 'init_pos_randomness': 10.0, 'movement_randomness': 0.5, 'center_boost': 1.3, 'l1_freq': 2, 'l1_amp': 0.05, 'l2_freq': 3, 'l2_amp': 0.03}, 'returned': {'type': 'SPICEData'}, 'callable_ref': None}, {'function_name': 'apply_shape_based', 'module_name': 'juspice.shape_based', 'timestamp': '2026-08-20T15:47:57', 'args': [], 'kwargs': {'method': 'apply_gaussian_smoothing', 'kernel_size': [5, 5], 'sigma': 1.0}, 'returned': {'type': 'SPICEData'}, 'callable_ref': None}, {'function_name': 'shape_based.apply_gaussian_smoothing', 'module_name': 'juspice.shape_based', 'timestamp': '2026-08-20T15:47:57', 'args': [], 'kwargs': {'kernel_size': [5, 5], 'sigma': 1.0}, 'returned': {'type': 'SPICEData'}, 'callable_ref': None}], version_info={'python': '3.12.8', 'juspice': '0.1.0', 'git_hash': '0a860d3'}, definitions={}, callable_instances={}, _instance_counter=0, import_function_name='load_data'), dataset_type='single_image')
[12]:
tracker.recording_stop()
Add particles¶
We draw 15 dark particles with radii of 10–40 pixels. noisy_boundary roughens their edges; blend_factor and accumulation_factor control how overlapping particles combine.
[13]:
tracker.recording_start()
[14]:
# --------- Block 4: Generate particles ----------
num_objects = 15
radii = (10, 40)
lighting_mode = 'dark'
bg_spice.shape_based.generate_gaussian_circles(
lighting_mode=lighting_mode,
num_objects=num_objects,
radii=radii,
p=2,
noisy_boundary=True,
min_max=(50, 150),
accumulation_factor=0.5,
blend_factor=0.3,
sigma=10,
)
[14]:
SPICEData(data=array([[73, 73, 74, ..., 68, 70, 71],
[73, 73, 73, ..., 69, 70, 70],
[73, 72, 72, ..., 69, 70, 70],
...,
[81, 80, 78, ..., 72, 74, 75],
[79, 78, 77, ..., 67, 68, 69],
[77, 77, 75, ..., 64, 65, 66]], shape=(512, 697), dtype=uint8), metadata={'source_path': '/Users/amir/GIT_repositories/juspice_pre_release/Sample_data/em/1b935635dd.png', 'shape': (512, 697, 3), 'dtype': 'uint8', 'mode': 'RGB', 'size': (697, 512), 'mask': array([[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0],
...,
[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0]], shape=(512, 697), dtype=uint8)}, data_type='image_png', source_path='/Users/amir/GIT_repositories/juspice_pre_release/Sample_data/em/1b935635dd.png', extra={'_shape_based_drawer': <juspice.shape_based.draw object at 0x1262d9520>}, history=DatasetHistory(source_path='/Users/amir/GIT_repositories/juspice_pre_release/Sample_data/em/1b935635dd.png', data_type='image_png', import_timestamp='2026-08-20T15:47:57', track_history=True, metadata={'source_path': '/Users/amir/GIT_repositories/juspice_pre_release/Sample_data/em/1b935635dd.png', 'shape': (512, 697, 3), 'dtype': 'uint8', 'mode': 'RGB', 'size': (697, 512)}, operations=[{'function_name': 'load_data', 'module_name': 'juspice.io', 'timestamp': '2026-08-20T15:47:57', 'args': ['/Users/amir/GIT_repositories/juspice_pre_release/Sample_data/em/1b935635dd.png'], 'kwargs': {}, 'returned': {'type': 'image_png'}, 'callable_ref': None}, {'function_name': 'apply_shape_based', 'module_name': 'juspice.shape_based', 'timestamp': '2026-08-20T15:47:57', 'args': [], 'kwargs': {'method': '__init__', 'width': 697, 'height': 512, 'n_frames': 1, 'noise_type': None, 'background_img_path': '/Users/amir/GIT_repositories/juspice_pre_release/Sample_data/em/1b935635dd.png', 'original_img_path': None, 'original_mask_path': None, 'base_scale': 1.25, 'grow_rate_per_frame': 0.2, 'init_pos_randomness': 10.0, 'movement_randomness': 0.5, 'center_boost': 1.3, 'l1_freq': 2, 'l1_amp': 0.05, 'l2_freq': 3, 'l2_amp': 0.03}, 'returned': {'type': 'SPICEData'}, 'callable_ref': None}, {'function_name': 'shape_based.create', 'module_name': 'juspice.shape_based', 'timestamp': '2026-08-20T15:47:57', 'args': [], 'kwargs': {'width': 697, 'height': 512, 'n_frames': 1, 'noise_type': None, 'background_img_path': '/Users/amir/GIT_repositories/juspice_pre_release/Sample_data/em/1b935635dd.png', 'original_img_path': None, 'original_mask_path': None, 'base_scale': 1.25, 'grow_rate_per_frame': 0.2, 'init_pos_randomness': 10.0, 'movement_randomness': 0.5, 'center_boost': 1.3, 'l1_freq': 2, 'l1_amp': 0.05, 'l2_freq': 3, 'l2_amp': 0.03}, 'returned': {'type': 'SPICEData'}, 'callable_ref': None}, {'function_name': 'apply_shape_based', 'module_name': 'juspice.shape_based', 'timestamp': '2026-08-20T15:47:57', 'args': [], 'kwargs': {'method': 'apply_gaussian_smoothing', 'kernel_size': [5, 5], 'sigma': 1.0}, 'returned': {'type': 'SPICEData'}, 'callable_ref': None}, {'function_name': 'shape_based.apply_gaussian_smoothing', 'module_name': 'juspice.shape_based', 'timestamp': '2026-08-20T15:47:57', 'args': [], 'kwargs': {'kernel_size': [5, 5], 'sigma': 1.0}, 'returned': {'type': 'SPICEData'}, 'callable_ref': None}, {'function_name': 'apply_shape_based', 'module_name': 'juspice.shape_based', 'timestamp': '2026-08-20T15:47:57', 'args': [], 'kwargs': {'method': 'generate_gaussian_circles', 'lighting_mode': 'dark', 'num_objects': 15, 'radii': [10, 40], 'p': 2, 'noisy_boundary': True, 'min_max': [50, 150], 'accumulation_factor': 0.5, 'blend_factor': 0.3, 'sigma': 10}, 'returned': {'type': 'SPICEData'}, 'callable_ref': None}, {'function_name': 'shape_based.generate_gaussian_circles', 'module_name': 'juspice.shape_based', 'timestamp': '2026-08-20T15:47:57', 'args': [], 'kwargs': {'lighting_mode': 'dark', 'num_objects': 15, 'radii': [10, 40], 'p': 2, 'noisy_boundary': True, 'min_max': [50, 150], 'accumulation_factor': 0.5, 'blend_factor': 0.3, 'sigma': 10}, 'returned': {'type': 'SPICEData'}, 'callable_ref': None}], version_info={'python': '3.12.8', 'juspice': '0.1.0', 'git_hash': '0a860d3'}, definitions={}, callable_instances={}, _instance_counter=0, import_function_name='load_data'), dataset_type='single_image')
[15]:
tracker.recording_stop()
Add cracks¶
Cracks are thin, light lines along random cubic curves (poly_order=3).
[16]:
tracker.recording_start()
[17]:
# --------- Block 5: Generate cracks ----------
num_cracks = 5
crack_length = 150
bg_spice.shape_based.generate_wiggly_cracks(
crack_type='polynomial',
num_cracks=num_cracks,
length=crack_length,
thickness=1,
lighting_mode='light',
poly_order=3,
)
[17]:
SPICEData(data=array([[73, 73, 74, ..., 68, 70, 71],
[73, 73, 73, ..., 69, 70, 70],
[73, 72, 72, ..., 69, 70, 70],
...,
[81, 80, 78, ..., 72, 74, 75],
[79, 78, 77, ..., 67, 68, 69],
[77, 77, 75, ..., 64, 65, 66]], shape=(512, 697), dtype=uint8), metadata={'source_path': '/Users/amir/GIT_repositories/juspice_pre_release/Sample_data/em/1b935635dd.png', 'shape': (512, 697, 3), 'dtype': 'uint8', 'mode': 'RGB', 'size': (697, 512), 'mask': array([[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0],
...,
[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0]], shape=(512, 697), dtype=uint8)}, data_type='image_png', source_path='/Users/amir/GIT_repositories/juspice_pre_release/Sample_data/em/1b935635dd.png', extra={'_shape_based_drawer': <juspice.shape_based.draw object at 0x1262d9520>}, history=DatasetHistory(source_path='/Users/amir/GIT_repositories/juspice_pre_release/Sample_data/em/1b935635dd.png', data_type='image_png', import_timestamp='2026-08-20T15:47:57', track_history=True, metadata={'source_path': '/Users/amir/GIT_repositories/juspice_pre_release/Sample_data/em/1b935635dd.png', 'shape': (512, 697, 3), 'dtype': 'uint8', 'mode': 'RGB', 'size': (697, 512)}, operations=[{'function_name': 'load_data', 'module_name': 'juspice.io', 'timestamp': '2026-08-20T15:47:57', 'args': ['/Users/amir/GIT_repositories/juspice_pre_release/Sample_data/em/1b935635dd.png'], 'kwargs': {}, 'returned': {'type': 'image_png'}, 'callable_ref': None}, {'function_name': 'apply_shape_based', 'module_name': 'juspice.shape_based', 'timestamp': '2026-08-20T15:47:57', 'args': [], 'kwargs': {'method': '__init__', 'width': 697, 'height': 512, 'n_frames': 1, 'noise_type': None, 'background_img_path': '/Users/amir/GIT_repositories/juspice_pre_release/Sample_data/em/1b935635dd.png', 'original_img_path': None, 'original_mask_path': None, 'base_scale': 1.25, 'grow_rate_per_frame': 0.2, 'init_pos_randomness': 10.0, 'movement_randomness': 0.5, 'center_boost': 1.3, 'l1_freq': 2, 'l1_amp': 0.05, 'l2_freq': 3, 'l2_amp': 0.03}, 'returned': {'type': 'SPICEData'}, 'callable_ref': None}, {'function_name': 'shape_based.create', 'module_name': 'juspice.shape_based', 'timestamp': '2026-08-20T15:47:57', 'args': [], 'kwargs': {'width': 697, 'height': 512, 'n_frames': 1, 'noise_type': None, 'background_img_path': '/Users/amir/GIT_repositories/juspice_pre_release/Sample_data/em/1b935635dd.png', 'original_img_path': None, 'original_mask_path': None, 'base_scale': 1.25, 'grow_rate_per_frame': 0.2, 'init_pos_randomness': 10.0, 'movement_randomness': 0.5, 'center_boost': 1.3, 'l1_freq': 2, 'l1_amp': 0.05, 'l2_freq': 3, 'l2_amp': 0.03}, 'returned': {'type': 'SPICEData'}, 'callable_ref': None}, {'function_name': 'apply_shape_based', 'module_name': 'juspice.shape_based', 'timestamp': '2026-08-20T15:47:57', 'args': [], 'kwargs': {'method': 'apply_gaussian_smoothing', 'kernel_size': [5, 5], 'sigma': 1.0}, 'returned': {'type': 'SPICEData'}, 'callable_ref': None}, {'function_name': 'shape_based.apply_gaussian_smoothing', 'module_name': 'juspice.shape_based', 'timestamp': '2026-08-20T15:47:57', 'args': [], 'kwargs': {'kernel_size': [5, 5], 'sigma': 1.0}, 'returned': {'type': 'SPICEData'}, 'callable_ref': None}, {'function_name': 'apply_shape_based', 'module_name': 'juspice.shape_based', 'timestamp': '2026-08-20T15:47:57', 'args': [], 'kwargs': {'method': 'generate_gaussian_circles', 'lighting_mode': 'dark', 'num_objects': 15, 'radii': [10, 40], 'p': 2, 'noisy_boundary': True, 'min_max': [50, 150], 'accumulation_factor': 0.5, 'blend_factor': 0.3, 'sigma': 10}, 'returned': {'type': 'SPICEData'}, 'callable_ref': None}, {'function_name': 'shape_based.generate_gaussian_circles', 'module_name': 'juspice.shape_based', 'timestamp': '2026-08-20T15:47:57', 'args': [], 'kwargs': {'lighting_mode': 'dark', 'num_objects': 15, 'radii': [10, 40], 'p': 2, 'noisy_boundary': True, 'min_max': [50, 150], 'accumulation_factor': 0.5, 'blend_factor': 0.3, 'sigma': 10}, 'returned': {'type': 'SPICEData'}, 'callable_ref': None}, {'function_name': 'apply_shape_based', 'module_name': 'juspice.shape_based', 'timestamp': '2026-08-20T15:47:58', 'args': [], 'kwargs': {'method': 'generate_wiggly_cracks', 'crack_type': 'polynomial', 'num_cracks': 5, 'length': 150, 'thickness': 1, 'lighting_mode': 'light', 'poly_order': 3}, 'returned': {'type': 'SPICEData'}, 'callable_ref': None}, {'function_name': 'shape_based.generate_wiggly_cracks', 'module_name': 'juspice.shape_based', 'timestamp': '2026-08-20T15:47:58', 'args': [], 'kwargs': {'crack_type': 'polynomial', 'num_cracks': 5, 'length': 150, 'thickness': 1, 'lighting_mode': 'light', 'poly_order': 3}, 'returned': {'type': 'SPICEData'}, 'callable_ref': None}], version_info={'python': '3.12.8', 'juspice': '0.1.0', 'git_hash': '0a860d3'}, definitions={}, callable_instances={}, _instance_counter=0, import_function_name='load_data'), dataset_type='single_image')
[18]:
tracker.recording_stop()
Compare image and mask¶
The synthetic image (middle) and its mask (right) together form one labelled training example.
[19]:
tracker.recording_start()
[20]:
# --------- Block 6: Visualize ----------
fig, axes = plt.subplots(1, 3, figsize=(18, 6))
if bg_image.ndim == 3:
axes[0].imshow(bg_image)
else:
axes[0].imshow(bg_image, cmap='gray')
axes[0].set_title('Background Image')
axes[0].axis('off')
axes[1].imshow(bg_spice.data, cmap='gray')
axes[1].set_title('Synthetic Image')
axes[1].axis('off')
axes[2].imshow(bg_spice.metadata['mask'], cmap='gray')
axes[2].set_title('Object Mask')
axes[2].axis('off')
plt.tight_layout()
plt.show()
[21]:
tracker.recording_stop()
Save and check¶
We save the image and check that the history contains every drawing step.
[22]:
tracker.recording_start()
[23]:
# --------- Block 7: Save ----------
# bg_spice is already the fully-formed synthetic-image SPICEData — every
# spice.shape_based.<method>() call above mutated it in place, so there is
# no need to construct a separate SPICEData before saving.
bg_spice.metadata.update({
'num_objects': num_objects,
'num_cracks': num_cracks,
'lighting_mode': lighting_mode,
})
# Writes <stem>.npy, <stem>.json, and <stem>_history.py next to the notebook
save_data(bg_spice)
stem = 'example_physics_based'
print(f'Data: {os.path.join(notebook_dir, stem + ".npy")}')
print(f'JSON sidecar: {os.path.join(notebook_dir, stem + ".json")}')
print(f'History script: {os.path.join(notebook_dir, stem + "_history.py")}')
Data: /Users/amir/GIT_repositories/juspice_pre_release/notebooks/physics_based_image_generation/example_physics_based.npy
JSON sidecar: /Users/amir/GIT_repositories/juspice_pre_release/notebooks/physics_based_image_generation/example_physics_based.json
History script: /Users/amir/GIT_repositories/juspice_pre_release/notebooks/physics_based_image_generation/example_physics_based_history.py
[24]:
# Validate
metadata_path = os.path.join(notebook_dir, 'example_physics_based.json')
with open(metadata_path, encoding='utf-8') as _fh:
metadata = json.loads(_fh.read())
# New JSON format: history is a list where each entry is one pipeline step.
history_lines = metadata.get('history', [])
assert isinstance(history_lines, list), 'history must be a list'
assert history_lines, 'history list is empty'
assert any('load_data(' in line for line in history_lines)
assert any('shape_based.create(' in line for line in history_lines)
assert any('shape_based.generate_gaussian_circles(' in line for line in history_lines)
assert any('shape_based.generate_wiggly_cracks(' in line for line in history_lines)
assert any('save_data(' in line for line in history_lines)
# Result object fields
assert metadata.get('dataset_type') == 'single_image'
assert 'data_shape' in metadata
assert 'data_dtype' in metadata
print('Validation passed.')
print('History pipeline:')
for step in history_lines:
print(' ', step)
Validation passed.
History pipeline:
import juspice
spice = juspice.io.load_data('/Users/amir/GIT_repositories/juspice_pre_release/Sample_data/em/1b935635dd.png')
spice = apply_shape_based(spice, method='__init__', width=697, height=512, n_frames=1, noise_type=None, background_img_path='/Users/amir/GIT_repositories/juspice_pre_release/Sample_data/em/1b935635dd.png', original_img_path=None, original_mask_path=None, base_scale=1.25, grow_rate_per_frame=0.2, init_pos_randomness=10.0, movement_randomness=0.5, center_boost=1.3, l1_freq=2, l1_amp=0.05, l2_freq=3, l2_amp=0.03)
spice.shape_based.create(width=697, height=512, n_frames=1, noise_type=None, background_img_path='/Users/amir/GIT_repositories/juspice_pre_release/Sample_data/em/1b935635dd.png', original_img_path=None, original_mask_path=None, base_scale=1.25, grow_rate_per_frame=0.2, init_pos_randomness=10.0, movement_randomness=0.5, center_boost=1.3, l1_freq=2, l1_amp=0.05, l2_freq=3, l2_amp=0.03)
spice = apply_shape_based(spice, method='apply_gaussian_smoothing', kernel_size=[5, 5], sigma=1.0)
spice.shape_based.apply_gaussian_smoothing(kernel_size=[5, 5], sigma=1.0)
spice = apply_shape_based(spice, method='generate_gaussian_circles', lighting_mode='dark', num_objects=15, radii=[10, 40], p=2, noisy_boundary=True, min_max=[50, 150], accumulation_factor=0.5, blend_factor=0.3, sigma=10)
spice.shape_based.generate_gaussian_circles(lighting_mode='dark', num_objects=15, radii=[10, 40], p=2, noisy_boundary=True, min_max=[50, 150], accumulation_factor=0.5, blend_factor=0.3, sigma=10)
spice = apply_shape_based(spice, method='generate_wiggly_cracks', crack_type='polynomial', num_cracks=5, length=150, thickness=1, lighting_mode='light', poly_order=3)
spice.shape_based.generate_wiggly_cracks(crack_type='polynomial', num_cracks=5, length=150, thickness=1, lighting_mode='light', poly_order=3)
juspice.io.save_data(spice)
[25]:
tracker.recording_stop()
Generate a short image sequence¶
generate_sequence() creates frames in which the particles move and grow, each with its own mask, for example to test tracking methods. Unlike the steps above, it returns a new SPICEData (dataset_type='multiple_frames') and leaves bg_spice.data unchanged.
[26]:
tracker.recording_start()
[27]:
# --------- Block 8: Generate frame sequence ----------
# Re-initialise a drawing session with n_frames > 1 for sequence generation.
# Re-using bg_spice keeps the same history lineage; all prior operations
# are preserved (they are recorded on bg_spice.history, not overwritten).
bg_spice.shape_based.create(
width=width,
height=height,
background_img_path=bg_image_path,
noise_type=None,
n_frames=3,
)
# generate_sequence() derives a new multiple_frames SPICEData sharing
# bg_spice's history — bg_spice.data itself is NOT mutated.
synth_before = bg_spice.data.copy()
frame_seq_spice = bg_spice.shape_based.generate_sequence(num_objects=10)
print(f'dataset_type: {frame_seq_spice.dataset_type!r}')
print(f'n_frames: {frame_seq_spice.n_frames}')
print(f'data.shape: {frame_seq_spice.data.shape}')
print(f'bg_spice.data unchanged: {np.array_equal(bg_spice.data, synth_before)}')
**** Frame 1/3 generated and added to the sequence. ****
**** Frame 2/3 generated and added to the sequence. ****
**** Frame 3/3 generated and added to the sequence. ****
dataset_type: 'multiple_frames'
n_frames: 3
data.shape: (3, 512, 697)
bg_spice.data unchanged: True
[28]:
# --------- Block 9: Inspect human-readable history ----------
# bg_spice.history.to_lines() reconstructs the full shape-based synthesis
# pipeline applied to this SPICEData object into readable, runnable code:
# the initial load_data(...) call followed by each spice.shape_based.<method>()
# call in execution order, using the actual runtime argument values, and a
# trailing save_data(bg_spice).
readable_lines = bg_spice.history.to_lines()
print('Reconstructed shape-based synthesis pipeline for this SPICEData object:')
print('\n'.join(readable_lines[:6]), '...')
Reconstructed shape-based synthesis pipeline for this SPICEData object:
import juspice
spice = juspice.io.load_data('/Users/amir/GIT_repositories/juspice_pre_release/Sample_data/em/1b935635dd.png')
spice = apply_shape_based(spice, method='__init__', width=697, height=512, n_frames=1, noise_type=None, background_img_path='/Users/amir/GIT_repositories/juspice_pre_release/Sample_data/em/1b935635dd.png', original_img_path=None, original_mask_path=None, base_scale=1.25, grow_rate_per_frame=0.2, init_pos_randomness=10.0, movement_randomness=0.5, center_boost=1.3, l1_freq=2, l1_amp=0.05, l2_freq=3, l2_amp=0.03)
spice.shape_based.create(width=697, height=512, n_frames=1, noise_type=None, background_img_path='/Users/amir/GIT_repositories/juspice_pre_release/Sample_data/em/1b935635dd.png', original_img_path=None, original_mask_path=None, base_scale=1.25, grow_rate_per_frame=0.2, init_pos_randomness=10.0, movement_randomness=0.5, center_boost=1.3, l1_freq=2, l1_amp=0.05, l2_freq=3, l2_amp=0.03)
spice = apply_shape_based(spice, method='apply_gaussian_smoothing', kernel_size=[5, 5], sigma=1.0)
spice.shape_based.apply_gaussian_smoothing(kernel_size=[5, 5], sigma=1.0) ...
[29]:
tracker.recording_stop()
[30]:
save_data(bg_spice)
stem = 'example_physics_based'
print(f'Data: {os.path.join(notebook_dir, stem + ".npy")}')
print(f'JSON sidecar: {os.path.join(notebook_dir, stem + ".json")}')
print(f'History script: {os.path.join(notebook_dir, stem + "_history.py")}')
Data: /Users/amir/GIT_repositories/juspice_pre_release/notebooks/physics_based_image_generation/example_physics_based.npy
JSON sidecar: /Users/amir/GIT_repositories/juspice_pre_release/notebooks/physics_based_image_generation/example_physics_based.json
History script: /Users/amir/GIT_repositories/juspice_pre_release/notebooks/physics_based_image_generation/example_physics_based_history.py