phenotypic.abc_.ImageOperation#
- class phenotypic.abc_.ImageOperation(*args, **kwargs)[source]
Bases:
BaseOperation,LazyWidgetMixin,ABCCore abstract base class for all single-image transformation operations in PhenoTypic.
ImageOperation is the foundation of PhenoTypic’s algorithm system. It defines the interface for algorithms that transform an Image object by modifying specific components. Unlike GridOperation (which handles grid-aligned operations on plate images), ImageOperation acts on a single image independently.
What is ImageOperation?
ImageOperation manages the distinction between:
apply() method: The user-facing interface that handles memory management (copy vs. in-place) and integrity validation
_operate() method: The abstract algorithm-specific method that subclasses implement with the actual processing logic
This separation ensures consistent behavior, automatic memory tracking, and validation across all image operations.
The Operation Hierarchy
ImageOperation has four main subclass categories, each modifying different image components with different integrity guarantees:
ImageOperation (this class) ├── ImageEnhancer │ └── Modifies ONLY image.detect_mat │ ├── GaussianBlur, CLAHE, RankMedianEnhancer, ... │ └── Use for: noise reduction, contrast, edge sharpening │ ├── ObjectDetector │ └── Modifies ONLY image.objmask and image.objmap │ ├── OtsuDetector, CannyDetector, RoundPeaksDetector, ... │ └── Use for: discovering and labeling colonies/particles │ ├── ObjectRefiner │ └── Modifies ONLY image.objmask and image.objmap │ ├── Size filtering, merging, removing objects │ └── Use for: cleaning up detection results │ └── ImageCorrector └── Modifies ALL image components ├── GridAligner, rotation, color correction └── Use for: general-purpose transformationsWhen to inherit from each subclass:
ImageEnhancer: You only modify
image.detect_mat(detection matrix). Originalimage.rgbandimage.grayare protected by integrity checks. Typical use: preprocessing before detection.ObjectDetector: You analyze image data and produce only
image.objmask(binary mask) andimage.objmap(labeled object map). Input image data is protected. Typical use: colony detection, particle finding.ObjectRefiner: You edit the mask and map (filtering, merging, removing). Input image data is protected. Typical use: post-detection cleanup.
ImageCorrector: You transform the entire Image (every component may change). No integrity checks are performed. Typical use: rotation, alignment, global color correction.
Never inherit directly from ImageOperation. Always choose one of the four subclasses above, as each provides appropriate integrity validation and shared utilities (e.g.,
_make_footprint()for morphology operations).How apply() and _operate() work together
The user-facing method
apply(image, inplace=False)is the entry point:Calls ``_apply_to_single_image()`` with the operation logic
Handles copy/inplace semantics:
If
inplace=False(default): Image is copied before modification, original unchangedIf
inplace=True: Image is modified in-place for memory efficiency
Calls your _operate() instance method with the image
Validates integrity (subclass-specific via
@validate_operation_integrity) - Detects unexpected modifications to protected image components - Only enabled ifVALIDATE_OPS=Truein environment
Your subclass only needs to implement
_operate(self, image) -> Image.The _operate() method contract
_operate()is an instance method (no@staticmethoddecorator):Signature:
def _operate(self, image: Image) -> Image:Parameters: Access operation parameters directly via
self.param_nameBehavior: Modify only the allowed image components (determined by subclass)
Returns: The modified Image object
Example implementation:
class MyEnhancer(ImageEnhancer): def __init__(self, sigma: float): super().__init__() self.sigma = sigma # Instance attribute def _operate(self, image: Image) -> Image: # Access parameters via self image.detect_mat[:] = gaussian_filter(image.detect_mat[:], sigma=self.sigma) return image
The instance method pattern is simpler and more Pythonic than the old static method approach.
Data access through accessors
Within
_operate(), always access image data through accessors (never direct attribute modification). This ensures lazy evaluation, caching, and consistency:Reading data:
image.detect_mat[:]- Detection matrix (for enhancers)image.rgb[:]- Original RGB dataimage.gray[:]- Luminance grayscaleimage.objmask[:]- Binary object maskimage.objmap[:]- Labeled object mapimage.color.Lab[:],image.color.HSV[:]- Color spaces
Modifying data:
image.detect_mat[:] = new_array- Set detection matriximage.objmask[:] = binary_array- Set object maskimage.objmap[:] = labeled_array- Set object map
Never do this:
# ✗ WRONG - direct attribute modification image.rgb = new_data image._detect_mat = new_data image.objects_handler.detect_mat = new_data
Do this instead:
# ✓ CORRECT - use accessors image.detect_mat[:] = new_data image.objmask[:] = new_mask
Integrity validation with @validate_operation_integrity
Intermediate subclasses use the
@validate_operation_integritydecorator to enforce that modifications are limited to specific components. For example:class ImageEnhancer(ImageOperation, ABC): @validate_operation_integrity('image.rgb', 'image.gray') def apply(self, image: Image, inplace=False) -> Image: return super().apply(image=image, inplace=inplace)
This decorator:
Calculates MurmurHash3 signatures of protected arrays before
apply()Calls the parent
apply()methodRecalculates signatures after operation completes
Raises
OperationIntegrityErrorif any protected component changed
Only enabled if
VALIDATE_OPS=Truein environment (for performance).Operation chaining and pipelines
Operations are designed for method chaining:
result = (GaussianBlur(sigma=2).apply(image) .apply_operation(OtsuDetector()))
Or use
ImagePipelinefor multi-step workflows with automatic benchmarking:pipeline = ImagePipeline() pipeline.add(GaussianBlur(sigma=2)) pipeline.add(OtsuDetector()) pipeline.add(GridFinder()) results = pipeline.operate([image1, image2, image3])
Parallel execution support
ImageOperation supports parallel execution through operation serialization. When
ImagePipelineruns with multiple images, it:Serializes the operation instance with all attributes (
op.__dict__)Sends the pickled operation to worker processes
Workers unpickle the operation (restoring all
self.paramvalues)Workers call
operation.apply(image)which invokes_operate(self, image)
Instance methods work perfectly with parallel execution because the entire operation object (with all parameters) is serialized together.
- None
- Type:
all operation state is stored in subclass instances as attributes
- apply(image, inplace=False)[source]
User-facing method that applies the operation. Handles copy/inplace logic, calls
_operate(), and validates integrity.
- _operate(self, image)[source]
Abstract instance method implemented by subclasses with algorithm logic. Access parameters via
self.param_name.
- _apply_to_single_image(cls_name, image, operation, inplace)[source]
Static helper method that performs the actual apply operation. Handles copy/inplace logic and error handling. Called internally by apply(). Also used by ImagePipeline for parallel execution.
Notes
No direct Image attribute modification: Never write to
image.rgb,image.gray, or other attributes directly. Use the accessor pattern (image.component[:] = new_data).Immutability by default: Operations return modified copies by default. Original image is unchanged unless
inplace=Trueis explicitly passed.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 than the old static method approach.Parallel execution compatibility: Instance methods work seamlessly with parallel execution. Operations are serialized with all instance attributes (
op.__dict__) and unpickled in worker processes with full state restored.Automatic memory/performance tracking: BaseOperation (parent class) automatically tracks memory usage and execution time when the logger is configured for INFO level or higher. Disable by setting logger to WARNING.
Cross-platform compatibility: Some dependencies (rawpy, pympler) are platform-specific. Code must gracefully handle missing optional dependencies.
Integrity validation is optional: The
@validate_operation_integritydecorator only runs ifVALIDATE_OPS=Truein environment. This provides development-time safety without production overhead.
Examples
Implementing a custom ImageEnhancer:
>>> from phenotypic.abc_ import ImageEnhancer >>> from phenotypic import Image >>> from scipy.ndimage import gaussian_filter >>> class GaussianEnhancer(ImageEnhancer): ... '''Custom enhancer applying Gaussian blur to detect_mat.''' ... ... def __init__(self, sigma: float = 1.0): ... super().__init__() ... self.sigma = sigma # Instance attribute ... ... def _operate(self, image: Image) -> Image: ... '''Apply Gaussian blur to detect_mat.''' ... # Read detection matrix ... enh = image.detect_mat[:] ... # Apply Gaussian filter (access parameter via self) ... blurred = gaussian_filter(enh.astype(float), sigma=self.sigma) ... # Modify detect_mat through accessor ... image.detect_mat[:] = blurred.astype(enh.dtype) ... return image >>> # Usage >>> enhancer = GaussianEnhancer(sigma=2.5) >>> enhanced = enhancer.apply(image) # Original unchanged >>> enhanced_inplace = enhancer.apply(image, inplace=True) # Original modified
Implementing a custom ObjectDetector:
>>> from phenotypic.abc_ import ObjectDetector >>> from phenotypic import Image >>> from skimage.feature import peak_local_max >>> from skimage.measure import label as measure_label >>> import numpy as np >>> class PeakDetector(ObjectDetector): ... '''Detector using local peak finding to locate colonies.''' ... ... def __init__(self, min_distance: int = 10, threshold_abs: int = 100): ... super().__init__() ... self.min_distance = min_distance ... self.threshold_abs = threshold_abs ... ... def _operate(self, image: Image) -> Image: ... '''Find peaks in detect_mat and create object mask/map.''' ... # Find local maxima (colony peaks) - access parameters via self ... coords = peak_local_max( ... image.detect_mat[:], ... min_distance=self.min_distance, ... threshold_abs=self.threshold_abs ... ) ... # Create binary mask from peaks ... mask = np.zeros(image.detect_mat.shape, dtype=bool) ... for y, x in coords: ... mask[y, x] = True ... # Create labeled map from mask ... labeled_map = measure_label(mask) ... # Set detection results ... image.objmask[:] = mask ... image.objmap[:] = labeled_map ... return image >>> # Usage - automatic integrity validation in ImageDetector >>> detector = PeakDetector(min_distance=15, threshold_abs=120) >>> detected = detector.apply(image) >>> colonies = detected.objects >>> print(f"Detected {len(colonies)} colonies")
Understanding inplace parameter and memory efficiency:
>>> from phenotypic.enhance import GaussianBlur >>> from phenotypic import Image >>> image = Image.imread('colony_plate.jpg') >>> enhancer = GaussianBlur(sigma=2.0) >>> # Default: inplace=False (safe, creates copy) >>> enhanced = enhancer.apply(image) >>> print(f"Same object? {id(image) == id(enhanced)}") # False >>> # For memory efficiency with large images >>> result = enhancer.apply(image, inplace=True) >>> print(f"Same object? {id(image) == id(result)}") # True # inplace=True is useful in pipelines with many large images # to minimize memory overhead, but modifies the original
Using operations in a processing pipeline:
>>> from phenotypic import Image, ImagePipeline >>> from phenotypic.enhance import GaussianBlur >>> from phenotypic.detect import OtsuDetector >>> from phenotypic.grid import GridFinder >>> # Load image >>> image = Image.imread('colony_plate.jpg') >>> # Sequential chaining >>> enhanced = GaussianBlur(sigma=2).apply(image) >>> detected = OtsuDetector().apply(enhanced) >>> grid = GridFinder().apply(detected) >>> # Or use ImagePipeline for batch processing >>> pipeline = ImagePipeline() >>> pipeline.add(GaussianBlur(sigma=2)) >>> pipeline.add(OtsuDetector()) >>> pipeline.add(GridFinder()) >>> # Process multiple images with automatic parallelization >>> images = [Image.imread(f) for f in image_files] >>> results = pipeline.operate(images) # Results are fully processed images
How instance methods work with parallel execution:
>>> from phenotypic.abc_ import ImageOperation >>> from phenotypic import Image >>> class CustomThreshold(ImageOperation): ... def __init__(self, threshold: int, min_size: int = 5): ... super().__init__() ... self.threshold = threshold ... self.min_size = min_size ... ... def _operate(self, image: Image) -> Image: ... # Access parameters via self ... binary = image.detect_mat[:] > self.threshold ... image.objmask[:] = binary ... return image >>> # When apply() is called: >>> op = CustomThreshold(threshold=100, min_size=10) # apply() internally: # 1. Calls _apply_to_single_image() with self._operate (bound method) # 2. _apply_to_single_image calls operation(image) # 3. The bound method includes self, so all parameters are available >>> result = op.apply(image) # For parallel execution, the entire operation object (with all # attributes) is pickled and sent to worker processes
Methods
__init__Applies the operation to an image, either in-place or on a copy.
Return (and optionally display) the root widget.
- 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
- __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:
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.