spacr.diameter¶
Propose Cellpose diameter values from blob statistics, without Cellpose.
Why this exists¶
diameter has the largest effect on the masks of all the Cellpose settings
spaCR exposes, and it is often guessed. Under Cellpose 4 (cpsam) the
constructor argument diam_mean= is ignored, but
CellposeModel.eval(diameter=...) is still applied: it rescales the input
by 30.0 / diameter so objects land near the network’s ~30 px working size.
A value two-fold off propagates into every downstream mask, count and
measurement. This module computes a starting value from image statistics in a
few seconds.
Design constraint: this module must be usable before committing to a
segmentation run, which means it may not cost what a segmentation run costs.
It therefore imports no torch and no cellpose, which
tests/test_diameter_estimator.py checks. It reuses
spacr.validate for filename-metadata parsing, which is the other
deliberately dependency-light module in the package (spacr.utils, where
_get_regex and _extract_filename_metadata live, imports torch and
cellpose at module scope and so cannot be touched from here). The regexes in
spacr.validate.METADATA_REGEXES mirror spacr.utils._get_regex, and the
channel ordering used below mirrors
spacr.io._rename_and_organize_image_files, which concatenates one plane per
distinct chanID in sorted order — so channel index i is the i-th
sorted chanID, exactly as cell_channel and friends mean it.
Method¶
For each requested object channel, on each sampled field:
Flatten. Denoise with a 1 px Gaussian, then subtract a heavily smoothed copy (sigma = max(32, min(H, W) / 4)) to remove illumination gradients. The sigma is deliberately far larger than any plausible object so that flattening removes vignetting without removing the objects — the opposite trade-off (a tight rolling ball) shrinks what it is trying to measure.
Reject noise. Compare the structural amplitude of the flattened plane (p99 - p30) against the pixel-level noise scale, measured as
1.4826 * MAD(img - gaussian(img, 1))on the raw plane. A pure-noise plane scores below 1; a plane with real objects scores in the tens. Belowmin_snrthe field is discarded rather than thresholded, because Otsu returns a threshold even for pure noise, and the resulting size looks valid.Threshold and label. Otsu, fill holes, label, drop components that touch the image border (truncated, so their size is too small) and components that are absurd (equivalent diameter below
min_object_diameter, or area abovemax_object_fractionof the field). Sizes are equivalent diameters,2 * sqrt(area / pi).Drop specks, then take the median. A textured or punctate stain (a mitochondrial cell stain, a parasite marker) thresholds into a few whole objects plus hundreds of 4-10 px specks, and a plain median counts every speck as an object: on spaCR’s own test plate that put the cell at 8 px and the pathogen at 11 px against 175 px and 32 px in the curated masks, while the nucleus, whose stain is solid, came out right at 91 px. So the typical object is first located by the stained area it carries (the area-weighted median, which specks cannot move because they hold almost no area), everything narrower than
SPECK_FRACTIONof it is dropped, and the characteristic size is the median of what remains. Small components that together holdSPECK_MAX_AREA_SHAREof the stained area or more are a second population, not specks, and stay.Cross-check by distance transform. Step 3 has one dominant failure mode: a confluent monolayer fuses into a single component, that component touches the border and is dropped, and the estimate is then computed from whatever debris survived — biased low, and silently. So the Euclidean distance transform of the (unfilled) foreground is computed as well, its local maxima are taken as one seed per object (two passes: a coarse pass sets the suppression radius for the refined pass), and a watershed on
-EDTsplits the fused foreground back into objects whose equivalent diameters are measured the same way.
Both estimates are always computed, and one of them is chosen as follows. The
watershed estimate is taken when the transform resolved something and either
the threshold path kept nothing at all, or the field is dense enough for fusion
to be the explanation (foreground at or above fused_fraction) and the
transform resolves at least five times as many objects. A count disagreement
alone is therefore never fusion — a hollow, membrane-only object is one correct
component by area but shatters into dozens of arcs under the distance
transform, so a bare ratio test would discard the right answer in favour of the
wall thickness. A high foreground fraction alone is not fusion either — a dense
but well-separated field can reach 30% foreground and still be measured
correctly by thresholding — so on its own it only brings the confidence down
while the threshold estimate stands. An empty threshold result is taken as
fusion on its own, with no density check, because no threshold estimate is left
to stand; if the transform resolved nothing either, no estimate is returned for
that channel. When fusion is not declared and the two estimates disagree by
more than 1.5-fold, the confidence is downgraded and the note says so, because
that disagreement indicates that neither number is reliable on its own.
Public API¶
DiameterEstimateOne proposal, with its plausible range, provenance and confidence.
estimate_diameters(src, channels, n_fields=5, ...){object_type: DiameterEstimate}.format_estimates(estimates)Human-readable block, ready to print.
channels_from_settings(settings)Pull
cell_channel/nucleus_channel/ … out of a settings dict.
Where a user meets this¶
spacr.qt.prerun.DiameterPanel sits above the Run row on the Mask
screen: it reads the channels off the settings form with
channels_from_settings(), calls estimate_diameters() on a worker
thread, and shows one row per object type carrying the proposal and its
evidence — the 10th-90th percentile range, how many objects it was pooled
from, how many fields contributed, the method and the confidence. Nothing
is written into
<object>_diameter until the user presses Use, and
DiameterEstimate.usable is checked first, so the NaN an unmeasurable
channel returns cannot reach a settings field.
Classes¶
One proposed diameter with its range, counts, method and confidence. |
Functions¶
|
Extract |
|
Propose a Cellpose |
|
Render |
Module Contents¶
- class spacr.diameter.DiameterEstimate[source]¶
One proposed diameter with its range, counts, method and confidence.
- Parameters:
object_type –
'cell','nucleus','pathogen'or'organelle'.diameter – proposed value in pixels — the median object diameter across the sampled fields.
float('nan')when nothing usable was found; seeusable. It is never a fabricated fallback.low – 10th percentile of the measured object diameters, in pixels.
high – 90th percentile of the measured object diameters, in pixels.
n_objects – how many objects the estimate was pooled from.
n_fields – how many fields actually contributed (which can be fewer than requested).
method – which measurement produced
diameter—'threshold_otsu','watershed_edt', or'none'.confidence –
'high','medium'or'low'.note – why the number is what it is, which confidence downgrades applied, and what to check if it looks wrong.
- spacr.diameter.channels_from_settings(settings: Dict[str, Any]) Dict[str, int][source]¶
Extract
{object_type: channel_index}from a spaCR settings dict.Reads
cell_channel,nucleus_channel,pathogen_channelandorganelle_channel; object types whose channel is None or unparseable are absent from the result.- Parameters:
settings – a spaCR settings dict (or anything dict-like).
- Returns:
mapping suitable for
estimate_diameters().
- spacr.diameter.estimate_diameters(src: Any, channels: Dict[str, Any], n_fields: int = 5, *, metadata_type: Any = 'cellvoyager', custom_regex: Any = None, random_state: int | None = None, max_pixels: int = 2250000, min_snr: float = 4.0, fused_fraction: float = 0.35, min_object_diameter: float = 4.0, max_object_fraction: float = 0.25, verbose: bool = False) Dict[str, DiameterEstimate][source]¶
Propose a Cellpose
diameterper object type from blob statistics.Samples
n_fieldsfields spread acrosssrc(never the first N — see_sample_indices()), measures characteristic object size in each requested channel by thresholding and by a distance-transform watershed, and pools the two into one proposal per object type. Cellpose is never loaded; neither is torch.- Parameters:
src – a source folder, or a list of them. Merged
stack//merged/.npyarrays are used when present, otherwise raw acquisition files are grouped by the metadata regex.channels –
{object_type: channel_index}, e.g.{'cell': 2, 'nucleus': 0}. Values that are not integers are skipped.channels_from_settings()builds this from a settings dict.n_fields – how many fields to sample. More is slower and steadier.
metadata_type –
'cellvoyager'/'cq1'/'auto'/'custom'; only consulted when raw files must be parsed.custom_regex – named-group regex used when
metadata_typeis'custom'.random_state –
None(default) samples on an even stride, which is deterministic; an int draws a reproducible random sample instead.max_pixels – fields larger than this are centre-cropped, which loses peripheral objects but does not bias their measured size.
min_snr – minimum ratio of structural amplitude to pixel noise for a field to be measured at all. Below it the field is discarded rather than thresholded, so a background-only channel yields no number.
fused_fraction – foreground fraction at or above which the field is called confluent, which downgrades the confidence and is recorded in the note. It does not by itself switch the measurement method; the distance-transform estimate takes over only when it resolves substantially more objects than plain labelling did.
min_object_diameter – components thinner than this many pixels are discarded as debris.
max_object_fraction – components covering more than this fraction of a field are discarded as fused blobs rather than measured.
verbose – print each estimate as it is produced.
- Returns:
{object_type: DiameterEstimate}for every object type given an integer channel. Entries whoseDiameterEstimate.usableis False carry NaN — check it before writing the value into settings.
- spacr.diameter.format_estimates(estimates: Dict[str, DiameterEstimate]) str[source]¶
Render
estimate_diameters()output as a printable block.- Parameters:
estimates – the mapping returned by
estimate_diameters().- Returns:
a multi-line string: one table row per object type carrying the proposed diameter, its plausible range, object and field counts, confidence and method, followed by the note behind each row.