Source code for phenotypic.refine._mask_erosion

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 erosion


[docs] class MaskErosion(ObjectRefiner, FootprintMixin): """Shrink colony masks inward by morphological erosion to remove boundary noise. Removes outer boundary pixels from every detected object, eliminating thin whiskers, isolated specks, and uncertain soft-edge pixels produced by over-sensitive thresholding. The core colony structure is preserved but masks are permanently reduced in area; pair with :class:`MaskDilation` or use :class:`MaskOpening` if area must be recovered. For an overview of morphological refinement methods, see :doc:`/explanation/refinement_strategies`. Best For: - Removing thin protrusions or whisker artefacts extending from colony edges after edge-sensitive detection. - Eliminating isolated noise specks that survived previous cleanup steps and registered as very small detections. - Excluding uncertain boundary pixels before high-precision shape or area measurements to tighten the colony footprint. Consider Also: - :class:`MaskDilation` for the opposite effect — expanding masks outward to recover halos or bridge gaps. - :class:`MaskOpening` for erosion followed by dilation that removes thin protrusions without permanently shrinking the colony body. - :class:`SmallObjectRemover` for discarding small objects entirely by area threshold rather than shrinking all objects uniformly. Args: shape: Structuring element shape for the erosion footprint. ``"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. Larger values erode more deeply inward. Typical range: 1--7. Default: 3. n_iter: Number of erosion iterations applied sequentially. Each additional iteration removes one further layer of boundary pixels. Default: 1. Returns: Image: Input image with ``objmask`` and ``objmap`` eroded inward. See Also: :doc:`/explanation/refinement_strategies` for the recommended morphological refinement sequence. """ shape: Literal["auto", "square", "diamond", "disk"] | NdArrayField | None = None width: Annotated[int, TuneSpec(1, 7)] = 3 n_iter: Annotated[int, TuneSpec(1, 3)] = 1 def _operate(self, image: Image) -> Image: if self.shape == "auto": footprint = FootprintMixin._make_footprint( "disk", width=max(2, round(np.min(image.shape) * 0.003)) ) 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") for _ in range(self.n_iter): image.objmask[:] = erosion(image.objmask[:], footprint=footprint) return image