spacr.zstack

Z- and time-axis handling for the 3D (Beta) and 4D (Beta) mask settings.

This module holds the two non-xy axes: the z half (3D Beta, the first part of the file) and the t half built on top of it (4D Beta, the second part, from the 4D (Beta) banner onwards – it has its own long preamble there). Both are deliberately kept free of Cellpose, of any tracker library and of any model at all, so that the logic can be tested against synthetic label volumes on a CPU in milliseconds: every function here either takes a plain numpy array or takes a segment_fn callable that the caller supplies. spacr.object provides the Cellpose adapter.

The two halves live in one file rather than two because a 4-D acquisition is a z-stack per timepoint and the t code delegates to the z code for every one of them – segment_4d is a loop around segment_3d(), track_4d’s overlap backend is stitch_planes() applied along t, and TStackSpec carries a ZStackSpec inside it. Nothing in the z half below knows the t half exists, so a 3-D run is unaffected by any of it.

Four things drive the design of the z half, and each of them is a place where a naive 3-D implementation silently produces wrong science:

Anisotropy is the whole game.

Confocal z-spacing is routinely 3-10x the xy pixel size. A volume segmented as if it were isotropic merges objects along z, because a 3-plane gap that is really 3 um reads to the segmenter as 3 pixels. anisotropy here always means dz / dxy – the z step divided by the xy pixel size – which is the same convention Cellpose uses. It is a required input for volumetric segmentation: resolve_anisotropy() raises UnknownAnisotropyError rather than defaulting to 1.0, because 1.0 is a claim about the microscope, not a neutral value.

Stitching 2-D planes is not 3-D segmentation.

Both are legitimate and they give different answers. Per-plane segmentation followed by IoU linking (MODE_STITCH) sees each plane independently and cannot recover an object that is invisible in one plane; true volumetric segmentation (MODE_VOLUMETRIC) uses the z gradient but is far more sensitive to a wrong anisotropy. The mode is therefore explicit, never inferred, and ZStackResult records which one actually ran so it can be written next to the numbers it produced.

Objects touching the first or last plane are truncated.

Exactly as an object touching the xy field edge is truncated. Their volume is a lower bound and their shape statistics are meaningless. flag_truncated_z() marks them, mirroring the reasoning spacr.seg_qc already established for xy border objects (see seg_qc._score_labels, which counts border objects and then excludes them from every size statistic).

A single-plane volume is 2-D, not degenerate 3-D.

n_z == 1 short-circuits to MODE_SINGLE_PLANE, which calls segment_fn on the plain 2-D plane and returns a 2-D mask. It does not resample, does not stitch, and does not consult anisotropy.

Memory

A z-stack is n_z times a field, and volumetric segmentation transiently needs several copies of it. estimate_peak_bytes() gives the number for a given field; the pipeline processes one field at a time and never holds a plate. For a 2048x2048 field at 21 planes in float32 that is ~350 MB for the volume, and ~1.4 GB peak in MODE_VOLUMETRIC with resampling at anisotropy 5 (the isotropic copy is anisotropy times taller). This is why MODE_VOLUMETRIC is not the default.

Scope, stated plainly

The functions here are correct and tested, but the pipeline cannot yet feed them: spacr.io._rename_and_organize_image_files collapses z before any array reaches segmentation, and spacr.measure cannot consume a 3-D label mask. plan_from_settings() therefore returns None whenever the 3D settings are off – which is the default – so the 2-D path is untouched, and spacr.object raises ZAxisNotPresentError rather than quietly projecting when the settings are on but no z axis survived ingest.

Exceptions

AmbiguousAxisOrderError

It could not be established which axis is t and which is z.

AmbiguousZAxisError

The z axis could not be identified and was not supplied.

TAxisNotPresentError

The 4D settings are on but the array that arrived has no t (or no z).

TStackError

Base class for every 4-D configuration problem.

TrackerIsTwoDError

A backend that cannot link volumes was asked to link volumes.

UnknownAnisotropyError

Volumetric segmentation was asked for without a z/xy voxel ratio.

UnknownDisplacementError

A distance-based backend was asked for without a displacement gate.

ZAxisNotPresentError

The 3D settings are on but the array that arrived has no z axis.

ZStackError

Base class for every z-stack configuration problem.

Classes

AxisOrder

Which axis of the incoming array is which.

TStackResult

The labels a 4-D run produced, plus how it produced them.

TStackSpec

Everything the 4-D plumbing needs to know about one acquisition.

TrackBackend

What one tracking backend can and cannot be driven to do here.

TrackResult

The outcome of linking a 4-D label array across time.

ZStackResult

The labels a z-aware run produced, plus how it produced them.

ZStackSpec

Everything the z plumbing needs to know about one acquisition.

Functions

as_t_first(→ numpy.ndarray)

Return a view of array with t at axis 0 and z at axis 1.

describe_3d_measurement(→ Dict[str, str])

What one measurement column means in a 3-D run.

detect_axes(→ Optional[AxisOrder])

Work out which leading axis is t and which is z, or refuse to.

detect_z_axis(→ Optional[int])

Identify which axis of a 3-D array is z.

estimate_peak_bytes(→ int)

Peak bytes one field needs, so a plate can be sized before it is run.

