Source code for phenotypic.enhance._rank_median_enhancer

from __future__ import annotations

from typing import TYPE_CHECKING

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

from phenotypic.abc_ import ImageEnhancer


[docs] class RankMedianEnhancer(ImageEnhancer): """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`. Args: shape: Footprint geometry. ``'disk'`` for isotropic smoothing; ``'square'`` (default) to align with grid artifacts. width: Footprint width in pixels. Set smaller than the minimum colony diameter to preserve colony edges. ``None`` (default) derives a small value from image size. shift_x: Horizontal footprint offset. Typically 0. Default: 0. shift_y: Vertical footprint offset. Typically 0. Default: 0. Returns: Image: Input image with ``detect_mat`` median-filtered. ``rgb`` and ``gray`` are unchanged. Best For: - Salt-and-pepper or impulsive noise from sensor defects. - Dust speckles and pixel-level artifacts on scanned plates. - Grid-like imaging artifacts when using a ``'square'`` footprint. Consider Also: - :class:`BilateralDenoise` for edge-preserving Gaussian noise removal without the intensity quantization of rank filters. - :class:`NonLocalMeansDenoiser` for patch-based denoising that preserves texture better on noisy plates. - :class:`GrayOpening` for morphological artifact removal that does not require uint8 conversion. See Also: :doc:`/tutorials/notebooks/03_enhancing_before_detection` for a visual walkthrough of denoising pipelines on plate images. """
[docs] def __init__(self, shape: str = "square", width: int = None, shift_x=0, shift_y=0): """ Parameters: shape (str): Geometry of the neighborhood. Use 'disk' for isotropic smoothing on plates; 'square' to align with grid noise; 'sphere'/'cube' for 3D contexts. Default 'square'. width (int | None): Neighborhood width in pixels. Set smaller than the minimum colony width to preserve colony edges; None chooses a small default based on image size. shift_x (int): Horizontal offset of the shape center to bias the neighborhood if artifacts are directional. Typically 0. shift_y (int): Vertical offset of the shape center. Typically 0. """ if shape not in ["disk", "square", "sphere", "cube"]: raise ValueError(f"shape shape {shape} is not supported") self.shape = shape self.width = width self.shift_x = shift_x self.shift_y = shift_y
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")