mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-14 19:27:01 -04:00
Merge PR #877
This commit is contained in:
3 files changed
+75
-4
No files matched your search
@@ -275,12 +275,13 @@ impl FinishParams {
|
||||
/// #146) is a new [`Process`] with its own arm here, and V1's arm stays as it is, so photos edited
|
||||
/// under V1 keep their look (`docs/process-versions.md`). CPU and GPU both take it from here.
|
||||
/// `peak` is the HDR render's peak relative to SDR white ([`lightcraft_develop::Hdr::peak`]); at 1.0 (SDR)
|
||||
/// every arm is the SDR tone map. The camera's look is bounded at white, so HDR renders use the HDR tone map.
|
||||
/// every arm is the SDR tone map. An HDR render keeps the camera's look through the midtones and
|
||||
/// lifts its highlights past white ([`ToneMap::camera_hdr`]), so it matches the SDR render below them.
|
||||
pub fn base_tone(s: &DevelopSettings, info: &SourceInfo, peak: f32) -> ToneMap {
|
||||
let l = &s.light;
|
||||
match s.process.process() {
|
||||
Process::V1 => match info.camera_tone.as_ref().filter(|_| info.raw && peak <= 1.0) {
|
||||
Some(curve) => ToneMap::camera(curve, l.contrast, l.whites, l.blacks),
|
||||
Process::V1 => match info.camera_tone.as_ref().filter(|_| info.raw) {
|
||||
Some(curve) => ToneMap::camera_hdr(curve, l.contrast, l.whites, l.blacks, peak),
|
||||
None if info.raw => ToneMap::hdr(l.contrast, l.whites, l.blacks, peak),
|
||||
None => ToneMap::display_hdr(l.contrast, l.whites, l.blacks, peak),
|
||||
},
|
||||
|
||||
@@ -61,6 +61,38 @@ fn hdr_midtones_match_the_sdr_render() {
|
||||
assert!(n > 100, "the scene should have midtones and shadows ({n})");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hdr_keeps_the_camera_tone_curve_in_the_midtones() {
|
||||
// A raw with a fitted camera look (brighter midtones than the generic curve): its HDR render
|
||||
// used to drop the look and come out more than a stop darker than the SDR render.
|
||||
let knots = std::array::from_fn(|i| {
|
||||
let x = 0.004 * 1.18f32.powi(i as i32);
|
||||
[x, 1.0 - (-2.0 * x).exp()]
|
||||
});
|
||||
let info = SourceInfo { camera_tone: crate::tone::CameraTone::new(knots), ..raw() };
|
||||
let src = sunset();
|
||||
let render_at = |s: &DevelopSettings, depth| {
|
||||
let req = RenderRequest { depth, ..RenderRequest::fit(240, 240) };
|
||||
match render(&src, &info, s, &req).deep.map(|d| d.samples) {
|
||||
Some(DeepSamples::F32(v)) => v,
|
||||
other => panic!("expected float samples, got {other:?}"),
|
||||
}
|
||||
};
|
||||
let hdr = render_at(&hdr_on(3.0), OutputDepth::F32Hdr);
|
||||
let sdr = render_at(&DevelopSettings::default(), OutputDepth::F32Linear);
|
||||
let mut n = 0;
|
||||
for (h, s) in hdr.as_chunks::<3>().0.iter().zip(sdr.as_chunks::<3>().0) {
|
||||
if s.iter().all(|v| *v < 0.4) {
|
||||
for (a, b) in h.iter().zip(s) {
|
||||
assert!((a - b).abs() < 2e-3, "{h:?} vs {s:?}");
|
||||
}
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
assert!(n > 100, "the scene should have midtones and shadows ({n})");
|
||||
assert!(hdr.iter().any(|v| *v > 1.0), "highlights go past SDR white");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f32hdr_without_hdr_is_the_sdr_float_render() {
|
||||
let src = sunset();
|
||||
|
||||
@@ -103,13 +103,24 @@ pub struct ToneMap {
|
||||
|
||||
impl ToneMap {
|
||||
pub fn camera(curve: &CameraTone, contrast: f64, whites: f64, blacks: f64) -> ToneMap {
|
||||
ToneMap::camera_hdr(curve, contrast, whites, blacks, 1.0)
|
||||
}
|
||||
|
||||
/// [`ToneMap::camera`] for an HDR render reaching `peak` (linear, SDR white = 1; 1 = SDR
|
||||
/// exactly): the camera's look up to 1.5 EV over grey, so midtones match the SDR render (and a
|
||||
/// gain map only lifts highlights), then the highlights gain up to `peak − 1` more as in
|
||||
/// [`ToneMap::hdr`].
|
||||
pub fn camera_hdr(curve: &CameraTone, contrast: f64, whites: f64, blacks: f64, peak: f32) -> ToneMap {
|
||||
let peak = if peak.is_finite() { peak.max(1.0) } else { 1.0 };
|
||||
let adjustment = Self::display(contrast, whites, blacks);
|
||||
let neutral = contrast == 0.0 && whites == 0.0 && blacks == 0.0;
|
||||
let top = (2.9 - 1.6 * (whites as f32 / 100.0) + peak.log2() + 1.0).max(HDR_FROM_EV + 0.5);
|
||||
let lut = (0..LUT_N)
|
||||
.map(|i| {
|
||||
let ev = LUT_MIN_EV + (LUT_MAX_EV - LUT_MIN_EV) * i as f32 / (LUT_N - 1) as f32;
|
||||
let y = curve.apply(GREY * 2f32.powf(ev));
|
||||
if neutral { y } else { adjustment.apply(y) }
|
||||
let o = if neutral { y } else { adjustment.apply(y) };
|
||||
if peak > 1.0 { (o + (peak - 1.0) * smooth(HDR_FROM_EV, top, ev)).clamp(0.0, peak) } else { o }
|
||||
})
|
||||
.collect();
|
||||
ToneMap { lut, chroma: curve.chroma }
|
||||
@@ -353,6 +364,33 @@ mod tests {
|
||||
assert_eq!(ToneMap::hdr(0.0, 0.0, 0.0, f32::NAN).lut(), ToneMap::new(0.0, 0.0, 0.0).lut());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn camera_hdr_keeps_the_camera_look_and_lifts_highlights() {
|
||||
let curve = CameraTone::new(knots()).unwrap().with_chroma([1.2, 1.1, 1.0, 1.0, 0.9, 0.8, 0.7, 0.6]).unwrap();
|
||||
for (c, w, b) in [(0.0, 0.0, 0.0), (40.0, -30.0, -20.0)] {
|
||||
let sdr = ToneMap::camera(&curve, c, w, b);
|
||||
assert_eq!(ToneMap::camera_hdr(&curve, c, w, b, 1.0).lut(), sdr.lut());
|
||||
assert_eq!(ToneMap::camera_hdr(&curve, c, w, b, f32::NAN).lut(), sdr.lut());
|
||||
for peak in [2.0f32, 8.0] {
|
||||
let t = ToneMap::camera_hdr(&curve, c, w, b, peak);
|
||||
assert_eq!(t.chroma_lut(), sdr.chroma_lut());
|
||||
let mut prev = -1.0;
|
||||
for i in 0..2400 {
|
||||
let y = 1e-5 * 1.01f32.powi(i);
|
||||
let o = t.apply(y);
|
||||
assert!((0.0..=peak).contains(&o), "{peak} at {y}: {o}");
|
||||
assert!(o >= prev - 1e-6, "{peak} at {y}: {o} < {prev}");
|
||||
prev = o;
|
||||
// the camera's midtones, not the generic HDR curve's (issue: dark HDR exports)
|
||||
if y <= GREY * 2f32.powf(HDR_FROM_EV - 0.01) {
|
||||
assert!((o - sdr.apply(y)).abs() < 1e-6, "midtones differ at {y}: {o} vs {}", sdr.apply(y));
|
||||
}
|
||||
}
|
||||
assert!(t.apply(4.0) > 1.0 && t.apply(4.0) > sdr.apply(4.0) + 0.5, "{peak}: highlights lifted");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_hdr_passes_hdr_sources_and_keeps_sdr() {
|
||||
assert_eq!(ToneMap::display_hdr(20.0, 10.0, -5.0, 1.0).lut(), ToneMap::display(20.0, 10.0, -5.0).lut());
|
||||
|
||||
Reference in new issue
Block a user