Skip to main content

codec/filter/denoise/
mod.rs

1//! `denoise` — spatial denoise with a **selectable algorithm** ([`DenoiseMethod`])
2//! and a `strength` (`0.0..=1.0`) that blends the filtered result back with the
3//! source. Each method lives in its own file; they share the dispatch + blend
4//! here. 8-bit `Yuv420p` only (luma + chroma).
5//!
6//! `strength` is a uniform "how much" dial: every method runs at a fixed,
7//! moderate internal setting and the output is `src·(1−s) + filtered·s`, so the
8//! same number means the same amount of denoising regardless of algorithm.
9
10use std::fmt;
11
12use anyhow::Result;
13
14use super::{assemble, planes_8bit};
15use crate::frame::VideoFrame;
16
17mod anisotropic;
18mod bilateral;
19mod gaussian;
20mod mean;
21mod median;
22mod nlmeans;
23
24/// Which spatial denoise algorithm [`super::VideoFilter::Denoise`] runs. Each
25/// suits a different kind of noise; `strength` then blends the result with the
26/// source. (Temporal denoisers — hqdn3d / NLM-temporal — need frame history and
27/// don't fit this stateless per-frame filter; a future extension.)
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
31pub enum DenoiseMethod {
32    /// Edge-preserving [**bilateral**](bilateral) filter (5×5): smooths flat /
33    /// sensor noise while keeping edges sharp. The general-purpose default.
34    #[default]
35    Bilateral,
36    /// [**Gaussian**](gaussian) low-pass blur (separable 5×5): smooths
37    /// everything, so it softens fine detail along with the noise.
38    Gaussian,
39    /// [**Median**](median) filter (3×3): best for salt-and-pepper / impulse
40    /// noise; also edge-preserving.
41    Median,
42    /// [**Mean**](mean) (box) blur over a 3×3 window — the cheapest smoother;
43    /// blurs noise and detail equally.
44    Mean,
45    /// [**Non-local means**](nlmeans): averages samples weighted by how similar
46    /// their surrounding patch is, so repeating texture denoises without
47    /// blurring. Highest classical quality — and by far the slowest.
48    Nlmeans,
49    /// [**Anisotropic diffusion**](anisotropic) (Perona–Malik): gradient-gated
50    /// diffusion — smooths flat regions but stops at edges. Edge-preserving like
51    /// bilateral, different character.
52    Anisotropic,
53}
54
55impl fmt::Display for DenoiseMethod {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        f.write_str(match self {
58            DenoiseMethod::Bilateral => "bilateral",
59            DenoiseMethod::Gaussian => "gaussian",
60            DenoiseMethod::Median => "median",
61            DenoiseMethod::Mean => "mean",
62            DenoiseMethod::Nlmeans => "nlmeans",
63            DenoiseMethod::Anisotropic => "anisotropic",
64        })
65    }
66}
67
68/// Serde default for [`super::VideoFilter::Denoise::strength`].
69#[cfg(feature = "serde")]
70pub(super) fn default_denoise_strength() -> f32 {
71    0.5
72}
73
74/// Denoise luma + chroma with `method`, blending by `strength`.
75pub(super) fn apply(frame: &VideoFrame, method: DenoiseMethod, strength: f32) -> Result<VideoFrame> {
76    let (yp, up, vp) = planes_8bit(frame, "denoise")?;
77    let s = strength.clamp(0.0, 1.0);
78    let (w, h) = (frame.width as usize, frame.height as usize);
79    let (cw, ch) = (w / 2, h / 2);
80    Ok(assemble(
81        frame,
82        frame.width,
83        frame.height,
84        plane(method, &yp, w, h, s),
85        plane(method, &up, cw, ch, s),
86        plane(method, &vp, cw, ch, s),
87    ))
88}
89
90/// Denoise one 8-bit plane with `method`, then blend the filtered plane back
91/// with the source by `strength` (`0` ⇒ source, `1` ⇒ fully filtered). `strength
92/// == 0` and degenerate sizes short-circuit to a copy.
93fn plane(method: DenoiseMethod, src: &[u8], w: usize, h: usize, strength: f32) -> Vec<u8> {
94    if w == 0 || h == 0 || strength <= 0.0 {
95        return src.to_vec();
96    }
97    let filtered = match method {
98        DenoiseMethod::Bilateral => bilateral::plane(src, w, h),
99        DenoiseMethod::Gaussian => gaussian::plane(src, w, h),
100        DenoiseMethod::Median => median::plane(src, w, h),
101        DenoiseMethod::Mean => mean::plane(src, w, h),
102        DenoiseMethod::Nlmeans => nlmeans::plane(src, w, h),
103        DenoiseMethod::Anisotropic => anisotropic::plane(src, w, h),
104    };
105    if strength >= 1.0 {
106        return filtered;
107    }
108    let inv = 1.0 - strength;
109    src.iter()
110        .zip(&filtered)
111        .map(|(&s, &f)| (s as f32 * inv + f as f32 * strength).round().clamp(0.0, 255.0) as u8)
112        .collect()
113}
114
115/// Clamp `v` to `0..hi` (edge-replicate border addressing). Shared by the
116/// method kernels that use a clamped window.
117pub(super) fn clamp_idx(v: isize, hi: usize) -> usize {
118    v.clamp(0, hi as isize - 1) as usize
119}