Source code for phenotypic.detect._secondary_otsu
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from phenotypic._core._image import Image
import numpy as np
from scipy.ndimage import labeled_comprehension
from skimage.filters import threshold_otsu
from skimage.measure import label
from phenotypic.abc_ import ThresholdDetector
def _safe_otsu(values: np.ndarray) -> float:
"""Compute Otsu threshold, returning -inf if not possible."""
if len(values) < 2 or values.min() == values.max():
return -np.inf
try:
return threshold_otsu(values)
except ValueError:
return -np.inf
[docs]
class SecondaryOtsuDetector(ThresholdDetector):
"""Detect colonies by two-stage Otsu thresholding with per-object boundary refinement.
Apply an initial global Otsu threshold (or reuse an existing ``objmask``),
then re-threshold each detected object independently using its own intensity
distribution. This sharpens colony boundaries on plates where colonies vary
in brightness, removing soft halos while preserving colony cores. For a
full comparison of detection strategies see
:doc:`/explanation/detection_strategies_compared`.
Best For:
- Refining boundaries after an initial global threshold that leaves
soft or blurry colony edges.
- Heterogeneous plates where colonies differ in pigmentation or optical
density across the plate surface.
- Suppressing preprocessing halos that expand colony outlines beyond
their true boundaries.
Consider Also:
- :class:`OtsuDetector` when a single global threshold already
produces clean colony boundaries.
- :class:`HysteresisDetector` when colony intensity varies smoothly
and dual-threshold expansion is more appropriate than per-object
refinement.
- :class:`RankOtsuDetector` when spatially varying illumination is the
primary cause of boundary inaccuracy.
Returns:
Image: Input image with ``objmask`` set to the refined binary colony
mask and ``objmap`` set to labeled connected components.
References:
[1] N. Otsu, "A threshold selection method from gray-level
histograms," *IEEE Trans. Syst., Man, Cybern.*, vol. 9, no. 1,
pp. 62--66, 1979.
See Also:
:doc:`/tutorials/notebooks/02_detecting_colonies` for a step-by-step
tutorial on basic colony detection.
:doc:`/how_to/notebooks/choose_detection_algorithm` for guidance on
selecting the right detector for your plate images.
:doc:`/explanation/detection_strategies_compared` for an in-depth
comparison of all thresholding strategies.
"""
def _operate(self, image: Image) -> Image:
"""Apply Otsu thresholding independently to each object in the mask.
If no object map exists, performs initial Otsu on the full image first,
then applies per-object Otsu refinement to each detected region.
"""
detect_mat = image.detect_mat[:]
# If there are no objects, perform an initial global Otsu
if image.num_objects == 0:
initial_mask = detect_mat >= threshold_otsu(detect_mat)
else:
initial_mask = image.objmask[:]
# Label connected components in the initial mask
labeled_mask = label(initial_mask)
num_objects = labeled_mask.max()
if num_objects == 0:
image.objmask = initial_mask
return image
# Compute Otsu threshold for each object (vectorized across all objects)
# Returns array of thresholds indexed by object id (1 to num_objects)
thresholds = labeled_comprehension(
detect_mat, labeled_mask, range(1, num_objects + 1),
_safe_otsu, float, -np.inf
)
# Build threshold lookup: index 0 = background (inf), indices 1..n = object thresholds
# Using inf for background ensures those pixels stay False
threshold_lookup = np.concatenate([[np.inf], thresholds])
# Create per-pixel threshold map via label indexing (vectorized)
threshold_map = threshold_lookup[labeled_mask]
# Vectorized comparison: pixels above their object's threshold
image.objmask = detect_mat >= threshold_map
return image