Source code for phenotypic.refine._mask_dilation

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


[docs] class MaskDilation(ObjectRefiner, FootprintMixin): """Expand colony masks outward by morphological dilation. Adds pixels around all object boundaries, extending each colony mask by one or more structuring element widths. This bridges thin gaps between nearby fragments and recovers faint colony halos that strict thresholding excluded. Dilation permanently inflates mask area; pair with :class:`MaskErosion` or use :class:`MaskClosing` if area accuracy matters. For an overview of morphological refinement methods, see :doc:`/explanation/refinement_strategies`. Best For: - Bridging thin background gaps between fragments of the same colony before merge-based refinement. - Recovering faint outgrowth halos that fall just below the detection threshold on plates with heterogeneous colony intensity. - Expanding colony masks to include diffuse colony edges before intensity or color measurements. Consider Also: - :class:`MaskClosing` for dilation followed by erosion that bridges gaps without permanently inflating colony area. - :class:`MaskErosion` for the opposite effect — shrinking masks inward to remove thin protrusions or boundary noise. Args: shape: Structuring element shape for the dilation footprint. ``"auto"`` scales a diamond 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 expand masks further. Typical range: 1--7. Default: 3. n_iter: Number of dilation iterations applied sequentially. Each additional iteration extends the mask by one further footprint radius. Default: 1. Returns: Image: Input image with ``objmask`` and ``objmap`` dilated outward. 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( shape="diamond", 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(shape=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[:] = dilation(image.objmask[:], footprint=footprint) return image