Source code for phenotypic.detect._canny_detector

from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from phenotypic._core._grid_image import GridImage
    from phenotypic._core._image import Image

from skimage import feature, morphology
from scipy import ndimage

from phenotypic.abc_ import ThresholdDetector


[docs] class CannyDetector(ThresholdDetector): """Detect colonies by Canny edge detection and enclosed-region labelling. Apply multi-stage Canny edge detection (Gaussian smoothing, gradient magnitude, non-maximum suppression, hysteresis thresholding) to produce thin, connected boundary contours, then label the enclosed regions as individual colonies. This edge-based approach segments colonies by boundary contrast rather than absolute intensity, making it effective on plates with uneven illumination or translucent colonies. For a full comparison see :doc:`/explanation/detection_strategies_compared`. Args: sigma: Gaussian smoothing standard deviation before edge detection (default 1.0). Higher values suppress noise and spurious edges but may blur fine boundaries or merge nearby colonies. Typical range: 0.5--3.0. Start with 1.0 for clean images; increase to 2.0--3.0 for noisy scans. low_threshold: Lower hysteresis threshold (default 0.1). When *use_quantiles* is True, this is a fraction (0.1 = retain edges stronger than 10 % of gradient magnitudes). When False, an absolute gradient value. Increase to suppress weak noise edges. Typical quantile range: 0.05--0.2. high_threshold: Upper hysteresis threshold seeding edge traces (default 0.2). Same interpretation as *low_threshold*. Must exceed *low_threshold*. Typical quantile range: 0.1--0.4. use_quantiles: If True (default), interpret thresholds as gradient- magnitude quantiles, adapting automatically to image contrast. Set to False for absolute gradient values when imaging conditions are tightly controlled. min_size: Minimum object area in pixels (default 50). Increase to filter dust, debris, and plate-edge artefacts; decrease to retain tiny colonies. Typical range: 20--500. invert_edges: If True (default), label enclosed regions as colony objects. If False, label edge pixels themselves (useful for debugging edge quality). connectivity: Connectivity for region labelling (1 = 4-connected, 2 = 8-connected; default 2). Higher connectivity bridges diagonal gaps in edge contours. Returns: Image: Input image with ``objmap`` set to a labelled colony map where each enclosed region receives a unique integer label. ``objmask`` is derived from the non-zero region of the label map. Raises: ValueError: If *high_threshold* is less than *low_threshold* or if threshold values are outside the valid range. Best For: * Well-separated colonies on solid media where colony edges are sharper than intensity differences relative to background. * Translucent or lightly pigmented colonies that lack sufficient intensity contrast for threshold-based methods. * Plates with heterogeneous colony texture or pigmentation that fragments under watershed or simple thresholding. * Images with moderate vignetting where edge contrast is preserved even though absolute intensity varies spatially. Consider Also: * :class:`OtsuDetector` when colonies differ from background primarily in brightness rather than edge contrast. * :class:`WatershedDetector` when touching colonies must be split by region-growing from interior seeds. * :class:`HysteresisDetector` when dual-threshold intensity segmentation is preferred over edge-based detection. References: [1] J. Canny, "A computational approach to edge detection," *IEEE Trans. Pattern Anal. Mach. Intell.*, vol. 8, no. 6, pp. 679--698, 1986. 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. """
[docs] def __init__( self, sigma: float = 1.0, low_threshold: float = 0.1, high_threshold: float = 0.2, use_quantiles: bool = True, min_size: int = 50, invert_edges: bool = True, connectivity: int = 2, ): """ Parameters: sigma (float): Gaussian smoothing strength before edge detection. Start with 1-2 for clean images; increase for noisy scans to suppress spurious edges. Keep below typical colony width to avoid merging. low_threshold (float): Lower hysteresis threshold. If use_quantiles=True, a fraction (e.g., 0.1 = retain edges stronger than 10% of gradients). If False, an absolute gradient magnitude. Increase to suppress weak edges from noise; decrease to recover faint colony boundaries. high_threshold (float): Upper hysteresis threshold. Seeds edge traces. If use_quantiles=True, a fraction (e.g., 0.2 = top 80% gradients); if False, an absolute magnitude. Raise to focus on strong boundaries; lower to include fainter edges. Must exceed low_threshold. use_quantiles (bool): Interpret thresholds as quantiles (True, default) or absolute values (False). Quantiles adapt to image contrast automatically, reducing manual tuning. min_size (int): Minimum object area in pixels. Increase to filter out dust, debris, and small artifacts; decrease to retain tiny colonies. invert_edges (bool): If True (default), label enclosed regions as objects (colonies). If False, label edge pixels (for atypical cases like ring colonies or edge quality checks). connectivity (int): Connectivity for labeling regions (1 or 2 in 2D). Higher values merge diagonally touching pixels, useful for bridging fragmented boundaries but may merge touching colonies. """ super().__init__() self.sigma = sigma self.low_threshold = low_threshold self.high_threshold = high_threshold self.use_quantiles = use_quantiles self.min_size = min_size self.invert_edges = invert_edges self.connectivity = connectivity
def _operate(self, image: Image | GridImage) -> Image: enhanced_matrix = image.detect_mat[:] # Apply Canny edge detection edges = feature.canny( image=enhanced_matrix, sigma=self.sigma, low_threshold=self.low_threshold, high_threshold=self.high_threshold, use_quantiles=self.use_quantiles, ) # Invert edges to get regions (colonies) if requested if self.invert_edges: regions = ~edges else: regions = edges # Label connected components objmap, _ = ndimage.label( regions, structure=ndimage.generate_binary_structure(2, self.connectivity) ) # Remove small objects (pass a boolean array when only one label # exists to avoid skimage's "single-label" ambiguity warning). if objmap.max() <= 1: objmap = morphology.remove_small_objects( objmap.astype(bool), min_size=self.min_size ).astype(objmap.dtype) else: objmap = morphology.remove_small_objects(objmap, min_size=self.min_size) # Ensure correct dtype if objmap.dtype != image._OBJMAP_DTYPE: objmap = objmap.astype(image._OBJMAP_DTYPE) # Relabel to ensure consecutive labels image.objmap[:] = objmap image.objmap.relabel(connectivity=self.connectivity) return image
# Set the docstring so that it appears in the sphinx documentation CannyDetector.apply.__doc__ = CannyDetector._operate.__doc__