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_data

  • juspice.shape_based: ShapeBasedAccessor (via spice.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()
../../_images/notebooks_physics_based_image_generation_example_physics_based_26_0.png
[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