Source code for phenotypic.refine._mask_gradient

from __future__ import annotations

from typing import Literal, TYPE_CHECKING

if TYPE_CHECKING:
    from phenotypic._core._image import Image

from phenotypic.abc_ import ObjectRefiner
from phenotypic.tools_.mixin import FootprintMixin

import numpy as np
from skimage.morphology import dilation, erosion


[docs] class MaskGradient(ObjectRefiner, FootprintMixin): """Extract object boundaries via morphological gradient (dilation minus erosion). Computes the difference between dilation and erosion of the binary mask, producing a thin outline of each object's boundary pixels. Interior and exterior pixels are removed, leaving only the colony perimeter for edge-focused analysis or visualization. Args: shape: Structuring element for gradient computation. ``"auto"`` selects a disk scaled to image size, ``"disk"``, ``"square"``, or ``"diamond"`` use a named shape at the given width, a NumPy array provides a custom element, and ``None`` uses the library default. Default: None. width: Footprint width in pixels when using named shapes or auto-scaling. Larger values produce thicker boundaries. Typical range: 1--5. Default: 1. Returns: Image: Input image with ``objmask`` replaced by the gradient boundary mask. Raises: AttributeError: If an invalid ``shape`` type is provided. Best For: - Extracting colony perimeters for boundary roughness or circularity measurements. - Creating boundary masks for edge-specific color or texture analysis. - Visualizing colony contours as QC overlays on raw images. - Detecting spreading or filamentous edges extending from colony cores. Consider Also: - :class:`Skeletonize` when you need medial-axis topology rather than boundary outlines. - :class:`MaskEroder` for uniform inward shrinking without extracting boundaries. - :class:`Thinning` for iterative boundary peeling that preserves connectivity. See Also: :doc:`/how_to/notebooks/refine_noisy_boundaries` for boundary extraction workflows. :doc:`/explanation/refinement_strategies` for a comparison of morphological refinement methods. """
[docs] def __init__( self, shape: Literal[ "auto", "square", "diamond", "disk"] | np.ndarray | None = None, width: int = 1 ): """Initialize the gradient extractor. Args: shape (Literal["auto", "square", "diamond", "disk"] | np.ndarray | None): Structuring element for gradient computation. Use: - "auto" to select a disk shape scaled to image size, - a NumPy array to pass a custom shape, - one of the named shapes ("disk", "square", "diamond") with a specified width, - or ``None`` to use the library default. Larger widths produce thicker boundaries with less precision but more robustness to noise. width (int): Footprint width in pixels when using named shapes or auto-scaling. Default: 1 pixel (thin, precise boundaries). """ super().__init__() self.shape = shape self.width = width
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