spacr.pipeline_v2¶
Streaming mask pipeline (v2).
An alternative to the multi-copy disk chain that
preprocess_generate_masks runs by default (pipeline_style='v1'):
- originals
→ renamed + split into channel folders → orig/ backup → per-channel npy → batch npz on disk → cellpose → per-field mask npy → concatenated into merged/
This module instead runs a two-pass streaming pipeline that keeps only what the downstream measure module reads:
- Pass 1 — assemble
walk originals, parse metadata regex, build one npy stack per field with all image channels in the C axis. Emit
filename_map.csvrecording every original → stack mapping.- Pass 2 — segment
stream the plate in batches of N fields, hand each batch to Cellpose, append the mask channels to the SAME stack file. Each batch is written to a scratch NPZ on the way through and deleted again unless
keep_npz=True.
Output — merged/ folder holds one file per field, each shape
(H, W, C_image + C_mask) in uint16, plus:
channel_order.json {“channels”: […]} filename_map.csv original path, plate/well/field/…, stack idx
Public API:
from spacr.pipeline_v2 import (
FilenameMapper, stream_originals_to_stack,
stream_masks_from_stack, run_v2,
)
# High-level (one call):
run_v2(src_folder, channels=(0,1,2,3), model="cyto", diameter=60)
# Low-level (two passes, run each explicitly):
mapper = FilenameMapper.discover(src_folder,
metadata_type="cellvoyager")
stacks = stream_originals_to_stack(src_folder, mapper, channels=(0,1,2,3))
stream_masks_from_stack(stacks, model="cyto", diameter=60)
This module is opt-in: spacr.core.preprocess_generate_masks() runs
it when pipeline_style='v2', and the default 'v1' runs the disk
chain above.
Classes¶
Walks a folder of microscopy images, parses each filename's |
|
One entry in the filename map. |
|
One field's on-disk stack: |
Functions¶
|
Run the entire v2 pipeline against |
|
Batch the field stacks through Cellpose, then append the mask |
|
Write one |
Module Contents¶
- class spacr.pipeline_v2.FilenameMapper(records: List[FilenameRecord], metadata_type: str, regex: str)[source]¶
Walks a folder of microscopy images, parses each filename’s metadata via a regex, and records the mapping to a per-plate CSV.
The CSV is written next to the
merged/folder (at the plate root) so users can Excel-openfilename_map.csvand see the original path of every image in the run.- Parameters:
records – parsed filename records in file-system order.
metadata_type – name of the metadata convention used to parse them.
regex – regular-expression source used for parsing.
- Variables:
records – list of
FilenameRecordin file-system order.metadata_type – which regex was used (
"cellvoyager"/"yokogawa"/"custom").regex – compiled regex pattern that matched.
Store parsed filename records and the metadata rule that made them.
- by_field() Dict[str, List[FilenameRecord]][source]¶
Group records by
stack_field_id— one entry per field, with one record per channel inside.
- classmethod discover(src: pathlib.Path, metadata_type: str = 'auto', custom_regex: str | None = None, exts: Sequence[str] = ('.tif', '.tiff', '.png', '.jpg', '.jpeg')) FilenameMapper[source]¶
Scan
srcfor images + parse each name with the metadata regex. Falls back throughcellvoyager→yokogawaonmetadata_type="auto".- Parameters:
src – folder to scan. Not recursive: images are expected at the top level, as in the current spacr layout.
metadata_type –
"auto"/"cellvoyager"/"yokogawa"/"custom". When"custom",custom_regexmust be given.custom_regex – user-supplied regex; required for
metadata_type="custom".exts – image file extensions to include.
- Returns:
a populated
FilenameMapper.- Raises:
ValueError – when no images are found or no regex fits.
- classmethod load_csv(path: pathlib.Path) FilenameMapper[source]¶
Rehydrate a mapper from a previously-saved CSV.
- Parameters:
path – mapping CSV previously written by
save_csv().
- save_csv(path: pathlib.Path) pathlib.Path[source]¶
Write the mapping to
pathas a CSV that Excel opens cleanly. One row per (original image, resulting stack slot).- Parameters:
path – destination CSV path; its parent directory is created.
- class spacr.pipeline_v2.FilenameRecord[source]¶
One entry in the filename map.
- Variables:
original_path – absolute path to the source image on disk.
plate – plate id parsed from the filename.
well – well id parsed from the filename.
field – field index parsed from the filename.
channel – channel index parsed from the filename.
time – time index parsed from the filename (defaults to 1).
z – z-slice index parsed from the filename (defaults to 1).
stack_field_id – the
fieldid used inmerged/stack_<X>.npy.
- class spacr.pipeline_v2.StackFile[source]¶
One field’s on-disk stack:
merged/stack_<id>.npywith shape(H, W, C).Populated by
stream_originals_to_stack()before Cellpose runs (C = image channels only). Afterstream_masks_from_stack()the same file has additional mask channels appended.- Variables:
field_id – stable field identifier used in the stack filename.
path – path to the on-disk NumPy stack.
shape –
(height, width, channels)shape at write time.channels – human-readable channel names in array order.
- spacr.pipeline_v2.run_v2(src: pathlib.Path, channels: Sequence[int] = (0, 1, 2, 3), channel_names: Sequence[str] | None = None, model_name: str = 'cyto', channels_for_cellpose: Sequence[int] = (0, 0), diameter: float | None = None, batch_fields: int = 8, metadata_type: str = 'auto', custom_regex: str | None = None, keep_npz: bool = False, cellprob_threshold: float = 0.0, flow_threshold: float = 0.4, min_size: int = 15, resample: bool = True, postprocess_settings: Dict[str, Any] | None = None, object_type: str = 'cell', illumination_settings: Dict[str, Any] | None = None) Dict[str, Any][source]¶
Run the entire v2 pipeline against
src. Convenience wrapper.Equivalent to:
mapper = FilenameMapper.discover(src, metadata_type, custom_regex) stacks = stream_originals_to_stack(src, mapper, channels, channel_names) stream_masks_from_stack(stacks, model_name, channels_for_cellpose, diameter, batch_fields, keep_npz=keep_npz)
- Parameters:
src – plate folder holding the originals, scanned non-recursively. Normally the mapping CSV lands here and output goes to
<src>/merged. Calibrated runs use a freshcalibrated_segmentationoutput directory instead, preserving source metadata and existing merged arrays. Occupied names receive a numeric suffix; calibrated results are never resumed.channels – channel numbers as parsed from the filename (
C01gives 1), not C-axis positions. The default(0, 1, 2, 3)therefore misfits stock CellVoyager/Yokogawa names: channel 0 never exists, so plane 0 is all zeros, andC04is dropped. Pass(1, 2, 3, 4)for those layouts.channel_names – names recorded in
channel_order.jsonand on eachStackFile. A length mismatch withchannelstrips a bareassert, so it goes unchecked underpython -O.model_name – resolved by
spacr.utils._resolve_cellpose_pretrained(), not passed as Cellpose’smodel_type. On Cellpose 4 every legacy name ("cyto","nuclei", …) collapses tocpsam, so only a fine-tuned checkpoint path actually changes the weights, and a path with no file behind it raises instead of falling back.channels_for_cellpose – despite the name this never reaches Cellpose’s
channels=argument; it selects C-axis indices from the assembled stack, taken modulo the channel count (7 with C=4 becomes 3) and de-duplicated in order. The default(0, 0)collapses to one plane, and an empty sequence falls back to plane 0. Calibrated runs require explicit in-range positions instead.diameter – forwarded to
model.eval;Noneleaves Cellpose to size objects itself. Unlikemodel_nameit is still honoured on Cellpose 4, which rescales the image by30 / diameter.batch_fields – fields loaded and segmented per batch, the memory-versus-speed dial.
0raisesValueError; a negative value segments nothing at all, yetchannel_order.jsonis still rewritten to claim a mask channel that no stack received. Calibrated batches are additionally bounded to64 * 1024**2packed input or padded model-input values; lower this field count when a batch refuses.metadata_type –
"cellvoyager","yokogawa","custom"or"auto". Only"custom"is special-cased; every other unrecognised string quietly behaves as"auto"instead of raising.custom_regex – required when
metadata_type="custom", elseValueError. It must supply the named groups (plateID,wellID,fieldID,chanID,timeID,sliceID); any group it omits silently defaults, so a regex withoutchanIDmakes every image channel 1 and therefore its own single-plane field.keep_npz – the per-batch NPZ is compressed into
merged/_scratcheither way — this only decides whether it and the scratch folder survive, soFalsedoes not save the write.cellprob_threshold – forwarded to
model.evalthroughfloat().flow_threshold – forwarded to
model.evalthroughfloat().min_size – forwarded to
model.evalthroughint();NoneraisesTypeErrorrather than meaning “no minimum”.resample – coerced with
bool(), so any non-empty string —"false"included — is True, whileNoneis False.postprocess_settings –
Nonehands the raw selected planes to Cellpose and skips post-processing entirely. Any dict,{}included, switches on bothspacr.io._normalize_img_batch()andspacr.object.merge_split_filter_masks(). The four*_channelrole keys are rewritten on a copy (the caller’s dict is left alone): all cleared to None, thenobject_typeset to 0, plusnucleus_channel=1whenobject_typeis"cell"and two or more planes were selected. An explicitunmix_calibrationwithunmixselects the organized 2D calibrated cell route: signed valid-ROI crops are retained separately, segmentation uses a nonnegative view and labels are restored to the original reference canvas with incomplete boundary objects removed. Unsupported preprocessing, custom/intensity/ perimeter filters and quality exclusion refuse before inference. Optionalunmix_per_plateselects complete physical-channel controls from a JSON master map for one exact source plate/time/plane; mixed sources and missing assignments refuse before output creation.object_type – picks the weights during
model_nameresolution, names the role given channel 0 in normalisation, and is passed to the mask post-processor. An unrecognised value does not raise — it adds a dead<value>_channelkey and leaves every real role unset.illumination_settings – full Mask settings mapping. When it enables
illumination_correction, one model is prepared from the raw persisted stacks after pass 1 and its session corrects only the private Cellpose inputs in pass 2; omitted/off preserves the previous byte-level output contract.
- Returns:
dict with
mapper(FilenameMapper),stacks(list ofStackFile), anddst(Path tomerged/). Calibrated output also names its separateoutput_root; its float32 display intensities are aligned and nonquantitative, never Measure inputs. Signed valid-ROI float32 diagnostics live underoutput_root/signed; its application record binds both artifacts and remains incomplete on error.
A nonempty
unmix_matrix_fileinpostprocess_settingssupplies a version-1 JSON matrix instead of estimating one. This requires calibrated organized 2D cell V2 processing, no control wells, andunmix_per_plate=False. Its physical channels must match the packed order exactly; no transpose, channel reordering or diagonal renormalization is inferred.matrix[i][j]is the camera reading in row-channeliper unit of the dye measured in its own column-channelj. The diagonal is exactly one; outputs retain camera-linear units and signed residuals, not calibrated concentrations. For physical order[3, 1], the file has exactly these keys:{ "artifact": "spacr_spectral_matrix", "version": 1, "source_context": {"plate": "plate1", "time": "", "z": ""}, "channels": [3, 1], "orientation": "rows_readings_columns_dyes", "matrix": [[1, 0.4], [0.7, 1]], "background_percentile": 0, "background_operator": "per_field_percentile_subtract_solve_restore", "calibration_source_sha256": "<SHA256 of the exact calibration-file bytes>", "acquisition_compatibility": "user_asserts_same_channel_gains_exposures_and_linear_response" }
Replace the example calibration hash with 64 lowercase hexadecimal digits. Background percentile must equal
unmix_background_percentile. Plate, time and Z must equal every mapped field’s exact acquisition IDs, including their string or integer types; an absent time or Z uses the empty string. This supplied-file route requires POSIX descriptor-relative access without symbolic links; unsupported platforms refuse before creating outputs. The acquisition-compatibility token is an unverified user assertion: unknown gains, exposures and response remain unknown. The file is captured before outputs and copied byte-for-byte intopsf/spectral_matrix_input.json. Signed-cell readers validate that copied bundle and never depend on the original external path. An empty path preserves control-estimation behavior.With
unmix_partition_plates=Trueandunmix_per_plate=True, one mixed-plate source produces independent calibrated child datasets. Supplyunmix_plate_calibrationsas a JSON string mapping every exact plate ID tocalibration_file, positive YXpixel_size_um,source_contextcontaining exactplate,timeandzIDs, andacquisition_compatibilityset touser_asserts_same_channel_gains_exposures_and_linear_response. Relative optical paths resolve against the original source folder. Each plate has one time/Z context, complete physical-channel controls in the master map, and its own optical assignment and pixel spacing. The assertion concerns controls and targets within that plate, remains unverified, and does not assert equal acquisition settings between plates. Global optical settings and supplied matrix files cannot be combined with partitioning.Before creating outputs, partitioning captures optical bytes, admits all source headers and batch bounds, and estimates each plate’s sampled-control matrix sequentially. Unread target values are validated later by the child route before model construction. Canonical control-value fingerprints are compared after assembly; actual NPY file hashes remain the child record’s authority. Limits are 8192 fields, 16 physical channels and 16 MiB retained metadata, with the ordinary per-field, control and batch budgets unchanged. Supported source formats are unchanged; QC supports report or off only. Directory-relative, no-follow and nonblocking descriptor access is required; unsupported hosts refuse before creating outputs.
A fresh
calibrated_plate_batchcollection directory contains aplate_batch.jsonmanifest and numberedplate_000001child datasets. Existing names receive a suffix. The collection is never a measurable dataset; each child retains the ordinary version-1 calibrated signed-cell contract and original global field/source identities. Original files and existing merged data remain untouched. Cancellation or failure preserves completed children and rethrows the original error. Segmentation completion is distinct from aggregate completion, which also requires completed or disabled QC. The opt-in return haspartitioned=True,plate_resultswith independent resolvedsettingsandoutput_rootper child,output_roots, andmanifest_path; it has no singular output fallback. With partitioning disabled, the existing return and processing are unchanged.- Raises:
ValueError – from
FilenameMapper.discover()whensrcholds no images, whenmetadata_type="custom"has nocustom_regex, or frombatch_fields=0.
- spacr.pipeline_v2.stream_masks_from_stack(stacks: List[StackFile], model_name: str = 'cyto', channels_for_cellpose: Sequence[int] = (0, 0), diameter: float | None = None, batch_fields: int = 8, mask_channel_name: str = 'mask', keep_npz: bool = False, npz_dir: pathlib.Path | None = None, cellprob_threshold: float = 0.0, flow_threshold: float = 0.4, min_size: int = 15, resample: bool = True, postprocess_settings: Dict[str, Any] | None = None, object_type: str = 'cell', illumination_session: Any | None = None, psf_session: Any | None = None) List[StackFile][source]¶
Batch the field stacks through Cellpose, then append the mask channel(s) to the SAME npy files.
- Parameters:
stacks – list produced by
stream_originals_to_stack().model_name – Cellpose model to use (
"cyto","nuclei", …).channels_for_cellpose – C-axis indices selected out of each assembled stack before it is handed to Cellpose — taken modulo the channel count and de-duplicated in order. It is NOT forwarded as Cellpose’s
channels=argument;[0, 0]therefore yields a single plane, not a grayscale pair. Calibrated sessions instead require explicit in-range positions. The first selected channel is the object’s own channel for absolute mean-intensity filtering.diameter – expected object diameter in px (None → Cellpose auto).
batch_fields – how many field stacks to load into memory at once. Larger = faster but more RAM. Calibrated batches refuse above
64 * 1024**2packed input or padded model-input values.mask_channel_name – human name to record for the appended mask channel (default
"mask").keep_npz – the intermediate batch is compressed to an NPZ under
npz_diron every batch regardless; this flag only decides whether that file and the scratch folder survive the run.npz_dir – where to write the (optional) intermediate NPZ files. Defaults to a scratch subfolder under the stack folder.
illumination_session – optional
spacr.illumination.SegmentationIlluminationSession. Its corrector sees private selected-channel copies immediately before normalisation/Cellpose; persisted intensity planes and scratch NPZs remain raw, and completion is recorded only after the combined stack has been atomically replaced.psf_session – optional captured PSF session. Unmixes each whole field first when unmixing is on, then processes selected intensities after illumination and before normalization, padding or Cellpose. Stored image channels stay raw; only the appended labels depend on PSF processing. A calibrated session instead supplies a signed valid crop for a nonnegative segmentation view, restores labels to the reference canvas and stores separate nonquantitative float32 display channels in its separate output tree, with signed crops under
signed/. These display channels are not Measure inputs; calibrated outputs cannot be resumed and completion requires both durable display and signed files.
- Returns:
the same list, with each
StackFile.shape/.channelsupdated to reflect the appended mask channel.
- spacr.pipeline_v2.stream_originals_to_stack(src: pathlib.Path, mapper: FilenameMapper, channels: Sequence[int] = (0, 1, 2, 3), channel_names: Sequence[str] | None = None, dst: pathlib.Path | None = None, *, metadata_root: pathlib.Path | None = None, calibrated: bool = False) List[StackFile][source]¶
Write one
merged/stack_<field>.npyper field.Reads originals directly (no rename-into-channel-folders step), stacks the selected channels along the C axis, and writes one npy per field. Also emits a
channel_order.jsonsidecar describing which C-index holds which channel.- Parameters:
src – plate folder containing the original images.
mapper –
FilenameMapperproduced fromsrc.channels – which channel numbers (as parsed from filenames) to include, in the order they should occupy the C axis.
channel_names – human names for those channels (must match
channelslength). Default:["ch0", "ch1", …].dst – override the output folder; defaults to
<src>/merged.metadata_root – mapping CSV destination, defaulting to the source folder.
calibrated – preserve numeric planes and refuse missing, duplicate, non-2D or oversized channels for the separate calibrated route.
- Returns:
list of
StackFile, one per field written.