collective

Browse / github.com/storytold/photocraft / patch

Reduce Noise: reuse guide statistics with bit-identical output

Reduce Noise: reuse guide statistics with bit-identical output Reduce Noise computed the same moments twice when luminance guided itself, then recomputed the cleaned luminance's statistics for each chroma plane. Reusing them cuts box-filter channel work from 18 to 14 (22.2% fewer) with bit-identical output. Complements #6481, which speeds up the underlying blur. This PR is based on main and does not depend on it. ## Change (`crates/algo/src/denoise.rs`) - Self-guided luminance filters only the guide and its square. The source mean and guide/source product are the same two moments, so the duplicate channels are gone. - Both chroma planes share the clean

Status
open
Score
0.00
Adopters
0 other operators
Author
wanikhawar (imported from pull request #9639, open)
Base
59cb4c49bd236df04d8e37d4764253f29ca6600a
Size
+154 −27 in 1 file
Review
pending
Shared
2026-10-28
Id
f4094142ca49b79b61cb2d93

How it went

No other operator has reported using it yet.

Diff

raw · crates/algo/src/denoise.rs

diff --git a/crates/algo/src/denoise.rs b/crates/algo/src/denoise.rs--- a/crates/algo/src/denoise.rs+++ b/crates/algo/src/denoise.rs@@ -28,28 +28,57 @@ pub(crate) fn reach() -> f32 {     (2 * MAX_LUMA_R + 2 * MAX_CHROMA_R + 6) as f32 } -/// Guided filter of `p` with guide `g` (both `w × h`), box radius `r`.-fn guided(g: &[f32], p: &[f32], w: usize, h: usize, r: usize, eps: f32) -> Vec<f32> {-    let len = w * h;-    // Interleave the four box-filtered moments to run one pass.-    let mut m = vec![0.0f32; len * 4];-    for i in 0..len {-        m[i * 4] = g[i];-        m[i * 4 + 1] = p[i];-        m[i * 4 + 2] = g[i] * g[i];-        m[i * 4 + 3] = g[i] * p[i];-    }-    box_blur_n(&mut m, w, h, 4, r);-    let mut ab = vec![0.0f32; len * 2];-    for i in 0..len {-        let (mg, mp, mgg, mgp) = (m[i * 4], m[i * 4 + 1], m[i * 4 + 2], m[i * 4 + 3]);-        let var = (mgg - mg * mg).max(0.0);-        let a = (mgp - mg * mp) / (var + eps);-        ab[i * 2] = a;-        ab[i * 2 + 1] = mp - a * mg;+/// Shared first and second moments of a guide (two filtered channels).+fn guide_moments(g: &[f32], w: usize, h: usize, r: usize) -> Vec<f32> {+    let mut moments = Vec::with_capacity(g.len() * 2);+    for &v in g {+        moments.extend_from_slice(&[v, v * v]);+    }+    box_blur_n(&mut moments, w, h, 2, r);+    moments+}++/// Self-guided luminance: mean(p) = mean(g), mean(g*p) = mean(g*g).+fn guided_luma(g: &[f32], w: usize, h: usize, r: usize, eps: f32) -> Vec<f32> {+    let mut ab = guide_moments(g, w, h, r);+    for pair in ab.as_chunks_mut::<2>().0.iter_mut() {+        let mean = pair[0];+        let covariance = pair[1] - mean * mean;+        // Keep the original unclamped numerator, including roundoff below zero.+        let a = covariance / (covariance.max(0.0) + eps);+        pair[0] = a;+        pair[1] = mean - a * mean;     }     box_blur_n(&mut ab, w, h, 2, r);-    (0..len).map(|i| ab[i * 2] * g[i] + ab[i * 2 + 1]).collect()+    g.iter().zip(ab.as_chunks::<2>().0.iter()).map(|(&v, pair)| pair[0] * v + pair[1]).collect()+}++/// Chroma planes share the cleaned guide's mean and regularised variance.+/// Two guide channels plus two moments and two coefficients per chroma plane:+/// ten filtered channels, versus twelve for two independent guided filters.+fn guided_chroma(g: &[f32], planes: [&mut [f32]; 2], w: usize, h: usize, r: usize, eps: f32, amount: f32) {+    let mut stats = guide_moments(g, w, h, r);+    for pair in stats.as_chunks_mut::<2>().0.iter_mut() {+        pair[1] = (pair[1] - pair[0] * pair[0]).max(0.0) + eps;+    }+    for plane in planes {+        let mut ab = Vec::with_capacity(g.len() * 2);+        for (&guide, &p) in g.iter().zip(plane.iter()) {+            ab.extend_from_slice(&[p, guide * p]);+        }+        box_blur_n(&mut ab, w, h, 2, r);+        for (pair, guide) in ab.as_chunks_mut::<2>().0.iter_mut().zip(stats.as_chunks::<2>().0.iter()) {+            let mean = pair[0];+            let a = (pair[1] - guide[0] * mean) / guide[1];+            pair[0] = a;+            pair[1] = mean - a * guide[0];+        }+        box_blur_n(&mut ab, w, h, 2, r);+        for ((v, &guide), pair) in plane.iter_mut().zip(g).zip(ab.as_chunks::<2>().0.iter()) {+            let filtered = pair[0] * guide + pair[1];+            *v += (filtered - *v) * amount;+        }+    } }  pub(crate) fn reduce_noise(src: &Image, out: Rect, ctx: &Ctx, spec: &DenoiseSpec) -> Vec<f32> {@@ -125,7 +154,7 @@ pub(crate) fn reduce_noise(src: &Image, out: Rect, ctx: &Ctx, spec: &DenoiseSpec         let r = (1.0 + spec.strength.clamp(0.0, 10.0) * 0.4).round() as usize;         let keep = spec.preserve.clamp(0.0, 100.0) / 100.0;         let eps = (0.004 + 0.05 * s).powi(2) * (1.05 - keep);-        let d = guided(&yv, &yv, ww, wh, r.min(MAX_LUMA_R), eps);+        let d = guided_luma(&yv, ww, wh, r.min(MAX_LUMA_R), eps);         let k = s.sqrt();         for (v, dv) in yv.iter_mut().zip(d) {             *v += (dv - *v) * k;@@ -135,12 +164,7 @@ pub(crate) fn reduce_noise(src: &Image, out: Rect, ctx: &Ctx, spec: &DenoiseSpec     if col > 0.0 && model != Model::Gray {         let r = (1.0 + col * 8.0).round() as usize;         let eps = (0.01 + 0.05 * col).powi(2);-        for plane in [&mut c1, &mut c2] {-            let d = guided(&yv, plane, ww, wh, r.min(MAX_CHROMA_R), eps);-            for (v, dv) in plane.iter_mut().zip(d) {-                *v += (dv - *v) * col;-            }-        }+        guided_chroma(&yv, [&mut c1, &mut c2], ww, wh, r.min(MAX_CHROMA_R), eps, col);     }     // Sharpen Details: unsharp mask on luma.     if sh > 0.0 {@@ -178,3 +202,106 @@ pub(crate) fn reduce_noise(src: &Image, out: Rect, ctx: &Ctx, spec: &DenoiseSpec     }     res }++#[cfg(test)]+mod tests {+    use super::*;++    /// Guided filter of `p` with guide `g` (both `w × h`), box radius `r`.+    fn reference_guided(g: &[f32], p: &[f32], w: usize, h: usize, r: usize, eps: f32) -> Vec<f32> {+        let len = w * h;+        // Interleave the four box-filtered moments to run one pass.+        let mut m = vec![0.0f32; len * 4];+        for i in 0..len {+            m[i * 4] = g[i];+            m[i * 4 + 1] = p[i];+            m[i * 4 + 2] = g[i] * g[i];+            m[i * 4 + 3] = g[i] * p[i];+        }+        box_blur_n(&mut m, w, h, 4, r);+        let mut ab = vec![0.0f32; len * 2];+        for i in 0..len {+            let (mg, mp, mgg, mgp) = (m[i * 4], m[i * 4 + 1], m[i * 4 + 2], m[i * 4 + 3]);+            let var = (mgg - mg * mg).max(0.0);+            let a = (mgp - mg * mp) / (var + eps);+            ab[i * 2] = a;+            ab[i * 2 + 1] = mp - a * mg;+        }+        box_blur_n(&mut ab, w, h, 2, r);+        (0..len).map(|i| ab[i * 2] * g[i] + ab[i * 2 + 1]).collect()+    }++    #[test]+    fn shared_statistics_match_independent_filters_bit_for_bit() {+        for (w, h) in [(1, 1), (1, 19), (23, 1), (7, 5), (33, 17)] {+            let len = w * h;+            for flat in [false, true] {+                let g: Vec<f32> = (0..len).map(|i| if flat { 0.1 } else { ((i * 37 % 101) as f32 - 20.0) / 63.0 }).collect();+                let p: Vec<f32> = (0..len).map(|i| (i * 13 % 71) as f32 / 70.0 - 0.5).collect();+                let q: Vec<f32> = (0..len).map(|i| (i * 29 % 83) as f32 / 41.0 - 1.0).collect();+                for r in [0, 1, MAX_LUMA_R, MAX_CHROMA_R, 25] {+                    for eps in [0.000001, 0.0036] {+                        let expected = reference_guided(&g, &g, w, h, r, eps);+                        let actual = guided_luma(&g, w, h, r, eps);+                        assert_bits(&actual, &expected);+                        for amount in [0.01, 0.45, 1.0] {+                            let mut actual_p = p.clone();+                            let mut actual_q = q.clone();+                            guided_chroma(&g, [&mut actual_p, &mut actual_q], w, h, r, eps, amount);+                            for (original, actual) in [(&p, &actual_p), (&q, &actual_q)] {+                                let filtered = reference_guided(&g, original, w, h, r, eps);+                                let expected: Vec<f32> = original.iter().zip(filtered).map(|(&v, d)| v + (d - v) * amount).collect();+                                assert_bits(actual, &expected);+                            }+                        }+                    }+                }+            }+        }+    }++    #[test]+    fn reduce_noise_guides_both_chroma_planes_by_the_cleaned_luma() {+        // Caller wiring: Lab keeps its planes as they are, so each output plane is compared with+        // the independent filters run on the planes in order (luma by itself, chroma by the+        // cleaned luma).+        let (w, h) = (19, 13);+        let rect = Rect::new(0, 0, w as i32, h as i32);+        let mut src = Image::new(rect, 3);+        for (i, px) in src.data.as_chunks_mut::<3>().0.iter_mut().enumerate() {+            px[0] = (i * 37 % 101) as f32 / 100.0;+            px[1] = (i * 13 % 71) as f32 / 70.0 - 0.5;+            px[2] = (i * 29 % 83) as f32 / 41.0 - 1.0;+        }+        let ctx = Ctx { bounds: rect, mode: ColorMode::Lab, alpha: false };+        let spec = DenoiseSpec { strength: 6.0, preserve: 30.0, color: 50.0, sharpen: 0.0, jpeg: false };+        let out = reduce_noise(&src, rect, &ctx, &spec);++        let plane = |c: usize| -> Vec<f32> { src.data.as_chunks::<3>().0.iter().map(|px| px[c]).collect() };+        let (luma, chroma) = (plane(0), [plane(1), plane(2)]);+        let s = spec.strength / 10.0;+        let eps = (0.004 + 0.05 * s).powi(2) * (1.05 - spec.preserve / 100.0);+        let smooth = reference_guided(&luma, &luma, w, h, (1.0 + spec.strength * 0.4).round() as usize, eps);+        let cleaned: Vec<f32> = luma.iter().zip(&smooth).map(|(&v, &d)| v + (d - v) * s.sqrt()).collect();+        let col = spec.color / 100.0;+        let chroma_r = (1.0 + col * 8.0).round() as usize;+        let chroma_eps = (0.01 + 0.05 * col).powi(2);+        let mut expected = vec![cleaned];+        for p in &chroma {+            let filtered = reference_guided(&expected[0], p, w, h, chroma_r, chroma_eps);+            expected.push(p.iter().zip(filtered).map(|(&v, f)| v + (f - v) * col).collect());+        }+        for (c, expected) in expected.iter().enumerate() {+            let actual: Vec<f32> = out.as_chunks::<3>().0.iter().map(|px| px[c]).collect();+            assert_bits(&actual, expected);+            assert!(actual.iter().zip(&plane(c)).any(|(a, o)| (a - o).abs() > 1e-3), "plane {c} was filtered");+        }+    }++    fn assert_bits(actual: &[f32], expected: &[f32]) {+        assert_eq!(actual.len(), expected.len());+        for (i, (actual, expected)) in actual.iter().zip(expected).enumerate() {+            assert_eq!(actual.to_bits(), expected.to_bits(), "pixel {i}: {actual} != {expected}");+        }+    }+}