spacr.crops¶
On-demand single-object crops cut straight out of merged/*.npy.
Background¶
spacr.measure.measure_crop() writes a per-object PNG for every object it
measures (<root>/data/.../{cell,nucleus,pathogen,cytoplasm}_png/) and records
the paths in the png_list table of measurements/measurements.db. Every
downstream consumer – the annotation GUIs, the classification datasets, the
image UMAP – reads those PNGs. That costs disk, has to be regenerated whenever
a crop setting changes, and goes stale silently.
The merged/ array already contains everything needed to cut the same crop on
demand: the intensity planes and the integer label-mask planes. This module is
that alternative source. It is deliberately additive – PngCropSource
wraps the existing behaviour unchanged, MergedCropSource is the new one,
and resolve_crop_source() picks between them and says which it picked.
Merged array layout¶
spacr.io._load_and_concatenate_arrays builds each merged/<fov>.npy as:
(H, W, n_intensity_channels + n_mask_planes)
The intensity channels come first (the subset selected by settings['channels']
at preprocessing time), then one uint16 label-mask plane per segmented object
class, always in the order cell, nucleus, pathogen, organelle – each present
only if that class was segmented. settings['cell_mask_dim'] /
nucleus_mask_dim / pathogen_mask_dim / organelle_mask_dim record the
resulting plane indices; spaCR’s default four-channel layout is
{cell: 4, nucleus: 5, pathogen: 6, organelle: 7} (DEFAULT_MASK_DIMS).
There is no cytoplasm plane on disk: measure_crop derives cytoplasm as
“cell minus nucleus/pathogen/organelle” in memory and never saves it back. This
module derives it the same way (see MergedField.mask_plane()).
Fidelity¶
extract_crop() reproduces the PNG path in spacr.measure._measure_crop_core
step for step: same channel selection (png_dims), same region definition
(object mask, optionally replaced by its padded bounding box, optionally
dilated), same _crop_center centering/padding, same normalize_to_dtype
percentile normalisation, same dtype. png_view() turns that array into
what a consumer sees after the PNG round trip, and read_crop_png() reads
a crop PNG back into the same thing – the two are the two halves of one
contract, and tests/test_crops.py asserts they agree.
Crop PNG format¶
This module is also the authority on what a crop PNG on disk means –
see the “Crop PNG format” section below. In short: the current format is 3
(“declared_rgb”, CROP_FORMAT_CURRENT), whose red, green and blue slots
hold exactly the source channels settings['png_channel_mapping'] names;
format 1 (“legacy”, unmarked) holds the same bytes for the default mapping and
is returned untouched; format 2, written for eleven days in 2026, is the one
that is reversed, and read_crop_png() is what reverses it back. A folder
says which format it is via a .spacr_crop_format.json sidecar, and an
unmarked folder is format 1.
Dependencies¶
numpy, and the standard library (plus PIL only inside read_crop_png()
and cv2 only inside migrate_crop_folder(), both imported lazily). No
torch, no cellpose, no scipy, no skimage – importing this module must stay
cheap enough for a GUI thumbnail path, and spacr.measure imports it for
the writer helpers, so it must not import spacr back.
Exceptions¶
The |
|
Base class for every failure raised while cutting an on-demand crop. |
|
The sidecar and the database disagree about a folder's crop format. |
|
The requested object label is not present in the mask plane. |
|
The array has no plane for the requested object type. |
|
The requested |
|
A requested mask plane disagrees with the merged-folder manifest. |
Classes¶
A source of single-object crops. |
|
Everything needed to reproduce one crop, byte for byte. |
|
The new one: cut the crop out of |
|
A memory-mapped |
|
What |
|
The existing behaviour: read the pre-generated PNG named by the row. |
Functions¶
|
Assemble the crop planes in file order -- red, green, blue. |
|
Forget every cached folder marker (and every "already stamped" folder). |
|
Drop every cached |
|
Return the crop format that applies to |
|
Return the crop format of one crop PNG. |
|
Read the |
|
Build a |
|
Cut one object out of a merged array, reproducing the PNG path exactly. |
|
Cut many objects out of one merged array, opening the file once. |
|
Return every |
|
Map a legacy-trained model's |
|
Return what a naive PIL read of a legacy crop PNG gives back. |
|
Return |
|
Repair reversed format-2 crops and stamp the folder. Idempotent. |
|
Run |
|
Convert |
|
Return a |
|
Translate a legacy |
|
Return what a consumer sees after the crop has made the PNG round trip. |
|
Return the parsed |
|
Read a crop PNG and return it in the corrected order, as 8-bit RGB. |
|
Return the crop format recorded in the database, or |
|
Read and validate a merged folder's optional plane-layout manifest. |
|
Apply a plane manifest and reject explicit conflicting mask indices. |
|
Pick the crop source for a run, and record which one it picked. |
|
Return the |
|
Ensure that |
|
Record the crop format on |
|
Return |
|
Write |
Module Contents¶
- exception spacr.crops.CorruptMergedFile[source]¶
Bases:
CropErrorThe
.npyexists but cannot be read as an(H, W, C)array.Initialize self. See help(type(self)) for accurate signature.
- exception spacr.crops.CropError[source]¶
Bases:
RuntimeErrorBase class for every failure raised while cutting an on-demand crop.
Initialize self. See help(type(self)) for accurate signature.
- exception spacr.crops.CropFormatConflict[source]¶
Bases:
CropErrorThe sidecar and the database disagree about a folder’s crop format.
Initialize self. See help(type(self)) for accurate signature.
- exception spacr.crops.LabelMissing[source]¶
Bases:
CropErrorThe requested object label is not present in the mask plane.
Initialize self. See help(type(self)) for accurate signature.
- exception spacr.crops.MaskPlaneMissing[source]¶
Bases:
CropErrorThe array has no plane for the requested object type.
Initialize self. See help(type(self)) for accurate signature.
- exception spacr.crops.MergedFileMissing[source]¶
Bases:
CropError,FileNotFoundErrorThe requested
merged/*.npydoes not exist.Initialize self. See help(type(self)) for accurate signature.
- exception spacr.crops.PlaneLayoutConflict[source]¶
Bases:
CropErrorA requested mask plane disagrees with the merged-folder manifest.
Initialize self. See help(type(self)) for accurate signature.
- class spacr.crops.CropSource[source]¶
A source of single-object crops.
Implementations return a
(H, W, 3)uint8 RGB array fromget(), so a consumer can swap one for the other without changing anything downstream.- abstract get(row: Any) numpy.ndarray[source]¶
Return the crop for
rowas a(H, W, 3)uint8 RGB array.- Parameters:
row – one measurement row – a mapping, a pandas
Series, or any object carrying the fields as attributes. Which fields are required is the implementation’s business, not the interface’s:PngCropSourceneedspng_path(or accepts a bare path string),MergedCropSourceneeds the merged file andobject_label.
- get_image(row: Any)[source]¶
Return the crop for
rowas a PILImagein RGB mode.- Parameters:
row – as for
get(). PIL is imported inside this method, so a consumer that only ever wants arrays never pays for it.
- get_many(rows: Iterable[Any]) List[numpy.ndarray][source]¶
Return crops for many rows. Overridden by sources that can batch.
- Parameters:
rows – rows to crop. The result has one entry per row in the same order, so a caller can zip the two. This base implementation is a plain loop over
get()and therefore raises on the first row it cannot crop, and so does the one override shipped here, onMergedCropSource. Successful calls therefore never containNone.
- class spacr.crops.CropSpec[source]¶
Everything needed to reproduce one crop, byte for byte.
The field names mirror the
measure_cropsettings that drive the PNG path, so a spec can be built straight from a saved settings snapshot (crop_spec_from_settings()).- Parameters:
merged_path – path to the
merged/<fov>.npythe object lives in.object_type –
'cell'|'nucleus'|'pathogen'|'organelle'|'cytoplasm'– selects the mask plane (measure_crop’scrop_mode).label – the object’s integer label in that mask plane (
object_labelinmeasurements.db).0is background and is always an error.channels – intensity plane indices, in output order (
measure_crop’spng_dims).size –
(width, height)of the crop (measure_crop’spng_size). Note the width-first order – that is the PNG path’s convention.mask_dims – object type -> plane index.
NoneusesDEFAULT_MASK_DIMS.use_bounding_box – crop the object’s padded bounding box instead of its exact outline (
measure_crop’suse_bounding_box). The pad is hard-coded to 10 px in the PNG path;bbox_buffermirrors it.bbox_buffer – pad added around the bounding box, in pixels.
bbox – optional pre-computed
(y0, y1, x0, x1)half-open bounding box (skimageregionpropsconvention) for this label, e.g. read from a database column. When given, the mask plane is never scanned to find the object, only the window is read.dilate – dilate the region before cropping (
dialate_pngs).dilate_ratio – dilation radius as a fraction of
sqrt(area)(dialate_png_ratios).normalize –
False(the shipped default) reproduces the PNG path’s fallback of a full 0-100 percentile stretch; a(p1, p2)pair reproduces the configured stretch.normalize_by –
'png'(percentiles from the crop) or'fov'(percentiles from the whole field before cropping).
- class spacr.crops.MergedCropSource(spec: CropSpec | None = None, merged_root: str | None = None, object_type: str | None = None, reason: str = '')[source]¶
Bases:
CropSourceThe new one: cut the crop out of
merged/*.npyon demand.A row needs the merged array it came from and the object’s label. Both are already in
measurements.db:path_name(written byspacr.utils._merge_and_save_to_database()) andobject_label.prcfo/plateID/rowID/columnID/fieldIDare used only as a fallback to rebuild<merged_root>/<plate>_<well>_<field>.npy.- Parameters:
spec – the template
CropSpec; each row suppliesmerged_pathandlabel.merged_root – folder holding the
.npyfiles, used to re-anchor apath_namerecorded on another machine and for theprcfofallback.object_type – default object type when a row does not carry one.
reason – why this source was chosen (for
describe()).
Configure on-demand crops, optionally overriding the object type.
- get(row: Any) numpy.ndarray[source]¶
Return the crop as a
(H, W, 3)uint8 RGB array.Deliberately routed through
png_view(), so what a consumer gets here is identical to whatread_crop_png()returns for the same object out of the PNG folder – 16-bit narrowing included.- Parameters:
row – a measurement row;
spec_for()says which fields it has to carry. Each row is resolved and cut on its own, so useget_many()when filling a grid – it opens each merged file once for the whole batch instead of once per row.
- get_array(row: Any) numpy.ndarray[source]¶
Return the raw crop (native dtype,
spec.channelsorder).- Parameters:
row – a measurement row;
spec_for()says which fields it has to carry. What comes back is the pre-write array – the merged file’s dtype (uint16on a normal run) and as many channels as the spec selects – not 8-bit RGB. Useget()for something a viewer or a classifier can take.
- get_many(rows: Iterable[Any]) List[numpy.ndarray][source]¶
Return crops for many rows, opening each merged file only once.
- Parameters:
rows – rows to crop;
spec_for()says which fields each has to carry. The result has one entry per row in the original order, however the rows were regrouped internally – they are bucketed by merged file so each.npyis memory-mapped and label-indexed once for the whole bucket. Every spec is built up front, so one row missing its label fails the batch before any file is opened. The default fail-loud extraction policy means successful calls never containNone.
- resolve_path(row: Any) str[source]¶
Return the merged
.npypath forrow.The row-to-well conversion uses
spacr.schema, imported lazily to keep this module’s import path dependency-light.- Parameters:
row – a measurement row.
merged_pathorpath_nameis used directly, and – when that path does not exist here – retried as<merged_root>/<basename>, which is how a database written on another machine still resolves. A path that exists nowhere is returned anyway, so the failure arrives later asMergedFileMissing. With neither key the name is rebuilt fromfile_name, or else fromplateID/rowID/columnID/fieldID(all four required), andmerged_rootmust be set or this raises.
- spec_for(row: Any) CropSpec[source]¶
Return the
CropSpecdescribingrow’s crop.- Parameters:
row – a measurement row. The label is the first present of
object_label,label,cell_id,nucleus_id,pathogen_id,cytoplasm_id, and a row with none of them raisesCropError;object_type, if present, overrides the template spec’s.bbox-0..bbox-3(orbbox_0..bbox_3) are honoured only when all four are there, and are reordered out of the skimageregionpropsconvention(min_row, min_col, max_row, max_col)into the spec’s(y0, y1, x0, x1)– supplying them lets the crop skip the whole-plane label index scan. The mask plane inside that box is still read, unless the spec also setsuse_bounding_box, which skips reading the label plane altogether.
- class spacr.crops.MergedField(path: str, array=None, mask_dims: Mapping[str, int] | None = None)[source]¶
A memory-mapped
merged/<fov>.npyplus its per-plane label indices.The array is opened with
mmap_mode='r': cutting one object reads the object’s mask plane once (cached) and then only the crop window, never the whole field. A 2048x2048x5 uint16 field is 40 MB; materialising it per object would make an on-demand grid slower than the PNG folder it replaces.Only
shape,dtype,ndimand__getitem__are ever used on the underlying array, so tests can substitute a recording proxy to assert on the access pattern.- Parameters:
path – the
merged/<fov>.npyfield on disk.array – the opened array. Defaults to memory-mapping
path; pass one to substitute a proxy or an already-open handle.mask_dims – which channel of the field holds each object’s mask, as
{object: plane index}. Defaults to the layout recorded beside the file, and toDEFAULT_MASK_DIMSwhen the file records none.
- Raises:
CorruptMergedFile – when the array is not
(H, W, C).
Open or adopt a three-dimensional field and resolve its mask layout.
- label_index(object_type: str) _LabelIndex[source]¶
Return the cached
_LabelIndexforobject_type’s plane.- Parameters:
object_type – which plane to index. The cache is keyed on the plane index (
'cytoplasm'gets its own key, since it has no plane on disk), so two object types recorded at the samemask_dimshare one index. The scan happens on first use and is kept for the life of the field, which is what makes drawing many objects out of one field a single pass over the plane.
- labels(object_type: str = 'cell') List[int][source]¶
Return every non-zero label present in
object_type’s plane.- Parameters:
object_type – which plane to list; defaults to
'cell'because that is the plane every spaCR run has. Labels come back in ascending order, background (0) is never among them, and an empty list means the plane holds no objects at all – not that the plane is missing, which raises instead.
- mask_dim(object_type: str) int[source]¶
Return the plane index holding
object_type’s labels.- Parameters:
object_type – one of
MASK_PLANE_ORDER–'cell','nucleus','pathogen','organelle'.'cytoplasm'is refused rather than defaulted: it has no plane on disk, somask_plane()is the only way to get it. The index comes from this field’smask_dims, not from the array, so a settings dict that names a plane the array does not have fails here rather than silently cropping by the wrong stain.- Raises:
MaskPlaneMissing – if the type has no plane, or the recorded plane index is out of range for this array.
- mask_plane(object_type: str) numpy.ndarray[source]¶
Return the 2-D label plane for
object_typeas a real array.'cytoplasm'is derived on the fly –measure_cropcomputes it as the cell mask with every nucleus / pathogen / organelle pixel zeroed and never writes it back to the merged file.- Parameters:
object_type –
'cell'|'nucleus'|'pathogen'|'organelle'come back as a view on the memory-mapped array, so nothing is read off disk until pixels are touched.'cytoplasm'is the derived plane and costs a full pass over the field to build, so it is cached on this field and every later call is free.
- read_mask_window(object_type: str, y0: int, y1: int, x0: int, x1: int) numpy.ndarray[source]¶
Read
object_type’s label plane over[y0:y1, x0:x1], zero-padded.- Parameters:
object_type – which label plane to read;
'cytoplasm'is served from the derived (and cached) plane, every other type straight off the memory map.y0 – first row; may be negative, and the overhang comes back as zeros – which reads as background, so no label ever appears to run past the edge of the field.
y1 – one past the last row (half-open); may exceed the field height, padded as for
y0.x0 – first column, negative allowed as for
y0.x1 – one past the last column, over-wide allowed as for
y1.
- read_window(y0: int, y1: int, x0: int, x1: int, channels: Sequence[int], dtype=None) numpy.ndarray[source]¶
Read
channelsover[y0:y1, x0:x1], zero-padding outside the array.The window may run off any edge; the out-of-array part comes back as zeros, which is what the PNG path’s
np.padproduces.- Parameters:
y0 – first row of the window. May be negative – that part is padded, not clamped, so the object stays centred in the result.
y1 – one past the last row (half-open). May exceed the field height; the overhang is padded the same way.
x0 – first column, negative allowed as for
y0.x1 – one past the last column, over-wide allowed as for
y1.channels – plane indices in output order – result channel
kholds planechannels[k], and repeating an index repeats the plane. Each must satisfy0 <= c < C; negative indices are rejected rather than wrapped, so-1is an error and not “the last plane”. An empty sequence yields an(h, w, 0)array here;extract_crop()rejects it first.dtype – dtype of the returned array.
Noneusescrop_dtype, i.e. what the PNG path would have cropped in; pass one explicitly only to match an array you already hold.
- class spacr.crops.MigrationResult[source]¶
What
migrate_crop_folder()did to one folder.- Parameters:
folder – crop folder that was examined or migrated.
converted – filenames whose channel order was or would be rewritten.
skipped – filenames needing no rewrite, including already-processed or single-channel crops.
failed –
(filename, reason)pairs for crops that could not be converted.already – whether the folder was already in a format requiring no migration.
dry_run – whether the result describes planned work without writing files.
mode –
"rewrite"for pixel conversion or"mark"for recording legacy format without touching pixels.
- class spacr.crops.PngCropSource(root: str | None = None, folder: str = 'data', reason: str = '', db_path: str | None = None)[source]¶
Bases:
CropSourceThe existing behaviour: read the pre-generated PNG named by the row.
Reads go through
read_crop_png(), so a folder of legacy (format 1) crops is corrected on load and comes back in the same channel order as a new one – the caller cannot tell which it opened, which is the point.- Parameters:
root – optional experiment root used to re-anchor
png_pathvalues recorded on another machine (the same rewritespacr.utils.correct_paths()performs).folder – the anchor folder name for that rewrite.
reason – why this source was chosen (for
describe()).db_path –
measurements.dbconsulted for thecrop_formatcolumn when a folder carries no sidecar; defaults to<root>/measurements/measurements.dbwhenrootis given.
Configure reanchoring and discover an existing default database.
- get(row: Any) numpy.ndarray[source]¶
Return the PNG for
rowdecoded as a(H, W, 3)uint8 RGB array.Legacy content is converted on load, so this equals
png_view(extract_crop(...))for the same object whichever format the folder is in.- Parameters:
row – as for
resolve(). The folder’s sidecar – failing that, this source’sdb_path– is what decides whether the file’s channels are reversed on the way back, so the same row can legitimately give different pixels before and after a folder is marked or migrated.
- resolve(row: Any) str[source]¶
Return the on-disk PNG path for
row, re-anchored underroot.- Parameters:
row – a row carrying
png_path(orpath), or a bare path string, which is accepted as-is and only re-anchored. A row with neither raisesCropError. Re-anchoring goes throughreanchor_path(), so it is separator-agnostic (a Windows path opened on Linux re-anchors), it takes the LAST<folder>component rather than the first (an old root that itself contained adatafolder used to produce a path naming a directory), and “already under the root” is a component-wise prefix test rather than a substring one. A path carrying no anchor at all is returned untouched even if it points nowhere on this machine, and the failure surfaces on read.
- spacr.crops.build_png_channels(data: numpy.ndarray, mapping: Dict[str, int | None], dtype=None) numpy.ndarray[source]¶
Assemble the crop planes in file order – red, green, blue.
The returned array uses the same channel order as the PNG file, so in-memory and decoded representations have the same colour semantics.
Greyscale is preserved: when all three colours name the same source channel the result is a single plane, so cv2 writes a one-channel PNG exactly as
png_dims=[a]always did, rather than three identical planes at three times the size.- Parameters:
data – the merged
(H, W, C)array.mapping – as returned by
resolve_png_channel_mapping().dtype – optional dtype to cast the assembled planes to.
- Returns:
(H, W, 1|3)array, red plane first.- Raises:
CropError – a mapping index is out of range for
data.
- spacr.crops.clear_crop_format_cache() None[source]¶
Forget every cached folder marker (and every “already stamped” folder).
- spacr.crops.clear_field_cache() None[source]¶
Drop every cached
MergedField(and its label indices).
- spacr.crops.crop_folder_format(folder: str, db_path: str | None = None, *, strict: bool = False) int[source]¶
Return the crop format that applies to
folderas a whole.Precedence: sidecar, then the database column, then
CROP_FORMAT_LEGACY_BGR. When both are present and they disagree, the sidecar wins – it is the marker that travels with the folder – and the disagreement is reported: printed by default, raised whenstrict.A folder in the middle of a migration reports
CROP_FORMAT_LEGACY_BGR, because the files that have not been converted yet still are; usecrop_format_for_png()to resolve one file inside such a folder.- Parameters:
folder – the crop folder.
db_path – optional
measurements.dbto consult.strict – raise
CropFormatConflictinstead of printing.
- Returns:
the format integer.
- spacr.crops.crop_format_for_png(png_path: str, db_path: str | None = None, *, strict: bool = False) int[source]¶
Return the crop format of one crop PNG.
Same precedence as
crop_folder_format(), plus the two per-file overrides an interruptedmigrate_crop_folder()leaves behind:a leftover
<name>.spacr_v2staging file means the file next to it has not been converted yet – it is still legacy;a name in the marker’s
unconvertedlist is a file the migration could not rewrite. It stays legacy for good, in a folder that is otherwise format 2, so it still has to be read correctly;otherwise, inside a folder whose marker carries a
migrationblock, a file at or before the recorded watermark is converted and one after it is not.
- Parameters:
png_path – the crop PNG.
db_path – optional
measurements.dbto consult.strict – raise on a sidecar/database conflict.
- Returns:
the format integer.
- spacr.crops.crop_settings_from_db(db_path: str) Dict[str, Any][source]¶
Read the
settingstablemeasure_cropwrites intomeasurements.db.spacr.io._save_settings_to_dbstores every setting as(setting_key, setting_value)strings; this parses them back so a crop cut on demand can use the samepng_dims/png_size/normalizethat produced the PNG folder.- Parameters:
db_path – path to
measurements.db.- Returns:
the settings dict, or
{}if the table is absent.
- spacr.crops.crop_spec_from_settings(settings: Mapping[str, Any], merged_path: str = '', object_type: str | None = None, label: int = 0) CropSpec[source]¶
Build a
CropSpecfrom ameasure_cropsettings dict.Uses
png_dims,png_size,normalize,normalize_by,use_bounding_box,dialate_pngs,dialate_png_ratios,crop_modeand the*_mask_dimkeys – i.e. everything that shaped the PNG folder.- Parameters:
settings – The
measure_cropsettings. A scalarpng_sizedefines a square crop. Object-specific values in nestedpng_size,dialate_pngs, anddialate_png_ratiosare selected by the object’s position incrop_modeand fall back to the first entry when that object is absent. Channels are resolved frompng_channel_mapping, or legacypng_dims, throughchannels_from_settings()and stored in colour order. Text forms such as"[2, 98]","2,98", and"[1 99]"are parsed as percentile windows; a non-text sequence with a length other than two disables normalization.merged_path – the
merged/<fov>.npyto record on the spec. The default""builds a template spec, which is whatMergedCropSourcewants: it fills the path (and label) in per row.object_type – which mask plane to crop by;
Nonetakes the first entry ofsettings['crop_mode'].'cytoplasm'forcesdilate=Falsewhatever the settings say, because_measure_crop_corehard-disables dilation for it.label – the object’s
object_label. The default0is background, so it is only meaningful on a template spec – cutting with it raisesLabelMissing.
- spacr.crops.extract_crop(merged_path: str, object_type: str = 'cell', label: int = 0, *, spec: CropSpec | None = None, field: MergedField | None = None, **kwargs) numpy.ndarray[source]¶
Cut one object out of a merged array, reproducing the PNG path exactly.
The returned array is the pre-write array: exactly what
_measure_crop_corehands to the writer. Its dtype is the merged array’s (uint16for a normal spaCR run) and its channel order isspec.channels. Usepng_view()to get what a consumer reading the written PNG (viaread_crop_png()) would see.- Parameters:
merged_path – the
merged/<fov>.npy.object_type – which mask plane to crop by.
label – the object’s
object_label.spec – a ready-made
CropSpec;merged_path/object_type/labelandkwargsoverride its fields.field – an already-open
MergedFieldto cut from.kwargs – any other
CropSpecfield.
- Returns:
(height, width, n_channels)array.- Raises:
MergedFileMissing – the merged file does not exist.
CorruptMergedFile – it is not a readable 3-D
.npy.MaskPlaneMissing – no plane for
object_type.LabelMissing –
labelis 0/negative or absent from the plane.CropError – a bad channel index, size, or an out-of-array
bbox.
- spacr.crops.extract_crops(merged_path: str, specs: Iterable[CropSpec], *, mask_dims: Mapping[str, int] | None = None, on_error: str = 'raise') List[numpy.ndarray | None][source]¶
Cut many objects out of one merged array, opening the file once.
A grid draws from a handful of fields, so batching matters: the
.npyis memory-mapped once and every object’s mask plane is indexed once, no matter how many objects are requested from it.- Parameters:
merged_path – the
merged/<fov>.npy; every spec is cut from it, whateverspec.merged_pathsays.specs – iterable of
CropSpec.mask_dims – object type -> plane index for the whole batch; defaults to the first spec’s
mask_dims.on_error –
'raise'(default) or'none'to putNonein the result for each failing spec instead of raising.
- Returns:
list of crops, one per spec, in order.
- spacr.crops.find_crop_folders(root: str) List[str][source]¶
Return every
*_pngcrop folder underroot, sorted.Accepts an experiment root, its
datafolder, or a crop folder itself.- Parameters:
root – where to look.
- Returns:
absolute folder paths.
- spacr.crops.legacy_channel_names(channels: Iterable[str]) List[str][source]¶
Map a legacy-trained model’s
train_channelsonto format-2 crops.A classifier trained on legacy crops learned “input plane 0 is whatever is in the file’s red channel”, and in a legacy file that is
png_dims[-1]. Feed the same model a format-2 crop and plane 0 is nowpng_dims[0]– a permutation of its input, which it will happily score and get wrong, with no error anywhere.Reversing the request undoes the permutation exactly: red and blue swap, green is unmoved. So a model trained with
train_channels=['r','g','b']keeps seeing the pixels it was trained on if it is applied with['b','g','r'], and one trained with['r','g']with['b','g'].This is a stopgap for a model you cannot retrain. Retraining on corrected crops is the real fix, and it is cheap compared with getting this wrong.
- Parameters:
channels – the
train_channelsthe model was trained with.- Returns:
the equivalent list to apply it with on format-2 crops.
- spacr.crops.legacy_png_view(crop: numpy.ndarray) numpy.ndarray[source]¶
Return what a naive PIL read of a legacy crop PNG gives back.
Kept, and named for what it is, because it is the inverse of the format-1 write and therefore the thing
read_crop_png()has to undo. Two behaviours, both of them the bug:the channel order is reversed relative to
png_dims–cv2.imwriteread the array as BGR;a
uint16crop is a 16-bit PNG and PIL narrows it two different ways: the high byte (// 256) for an RGB image, but a clip at 255 for a single-channel one, which flattens any crop brighter than 255/65535 to solid white.
Nothing in spaCR calls this on the live path any more. It exists so tests can prove the legacy reader inverts the legacy writer exactly, and so code that genuinely needs bug-compatible pixels (a classifier trained on legacy crops, say) can ask for them by name instead of by accident.
- Parameters:
crop – the array returned by
extract_crop().- Returns:
(H, W, 3)uint8 RGB array.
- spacr.crops.mask_dims_from_settings(settings: Mapping[str, Any]) Dict[str, int][source]¶
Return
{object_type: plane index}from ameasure_cropsettings dict.Falls back to
DEFAULT_MASK_DIMSfor anything the dict does not name.- Parameters:
settings – a
measure_cropsettings mapping (a live dict or one read back bycrop_settings_from_db()). Only thecell_mask_dim/nucleus_mask_dim/pathogen_mask_dim/organelle_mask_dimkeys are read; a key that is absent, blank, the string'none'or not an integer is skipped rather than raised on. The fallback is all-or-nothing: a dict naming even one plane returns only the planes it named, so an object type it left out has no entry at all – it is not filled in fromDEFAULT_MASK_DIMS.
- spacr.crops.migrate_crop_folder(folder: str, *, mode: str = 'rewrite', dry_run: bool = False, on_error: str = 'raise', db_path: str | None = None, progress: Any | None = None) MigrationResult[source]¶
Repair reversed format-2 crops and stamp the folder. Idempotent.
Format 2 stores three-channel crops in the reverse of their declared channel mapping. Formats 1 and 3, as well as unmarked folders, already use declared order and require no pixel rewrite.
mode='rewrite'(the default) rewrites every 3-channel PNG of a format-2 folder with its channels put back, and marks the folder format 3. A folder that is format 1, format 3 or unmarked is already in declared order, so it is an immediate no-op – which is the answer for almost every folder that exists.mode='mark'touches no pixels and only records that the folder is format 1 – use it when something outside spaCR reads those exact bytes and must keep seeing them (a classifier trained on legacy crops, for instance).Interruption safety, which is the whole design:
each file is converted into a durable staging file
<name>.spacr_v2(itself written temp-then-os.replace, perio._save_array_atomic) and only thenos.replace-d over the original, so the crop at its real name is always a complete PNG – the old one or the new one;the folder marker carries a
migrationblock with adone_throughwatermark, advanced before the install, so the rule “a staging file exists ⇒ the crop beside it is still legacy” resolves every file at every point in the sequence. That is whatcrop_format_for_png()reads, and it is why a killed migration is still read correctly and can be run again.
Running it on an already-converted folder is an immediate no-op: nothing is decoded, nothing is written, and
result.alreadyis True. Running it twice therefore cannot double-reverse anything.The one exception is a folder finished with
on_error='skip': its marker names the files that could not be rewritten, those stay legacy (and are read as legacy) inside an otherwise format-2 folder, and a later run retries only them.- Parameters:
folder – the crop folder (
.../<well>/cell_pngand friends).mode –
'rewrite'or'mark'.dry_run – report what would happen; write nothing.
on_error –
'raise'(default) or'skip', which records the file in the marker’sunconvertedlist and keeps reading it as legacy.db_path – also stamp
png_list.crop_formatin this database.progress – optional callable
(done, total, name).
- Returns:
- Raises:
CropError – bad arguments, or a file that cannot be converted when
on_error='raise'.
- spacr.crops.migrate_crop_tree(root: str, **kwargs) List[MigrationResult][source]¶
Run
migrate_crop_folder()on every crop folder underroot.- Parameters:
root – experiment root, its
datafolder, or one crop folder.kwargs – forwarded to
migrate_crop_folder().
- Returns:
one
MigrationResultper folder, in folder order.
- spacr.crops.narrow_to_uint8(arr: numpy.ndarray) numpy.ndarray[source]¶
Convert
arrtouint8using the crop-writer convention.uint16(and anything wider) is narrowed by taking the high byte, which is a plain linear rescale of a crop thatnormalize_to_dtypealready stretched across the full dtype range. Floats, which only appear when a caller hands in something the crop path never produces, are clipped – there is no dtype range to rescale from.This differs from PIL’s format-dependent behaviour: PIL takes the high byte of a 16-bit RGB PNG but clips a 16-bit single-channel one at 255, so the same pixel value survives or saturates depending on how many channels its neighbours have. One behaviour, applied here, replaces both.
- Parameters:
arr – any numeric array.
- Returns:
uint8array of the same shape.
- spacr.crops.open_merged_field(path: str, mask_dims: Mapping[str, int] | None = None, use_cache: bool = True) MergedField[source]¶
Return a
MergedFieldforpath, reusing a cached one if possible.- Parameters:
path – the
merged/<fov>.npy.mask_dims – object type -> plane index;
NoneusesDEFAULT_MASK_DIMS.use_cache – set False to force a fresh open (and a fresh label index).
- Raises:
MergedFileMissing – the file does not exist.
CorruptMergedFile – the file is not a readable 3-D
.npy.
- spacr.crops.png_dims_to_channel_mapping(png_dims) Dict[str, int | None][source]¶
Translate a legacy
png_dimslist into an explicit{r, g, b}map.png_dimsnever said which colour it meant; the answer was buried in cv2’s BGR interpretation of the array it was handed, which is how the convention got inverted for eleven days without anyone being able to point at the line that decided it. The list is still accepted – every settings CSV and every notebook in the wild holds one – but it is translated here, once, into a mapping that says what it means.The translation is the legacy reading, because that is the one that was ever on screen: entry 0 is blue, 1 is green, 2 is red.
[a, b, c]->{'r': c, 'g': b, 'b': a}[a, b]->{'r': None, 'g': b, 'b': a}(the old zero third plane)[a]->{'r': a, 'g': a, 'b': a}(greyscale; seebuild_png_channels(), which keeps it a one-plane image)
- Parameters:
png_dims – the legacy list of source channel indices.
- Returns:
a
{'r': idx, 'g': idx, 'b': idx}dict;Nonemeans an empty plane.- Raises:
CropError – more than three entries, or an empty list.
- spacr.crops.png_view(crop: numpy.ndarray) numpy.ndarray[source]¶
Return what a consumer sees after the crop has made the PNG round trip.
This is the contract, and it is deliberately boring: channel
iof the crop is channeliof the result, narrowed to 8 bit bynarrow_to_uint8().That holds because a crop is cut in COLOUR order –
CropSpec.channelsis(red_source, green_source, blue_source), built bychannels_from_settings()from the declaredpng_channel_mapping. So channel 0 is the red one here, in the file, and in whatread_crop_png()hands back. There is exactly one order and every part of the crop path speaks it.The alternative – keeping crops in
png_dimslist order and translating at the edges – is what made the on-demand source and the PNG folder return different pixels for the same object.read_crop_png()returns exactly this for the same object, for a crop written in either format, which is what makes the on-demand source and the PNG folder interchangeable.Before spaCR grew a crop-format marker the answer was the reverse of this (see
legacy_png_view()); that was a bug, not a convention.- Parameters:
crop – the array returned by
extract_crop().- Returns:
(H, W, 3)uint8 RGB array.
- spacr.crops.read_crop_folder_marker(folder: str, use_cache: bool = True) Dict[str, Any] | None[source]¶
Return the parsed
.spacr_crop_format.jsonoffolder, orNone.A sidecar that cannot be parsed is treated as absent – a corrupt marker must not be more trusted than no marker, and no marker means legacy, which is the safe answer.
- Parameters:
folder – the crop folder (the one holding the PNGs).
use_cache – reuse a cached read while the sidecar is unchanged.
- Returns:
the marker dict, or
Nonewhen there is no usable one.
- spacr.crops.read_crop_png(path: str, fmt: int | None = None, db_path: str | None = None, as_format: int = CROP_FORMAT_CURRENT) numpy.ndarray[source]¶
Read a crop PNG and return it in the corrected order, as 8-bit RGB.
The one function every consumer of a crop folder should go through. It resolves the file’s format (see
crop_format_for_png()), reverses the channel axis when the file’s ordering differs from the one asked for – which today means format 2 only, since formats 1 and 3 are both already in declared order – and narrows to 8 bit withnarrow_to_uint8(), so a legacy dataset and a new one come back identical and the caller never has to know which it opened.The result equals
png_view(extract_crop(...))for the same object, under either format. That equality is the contract, andtests/test_crops.pyasserts it.- Parameters:
path – the crop PNG.
fmt – what the file on disk is, when you know better than the marker does.
Noneresolves it.as_format – what ordering you want back. The default is the declared one. Formats 1 and 3 share this order; format 2 requests the intermediate reversed order. Classification also needs its recorded intensity-decoding policy, handled by
spacr.classification_pixels.db_path – optional
measurements.dbconsulted when the folder has no sidecar.
- Returns:
(H, W, 3)uint8 RGB array.- Raises:
MergedFileMissing – the file does not exist.
CropError –
as_formatis not a known format.
- spacr.crops.read_db_crop_format(db_path: str, png_path: str | None = None, table: str = 'png_list') int | None[source]¶
Return the crop format recorded in the database, or
None.Reads the
crop_formatcolumn ofpng_list: for onepng_pathif given, otherwise the single distinct value covering the whole table (a table holding both formats returnsNone– ambiguous is not an answer).- Parameters:
db_path – path to
measurements.db.png_path – restrict to one crop’s row.
table – table holding the crops.
- Returns:
the format integer, or
Nonewhen the column, the table or the database is absent, or the answer is ambiguous.
- spacr.crops.read_merged_plane_layout(path: str) Dict[str, Any] | None[source]¶
Read and validate a merged folder’s optional plane-layout manifest.
Legacy folders have no manifest and return
None. A present but malformed manifest raises: once metadata exists, silently ignoring it would recreate the exact wrong-plane failure the manifest prevents.- Parameters:
path – a merged folder, or a
.npyfile inside one (its parent folder is used); the.spacr_plane_layout.jsonsidecar is read from that folder.
- spacr.crops.reconcile_merged_mask_dims(settings: Mapping[str, Any], merged_folder: str, *, explicit_keys: Iterable[str] = ()) Dict[str, Any][source]¶
Apply a plane manifest and reject explicit conflicting mask indices.
A manifest is authoritative for the folder it accompanies. Defaults are replaced automatically; a non-
Nonevalue the caller explicitly supplied must agree or the run stops before measuring a wrong plane. Legacy folders without a manifest return an unchanged copy.Every organelle slot the manifest records gets its plane, and every slot
settingscarries is set to the manifest’s answer. A slot that neither names is left out of the copy rather than set toNone. The cell, nucleus and pathogen keys are always set, so a plane the manifest does not record is switched off.- Parameters:
settings – the run settings; its
<role>_mask_dimkeys are read and a copy with those keys reconciled is returned (the mapping itself is not modified).merged_folder – the merged folder whose plane-layout manifest is applied.
explicit_keys – settings keys the caller supplied explicitly; a non-
Nonevalue under one of these that disagrees with the manifest raisesPlaneLayoutConflict.
- spacr.crops.resolve_crop_source(settings_or_src: str | Sequence[str] | Mapping[str, Any], *, object_type: str | None = None, prefer: str | None = None, ask: Any | None = None) CropSource[source]¶
Pick the crop source for a run, and record which one it picked.
The returned object’s
CropSource.kindis'png'or'merged'andCropSource.reasonsays why, so a caller can printsource.describe()instead of guessing.Selection order:
an explicit
preferargument, thensettings['crop_source']('png'|'merged'|'auto');otherwise
'auto': the PNG folder if one exists (nothing changes for existing datasets), else the merged folder.
When the merged source is chosen and
measurements.dbholds themeasure_cropsettings, the crop parameters (png_dims,png_size,normalize, mask plane indices, …) are read back from it, so the on-demand crops match the PNGs that run would have produced.- Parameters:
settings_or_src – a settings dict (with
src, optionallycrop_source), a source path, or a list/tuple whose first entry is the source path – the experiment root or itsmergedfolder.object_type – default object type for the merged source.
prefer – force
'png'or'merged'.
- Raises:
CropError – the requested source is not available.
- spacr.crops.resolve_png_channel_mapping(settings) Dict[str, int | None][source]¶
Return the
{r, g, b}source-channel mapping a run should use.Precedence, and the reason for it:
settings['png_channel_mapping']– the explicit form. If the user said which channel is red, that is the answer.settings['png_dims']– the legacy list, translated bypng_dims_to_channel_mapping(). A settings CSV written by any older build lands here and keeps rendering the way it always did.DEFAULT_PNG_CHANNEL_MAPPING.
A mapping that names a colour spaCR does not have, or a non-integer index, is an error rather than a silent drop: a mis-keyed mapping would otherwise delete a whole stain from every crop in the run and say nothing.
- Parameters:
settings – the run settings dict (or anything with
.get).- Returns:
{'r': idx, 'g': idx, 'b': idx};Nonemeans an empty plane.- Raises:
CropError – an unknown colour key or a non-integer channel index.
- spacr.crops.stamp_crop_folder(folder: str, fmt: int = CROP_FORMAT_CURRENT) str | None[source]¶
Ensure that
foldercontains a crop-format marker.The crop writer calls this before writing the first PNG. An interrupted run therefore leaves a marked, possibly incomplete folder rather than an unmarked folder that could be interpreted as the legacy format.
Each folder is checked once per process. Failure to write the marker emits a warning instead of aborting the measurement run, because the image data remain valid but their stored channel convention becomes ambiguous.
If a folder already contains crops in another format, the function reports the conflict but does not migrate files. Migration remains a separate, single-process operation in
migrate_crop_folder().- Parameters:
folder – the crop folder.
fmt – format to record; defaults to
CROP_FORMAT_CURRENT.
- Returns:
the sidecar path, or
Noneif it could not be written.
- spacr.crops.stamp_crop_format_in_db(db_path: str, png_paths: Iterable[str] | None = None, fmt: int = CROP_FORMAT_CURRENT, table: str = 'png_list') int[source]¶
Record the crop format on
png_list, adding the column if needed.The database copy is advisory –
crop_format_for_png()prefers the sidecar – but it makes “which of my plates are still legacy?” a query rather than a filesystem walk.- Parameters:
db_path – path to
measurements.db.png_paths – rows to stamp;
Nonestamps every row.fmt – the format to record.
table – table holding the crops.
- Returns:
number of rows updated.
- Raises:
CropError – unknown
fmt.
- spacr.crops.to_cv2_bgr(png_channels: numpy.ndarray) numpy.ndarray[source]¶
Return
png_channelsin the ordercv2.imwritehas to be handed it.build_png_channels()assembles the crop in file order – red plane first – whilecv2.imwriteinterprets a 3-channel array as BGR. Reversing the channel axis here, once, in the writer, makes cv2’s interpretation land the array’s red plane in the file’s red slot, so the PNG’s slots hold the channelssettings['png_channel_mapping']named (format 3). UnderDEFAULT_PNG_CHANNEL_MAPPINGthat putspng_dims[0]in blue, byte-identical to format 1.2-D or single-channel: returned unchanged. cv2 writes a grayscale PNG and does no colour interpretation, so there is nothing to reverse.
2 channels: padded with a zero plane to RGB first, then reversed.
build_png_channels()never emits two planes – it carries an empty colour as a zero plane in the slot the user left blank – so this is for callers that assemble their own array.3 channels: reversed.
4 or more: refused. cv2 would write BGRA, and PIL then reads the fourth intensity plane as an alpha channel and drops it on
convert('RGB')– a whole stain silently deleted from every crop.settings['png_dims']documents a maximum of three entries; this is where a fourth stops being ignored and starts being an error.
- Parameters:
png_channels – the crop in file order, as
build_png_channels()assembles it for_measure_crop_core.- Returns:
the array to hand to
cv2.imwrite.- Raises:
CropError – more than three channels.
- spacr.crops.write_crop_folder_marker(folder: str, fmt: int = CROP_FORMAT_CURRENT, **extra: Any) str[source]¶
Write
folder’s crop-format sidecar atomically.Temp file plus
os.replace(), likespacr.io._save_array_atomic: a marker is either the previous one or the complete new one, never a half-written JSON document thatread_crop_folder_marker()would then read as “no marker” – i.e. as legacy – over a folder of corrected crops.- Parameters:
folder – the crop folder.
fmt – any known crop format (1, 2 or 3); defaults to
CROP_FORMAT_CURRENT, i.e.CROP_FORMAT_DECLARED_RGB.extra – extra keys to record (
migration,png_dims, …). A key whose value isNoneis dropped.
- Returns:
the sidecar path.
- Raises:
CropError –
fmtis not a known format.