raw: a non-finite black level no longer panics the AHD demosaic

A DNG whose BlackLevel is a DOUBLE beyond f32 reads as inf. The periodic
fill then computed (raw - inf) * 0 = NaN, and AHD's f32::clamp with NaN
bounds asserts. The fill now returns (0, 0) for a non-finite black, as
Scale::value does, and AHD uses a compare-and-select clamp that gives the
same result for every non-NaN bound.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
vangel76
2026-10-25 13:58:12 +01:00
parent b0622d20ca
commit 97a719a7a7
3 changed files with 37 additions and 4 deletions
+16 -2
View File
@@ -472,6 +472,14 @@ pub(crate) fn ahd_bands(h: usize) -> usize {
pub(crate) const AHD_TABLE_BYTES: u64 = (LABF_STEPS as u64 + 2) * 4;
/// `v` limited to `lo..=hi` like `f32::clamp`, without its panic on NaN bounds (hostile black
/// levels can put NaN in the plane): same result for every non-NaN bound.
#[inline(always)]
fn clampf(v: f32, lo: f32, hi: f32) -> f32 {
let v = if v < lo { lo } else { v };
if v > hi { hi } else { v }
}
fn ahd(p: &Padded, phase: [u8; 4], to_xyz: &Mat3, out: &mut [f32], jobs: usize) {
let (w, s) = (p.w, p.stride);
let d = &p.data;
@@ -500,10 +508,10 @@ fn ahd(p: &Padded, phase: [u8; 4], to_xyz: &Mat3, out: &mut [f32], jobs: usize)
}
let (l, r) = (d[i - 1], d[i + 1]);
let gh = (l + r) * 0.5 + (2.0 * c - d[i - 2] - d[i + 2]) * 0.25;
g[0][k] = gh.clamp(l.min(r), l.max(r));
g[0][k] = clampf(gh, l.min(r), l.max(r));
let (u, dn) = (d[i - s], d[i + s]);
let gv = (u + dn) * 0.5 + (2.0 * c - d[i - 2 * s] - d[i + 2 * s]) * 0.25;
g[1][k] = gv.clamp(u.min(dn), u.max(dn));
g[1][k] = clampf(gv, u.min(dn), u.max(dn));
}
}
// 2. RGB candidates and their Lab, band rows 1..n-1, columns 3..s-3.
@@ -600,6 +608,12 @@ mod tests {
}
}
#[test]
fn ahd_survives_nan_planes() {
let p = Padded::from_plane(&[f32::NAN; 64], 8, 8);
assert_eq!(demosaic(&p, [0, 1, 1, 2], Demosaic::Ahd, &IDENTITY, 1).len(), 192);
}
/// Mosaics an RGB image with the given phase.
fn mosaic(rgb: &[[f32; 3]], w: usize, h: usize, phase: [u8; 4]) -> Vec<f32> {
(0..w * h).map(|i| rgb[i][phase[((i / w) & 1) * 2 + ((i % w) & 1)] as usize]).collect()
+6 -2
View File
@@ -279,8 +279,12 @@ pub fn develop_sensor(s: &Sensor, opts: &DevelopOptions) -> Result<Developed> {
let x = c.x + p;
let black = bl.at(x.saturating_sub(s.active.x), y.saturating_sub(s.active.y), 0, 1);
let range = s.white[0] - black;
let gain = if range >= 1.0 { mult[usize::from(cfa.color(x, y)).min(2)] / range } else { 0.0 };
(black, gain)
// A non-finite black (a BlackLevel DOUBLE beyond f32) gives 0, as
// `Scale::value` does, instead of (raw - inf) * 0 = NaN.
if !(range >= 1.0 && black.is_finite()) {
return (0.0, 0.0);
}
(black, mult[usize::from(cfa.color(x, y)).min(2)] / range)
})
.collect();
let src = &s.data[y * s.width + c.x..y * s.width + c.x + w];
+15
View File
@@ -270,3 +270,18 @@ fn lossless_jpeg_frame_larger_than_the_budget_is_refused() {
let budget = Limits { max_alloc: 256 << 10, ..Limits::default() };
assert!(matches!(decode(&b, &budget), Err(RawError::LimitExceeded(_))));
}
#[test]
fn a_non_finite_black_level_develops_without_panicking() {
// A BlackLevel DOUBLE beyond f32 reads as inf: the planes must not turn into NaN and trip
// the AHD demosaic.
let (w, h) = (64, 48);
let mut sensor = decode(&DngSpec::cfa(w, h, vec![1000; w * h]).build(), &Limits::default()).unwrap();
for black in [f32::INFINITY, f32::NEG_INFINITY, f32::NAN] {
sensor.black = BlackLevels::uniform(black);
for demosaic in [Demosaic::Ahd, Demosaic::Bilinear, Demosaic::Mhc] {
let d = develop_sensor(&sensor, &DevelopOptions { demosaic, ..DevelopOptions::default() });
assert!(d.is_ok(), "{black} {demosaic:?}: {:?}", d.err());
}
}
}