Source code for phenotypic.abc_._threshold_detector

from ._object_detector import ObjectDetector
from abc import ABC


# <<Interface>>
[docs] class ThresholdDetector(ObjectDetector, ABC): """Marker ABC for threshold-based colony detection strategies. ThresholdDetector specializes ObjectDetector for algorithms that detect colonies by converting grayscale intensity to a binary mask via thresholding. Unlike edge-based (Canny) or peak-based (RoundPeaks) approaches, thresholding works by partitioning intensity space: pixels above a threshold value become foreground (colonies), pixels below become background. **Why threshold-based detection?** Thresholding is ideal when: - **Clear intensity separation:** Colonies have distinctly different intensity than background (common on high-contrast agar plates or with good lighting). - **Simplicity and speed:** Single-pass algorithms (no iterative edge tracking or distance computation). - **Robustness to morphology:** Works equally well on round and irregular colonies (unlike peak-based approaches that assume circular shapes). - **Well-defined boundary:** Sharp transitions between foreground and background (less effective on blurry or faded colonies). **Thresholding strategies implemented in PhenoTypic** - **Otsu's method:** Finds threshold that minimizes within-class variance. Automatic, global, works for most balanced foreground/background histograms. - **Li's method:** Minimizes Kullback-Leibler divergence. Good for dark foreground on bright background. - **Yen's method:** Maximizes Yen's object variance criterion. Good for sharply defined objects. - **Triangle method:** Connects histogram extrema. Works well for non-overlapping bimodal distributions. - **Isodata/Iterative selection:** Iteratively refines threshold based on class means. Robust but slower. - **Mean/Minimum methods:** Simple heuristic thresholds (average or minimum intensity). Fast, useful for baseline or preprocessing. - **Local/Adaptive thresholding:** Applies threshold per neighborhood instead of globally. Handles uneven illumination on agar. **When to subclass ThresholdDetector vs ObjectDetector directly** - **Subclass ThresholdDetector if:** - Your algorithm produces objmask and objmap via thresholding (any strategy). - You want to signal intent: "this detector groups with other thresholding methods." - You may add shared utility methods later (e.g., post-processing filters). - You value categorization for discovery and code organization. - **Subclass ObjectDetector directly if:** - Your algorithm uses edge detection (Canny), peak finding, watershed, or morphological operations (not thresholding). - Your approach doesn't fit the threshold → binary mask → label pattern. **Typical workflow: enhance → threshold → label → refine** Most ThresholdDetector implementations follow this pipeline: 1. **Read enhanced grayscale:** ``enh = image.enh_gray[:]`` (preprocessed for contrast and noise suppression). 2. **Compute threshold:** Use chosen strategy (Otsu, Li, Yen, etc.) to find optimal threshold value from histogram. 3. **Create binary mask:** ``mask = enh > threshold`` or ``mask = enh >= threshold`` (test both if edge pixels ambiguous). 4. **Post-process (optional):** Remove small noise, clear borders, morphological cleanup to improve mask quality. 5. **Label connected components:** Use ``scipy.ndimage.label()`` to assign unique integer IDs to each colony (objmap). 6. **Set both outputs:** ``image.objmask = mask``, ``image.objmap = labeled_map``. **Parameter tuning guidance** Threshold-based detectors typically expose parameters that affect detection quality: - **Threshold value:** For manual methods (Mean, Minimum), directly controls the intensity cutoff. Higher values → fewer, larger colonies; lower → more, noisier. - **Block size (local methods):** Size of neighborhood for adaptive threshold. Larger blocks → smoother mask but may miss small colonies; smaller blocks → more detail but noise-prone. - **Post-processing parameters:** ``ignore_zeros`` (skip pure black pixels in threshold computation), ``ignore_borders`` (remove edge-touching objects), ``min_size`` (filter objects below pixel count). **Comparison with other detection strategies** - **Edge-based (CannyDetector):** Finds intensity gradients (colony boundaries). Better for faint or merged colonies; requires gradient-based preprocessing. - **Peak-based (RoundPeaksDetector):** Assumes round peaks; grows from maxima. Excellent for well-separated round colonies; fails on irregular shapes. - **Threshold-based (this class):** Direct intensity partitioning. Robust, fast, works for any shape; requires good intensity separation. **Common pitfalls and remedies** - **Over-segmentation (too many small objects):** Use ``ignore_zeros=True`` to skip dark pixels, apply morphological opening, or use ObjectRefiner with ``remove_small_objects(min_size=...)``. - **Under-segmentation (merged colonies):** Local thresholding, morphological closing, or watershed post-processing. - **False positives at edges:** Use ``ignore_borders=True`` or ``clear_border()`` in post-processing. - **Uneven illumination:** Apply enhancement (contrast stretching, illumination correction) before detection, or use local thresholding. **Example implementations** See concrete subclasses for reference patterns: - **OtsuDetector:** Global automatic thresholding via Otsu's variance minimization. - **LiDetector, YenDetector, TriangleDetector:** Alternative global strategies from scikit-image.filters. - **MeanDetector, MinimumDetector:** Simple heuristic thresholds. **Interface specification** Subclasses of ThresholdDetector must: 1. Inherit from ThresholdDetector (which provides ObjectDetector's interface). 2. Implement ``_operate(image: Image) -> Image`` as a static method. 3. Within ``_operate()``: - Read ``image.enh_gray[:]`` (and optionally ``image.rgb[:], image.gray[:]``). - Compute threshold (automatically or from parameter). - Generate binary mask via comparison: ``mask = enh > threshold``. - Label connected components: ``labeled, _ = ndimage.label(mask)``. - Set both outputs: ``image.objmask = mask``, ``image.objmap = labeled``. - Return modified image. 4. Add to ``phenotypic.detect.__init__.py`` exports for public discovery. Notes: This is a marker ABC with no additional methods. It exists to categorize threshold-based detectors in the class hierarchy and enable flexible discovery and code organization. Examples: .. dropdown:: Detect colonies using Otsu's automatic threshold .. code-block:: python from phenotypic import Image from phenotypic.detect import OtsuDetector # Load a plate image plate = Image.from_image_path("agar_plate.jpg") # Apply Otsu threshold detection detector = OtsuDetector(ignore_zeros=True, ignore_borders=True) detected = detector.apply(plate) # Access results mask = detected.objmask[:] # Binary mask objmap = detected.objmap[:] # Labeled map num_colonies = objmap.max() print(f"Detected {num_colonies} colonies") # Iterate over colonies for colony in detected.objects: print(f"Colony {colony.label}: area={colony.area} px") .. dropdown:: Compare different threshold strategies .. code-block:: python from phenotypic import Image from phenotypic.detect import ( OtsuDetector, LiDetector, YenDetector, TriangleDetector ) plate = Image.from_image_path("agar_plate.jpg") # Test multiple threshold strategies detectors = { "Otsu": OtsuDetector(), "Li": LiDetector(), "Yen": YenDetector(), "Triangle": TriangleDetector(), } for name, detector in detectors.items(): result = detector.apply(plate) num = result.objmap[:].max() print(f"{name}: detected {num} colonies") .. dropdown:: Build a pipeline with thresholding and refinement .. code-block:: python from phenotypic import Image, ImagePipeline from phenotypic.enhance import ContrastEnhancer from phenotypic.detect import OtsuDetector from phenotypic.refine import RemoveSmallObjectsRefiner # Create pipeline pipeline = ImagePipeline() pipeline.add(ContrastEnhancer(factor=1.5)) # Boost contrast pipeline.add(OtsuDetector(ignore_zeros=True)) # Threshold pipeline.add(RemoveSmallObjectsRefiner(min_size=50)) # Cleanup # Process image plate = Image.from_image_path("agar_plate.jpg") result = pipeline.operate([plate])[0] print(f"Final colonies: {result.objmap[:].max()}") """ pass