Source code for phenotypic.enhance._bayesshrink_enhancer

from __future__ import annotations

from typing import TYPE_CHECKING, Annotated, ClassVar, Literal

if TYPE_CHECKING:
    from phenotypic._core._image import Image

from skimage.restoration import denoise_wavelet

from ..abc_ import ImageDenoiser
from ..sdk_.mixin import _GATSupportMixin
from ..sdk_.typing_ import TuneSpec


[docs] class BayesShrinkEnhancer(_GATSupportMixin, ImageDenoiser): """Denoise ``detect_mat`` with adaptive per-subband BayesShrink wavelet thresholding. Decomposes the detection matrix into wavelet subbands and applies a separate soft-threshold to each, computed from the estimated signal and noise variances. This preserves colony texture more faithfully than a universal threshold while suppressing scanner and camera noise. For algorithm details see :doc:`/explanation/what_enhancement_does`. Best For: - Flatbed scanner images where CCD read noise is spatially uniform and the auto-estimated sigma is reliable. - Plates requiring fine colony texture retention before downstream morphology measurement or texture analysis. - Pre-processing before ridge or edge enhancers that amplify noise alongside structure. - Low-light fluorescence plate images with Poisson-dominated shot noise when ``use_gat=True`` is enabled. - Pipelines where denoising must be fully serialisable and reproducible across runs. Consider Also: - :class:`VisuShrinkEnhancer` when a single universal threshold across all subbands is preferred for speed or simplicity. - :class:`EnhanceBlockMatch` for structured scanner banding or systematic CCD patterned noise that wavelet thresholding does not fully remove. - :class:`NonLocalMeansDenoiser` when patch-based self-similarity in the agar or colony texture can be exploited. - :class:`MedianFilter` for salt-and-pepper impulse noise rather than Gaussian additive noise. Args: sigma: Noise standard deviation on the [0, 1] intensity scale used to compute per-subband BayesShrink thresholds. ``None`` (default) auto-estimates via the MAD of the finest-scale HH subband (``median(|coeff|) / 0.6745``). Typical manual override: 0.01--0.05 for moderate scanner noise. Raising sigma shrinks more coefficients and smooths more aggressively; lowering it retains more texture alongside residual noise. Ignored when ``use_gat=True`` — the stabilised-domain value 1.0 is used internally. Default: ``None``. wavelet: PyWavelets wavelet family string. Must be an orthogonal wavelet for BayesShrink's variance-preservation property to hold. ``'db2'`` (default) has compact support and handles colony edges well; ``'db4'`` represents smoother signals more sparsely and may suit filamentous fungi hyphae. Accepted examples: ``'db1'``-- ``'db8'``, ``'sym2'``--``'sym8'``. Default: ``'db2'``. mode: Wavelet coefficient thresholding mode. ``'soft'`` (default) shrinks each coefficient toward zero by the threshold amount, producing a smooth, ringing-free output consistent with BayesShrink's Bayesian risk derivation. ``'hard'`` zeros coefficients below the threshold and leaves those above unchanged; this can introduce pseudo-Gibbs artefacts near sharp colony edges. Default: ``'soft'``. wavelet_levels: Decomposition depth. ``None`` (default) uses ``max_possible_levels - 3``, a conservative choice that avoids the coarsest subbands where reliable per-subband threshold estimation fails. Practical range: 2--8 integers; fewer than 3 leaves coarse-scale noise untouched; more than ``max - 3`` risks over-smoothing broad colony texture. Default: ``None``. clip: Clamp output to [0, 1] after reconstruction. Soft thresholding can produce values marginally outside [0, 1] due to floating-point accumulation. Default: ``True``. Automatically set to ``False`` inside the GAT region when ``use_gat=True``. rescale_sigma: Allow skimage to rescale ``sigma`` to match each subband's energy (the statistically correct behaviour for orthonormal wavelets). Default: ``True``. Automatically forced to ``False`` when ``use_gat=True`` because the stabilised domain has a different variance profile. Default: ``True``. # GAT parameters — only active when use_gat=True use_gat: Wrap the denoise call in a forward Generalised Anscombe Transform (GAT) → denoise at fixed sigma=1.0 → exact unbiased inverse GAT pipeline. Enables correct denoising under mixed Poisson-Gaussian noise (e.g. fluorescence plate readers, low-light incubator cameras). Leave ``False`` for standard flatbed scanner images where additive Gaussian noise dominates. Default: ``False``. gat_gain: Camera gain in electrons per ADU, used by the GAT to model Poisson variance scaling. Obtain from the sensor datasheet or a photon-transfer curve. Only relevant when ``use_gat=True``. Default: 1.0. gat_mu: Read-noise mean (DC baseline offset) in ADU before [0, 1] normalisation. Set to the dark-current bias level if the image has not been background-subtracted. Only relevant when ``use_gat=True``. Default: 0.0. gat_read_sigma: Standard deviation of the Gaussian read-noise component (electrons RMS). Setting this to the manufacturer's read noise improves stabilisation accuracy under mixed Poisson-Gaussian conditions. Only relevant when ``use_gat=True``. Default: 0.0. gat_scale_factor: Multiplier converting the [0, 1] normalised ``detect_mat`` back to photon counts before the forward GAT. ``None`` auto-detects from ``image.metadata.bit_depth`` (255 for 8-bit, 65535 for 16-bit). Only relevant when ``use_gat=True``. Default: ``None``. Returns: Image: Input image with ``detect_mat`` denoised via adaptive wavelet thresholding. ``rgb`` and ``gray`` are unchanged. References: [1] S. G. Chang, B. Yu, and M. Vetterli, "Adaptive wavelet thresholding for image denoising and compression," *IEEE Trans. Image Process.*, vol. 9, no. 9, pp. 1532--1546, Sep. 2000. [2] D. L. Donoho and I. M. Johnstone, "Ideal spatial adaptation by wavelet shrinkage," *Biometrika*, vol. 81, no. 3, pp. 425--455, Sep. 1994. [3] M. Mäkitalo and A. Foi, "Optimal inversion of the generalized Anscombe transformation for Poisson-Gaussian noise," *IEEE Trans. Image Process.*, vol. 22, no. 1, pp. 91--103, Jan. 2013. See Also: :doc:`/tutorials/notebooks/03_enhancing_before_detection` for a visual walkthrough of enhancement pipelines on plate images. :doc:`/how_to/notebooks/denoise_low_light` for guidance on choosing between BayesShrink, BM3D, and non-local means for low-light plates. :doc:`/explanation/what_enhancement_does` for background on wavelet denoising and threshold selection strategies. """ _GAT_NOISE_PARAMS: ClassVar[dict[str, float]] = {"sigma": 1.0} _GAT_DEFER_ATTRS: ClassVar[tuple[str, ...]] = ("clip", "rescale_sigma") sigma: float | None = None wavelet: str = "db2" mode: Literal["soft", "hard"] = "soft" wavelet_levels: Annotated[int | None, TuneSpec(2, 6)] = None clip: bool = True rescale_sigma: bool = True def _operate(self, image: Image) -> Image: """Apply BayesShrink adaptive wavelet denoising to detection matrix.""" self._gat_apply(image, "detect_mat", self._denoise_detect_mat) return image def _denoise_detect_mat(self, image: Image) -> None: denoised = denoise_wavelet( image=image.detect_mat[:], sigma=self.sigma, wavelet=self.wavelet, mode=self.mode, wavelet_levels=self.wavelet_levels, method="BayesShrink", channel_axis=None, rescale_sigma=self.rescale_sigma, ) if self.clip: denoised = denoised.clip(0.0, 1.0) image.detect_mat[:] = denoised