estimate_peak_bytes_4d(→ int)

Peak bytes a 4-D run needs, so an acquisition can be sized before it runs.

flag_truncated_t(→ numpy.ndarray)

Track ids present in the first or last timepoint, and so cut off in t.

flag_truncated_z(→ numpy.ndarray)

Labels that touch the first or last z plane, and are therefore cut off.

format_4d(→ str)

Render a TStackResult or TrackResult as text.

iter_volumes(→ Iterator[numpy.ndarray])

Yield one (Z, Y, X[, C]) volume per timepoint, lazily.

plan_4d_from_settings(→ Optional[TStackSpec])

Build a TStackSpec from a settings dict, or None when off.

plan_from_settings(→ Optional[ZStackSpec])

Build a ZStackSpec from a settings dict, or None when off.

project(volume[, mode, z_axis])

Collapse the z axis of volume to a single plane.

project_labels(→ numpy.ndarray)

Collapse a (Z, Y, X) label volume to (Y, X), honestly.

relabel_volume(→ numpy.ndarray)

Renumber labels to a contiguous 1..N with no gaps, background 0.

report_3d_measurements(→ Dict[str, List[str]])

Sort a real measurement table's columns by how 3-D treats them.

resample_isotropic(→ numpy.ndarray)

Stretch the z axis by anisotropy so the voxels become cubic.

resolve_anisotropy(→ float)

Return dz / dxy, derived from the voxel size when available.

resolve_axis_order(→ AxisOrder)

Return the AxisOrder for array, or explain why it cannot.

restore_anisotropic(→ numpy.ndarray)

Undo resample_isotropic(), back to n_z planes.

segment_3d(→ ZStackResult)

Segment a volume in the requested mode and record how it was done.

segment_4d(→ TStackResult)

Segment every timepoint independently, in 3-D, and stack the results.

stitch_planes(→ numpy.ndarray)

Link per-plane 2-D labels into 3-D objects by IoU between neighbours.

track_4d(→ TrackResult)

Link objects across time, in 3-D, and renumber them by track.

volume_stats(labels[, voxel_size])

Per-object volume, surface, z extent and truncation flag.

volume_tracks(labels_4d[, spec])

One row per object per timepoint: where it is, how big, how truncated.

Module Contents

exception spacr.zstack.AmbiguousAxisOrderError[source]

Bases: TStackError

It could not be established which axis is t and which is z.

Guessing here chooses between tracking through time and “tracking” down a z-stack, and the second produces smooth plausible trajectories that mean nothing, so the shape is reported back to the user instead.

Initialize self. See help(type(self)) for accurate signature.

exception spacr.zstack.AmbiguousZAxisError[source]

Bases: ZStackError

The z axis could not be identified and was not supplied.

Raised only by detect_z_axis(..., strict=True). Guessing here picks between segmenting a volume and segmenting a transposed one, so the shape is reported back to the user instead.

Initialize self. See help(type(self)) for accurate signature.

exception spacr.zstack.TAxisNotPresentError[source]

Bases: TStackError

The 4D settings are on but the array that arrived has no t (or no z).

Almost always because spacr.io collapsed z during ingest. The message names the setting to turn off and where the axis went.

Initialize self. See help(type(self)) for accurate signature.

exception spacr.zstack.TStackError[source]

Bases: spacr.errors.ConfigurationError

Base class for every 4-D configuration problem.

A spacr.errors.ConfigurationError, so a run never continues past one: every field would be wrong in the same way.

Initialize self. See help(type(self)) for accurate signature.

exception spacr.zstack.TrackerIsTwoDError[source]

Bases: TStackError

A backend that cannot link volumes was asked to link volumes.

Raised rather than projecting z away, because a projected track table is indistinguishable from a real one after the fact.

Initialize self. See help(type(self)) for accurate signature.

exception spacr.zstack.UnknownAnisotropyError[source]

Bases: ZStackError

Volumetric segmentation was asked for without a z/xy voxel ratio.

Defaulting to 1.0 would be a silent claim that the z step equals the xy pixel size, which for confocal data is wrong by 3-10x and merges objects along z.

Initialize self. See help(type(self)) for accurate signature.

exception spacr.zstack.UnknownDisplacementError[source]

Bases: TStackError

A distance-based backend was asked for without a displacement gate.

There is no safe default: the right value is set by how fast the objects move relative to the frame interval, and a wrong one either links objects that are not the same or splits one object into a track per frame.

Initialize self. See help(type(self)) for accurate signature.

exception spacr.zstack.ZAxisNotPresentError[source]

Bases: ZStackError

The 3D settings are on but the array that arrived has no z axis.

Almost always because z was collapsed during ingest. The message names the setting to turn off and where the z went.

Initialize self. See help(type(self)) for accurate signature.

exception spacr.zstack.ZStackError[source]

Bases: spacr.errors.ConfigurationError

Base class for every z-stack configuration problem.

A spacr.errors.ConfigurationError, so a run never continues past one: every field would be wrong in the same way.

Initialize self. See help(type(self)) for accurate signature.

class spacr.zstack.AxisOrder[source]

Which axis of the incoming array is which.

