phenotypic.abc_.ObjectDetector#
- class phenotypic.abc_.ObjectDetector[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).
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.enh_gray[:](enhanced grayscale),image.rgb[:], and optionally other image data to inform detection.Write only
image.objmask[:]andimage.objmap[:].Protect
image.rgb,image.gray, andimage.enh_grayvia 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: Otsu thresholding, Canny edges, peak detection (RoundPeaks), watershed.
ThresholdDetector (ObjectDetector subclass): Inherit from this if your detection relies on a threshold value. Provides common patterns and may offer utility methods. Examples: OtsuDetector, TriangleDetector, LocalThresholdDetector.
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.enh_gray[:],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
Edge-based: Edge detector (Canny) → invert edges → label regions
Peak-based: Detect intensity peaks → grow regions → label
Region-based: Watershed or morphological operations
Create and set the binary mask and labeled map:
from scipy import ndimage import numpy as np # Example: simple Otsu thresholding enh = image.enh_gray[:] 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:
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 footprint (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.
Examples of different detection strategies
Otsu thresholding (simple, fast, global intensity)
from phenotypic.abc_ import ObjectDetector from phenotypic import Image from skimage import filters from scipy import ndimage import numpy as np class SimpleOtsuDetector(ObjectDetector): """Detects colonies using global Otsu thresholding.""" def __init__(self): super().__init__() @staticmethod def _operate(image: Image) -> Image: enh = image.enh_gray[:] # Compute threshold thresh = filters.threshold_otsu(enh) # Create binary mask mask = enh > thresh # Label connected components labeled, num = ndimage.label(mask) # Set results image.objmask[:] = mask image.objmap[:] = labeled return image # Usage: # detector = SimpleOtsuDetector() # plate = Image.from_image_path("plate.jpg") # result = detector.apply(plate) # colonies = result.objects # Access detected colonies
Edge-based detection with Canny and post-processing
from phenotypic.abc_ import ObjectDetector from phenotypic import Image from skimage import feature, morphology from scipy import ndimage import numpy as np class EdgeDetector(ObjectDetector): """Detects colonies by finding edges and labeling enclosed regions.""" def __init__(self, sigma: float = 1.5, min_size: int = 50): super().__init__() self.sigma = sigma self.min_size = min_size @staticmethod def _operate(image: Image, sigma: float = 1.5, min_size: int = 50) -> Image: enh = image.enh_gray[:] # Detect edges using Canny edges = feature.canny(enh, sigma=sigma) # Invert to get regions (not edges) regions = ~edges # Label connected regions labeled, num = ndimage.label(regions) # Remove small objects labeled = morphology.remove_small_objects( labeled, min_size=min_size) # Create binary mask from labeled map mask = labeled > 0 # Set results image.objmask[:] = mask image.objmap[:] = labeled return image # Usage: # detector = EdgeDetector(sigma=1.0, min_size=100) # result = detector.apply(plate)
Peak detection with cleanup (RoundPeaks-inspired approach)
from phenotypic.abc_ import ObjectDetector from phenotypic import Image from scipy import ndimage from scipy.signal import find_peaks import numpy as np class PeakColumnDetector(ObjectDetector): """Detects colonies via 1D peak finding on intensity profiles.""" def __init__(self, min_distance: int = 15, threshold_abs: int = 100): super().__init__() self.min_distance = min_distance self.threshold_abs = threshold_abs @staticmethod def _operate(image: Image, min_distance: int = 15, threshold_abs: int = 100) -> Image: enh = image.enh_gray[:] # Sum intensity along rows to get column profile col_sums = np.sum(enh, axis=0) # Find peaks in profile peaks, _ = find_peaks(col_sums, distance=min_distance, height=threshold_abs) # Simple approach: threshold and label # (Production code would do more sophisticated grid inference) mask = enh > np.median(enh) # Placeholder threshold labeled, num = ndimage.label(mask) image.objmask[:] = mask image.objmap[:] = labeled return image
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.from_image_path("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)#
Abstract static 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.enh_gray 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).
Static _operate() method: Required for parallel execution in ImagePipeline. All parameters (except image) must be instance attributes.
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.from_image_path("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}")
Custom detector with parameter tuning
from phenotypic.abc_ import ObjectDetector from phenotypic import Image from skimage import filters from scipy import ndimage class LocalThresholdDetector(ObjectDetector): """Detects colonies using adaptive local thresholding.""" def __init__(self, block_size: int = 31): super().__init__() self.block_size = block_size @staticmethod def _operate(image: Image, block_size: int = 31) -> Image: enh = image.enh_gray[:] # Ensure block_size is odd if block_size % 2 == 0: block_size += 1 # Apply local threshold threshold = filters.threshold_local(enh, block_size=block_size) mask = enh > threshold # Label labeled, num = ndimage.label(mask) # Set results image.objmask[:] = mask image.objmap[:] = labeled return image # Usage detector = LocalThresholdDetector(block_size=51) result = detector.apply(plate) print(f"Found {result.objmap[:].max()} colonies")
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.from_image_path("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__Binarizes the given image gray using the Yen threshold method.
Drop references to the UI widgets.
Push internal state into widgets.
Return (and optionally display) the root widget.
- apply(image, inplace=False)[source]#
Binarizes the given image gray using the Yen threshold method.
This function modifies the arr image by applying a binary mask to its enhanced gray (enh_gray). The binarization threshold is automatically determined using Yen’s method. The resulting binary mask is stored in the image’s objmask attribute.
- __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.
- widget(image: Image | None = None, show: bool = False) Widget#
Return (and optionally display) the root widget.
- Parameters:
- Returns:
The root widget.
- Return type:
ipywidgets.Widget
- Raises:
ImportError – If ipywidgets or IPython are not installed.