Source code for phenotypic.refine._mask_fill
from __future__ import annotations
from typing import TYPE_CHECKING, Annotated
if TYPE_CHECKING:
from phenotypic._core._image import Image
import numpy as np
from pydantic import field_validator
from scipy.ndimage import binary_fill_holes
from phenotypic.abc_ import ObjectRefiner
from phenotypic.sdk_.funcs_ import is_binary_mask
from phenotypic.sdk_.typing_ import NdArrayField, TuneSpec
[docs]
class MaskFill(ObjectRefiner):
"""Fill enclosed holes within colony masks using binary flood fill.
Applies ``scipy.ndimage.binary_fill_holes`` to close voids produced by
illumination gradients, pigment heterogeneity, or specular glare within
colonies. Only holes that are completely enclosed by the mask are filled;
gaps that connect to the image boundary are left unchanged. The operation
produces simply connected colony masks better suited for area and shape
measurements.
For an overview of morphological refinement methods, see
:doc:`/explanation/refinement_strategies`.
Best For:
- Donut-shaped masks produced by global thresholding on colonies with
dark or depigmented centers.
- Colonies with radial pigment patterns or sector-level texture that
create interior gaps in the binary mask.
- Pre-measurement cleanup to ensure colony area counts every interior
pixel and circularity is not distorted by enclosed voids.
Consider Also:
- :class:`MaskClosing` for bridging narrow gaps between separate
fragments rather than filling holes within a single object.
- :class:`MaskOpening` for removing thin protrusions and external
connections rather than filling internal holes.
Args:
structure: Binary ndarray defining the connectivity neighbourhood for
flood fill. ``None`` uses the default cross-shaped (4-connected)
element from ``scipy.ndimage``. Default: ``None``.
origin: Integer offset applied to the structuring element centre.
Rarely needs adjustment from the default. Default: 0.
Returns:
Image: Input image with ``objmask`` updated so all enclosed holes are
filled; ``objmap`` is updated to reflect the filled mask.
Raises:
ValueError: If ``structure`` is provided but is not a binary array.
See Also:
:doc:`/how_to/notebooks/refine_noisy_boundaries` for a walkthrough
of refinement operations including hole filling.
"""
structure: NdArrayField | None = None
origin: Annotated[int, TuneSpec(tunable=False)] = 0
@field_validator("structure")
@classmethod
def _validate_structure(cls, structure: np.ndarray | None) -> np.ndarray | None:
"""Require ``structure``, when provided, to be a binary mask.
Reproduces the pre-migration ``__init__`` guard verbatim. The
``NdArrayField`` ``BeforeValidator`` has already coerced any
list input to an ``np.ndarray`` by the time this runs.
"""
if structure is not None:
if not is_binary_mask(structure):
raise ValueError("arr object array must be a binary array")
return structure
def _operate(self, image: Image) -> Image:
image.objmask[:] = binary_fill_holes(
input=image.objmask[:], structure=self.structure, origin=self.origin
)
return image