Source code for phenotypic.enhance._rank_median_enhancer

from __future__ import annotations

from typing import Annotated, TYPE_CHECKING

if TYPE_CHECKING:
    from phenotypic._core._image import Image
import numpy as np
from pydantic import field_validator
from skimage.filters.rank import median
from skimage.util import img_as_ubyte, img_as_float

from phenotypic.abc_ import Smoothing
from phenotypic.sdk_.typing_ import TuneSpec


[docs] class RankMedianEnhancer(Smoothing): """Suppress impulsive noise in ``detect_mat`` with rank-based median filtering. Applies a local median using rank filters with a configurable structuring element shape and size. Effectively removes salt-and-pepper noise, dust speckles, and pixel-level artifacts while preserving colony boundaries when the footprint is smaller than the minimum colony diameter. For algorithm details, see :doc:`/explanation/what_enhancement_does`. Best For: - Salt-and-pepper or impulsive noise from sensor defects or scanner CCD artifacts. - Dust speckles and pixel-level artifacts on scanned plates. - Grid-like imaging artifacts where a ``'square'`` footprint aligns with the noise geometry. - Pre-detection cleanup before applying a threshold-based detector. Consider Also: - :class:`LocalEdgeDenoise` for edge-preserving smoothing of Gaussian noise without the uint8 conversion required by rank filters. - :class:`NonLocalMeansDenoiser` for patch-based denoising that preserves fine colony texture better on noisy plates. - :class:`GrayOpening` for morphological artifact removal that does not require uint8 quantization. Args: shape: Footprint geometry. Accepted values: ``'disk'`` for isotropic smoothing suited to round colonies; ``'square'`` (default) to align with grid-pattern sensor artifacts. width: Footprint width in pixels. Set smaller than the minimum colony diameter to preserve colony edges. ``None`` (default) auto-derives a small value as approximately 0.2 % of the shorter image dimension. shift_x: Horizontal offset of the footprint centre in pixels. Non-zero values shift the filter kernel to correct for directional streak artefacts. Default: 0. shift_y: Vertical offset of the footprint centre in pixels. Non-zero values shift the filter kernel to correct for directional streak artefacts. Default: 0. Returns: Image: Input image with ``detect_mat`` median-filtered. ``rgb`` and ``gray`` are unchanged. See Also: :doc:`/tutorials/notebooks/03_enhancing_before_detection` for a visual walkthrough of denoising pipelines on plate images. :doc:`/explanation/what_enhancement_does` for background on rank filtering and its role in colony detection pipelines. """ shape: str = "square" width: Annotated[int | None, TuneSpec(3, 15, step=2)] = None shift_x: Annotated[int, TuneSpec(tunable=False)] = 0 shift_y: Annotated[int, TuneSpec(tunable=False)] = 0 @field_validator("shape") @classmethod def _check_shape_supported(cls, shape: str) -> str: """Reject an unsupported footprint shape (matches the legacy guard).""" if shape not in ["disk", "square", "sphere", "cube"]: raise ValueError(f"shape shape {shape} is not supported") return shape def _operate(self, image: Image) -> Image: image.detect_mat[:] = img_as_float( median( image=img_as_ubyte(image.detect_mat[:]), footprint=self._get_footprint( self._get_footprint_width(image.detect_mat[:]) ), ) ) return image def _get_footprint_width(self, detect_mat: np.ndarray) -> int: if self.width is None: return int(np.min(detect_mat.shape) * 0.002) else: return self.width def _get_footprint(self, width: int) -> np.ndarray: match self.shape: # Use the central ImageEnhancer utility for common 2D shapes case "disk" | "square": return self._make_footprint(shape=self.shape, width=width) case _: raise TypeError("Unknown shape shape")