Parameters:
  • t_axis – index of the time axis.

  • z_axis – index of the z axis, or None for a flat time series.

  • y_axis – index of the row axis.

  • x_axis – index of the column axis.

  • channel_axis – index of the channel axis, or None.

  • source – how the order was established – 'explicit', 'n_t', 'n_z' or 'n_t+n_z' – recorded so it can be written next to the tracks it produced.

__post_init__()[source]

Reject an axis order that names one array axis twice.

Raises:

TStackError – if any two of t, z, y, x and channel share an index. One array axis cannot be two things at once, and the failure it causes downstream names neither of them.

property name: str[source]

'TZYX', 'ZTYX', 'TYX' … in ascending axis order.

class spacr.zstack.TStackResult[source]

The labels a 4-D run produced, plus how it produced them.

Parameters:
  • labels – t-first labels: (T, Z, Y, X) for the volumetric z modes, (T, Y, X) for project and for a single-plane acquisition. Label values are per-timepoint and carry no identity across t until track_4d() has run.

  • z_results – the spacr.zstack.ZStackResult for each timepoint, in order.

  • spec – the spec that produced them.

  • n_t – number of timepoints segmented.

  • n_z – planes per timepoint.

  • notes – remarks worth surfacing to the user.

property has_z: bool[source]

Whether the labels kept a z axis (i.e. are (T, Z, Y, X)).

property objects_per_timepoint: List[int][source]

Non-background label count at each timepoint.

property z_mode: str[source]

The mode that actually ran, as recorded by the first timepoint.

class spacr.zstack.TStackSpec[source]

Everything the 4-D plumbing needs to know about one acquisition.

The geometry half (t_axis … voxel_size_um) describes the data; the rest describes what to do with it. z_mode, projection and stitch_threshold are handed straight to spacr.zstack via to_z_spec() and mean exactly what they mean there.

Parameters:
  • t_axis – index of the time axis in the incoming array.

  • z_axis – index of the z axis, or None for a flat time series.

  • n_t – number of timepoints, or None when not yet known.

  • n_z – number of z planes, or None when not yet known.

  • anisotropy – dz / dxy. None means “not known”, which is fatal for volumetric segmentation and for the distance-based trackers.

  • frame_interval_s – seconds between consecutive timepoints, or None. Only ever used to add a time_s column – no linking decision depends on it.

  • voxel_size_um – (dz, dy, dx) in micrometres, or None.

  • channel_axis – index of a channel axis, or None.

  • z_mode – one of spacr.zstack.SEGMENTATION_MODES.

  • projection – reducer used by MODE_PROJECT.

  • stitch_threshold – IoU floor for stitch_planes(), i.e. for linking across z.

  • track_backend – one of TRACK_BACKENDS.

  • link_threshold – IoU floor for linking across t in BACKEND_IOU. Deliberately a separate number from stitch_threshold: consecutive z planes and consecutive timepoints do not overlap by the same amount.

  • max_displacement_px – gate for the distance-based backends, in xy pixels, with z scaled by anisotropy.

  • max_displacement_um – the same gate in micrometres; needs voxel_size_um. Mutually exclusive with max_displacement_px.

  • project_for_tracking – opt in to collapsing z before linking, so that linking happens on the projection rather than on the volume. Off by default and never implied. It does not unlock the backends spaCR cannot drive volumetrically – see track_4d().

__post_init__()[source]

Validate the timelapse settings, including the z settings it carries.

The z spec is constructed here rather than on first use, so an impossible z mode, projection or anisotropy is refused when the spec is built rather than after the first field has been read.

Raises:
  • TStackError – if t, z and channel do not name distinct axes; if the tracking backend is not one this build offers; if the link threshold is not an IoU in [0, 1]; if the maximum displacement is given in both pixels and microns – the same gate in two units, and spaCR will not pick one – or if a displacement or frame interval is not finite and positive.

  • ZStackError – from the z spec it builds.

require_anisotropy() → float[source]

Return dz/dxy, or explain why the run cannot proceed.

Raises:

spacr.zstack.UnknownAnisotropyError – when it is not known.

to_z_spec() → ZStackSpec[source]

The spacr.zstack.ZStackSpec for one timepoint of this run.

Returns:

a z spec carrying this spec’s z settings verbatim.

property axis_order: str | None[source]

'TZYX' / 'ZTYX' when the two leading axes are t and z.

property backend: TrackBackend[source]

The TrackBackend record for track_backend.

property voxel_size: Tuple[float, float, float] | None[source]

Alias of voxel_size_um; the units are in the canonical name.

class spacr.zstack.TrackBackend[source]

What one tracking backend can and cannot be driven to do here.

Two separate booleans, because they are two separate facts and conflating them is how a user ends up believing spaCR cannot do something the library plainly can.

Parameters:
  • name – the timelapse_mode / t_track_backend spelling.

  • links_3d – whether track_4d() can drive it on a (Z, Y, X) volume per timepoint.

  • library_links_3d – whether the upstream package is capable of 3-D at all, independent of spaCR.

  • note – the sentence shown to the user when the two disagree.

class spacr.zstack.TrackResult[source]

The outcome of linking a 4-D label array across time.

