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__