Keep blown highlights grey in a panorama
A clipped photosite reaches the merge as (1, 1, 1), which the as-shot balance turns magenta. The preview balanced it with no highlight rule at all, so every blown cloud was pink on the alignment page. The DNG had the quieter form of the same fault: a frame's gain below one moved a blown sample off the white level, and a feather mixed it into a neighbour's real sky, so the develop's own desaturation no longer recognised it. The merge shader and the preview now write a blown sample, before the gain, as the camera value the composite's balance calls grey — the develop pipeline's neutral, fading in from CLIP_ONSET.
This commit is contained in:
+43
-1
@@ -560,6 +560,7 @@ fn run_inner(
|
||||
let black = first.raw.black_level[0];
|
||||
let white_level = u32::from(first.raw.white_level.saturating_sub(black)).max(1);
|
||||
let profile = dng_profile(first, white_level);
|
||||
let balance = profile.as_shot_neutral.map(|n| 1.0 / n.max(1e-3));
|
||||
let (_, mut carried) = crate::export::header_for_file(&first.meta);
|
||||
// The composite is dated at the middle of its sweep — the mean of the
|
||||
// frames' capture times — so it sorts among the frames it was made
|
||||
@@ -578,6 +579,7 @@ fn run_inner(
|
||||
feather: request.feather_px,
|
||||
chunk: request.chunk,
|
||||
sample_scale: white_level as f32,
|
||||
balance,
|
||||
};
|
||||
let merge_frames: Vec<MergeFrame> = frames
|
||||
.iter()
|
||||
@@ -619,6 +621,7 @@ fn run_inner(
|
||||
feather: request.feather_px,
|
||||
chunk: request.chunk,
|
||||
sample_scale: white_level as f32,
|
||||
balance,
|
||||
};
|
||||
let (qw, qh) = (qout.width() as usize, qout.height() as usize);
|
||||
let mut qrgb = vec![0.0f32; qw * qh * 3];
|
||||
@@ -987,8 +990,10 @@ fn preview_planes(
|
||||
continue;
|
||||
}
|
||||
let i = (y as usize * g.width + x as usize) * 3;
|
||||
let cam =
|
||||
grey_if_blown([colour[k][i], colour[k][i + 1], colour[k][i + 2]], look.wb);
|
||||
for c in 0..3 {
|
||||
sum[c] += colour[k][i + c] * gains[k];
|
||||
sum[c] += cam[c] * gains[k];
|
||||
}
|
||||
n += 1;
|
||||
}
|
||||
@@ -1133,6 +1138,27 @@ impl Look {
|
||||
}
|
||||
}
|
||||
|
||||
/// A blown camera sample as the grey `wb` balances it to; the merge
|
||||
/// shader's rule, for the preview.
|
||||
///
|
||||
/// A clipped photosite is (1, 1, 1), which balances to magenta. The develop
|
||||
/// pipeline pulls it back to the brightest balanced channel as it nears
|
||||
/// white (`CLIP_ONSET`); this does the same in camera space, before any
|
||||
/// gain moves it off the white level or an overlap averages it with a
|
||||
/// neighbour's real sky — after either, nothing downstream can tell it was
|
||||
/// blown and the clouds come out pink.
|
||||
fn grey_if_blown(cam: [f32; 3], wb: [f32; 3]) -> [f32; 3] {
|
||||
let peak = cam[0].max(cam[1]).max(cam[2]);
|
||||
let t = ((peak - dr_pipeline::CLIP_ONSET) / (1.0 - dr_pipeline::CLIP_ONSET)).clamp(0.0, 1.0);
|
||||
let clipped = t * t * (3.0 - 2.0 * t);
|
||||
if clipped <= 0.0 {
|
||||
return cam;
|
||||
}
|
||||
let wb = wb.map(|w| w.max(1e-3));
|
||||
let grey = (cam[0] * wb[0]).max(cam[1] * wb[1]).max(cam[2] * wb[2]);
|
||||
[0, 1, 2].map(|c| cam[c] + (grey / wb[c] - cam[c]) * clipped)
|
||||
}
|
||||
|
||||
fn invert3(m: &[f32; 9]) -> Option<[f32; 9]> {
|
||||
let (a, b, c, d, e, f, g, h, i) = (m[0], m[1], m[2], m[3], m[4], m[5], m[6], m[7], m[8]);
|
||||
let det = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g);
|
||||
@@ -1353,6 +1379,22 @@ fn dump_fill_input(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_blown_sample_stays_grey_under_a_gain_below_one() {
|
||||
// The 6D's as-shot balance: a clipped (1, 1, 1) balances to
|
||||
// (1.93, 1, 1.68), magenta. After the rule, balanced, every channel
|
||||
// is equal — and still equal once a gain of 0.9 has scaled it.
|
||||
let wb = [1.93, 1.0, 1.68];
|
||||
let cam = grey_if_blown([1.0, 1.0, 1.0], wb).map(|v| v * 0.9);
|
||||
let b = [cam[0] * wb[0], cam[1] * wb[1], cam[2] * wb[2]];
|
||||
assert!(
|
||||
(b[0] - b[1]).abs() < 1e-5 && (b[1] - b[2]).abs() < 1e-5,
|
||||
"{b:?}"
|
||||
);
|
||||
// And a sample well below white is left alone.
|
||||
assert_eq!(grey_if_blown([0.5, 0.8, 0.4], wb), [0.5, 0.8, 0.4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_worker_that_hangs_up_mid_job_is_reported_as_gone() {
|
||||
// The failure the page could not see: a panic drops the sender with
|
||||
|
||||
Reference in New Issue
Block a user