Parameters:
  • labels – t-first labels renumbered so that one value means one track for the whole acquisition; same shape as the input.

  • tracks – the volume_tracks() table.

  • backend – which backend linked them.

  • n_tracks – number of distinct tracks.

  • anisotropy – the value used, or None when the backend has no distance in it.

  • projected – whether z was collapsed before linking (only ever true when the caller asked for it).

  • notes – remarks worth surfacing to the user.

property truncated_fraction: float[source]

Share of tracks cut off by the start or end of the acquisition.

The t counterpart of spacr.zstack.ZStackResult.truncated_fraction; a high value means the movie does not span the process being measured and every lifetime in the table is a lower bound.

property truncated_tracks: numpy.ndarray[source]

Track ids present in the first or last timepoint.

class spacr.zstack.ZStackResult[source]

The labels a z-aware run produced, plus how it produced them.

mode is recorded rather than inferred because a stitched volume and a volumetric one are different measurements of the same sample, and a number without its mode cannot be compared with anything.

Parameters:
  • labels – (Z, Y, X) for stitch/volumetric, (Y, X) for project and single-plane.

  • mode – The mode that actually ran – may be MODE_SINGLE_PLANE even though that is not selectable.

  • anisotropy – The value used, or None when the mode ignores it.

  • n_z – Planes in the input volume.

  • truncated_labels – Labels touching the first or last plane.

  • notes – Human-readable remarks worth surfacing to the user, e.g. that anisotropy was supplied but the chosen mode ignores it.

property n_objects: int[source]

Number of distinct non-background labels.

property truncated_fraction: float[source]

Share of objects cut off by the first or last plane.

The z equivalent of seg_qc’s border_fraction; like it, a high value means the stack does not span the objects and their volumes are lower bounds.

class spacr.zstack.ZStackSpec[source]

Everything the z plumbing needs to know about one acquisition.

Parameters:
  • z_axis – Index of the z axis in the incoming array, or None to detect it with detect_z_axis().

  • n_z – Number of planes, or None when not yet known.

  • anisotropy – dz / dxy. None means “not known”, which is fatal for MODE_VOLUMETRIC and merely recorded otherwise.

  • voxel_size_um – (dz, dy, dx) in micrometres, or None.

  • projection – Reducer used by MODE_PROJECT.

  • mode – One of SEGMENTATION_MODES.

  • stitch_threshold – IoU floor for stitch_planes().

  • resample_to_isotropic – Pre-resample the volume to isotropic voxels instead of handing anisotropy to the segmenter.

__post_init__()[source]

Validate the z-stack settings before anything reads a field.

Raises:

ZStackError – if the segmentation mode or the projection is not one this build offers; if anisotropy is not positive – it is the ratio dz/dxy, so 1.0 is isotropic voxels and 5.0 means the z step is five times the xy pixel; or if stitch_threshold is not an IoU in [0, 1].

require_anisotropy() → float[source]

Return the anisotropy, or explain why the run cannot proceed.

Raises:

UnknownAnisotropyError – when it is not known.

spacr.zstack.as_t_first(array, spec: TStackSpec) → numpy.ndarray[source]

Return a view of array with t at axis 0 and z at axis 1.

A view, not a copy: np.moveaxis never copies, which is what keeps a 4-D acquisition from being materialised twice.

Parameters:
  • array – the acquisition, axes as described by spec.

  • spec – the TStackSpec naming t_axis / z_axis.

Returns:

the same data as (T, Z, Y, X[, C]) or (T, Y, X[, C]).

Raises:

TAxisNotPresentError – when the array has fewer axes than the spec claims.

spacr.zstack.describe_3d_measurement(name: str) → Dict[str, str][source]

What one measurement column means in a 3-D run.

Accepts either the bare regionprops name ("area") or a full column name carrying its object-type prefix ("cell_area").

Parameters:

name – measurement or column name.

Returns:

a copy of the MEASUREMENT_MEANING_3D entry, or an entry with kind="unknown" for a name this table says nothing about.

spacr.zstack.detect_axes(array, n_t: int | None = None, n_z: int | None = None, xy_min: int = 32, channel_axis: int | None = None, strict: bool = False) → AxisOrder | None[source]

Work out which leading axis is t and which is z, or refuse to.

The trailing two axes are taken to be (Y, X). That is not a guess: AXIS_ORDER_TZYX and AXIS_ORDER_ZTYX agree on it, so there is nothing to choose between, and it is checked – if either of them is shorter than xy_min the array is not (..., Y, X) at all and that is an error rather than a silent transposition.

The two leading axes are t and z in an order the shape cannot reveal. This function settles it only from evidence:

  • n_t and/or n_z given, and exactly one assignment matches -> that assignment;

  • a hint given that matches both assignments (n_t == n_z, or the two leading axes are the same length) -> ambiguous, None;

  • a hint given that matches neither -> TStackError, because the user’s belief about the data and the data disagree;

  • no hint at all -> ambiguous, None.

There is deliberately no heuristic on the leading lengths. “Time series are longer than z stacks” is true often enough to be dangerous and false often enough to ruin an experiment – a 5-timepoint acquisition of 40-plane stacks is an ordinary thing to collect.

