spacr.roi

A drawn region of interest, honoured by Measure.

Draw a polygon over a field and measure only the objects inside it. The drawing half is spacr.layers — a ShapesLayer holds the vertices as geometry, in world coordinates, so the same ROI means the same region on a downsampled preview and on the full-resolution mask. This module is the other half: turning that geometry into the keep/drop decision spacr.measure_hooks.apply_region_filter_hooks() asks for, and — the part that is easy to get silently wrong — getting it into the worker processes that do the measuring.

Nothing here edits spacr.measure. The extension point already exists (spacr.measure_hooks.register_region_filter_hook()), it is applied after the size filters and before _exclude_objects, and a dropped label is zeroed out of its mask before a single regionprops call, so keeping 5 of 500 objects costs 5 objects’ worth of work.

Why an ROI is stored in world coordinates

A polygon drawn on a 512-pixel preview of a 2048-pixel field is not the same set of array indices as the region it names. Storing (row, column) would make the ROI mean four different things at four zoom levels, and the picture would look right in every one of them. So a saved ROI is a list of world points plus the unit they are measured in, and placing it on a mask goes through Spacing and for_grid() exactly like every other render in this codebase. Mixing a µm ROI with a pixel-spaced measurement raises rather than drawing a plausible region in the wrong place.

Two decision rules

mode='centroid' (the default) keeps an object when its centroid falls inside the ROI. It is the rule that partitions cleanly: an object is inside exactly one of two ROIs that share an edge, so an object on a boundary is counted once, and every object type is judged the same way without the cell and its nucleus ever disagreeing.

mode='overlap' keeps an object when at least RoiSet.min_overlap of its pixels are inside. Use it when the objects are large compared with the ROI and “the middle of the cell” is not the question being asked.

Reaching the workers

spacr.measure.measure_crop() measures fields in a process pool. Under spawn (Windows, macOS, SPACR_START_METHOD=spawn, and Python 3.14 on Linux) a worker is a fresh interpreter with an empty hook registry: a filter registered with register_region_filter_hook() in the parent applies to nothing at all, the run completes, and every object in the field is measured while the user believes only the ROI was. That is a silent scientific error, so this module refuses to rely on inheritance: enable_roi_filter() writes the ROI to disk and names spacr.roi:install in HOOKS_ENV_VAR, and each worker installs the filter for itself from the environment it inherits. worker_delivery_status() answers “will this actually reach the workers?” before the run rather than after it.

Exceptions

RoiError

An ROI that cannot be placed on the mask it was handed.

Classes

RegionOfInterest

One drawn region, in world coordinates.

RoiRegionFilter

The callable spacr.measure_hooks.register_region_filter_hook() runs.

RoiSet

Which regions apply to which fields, and how an object is judged.

Functions

disable_roi_filter(→ bool)

Measure whole fields again, here and for any worker started afterwards.

enable_roi_filter(→ str)

Measure only inside these regions, here and in every worker process.

install(→ str)

Install the ROI filter in this process from the environment.

worker_delivery_status(→ Tuple[bool, str])

Whether the ROI will actually reach measure_crop's workers.

Module Contents

exception spacr.roi.RoiError[source]

Bases: spacr.errors.ConfigurationError

An ROI that cannot be placed on the mask it was handed.

A spacr.errors.ConfigurationError, like every other measurement hook failure: a mis-specified ROI is wrong for every field on the plate, not bad luck on one of them.

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

class spacr.roi.RegionOfInterest[source]

One drawn region, in world coordinates.

Compared by identity (eq=False), for the reason spacr.layers.Shape is: a generated __eq__ would compare the vertex arrays elementwise and raise “truth value of an array is ambiguous” from anything as ordinary as roi in roi_set.fields['*'].

Parameters:
  • kind – 'polygon', 'rectangle' or 'ellipse' — the closed kinds of spacr.layers.Shape. An open shape (a line, a path) encloses nothing and is not an ROI.

  • vertices – (M, 2) world coordinates, in the order named by RoiSet.axes ((y, x) unless something says otherwise). A rectangle or an ellipse may be given as two opposite corners.

  • name – what the user called it, carried through to the diagnostic so “dropped by ROI ‘well edge’” is possible.

