Source code for phenotypic.refine._mask_gradient
from __future__ import annotations
from typing import Annotated, Literal, TYPE_CHECKING
if TYPE_CHECKING:
from phenotypic._core._image import Image
from phenotypic.abc_ import ObjectRefiner
from phenotypic.sdk_.mixin import FootprintMixin
from phenotypic.sdk_.typing_ import NdArrayField, TuneSpec
import numpy as np
from skimage.morphology import dilation, erosion
[docs]
class MaskGradient(ObjectRefiner, FootprintMixin):
"""Extract colony boundary outlines via morphological gradient.
Computes the morphological gradient (dilation minus erosion) of the binary
object mask, retaining only the pixels that lie on object boundaries. The
result is a thin perimeter outline of each colony with interior and exterior
pixels set to background. Boundary thickness scales with ``width``.
For an overview of morphological refinement methods, see
:doc:`/explanation/refinement_strategies`.
Best For:
- Extracting colony perimeters for boundary roughness or circularity
measurements that require edge-only pixel sets.
- Creating boundary masks for edge-specific color or texture sampling
to characterize colony rim pigmentation.
- Generating QC overlays that visualize detected colony contours on
top of the raw RGB image.
- Isolating spreading or filamentous edges extending from colony cores
as a precursor to morphological analysis.
Consider Also:
- :class:`Skeletonize` when medial-axis topology or branch structure
is needed rather than a boundary outline.
- :class:`Thinning` for iterative boundary peeling that preserves
connectivity and produces a thinner skeleton-like result.
- :class:`MaskErosion` for uniform inward shrinking of the full mask
without reducing the result to boundary pixels only.
Args:
shape: Structuring element shape for gradient computation. ``"auto"``
scales a disk to the image size; ``"disk"``, ``"square"``, and
``"diamond"`` use named shapes at the given ``width``; a NumPy
array provides a custom element; ``None`` uses the skimage library
default. Default: ``None``.
width: Footprint width in pixels when using a named shape or auto
scaling. Larger values produce thicker boundary outlines. Typical
range: 1--5. Default: 1.
Returns:
Image: Input image with ``objmask`` replaced by the gradient boundary
mask; interior and exterior pixels are set to background.
Raises:
AttributeError: If an unrecognised ``shape`` value is provided.
See Also:
:doc:`/how_to/notebooks/refine_noisy_boundaries` for boundary
extraction workflows on real plate images.
"""
shape: Literal["auto", "square", "diamond", "disk"] | NdArrayField | None = None
width: Annotated[int, TuneSpec(1, 5)] = 1
def _operate(self, image: Image) -> Image:
if self.shape == "auto":
footprint = FootprintMixin._make_footprint(
"disk", width=max(1, round(np.min(image.shape) * 0.002))
)
elif isinstance(self.shape, np.ndarray):
footprint = self.shape
elif self.shape in self._footprint_shapes:
footprint = FootprintMixin._make_footprint(self.shape, width=self.width)
elif not self.shape:
footprint = None
else:
raise AttributeError("Invalid shape type")
# Compute morphological gradient: dilated - eroded
mask = image.objmask[:]
dilated_mask = dilation(mask, footprint=footprint)
eroded_mask = erosion(mask, footprint=footprint)
gradient_mask = dilated_mask & ~eroded_mask # Boundary pixels
image.objmask[:] = gradient_mask
return image