Parameters:
  • array – a 4-D array (or a shape tuple).

  • n_t – number of timepoints, if known independently of the array.

  • n_z – number of z planes, if known independently of the array.

  • xy_min – smallest side length still considered an image axis.

  • channel_axis – index of a channel axis to ignore, if present.

  • strict – raise instead of returning None when ambiguous.

Returns:

an AxisOrder, or None when it cannot be settled.

Raises:
  • ValueError – when the array is not 4-D (after removing any channel axis), or its trailing two axes are not an image plane.

  • TStackError – when a supplied n_t/n_z matches neither reading.

  • AmbiguousAxisOrderError – when strict and the order is ambiguous.

spacr.zstack.detect_z_axis(array, xy_min: int = 32, strict: bool = False) → int | None[source]

Identify which axis of a 3-D array is z.

Two rules, applied in order, both of which encode the same fact: an image plane is big and a z stack is short.

  1. If exactly one axis is smaller than xy_min while the other two are not, that axis is z. This catches (21, 512, 512) and (512, 512, 21).

  2. Otherwise, if two axes are equal and the third is smaller, the third is z. This catches (100, 512, 512), where no axis is short in absolute terms.

Anything else is ambiguous and is reported as such – (64, 64, 64) and (10, 20, 512) return None. Guessing would silently transpose the volume, so the caller must supply z_axis instead.

Parameters:
  • array – A 3-D array, or a shape tuple.

  • xy_min – Smallest side length still considered an image axis.

  • strict – Raise instead of returning None when ambiguous.

Returns:

The z axis index, or None when it cannot be determined.

Raises:
spacr.zstack.estimate_peak_bytes(volume_shape: Sequence[int], dtype=np.float32, mode: str = MODE_PROJECT, anisotropy: float = 1.0) → int[source]

Peak bytes one field needs, so a plate can be sized before it is run.

spaCR processes one field at a time and never holds a plate, so this is the number that matters. The multipliers are the live copies each mode keeps: the input volume, the segmenter’s float working copy, and the label output.

Parameters:
  • volume_shape – (Z, Y, X) or (Z, Y, X, C).

  • dtype – Image dtype.

  • mode – One of SEGMENTATION_MODES.

  • anisotropy – Used only when mode is MODE_VOLUMETRIC, where the isotropic copy is anisotropy times taller.

Returns:

Estimated peak bytes.

spacr.zstack.estimate_peak_bytes_4d(shape: Sequence[int], dtype=np.float32, z_mode: str = MODE_PROJECT, anisotropy: float = 1.0, label_dtype=np.int32) → int[source]

Peak bytes a 4-D run needs, so an acquisition can be sized before it runs.

Two terms, and the second is the one that surprises people:

  • one timepoint live – spacr.zstack.estimate_peak_bytes() for a single (Z, Y, X) volume, because iter_volumes() yields views and segment_4d() holds exactly one of them at a time;

  • every timepoint’s labels – linking cannot begin until the last timepoint is segmented, so the whole (T, Z, Y, X) label array is resident. At int32 that is 4 bytes per voxel per timepoint, and for a 41 x 21 x 2048 x 2048 acquisition it is ~14 GB against ~350 MB for the live volume – which is the number that decides whether the run fits.

Parameters:
  • shape – (T, Z, Y, X) or (T, Z, Y, X, C).

  • dtype – image dtype.

  • z_mode – one of spacr.zstack.SEGMENTATION_MODES.

  • anisotropy – used only by MODE_VOLUMETRIC.

  • label_dtype – dtype of the retained label array.

Returns:

estimated peak bytes.

Raises:

ValueError – when shape is not at least (T, Z, Y, X).

spacr.zstack.flag_truncated_t(labels_4d) → numpy.ndarray[source]

Track ids present in the first or last timepoint, and so cut off in t.

The time counterpart of spacr.zstack.flag_truncated_z(): such a track began before the acquisition did, or was still running when it ended, so its lifetime, total displacement and division count are lower bounds and it should be reported rather than quietly averaged into a survival curve.

Parameters:

labels_4d – a t-first label array; (T, Z, Y, X) or (T, Y, X).

Returns:

sorted array of truncated track ids.

spacr.zstack.flag_truncated_z(labels) → numpy.ndarray[source]

Labels that touch the first or last z plane, and are therefore cut off.

The z counterpart of seg_qc’s border-object rule: an object clipped by the end of the stack has a volume that is only a lower bound and shape statistics that describe the visible part, so it should be reported and excluded from size distributions rather than quietly averaged in.

Parameters:

labels – (Z, Y, X) label volume. A 2-D array has no z extent and returns an empty array.

Returns:

Sorted array of truncated label ids.

spacr.zstack.format_4d(result) → str[source]

Render a TStackResult or TrackResult as text.

Written to be pasted next to the numbers it describes: the axis order, the modes and the truncation counts are the three things that make a 4-D result interpretable, and none of them can be recovered from the tracks table afterwards.

Parameters:

result – a TStackResult or a TrackResult.

Returns:

a multi-line summary.

Raises:

TypeError – for anything else.

spacr.zstack.iter_volumes(array, spec: TStackSpec) → Iterator[numpy.ndarray][source]

Yield one (Z, Y, X[, C]) volume per timepoint, lazily.

