Source code for phenotypic.detect._rank_otsu

from __future__ import annotations

import numpy as np
import skimage.filters.rank as rank
from skimage.util import img_as_ubyte
from phenotypic.abc_ import ObjectDetector
from phenotypic.sdk_ import FootprintMixin
from phenotypic.sdk_.typing_ import TuneSpec
from typing import Annotated, Literal, TYPE_CHECKING

if TYPE_CHECKING:
    from phenotypic._core._image import Image


[docs] class RankOtsuDetector(ObjectDetector, FootprintMixin): """Detect colonies by adaptive local Otsu thresholding within a sliding footprint. Compute an Otsu threshold independently for every pixel using a local spatial neighbourhood, producing a per-pixel adaptive threshold map. This compensates for vignetting, lighting gradients, and spatially varying agar colour that cause a single global threshold to over- or under-segment parts of the plate. For a full comparison see :doc:`/explanation/detection_strategies_compared`. Best For: - Plates with vignetting, hot-spots, or centre-to-edge illumination gradients. - Large-format plates (384-well or larger) where lighting uniformity is difficult to achieve. - Images with spatially varying agar colour or reflectance that shifts across the plate surface. Consider Also: - :class:`OtsuDetector` when illumination is uniform and a fast global threshold suffices. - :class:`HysteresisDetector` when colony brightness varies but spatial illumination is reasonably even. - :class:`ChanVeseDetector` when colonies have diffuse edges and region-based segmentation is more appropriate. Args: shape: Footprint shape for the local neighbourhood. Disk and diamond are rotationally symmetric; square is faster. Accepted values: ``'square'``, ``'diamond'``, ``'disk'``. Default: ``'square'``. width: Footprint width (or radius for disk/diamond) in pixels. Larger values smooth the threshold spatially (less local adaptation); smaller values track finer illumination changes but may over-segment. When ``None``, auto-scales to ``min(height, width) // 8``. Default: None. ignore_zeros: Exclude zero-intensity pixels from the local threshold computation. Enable for plates with black borders or masked regions. Default: False. Returns: Image: Input image with ``objmask`` set to binary mask and ``objmap`` set to labeled connected components. Raises: ValueError: If ``shape`` is not one of the accepted values or ``width`` is not positive. 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. """ shape: Literal["square", "diamond", "disk"] = "square" width: Annotated[int | None, TuneSpec(3, 31)] = None ignore_zeros: bool = False def _operate(self, image: Image) -> Image: detect_mat = img_as_ubyte(image.detect_mat[:]) if self.ignore_zeros: mask = np.zeros(image.shape[:2], dtype=np.uint8) mask[detect_mat.nonzero()] = 1 mask = mask > 0 else: mask = None width = min(image.shape[:2]) // 8 if self.width is None else self.width image.objmask[:] = detect_mat >= rank.otsu( image=detect_mat, footprint=self._make_footprint( shape=self.shape, width=width, ), mask=mask, ) return image