phenotypic.abc_.ObjectDetector#
- class phenotypic.abc_.ObjectDetector(*args, **kwargs)[source]
Bases:
ImageOperation,ABCAbstract base class for colony detection operations on agar plate images.
ObjectDetector defines the interface for algorithms that identify and label microbial colonies (or other objects) in image data. Detection is a critical step in the PhenoTypic image processing pipeline: it bridges image preprocessing (enhancement) and downstream analysis (measurement, refinement, and statistical analysis).
Quick Decision Guide
Use this guide to choose the right operation for your task:
ObjectDetector: Implementing a novel detection algorithm? Produces both objmask (binary) and objmap (labeled).
ThresholdDetector: Your algorithm converts intensity to binary via thresholding? Subclass ThresholdDetector for specialized threshold strategies.
ImageEnhancer: Need to preprocess image data (blur, contrast, denoise) before detection? Use enhancement to prepare detect_mat for better detection.
ObjectRefiner: Need to clean up existing masks (size filter, morphology, merge)? Refiner operates on objmask/objmap without analyzing image data.
Threshold vs Edge vs Peak: Threshold works when intensity separates colonies from background; edge-based (Canny) finds boundaries; peak-based assumes circular shapes.
Grid-aware analysis: Processing arrayed plates? Use GridObjectRefiner or GridFinder for well-plate-specific logic.
What does ObjectDetector do?
ObjectDetector subclasses analyze image data and produce two outputs that describe detected colonies:
image.objmask (binary mask): A 2D boolean array where True indicates colony pixels and False indicates background. Each True pixel belongs to some colony but the mask does not distinguish which colony each pixel belongs to—that is the role of objmap.
image.objmap (labeled map): A 2D integer array where each pixel value identifies the colony it belongs to. Background is 0, and each unique positive integer (1, 2, 3, …, N) represents a distinct labeled colony. This enables accessing individual colonies via
image.objectsafter detection.
Key principle: ObjectDetector is READ-ONLY for image data
ObjectDetector operations:
Read
image.detect_mat[:](detection matrix),image.rgb[:], and optionally other image data to inform detection.Write only
image.objmask[:]andimage.objmap[:].Protect
image.rgb,image.gray, andimage.detect_matvia automatic integrity validation (@validate_operation_integritydecorator).
Any attempt to modify protected image components raises
OperationIntegrityErrorwhenVALIDATE_OPS=Truein the environment (enabled during development/testing).Why is detection central to the pipeline?
Detection enables:
Object identification: Distinguishes individual colonies from background and from each other.
Downstream analysis: Once colonies are labeled,
image.objectsprovides access to properties (area, intensity, centroid, morphology) for each colony.Refinement: ObjectRefiner operations clean up detection masks/maps post-detection (e.g., removing spurious objects, merging fragments, filtering by size).
Phenotyping: Measurement operations (MeasureFeatures) extract colony features (color, morphology, growth) for statistical analysis.
Differences: objmask vs objmap
objmask (binary): Answers “is this pixel part of any colony?” Simple, useful for visualization or as input to further processing (e.g., morphological operations). Generated by most detectors via thresholding or edge detection.
objmap (labeled): Answers “which colony does this pixel belong to?” Enables per-object analysis. Each colony has a unique integer label, and connected-component labeling (usually
scipy.ndimage.label) assigns these labels.
Both are typically set together in
_operate()via:image.objmask[:] = binary_mask image.objmap[:] = labeled_map
When to use ObjectDetector vs ThresholdDetector vs ObjectRefiner
ObjectDetector (this class): Implement when you have a novel algorithm that produces both objmask and objmap from image data. Examples: [OtsuDetector](src/phenotypic/detect/_otsu_detector.py), [CannyDetector](src/phenotypic/detect/_canny_detector.py), [RoundPeaksDetector](src/phenotypic/detect/_round_peaks_detector.py), [WatershedDetector](src/phenotypic/detect/_watershed_detector.py).
ThresholdDetector (ObjectDetector subclass): Inherit from this if your detection relies on a threshold value. Provides common patterns and signals intent. Examples: [OtsuDetector](src/phenotypic/detect/_otsu_detector.py), [LiDetector](src/phenotypic/detect/_li_detector.py), [YenDetector](src/phenotypic/detect/_yen_detector.py), [TriangleDetector](src/phenotypic/detect/_triangle_detector.py).
ObjectRefiner (different ABC): Use when modifying existing masks/maps without analyzing image data. Examples: size filtering, morphological cleanup, erosion/dilation, merging nearby objects, removing objects near borders.
How to implement a custom ObjectDetector
Create the class:
from phenotypic.abc_ import ObjectDetector from phenotypic import Image class MyDetector(ObjectDetector): def __init__(self, param1: float, param2: int = 10): super().__init__() self.param1 = param1 self.param2 = param2 @staticmethod def _operate(image: Image, param1: float, param2: int = 10) -> Image: # Detection logic here return image
Within _operate(), read image data carefully:
Access via accessors:
image.detect_mat[:],image.gray[:],image.rgb[:]Never modify these; integrity validation will catch it
Consider the data type and range (uint8, uint16, float, etc.)
Perform detection: Use your algorithm to create a binary mask and labeled map. Typical approaches:
Thresholding-based: Global or local threshold → binary mask → label (see [OtsuDetector](src/phenotypic/detect/_otsu_detector.py))
Edge-based: Edge detector (Canny) → invert edges → label regions (see [CannyDetector](src/phenotypic/detect/_canny_detector.py))
Peak-based: Detect intensity peaks → grow regions → label (see [RoundPeaksDetector](src/phenotypic/detect/_round_peaks_detector.py))
Region-based: Watershed or morphological operations (see [WatershedDetector](src/phenotypic/detect/_watershed_detector.py))
Create and set the binary mask and labeled map:
from scipy import ndimage import numpy as np # Example: simple Otsu thresholding enh = image.detect_mat[:] threshold = skimage.filters.threshold_otsu(enh) binary_mask = enh > threshold # Remove small noise binary_mask = skimage.morphology.remove_small_objects(binary_mask, min_size=20) # Label connected components labeled_map, num_objects = ndimage.label(binary_mask) # Set both outputs image.objmask[:] = binary_mask image.objmap[:] = labeled_map return image
Post-processing (optional): Some detectors include additional cleanup:
Morphological operations: Apply erosion, dilation, opening, or closing to refine mask topology (remove noise, bridge fragments, smooth boundaries).
Clear borders: Use
skimage.segmentation.clear_border()to remove objects touching image edges.Remove small/large objects: Use
skimage.morphology.remove_small_objects()orskimage.morphology.remove_large_objects()to filter by area.Relabel: Call
image.objmap.relabel(connectivity=...)to ensure consecutive labels.
Helper functions from scipy and scikit-image
Common utilities for ObjectDetector implementations:
scipy.ndimage.label(): Assigns unique integers to connected components in a binary mask. Returns (labeled_array, num_features). Specify
structurefor connectivity (default 3x3 with all 8 neighbors; usegenerate_binary_structure(2, 1)for 4-connectivity).skimage.morphology.remove_small_objects(): Removes binary regions smaller than min_size pixels. Helpful for filtering noise or spurious detections.
skimage.morphology.remove_large_objects(): Removes regions larger than a threshold. Useful for excluding large artefacts or plate boundaries.
skimage.segmentation.clear_border(): Sets pixels on the image border to False, eliminating objects that touch the edge (common in arrayed imaging where wells at plate boundaries may be partially cut off).
skimage.morphology.binary_opening(): Erosion followed by dilation; removes small noise while preserving larger objects. Use with a suitable shape (disk, square, or diamond).
scipy.ndimage.binary_dilation() / binary_erosion(): Expand or shrink objects morphologically. Useful for bridging fragmented colonies or removing small protrusions.
skimage.feature.canny(): Multi-stage edge detection (Gaussian → gradient → non-max suppression → hysteresis). Robust but requires threshold tuning.
Reference implementations in PhenoTypic
Study these implementations to learn detection patterns:
[OtsuDetector](src/phenotypic/detect/_otsu_detector.py): Simple thresholding with global Otsu method
[HysteresisDetector](src/phenotypic/detect/_hysteresis_detector.py): Advanced dual-threshold with edge tracking (excellent reference for complex detection)
[CannyDetector](src/phenotypic/detect/_canny_detector.py): Edge-based detection with connectivity cleanup
[RoundPeaksDetector](src/phenotypic/detect/_round_peaks_detector.py): Peak-based approach for round colonies
[WatershedDetector](src/phenotypic/detect/_watershed_detector.py): Region-based segmentation
When and how to refine detections (post-processing)
Raw detections often need cleanup:
Remove small noise: Spurious single-pixel detections or tiny salt-and-pepper artifacts. Use ObjectRefiner + remove_small_objects.
Clean borders: Colonies at plate edges may be incomplete. Use ObjectRefiner or clear_border() in detector.
Merge fragments: Noise or uneven lighting can fragment a single colony into multiple labels. Use ObjectRefiner with morphological dilation or connected-component merging.
Remove large objects: Plate edges, dust on the scanner, or agar artifacts appear as large regions. Use ObjectRefiner + remove_large_objects.
Grid-aware filtering: In arrayed formats (96-well, 384-well), one object per grid cell is expected. Use GridObjectRefiner to enforce this constraint or GridRefiner to assign dominant objects to grid positions.
Example pipeline with detection + refinement:
from phenotypic import Image, ImagePipeline from phenotypic.detect import OtsuDetector from phenotypic.refine import RemoveSmallObjectsRefiner, ClearBorderRefiner pipeline = ImagePipeline() pipeline.add(OtsuDetector()) # Initial detection pipeline.add(ClearBorderRefiner()) # Remove edge-touching objects pipeline.add(RemoveSmallObjectsRefiner(min_size=100)) # Filter noise image = Image("plate.jpg") result = pipeline.operate([image])[0] # result now has clean, labeled colonies ready for measurement
- None
- Type:
all operation parameters are stored in subclass instances
- apply(image, inplace=False)[source]
User-facing method to apply detection to an image. Handles copy/inplace logic and parameter matching.
- _operate(image, **kwargs)[source]
Abstract instance method implemented by subclasses with detection logic. Must set image.objmask and image.objmap.
Notes
Integrity protection: The @validate_operation_integrity decorator on apply() ensures image.rgb, image.gray, and image.detect_mat are not modified. Violations raise OperationIntegrityError during development (VALIDATE_OPS=True).
Binary mask is often intermediate: Many implementations create objmask first, then derive objmap via connected-component labeling. Both must be set for downstream code to work correctly.
Label consistency: Use image.objmap.relabel() after manipulating the labeled map to ensure labels are consecutive (1, 2, 3, …, N) and to update objmask.
Memory efficiency: Large images and detailed segmentations consume memory. Consider inplace=True in pipelines processing many images, or use sparse representations (objmap uses scipy.sparse internally).
Instance _operate() method: Access parameters via
selfattributes.
Examples
Detect colonies in a plate image and access results:
>>> from phenotypic import Image >>> from phenotypic.detect import OtsuDetector >>> # Load a plate image >>> plate = Image("agar_plate.jpg") >>> # Apply detection >>> detector = OtsuDetector() >>> detected = detector.apply(plate) >>> # Access binary mask >>> mask = detected.objmask[:] # numpy array >>> print(f"Mask shape: {mask.shape}, True pixels: {mask.sum()}") >>> # Access labeled map >>> objmap = detected.objmap[:] >>> print(f"Detected {objmap.max()} colonies") >>> # Iterate over colonies and measure properties >>> for colony in detected.objects: ... print(f"Colony area: {colony.area} px, " ... f"centroid: {colony.centroid}")
Detection in a full pipeline with enhancement and refinement:
>>> from phenotypic import Image, ImagePipeline >>> from phenotypic.enhance import GaussianBlur >>> from phenotypic.detect import CannyDetector >>> from phenotypic.refine import RemoveSmallObjectsRefiner >>> from phenotypic.measure import MeasureColor >>> # Create a processing pipeline >>> pipeline = ImagePipeline() >>> pipeline.add(GaussianBlur(sigma=2.0)) # Preprocessing >>> pipeline.add(CannyDetector(sigma=1.5)) # Detection >>> pipeline.add(RemoveSmallObjectsRefiner(min_size=50)) # Cleanup >>> pipeline.add(MeasureColor()) # Downstream analysis >>> # Load image and process >>> image = Image("plate.jpg") >>> result = pipeline.operate([image])[0] >>> # Results include enhanced image, detected/refined colonies, and measurements >>> print(f"Colonies: {result.objmap[:].max()}") >>> print(f"Measurements: {result.measurements.shape}")
Methods
__init__Detect colonies using sinusoidal cross-correlation grid estimation.
Return (and optionally display) the root widget.
- __del__()
Automatically stop tracemalloc when the object is deleted.
- __getstate__()
Prepare the object for pickling by disposing of any widgets.
This ensures that UI components (which may contain unpickleable objects like input functions or thread locks) are cleaned up before serialization.
Note
This method modifies the object state by calling dispose_widgets(). Any active widgets will be detached from the object.
- apply(image: Image, inplace: bool = False) Image[source]
- apply(image: GridImage, inplace: bool = False) GridImage
Detect colonies using sinusoidal cross-correlation grid estimation.
This method performs the core detection workflow: 1. Extract grid dimensions (if GridImage) 2. Threshold the detection matrix with adaptive kernel sizing 3. Remove noise if requested 4. Label connected components 5. Determine or estimate grid edges (via sinusoidal cross-correlation) 6. Assign dominant colonies to grid cells 7. Create final object map
- Parameters:
image – Image object to process. Can be a regular Image or GridImage.
- Returns:
The processed image with updated objmask and objmap.
- Return type:
Image
- widget(image: Image | None = None, show: bool = False) Widget
Return (and optionally display) the root widget.
- Parameters:
image (Image | None) – Optional image to visualize. If provided, visualization controls will be added to the widget.
show (bool) – Whether to display the widget immediately. Defaults to False.
- Returns:
The root widget.
- Return type:
ipywidgets.Widget
- Raises:
ImportError – If ipywidgets or IPython are not installed.