spacr.measure_hooks¶
Opt-in extension points for the per-object measurement pipeline.
spacr.measure is one 3 000-line module with a single hot path
(spacr.measure._measure_crop_core()). Two features that are being built
alongside it need to change what that path measures without editing it:
illumination / flat-field correction — a per-channel multiplicative gain estimated across a plate, applied to the intensity channels before any feature is computed;
a user-drawn ROI / shapes layer — “only measure the objects inside this polygon”.
Both are expressed here as registries of plain callables. Nothing in this module runs unless something registers a hook, and with an empty registry every entry point returns its input object unchanged, so a default run measures byte-for-byte what it measured before this module existed.
This is a separate module from spacr.measure on purpose. spacr.measure
imports matplotlib, skimage, cv2 and scipy at module scope — seconds of import
time and hundreds of MB of RSS — and an extension that only wants to register
a callable should not pay for that, nor should it risk the import cycle that
spacr.measure importing it back would create. This module imports numpy and
the stdlib, and nothing else from spaCR except spacr.errors.
The two hook kinds¶
Preprocessing — hook(channel_arrays, context) -> np.ndarray
channel_arraysis exactly the array the intensity measurements see: the intensity channels named bysettings['channels'], already selected out of the merged stack, shaped(Y, X, C)in 2-D or(Z, Y, X, C)in 3-D.contextis aPreprocessingContext. The hook returns a replacement of the same shape and the same dtype — seeapply_preprocessing_hooks()for why that is enforced rather than coerced.
Region filter — hook(context) -> np.ndarray[bool]
contextis aRegionContextcarrying the object type, the label mask, the ascending array of its non-zero label ids and (computed only if the hook asks for them) their centroids. The hook returns a boolean array oflen(context.labels):Truekeeps the object,Falsedrops it. Dropped labels are zeroed out of the mask before morphology or intensity is computed, so excluding 495 of 500 objects costs 5 objects’ worth of work, not 500.
Ordering¶
Every hook is registered with an integer priority (default 0) and runs
in ascending (priority, registration order). Ties keep registration order,
so the rule is fully deterministic. The two kinds then combine differently:
Preprocessing hooks form a pipeline: each one receives the previous one’s output. Order therefore matters, and
priorityis how two independent extensions agree on it without knowing about each other.Region filters intersect: every filter is handed the same, original set of labels, and an object is measured only if every filter kept it. Order does not change the outcome — deliberately, so that two workstreams adding a filter each cannot produce a result that depends on import order.
Registering the same function object again, or registering explicitly under a name that is already taken, replaces the existing entry rather than adding a second one. Re-running a GUI action that installs a flat-field correction therefore cannot apply that correction twice.
Errors¶
A hook that raises, returns None, or returns the wrong shape/dtype gets its
exception re-raised as MeasurementHookError (a
spacr.errors.ConfigurationError) naming the hook and the field. It is
raised inside _measure_crop_core’s try, so it takes the ordinary
per-field failure route: the traceback is printed, the field is recorded on the
RunLedger as a failure, and measurements.db is stamped incomplete. It
is never swallowed into a row of quietly-wrong numbers.
Reaching worker processes¶
spacr.measure.measure_crop() measures fields in a
multiprocessing.Pool. Under fork (the Linux default) a worker
inherits this module’s registries from the parent and hooks registered with
register_preprocessing_hook() work without additional setup. Under spawn /
forkserver (Windows, macOS, or SPACR_START_METHOD) a worker is a fresh
interpreter that has never seen them, so an in-process registration would be a
silent no-op in every worker. For that case set
HOOKS_ENV_VAR:
SPACR_MEASURE_HOOKS="mypkg.illumination:install,mypkg.roi:install"
Each entry is module:attribute naming a zero-argument callable that does
its own register_* calls. The environment is inherited by every start
method, so each worker installs the same hooks for itself. measure_crop
prints a warning if hooks were registered in-process and the pool will not
inherit them (warn_if_hooks_will_not_reach_workers()).
Exceptions¶
A registered measurement hook raised, or returned something unusable. |
Classes¶
Everything a preprocessing hook is told about the field it is given. |
|
Everything a region filter is told about one object type in one field. |
|
One entry in a hook registry. |
Functions¶
|
Run every preprocessing hook in order and return the transformed array. |
|
Zero out the objects every registered region filter agreed to drop. |
|
Empty both registries and re-arm |
|
Return a one-line-per-hook summary, or a line saying there are none. |
|
Return the registered preprocessing hooks in the order they will run. |
|
Return the registered region-filter hooks in reporting order. |
|
Register |
|
Register |
|
Remove a preprocessing hook by name. |
|
Remove a region-filter hook by name. |
Print a warning when in-process hooks cannot reach the worker pool. |
Module Contents¶
- exception spacr.measure_hooks.MeasurementHookError[source]¶
Bases:
spacr.errors.ConfigurationErrorA registered measurement hook raised, or returned something unusable.
A
spacr.errors.ConfigurationErrorrather than a per-field data error, because a broken hook is broken for every field on the plate: the message names the hook and tells the caller how to unregister it.Initialize self. See help(type(self)) for accurate signature.
- class spacr.measure_hooks.PreprocessingContext(*, file_name: str, channels: Sequence[int], settings: Mapping[str, Any], volumetric: bool = False, spacing: Sequence[float] | None = None)[source]¶
Everything a preprocessing hook is told about the field it is given.
- Variables:
file_name – the field’s
.npystem, e.g.plate1_A01_F001. A plate-wide illumination model keys its per-well gain off this.channels – the intensity channel indices selected out of the merged stack, in the order they appear along the last axis of the array the hook receives.
channel_arrays[..., i]is source channelchannels[i].settings – read-only view of the run settings dict.
volumetric – True when the field is a
(Z, Y, X, C)z-stack.spacing – voxel spacing tuple in the array’s index order, or None in 2-D (spaCR does not scale 2-D measurements; see
spacr.measure.resolve_measurement_spacing()).
Build the context passed to one preprocessing hook.
- Parameters:
file_name – field stem identifying the source array.
channels – source channel indices in array-axis order.
settings – run settings to expose through a read-only view.
volumetric – whether the source has a leading Z dimension.
spacing – voxel spacing in array-index order, or
Nonefor unscaled 2-D measurements.
- class spacr.measure_hooks.RegionContext(*, object_type: str, file_name: str, mask: numpy.ndarray, settings: Mapping[str, Any], spacing: Sequence[float] | None = None)[source]¶
Everything a region filter is told about one object type in one field.
- Variables:
object_type – one of
OBJECT_TYPES. Each type is offered separately, so a filter can act on one and wave the rest through withnp.ones(len(context.labels), bool). Note that culling only the cells cascades to their nuclei, pathogens and cytoplasm when the run has all ofcell_mask_dim,nucleus_mask_dimandpathogen_mask_dimset — that is the condition under which_measure_crop_corecalls_exclude_objects, which zeroes the child masks outside the surviving cells. Otherwise apply the same decision to each type, which is what a polygon test on the centroid does naturally.file_name – the field’s
.npystem.mask – the label mask,
(Y, X)or(Z, Y, X). Read-only.settings – read-only view of the run settings dict.
spacing – voxel spacing in the mask’s index order, or None in 2-D.
labels – ascending array of the mask’s non-zero label ids. The boolean array the hook returns is aligned with this, element for element.
centroids –
(len(labels), mask.ndim)float array of centroids in array index order —(row, col)in 2-D,(z, y, x)in 3-D — in pixels/voxels, unscaled byspacing. Computed on first access and cached, so a filter that rasterises its polygon and readsmaskdirectly never pays for it.
Build the context passed to one region-filter hook.
- Parameters:
object_type – object class represented by the mask.
file_name – field stem identifying the source array.
mask – label mask to expose through a read-only array view.
settings – run settings to expose through a read-only view.
spacing – voxel spacing in mask-index order, or
Nonein 2-D.
- property centroids: numpy.ndarray[source]¶
(N, ndim)centroids perlabels, in array index order.
- property labels: numpy.ndarray[source]¶
Ascending array of the mask’s non-zero label ids.
- class spacr.measure_hooks.RegisteredHook[source]¶
Bases:
NamedTupleOne entry in a hook registry.
- Parameters:
name – unique registry key accepted by the corresponding unregister function.
func – registered preprocessing or region-filter callable.
priority – execution priority; lower values run first.
sequence – monotonic registration number used to preserve order between equal priorities.
source –
"api"for direct registration or"env"for installation throughHOOKS_ENV_VAR.
- spacr.measure_hooks.apply_preprocessing_hooks(channel_arrays: numpy.ndarray, context: PreprocessingContext) numpy.ndarray[source]¶
Run every preprocessing hook in order and return the transformed array.
With no hook registered this returns
channel_arraysitself — the same object, not a copy — so the default measurement path is unchanged.Each hook must return an array of the same shape and the same dtype. The dtype is checked rather than coerced on purpose: a multiplicative correction naturally computes in float, and only its author knows whether going back to
uint16should round, truncate, clip or rescale. Silently picking one here would change every intensity column inmeasurements.dbby an amount nobody chose.- Parameters:
channel_arrays –
(Y, X, C)or(Z, Y, X, C)intensity array.context – the
PreprocessingContexthanded to each hook.
- Returns:
the array the measurement code should use.
- Raises:
MeasurementHookError – if a hook raises, returns None, or returns the wrong shape or dtype. The message names the hook.
- spacr.measure_hooks.apply_region_filter_hooks(mask: numpy.ndarray, *, object_type: str, file_name: str, settings: Mapping[str, Any], spacing: Sequence[float] | None = None) Tuple[numpy.ndarray, Tuple[int, ...]][source]¶
Zero out the objects every registered region filter agreed to drop.
With no filter registered — or with nothing dropped — this returns
maskitself, so the default path neither copies nor changes anything.Each filter sees the same original label set; the keep-masks are AND-ed, so an object survives only if every filter kept it and the outcome does not depend on registration order.
- Parameters:
mask – the label mask for
object_type.object_type – one of
OBJECT_TYPES.file_name – the field’s
.npystem, for error messages.settings – the run settings dict.
spacing – voxel spacing in the mask’s index order, or None in 2-D.
- Returns:
(filtered_mask, dropped_labels).- Raises:
MeasurementHookError – if a filter raises or returns something that is not a boolean array of
len(labels).
- spacr.measure_hooks.clear_measurement_hooks() None[source]¶
Empty both registries and re-arm
HOOKS_ENV_VARloading.Returns the module to its pristine, no-op state. Mainly for tests and for a GUI tearing down a session.
- spacr.measure_hooks.describe_hooks() str[source]¶
Return a one-line-per-hook summary, or a line saying there are none.
- spacr.measure_hooks.preprocessing_hooks() Tuple[RegisteredHook, ...][source]¶
Return the registered preprocessing hooks in the order they will run.
Also the cheap “is anything registered at all?” test — an empty tuple is falsy, and
spacr.measure._measure_crop_core()branches on it so a default run does not even build a context object.
- spacr.measure_hooks.region_filter_hooks() Tuple[RegisteredHook, ...][source]¶
Return the registered region-filter hooks in reporting order.
Their results are intersected, so this order does not affect which objects survive — only which hook is named first in a diagnostic.
- spacr.measure_hooks.register_preprocessing_hook(hook: Callable[..., Any], *, name: str | None = None, priority: int = 0) str[source]¶
Register
hook(channel_arrays, context) -> np.ndarray.The hook is called once per field, with the intensity channels named by
settings['channels']already selected and before a single feature is computed. It must return an array of the same shape and dtype. This is where a flat-field / illumination correction belongs.- Parameters:
hook – the callable.
contextis aPreprocessingContext.name – registry key. Defaults to
module.qualname; registering the same key (or the same function object) again replaces the entry instead of adding a second one.priority – lower runs first; ties keep registration order.
- Returns:
the key the hook was registered under — pass it to
unregister_preprocessing_hook().- Raises:
MeasurementHookError – if
hookis not callable orpriorityis not an int.
- spacr.measure_hooks.register_region_filter_hook(hook: Callable[..., Any], *, name: str | None = None, priority: int = 0) str[source]¶
Register
hook(context) -> np.ndarray[bool].The hook is called once per object type per field with a
RegionContext, and returns a boolean array aligned withcontext.labels:Truemeasures the object,Falsedrops it before any feature is computed. This is where a user-drawn ROI belongs.- Parameters:
hook – the callable.
name – registry key; see
register_preprocessing_hook().priority – affects only the order filters are reported in — the results are intersected, so the outcome is order-independent.
- Returns:
the key the hook was registered under.
- Raises:
MeasurementHookError – if
hookis not callable orpriorityis not an int.
- spacr.measure_hooks.unregister_preprocessing_hook(name: str) bool[source]¶
Remove a preprocessing hook by name.
- Parameters:
name – the key returned by
register_preprocessing_hook().- Returns:
True if something was removed, False if that name was not registered.
- spacr.measure_hooks.unregister_region_filter_hook(name: str) bool[source]¶
Remove a region-filter hook by name.
- Parameters:
name – the key returned by
register_region_filter_hook().- Returns:
True if something was removed, False otherwise.
- spacr.measure_hooks.warn_if_hooks_will_not_reach_workers(start_method: str) bool[source]¶
Print a warning when in-process hooks cannot reach the worker pool.
A
spawn/forkserverworker is a fresh interpreter with empty registries, so hooks registered through the Python API would apply to nothing at all — the exact silent no-op this module exists to avoid. Hooks installed viaHOOKS_ENV_VARare re-installed by each worker and are not warned about.- Parameters:
start_method – the pool’s multiprocessing start method.
- Returns:
True if a warning was printed.