From 89424084b222e7f9260f7a348314d0428d0fedb4 Mon Sep 17 00:00:00 2001 From: strebdom Date: Fri, 23 Oct 2026 15:25:55 +0200 Subject: [PATCH] LR-IMP-FORMATS: AHD's blend leaves out a candidate that isn't finite (#2698 review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The outright pick never let the losing candidate's NaN (beside a float raw's NaN sample) into the output; the blend did, as 0 · NaN. A candidate taken alone is now copied, and one that isn't finite is left out while the other is. A NaN sample reaches as many pixels as before the blend (9 in the new test; the blend made it 33). --- crates/raw/src/demosaic/ahd.rs | 11 ++++++++++- crates/raw/src/demosaic/mod.rs | 14 ++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/crates/raw/src/demosaic/ahd.rs b/crates/raw/src/demosaic/ahd.rs index 622a19ce9..c4180de4d 100644 --- a/crates/raw/src/demosaic/ahd.rs +++ b/crates/raw/src/demosaic/ahd.rs @@ -159,7 +159,16 @@ fn tile(m: &Mosaic, x0: usize, y0: usize, tw: usize, th: usize) -> Vec<[f32; 3]> sv += hv[j] as u32; } let wh = (0.5 + (sh as f32 - sv as f32) / (2.0 * BLEND)).clamp(0.0, 1.0); - out.push([0, 1, 2].map(|c| rh[i][c] * wh + rv[i][c] * (1.0 - wh))); + // a candidate taken alone is copied, and one that isn't finite (beside a float raw's NaN sample) is left out, + // so the other's value doesn't become 0 · NaN + let finite = |p: &[f32; 3]| p.iter().all(|v| v.is_finite()); + out.push(if wh >= 1.0 || !finite(&rv[i]) { + rh[i] + } else if wh <= 0.0 || !finite(&rh[i]) { + rv[i] + } else { + [0, 1, 2].map(|c| rh[i][c] * wh + rv[i][c] * (1.0 - wh)) + }); } out } diff --git a/crates/raw/src/demosaic/mod.rs b/crates/raw/src/demosaic/mod.rs index b99f7b015..07d45b6de 100644 --- a/crates/raw/src/demosaic/mod.rs +++ b/crates/raw/src/demosaic/mod.rs @@ -290,6 +290,20 @@ mod tests { assert_eq!(demosaic(&n, Method::Ahd).data, vec![[0.1, 0.2, 0.3]]); } + #[test] + fn a_nan_sample_spreads_no_further_than_the_chosen_candidate_takes_it() { + // vertical stripes: the vertical candidate wins outright beside them, and the horizontal one, which reads two + // samples to each side, is the only one that sees a NaN two columns away + let img = Rgb32f::from_fn(32, 24, |x, _| if (x / 2) % 2 == 0 { [0.2, 0.3, 0.25] } else { [0.7, 0.8, 0.6] }); + let cfa = Cfa::bayer("RGGB").unwrap(); + let mut n = mosaic_from_rgb(&img, &cfa); + n.data[12 * 32 + 16] = f32::NAN; + let out = demosaic(&n, Method::Ahd); + let bad = out.data.iter().filter(|p| p.iter().any(|v| !v.is_finite())).count(); + // the pixels that read the NaN sample themselves, as many as the outright pick gave; the blend made it 33 + assert!(bad <= 9, "{bad} pixels not finite"); + } + #[test] fn non_finite_samples_do_not_panic() { // regression: NaN samples (a float DNG) gave AHD and PPG NaN bounds for `f32::clamp`, which panics