phenotypic.abc_.ImageEnhancer#
- class phenotypic.abc_.ImageEnhancer(*args, **kwargs)[source]
Bases:
FootprintMixin,ImageOperation,ABCAbstract base class for preprocessing operations that improve colony detection through detection matrix.
ImageEnhancer is the foundation for all preprocessing algorithms that modify only the enhanced grayscale channel (image.detect_mat) to improve colony visibility and detection quality. Unlike ImageCorrector (which transforms the entire Image), ImageEnhancer leaves the original RGB and grayscale data untouched, protecting image integrity while enabling targeted preprocessing.
Quick Decision Guide: Which Operation Type?
ImageEnhancer (this class): Modify only
image.detect_matfor preprocessing. Use for: noise reduction, contrast enhancement, illumination correction. Examples: [GaussianBlur](src/phenotypic/enhance/_gaussian_blur.py), [CLAHE](src/phenotypic/enhance/_clahe.py), [BilateralDenoise](src/phenotypic/enhance/_bilateral_denoise.py).ImageCorrector: Transform entire image (rotation, cropping, perspective). Use for: geometric corrections, global color transformations.
ObjectDetector: Analyze image, produce only
objmaskandobjmap. Use for: colony/object detection and labeling.ObjectRefiner: Edit mask and map (filtering, merging, removing objects). Use for: post-detection cleanup and refinement.
What is ImageEnhancer?
ImageEnhancer operates on the principle of non-destructive preprocessing: all modifications are applied to image.detect_mat (a working copy of grayscale), while original image components (image.rgb, image.gray, image.objmask, image.objmap) remain protected and unchanged. This allows you to experiment with multiple enhancement chains without affecting raw data or detection results.
Role in the Detection Pipeline
ImageEnhancer sits at the beginning of the processing chain:
Raw Image (image.rgb, image.gray) ↓ ImageEnhancer(s) → Improve visibility, reduce noise ↓ ObjectDetector → Detect colonies/objects ↓ ObjectRefiner → Clean up detections (optional)When you call enhancer.apply(image), you get back an Image with improved detect_mat but identical RGB/gray data—ready for detection algorithms to operate on enhanced contrast.
Why Enhancement Matters for Colony Phenotyping
Real agar plate imaging introduces several challenges that enhancement operations address:
Uneven illumination: Vignetting, shadows, and scanner lighting gradients make colonies appear faint in dark regions.
Noise and texture: Scanner noise, agar granularity, dust, and condensation create artifacts confusing detection.
Faint colonies: Small or translucent colonies blend into background, reducing detectability.
Poor contrast: Low-contrast colonies on dense plates require local contrast enhancement.
Enhancement operations preserve colony morphology while suppressing artifacts for robust detection.
Subclass References
The following are canonical examples of ImageEnhancer implementations:
[GaussianBlur](src/phenotypic/enhance/_gaussian_blur.py): Noise reduction via Gaussian filtering.
[CLAHE](src/phenotypic/enhance/_clahe.py): Contrast-limited adaptive histogram equalization for local contrast.
GrayOpening: Morphological opening usingFootprintMixin.[BilateralDenoise](src/phenotypic/enhance/_bilateral_denoise.py): Edge-preserving denoising.
Integrity Validation: Protection of Core Data
ImageEnhancer uses the
@validate_operation_integritydecorator on theapply()method to guarantee that RGB and grayscale data are never modified:@validate_operation_integrity('image.rgb', 'image.gray') def apply(self, image: Image, inplace: bool = False) -> Image: return super().apply(image=image, inplace=inplace)
This decorator:
Calculates cryptographic signatures of image.rgb and image.gray before processing
Calls the parent apply() method to execute your _operate() implementation
Recalculates signatures after operation completes
Raises
OperationIntegrityErrorif any protected component was modified
Note: Integrity validation only runs if the
VALIDATE_OPS=Trueenvironment variable is set (development-time safety; disabled in production for performance).Implementing a Custom ImageEnhancer
Subclass ImageEnhancer and implement a single method:
from phenotypic.abc_ import ImageEnhancer from phenotypic import Image from scipy.ndimage import gaussian_filter class MyCustomEnhancer(ImageEnhancer): def __init__(self, sigma: float = 1.0): super().__init__() self.sigma = sigma # Instance attribute matched to _operate() def _operate(self, image: Image) -> Image: # Modify ONLY detect_mat; read, process, write back enh = image.detect_mat[:] filtered = gaussian_filter(enh.astype(float), sigma=self.sigma) image.detect_mat[:] = filtered.astype(enh.dtype) return image
Key Rules for Implementation:
_operate()should be an instance method (no@staticmethoddecorator).Access operation parameters directly via
self.param_name.Only modify ``image.detect_mat[:]``—all other components are protected.
Always use the accessor pattern:
image.detect_mat[:] = new_data(never direct attribute assignment likeimage._detect_mat = ...).Return the modified Image object.
Accessing and Modifying detect_mat
Within your _operate() method, use the accessor interface:
# Reading detection matrix data enh_data = image.detect_mat[:] # Full array region = image.detect_mat[10:50, 20:80] # Slicing with NumPy syntax # Modifying detection matrix image.detect_mat[:] = processed_array # Full replacement image.detect_mat[10:50, 20:80] = new_region # Partial update
The accessor handles all consistency checks and automatic cache invalidation.
The _make_footprint() Static Utility
ImageEnhancer provides a static helper for generating morphological structuring elements (footprints) used in morphological operations like erosion, dilation, and median filtering:
@staticmethod def _make_footprint(shape: Literal["square", "diamond", "disk"], width: int) -> np.ndarray: '''Creates a binary morphological shape for image processing.'''
Footprint Shapes and When to Use Each
“disk”: Circular/isotropic shape. Best for preserving rounded colony shapes and applying uniform processing in all directions. Use for: general-purpose smoothing, median filtering, dilations that expand colonies symmetrically.
“square”: Square shape with 8-connectivity. Emphasizes horizontal/vertical edges and aligns with pixel grid. Use for: grid-aligned artifacts (imaging hardware stripe patterns), when processing speed matters (slightly faster than disk).
“diamond”: Diamond-shaped (rotated square) shape with 4-connectivity. Creates a cross-like neighborhood pattern. Use for: specialized cases where diagonal connections should be de-emphasized; less common in practice.
The width parameter controls the neighborhood size (in pixels). Larger radii affect more neighbors and produce broader effects (more noise suppression, but potential colony merging). Choose width smaller than the minimum colony diameter to avoid destroying fine details.
Common Morphological Patterns
Use _make_footprint() with morphological operations from scipy.ndimage or skimage.morphology:
from skimage.morphology import erosion, dilation from phenotypic.abc_ import ImageEnhancer disk_fp = ImageEnhancer._make_footprint('disk', width=5) # Erosion: shrink bright regions (removes small colonies/noise) eroded = erosion(binary_image, footprint=disk_fp) # Dilation: expand bright regions (closes holes, merges nearby colonies) dilated = dilation(binary_image, footprint=disk_fp)
When and Why to Chain Multiple Enhancements
Enhancement operations are typically chained together to address multiple issues in sequence:
# Example pipeline: handle uneven illumination + noise # Step 1: Remove background gradients result = SubtractRollingBall(width=50).apply(image) # Step 2: Boost local contrast for faint colonies result = CLAHE(kernel_size=50, clip_limit=0.02).apply(result) # Step 3: Smooth remaining noise result = GaussianBlur(sigma=2).apply(result) # Step 4: Detect colonies in detection matrix result = OtsuDetector().apply(result)
Rationale for chaining:
Order matters: Background correction before contrast enhancement yields better results than vice versa.
Divide and conquer: One enhancer per problem (illumination, noise, contrast) is more maintainable and tunable than one monolithic algorithm.
No data loss: Each enhancer preserves the original RGB/gray, so intermediate results can be inspected and validated.
Reproducibility: Chained operations can be serialized to YAML for documentation and reuse across experiments.
Use ImagePipeline for convenient chaining:
from phenotypic import Image, ImagePipeline from phenotypic.enhance import SubtractRollingBall, CLAHE, GaussianBlur from phenotypic.detect import OtsuDetector pipeline = ImagePipeline() pipeline.add(SubtractRollingBall(width=50)) pipeline.add(CLAHE(kernel_size=50, clip_limit=0.02)) pipeline.add(GaussianBlur(sigma=2)) pipeline.add(OtsuDetector()) # Process a batch of images with automatic parallelization images = [Image.imread(f) for f in plate_scans] results = pipeline.operate(images)
Methods and Attributes
- None at the ImageEnhancer level; subclasses define enhancement parameters
- as instance attributes
- Type:
e.g., sigma, kernel_size, clip_limit
- apply(image, inplace=False)[source]
Applies the enhancement to an image. Returns a modified Image with enhanced detect_mat but unchanged RGB/gray/objects. Handles copy/inplace logic and validates data integrity.
- _operate(self, image)[source]
Abstract instance method implemented by subclasses. Performs the actual enhancement algorithm. Access parameters via
self.param_name.
- _make_footprint(shape, width)
Static utility that creates a binary morphological shape (disk, square, or diamond) for use in morphological operations.
Notes
Protected components: The
@validate_operation_integritydecorator ensures thatimage.rgbandimage.graycannot be modified. Onlyimage.detect_matcan be changed.Immutability by default:
apply(image)returns a modified copy by default. Setinplace=Truefor memory-efficient in-place modification.Instance method pattern: The
_operate()method should be an instance method (no@staticmethoddecorator). Access operation parameters directly viaself.param_name. This is simpler and more Pythonic.Accessor pattern: Always use
image.detect_mat[:] = new_datato modify detection matrix. Never use direct attribute assignment.Automatic cache invalidation: When you modify
image.detect_mat[:], the Image’s internal caches (e.g., color space conversions, object maps) are automatically invalidated to prevent stale results.
Examples
Basic usage with noise reduction:
>>> from phenotypic.abc_ import ImageEnhancer >>> from phenotypic.data import load_synth_yeast_plate >>> from scipy.ndimage import gaussian_filter >>> >>> class GaussianEnhancer(ImageEnhancer): ... def __init__(self, sigma=1.5): ... super().__init__() ... self.sigma = sigma ... def _operate(self, image): ... enh = image.detect_mat[:] ... filtered = gaussian_filter(enh.astype(float), sigma=self.sigma) ... image.detect_mat[:] = filtered.astype(enh.dtype) ... return image >>> >>> image = load_synth_yeast_plate() >>> enhancer = GaussianEnhancer(sigma=2.0) >>> enhanced = enhancer.apply(image) >>> # Original RGB and gray are unchanged >>> assert (image.gray[:] == enhanced.gray[:]).all()
Morphological enhancement with FootprintMixin for colony hole-filling:
>>> from phenotypic.abc_ import ImageEnhancer >>> from phenotypic.data import load_synth_yeast_plate >>> from skimage.morphology import closing >>> >>> class MorphologicalEnhancer(ImageEnhancer): ... def __init__(self, operation='closing', width=3): ... super().__init__() ... self.operation = operation ... self.width = width ... def _operate(self, image): ... enh = image.detect_mat[:] ... footprint = ImageEnhancer._make_footprint('disk', self.width) ... binary = enh > enh.mean() ... refined = closing(binary, footprint=footprint) ... image.detect_mat[:] = (refined * 255).astype(enh.dtype) ... return image >>> >>> image = load_synth_yeast_plate() >>> enhancer = MorphologicalEnhancer(operation='closing', width=5) >>> enhanced = enhancer.apply(image)
Chaining multiple enhancements in pipeline:
>>> from phenotypic import ImagePipeline >>> from phenotypic.enhance import GaussianBlur, CLAHE >>> from phenotypic.detect import OtsuDetector >>> from phenotypic.data import load_synth_yeast_plate >>> >>> image = load_synth_yeast_plate() >>> pipeline = ImagePipeline([ ... GaussianBlur(sigma=1.5), ... CLAHE(clip_limit=2.0), ... OtsuDetector() ... ]) >>> result = pipeline.apply(image) >>> colonies = result.objects >>> len(colonies) > 0 True
Methods
__init__Applies the operation to an image, either in-place or on a copy.
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
Applies the operation to an image, either in-place or on a copy.
- Parameters:
image (Image) – The arr image to apply the operation on.
inplace (bool) – If True, modifies the image in place; otherwise, operates on a copy of the image.
- Returns:
The modified image after applying the operation.
- 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.