Each yielded array is a view into array: nothing is copied and the 4-D acquisition is never materialised a second time. A spec with z_axis=None yields (Y, X[, C]) planes instead, which is the ordinary 2-D time series and is handled by exactly the same code.

Parameters:
  • array – the acquisition, axes as described by spec.

  • spec – the TStackSpec.

Yields:

one volume (or plane) per timepoint, in acquisition order.

Raises:
  • TStackError – when the array’s t/z lengths contradict spec.n_t or spec.n_z.

  • TAxisNotPresentError – when the array has no z axis but the spec says it should.

spacr.zstack.plan_4d_from_settings(settings) → TStackSpec | None[source]

Build a TStackSpec from a settings dict, or None when off.

Returning None is the contract that keeps the 2-D and 3-D paths bit-identical: every caller branches on spec is None and, when it is, does not touch any 4-D code at all. t_stack absent and t_stack=False are the same thing.

The z half of the spec is read from the very same keys spacr.zstack.plan_from_settings() reads, so a 4-D run and a 3-D run configured the same way segment identically. frame_interval_s falls back to the motility module’s seconds_per_frame when it is not set, rather than becoming a second source of truth for the same physical number.

Parameters:

settings – the pipeline settings dict.

Returns:

a spec, or None when 4D handling is off.

Raises:
spacr.zstack.plan_from_settings(settings) → ZStackSpec | None[source]

Build a ZStackSpec from a settings dict, or None when off.

Returning None is the contract that keeps the 2-D path bit-identical: every caller branches on spec is None and, when it is, does not touch any z code at all. z_stack absent and z_stack=False are the same thing.

Parameters:

settings – The pipeline settings dict.

Returns:

A spec, or None when 3D handling is off.

Raises:

ZStackError – when 3D is on but the settings are self-inconsistent.

spacr.zstack.project(volume, mode: str | None = 'max', z_axis: int | None = 0)[source]

Collapse the z axis of volume to a single plane.

Parameters:
  • volume – Volume with a z axis; extra trailing axes (channels) are preserved.

  • mode – 'max', 'mean', 'sum', 'best_focus' or None. None returns the volume untouched, which is what the stitch and volumetric modes want.

  • z_axis – Which axis is z; detected when None.

Returns:

The projected array, one axis smaller than the input – or the input itself when mode is None.

Raises:

ZStackError – on an unknown mode.

spacr.zstack.project_labels(labels_3d) → numpy.ndarray[source]

Collapse a (Z, Y, X) label volume to (Y, X), honestly.

Taking a maximum along z – what spacr.zstack.project() does to intensities – is meaningless for labels, because label values are arbitrary ids and the maximum picks the highest-numbered object. This instead gives each pixel the label that occupies the most planes in that column, which is the only projection that answers “which object is here?”.

It costs one pass over the volume per label present, which is why it is only ever reached when the caller has explicitly asked for project_for_tracking.

Parameters:

labels_3d – a (Z, Y, X) label volume.

Returns:

a (Y, X) label image.

spacr.zstack.relabel_volume(labels) → numpy.ndarray[source]

Renumber labels to a contiguous 1..N with no gaps, background 0.

Filtering objects out of a volume leaves holes in the numbering, and a consumer that sizes an array by labels.max() then over-allocates or indexes past the end.

Parameters:

labels – Integer label array of any shape.

Returns:

A relabelled array of the same shape and dtype.

spacr.zstack.report_3d_measurements(columns: Sequence[str]) → Dict[str, List[str]][source]

Sort a real measurement table’s columns by how 3-D treats them.

Meant to be run against the columns a 3-D run actually produced, so the answer describes that run rather than an intention.

Parameters:

columns – column names from a measurements table.

Returns:

{"same": [...], "renamed": [...], "added": [...], "absent": [...], "unknown": [...]}. "renamed" is the list to read: those columns kept their 2-D name and changed their meaning. "absent" lists the 2-D-only properties that ought to be missing; one of them turning up in columns means a 2-D property was computed on a volume, and it is reported under "renamed" so it cannot be mistaken for a normal column.

spacr.zstack.resample_isotropic(volume, anisotropy: float, z_axis: int | None = 0, order: int = 1) → numpy.ndarray[source]

Stretch the z axis by anisotropy so the voxels become cubic.

Segmenters that reason about distance – anything doing a morphological operation, a watershed or a flow field – need cubic voxels or they will treat a 5 um z step as if it were a 1 um xy step. Cellpose does this internally when do_3D=True; this function exists for every other segmenter, and for making the effect visible in tests.

Parameters:
  • volume – (Z, Y, X) volume (or with z at z_axis).

  • anisotropy – dz / dxy. 1.0 returns the input unchanged.

  • z_axis – Which axis is z.

  • order – Interpolation order; use 0 for label images.

Returns:

A z-first array with round(Z * anisotropy) planes.

spacr.zstack.resolve_anisotropy(anisotropy: float | None = None, voxel_size_um: Sequence[float] | None = None) → float[source]

Return dz / dxy, derived from the voxel size when available.

An explicit anisotropy wins. Otherwise it is computed from voxel_size_um = (dz, dy, dx) as dz / mean(dy, dx). If neither is known the run stops: there is no safe default, because assuming 1.0 claims the z step equals the xy pixel size and, when that is wrong by the usual 3-10x, merges every object along z.

