Source code for phenotypic.detect._chan_vese_detector

from __future__ import annotations

from typing import TYPE_CHECKING, Annotated

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

from pydantic import Field
from skimage import segmentation, morphology
from scipy import ndimage

from ..abc_ import ObjectDetector
from ..sdk_.typing_ import TuneSpec


[docs] class ChanVeseDetector(ObjectDetector): """Detect colonies by evolving a level-set contour that minimises region-intensity variance via the Chan-Vese method. Partition ``detect_mat`` into foreground and background by iterating a level-set that minimises the summed intensity variance inside and outside the detected regions. Because the energy is driven by region statistics rather than edge gradients, colonies with diffuse boundaries, gradual transitions into agar, or heterogeneous surface texture are captured cleanly as smooth, well-regularised outlines. The contour is initialised and evolved until the per-pixel change falls below ``tol`` or ``max_num_iter`` is reached, after which the binary mask is labelled into individual connected-component colonies. For a comparison of region-based and edge-based detection strategies, see :doc:`/explanation/detection_strategies_compared`. Best For: - Mucoid or spreading colonies whose boundaries lack sharp intensity gradients. - Plates with uneven colony pigmentation or sector-variant morphology that fragments threshold-based masks. - Low-contrast imaging conditions where colony and agar intensities overlap in the histogram. - Morphology studies requiring smooth, well-regularised colony outlines rather than jagged threshold masks. Consider Also: - :class:`OtsuDetector` when colonies and background form two distinct histogram peaks and a fast global threshold suffices. - :class:`HysteresisDetector` when colony brightness varies but edges remain reasonably sharp. - :class:`CannyDetector` when colonies are best delineated by edge contrast rather than interior region homogeneity. - :class:`WatershedDetector` when touching, compact colonies must be separated after an initial binary detection. Args: mu: Contour-length penalty weight in the Chan-Vese energy functional. Higher values produce shorter, smoother outlines and merge nearby colonies; lower values allow the contour to track fine boundary detail at the cost of noise sensitivity. Typical range: 0--1 (lower for diffuse mucoid edges, higher for noisy plates). Default: 0.25. lambda1: Fidelity weight for intensity deviation inside detected regions. Increasing ``lambda1`` forces the contour to enclose pixels whose intensity matches the estimated colony mean more tightly. Equal weights (``lambda1`` = ``lambda2`` = 1.0) are the standard default recommended by Chan & Vese (2001); use asymmetric weights when colony and agar intensity distributions differ strongly. Default: 1.0. lambda2: Fidelity weight for intensity deviation outside detected regions (background). Increasing ``lambda2`` relative to ``lambda1`` penalises background heterogeneity more strongly (follows directly from the energy functional); useful when agar is uniform but colony texture is heterogeneous. Default: 1.0. max_num_iter: Hard cap on level-set evolution iterations. The algorithm stops early if ``tol`` is satisfied. Increase for complex or slowly converging shapes (mucoid, low-contrast); decrease to trade boundary accuracy for speed. Typical range: 100--1000. Default: 500. tol: Early-stopping criterion: per-pixel level-set change normalised by image area. Smaller values give more precise convergence but require more iterations; values below the noise floor exhaust ``max_num_iter`` without improving results. Typical range: 1e-5--1e-2. Default: 1e-3. dt: Step-size multiplier for each level-set iteration. Larger values converge faster but may lead to convergence problems on complex or low-contrast shapes; keep within 0.1--1.0 on real plate images. Default: 0.5. init_level_set: Shape used to initialise the level-set function before evolution. Accepted values: ``"checkerboard"`` (sin × sin pattern; fast multi-front convergence, well-suited to arrayed plates with spatially distributed colonies), ``"disk"`` (large circle shrinking inward; slower but more likely to detect implicit edges), ``"small disk"`` (small expanding circle at the image centre). Default: ``"checkerboard"``. min_size: Minimum connected-component area in pixels. Components smaller than this are discarded after labelling. Increase to remove condensation droplets, debris, or noise fragments; decrease for high-density formats where each colony occupies few pixels. Typical range: 10--500. Default: 50. connectivity: Pixel connectivity for connected-component labelling. ``1`` uses 4-connectivity (orthogonal neighbours only; keeps colonies that touch only at corners separate); ``2`` uses 8-connectivity (all neighbours including diagonals; merges corner-touching regions). Default: 2. Returns: Image: Input image with ``objmask`` set to the binary segmentation mask and ``objmap`` set to consecutively labelled connected components. Raises: ValueError: If ``init_level_set`` is not one of the recognised initialisation strings. References: [1] T. F. Chan and L. A. Vese, "Active contours without edges," *IEEE Trans. Image Process.*, vol. 10, no. 2, pp. 266--277, Feb. 2001. [2] P. Getreuer, "Chan-Vese segmentation," *Image Process. On Line*, vol. 2, pp. 214--224, 2012. See Also: :doc:`/tutorials/notebooks/02_detecting_colonies` Step-by-step tutorial for basic colony detection on plate images. :doc:`/how_to/notebooks/choose_detection_algorithm` Guide for selecting the right detector for your imaging conditions. :doc:`/explanation/detection_strategies_compared` In-depth comparison of all detection strategies and their failure modes. """ mu: Annotated[float, TuneSpec(0.0, 1.0)] = 0.25 lambda1: Annotated[float, TuneSpec(0.5, 2.0)] = 1.0 lambda2: Annotated[float, TuneSpec(0.5, 2.0)] = 1.0 max_num_iter: Annotated[int, TuneSpec(100, 1000)] = 500 tol: Annotated[float, TuneSpec(1e-5, 1e-2, log=True)] = 1e-3 dt: Annotated[float, TuneSpec(0.1, 1.0)] = 0.5 init_level_set: str = "checkerboard" min_size: Annotated[int, TuneSpec(10, 500, log=True)] = 50 connectivity: Annotated[int, TuneSpec(categories=[1, 2])] = Field(2, ge=1, le=2) 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__