Render raws through Camera Raw's tone by default, after baseline exposure
The rendering half of camera-profiles.md §11-§12 (D21). The view transform gains a curve choice — Camera Raw (the default) or D19's sigmoid. Camera Raw converts to linear ProPhoto, clips to [0, 1], runs the curve on the largest and smallest channel and places the middle one at its old fraction between them (RefBaselineRGBTone), and converts back: hue kept, saturation raised where the curve is steep, which is where Adobe Standard's look desaturated. White sets the input scale (1 at its default, so sensor white is display white) and contrast bends the input about grey (1 at its default). The curve rides in the profile buffer after the tables: the profile's own, else the ACR3 default, which the placeholder every profile-less source binds also carries — so a CR2 with no .dcp still gets Camera Raw's tone. Baseline exposure is a gain folded into the rendering matrix at upload; RawImage::color_matrix stays the file's for the merge's linear DNG. camera_raw::apply_reference is the CPU statement; GPU tests hold the shader to it on 256 colours and on greys against the ACR3 table. The sigmoid's own tests now choose it explicitly.
This commit is contained in:
@@ -3420,8 +3420,10 @@ mod tests {
|
||||
read_centre(&ctx, t)
|
||||
};
|
||||
|
||||
// The default rendering of a raw is Camera Raw's tone (D21): a grey
|
||||
// goes through the ACR3 curve unchanged in hue.
|
||||
let scene = 3537.0 / 16383.0;
|
||||
let viewed = dr_pipeline::view::Sigmoid::default_curve().channel(scene);
|
||||
let viewed = dr_types::tone::evaluate(&dr_types::tone::ACR3_DEFAULT, scene);
|
||||
let expected = (dr_types::Transfer::Srgb.encode(viewed) * 255.0).round() as i32;
|
||||
let delta = (i32::from(from_sensor[0]) - expected).abs();
|
||||
assert!(
|
||||
|
||||
@@ -578,7 +578,7 @@ impl DemosaicedImage {
|
||||
view,
|
||||
width,
|
||||
height,
|
||||
color_matrix: raw.color_matrix.unwrap_or(IDENTITY_3X3),
|
||||
color_matrix: rendering_matrix(raw),
|
||||
profile_tables: raw.profile_tables.clone(),
|
||||
as_shot_wb: [raw.wb_coeffs[0], raw.wb_coeffs[1], raw.wb_coeffs[2]],
|
||||
non_linear: false,
|
||||
@@ -589,6 +589,23 @@ impl DemosaicedImage {
|
||||
}
|
||||
}
|
||||
|
||||
/// TRACES: FR-DEV-3e
|
||||
/// The camera matrix a raw renders through: the file's — identity where the
|
||||
/// body is uncalibrated, so the image renders with no colour transform
|
||||
/// rather than not at all — times the baseline exposure as a gain
|
||||
/// (camera-profiles.md §11).
|
||||
///
|
||||
/// A uniform gain commutes with every scene operation before the view
|
||||
/// transform, so folding it in here is the same as an exposure step at the
|
||||
/// head of the chain, at no cost. The camera-space tap and the white-balance
|
||||
/// probe read camera RGB before this matrix and are unaffected.
|
||||
/// `RawImage::color_matrix` stays the file's: a merge writes a linear DNG
|
||||
/// from it and must not bake a gain into its pixels.
|
||||
fn rendering_matrix(raw: &RawImage) -> [f32; 9] {
|
||||
let gain = raw.baseline_exposure.exp2();
|
||||
raw.color_matrix.unwrap_or(IDENTITY_3X3).map(|v| v * gain)
|
||||
}
|
||||
|
||||
/// Convert an f32 to half-precision bits, the general case: sign,
|
||||
/// subnormals, round-to-nearest-even, saturation at the largest finite.
|
||||
///
|
||||
@@ -937,7 +954,7 @@ impl Demosaicer {
|
||||
height,
|
||||
// Identity where the body is uncalibrated: the image renders with
|
||||
// no colour transform rather than not at all.
|
||||
color_matrix: raw.color_matrix.unwrap_or(IDENTITY_3X3),
|
||||
color_matrix: rendering_matrix(raw),
|
||||
profile_tables: raw.profile_tables.clone(),
|
||||
as_shot_wb: [raw.wb_coeffs[0], raw.wb_coeffs[1], raw.wb_coeffs[2]],
|
||||
// Whatever the profile database had for this body (FR-DEV-3e),
|
||||
|
||||
@@ -192,6 +192,41 @@ fn the_shader_agrees_with_the_cpu_reference() {
|
||||
assert_agrees(&got, |c| apply_reference(&tables, c, 2.0), "look at 200%");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn camera_raw_tone_agrees_with_its_cpu_reference() {
|
||||
// TRACES: FR-DEV-3j
|
||||
// D21's rendering on 256 colours: the ProPhoto round trip, the clip, the
|
||||
// curve from the profile buffer's placeholder, and RGBTone's placement
|
||||
// of the middle channel, against `camera_raw::apply_reference`.
|
||||
let Some(ctx) = ctx() else {
|
||||
eprintln!("skipping: no GPU adapter");
|
||||
return;
|
||||
};
|
||||
let source = Demosaicer::new(&ctx)
|
||||
.expect("demosaicer")
|
||||
.run(&frame(None))
|
||||
.expect("upload");
|
||||
let ops: Vec<Box<dyn Operation>> = vec![Box::new(dr_pipeline::ops::ViewTransform::new())];
|
||||
let shader = dr_pipeline::compose(&ops);
|
||||
let mut adjust = AdjustPass::new(&ctx);
|
||||
adjust.render(&source, &shader, SIZE, SIZE).expect("render");
|
||||
let (pixels, _, _) = adjust.export_pixels().expect("readback");
|
||||
let got: Vec<[u8; 3]> = pixels.chunks_exact(4).map(|p| [p[0], p[1], p[2]]).collect();
|
||||
let curve = &dr_types::tone::ACR3_DEFAULT;
|
||||
assert_agrees(
|
||||
&got,
|
||||
|c| {
|
||||
dr_pipeline::camera_raw::apply_reference(
|
||||
curve,
|
||||
c,
|
||||
dr_pipeline::view::DEFAULT_CONTRAST,
|
||||
dr_pipeline::view::DEFAULT_WHITE,
|
||||
)
|
||||
},
|
||||
"Camera Raw tone",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn switched_off_or_absent_the_render_is_unchanged() {
|
||||
let Some(ctx) = ctx() else {
|
||||
|
||||
@@ -46,6 +46,40 @@ fn flat_raw(level: u16) -> RawImage {
|
||||
}
|
||||
}
|
||||
|
||||
/// The default chain with D19's sigmoid chosen: Camera Raw's tone is the
|
||||
/// default since D21, and these tests are about the sigmoid.
|
||||
fn sigmoid_chain() -> EditGraph {
|
||||
let mut g = EditGraph::default_chain();
|
||||
g.set_param(
|
||||
dr_pipeline::ops::view_transform::ID,
|
||||
dr_pipeline::ops::view_transform::CURVE,
|
||||
dr_pipeline::ops::view_transform::SIGMOID,
|
||||
);
|
||||
g
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn camera_raw_tone_is_the_default_and_agrees_with_the_acr3_curve() {
|
||||
// TRACES: FR-DEV-3j
|
||||
// D21: a raw with no profile renders a grey through Camera Raw's default
|
||||
// curve, which the profile buffer's placeholder carries.
|
||||
let Some(ctx) = ctx() else {
|
||||
eprintln!("skipping: no GPU adapter");
|
||||
return;
|
||||
};
|
||||
let graph = EditGraph::default_chain();
|
||||
for level in [500u16, 4_000, 8_520, 20_000, 40_000] {
|
||||
let scene = f32::from(level) / f32::from(u16::MAX);
|
||||
let display = dr_types::tone::evaluate(&dr_types::tone::ACR3_DEFAULT, scene);
|
||||
let expected = (dr_types::Transfer::Srgb.encode(display) * 255.0).round() as i32;
|
||||
let got = i32::from(rendered(&ctx, level, &graph));
|
||||
assert!(
|
||||
(got - expected).abs() <= 2,
|
||||
"raw {level} rendered as {got}, the ACR3 curve says {expected}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Render `graph` over a flat frame and return the centre pixel's red.
|
||||
///
|
||||
/// The centre rather than a corner: a demosaic has to invent its edges.
|
||||
@@ -70,7 +104,7 @@ fn the_shader_agrees_with_the_cpu_reference() {
|
||||
return;
|
||||
};
|
||||
let curve = Sigmoid::default_curve();
|
||||
let graph = EditGraph::default_chain();
|
||||
let graph = sigmoid_chain();
|
||||
for level in [0u16, 500, 4_000, 8_520, 32_768, 60_000, u16::MAX] {
|
||||
let scene = f32::from(level) / f32::from(u16::MAX);
|
||||
let display = curve.channel(scene).min(1.0);
|
||||
@@ -96,7 +130,7 @@ fn highlights_above_one_stay_distinct() {
|
||||
eprintln!("skipping: no GPU adapter");
|
||||
return;
|
||||
};
|
||||
let mut graph = EditGraph::default_chain();
|
||||
let mut graph = sigmoid_chain();
|
||||
graph.set_param(
|
||||
dr_pipeline::ops::exposure::ID,
|
||||
dr_pipeline::ops::exposure::EXPOSURE,
|
||||
|
||||
Reference in New Issue
Block a user