Multi-view light sheet microscopy image processing pipeline
Project description
IsoView Light Sheet Microscopy Pipeline
Input is a flat directory of raw SPC##_TM#####_ANG###_CM#_CHN##_PH#.stack files
(little-endian uint16) with a push_config XML per channel describing the
acquisition (dimensions, pixel/axial spacing, wavelength, exposure, stage, etc.).
Quickstart
- Point
input_dirinpipeline/correct_stack.pyat your raw data directory. - Run
pipeline/correct_stack.py(correction) thenpipeline/multi_fuse.py(fusion). - Each step appends its config +
isoview_versiontoisoview_config.jsonnext to the data, so every run is reproducible.
Usage
Two entrypoints, both take one ProcessingConfig: run correct_stack(config)
then multi_fuse(config). Mode (timelapse vs tiled) is auto-detected from the
raw filenames — you do not set it. Editable templates live in
pipeline/correct_stack.py and pipeline/multi_fuse.py.
Timelapse (multiple TM, one or more specimens)
from pathlib import Path
from isoview import ProcessingConfig, correct_stack, multi_fuse
config = ProcessingConfig(
input_dir=Path(r"E:\isoview\dataset"), # flat SPC##_TM#####_*.stack files
timepoints=None, # default=None · auto-detect all TM## (or pass [0, 1, 2])
workers=4, # default=1 · process timepoints in parallel
blending_method="geometric", # default="geometric"
)
correct_stack(config) # -> dataset.corrected/SPM00/TM000000/...
multi_fuse(config) # -> dataset.fused/SPM00/TM000000/...
Tiled (single TM, multiple SPC)
Same calls — auto-detected as tiled. Per-specimen crop overrides go through
tile_crops, keyed by SPM##, then crop param, then camera index:
config = ProcessingConfig(
input_dir=Path(r"E:\isoview\tiled_dataset"),
tile_crops={
"SPM00": {"crop_front": {0: 40}, "crop_depth": {0: 450}},
"SPM01": {"crop_depth": {1: 382, 3: 400}},
},
)
correct_stack(config) # -> tiled_dataset.corrected/SPM00/, SPM01/, ...
multi_fuse(config) # -> tiled_dataset.fused/SPM00/, ...
BigStitcher export (optional stage 3)
Export the two fused views as a BigDataViewer/BigStitcher dataset, then register
VW00↔VW90 in the BigStitcher GUI (see bigstitcher.md):
from isoview import generate_bigstitcher_xml
generate_bigstitcher_xml(config) # -> dataset.stitcher/dataset.xml + dataset.ome.zarr/
All parameters
Every field with its default and tuning guidance. leave = don't change
unless you have a reason (hardware/structural/auto-derived); tune = worth
adjusting per dataset, with a suggested range. The Gaussian segmentation kernels
(gauss_kernel, gauss_sigma) should be left as-is.
config = ProcessingConfig(
# paths & selection
input_dir=Path(r"E:\isoview\dataset"), # required · folder of flat SPC##_TM#####_*.stack files
output_dir=None, # default=None · auto = <raw>.corrected[_suffix]; leave
projection_dir=None, # default=None · auto; leave
specimen=0, # default=0 · timelapse specimen index; leave
specimens=None, # default=None · auto-detect SPC##; leave
timepoints=None, # default=None · auto-detect TM##; set a list for a subset
cameras=None, # default=None · derived from camera_pairs; leave
views=[0, 90], # default=[0, 90] · hardware-defined; leave
output_suffix="", # default="" · names a variant (.corrected_<x>/.fused_<x>); optional
stitcher_suffix="", # default="" · BigStitcher dir variant; leave
# output format
output_format="zarr", # default="zarr" · zarr | tif | klb (pick one, not a tuning knob)
compression="zstd", # default="zstd" · zstd|lzw|deflate (tif), blosc-zstd (zarr), or None
compression_level=3, # default=3 · 1–22 zstd / 1–9 others · tune 1–9, diminishing past ~6
zarr_chunks=None, # default=None · auto (1, Y, X) one plane/chunk; leave unless tuning IO
zarr_shards=None, # default=None · auto; leave
pyramid=True, # default=True · leave
pyramid_max_layers=4, # default=4 · 0–~6; leave
# parallelism & logging
workers=1, # default=1 · 1–CPU cores · TUNE to machine; fusion is RAM-heavy, use fewer
log=True, # default=True · leave
overwrite=False, # default=False · True to recompute existing outputs
# dead-pixel correction
median_kernel=(3, 3), # default=(3, 3) · leave (None disables); larger blurs real signal
background_percentile=5.0, # default=5.0 · 0–~20 · leave (dark-current estimate)
mask_percentile=1.0, # default=1.0 · 0–~5 · leave
subsample_factor=100, # default=100 · 1–~1000 · speed only; higher = faster/coarser percentiles
# segmentation
segment_mode=1, # default=1 · 0=off, 1=generate+save masks (2/3 not implemented)
gauss_kernel=5, # default=5 · LEAVE AS-IS
gauss_sigma=2.0, # default=2.0 · LEAVE AS-IS
segment_threshold=0.4, # default=0.4 · 0–1 · MAIN TUNE: lower = more foreground (try 0.2–0.6)
splitting=10, # default=10 · 1+ · memory knob; raise if OOM (more slabs = less RAM)
apply_segmentation_mask=False, # default=False · True zeros background; masks are saved either way
# transforms (fusion; applied to the 2nd camera in each pair)
rotation=0, # default=0 · -1|0|1 = 90ccw|none|90cw · hardware-defined; leave
flip_horizontal=True, # default=True · hardware-defined; leave
flip_vertical=False, # default=False · leave
flip_z=False, # default=False · True if opposing cameras need a Z flip
# camera-camera blending
blending_method="geometric", # default="geometric" · geometric | adaptive | average (or auto)
blending_range=4, # default=4 · 1–~20 · tune 2–10 (Z-plane transition width)
transition_plane=None, # default=None=center · set a Z-index to move the crossover (geometric)
front_flag=1, # default=1 · 1|2 · which camera is sharp at low Z; flip if wrong
# registration (coarse grid search + gradient-descent refine)
search_offsets_x=(-50, 50, 10), # default=(-50, 50, 10) · (start, stop, step) px · widen/finer if misaligned
search_offsets_y=(-50, 50, 10), # default=(-50, 50, 10) · same
# microscope (read from XML when present)
pixel_spacing_z=None, # default=None · XML axial_step; set only if no XML
detection_objective_mag=None, # default=None · XML objective mag; set only if no XML
pixel_spacing_camera=6.5, # default=6.5 · sensor pixel size (µm); leave
camera_view_map=None, # default=None -> {0:0, 1:0, 2:90, 3:90}; leave
camera_pairs=None, # default=None -> [(0, 1), (2, 3)]; change only for different wiring
# cropping (fusion; keyed by CAMERA index 0=CM00, 1=CM01, ...)
crop_front=None, # default=None · {camera: z-start}
crop_depth=None, # default=None · {camera: z-count}
crop_top=None, # default=None · {camera: y-start}
crop_height=None, # default=None · {camera: y-count}
crop_left=None, # default=None · {camera: x-start}
crop_width=None, # default=None · {camera: x-count}
tile_crops=None, # default=None · per-specimen overrides {"SPM03": {"crop_depth": {1: 460}}}
# diagnostics
do_tenengrad=False, # default=False · True for per-Z focus (Tenengrad) QC plots
diagnostics_dir=None, # default=None · auto = output_dir/diagnostics; leave
)
Per-view fusion overrides — blending_method_by_view, blending_range_by_view,
transition_plane_by_view, front_flag_by_view, flip_z_by_view,
flip_horizontal_by_view, flip_vertical_by_view, rotation_by_view,
search_offsets_x_by_view, search_offsets_y_by_view — each take a
{view: value} dict (0=VW00, 90=VW90) and override the matching scalar above for
that view only.
Acquisition Modes
Raw input is always flat — SPC##_TM#####_ANG###_CM#_CHN##_PH#.stack files in a single directory.
Mode is auto-detected from SPC/TM counts:
| Condition | Mode | Description |
|---|---|---|
| Multiple TM values | timelapse | time series, any number of specimens |
| Single TM + multiple SPC | tiled | spatial tiles, one timepoint |
| Single TM + single SPC | single | treated as timelapse with 1 timepoint |
XML Metadata
read_xml_metadata(xml_path) parses one push_config XML; read_all_xml_metadata(input_dir, specimen) returns (common, per_camera) — fields equal across cameras vs camera-specific.
| Field | Type | Source | Notes |
|---|---|---|---|
data_header |
str | @data_header |
acquisition session label |
specimen_name |
str | @specimen_name |
|
timestamp |
str | @timestamp |
acquisition datetime |
time_point |
int | @time_point |
|
specimen_XYZT |
str | @specimen_XYZT |
also parsed → stage_x/y/z (float, µm) |
angle |
float | @angle |
|
camera_index |
str | @camera_index |
comma-separated per camera |
camera_type |
str | @camera_type |
|
camera_roi |
str | @camera_roi |
|
wavelength |
str | @wavelength |
emission, per camera |
illumination_arms |
str | @illumination_arms |
per camera |
illumination_filter |
str | @illumination_filter |
|
exposure_time |
float | @exposure_time |
ms |
detection_filter |
str | @detection_filter |
|
detection_objective |
str | @detection_objective |
also parsed → objective_mag (float) |
dimensions |
ndarray | @dimensions |
(n_cameras, 3) from "WxHxD,WxHxD,…" |
z_step |
float | @z_step |
µm; VW00 (Z-scan) |
y_step |
float | @y_step |
µm; VW90 (Y-scan) |
stack_direction |
str | @stack_direction |
drives camera_view_map |
planes |
str | @planes |
|
laser_power |
str | @laser_power |
|
experiment_notes |
str | @experiment_notes |
|
zplanes |
int | derived | dimensions[0][-1] |
fps |
float | derived | 1000.0 / exposure_time |
vps |
float | derived | fps / zplanes |
camera_pixel_size_um |
float | constant | 6.5 (Hamamatsu C11440-22C) |
pixel_resolution_um |
float | derived | camera_pixel_size_um / objective_mag |
axial_step |
float | merged | unifies z_step / y_step across XMLs |
camera_view_map |
dict | synthesized | from stack_direction: Z-scan → {0:0,1:0}, Y-scan → {2:90,3:90} |
XML discovery order (in read_all_xml_metadata): ch*_spec{NN}.xml → ch*.xml → *_CHN*.xml. Channel inferred from filename via ch(\d+) → CHN(\d+) → VW(\d+).
Filename Tags
| Tag | Meaning | Raw | Corrected | Fused |
|---|---|---|---|---|
SPC## / SPM## |
specimen / tile | SPC00 |
SPM00 |
SPM00 |
TM###### |
timepoint | TM00000 |
TM000000 |
TM000000 |
CM## |
camera | CM0 |
CM00 |
CM00_CM01 (pair) |
CHN## |
acquisition channel | CHN00, CHN01 |
CHN00, CHN01 |
CHN00, CHN01 |
VW## |
fused view (scan axis) | — | — | VW00 (z-scan), VW90 (y-scan) |
ANG### |
illumination angle | ANG000 |
— | — |
PH# |
phase | PH0 |
— | — |
Correction iterates over all (camera, channel) pairs present in the raw data,
so dual-channel acquisitions (same camera capturing multiple wavelengths) produce
one corrected volume per (CM, CHN). Fusion fuses each camera pair separately
for every channel both cameras share.
Pipeline & Output Layout
Three stages run in sequence, each writing a sibling directory next to the raw input. The data reduces at every step — 4 cameras → 2 fused views → 1 registered volume:
| Stage | Call | Output dir | Reduces |
|---|---|---|---|
| 1. Correction | correct_stack |
<raw>.corrected/ |
raw 4 cameras → 4 corrected volumes (CM00–CM03) |
| 2. Fusion | multi_fuse |
<raw>.fused/ |
4 cameras → 2 orthogonal views (VW00, VW90) |
| 3. Stitch export | generate_bigstitcher_xml |
<raw>.stitcher/ |
2 views → 1 registered volume (in BigStitcher) |
.corrected— per-camera correction: dead-pixel removal, background subtraction, and segmentation masks. Produces one corrected volume per camera, kept in raw orientation; rotation and flips are deferred to fusion..fused— fuses each opposing camera pair into a single view: CM00+CM01 → VW00 (Z-scan), CM02+CM03 → VW90 (Y-scan). Applies the rotation/flip and intensity correction, then blends the two cameras along Z. Four cameras become two orthogonal views..stitcher— exports the two views as a BigDataViewer/BigStitcher dataset (zarr +dataset.xml). BigStitcher registers VW00↔VW90 into one isotropic volume; that registration runs in the BigStitcher GUI, not here (seebigstitcher.md).
All three are siblings of the raw input directory. output_suffix is one shared
field appended to every stage dir (.corrected_<x>, .fused_<x>,
.stitcher_<x>); empty gives the bare .corrected / .fused / .stitcher.
Correction (correct_stack.py)
| Mode | Path | Filename |
|---|---|---|
| Timelapse | root.corrected/SPM00/TM000000/ |
SPM00_TM000000_CM00_CHN00.ome.tif |
| Tiled | root.corrected/SPM00/ |
SPM00_CM00_CHN00.ome.tif |
Each tile gets a SPM0N folder, where N is the tile index. Each SPM## dir
also contains a projections/ subfolder with per-view XY MIPs and a raw.xyProjection
counterpart for QC. Masks share the same prefix with suffixes
.segmentationMask, .xyMask, .xzMask. minIntensity.npz carries the
percentile values used downstream.
Fusion (multi_fuse)
| Mode | Path | Filename |
|---|---|---|
| Timelapse | root.fused/SPM00/TM000000/ |
SPM00_TM000000_CM00_CM01_VW00_CHN00.ome.zarr |
| Tiled | root.fused/SPM00/ |
SPM00_CM00_CM01_VW00_CHN00.ome.zarr |
Fused volumes go directly under each SPM## (mirroring the corrected tree — no
per-method subfolder). Projections for QC land in a single shared
root.fused/projections/ sibling of the SPM## dirs.
Default pairs: [(0, 1), (2, 3)] — cameras 0,1 fuse to VW00 (z-scan); cameras 2,3
fuse to VW90 (y-scan). The CHN value reflects the acquisition channel of the pair
(in the standard Keller IsoView wiring, CHN00 for CM0/1 and CHN01 for CM2/3). For
dual-channel acquisitions, each pair fuses once per shared channel, producing
multiple outputs with the same VW## but different CHN##.
Only the second camera in each pair gets rotation/flip transforms.
Supported Output Formats
| Format | Extension | Notes |
|---|---|---|
| OME-TIFF | .ome.tif |
with metadata, optional resolution pyramids |
| Zarr v3 | ome.zarr |
OME-NGFF metadata |
| KLB | .klb |
Keller Lab Block (bzip2) |
MATLAB Parity
The correction and fusion math is a faithful port of the original IsoView MATLAB
pipeline, verified against processTimepoint_RC.m, clusterPT_RC.m, and multiFuse.m.
Preserved end-to-end:
| Stage | Algorithm |
|---|---|
| Dead-pixel detection | std/mean projection, knee threshold, per-Z 2D median replacement |
| Knee threshold | 50k subsample, max-distance-from-line |
| Anisotropic Gaussian | separable, [k, k, max(1, k/scaling)] |
| Slab smoothing | margin = 2·kernelSize, crop-interior reassembly |
| Adaptive threshold | level = minI + (meanI − minI)·threshold |
| Background | Nth percentile of nonzero voxels, subsampled |
| Masks | uint16 0/1 |
| Camera registration | per-pair X/Y offset + rotation |
| Intensity correction | overlap-sum ratio, dimmer view scaled up |
| Blending | adaptive (mask) and geometric (crossover) paths |
Intentionally omitted (multi-channel features unused on single-color hardware):
reference/dependent channel groups, cross-channel mask OR-fusion, the 3-pass global
temporal mask, and per-channel-per-camera nesting. Rotation and cropping are deferred
from correction to fusion. apply_segmentation_mask defaults off (masks are saved,
not baked into the corrected volume). Downstream stages — temporal fusion, drift
correction, dF/F, isotropic interpolation — are out of scope.
Known minor differences: median-filter border handling (symmetric vs cv2
replicate, ~1px edge); coordinate masks use a mask-weighted centroid (equivalent
to the MATLAB NaN-mean for binary masks) with 0-based vs 1-based coordinates.
.stack Reading
Raw .stack is little-endian uint16, shape (D, H, W), memmappable. (W, H) from XML @dimensions[0]; D derived from file size (stat().st_size // 2 // (H*W)) — XML's D may be stale on aborted acquisitions.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file isoview-0.4.1.tar.gz.
File metadata
- Download URL: isoview-0.4.1.tar.gz
- Upload date:
- Size: 2.7 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
355044223d0a6e95add00dacbc6fea3a5122296bad66d9afdc3ecf1defd19df6
|
|
| MD5 |
85d18b23aecd537c2761795ef9c122b0
|
|
| BLAKE2b-256 |
6607d5d4f7124bb43c6c0f480c6152c31a58fa5e915ed3de4df5e2c3d6edb75b
|
File details
Details for the file isoview-0.4.1-py3-none-any.whl.
File metadata
- Download URL: isoview-0.4.1-py3-none-any.whl
- Upload date:
- Size: 108.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bab7def4ab2ce619bbc0f2628253b8a132d4eb721657699c4cd4577117b20e4a
|
|
| MD5 |
03686e1fa9a01a52b24b29719ef5ebf1
|
|
| BLAKE2b-256 |
5584040cc302c2c3c67a94d31bf7ae8663b186785d9e314a5bd3df2498676edb
|