__post_init__() → None[source]

Normalize the fields and reject geometry that cannot enclose area.

Raises:

RoiError – if the shape kind is open or unknown, vertices are not a finite (M, 2) array, or too few vertices are supplied.

as_dict() → Dict[str, Any][source]

A JSON-safe dict, the form RoiSet.save() writes.

classmethod from_dict(payload: Mapping[str, Any]) → RegionOfInterest[source]

Rebuild one from as_dict().

Parameters:

payload – serialized ROI mapping.

to_shape(spacing) → Any[source]

This ROI as a spacr.layers.Shape on spacing’s grid.

The world→data conversion is spacing’s own, so a shape built here rasterises onto that grid through the ordinary spacr.layers.ShapesLayer.mask() path.

Parameters:

spacing – a two-axis spacr.layers.Spacing whose axes are this ROI’s axes, in order.

class spacr.roi.RoiRegionFilter(roi_set: RoiSet, *, on_missing: str | None = None)[source]

The callable spacr.measure_hooks.register_region_filter_hook() runs.

Holds one RoiSet and the raster of the field it last saw. The cache matters: _measure_crop_core consults the filter once per object type, five times per field, and every one of those calls wants the same rasterised polygon on the same grid.

Parameters:
  • roi_set – the regions and the rule.

  • on_missing – overrides RoiSet.on_missing when given.

Validate the rules and initialize cumulative counters and cache.

Parameters:
  • roi_set – regions and object-selection rules to apply.

  • on_missing – optional missing-field policy overriding RoiSet.on_missing.

Raises:

RoiError – if roi_set or the override is invalid.

stats['fields'] counts distinct raster/cache misses; fields that are uncovered, empty, or waved through do not increment it.

__call__(context) → numpy.ndarray[source]

Decide which of context.labels are inside the ROI.

Parameters:

context – a spacr.measure_hooks.RegionContext.

Returns:

a boolean array of len(context.labels).

Raises:

RoiError – if the field is not covered and on_missing is 'error', or if the ROI cannot be placed on this mask.

report() → str[source]

One line summarising what this filter has done so far.

class spacr.roi.RoiSet[source]

Which regions apply to which fields, and how an object is judged.

Parameters:
  • fields – {field name: (RegionOfInterest, ...)}. The field name is the .npy stem the pipeline uses, e.g. plate1_A01_F001; ANY_FIELD ('*') is the fallback for every field with no entry of its own. Several ROIs on one field are a UNION — drawing a second polygon adds to what is measured.

  • axes – which world axes the vertex columns are, outermost first, matching the mask’s own array axis order — ('y', 'x') unless the ROI was drawn on some other plane. The order is not cosmetic: a transposed pair puts the region somewhere plausible and wrong, so the standard pair is only accepted the standard way round.

  • units – what one world unit is, compared by name against the measurement’s own spacing. Mixing µm with px raises rather than drawing a plausible region in the wrong place.

  • mode – 'centroid' or 'overlap'; see the module docstring.

  • min_overlap – for 'overlap', the fraction of an object’s pixels that must be inside for it to be kept.

  • invert – keep the objects OUTSIDE the ROI instead. “Exclude this debris” is as common a request as “measure this colony”.

  • object_types – which of spacr.measure_hooks.OBJECT_TYPES the ROI applies to. The default is all of them, which is what makes the cell and its nucleus agree.

  • on_missing – what a field with no ROI means — 'error' (the default: refuse, because measuring everything when the user drew a region is the silent answer), 'all' (measure the whole field) or 'none' (measure nothing in it).

__len__() → int[source]

Return the total number of ROIs assigned across all fields.

__post_init__() → None[source]

Copy and validate the region mapping and filtering rules.

Raises:

RoiError – if a field contains a non-ROI value, the axes are invalid, or any mode, overlap, object type, or missing-field rule is unsupported.

as_dict() → Dict[str, Any][source]

A JSON-safe dict — what save() writes and load() reads.

covers(file_name: str) → bool[source]

Whether this set has anything to say about file_name.

Parameters:

file_name – merged-field filename or stem to test.

describe() → str[source]

One line for a status bar or a run log.

