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.
anisotropyhere always meansdz / 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()raisesUnknownAnisotropyErrorrather 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, andZStackResultrecords 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 reasoningspacr.seg_qcalready established for xy border objects (seeseg_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 == 1short-circuits toMODE_SINGLE_PLANE, which callssegment_fnon 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¶
It could not be established which axis is t and which is z. |
|
The z axis could not be identified and was not supplied. |
|
The 4D settings are on but the array that arrived has no t (or no z). |
|
Base class for every 4-D configuration problem. |
|
A backend that cannot link volumes was asked to link volumes. |
|
Volumetric segmentation was asked for without a z/xy voxel ratio. |
|
A distance-based backend was asked for without a displacement gate. |
|
The 3D settings are on but the array that arrived has no z axis. |
|
Base class for every z-stack configuration problem. |
Classes¶
Which axis of the incoming array is which. |
|
The labels a 4-D run produced, plus how it produced them. |
|
Everything the 4-D plumbing needs to know about one acquisition. |
|
What one tracking backend can and cannot be driven to do here. |
|
The outcome of linking a 4-D label array across time. |
|
The labels a z-aware run produced, plus how it produced them. |
|
Everything the z plumbing needs to know about one acquisition. |
Functions¶
|
Return a view of |
|
What one measurement column means in a 3-D run. |
|
Work out which leading axis is t and which is z, or refuse to. |
|
Identify which axis of a 3-D array is z. |
|
Peak bytes one field needs, so a plate can be sized before it is run. |
|
Peak bytes a 4-D run needs, so an acquisition can be sized before it runs. |
|
Track ids present in the first or last timepoint, and so cut off in t. |
|
Labels that touch the first or last z plane, and are therefore cut off. |
|
Render a |
|
Yield one |
|
Build a |
|
Build a |
|
Collapse the z axis of |
|
Collapse a |
|
Renumber labels to a contiguous |
|
Sort a real measurement table's columns by how 3-D treats them. |
|
Stretch the z axis by |
|
Return |
|
Return the |
|
Undo |
|
Segment a volume in the requested mode and record how it was done. |
|
Segment every timepoint independently, in 3-D, and stack the results. |
|
Link per-plane 2-D labels into 3-D objects by IoU between neighbours. |
|
Link objects across time, in 3-D, and renumber them by track. |
|
Per-object volume, surface, z extent and truncation flag. |
|
One row per object per timepoint: where it is, how big, how truncated. |
Module Contents¶
- exception spacr.zstack.AmbiguousAxisOrderError[source]¶
Bases:
TStackErrorIt 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:
ZStackErrorThe 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:
TStackErrorThe 4D settings are on but the array that arrived has no t (or no z).
Almost always because
spacr.iocollapsed 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.ConfigurationErrorBase 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:
TStackErrorA 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:
ZStackErrorVolumetric 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:
TStackErrorA 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:
ZStackErrorThe 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.ConfigurationErrorBase 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
Nonefor 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.
- 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)forprojectand for a single-plane acquisition. Label values are per-timepoint and carry no identity across t untiltrack_4d()has run.z_results – the
spacr.zstack.ZStackResultfor 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.
- 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,projectionandstitch_thresholdare handed straight tospacr.zstackviato_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
Nonefor a flat time series.n_t – number of timepoints, or
Nonewhen not yet known.n_z – number of z planes, or
Nonewhen not yet known.anisotropy –
dz / dxy.Nonemeans “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 atime_scolumn – no linking decision depends on it.voxel_size_um –
(dz, dy, dx)in micrometres, orNone.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 fromstitch_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 withmax_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.ZStackSpecfor one timepoint of this run.- Returns:
a z spec carrying this spec’s z settings verbatim.
- property backend: TrackBackend[source]¶
The
TrackBackendrecord fortrack_backend.
- 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_backendspelling.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
Nonewhen 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.
modeis 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_PLANEeven though that is not selectable.anisotropy – The value used, or
Nonewhen 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.
- 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
Noneto detect it withdetect_z_axis().n_z – Number of planes, or
Nonewhen not yet known.anisotropy –
dz / dxy.Nonemeans “not known”, which is fatal forMODE_VOLUMETRICand merely recorded otherwise.voxel_size_um –
(dz, dy, dx)in micrometres, orNone.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
anisotropyto 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
anisotropyis 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 ifstitch_thresholdis 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
arraywith t at axis 0 and z at axis 1.A view, not a copy:
np.moveaxisnever copies, which is what keeps a 4-D acquisition from being materialised twice.- Parameters:
array – the acquisition, axes as described by
spec.spec – the
TStackSpecnamingt_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_3Dentry, or an entry withkind="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_TZYXandAXIS_ORDER_ZTYXagree on it, so there is nothing to choose between, and it is checked – if either of them is shorter thanxy_minthe array is not(..., Y, X)at all and that is an error rather than a silent transposition.The two leading axes are
tandzin an order the shape cannot reveal. This function settles it only from evidence:n_tand/orn_zgiven, 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
Nonewhen ambiguous.
- Returns:
an
AxisOrder, orNonewhen 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_zmatches neither reading.AmbiguousAxisOrderError – when
strictand 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.
If exactly one axis is smaller than
xy_minwhile the other two are not, that axis is z. This catches(21, 512, 512)and(512, 512, 21).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)returnNone. Guessing would silently transpose the volume, so the caller must supplyz_axisinstead.- 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
Nonewhen ambiguous.
- Returns:
The z axis index, or
Nonewhen it cannot be determined.- Raises:
ValueError – when the input is not 3-D.
AmbiguousZAxisError – when
strictand the axis is ambiguous.
- 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
modeisMODE_VOLUMETRIC, where the isotropic copy isanisotropytimes 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, becauseiter_volumes()yields views andsegment_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
shapeis 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
TStackResultorTrackResultas 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
TStackResultor aTrackResult.- 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 withz_axis=Noneyields(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_torspec.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
TStackSpecfrom a settings dict, orNonewhen off.Returning
Noneis the contract that keeps the 2-D and 3-D paths bit-identical: every caller branches onspec is Noneand, when it is, does not touch any 4-D code at all.t_stackabsent andt_stack=Falseare 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_sfalls back to the motility module’sseconds_per_framewhen 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
Nonewhen 4D handling is off.- Raises:
TStackError – when 4D is on but the settings are self-inconsistent.
spacr.zstack.ZStackError – when the z half is self-inconsistent.
- spacr.zstack.plan_from_settings(settings) ZStackSpec | None[source]¶
Build a
ZStackSpecfrom a settings dict, orNonewhen off.Returning
Noneis the contract that keeps the 2-D path bit-identical: every caller branches onspec is Noneand, when it is, does not touch any z code at all.z_stackabsent andz_stack=Falseare the same thing.- Parameters:
settings – The pipeline settings dict.
- Returns:
A spec, or
Nonewhen 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
volumeto a single plane.- Parameters:
volume – Volume with a z axis; extra trailing axes (channels) are preserved.
mode –
'max','mean','sum','best_focus'orNone.Nonereturns 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
modeisNone.- 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..Nwith 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 incolumnsmeans 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
anisotropyso 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 atz_axis).anisotropy –
dz / dxy. 1.0 returns the input unchanged.z_axis – Which axis is z.
order – Interpolation order; use
0for 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
anisotropywins. Otherwise it is computed fromvoxel_size_um = (dz, dy, dx)asdz / 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, orNone.
- Returns:
The anisotropy as a float.
- Raises:
UnknownAnisotropyError – when neither input determines it.
ZStackError – when the values given are not usable.
- 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
AxisOrderforarray, or explain why it cannot.An explicit
axis_ordername wins, then explicitt_axis/z_axisindices, thendetect_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:
TStackError – on an unknown
axis_ordername, or when the explicit indices contradict the array.AmbiguousAxisOrderError – when nothing settles the order.
- spacr.zstack.restore_anisotropic(volume, n_z: int, order: int = 0) numpy.ndarray[source]¶
Undo
resample_isotropic(), back ton_zplanes.- 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_zplanes.
- 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_fnis the only model-aware part and is supplied by the caller. It is called assegment_fn(array, **kwargs)and must return a label array matchingarray’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 bystitch_planes(). Anisotropy is not passed, because neither this code nor Cellpose uses it withoutdo_3D; supplying it is recorded as a no-op innotes.MODE_VOLUMETRIC–segment_fn(volume, do_3D=True, anisotropy=..., z_axis=0), or, whenresample_to_isotropic, the volume is stretched first andanisotropy=1.0is passed instead.
n_z == 1overrides everything: the single plane goes tosegment_fnon 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 byMODE_VOLUMETRIC.voxel_size_um –
(dz, dy, dx), used to deriveanisotropy.projection – Reducer for
MODE_PROJECT.z_axis – Which axis of
volumeis z.resample_to_isotropic – Stretch z before segmenting rather than delegating anisotropy handling to
segment_fn.
- Returns:
A
ZStackResult.- Raises:
ZStackError – on an unknown mode.
UnknownAnisotropyError – for
MODE_VOLUMETRICwithout a known anisotropy.
- 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 bytrack_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 whatspacr.zstack.segment_3d()returns for that volume.n_z == 1– every timepoint short-circuits toMODE_SINGLE_PLANE, solabelsis(T, Y, X)and each frame is byte-identical tosegment_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_fncontract ofspacr.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
TStackResultwhose 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_thresholdis 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_thresholdis 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 whatsegment_4d()returns, or aTStackResultitself. 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
TrackerIsTwoDErrornaming the backend and saying whether the limit is the library’s or spaCR’s; it does not project.project_for_trackingturns 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:
TrackerIsTwoDError – when the backend cannot link volumes and the projection was not explicitly asked for.
UnknownDisplacementError – when a distance-based backend has no gate.
spacr.zstack.UnknownAnisotropyError – when a distance-based backend has no anisotropy and a volume to link.
TStackError – on an unknown backend or a non-t-first input.
- 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. Passingvoxel_size=Nonegives the voxel-count columns only.- Parameters:
labels –
(Z, Y, X)label volume.voxel_size –
(dz, dy, dx)in micrometres, orNoneto skip the physical columns.
- Returns:
A
pandas.DataFrame, one row per label, sorted by label.- Raises:
ValueError – when
labelsis 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 thatspacr.timelapse._relabelled_stack_to_tracks_dfalready emits from a relabelled stack, so the track visualiser and the motility assay consume this table unchanged. Everything after them is new.original_labelequalstrack_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_voxelsfor a(T, Z, Y, X)stack andarea_px2for 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 pointspacr.zstack.VOLUME_STATS_UNITSexists 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.DataFramesorted bytrack_idthenframe.- 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