Parameters:
  • anisotropy – Explicit ratio, or None.

  • voxel_size_um – (dz, dy, dx) in micrometres, or None.

Returns:

The anisotropy as a float.

Raises:
spacr.zstack.resolve_axis_order(array, axis_order: str | None = None, t_axis: int | None = None, z_axis: int | None = None, n_t: int | None = None, n_z: int | None = None, channel_axis: int | None = None, xy_min: int = 32) → AxisOrder[source]

Return the AxisOrder for array, or explain why it cannot.

An explicit axis_order name wins, then explicit t_axis/z_axis indices, then detect_axes() in strict mode.

Parameters:
  • array – a 4-D array (or a shape tuple).

  • axis_order – 'TZYX' or 'ZTYX'; case-insensitive.

  • t_axis – index of the time axis, if known.

  • z_axis – index of the z axis, if known.

  • n_t – number of timepoints, used as a disambiguating hint.

  • n_z – number of z planes, used as a disambiguating hint.

  • channel_axis – index of a channel axis to ignore, if present.

  • xy_min – smallest side length still considered an image axis.

Returns:

the resolved AxisOrder.

Raises:
spacr.zstack.restore_anisotropic(volume, n_z: int, order: int = 0) → numpy.ndarray[source]

Undo resample_isotropic(), back to n_z planes.

Parameters:
  • volume – z-first array to shrink.

  • n_z – Plane count of the original acquisition.

  • order – Interpolation order; 0 (nearest) keeps label values intact and is the default because this is normally called on masks.

Returns:

A z-first array with exactly n_z planes.

spacr.zstack.segment_3d(volume, segment_fn: Callable[..., Any], mode: str = MODE_PROJECT, stitch_threshold: float = 0.25, anisotropy: float | None = None, voxel_size_um: Sequence[float] | None = None, projection: str | None = 'max', z_axis: int | None = 0, resample_to_isotropic: bool = False) → ZStackResult[source]

Segment a volume in the requested mode and record how it was done.

segment_fn is the only model-aware part and is supplied by the caller. It is called as segment_fn(array, **kwargs) and must return a label array matching array’s spatial shape. The kwargs it receives depend on the mode, mirroring what Cellpose accepts:

  • MODE_PROJECT – segment_fn(plane_2d), no kwargs.

  • MODE_STITCH – segment_fn(volume, stitch=True); the result may be per-plane labels, which are then linked by stitch_planes(). Anisotropy is not passed, because neither this code nor Cellpose uses it without do_3D; supplying it is recorded as a no-op in notes.

  • MODE_VOLUMETRIC – segment_fn(volume, do_3D=True, anisotropy=..., z_axis=0), or, when resample_to_isotropic, the volume is stretched first and anisotropy=1.0 is passed instead.

n_z == 1 overrides everything: the single plane goes to segment_fn on its own and a 2-D mask comes back, identical to the ordinary 2-D path.

Parameters:
  • volume – The image volume, z at z_axis.

  • segment_fn – Callable performing the actual segmentation.

  • mode – One of SEGMENTATION_MODES.

  • stitch_threshold – IoU floor used by MODE_STITCH.

  • anisotropy – dz / dxy; required by MODE_VOLUMETRIC.

  • voxel_size_um – (dz, dy, dx), used to derive anisotropy.

  • projection – Reducer for MODE_PROJECT.

  • z_axis – Which axis of volume is z.

  • resample_to_isotropic – Stretch z before segmenting rather than delegating anisotropy handling to segment_fn.

Returns:

A ZStackResult.

Raises:
spacr.zstack.segment_4d(array, spec: TStackSpec, segment_fn: Callable[..., Any], verbose: bool = False) → TStackResult[source]

Segment every timepoint independently, in 3-D, and stack the results.

Each timepoint goes to spacr.zstack.segment_3d() with this spec’s z settings, so the per-timepoint behaviour is identical to a 3-D run and the two cannot drift apart. Segmentation is deliberately per-timepoint and independent: linking is a separate decision made by track_4d(), and fusing the two is what makes a tracker’s mistakes indistinguishable from a segmenter’s.

Two degenerate cases are not degenerate at all, and both are exact:

  • n_t == 1 – the result is the 3-D result. labels[0] is byte-identical to what spacr.zstack.segment_3d() returns for that volume.

  • n_z == 1 – every timepoint short-circuits to MODE_SINGLE_PLANE, so labels is (T, Y, X) and each frame is byte-identical to segment_fn(plane). That is the ordinary 2-D path.

Parameters:
  • array – the acquisition, axes as described by spec.

  • spec – the TStackSpec.

  • segment_fn – the segment_fn contract of spacr.zstack.segment_3d(); see that function for the kwargs it receives in each mode.

  • verbose – print each timepoint’s z notes as it is segmented.

Returns:

a TStackResult whose labels are t-first.

Raises:

TStackError – on an inconsistent spec (see iter_volumes()).

spacr.zstack.stitch_planes(masks_2d_stack, iou_threshold: float = 0.25) → numpy.ndarray[source]

