Source code for phenotypic.detect._mad_hysteresis_detector
from __future__ import annotations
from typing import TYPE_CHECKING
import numpy as np
if TYPE_CHECKING:
from phenotypic._core._image import Image
from skimage.filters import apply_hysteresis_threshold
from skimage.morphology import remove_small_objects
from skimage.segmentation import clear_border
from ..abc_ import ThresholdDetector
[docs]
class MadHysteresisDetector(ThresholdDetector):
"""Detect colonies by MAD-based noise estimation and hysteresis thresholding.
Estimate the background noise floor using the Median Absolute Deviation
(MAD), then apply hysteresis thresholding with thresholds set as
multiples of the estimated noise standard deviation. Designed for
filter response maps (CED, Hessian, LoG, Frangi) where the noise
structure is approximately Gaussian and histogram-based methods produce
unstable thresholds. For a full comparison see
:doc:`/explanation/detection_strategies_compared`.
Args:
k_high: High-threshold multiplier. The high threshold is
``k_high * sigma_noise``; pixels above this seed connected
regions. Higher values are more conservative. Typical range:
3.0--8.0. Default: 5.0.
k_low: Low-threshold multiplier. The low threshold is
``k_low * sigma_noise``; pixels above this are included if
connected to a high-threshold seed. Must be less than
``k_high``. Typical range: 1.5--4.0. Default: 2.5.
min_size: Minimum colony area in pixels. Connected components
smaller than this are removed as noise. Default: 20.
connectivity: Pixel connectivity for labelling connected
components. ``1`` for 4-connectivity, ``2`` for
8-connectivity. Default: 2.
ignore_zeros: Exclude zero-intensity pixels from MAD computation.
Enable for plates with black borders or masked regions.
Default: True.
ignore_borders: Remove colonies touching image edges via
``clear_border()``. Recommended for grid-based colony counting
to eliminate partial colonies at plate boundaries. Default:
True.
Returns:
Image: Input image with ``objmask`` set to binary mask and
``objmap`` set to labeled connected components.
Raises:
ValueError: If ``k_low`` >= ``k_high``.
Best For:
* Filter response maps (CED, Hessian, LoG, Frangi) where the
noise floor is approximately Gaussian.
* Low-contrast colonies where signal is faint relative to
background texture and MAD provides a stable noise estimate.
* Standardised pipelines where multiplier-based thresholds
generalise across plates with varying colony density.
Consider Also:
* :class:`HysteresisDetector` when thresholds should be derived
from the intensity histogram rather than noise statistics.
* :class:`OtsuDetector` when the image is a raw intensity plate
with a bimodal histogram.
* :class:`ChanVeseDetector` when colonies have diffuse edges and
region-based segmentation is more appropriate.
See Also:
:doc:`/tutorials/notebooks/02_detecting_colonies`
Step-by-step tutorial for basic colony detection.
:doc:`/how_to/notebooks/choose_detection_algorithm`
Guide for selecting the right detector for your plate images.
:doc:`/explanation/detection_strategies_compared`
In-depth comparison of all detection strategies.
"""
def __init__(
self,
k_high: float = 5.0,
k_low: float = 2.5,
min_size: int = 20,
connectivity: int = 2,
ignore_zeros: bool = False,
ignore_borders: bool = True,
):
self.k_high = k_high
self.k_low = k_low
self.min_size = min_size
self.connectivity = connectivity
self.ignore_zeros = ignore_zeros
self.ignore_borders = ignore_borders
def _operate(self, image: Image) -> Image:
"""Apply MAD-based hysteresis thresholding to detect colonies.
Estimates background noise via the Median Absolute Deviation, computes
high and low thresholds as multiples of the estimated noise standard
deviation, then applies hysteresis thresholding followed by small-object
removal and optional border clearing.
Args:
image: The input image object. Must have ``detect_mat`` attribute
(detection matrix, typically a filter response map).
Returns:
Image: The input image with ``objmask`` attribute set to the binary
mask (True = detected colony pixel, False = background).
Raises:
ValueError: If k_low >= k_high.
"""
if self.k_low >= self.k_high:
raise ValueError(
f"k_low ({self.k_low}) must be less than k_high ({self.k_high})"
)
response = np.clip(image.detect_mat[:].astype(np.float64), 0, None)
# Select data for MAD computation
if self.ignore_zeros:
data = response[response != 0]
else:
data = response.ravel()
# Handle empty data
if data.size == 0:
image.objmask = np.zeros(response.shape, dtype=bool)
return image
# Compute MAD-based noise estimate
median_val = np.median(data)
mad = np.median(np.abs(data - median_val))
sigma_noise = 1.4826 * mad
# Handle uniform image (zero noise)
if sigma_noise == 0.0:
image.objmask = np.zeros(response.shape, dtype=bool)
return image
# Compute thresholds
t_high = self.k_high * sigma_noise
t_low = self.k_low * sigma_noise
# Hysteresis thresholding
mask = apply_hysteresis_threshold(response, t_low, t_high)
mask = mask.astype(bool)
# Remove small objects
mask = remove_small_objects(mask, min_size=self.min_size,
connectivity=self.connectivity)
# Optionally clear borders
if self.ignore_borders:
mask = clear_border(mask)
image.objmask = mask
return image
# Set the docstring so that it appears in the sphinx documentation
MadHysteresisDetector.apply.__doc__ = MadHysteresisDetector._operate.__doc__