phenotypic.abc_.ImageEnhancer#
- class phenotypic.abc_.ImageEnhancer[source]#
Bases:
ImageOperation,ABCAbstract base class for preprocessing operations that improve colony detection through enhanced grayscale.
ImageEnhancer is the foundation for all preprocessing algorithms that modify only the enhanced grayscale channel (image.enh_gray) 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.
What is ImageEnhancer?
ImageEnhancer operates on the principle of non-destructive preprocessing: all modifications are applied to image.enh_gray (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 enh_gray 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:
Uneven illumination: Vignetting, shadows, and scanner lighting gradients make colonies appear faint in dark regions and over-exposed elsewhere.
Noise and texture: Scanner noise, agar granularity, condensation droplets, and dust create artifacts that confuse thresholding or edge 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 target these issues in a domain-specific way: they preserve colony morphology while suppressing artifacts, enabling robust detection in downstream algorithms.
When to Use ImageEnhancer vs Other Operations
ImageEnhancer: You only modify enh_gray for preprocessing. Use for: noise reduction (Gaussian blur, median filtering), contrast enhancement (CLAHE), illumination compensation (background subtraction), edge enhancement (Sobel, Laplacian). Typical use: before detection.
ImageCorrector: You transform the entire Image (rotation, cropping, color correction). Typical use: geometric corrections or global color transformations.
ObjectDetector: You analyze image data and produce only objmask and objmap. Input image data is protected. Typical use: colony detection and labeling.
ObjectRefiner: You edit mask and map (filtering, merging, removing objects). Typical use: post-detection cleanup and validation.
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() @staticmethod def _operate(image: Image, sigma: float = 1.0) -> Image: # Modify ONLY enh_gray; read, process, write back enh = image.enh_gray[:] filtered = gaussian_filter(enh.astype(float), sigma=sigma) image.enh_gray[:] = filtered.astype(enh.dtype) return image
Key Rules for Implementation:
_operate()must be static (required for parallel execution in pipelines).All parameters except image must exist as instance attributes with matching names (enables automatic parameter matching via _get_matched_operation_args()).
Only modify ``image.enh_gray[:]``—all other components are protected.
Always use the accessor pattern:
image.enh_gray[:] = new_data(never direct attribute assignment likeimage._enh_gray = ...).Return the modified Image object.
Accessing and Modifying enh_gray
Within your _operate() method, use the accessor interface:
# Reading enhanced grayscale data enh_data = image.enh_gray[:] # Full array region = image.enh_gray[10:50, 20:80] # Slicing with NumPy syntax # Modifying enhanced grayscale image.enh_gray[:] = processed_array # Full replacement image.enh_gray[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"], radius: int) -> np.ndarray: '''Creates a binary morphological footprint for image processing.'''
Footprint Shapes and When to Use Each
“disk”: Circular/isotropic footprint. 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 footprint 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) footprint 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 radius parameter controls the neighborhood size (in pixels). Larger radii affect more neighbors and produce broader effects (more noise suppression, but potential colony merging). Choose radius 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 scipy.ndimage import binary_dilation, binary_erosion from phenotypic.abc_ import ImageEnhancer disk_fp = ImageEnhancer._make_footprint('disk', radius=5) # Erosion: shrink bright regions (removes small colonies/noise) eroded = binary_erosion(binary_image, structure=disk_fp) # Dilation: expand bright regions (closes holes, merges nearby colonies) dilated = binary_dilation(binary_image, structure=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 = RollingBallRemoveBG(radius=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 enhanced grayscale 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 RollingBallRemoveBG, CLAHE, GaussianBlur from phenotypic.detect import OtsuDetector pipeline = ImagePipeline() pipeline.add(RollingBallRemoveBG(radius=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.from_image_path(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 enh_gray but unchanged RGB/gray/objects. Handles copy/inplace logic and validates data integrity.
- _operate(image, **kwargs)#
Abstract static method implemented by subclasses. Performs the actual enhancement algorithm. Parameters are automatically matched to instance attributes.
- _make_footprint(shape, radius)[source]#
Static utility that creates a binary morphological footprint (disk, square, or diamond) for use in morphological operations.
Notes
Protected components: The
@validate_operation_integritydecorator ensures thatimage.rgbandimage.graycannot be modified. Onlyimage.enh_graycan be changed.Immutability by default:
apply(image)returns a modified copy by default. Setinplace=Truefor memory-efficient in-place modification.Static _operate() requirement: The
_operate()method must be static to support parallel execution in pipelines.Parameter matching for parallelization: All
_operate()parameters exceptimagemust exist as instance attributes. Whenapply()is called, these values are extracted and passed to_operate().Accessor pattern: Always use
image.enh_gray[:] = new_datato modify enhanced grayscale. Never use direct attribute assignment.Automatic cache invalidation: When you modify
image.enh_gray[:], the Image’s internal caches (e.g., color space conversions, object maps) are automatically invalidated to prevent stale results.
Examples
Implementing a custom noise-reduction enhancer with Gaussian blur
from phenotypic.abc_ import ImageEnhancer from phenotypic import Image from scipy.ndimage import gaussian_filter import numpy as np class CustomGaussianEnhancer(ImageEnhancer): '''Enhance by applying Gaussian blur to reduce noise.''' def __init__(self, sigma: float = 1.5): super().__init__() self.sigma = sigma @staticmethod def _operate(image: Image, sigma: float = 1.5) -> Image: enh = image.enh_gray[:] # Convert to float for processing filtered = gaussian_filter(enh.astype(float), sigma=sigma) # Restore original dtype image.enh_gray[:] = filtered.astype(enh.dtype) return image # Usage from phenotypic import Image from phenotypic.detect import OtsuDetector image = Image.from_image_path('agar_plate.jpg') enhancer = CustomGaussianEnhancer(sigma=2.0) enhanced = enhancer.apply(image) # Original unchanged detected = OtsuDetector().apply(enhanced) # Detect in enhanced data colonies = detected.objects print(f"Detected {len(colonies)} colonies")
Morphological operations using _make_footprint for colony refinement
from phenotypic.abc_ import ImageEnhancer from phenotypic import Image from scipy.ndimage import binary_closing, binary_opening import numpy as np class MorphologicalEnhancer(ImageEnhancer): '''Enhance by applying morphological closing/opening to fill holes and remove noise.''' def __init__(self, operation: str = 'closing', radius: int = 3): super().__init__() self.operation = operation # 'closing' or 'opening' self.radius = radius @staticmethod def _operate(image: Image, operation: str = 'closing', radius: int = 3) -> Image: enh = image.enh_gray[:] # Create a disk footprint for isotropic processing footprint = ImageEnhancer._make_footprint('disk', radius) # Apply morphological operation to binary image binary = enh > enh.mean() if operation == 'closing': # Close small holes within colonies refined = binary_closing(binary, structure=footprint) elif operation == 'opening': # Remove small noise regions refined = binary_opening(binary, structure=footprint) else: return image # Convert back to grayscale (refined mask as 0/255) image.enh_gray[:] = (refined * 255).astype(enh.dtype) return image # Usage enhancer = MorphologicalEnhancer(operation='closing', radius=5) result = enhancer.apply(image)
Chaining multiple enhancements to handle complex agar plate imaging conditions
from phenotypic import Image, ImagePipeline from phenotypic.enhance import ( RollingBallRemoveBG, CLAHE, GaussianBlur ) from phenotypic.detect import OtsuDetector # Scenario: Agar plate image with vignetting, dust, and low contrast # Build a processing pipeline pipeline = ImagePipeline() # Step 1: Remove illumination gradient (vignetting) pipeline.add(RollingBallRemoveBG(radius=80)) # Step 2: Boost local contrast for faint colonies pipeline.add(CLAHE(kernel_size=50, clip_limit=0.02)) # Step 3: Smooth dust and scanner noise pipeline.add(GaussianBlur(sigma=1.5)) # Step 4: Detect colonies pipeline.add(OtsuDetector()) # Process a batch of plate images image_paths = ['plate1.tif', 'plate2.tif', 'plate3.tif'] images = [Image.from_image_path(p) for p in image_paths] results = pipeline.operate(images) # Each result has cleaned detection results for i, result in enumerate(results): colonies = result.objects print(f"Plate {i}: {len(colonies)} colonies detected")
Using different footprint shapes for specialized morphological filtering
from phenotypic.abc_ import ImageEnhancer from phenotypic import Image from skimage.filters.rank import median from skimage.util import img_as_ubyte, img_as_float import numpy as np class SelectiveMedianEnhancer(ImageEnhancer): '''Enhance by applying median filtering with configurable footprint shape.''' def __init__(self, shape: str = 'disk', radius: int = 3): super().__init__() self.shape = shape # 'disk', 'square', or 'diamond' self.radius = radius @staticmethod def _operate(image: Image, shape: str = 'disk', radius: int = 3) -> Image: enh = image.enh_gray[:] # Create footprint with specified shape footprint = ImageEnhancer._make_footprint(shape, radius) # Apply median filter (rank filter) # Convert to uint8 for rank filter compatibility as_uint8 = img_as_ubyte(enh) filtered = median(as_uint8, footprint=footprint) # Restore original dtype image.enh_gray[:] = img_as_float(filtered) if enh.dtype == np.float64 else filtered return image # Usage with different shapes image = Image.from_image_path('plate.jpg') # Isotropic smoothing (preserves round colony shapes) result1 = SelectiveMedianEnhancer(shape='disk', radius=3).apply(image) # Grid-aligned smoothing (for hardware artifacts) result2 = SelectiveMedianEnhancer(shape='square', radius=3).apply(image) # Both preserve original image.rgb and image.gray assert np.array_equal(image.gray[:], result1.gray[:]) assert np.array_equal(image.rgb[:], result1.rgb[:])
Methods
__init__Applies the operation to an image, either in-place or on a copy.
Drop references to the UI widgets.
Push internal state into widgets.
Return (and optionally display) the root widget.
- apply(image, inplace=False)[source]#
Applies the operation to an image, either in-place or on a copy.
- __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.