Source code for phenotypic.detect._user_threshold

from __future__ import annotations

from typing import TYPE_CHECKING, Annotated

if TYPE_CHECKING:
    from phenotypic._core._image import Image
from skimage.segmentation import clear_border

from ..abc_ import ThresholdDetector
from ..sdk_.typing_ import TuneSpec


[docs] class UserThreshold(ThresholdDetector): """Detect colonies by applying a user-specified intensity threshold. Apply a fixed intensity cutoff to the plate detection matrix, producing a binary colony mask without any automatic threshold computation. This gives explicit control over detection sensitivity and is the preferred approach when empirical testing has identified an optimal threshold for a specific imaging setup. For a full comparison see :doc:`/explanation/detection_strategies_compared`. Best For: * Standardised imaging setups where the optimal threshold has been determined empirically and remains stable across plates. * Overriding automatic methods (Otsu, triangle, etc.) that consistently over- or under-segment on a particular plate type. * High-contrast plates where colonies are uniformly bright or dark relative to background and a single cutoff cleanly separates foreground from background. * Reproducibility-critical workflows where a fixed numeric threshold eliminates variability introduced by automatic selection. Consider Also: * :class:`OtsuDetector` when an automatic, parameter-free threshold is preferred and the histogram is bimodal. * :class:`HysteresisDetector` when colony intensity varies across the plate and a single threshold cannot capture all colonies. * :class:`TriangleDetector` when colonies are sparse and the histogram is skewed toward background. Args: threshold: Intensity cutoff for binary segmentation; pixels with intensity >= *threshold* become colony (True), others background (False). This is inherently a user choice -- there is no universal optimum, since it depends on the imaging setup and organism contrast. Valid range is bit-depth dependent: 0--255 for 8-bit, 0--65535 for 16-bit, 0.0--1.0 for float images; must be non-negative. Raising it is more conservative (fewer colonies, less noise); lowering it is more sensitive (more colonies, more noise). Default: 0.5. Start by inspecting the histogram for the valley between the background and colony peaks, then nudge it while watching whether faint colonies survive or agar texture leaks in. ignore_zeros: If True, exclude zero-intensity pixels from processing. Enable for plates with black borders or masked regions. Default: False. ignore_borders: If True (default), remove colonies touching image edges via ``clear_border()``. Recommended for grid-based colony counting; disable to retain peripheral colonies. Returns: Image: Input image with ``objmask`` set to a binary colony mask (True = colony, False = background). Assigning ``objmask`` rebuilds ``objmap`` from the binary mask via the accessor. Raises: ValueError: If *threshold* is negative. 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. """ # ``detect_mat`` follows the project's float-normalized [0, 1] intensity # convention, so the search window spans that range. The *valid* domain is # bit-depth dependent (0--255 / 0--65535 for integer images), so no tight # Field bound is added — only non-negativity is enforced in ``_operate``. threshold: Annotated[float, TuneSpec(0.0, 1.0)] = 0.5 ignore_zeros: bool = False ignore_borders: bool = True def _operate(self, image: Image) -> Image: """Apply manual binary thresholding to the detection matrix image. This function modifies the input image by applying a user-specified threshold to its enhanced matrix (``detect_mat``). Pixels with intensity >= threshold become foreground (True in the binary mask), pixels < threshold become background (False). The resulting binary mask is stored in the image's ``objmask`` attribute. Args: image: The input image object. Must have an ``detect_mat`` attribute (detection matrix for processing). Optionally uses ``bit_depth`` to validate threshold range. Returns: Image: The input image with its ``objmask`` attribute updated to the computed binary mask. Raises: ValueError: If threshold is negative or exceeds the image's intensity range (inferred from bit depth if available). """ enh_matrix = image.detect_mat[:] # Validate threshold range if self.threshold < 0: raise ValueError(f"Threshold must be non-negative, got {self.threshold}") # Apply threshold mask = enh_matrix >= self.threshold # Optionally clear borders mask = clear_border(mask) if self.ignore_borders else mask # Set objmask image.objmask = mask return image
# Set the docstring so that it appears in the sphinx documentation UserThreshold.apply.__doc__ = UserThreshold._operate.__doc__