Link per-plane 2-D labels into 3-D objects by IoU between neighbours.

Each plane is compared with the plane below it, and the pairing is greedy and one-to-one: pairs are considered in descending IoU and a label may take part in at most one link. That is what stops two genuinely different objects that merely happen to overlap the same object in xy from being fused – only the better match inherits the label, the other starts a new one. Any pair below iou_threshold is not linked at all.

Unlike cellpose.utils.stitch3D, new labels are always drawn from a single monotonically increasing counter. Cellpose resets its counter after an empty plane, so [objects] [empty] [objects] there silently reuses label ids and merges unrelated objects; here an empty plane links nothing.

Parameters:
  • masks_2d_stack – (Z, Y, X) array, or a sequence of 2-D arrays, of per-plane label images whose label values need not be unique across planes.

  • iou_threshold – Minimum IoU for two labels to be the same object.

Returns:

A (Z, Y, X) array of contiguously numbered 3-D labels.

Raises:

ZStackError – when iou_threshold is outside [0, 1].

spacr.zstack.track_4d(labels_or_result, spec: TStackSpec | None = None, backend: str | None = None, link_threshold: float | None = None, max_displacement_px: float | None = None, max_displacement_um: float | None = None, anisotropy: float | None = None, project_for_tracking: bool | None = None, memory: int = 0) → TrackResult[source]

Link objects across time, in 3-D, and renumber them by track.

The input is t-first labels – (T, Z, Y, X) or (T, Y, X) – which is exactly what segment_4d() returns, or a TStackResult itself. There is no axis detection here on purpose: by this point the order is a decision that has already been made and recorded, and re-deriving it would be a second chance to get it wrong.

Handing a volume to a backend that cannot link volumes raises TrackerIsTwoDError naming the backend and saying whether the limit is the library’s or spaCR’s; it does not project. project_for_tracking turns the projection on explicitly, and then the result records that z was destroyed, because two objects stacked in z become one object in the projection and no downstream number can tell that it happened.

Parameters:
  • labels_or_result – t-first label array, or a TStackResult.

  • spec – the TStackSpec; taken from the result when it carries one, and defaulted otherwise.

  • backend – overrides spec.track_backend.

  • link_threshold – overrides spec.link_threshold (IoU backend).

  • max_displacement_px – overrides the spec’s gate, in xy pixels.

  • max_displacement_um – overrides the spec’s gate, in micrometres.

  • anisotropy – overrides spec.anisotropy.

  • project_for_tracking – overrides spec.project_for_tracking.

  • memory – frames a track may vanish for; supported by the trackpy backend only, and 0 everywhere else – the built-in linkers compare consecutive timepoints and nothing else, so a missed detection ends a track and starts a new one.

Returns:

a TrackResult.

Raises:
spacr.zstack.volume_stats(labels, voxel_size: Sequence[float] | None = None)[source]

Per-object volume, surface, z extent and truncation flag.

Every column’s unit is in VOLUME_STATS_UNITS, and the names carry the unit too, because a 3-D run produces voxel counts where a 2-D run produces px^2 areas. Writing one into a column named for the other is the single easiest way to silently corrupt a screen.

With anisotropic voxels the physical numbers differ from the voxel counts by more than a constant: the surface area weights each face by the area of the face, so a z-facing face (dy*dx) and an x-facing face (dz*dy) contribute differently. Passing voxel_size=None gives the voxel-count columns only.

Parameters:
  • labels – (Z, Y, X) label volume.

  • voxel_size – (dz, dy, dx) in micrometres, or None to skip the physical columns.

Returns:

A pandas.DataFrame, one row per label, sorted by label.

Raises:

ValueError – when labels is not 3-D.

spacr.zstack.volume_tracks(labels_4d, spec: TStackSpec | None = None)[source]

One row per object per timepoint: where it is, how big, how truncated.

The first five columns are frame, track_id, original_label, x, y – the same names, order and meanings that spacr.timelapse._relabelled_stack_to_tracks_df already emits from a relabelled stack, so the track visualiser and the motility assay consume this table unchanged. Everything after them is new.

original_label equals track_id, as it does in the existing table: after linking, the label value in the stack is the track id.

Size is reported in exactly one column, never two: volume_voxels for a (T, Z, Y, X) stack and area_px2 for a (T, Y, X) one. They are different quantities and writing one into the other’s column is the single easiest way to corrupt a screen – the same point spacr.zstack.VOLUME_STATS_UNITS exists to make. Micrometre and second columns appear only when the voxel size and frame interval are known.

Parameters:
  • labels_4d – t-first labels whose values are track ids.

  • spec – the TStackSpec; supplies the voxel size and frame interval when known.

Returns:

a pandas.DataFrame sorted by track_id then frame.

Raises:

TStackError – when the array is not t-first 3-D or 4-D.

Nested helpers

detect_axes._order(t_axis: int, z_axis: int, source: str) → AxisOrder

Build one axis-order candidate with captured image axes.

Parameters:
  • t_axis – proposed time-axis index.

  • z_axis – proposed depth-axis index.

  • source – evidence label explaining how the proposal was chosen.

Returns:

an axis order combining the proposal with the captured Y, X, and normalized optional channel axes.

spacr/zstack.py:1672