classmethod from_dict(payload: Mapping[str, Any]) → RoiSet[source]

Rebuild a set from as_dict().

Parameters:

payload – serialized ROI-set mapping.

classmethod from_shapes_layer(layer, *, fields: Any = ANY_FIELD, **kwargs: Any) → RoiSet[source]

Take the closed shapes off a spacr.layers.ShapesLayer.

The layer’s vertices are in data coordinates on its own grid; they are converted to world here through the layer’s Spacing, which is what lets an ROI drawn on a preview be applied to a full-resolution mask.

Parameters:
  • layer – the shapes layer the user drew on.

  • fields – a field name, an iterable of them, or ANY_FIELD. Every closed shape is attached to each.

Raises:

RoiError – if the layer holds no closed shape, or its spacing has no axis for the plane the shapes were drawn in.

classmethod load(path: str) → RoiSet[source]

Read a set back from save().

Parameters:

path – ROI-set JSON file to read.

Raises:

RoiError – if the file is missing or is not an ROI file. Loudly, because a worker that cannot load the ROI must not go on to measure the whole field.

rois_for(file_name: str) → Tuple[RegionOfInterest, ...] | None[source]

The ROIs that apply to a field, or None when none do.

Parameters:

file_name – merged-field filename or stem to resolve.

The field’s own entry wins over ANY_FIELD; a field entry that is present but empty means “this field has an ROI and it encloses nothing”, which is not the same as having no entry.

save(path: str) → str[source]

Write this set to path as JSON; returns the absolute path.

Parameters:

path – destination JSON file.

JSON rather than .npz on purpose: an ROI is a few dozen numbers and a human being should be able to read the file that decided which cells were measured. Creates the parent folder.

Raises:

RoiError – if the file cannot be written.

spacr.roi.disable_roi_filter() → bool[source]

Measure whole fields again, here and for any worker started afterwards.

Unregisters the filter and removes this module’s entry from SPACR_MEASURE_HOOKS — leaving any other extension’s entries alone — plus the two ROI variables.

Returns:

True if a filter was registered and has been removed.

spacr.roi.enable_roi_filter(roi_set: Any, *, path: str | None = None, on_missing: str | None = None, verbose: bool = True) → str[source]

Measure only inside these regions, here and in every worker process.

Three things happen, and the third is the one that matters:

  1. the ROI is written to disk if it is not already a path — a spawn worker can only reach it through the file system;

  2. ROI_ENV_VAR (and ON_MISSING_ENV_VAR) are set;

  3. spacr.roi:install is APPENDED to HOOKS_ENV_VAR — appended, not assigned, because the illumination correction may already be in there — and the registry is then consulted so this process installs the filter through that same environment route.

Parameters:
  • roi_set – a RoiSet, a spacr.layers.ShapesLayer, or the path of a saved set.

  • path – where to write roi_set when it is not already a path. Defaults to ./roi/measure_roi.json.

  • on_missing – override the set’s own rule for uncovered fields.

  • verbose – print what was enabled and whether the workers will see it.

Returns:

the registry key the filter is registered under.

Raises:

RoiError – when the ROI cannot be saved or loaded.

spacr.roi.install() → str[source]

Install the ROI filter in this process from the environment.

The zero-argument installer HOOKS_ENV_VAR names, and the only route that survives a spawn worker: the worker is a fresh interpreter, so it imports this module and calls this function for itself, reading the ROI from ROI_ENV_VAR.

Returns:

the registry key the filter was registered under.

Raises:

RoiError – if the environment does not name a readable ROI file. Refusing loudly is the point — a worker that cannot load the ROI must not go on to measure every object in the field.

spacr.roi.worker_delivery_status(start_method: str | None = None) → Tuple[bool, str][source]

Whether the ROI will actually reach measure_crop’s workers.

The failure this answers is silent by construction: a filter registered only in the parent process is a no-op in every spawn worker, the run completes, and every object in the field is measured while the user believes only the ROI was.

Parameters:

start_method – the pool start method to judge against. Defaults to whatever SPACR_START_METHOD selects, falling back to the platform default — i.e. what spacr.measure.measure_crop() will use.

Returns:

(ok, message). ok is False whenever a field could be measured whole without anything saying so.