M5: highlight reconstruction starts at half resolution (2-2.5x faster, identical output)

A clipped pixel is never valid at full resolution, so its diffused chromaticity was always the
bilinear sample of the half-resolution pyramid level; the full-resolution chroma field (288 MB
at 24 MP) and its fill pass were wasted work. The pyramid now starts at half resolution straight
from the image. Bit-identical to the previous implementation (kept as a test reference over odd
sizes and clipping densities). 24 MP: 319 -> 145 ms isolated; real CR2/ARW loads 0.8-1.1 s ->
0.3-0.5 s under load. Runs on every full-resolution raw load (1:1 loupe, export).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-01 22:35:18 -04:00
parent 8a83f8c6ce
commit c812352940
1 changed files with 283 additions and 73 deletions

View File

@ -31,26 +31,7 @@ fn fill_invalid(w: usize, h: usize, values: &mut [[f32; 3]], valid: &mut [bool])
let mut cval = vec![false; cw * ch];
{
let (values, valid) = (&*values, &*valid);
cv.par_chunks_mut(cw).zip(cval.par_chunks_mut(cw)).enumerate().for_each(|(y, (vrow, okrow))| {
for x in 0..cw {
let (mut s, mut n) = ([0f32; 3], 0);
for dy in 0..2 {
for dx in 0..2 {
let (sx, sy) = (2 * x + dx, 2 * y + dy);
if sx < w && sy < h && valid[sy * w + sx] {
for c in 0..3 {
s[c] += values[sy * w + sx][c];
}
n += 1;
}
}
}
if n > 0 {
vrow[x] = s.map(|v| v / n as f32);
okrow[x] = true;
}
}
});
downsample(w, h, &mut cv, &mut cval, |i| valid[i].then(|| values[i]));
}
if cw * ch < w * h {
fill_invalid(cw, ch, &mut cv, &mut cval);
@ -61,81 +42,129 @@ fn fill_invalid(w: usize, h: usize, values: &mut [[f32; 3]], valid: &mut [bool])
values.par_chunks_mut(w).zip(valid.par_chunks_mut(w)).enumerate().for_each(|(y, (vrow, okrow))| {
for x in 0..w {
if !okrow[x] {
// bilinear upsample of the coarse level
let fx = ((x as f32 + 0.5) / 2.0 - 0.5).clamp(0.0, (cw - 1) as f32);
let fy = ((y as f32 + 0.5) / 2.0 - 0.5).clamp(0.0, (ch - 1) as f32);
let (x0, y0) = (fx.floor() as usize, fy.floor() as usize);
let (x1, y1) = ((x0 + 1).min(cw - 1), (y0 + 1).min(ch - 1));
let (tx, ty) = (fx - x0 as f32, fy - y0 as f32);
let g = |xx: usize, yy: usize| cv[yy * cw + xx];
let mut v = [0f32; 3];
for c in 0..3 {
let top = g(x0, y0)[c] + (g(x1, y0)[c] - g(x0, y0)[c]) * tx;
let bot = g(x0, y1)[c] + (g(x1, y1)[c] - g(x0, y1)[c]) * tx;
v[c] = top + (bot - top) * ty;
}
vrow[x] = v;
vrow[x] = upsample(&cv, cw, ch, x, y);
okrow[x] = true;
}
}
});
}
/// One pyramid step: each `2 × 2` block of a `w × h` level becomes the mean of its valid
/// entries (`at(i)`: the value at index `i` when valid) in `cv`, `cval` (`⌈w/2⌉ × ⌈h/2⌉`).
fn downsample(w: usize, h: usize, cv: &mut [[f32; 3]], cval: &mut [bool], at: impl Fn(usize) -> Option<[f32; 3]> + Sync) {
let cw = w.div_ceil(2);
cv.par_chunks_mut(cw).zip(cval.par_chunks_mut(cw)).enumerate().for_each(|(y, (vrow, okrow))| {
for x in 0..cw {
let (mut s, mut n) = ([0f32; 3], 0);
for dy in 0..2 {
for dx in 0..2 {
let (sx, sy) = (2 * x + dx, 2 * y + dy);
if sx < w
&& sy < h
&& let Some(v) = at(sy * w + sx)
{
for c in 0..3 {
s[c] += v[c];
}
n += 1;
}
}
}
if n > 0 {
vrow[x] = s.map(|v| v / n as f32);
okrow[x] = true;
}
}
});
}
/// Bilinear sample of the `cw × ch` coarse level at fine pixel `(x, y)` (twice the resolution).
fn upsample(cv: &[[f32; 3]], cw: usize, ch: usize, x: usize, y: usize) -> [f32; 3] {
let fx = ((x as f32 + 0.5) / 2.0 - 0.5).clamp(0.0, (cw - 1) as f32);
let fy = ((y as f32 + 0.5) / 2.0 - 0.5).clamp(0.0, (ch - 1) as f32);
let (x0, y0) = (fx.floor() as usize, fy.floor() as usize);
let (x1, y1) = ((x0 + 1).min(cw - 1), (y0 + 1).min(ch - 1));
let (tx, ty) = (fx - x0 as f32, fy - y0 as f32);
let g = |xx: usize, yy: usize| cv[yy * cw + xx];
let mut v = [0f32; 3];
for c in 0..3 {
let top = g(x0, y0)[c] + (g(x1, y0)[c] - g(x0, y0)[c]) * tx;
let bot = g(x0, y1)[c] + (g(x1, y1)[c] - g(x0, y1)[c]) * tx;
v[c] = top + (bot - top) * ty;
}
v
}
/// White-balanced chromaticity of an unclipped pixel (`None`: clipped or too dark to tell).
#[inline]
fn chroma_of(p: &[f32; 3], wb: [f32; 3], clip: f32) -> Option<[f32; 3]> {
if p[0] >= clip || p[1] >= clip || p[2] >= clip {
return None;
}
let q = [p[0] * wb[0], p[1] * wb[1], p[2] * wb[2]];
let s = q[0] + q[1] + q[2];
(s > 1e-4).then(|| q.map(|v| v.max(0.0) / s))
}
/// Reconstruct partially clipped channels. `clip` is the sensor clip level in normalised units (use slightly
/// below 1.0, e.g. 0.99), `wb` the white-balance multipliers that will be applied afterwards.
/// Returns the number of pixels that had at least one clipped channel.
///
/// The chromaticity field is diffused coarse to fine from the unclipped pixels. A clipped pixel is
/// never valid at full resolution, so its chromaticity is always the bilinear sample of the
/// half-resolution level: we start the pyramid there, straight from the image, and never build
/// the full-resolution field (same result, a quarter of the memory and far less work).
pub fn reconstruct(img: &mut Rgb32f, wb: [f32; 3], clip: f32) -> usize {
let (w, h) = (img.width, img.height);
let n = w * h;
let is_clipped = |p: &[f32; 3]| p[0] >= clip || p[1] >= clip || p[2] >= clip;
let count = img.data.par_iter().filter(|p| is_clipped(p)).count();
if count == 0 {
return 0;
}
// chromaticity (white-balanced ratios to the channel mean) of unclipped pixels
let mut chroma = vec![[1f32 / 3.0; 3]; n];
let mut valid = vec![false; n];
chroma.par_iter_mut().zip(valid.par_iter_mut()).zip(img.data.par_iter()).for_each(|((r, v), p)| {
if is_clipped(p) {
return;
}
let q = [p[0] * wb[0], p[1] * wb[1], p[2] * wb[2]];
let s = q[0] + q[1] + q[2];
if s > 1e-4 {
*r = q.map(|v| v.max(0.0) / s);
*v = true;
}
});
fill_invalid(w, h, &mut chroma, &mut valid);
// chromaticity (white-balanced ratios to the channel sum) of unclipped pixels, at half resolution
let (cw, ch) = (w.div_ceil(2), h.div_ceil(2));
let mut cv = vec![[1f32 / 3.0; 3]; cw * ch];
let mut cval = vec![false; cw * ch];
{
let data = &img.data;
downsample(w, h, &mut cv, &mut cval, |i| chroma_of(&data[i], wb, clip));
}
if cw * ch < w * h {
fill_invalid(cw, ch, &mut cv, &mut cval);
}
let any_valid = cw * ch < w * h && cval.iter().any(|&v| v);
let max_level = wb.iter().cloned().fold(0.0f32, f32::max) * clip;
img.data.par_iter_mut().zip(chroma.par_iter()).for_each(|(px, r)| {
let p = *px;
let cl = [p[0] >= clip, p[1] >= clip, p[2] >= clip];
if !cl.iter().any(|&b| b) {
return;
}
let q = [p[0] * wb[0], p[1] * wb[1], p[2] * wb[2]];
// estimate the white-balanced channel sum from the unclipped channels
let (mut sum, mut k) = (0f32, 0usize);
for c in 0..3 {
if !cl[c] && r[c] > 1e-3 {
sum += q[c] / r[c];
k += 1;
img.data.par_chunks_mut(w).enumerate().for_each(|(y, row)| {
for (x, px) in row.iter_mut().enumerate() {
let p = *px;
let cl = [p[0] >= clip, p[1] >= clip, p[2] >= clip];
if !cl.iter().any(|&b| b) {
continue;
}
}
let mut out = q;
if k == 0 {
let v = q.iter().cloned().fold(0.0f32, f32::max).max(max_level);
out = [v; 3];
} else {
let sum = sum / k as f32;
let r = if any_valid { upsample(&cv, cw, ch, x, y) } else { [1f32 / 3.0; 3] };
let q = [p[0] * wb[0], p[1] * wb[1], p[2] * wb[2]];
// estimate the white-balanced channel sum from the unclipped channels
let (mut sum, mut k) = (0f32, 0usize);
for c in 0..3 {
if cl[c] {
out[c] = q[c].max(sum * r[c]);
if !cl[c] && r[c] > 1e-3 {
sum += q[c] / r[c];
k += 1;
}
}
let mut out = q;
if k == 0 {
let v = q.iter().cloned().fold(0.0f32, f32::max).max(max_level);
out = [v; 3];
} else {
let sum = sum / k as f32;
for c in 0..3 {
if cl[c] {
out[c] = q[c].max(sum * r[c]);
}
}
}
*px = [out[0] / wb[0], out[1] / wb[1], out[2] / wb[2]];
}
*px = [out[0] / wb[0], out[1] / wb[1], out[2] / wb[2]];
});
count
}
@ -144,6 +173,187 @@ pub fn reconstruct(img: &mut Rgb32f, wb: [f32; 3], clip: f32) -> usize {
mod tests {
use super::*;
// The previous full-resolution implementation, kept as the reference for `reconstruct`.
/// Fill `values` (per-pixel vectors) where `valid` is false from valid neighbours, coarse to fine.
fn fill_invalid_ref(w: usize, h: usize, values: &mut [[f32; 3]], valid: &mut [bool]) {
if w == 0 || h == 0 || valid.iter().all(|&v| v) || !valid.iter().any(|&v| v) {
return;
}
// Build a pyramid of weighted means, fill each level from the next coarser one.
let (cw, ch) = (w.div_ceil(2), h.div_ceil(2));
let mut cv = vec![[0f32; 3]; cw * ch];
let mut cval = vec![false; cw * ch];
{
let (values, valid) = (&*values, &*valid);
cv.par_chunks_mut(cw).zip(cval.par_chunks_mut(cw)).enumerate().for_each(|(y, (vrow, okrow))| {
for x in 0..cw {
let (mut s, mut n) = ([0f32; 3], 0);
for dy in 0..2 {
for dx in 0..2 {
let (sx, sy) = (2 * x + dx, 2 * y + dy);
if sx < w && sy < h && valid[sy * w + sx] {
for c in 0..3 {
s[c] += values[sy * w + sx][c];
}
n += 1;
}
}
}
if n > 0 {
vrow[x] = s.map(|v| v / n as f32);
okrow[x] = true;
}
}
});
}
if cw * ch < w * h {
fill_invalid_ref(cw, ch, &mut cv, &mut cval);
} else {
// cannot shrink further (1×1): nothing valid anywhere handled above
return;
}
values.par_chunks_mut(w).zip(valid.par_chunks_mut(w)).enumerate().for_each(|(y, (vrow, okrow))| {
for x in 0..w {
if !okrow[x] {
// bilinear upsample of the coarse level
let fx = ((x as f32 + 0.5) / 2.0 - 0.5).clamp(0.0, (cw - 1) as f32);
let fy = ((y as f32 + 0.5) / 2.0 - 0.5).clamp(0.0, (ch - 1) as f32);
let (x0, y0) = (fx.floor() as usize, fy.floor() as usize);
let (x1, y1) = ((x0 + 1).min(cw - 1), (y0 + 1).min(ch - 1));
let (tx, ty) = (fx - x0 as f32, fy - y0 as f32);
let g = |xx: usize, yy: usize| cv[yy * cw + xx];
let mut v = [0f32; 3];
for c in 0..3 {
let top = g(x0, y0)[c] + (g(x1, y0)[c] - g(x0, y0)[c]) * tx;
let bot = g(x0, y1)[c] + (g(x1, y1)[c] - g(x0, y1)[c]) * tx;
v[c] = top + (bot - top) * ty;
}
vrow[x] = v;
okrow[x] = true;
}
}
});
}
/// Reconstruct partially clipped channels. `clip` is the sensor clip level in normalised units (use slightly
/// below 1.0, e.g. 0.99), `wb` the white-balance multipliers that will be applied afterwards.
/// Returns the number of pixels that had at least one clipped channel.
fn reconstruct_ref(img: &mut Rgb32f, wb: [f32; 3], clip: f32) -> usize {
let (w, h) = (img.width, img.height);
let n = w * h;
let is_clipped = |p: &[f32; 3]| p[0] >= clip || p[1] >= clip || p[2] >= clip;
let count = img.data.par_iter().filter(|p| is_clipped(p)).count();
if count == 0 {
return 0;
}
// chromaticity (white-balanced ratios to the channel mean) of unclipped pixels
let mut chroma = vec![[1f32 / 3.0; 3]; n];
let mut valid = vec![false; n];
chroma.par_iter_mut().zip(valid.par_iter_mut()).zip(img.data.par_iter()).for_each(|((r, v), p)| {
if is_clipped(p) {
return;
}
let q = [p[0] * wb[0], p[1] * wb[1], p[2] * wb[2]];
let s = q[0] + q[1] + q[2];
if s > 1e-4 {
*r = q.map(|v| v.max(0.0) / s);
*v = true;
}
});
fill_invalid_ref(w, h, &mut chroma, &mut valid);
let max_level = wb.iter().cloned().fold(0.0f32, f32::max) * clip;
img.data.par_iter_mut().zip(chroma.par_iter()).for_each(|(px, r)| {
let p = *px;
let cl = [p[0] >= clip, p[1] >= clip, p[2] >= clip];
if !cl.iter().any(|&b| b) {
return;
}
let q = [p[0] * wb[0], p[1] * wb[1], p[2] * wb[2]];
// estimate the white-balanced channel sum from the unclipped channels
let (mut sum, mut k) = (0f32, 0usize);
for c in 0..3 {
if !cl[c] && r[c] > 1e-3 {
sum += q[c] / r[c];
k += 1;
}
}
let mut out = q;
if k == 0 {
let v = q.iter().cloned().fold(0.0f32, f32::max).max(max_level);
out = [v; 3];
} else {
let sum = sum / k as f32;
for c in 0..3 {
if cl[c] {
out[c] = q[c].max(sum * r[c]);
}
}
}
*px = [out[0] / wb[0], out[1] / wb[1], out[2] / wb[2]];
});
count
}
/// `cargo test --release -p lightcraft-raw --lib -- --ignored bench_reconstruct --nocapture`
#[test]
#[ignore]
fn bench_reconstruct() {
let img = Rgb32f::from_fn(6000, 4000, |x, y| {
let v = 0.4 + 0.8 * ((x as f32 * 0.003).sin() * (y as f32 * 0.002).cos()).abs();
[v.min(1.0), (v * 0.7).min(1.0), (v * 0.5).min(1.0)]
});
let wb = [2.0, 1.0, 1.5];
let time = |f: &dyn Fn(&mut Rgb32f) -> usize| {
(0..3)
.map(|_| {
let mut i = img.clone();
let t = std::time::Instant::now();
f(&mut i);
t.elapsed().as_secs_f64() * 1e3
})
.fold(f64::MAX, f64::min)
};
let new = time(&|i| reconstruct(i, wb, 0.99));
let old = time(&|i| reconstruct_ref(i, wb, 0.99));
eprintln!("reconstruct 24 MP: {new:.0} ms (full-resolution reference: {old:.0} ms)");
}
/// The half-resolution start gives exactly the full-resolution algorithm's result.
#[test]
fn matches_the_full_resolution_reference() {
let mut seed = 0x2545_f491_4f6c_dd1du64;
let mut rnd = move || {
seed ^= seed << 13;
seed ^= seed >> 7;
seed ^= seed << 17;
(seed >> 40) as f32 / (1u64 << 24) as f32
};
for (w, h) in [(1, 1), (1, 2), (3, 1), (7, 5), (64, 48), (129, 77)] {
for density in [0.02f32, 0.3, 0.97] {
// smooth colour with clipped blobs, some dark pixels, some fully clipped ones
let noise: Vec<f32> = (0..w * h).map(|_| rnd()).collect();
let img = Rgb32f::from_fn(w, h, |x, y| {
let t = ((x as f32 * 0.21).sin() + (y as f32 * 0.17).cos()) * 0.5;
let base = [0.5 + 0.4 * t, 0.45 - 0.2 * t, 0.3 + 0.1 * t];
let r = noise[y * w + x];
if r < density * 0.3 {
[1.0, 1.0, 1.0]
} else if r < density {
[1.0, base[1] * 1.6, base[2]]
} else if r > 0.995 {
[0.0, 0.0, 0.00001]
} else {
base
}
});
let wb = [2.1, 1.0, 1.6];
let (mut a, mut b) = (img.clone(), img.clone());
assert_eq!(reconstruct(&mut a, wb, 0.99), reconstruct_ref(&mut b, wb, 0.99));
assert!(a.data == b.data, "{w}×{h} at {density}");
}
}
}
#[test]
fn clip_neutral_makes_clipped_white_neutral() {
let wb = [2.0, 1.0, 1.5];