from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from phenotypic._core._image import Image
from skimage.restoration import denoise_bilateral
from ..abc_ import ImageEnhancer
[docs]
class BilateralDenoise(ImageEnhancer):
"""Denoise ``detect_mat`` with edge-preserving bilateral filtering.
Averages pixel values based on both spatial proximity and intensity
similarity, preserving sharp colony boundaries while smoothing uniform
regions such as agar background. Effectively removes scanner noise,
agar grain, dust speckles, and condensation artifacts without blurring
colony edges.
For algorithm details, see :doc:`/explanation/what_enhancement_does`.
Args:
sigma_color: Intensity similarity weighting. Small values
(0.02--0.05) preserve subtle boundaries; medium values
(0.05--0.15) balance denoising and edge preservation; large
values (0.2--0.5) smooth aggressively. ``None`` (default)
auto-estimates from image statistics.
sigma_spatial: Spatial distance weighting in pixels. Small values
(1--5) apply local denoising; medium values (10--20) smooth
regionally; large values (30--50) smooth wide areas. Keep
below the minimum colony diameter. Default: 15.
win_size: Window size for filter computation. ``None`` (default)
auto-calculates from ``sigma_spatial``.
mode: Boundary handling. Accepted values: ``'constant'``,
``'edge'``, ``'symmetric'``, ``'reflect'``, ``'wrap'``.
Default: ``'constant'``.
cval: Fill value when ``mode='constant'``. Default: 0.
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`` smoothed by bilateral
filtering. ``rgb`` and ``gray`` are unchanged.
Best For:
- Noisy or grainy agar scans from high-ISO photography or old scanners.
- Plates with surface condensation, dust speckles, or uneven agar
texture.
- Preprocessing before thresholding when colony edges must remain
sharp.
- Low-quality captures where colony morphology must be preserved.
Consider Also:
- :class:`NonLocalMeansDenoiser` for stronger denoising of repetitive
textures at higher computational cost.
- :class:`BM3DDenoiser` for state-of-the-art structured noise removal.
- :class:`SubtractGaussian` when the primary problem is illumination
gradients rather than pixel-level noise.
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 edge-preserving
denoising strategies on low-light plate images.
"""
[docs]
def __init__(
self,
sigma_color: float | None = None,
sigma_spatial: float = 15,
*,
win_size: int | None = None,
mode: str = "constant",
cval: float = 0,
clip: bool = True,
):
"""
Parameters:
sigma_color (float | None): Standard deviation for grayvalue/color similarity.
Controls how permissive the filter is when averaging nearby pixels. Small
values (0.02–0.05 for float images) enforce strict color matching,
preserving edges but leaving more noise. Medium values (0.05–0.15)
provide balanced denoising and edge preservation—recommended for most
fungal colony imaging. Large values (0.2–0.5) aggressively average
across brightness ranges, risking boundary blur. If None (default),
automatically estimated from the standard deviation of the image.
For uint8 images (0–255), scale values proportionally: 0.05 float
corresponds roughly to 13 in uint8 scale. Recommended: leave as None
for automatic estimation, or set to 0.08–0.12 for typical colony plates.
sigma_spatial (float): Standard deviation for spatial distance in pixels.
Controls the extent of the neighborhood influencing each pixel. Small
values (1–5) apply highly local denoising, preserving fine texture.
Medium values (10–20) smooth regionally without over-smoothing—suitable
for general use. Large values (30–50) smooth broad areas, helpful for
correcting illumination variations but risking loss of small colonies
or merging of adjacent growth. Recommended: 15 for balanced results;
adjust based on colony size (keep smaller than minimum colony diameter).
win_size (int | None): Window size for bilateral filter computation. If None
(default), automatically calculated as max(5, 2 * ceil(3 * sigma_spatial) + 1).
Generally safe to leave as None; adjust only if you have specific
performance or memory constraints.
mode (str): How to handle image boundaries. Options: 'constant' (default,
pad with cval), 'edge' (replicate edge), 'symmetric', 'reflect', 'wrap'.
'constant' with cval=0 works well for agar plate backgrounds (black edges).
'reflect' mirrors edges, useful for non-border regions.
cval (float): Constant fill value for boundaries when mode='constant'. Default
is 0 (black), appropriate for agar backgrounds.
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 of transformed data.
"""
if sigma_spatial <= 0:
raise ValueError("sigma_spatial must be > 0")
if sigma_color is not None and sigma_color <= 0:
raise ValueError("sigma_color must be > 0 or None")
if mode not in ["constant", "edge", "symmetric", "reflect", "wrap"]:
raise ValueError(
f'mode must be one of "constant", "edge", "symmetric", "reflect", '
f'"wrap"; got {mode!r}'
)
self.sigma_color = sigma_color
self.sigma_spatial = float(sigma_spatial)
self.win_size = win_size
self.mode = mode
self.cval = cval
self.clip = clip
def _operate(self, image: Image) -> Image:
"""Apply bilateral denoising to reduce noise while preserving colony edges in the detection matrix channel."""
# denoise_bilateral may require a writable array, so create a copy
result = denoise_bilateral(
image=image.detect_mat[:].copy(),
sigma_color=self.sigma_color,
sigma_spatial=self.sigma_spatial,
win_size=self.win_size,
mode=self.mode,
cval=self.cval,
channel_axis=None,
)
if self.clip:
result = result.clip(0.0, 1.0)
image.detect_mat[:] = result
return image