Source code for phenotypic.abc_._grid_operation

from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from phenotypic._core._grid_image import GridImage
from phenotypic.abc_ import ImageOperation
from abc import ABC


[docs] class GridOperation(ImageOperation, ABC): """Abstract base class for operations on grid-aligned plate images. GridOperation is a marker abstract base class that enforces type safety for operations designed to work exclusively with GridImage objects. It's a lightweight subclass of ImageOperation that overrides the apply() method to require a GridImage input instead of a generic Image. **Quick Decision Guide** Use GridOperation when: - Your operation requires grid structure information (well positions, row/column layout) - You're processing arrayed plate images with regular grid layouts (96-well, 384-well) - Your algorithm needs per-well analysis or grid-aligned regions - You want to enforce that input must be GridImage (type safety) Use ImageOperation when: - Your operation works on general Image objects regardless of grid state - You're doing global preprocessing, detection, or measurement - Your algorithm doesn't depend on well structure or grid alignment - Your operation should accept both Image and GridImage inputs Combining GridOperation with ImageOperation: - GridOperation is typically paired with other ABCs (ObjectDetector, ImageCorrector, etc.) - Use multiple inheritance: class GridObjectDetector(ObjectDetector, GridOperation, ABC) - GridOperation adds type safety without changing algorithm implementation - Most grid operations inherit from both a specific ABC and GridOperation **What is GridOperation?** GridOperation exists to distinguish between two categories of image operations: - **ImageOperation:** Works on single, unaligned Image objects. The image may or may not have grid information. Used for general-purpose preprocessing, detection, and measurement. Examples: GaussianBlur, OtsuDetector, MeasureColorComposition. - **GridOperation:** Works only on GridImage objects that have grid structure information (row/column layout of wells on an agar plate). The operation assumes grid information is present and available. Used for grid-aware operations where well-level analysis or grid alignment is required. Examples: GridObjectDetector, GridCorrector, GridRefiner. **Why GridOperation exists** GridOperation provides three key benefits: 1. **Type Safety:** The apply() method signature requires a GridImage argument, catching misuse at runtime if someone tries to apply a grid operation to a plain Image. 2. **Intent Clarity:** Developers can immediately see which operations require grid information, making the design space clear: "Use ImageOperation for general image ops, GridOperation for plate-specific grid-aware ops." 3. **Documentation:** Allows documentation and tutorials to clearly distinguish operations by their input type requirements. **What is GridImage?** GridImage is a specialized Image subclass that adds grid structure information: - **Inherits from Image:** All standard image capabilities (RGB, grayscale, color spaces, object detection results, etc.) are available. - **Adds grid field:** Contains a ``grid`` attribute (GridInfo object) storing the detected or specified grid layout (row/column positions, cell dimensions, rotation angle). - **Arrayed plate context:** Represents images of agar plates with samples arranged in regular grids (96-well, 384-well, 1536-well formats). Typical nrows=8, ncols=12 for 96-well plates. - **Grid accessors:** Via ``image.grid``, provides row/column counts, well positions, and grid-related metadata. **GridOperation Subclasses** Most concrete grid operations inherit from BOTH a specific operation ABC (like ObjectDetector) AND GridOperation to create specialized grid-aware variants: - ``GridObjectDetector``: Detects objects using grid structure. Subclasses implement well-level colony detection on gridded plates. - ``GridCorrector``: Corrects grid alignment, rotation, and per-well color correction. Improves grid positioning and well-level alignment. - ``GridObjectRefiner``: Refines detection masks at the well level. Filters and adjusts masks based on well location and size constraints. - ``GridMeasureFeatures``: Extracts per-well measurements. Computes features organized by grid coordinates rather than globally. - ``GridFinder``: Detects grid structure from object positions. Assigns detected objects to grid cells and determines well locations. **Multiple Inheritance Pattern** Most GridOperation subclasses use multiple inheritance to combine operation behavior with grid type safety: - Combine with ObjectDetector: class GridObjectDetector(ObjectDetector, GridOperation, ABC) - Combine with ImageCorrector: class GridCorrector(ImageCorrector, GridOperation, ABC) - Combine with any operation ABC: class CustomGridOp(SomeABC, GridOperation, ABC) The inheritance order matters: specific ABC first, then GridOperation. Example of multiple inheritance pattern: >>> from phenotypic.abc_ import ImageOperation, GridOperation >>> from phenotypic import GridImage, Image >>> # Concrete implementation combining ObjectDetector + GridOperation >>> # class GridObjectDetector(ObjectDetector, GridOperation, ABC): >>> # def _operate(self, image: GridImage) -> GridImage: >>> # # Implementation uses grid structure from image.grid >>> # return image This combines: - **Operation behavior:** Sets image.objmask and image.objmap, with integrity checks. - **GridOperation type safety:** Requires GridImage input, enforced at runtime. - **ABC pattern:** Subclasses implement _operate() with grid-aware logic. The key insight: GridOperation is just a type annotation layer over ImageOperation that makes the grid requirement explicit in the method signature. **Type Safety Example** GridOperation enforces type checking at apply() time to catch errors early: >>> from phenotypic import Image, GridImage >>> from phenotypic.abc_ import GridOperation >>> # When a GridOperation is called with wrong type: >>> # detector = SomeGridOperation() # subclass of GridOperation >>> # result = detector.apply(Image('plain.jpg')) # Raises GridImageInputError >>> # result = detector.apply(GridImage('plate.jpg', nrows=8, ncols=12)) # OK **When to subclass GridOperation** Subclass GridOperation when your operation: 1. **Requires grid information:** Needs to access ``image.grid`` to get well positions, row/column structure, or grid-aligned regions. 2. **Operates on well-level data:** Processes colonies at the well level rather than globally on the image (e.g., per-well filtering, well-based alignment). 3. **Makes assumptions about grid structure:** Your algorithm assumes a regular grid layout and would fail or produce nonsensical results on an image without grid info. Otherwise, subclass ImageOperation instead. GridOperation operations are more specialized and less broadly applicable. Notes: - GridOperation is a marker class with no implementation. It only overrides apply() to specify the GridImage type and enforce input validation. - GridImage inherits all Image functionality. Grid information is accessed via the ``grid`` accessor: ``image.grid.nrows``, ``image.grid.ncols``, etc. - If you're unsure whether your operation needs GridOperation, ask: "Does this algorithm fundamentally depend on grid structure?" If yes, use GridOperation. If it works equally well on plain Images, use ImageOperation. - GridImage is typically created with GridFinder operations that detect grid structure. GridFinder detects grid positions and creates a GridImage suitable for downstream GridOperation subclasses. Examples: Using a GridOperation subclass with GridImage: >>> from phenotypic import GridImage >>> from phenotypic.data import load_synth_yeast_plate >>> from phenotypic.detect import GridObjectDetector >>> # Load plate image with grid info >>> image = load_synth_yeast_plate() # GridImage with detected colonies >>> grid_image = image >>> # Apply a grid-aware detector (subclass of GridObjectDetector) >>> # GridImage is required - type-safe operation >>> # detector = GridObjectDetector() # Concrete subclass in practice >>> # detected = detector.apply(grid_image) Type safety: GridOperation prevents misuse: >>> from phenotypic import Image, GridImage >>> from phenotypic.enhance import GaussianBlur >>> from phenotypic.data import load_synth_yeast_plate >>> image = Image('generic.jpg') # Plain Image >>> grid_image = load_synth_yeast_plate() # GridImage >>> # ImageOperation (GaussianBlur) accepts both >>> enhancer = GaussianBlur(sigma=2) >>> result1 = enhancer.apply(image) # OK: Image -> Image >>> result2 = enhancer.apply(grid_image) # OK: GridImage -> GridImage >>> # GridOperation requires GridImage only >>> # detector = SomeGridOperation() # subclass of GridOperation >>> # result3 = detector.apply(grid_image) # OK: GridImage -> GridImage >>> # result4 = detector.apply(image) # ERROR: raises GridImageInputError """
[docs] def apply(self, image: GridImage, inplace: bool = False) -> GridImage: return super().apply(image=image, inplace=inplace)