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:

  1. Unmasked intensity image — thresholding inside the bounding box works correctly.

  2. Named channel support — channel indices are associated with semantic names.

  3. Channel-targeted extra properties — a function can declare which channel it applies to via the default value of a target_channel parameter.

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 ValueError if not).

  • Enable channel targeting in extra-property functions (see below).

  • Drive the column-renaming step in data_cleaning.py, which replaces the raw intensity prefix with the actual channel name in output column names (e.g. intensity_mean-0GFP_mean).

Difference 3: Channel-targeted extra properties

Extra-property functions follow one of two calling conventions:

All-channel functions (no target_channel parameter)

Called once per cell per channel. intensity_image receives 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_channel with 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.signature to 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 with NaN. target_channel is 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 remain NaN.

Decision table:

Signature

Number of calls per cell

intensity_image content

No target_channel

Once per channel (N calls)

Single-channel 2-D crop for each channel

target_channel='name'

Once (for the named channel)

Single-channel 2-D crop for 'name' only; other slots → NaN

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.