Source code for phenotypic.detect._chan_vese_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 segmentation, morphology
from scipy import ndimage

from ..abc_ import ObjectDetector


[docs] class ChanVeseDetector(ObjectDetector): """Detect colonies by region-based level-set segmentation using the Chan-Vese energy functional. Partition the image into foreground and background by minimising an energy functional based on intensity homogeneity within each region. Because segmentation is driven by region statistics rather than edge gradients, colonies with diffuse boundaries, uneven texture, or gradual transitions into the agar background are captured cleanly. For algorithm details see :doc:`/explanation/detection_strategies_compared`. Args: mu: Edge-length penalty weight. Higher values produce smoother, rounder colony outlines; lower values preserve fine boundary detail. Typical range: 0.05--1.0. Default: 0.25. lambda1: Weight for intensity deviation inside detected regions. Increase to enforce uniform colony brightness. Default: 1.0. lambda2: Weight for intensity deviation outside detected regions. Increase when the agar background is homogeneous. Default: 1.0. max_num_iter: Maximum level-set iterations. Increase for complex images where convergence is slow; decrease for faster (but potentially incomplete) segmentation. Default: 500. tol: Convergence tolerance (L2 norm of level-set change). Smaller values require tighter convergence but more iterations. Default: 1e-3. dt: Step-size multiplier for level-set evolution. Larger values evolve faster but risk instability. Default: 0.5. init_level_set: Initialisation method for the level set. Accepted values: ``"checkerboard"``, ``"disk"``, ``"small disk"``. Checkerboard is robust for most plate images. Default: ``"checkerboard"``. min_size: Minimum colony area in pixels. Connected components smaller than this are removed as noise. Default: 50. connectivity: Pixel connectivity for labelling connected components. ``1`` for 4-connectivity, ``2`` for 8-connectivity. Default: 2. Returns: Image: Input image with ``objmask`` set to binary mask and ``objmap`` set to labeled connected components. Raises: ValueError: If ``init_level_set`` is not a recognised initialisation method. Best For: * Mucoid or fuzzy colonies whose edges lack sharp intensity gradients. * Plates with uneven colony pigmentation or heterogeneous surface texture that fragments threshold-based masks. * Low-contrast imaging where colony and agar intensities are similar. * Morphology studies where smooth, accurate colony outlines are required. Consider Also: * :class:`OtsuDetector` when colonies and background form two clear histogram peaks and a fast global threshold suffices. * :class:`HysteresisDetector` when colony brightness varies but edges are still reasonably sharp. * :class:`CannyDetector` when colonies are best delineated by edge contrast rather than region homogeneity. References: [1] T. F. Chan and L. A. Vese, "Active contours without edges," *IEEE Trans. Image Process.*, vol. 10, no. 2, pp. 266--277, 2001. 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, mu: float = 0.25, lambda1: float = 1.0, lambda2: float = 1.0, max_num_iter: int = 500, tol: float = 1e-3, dt: float = 0.5, init_level_set: str = "checkerboard", min_size: int = 50, connectivity: int = 2, ): super().__init__() self.mu = mu self.lambda1 = lambda1 self.lambda2 = lambda2 self.max_num_iter = max_num_iter self.tol = tol self.dt = dt self.init_level_set = init_level_set self.min_size = min_size self.connectivity = connectivity def _operate(self, image: Image | GridImage) -> Image: """Apply Chan-Vese level-set segmentation to detect colonies. Reads the detection matrix, runs the Chan-Vese algorithm to produce a binary segmentation, then labels connected components and filters small objects. Sets image.objmap with consecutive colony labels. Args: image: Input image with ``detect_mat`` attribute (2D grayscale detection matrix). Returns: Image: Input image with ``objmap`` set to labeled colony map (consecutive integer labels, background = 0). """ enhanced_matrix = image.detect_mat[:] # Run Chan-Vese segmentation → boolean mask cv_mask = segmentation.chan_vese( enhanced_matrix, mu=self.mu, lambda1=self.lambda1, lambda2=self.lambda2, max_num_iter=self.max_num_iter, tol=self.tol, dt=self.dt, init_level_set=self.init_level_set, ) # Label connected components objmap, _ = ndimage.label( cv_mask, structure=ndimage.generate_binary_structure(2, self.connectivity), ) # Remove small objects 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) # Set objmap and relabel for consecutive IDs image.objmap[:] = objmap image.objmap.relabel(connectivity=self.connectivity) return image
# Set the docstring so that it appears in the sphinx documentation ChanVeseDetector.apply.__doc__ = ChanVeseDetector._operate.__doc__