from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from phenotypic 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.
**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 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:
.. code-block:: text
GridOperation (marker ABC)
├── GridObjectDetector (inherits ObjectDetector + GridOperation)
│ ├── GridInstanceDetector, GridThresholdDetector, GridCannyDetector, ...
│ └── Use for: well-level colony detection on gridded plates
│
├── GridCorrector (inherits ImageCorrector + GridOperation)
│ ├── GridAligner, ...
│ └── Use for: grid alignment, rotation, color correction per-well
│
└── GridObjectRefiner (inherits ObjectRefiner + GridOperation)
├── GridSizeRefiner, ...
└── Use for: per-well mask refinement, filtering by well location
**When to use GridOperation vs ImageOperation**
- **ImageOperation:** Input is a plain Image with unknown grid state.
Typical use: preprocessing (blur, contrast), general-purpose detection,
color measurements that don't depend on grid layout.
- **GridOperation:** Input is a GridImage with detected/specified grid structure.
Typical use: well-level analysis, grid-based refinement, operations that
reference well positions or grid-aligned regions.
- **Overlap:** Some operations work on both. E.g., a ColorComposition measurement
can apply to an Image, but a GridColorComposition can specialize to per-well
measurements on a GridImage.
**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.
**Multiple inheritance pattern**
Most GridOperation subclasses use multiple inheritance:
.. code-block:: python
class GridObjectDetector(ObjectDetector, GridOperation, ABC):
'''Detects objects using grid structure.'''
def apply(self, image: GridImage, inplace=False) -> GridImage:
if not isinstance(image, GridImage):
raise GridImageInputError
return super().apply(image=image, inplace=inplace)
This combines:
- **ObjectDetector 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.
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 ImageGridHandler or GridFinder operations
that detect grid structure. GridFinder is an ImageOperation, but the result
is a GridImage suitable for downstream GridOperation subclasses.
Examples:
.. dropdown:: Using a GridOperation subclass
.. code-block:: python
from phenotypic import GridImage
from phenotypic.detect import GridObjectDetector
# Load plate image (96-well)
grid_image = GridImage('plate_scan.jpg', nrows=8, ncols=12)
# Apply a grid-aware detector (subclass of GridObjectDetector)
# This operation requires GridImage and uses well structure
detector = GridObjectDetector() # Concrete subclass
detected = detector.apply(grid_image) # Type-safe: GridImage -> GridImage
# Access detected colonies per well
for well_row in range(grid_image.nrows):
for well_col in range(grid_image.ncols):
# Per-well analysis available because operation is grid-aware
pass
.. dropdown:: Understanding the type safety benefit
.. code-block:: python
from phenotypic import Image, GridImage
from phenotypic.enhance import GaussianBlur
from phenotypic.detect import GridObjectDetector
image = Image.from_image_path('generic.jpg') # Plain Image
grid_image = GridImage('plate.jpg') # 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
detector = GridObjectDetector() # 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)