Source code for phenotypic.enhance._bm3d_denoiser

from __future__ import annotations

from typing import TYPE_CHECKING, Literal

if TYPE_CHECKING:
    from phenotypic._core._image import Image
import bm3d
from bm3d.profiles import BM3DStages

from ..abc_ import ImageEnhancer


[docs] class BM3DDenoiser(ImageEnhancer): """Denoise ``detect_mat`` with block-matching and 3D collaborative filtering. Groups similar image patches and filters them jointly in the transform domain, preserving fine colony details while removing structured noise patterns (scanner artifacts, systematic CCD noise, imaging hardware texture). Produces higher-quality results than simple Gaussian blur at significantly higher computational cost. For algorithm details, see :doc:`/explanation/what_enhancement_does`. Args: sigma_psd: Noise standard deviation in [0, 1] normalized scale. Typical range: 0.01--0.05 for moderate noise, 0.05--0.15 for heavy noise. Too low preserves noise; too high removes colony texture. Default: 0.02. block_size: Block size for BM3D patch matching. Default: 8. stage_arg: Processing mode. ``'all_stages'`` (default) applies both hard thresholding and Wiener filtering for highest quality; ``'hard_thresholding'`` runs only the first stage for faster processing. clip: Clip output to [0, 1]. Default: ``True``. Set to ``False`` when using with variance-stabilizing transforms (e.g., GAT). Returns: Image: Input image with ``detect_mat`` denoised via BM3D collaborative filtering. ``rgb`` and ``gray`` are unchanged. Best For: - Structured camera or scanner noise on plate images. - Low-light imaging where high ISO introduces patterned noise. - Preserving fine morphological features (wrinkles, satellite colonies) during denoising. Consider Also: - :class:`BilateralDenoise` for faster edge-preserving denoising when structured noise is not the primary concern. - :class:`NonLocalMeansDenoiser` for patch-based denoising with lower computational overhead. - :class:`VisuShrinkEnhancer` for fast wavelet denoising when speed matters more than quality. References: [1] K. Dabov, A. Foi, V. Katkovnik, and K. Egiazarian, "Image denoising by sparse 3-D transform-domain collaborative filtering," *IEEE Trans. Image Process.*, vol. 16, no. 8, pp. 2080--2095, Aug. 2007. See Also: :doc:`/tutorials/notebooks/03_enhancing_before_detection` for a visual walkthrough of denoising pipelines on plate images. :doc:`/how_to/notebooks/denoise_low_light` for BM3D and other denoising strategies on low-light plate images. """
[docs] def __init__( self, sigma_psd: float = 0.02, block_size: int = 8, *, stage_arg: Literal["all_stages", "hard_thresholding"] = "all_stages", clip: bool = True, ): """ Parameters: sigma_psd (float): Noise level estimate in [0, 1] normalized scale. Start with 0.02-0.05 for typical scanner noise on plates (equivalent to σ=5-12 on 8-bit). Higher value -> more noise. block_size (int): Block size for BM3D denoising. Default is 8. stage_arg (Literal["all_stages", "hard_thresholding"]): Denoising stages to run. 'all_stages' gives best quality at the cost of speed; 'hard_thresholding' is faster and adequate for routine plate analysis. clip (bool): Whether to clip output to [0, 1] range. Default True. Set to False when using with variance-stabilizing transforms (e.g., GAT) that require preserving the original scale. """ if not isinstance(sigma_psd, (int, float)): raise TypeError("sigma_psd must be a number or None") if sigma_psd < 0: raise ValueError("sigma_psd must be non-negative") self.sigma_psd = float(sigma_psd) if stage_arg not in ["all_stages", "hard_thresholding"]: raise ValueError("stage_arg must be 'all_stages' or 'hard_thresholding'") else: self.stage_arg = stage_arg self.block_size = block_size self.clip = clip
def _operate(self, image: Image) -> Image: # detect_mat is guaranteed to be in [0, 1] range, which BM3D expects profile = bm3d.BM3DProfile() profile.bs_ht = self.block_size profile.bs_wiener = self.block_size denoised = bm3d.bm3d( image.detect_mat[:], profile=profile, sigma_psd=self.sigma_psd, stage_arg=self._convert_stage_arg(self.stage_arg), ) if self.clip: denoised = denoised.clip(0.0, 1.0) image.detect_mat[:] = denoised return image def _convert_stage_arg(self, stage_arg: Literal["all_stages", "hard_thresholding"]): match stage_arg: case "hard_thresholding": return BM3DStages.HARD_THRESHOLDING case "all_stages": return BM3DStages.ALL_STAGES case _: raise ValueError(f"Unknown stage arg: {stage_arg}")