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:
@@ -73,6 +73,10 @@ pub struct MergeOutput {
|
|||||||
pub chunk: (u32, u32),
|
pub chunk: (u32, u32),
|
||||||
/// Multiplies a normalised sample (1.0 = white) to the sensor's scale.
|
/// Multiplies a normalised sample (1.0 = white) to the sensor's scale.
|
||||||
pub sample_scale: f32,
|
pub sample_scale: f32,
|
||||||
|
/// The white balance the composite will be developed with — the
|
||||||
|
/// inverse of its `AsShotNeutral` — so that a blown sample can be
|
||||||
|
/// written as the camera value that balance calls grey.
|
||||||
|
pub balance: [f32; 3],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MergeOutput {
|
impl MergeOutput {
|
||||||
@@ -109,7 +113,8 @@ struct WarpParams {
|
|||||||
tile_origin: [f32; 2],
|
tile_origin: [f32; 2],
|
||||||
tile_size: [u32; 2],
|
tile_size: [u32; 2],
|
||||||
feather: f32,
|
feather: f32,
|
||||||
_pad: f32,
|
clip_onset: f32,
|
||||||
|
balance: [f32; 4],
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
@@ -334,7 +339,13 @@ impl MergePass {
|
|||||||
tile_origin: [rect.0 as f32, rect.1 as f32],
|
tile_origin: [rect.0 as f32, rect.1 as f32],
|
||||||
tile_size: [rect.2, rect.3],
|
tile_size: [rect.2, rect.3],
|
||||||
feather: output.feather,
|
feather: output.feather,
|
||||||
_pad: 0.0,
|
clip_onset: dr_pipeline::CLIP_ONSET,
|
||||||
|
balance: [
|
||||||
|
output.balance[0].max(1e-3),
|
||||||
|
output.balance[1].max(1e-3),
|
||||||
|
output.balance[2].max(1e-3),
|
||||||
|
0.0,
|
||||||
|
],
|
||||||
};
|
};
|
||||||
self.accumulate(¶ms, tile);
|
self.accumulate(¶ms, tile);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,10 @@ struct Params {
|
|||||||
tile_size: vec2<u32>,
|
tile_size: vec2<u32>,
|
||||||
// Pixels over which the weight ramps from the edge to full.
|
// Pixels over which the weight ramps from the edge to full.
|
||||||
feather: f32,
|
feather: f32,
|
||||||
_pad: f32,
|
// Where a sample starts to count as blown (`CLIP_ONSET`), and the
|
||||||
|
// white balance the composite will be developed with.
|
||||||
|
clip_onset: f32,
|
||||||
|
balance: vec4<f32>,
|
||||||
};
|
};
|
||||||
|
|
||||||
@group(0) @binding(0) var<uniform> p: Params;
|
@group(0) @binding(0) var<uniform> p: Params;
|
||||||
@@ -125,7 +128,19 @@ fn warp(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Colour is the alpha-weighted mean of the texels that exist.
|
// Colour is the alpha-weighted mean of the texels that exist.
|
||||||
let rgb = s.rgb / s.a * p.gain;
|
let cam = s.rgb / s.a;
|
||||||
|
// **A blown sample is written as grey, before the gain.** A clipped
|
||||||
|
// photosite arrives as (1, 1, 1), which is not a colour: balanced, it
|
||||||
|
// is magenta, and the develop's highlight desaturation only rescues it
|
||||||
|
// while it is still at the white level. A gain below one moved it off
|
||||||
|
// that level, and a feather mixed it into a neighbour's real sky, so
|
||||||
|
// the composite's blown clouds came out pink. Written instead as the
|
||||||
|
// camera value the balance maps to grey — the develop pipeline's own
|
||||||
|
// neutral, the brightest balanced channel — it survives both.
|
||||||
|
let clipped = smoothstep(p.clip_onset, 1.0, max(cam.r, max(cam.g, cam.b)));
|
||||||
|
let balanced = cam * p.balance.rgb;
|
||||||
|
let grey = vec3<f32>(max(balanced.r, max(balanced.g, balanced.b))) / p.balance.rgb;
|
||||||
|
let rgb = mix(cam, grey, clipped) * p.gain;
|
||||||
let wa = w * s.a;
|
let wa = w * s.a;
|
||||||
let i = gid.y * p.chunk_size.x + gid.x;
|
let i = gid.y * p.chunk_size.x + gid.x;
|
||||||
acc[i] = acc[i] + vec4<f32>(rgb * wa, wa);
|
acc[i] = acc[i] + vec4<f32>(rgb * wa, wa);
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ _None._
|
|||||||
| FR-MRG-3 | [`core/dr-decode/examples/linear_dng.rs:1`](../../core/dr-decode/examples/linear_dng.rs#L1), [`core/dr-decode/src/lib.rs:143`](../../core/dr-decode/src/lib.rs#L143), [`core/dr-decode/src/lib.rs:150`](../../core/dr-decode/src/lib.rs#L150), [`core/dr-decode/src/lib.rs:604`](../../core/dr-decode/src/lib.rs#L604), [`core/dr-decode/src/profile.rs:395`](../../core/dr-decode/src/profile.rs#L395), [`core/dr-export/src/dng.rs:1`](../../core/dr-export/src/dng.rs#L1), [`core/dr-gpu/src/demosaic.rs:302`](../../core/dr-gpu/src/demosaic.rs#L302), [`ui/dr-ui/src/merge.rs:1`](../../ui/dr-ui/src/merge.rs#L1) |
|
| FR-MRG-3 | [`core/dr-decode/examples/linear_dng.rs:1`](../../core/dr-decode/examples/linear_dng.rs#L1), [`core/dr-decode/src/lib.rs:143`](../../core/dr-decode/src/lib.rs#L143), [`core/dr-decode/src/lib.rs:150`](../../core/dr-decode/src/lib.rs#L150), [`core/dr-decode/src/lib.rs:604`](../../core/dr-decode/src/lib.rs#L604), [`core/dr-decode/src/profile.rs:395`](../../core/dr-decode/src/profile.rs#L395), [`core/dr-export/src/dng.rs:1`](../../core/dr-export/src/dng.rs#L1), [`core/dr-gpu/src/demosaic.rs:302`](../../core/dr-gpu/src/demosaic.rs#L302), [`ui/dr-ui/src/merge.rs:1`](../../ui/dr-ui/src/merge.rs#L1) |
|
||||||
| FR-MRG-4 | [`core/dr-export/src/inscribed.rs:1`](../../core/dr-export/src/inscribed.rs#L1), [`core/dr-pano/src/fill.rs:1`](../../core/dr-pano/src/fill.rs#L1), [`core/dr-pano/src/migan.rs:1`](../../core/dr-pano/src/migan.rs#L1), [`core/dr-pano/src/projection.rs:1`](../../core/dr-pano/src/projection.rs#L1), [`ui/dr-ui/src/library/paths.rs:365`](../../ui/dr-ui/src/library/paths.rs#L365), [`ui/dr-ui/src/library/paths.rs:369`](../../ui/dr-ui/src/library/paths.rs#L369), [`ui/dr-ui/src/merge.rs:148`](../../ui/dr-ui/src/merge.rs#L148), [`ui/dr-ui/src/merge_ui.rs:1`](../../ui/dr-ui/src/merge_ui.rs#L1), [`ui/dr-ui/ui/merge.slint:1`](../../ui/dr-ui/ui/merge.slint#L1), [`ui/dr-ui/ui/merge.slint:46`](../../ui/dr-ui/ui/merge.slint#L46) |
|
| FR-MRG-4 | [`core/dr-export/src/inscribed.rs:1`](../../core/dr-export/src/inscribed.rs#L1), [`core/dr-pano/src/fill.rs:1`](../../core/dr-pano/src/fill.rs#L1), [`core/dr-pano/src/migan.rs:1`](../../core/dr-pano/src/migan.rs#L1), [`core/dr-pano/src/projection.rs:1`](../../core/dr-pano/src/projection.rs#L1), [`ui/dr-ui/src/library/paths.rs:365`](../../ui/dr-ui/src/library/paths.rs#L365), [`ui/dr-ui/src/library/paths.rs:369`](../../ui/dr-ui/src/library/paths.rs#L369), [`ui/dr-ui/src/merge.rs:148`](../../ui/dr-ui/src/merge.rs#L148), [`ui/dr-ui/src/merge_ui.rs:1`](../../ui/dr-ui/src/merge_ui.rs#L1), [`ui/dr-ui/ui/merge.slint:1`](../../ui/dr-ui/ui/merge.slint#L1), [`ui/dr-ui/ui/merge.slint:46`](../../ui/dr-ui/ui/merge.slint#L46) |
|
||||||
| FR-MRG-5 | [`core/dr-pano/src/align.rs:1`](../../core/dr-pano/src/align.rs#L1), [`ui/dr-ui/src/merge.rs:1`](../../ui/dr-ui/src/merge.rs#L1), [`ui/dr-ui/src/merge_ui.rs:1`](../../ui/dr-ui/src/merge_ui.rs#L1), [`ui/dr-ui/ui/merge.slint:1`](../../ui/dr-ui/ui/merge.slint#L1) |
|
| FR-MRG-5 | [`core/dr-pano/src/align.rs:1`](../../core/dr-pano/src/align.rs#L1), [`ui/dr-ui/src/merge.rs:1`](../../ui/dr-ui/src/merge.rs#L1), [`ui/dr-ui/src/merge_ui.rs:1`](../../ui/dr-ui/src/merge_ui.rs#L1), [`ui/dr-ui/ui/merge.slint:1`](../../ui/dr-ui/ui/merge.slint#L1) |
|
||||||
| FR-MRG-6 | [`core/dr-pipeline/src/sidecar.rs:1055`](../../core/dr-pipeline/src/sidecar.rs#L1055), [`core/dr-pipeline/src/sidecar.rs:116`](../../core/dr-pipeline/src/sidecar.rs#L116), [`core/dr-pipeline/src/sidecar.rs:126`](../../core/dr-pipeline/src/sidecar.rs#L126), [`core/dr-pipeline/src/sidecar.rs:2284`](../../core/dr-pipeline/src/sidecar.rs#L2284), [`core/dr-pipeline/src/sidecar.rs:860`](../../core/dr-pipeline/src/sidecar.rs#L860), [`ui/dr-ui/src/merge.rs:850`](../../ui/dr-ui/src/merge.rs#L850) |
|
| FR-MRG-6 | [`core/dr-pipeline/src/sidecar.rs:1055`](../../core/dr-pipeline/src/sidecar.rs#L1055), [`core/dr-pipeline/src/sidecar.rs:116`](../../core/dr-pipeline/src/sidecar.rs#L116), [`core/dr-pipeline/src/sidecar.rs:126`](../../core/dr-pipeline/src/sidecar.rs#L126), [`core/dr-pipeline/src/sidecar.rs:2284`](../../core/dr-pipeline/src/sidecar.rs#L2284), [`core/dr-pipeline/src/sidecar.rs:860`](../../core/dr-pipeline/src/sidecar.rs#L860), [`ui/dr-ui/src/merge.rs:853`](../../ui/dr-ui/src/merge.rs#L853) |
|
||||||
| FR-MRG-7 | [`ui/dr-ui/src/merge.rs:1`](../../ui/dr-ui/src/merge.rs#L1), [`ui/dr-ui/src/merge_ui.rs:1`](../../ui/dr-ui/src/merge_ui.rs#L1), [`ui/dr-ui/ui/app.slint:589`](../../ui/dr-ui/ui/app.slint#L589), [`ui/dr-ui/ui/merge.slint:1`](../../ui/dr-ui/ui/merge.slint#L1) |
|
| FR-MRG-7 | [`ui/dr-ui/src/merge.rs:1`](../../ui/dr-ui/src/merge.rs#L1), [`ui/dr-ui/src/merge_ui.rs:1`](../../ui/dr-ui/src/merge_ui.rs#L1), [`ui/dr-ui/ui/app.slint:589`](../../ui/dr-ui/ui/app.slint#L589), [`ui/dr-ui/ui/merge.slint:1`](../../ui/dr-ui/ui/merge.slint#L1) |
|
||||||
| FR-MRG-8 | [`core/dr-pano/src/xfeat.rs:1`](../../core/dr-pano/src/xfeat.rs#L1), [`core/dr-segment/examples/onnx_probe.rs:1`](../../core/dr-segment/examples/onnx_probe.rs#L1) |
|
| FR-MRG-8 | [`core/dr-pano/src/xfeat.rs:1`](../../core/dr-pano/src/xfeat.rs#L1), [`core/dr-segment/examples/onnx_probe.rs:1`](../../core/dr-segment/examples/onnx_probe.rs#L1) |
|
||||||
| FR-NC-1 | [`core/dr-sync-nextcloud/src/auth.rs:206`](../../core/dr-sync-nextcloud/src/auth.rs#L206), [`core/dr-sync-nextcloud/src/auth.rs:49`](../../core/dr-sync-nextcloud/src/auth.rs#L49), [`core/dr-sync-nextcloud/src/provider.rs:1`](../../core/dr-sync-nextcloud/src/provider.rs#L1), [`core/dr-sync/src/account.rs:375`](../../core/dr-sync/src/account.rs#L375), [`ui/dr-ui/src/launch.rs:316`](../../ui/dr-ui/src/launch.rs#L316), [`ui/dr-ui/src/launch.rs:61`](../../ui/dr-ui/src/launch.rs#L61), [`ui/dr-ui/src/launch_ui.rs:521`](../../ui/dr-ui/src/launch_ui.rs#L521) |
|
| FR-NC-1 | [`core/dr-sync-nextcloud/src/auth.rs:206`](../../core/dr-sync-nextcloud/src/auth.rs#L206), [`core/dr-sync-nextcloud/src/auth.rs:49`](../../core/dr-sync-nextcloud/src/auth.rs#L49), [`core/dr-sync-nextcloud/src/provider.rs:1`](../../core/dr-sync-nextcloud/src/provider.rs#L1), [`core/dr-sync/src/account.rs:375`](../../core/dr-sync/src/account.rs#L375), [`ui/dr-ui/src/launch.rs:316`](../../ui/dr-ui/src/launch.rs#L316), [`ui/dr-ui/src/launch.rs:61`](../../ui/dr-ui/src/launch.rs#L61), [`ui/dr-ui/src/launch_ui.rs:521`](../../ui/dr-ui/src/launch_ui.rs#L521) |
|
||||||
|
|||||||
+43
-1
@@ -560,6 +560,7 @@ fn run_inner(
|
|||||||
let black = first.raw.black_level[0];
|
let black = first.raw.black_level[0];
|
||||||
let white_level = u32::from(first.raw.white_level.saturating_sub(black)).max(1);
|
let white_level = u32::from(first.raw.white_level.saturating_sub(black)).max(1);
|
||||||
let profile = dng_profile(first, white_level);
|
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);
|
let (_, mut carried) = crate::export::header_for_file(&first.meta);
|
||||||
// The composite is dated at the middle of its sweep — the mean of the
|
// 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
|
// frames' capture times — so it sorts among the frames it was made
|
||||||
@@ -578,6 +579,7 @@ fn run_inner(
|
|||||||
feather: request.feather_px,
|
feather: request.feather_px,
|
||||||
chunk: request.chunk,
|
chunk: request.chunk,
|
||||||
sample_scale: white_level as f32,
|
sample_scale: white_level as f32,
|
||||||
|
balance,
|
||||||
};
|
};
|
||||||
let merge_frames: Vec<MergeFrame> = frames
|
let merge_frames: Vec<MergeFrame> = frames
|
||||||
.iter()
|
.iter()
|
||||||
@@ -619,6 +621,7 @@ fn run_inner(
|
|||||||
feather: request.feather_px,
|
feather: request.feather_px,
|
||||||
chunk: request.chunk,
|
chunk: request.chunk,
|
||||||
sample_scale: white_level as f32,
|
sample_scale: white_level as f32,
|
||||||
|
balance,
|
||||||
};
|
};
|
||||||
let (qw, qh) = (qout.width() as usize, qout.height() as usize);
|
let (qw, qh) = (qout.width() as usize, qout.height() as usize);
|
||||||
let mut qrgb = vec![0.0f32; qw * qh * 3];
|
let mut qrgb = vec![0.0f32; qw * qh * 3];
|
||||||
@@ -987,8 +990,10 @@ fn preview_planes(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let i = (y as usize * g.width + x as usize) * 3;
|
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 {
|
for c in 0..3 {
|
||||||
sum[c] += colour[k][i + c] * gains[k];
|
sum[c] += cam[c] * gains[k];
|
||||||
}
|
}
|
||||||
n += 1;
|
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]> {
|
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 (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);
|
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 {
|
mod tests {
|
||||||
use super::*;
|
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]
|
#[test]
|
||||||
fn a_worker_that_hangs_up_mid_job_is_reported_as_gone() {
|
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
|
// The failure the page could not see: a panic drops the sender with
|
||||||
|
|||||||
Reference in New Issue
Block a user