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¶
An ROI that cannot be placed on the mask it was handed. |
Classes¶
One drawn region, in world coordinates. |
|
The callable |
|
Which regions apply to which fields, and how an object is judged. |
Functions¶
|
Measure whole fields again, here and for any worker started afterwards. |
|
Measure only inside these regions, here and in every worker process. |
|
Install the ROI filter in this process from the environment. |
|
Whether the ROI will actually reach |
Module Contents¶
- exception spacr.roi.RoiError[source]¶
Bases:
spacr.errors.ConfigurationErrorAn 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 reasonspacr.layers.Shapeis: a generated__eq__would compare the vertex arrays elementwise and raise “truth value of an array is ambiguous” from anything as ordinary asroi in roi_set.fields['*'].- Parameters:
kind –
'polygon','rectangle'or'ellipse'— the closed kinds ofspacr.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 byRoiSet.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.Shapeonspacing’s grid.The world→data conversion is
spacing’s own, so a shape built here rasterises onto that grid through the ordinaryspacr.layers.ShapesLayer.mask()path.- Parameters:
spacing – a two-axis
spacr.layers.Spacingwhose 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
RoiSetand the raster of the field it last saw. The cache matters:_measure_crop_coreconsults 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_missingwhen 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_setor 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.labelsare 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_missingis'error', or if the ROI cannot be placed on this mask.
- 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.npystem 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_TYPESthe 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).
- __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.
- 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.
- 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
Nonewhen 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
pathas JSON; returns the absolute path.- Parameters:
path – destination JSON file.
JSON rather than
.npzon 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:
the ROI is written to disk if it is not already a path — a
spawnworker can only reach it through the file system;ROI_ENV_VAR(andON_MISSING_ENV_VAR) are set;spacr.roi:installis APPENDED toHOOKS_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, aspacr.layers.ShapesLayer, or the path of a saved set.path – where to write
roi_setwhen 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_VARnames, and the only route that survives aspawnworker: the worker is a fresh interpreter, so it imports this module and calls this function for itself, reading the ROI fromROI_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
spawnworker, 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_METHODselects, falling back to the platform default — i.e. whatspacr.measure.measure_crop()will use.- Returns:
(ok, message).okis False whenever a field could be measured whole without anything saying so.