Custom regionprops Implementation
Celldetective uses a custom regionprops implementation
(celldetective.regionprops) in place of scikit-image’s
skimage.measure.regionprops_table for all single-cell measurements.
This page documents how it differs from the original and why.
Overview
The custom implementation is a thin subclass of scikit-image’s
RegionProperties, extended with three capabilities that the original
does not support:
Unmasked intensity image — thresholding inside the bounding box works correctly.
Named channel support — channel indices are associated with semantic names.
Channel-targeted extra properties — a function can declare which channel it applies to via the default value of a
target_channelparameter.
Additionally, regionprops_table silently drops properties whose values
are all NaN across all cells, rather than propagating empty columns.
Difference 1: Unmasked intensity image
In scikit-image’s original implementation, the image_intensity property
returns the bounding-box crop multiplied by the cell mask:
# scikit-image original
return self._intensity_image[self.slice] * image # image = boolean mask
Background pixels (outside the cell) are zeroed before the measurement function sees them.
Celldetective’s CustomRegionProps overrides this property to return the
raw bounding-box crop without masking:
# CustomRegionProps
return self._intensity_image[self.slice] # no masking
Why: threshold-based custom measurements (e.g. area_dark_intensity)
need to classify pixels as “dark” relative to the actual intensity distribution
inside the cell. If the background were zeroed, those zero-valued pixels would
be misclassified as dark signal, inflating the result. With unmasked crops, the
threshold is applied to real pixel values; the cell mask is then used separately
to restrict which pixels are counted.
Warning
Because background pixels are not zeroed, any custom function that uses
raw pixel values (e.g. mean, sum) without applying regionmask will
include pixels outside the cell boundary. Always restrict computations to
the region of interest:
def my_measurement(regionmask, intensity_image, **kwargs):
return np.mean(intensity_image[regionmask]) # correct
# not: np.mean(intensity_image) # would include background
Difference 2: Named channel support
Standard regionprops_table has no concept of channel names — channels are
identified only by integer index.
regionprops_table in Celldetective accepts an optional channel_names
list that is passed down to every CustomRegionProps instance. When
present, channel_names is used to:
Validate that the number of names matches the number of channels in the image (raises
ValueErrorif not).Enable channel targeting in extra-property functions (see below).
Drive the column-renaming step in
data_cleaning.py, which replaces the rawintensityprefix with the actual channel name in output column names (e.g.intensity_mean-0→GFP_mean).
Difference 3: Channel-targeted extra properties
Extra-property functions follow one of two calling conventions:
- All-channel functions (no
target_channelparameter) Called once per cell per channel.
intensity_imagereceives a 2-D single-channel crop for each iteration:def my_measurement(regionmask, intensity_image, **kwargs): return np.mean(intensity_image[regionmask])
- Channel-targeted functions (
target_channelwith a default value) The function declares which channel it applies to through the default value of
target_channel:def my_measurement(regionmask, intensity_image, target_channel='GFP', **kwargs): return np.mean(intensity_image[regionmask])
The framework uses
inspect.signatureto read the default value at runtime. The function is then called once, receiving only that channel’s 2-D crop. All other channel output slots are filled withNaN.target_channelis never passed as an argument; only its default is inspected.If the named channel is absent from the experiment’s
channel_names, a warning is logged and all output slots remainNaN.
Decision table:
Signature |
Number of calls per cell |
|
|---|---|---|
No |
Once per channel (N calls) |
Single-channel 2-D crop for each channel |
|
Once (for the named channel) |
Single-channel 2-D crop for |
Difference 4: All-NaN property filtering
Scikit-image’s regionprops_table returns a column for every requested
property regardless of whether any cell produced a valid value.
Celldetective’s version tests each property across all cells before assembling the output table:
good_props = []
for prop in properties:
try:
nan_test = [np.isnan(getattr(r, prop)) for r in regions]
if not np.all(nan_test):
good_props.append(prop)
except AttributeError:
logger.warning(f"Could not measure {prop}... Skip...")
Properties where every cell returned NaN (e.g. a channel-targeted
function whose channel is absent from the experiment) are silently excluded
from the output DataFrame. This keeps the measurement table free of
uninformative all-NaN columns without requiring the caller to clean up.
See also
How to write a custom measurement — how to write a custom measurement function that integrates with this system.
ref_measurements — reference for all built-in measurement column names.