This commit is contained in:
Brandon Thomas
2026-10-13 11:48:34 -04:00
10 changed files with 235 additions and 13 deletions
+6 -2
View File
@@ -130,11 +130,14 @@ impl MergeJob {
return Err("cancelled".into());
}
let sub = |a: f32, b: f32| move |f: f32, s: &str| progress(a + (b - a) * f, s);
// the reference frame's lens corrections (HDR); panoramas carry none
let mut lens = Vec::new();
let (image, color, orientation, metadata, baseline, samples, crop, mut info, ghost) = match &self.kind {
MergeKind::Hdr { hdr } => {
let frames = self.load(false, progress, (0.0, 0.3))?;
let p = sub(0.3, 0.9);
let r = lightcraft_merge::merge_hdr(frames, hdr, &p).map_err(err)?;
let mut r = lightcraft_merge::merge_hdr(frames, hdr, &p).map_err(err)?;
lens = std::mem::take(&mut r.lens);
let info = json!({
"reference": self.sources[r.reference].0.0,
"ev": r.ev, "evExif": r.ev_exif,
@@ -183,6 +186,7 @@ impl MergeJob {
orientation: r.orientation,
metadata: r.metadata,
baseline_exposure: r.baseline_exposure,
lens: Vec::new(),
};
f.orient();
merged.push(f);
@@ -214,7 +218,7 @@ impl MergeJob {
metadata.caption = Some(format!("Merged from {}", names.join(", ")));
}
metadata.software = Some("LightCraft Photo Merge".into());
let dng = lightcraft_merge::write_linear_dng(&image, &color, orientation, &metadata, baseline, samples).map_err(err)?;
let dng = lightcraft_merge::write_linear_dng(&image, &color, orientation, &metadata, baseline, samples, &lens).map_err(err)?;
info["width"] = json!(image.width);
info["height"] = json!(image.height);
let preview = match self.preview {
+51
View File
@@ -67,6 +67,57 @@ fn hdr_merge_command_creates_and_imports_a_dng() {
let _ = std::fs::remove_dir_all(&dir);
}
/// An HDR merge keeps the reference frame's lens corrections. The sources carry an off-centre `WarpRectilinear` and
/// `FixVignetteRadial` for their active area and have a default crop; the merged DNG must give "Enable Profile
/// Corrections" the same correction as the reference photo, upright and for a vertical bracket (whose DNG keeps the
/// pixels unturned).
#[test]
fn hdr_merge_keeps_the_reference_frames_lens_corrections() {
use lightcraft_geom::{Orientation, Point};
use lightcraft_pipeline::optics::{embedded_vignette_gain, embedded_warp};
use lightcraft_raw::Opcode;
for orientation in [Orientation::Normal, Orientation::Rotate90] {
let dir = temp_dir(&format!("hdr-lens-{orientation:?}"));
let mut paths = Vec::new();
for (i, b) in lightcraft_merge::synth::bracket_dngs(480, 320, &[-2.0, 0.0, 2.0]).unwrap().into_iter().enumerate() {
let mut raw = lightcraft_raw::decode(&b).unwrap();
raw.crop = lightcraft_raw::Rect::new(10, 6, 452, 304);
raw.orientation = orientation;
raw.metadata.orientation = Some(orientation);
raw.opcodes.list3 = vec![
Opcode::WarpRectilinear { planes: vec![[0.96, 0.05, -0.012, 0.002, 0.001, -0.0005]], center: [0.48, 0.53] },
Opcode::FixVignetteRadial { k: [0.35, -0.05, 0.01, 0.0, 0.0], center: [0.48, 0.53] },
];
let p = dir.join(format!("IMG_{i}.dng"));
std::fs::write(&p, lightcraft_raw::write_dng(&raw, &Default::default()).unwrap()).unwrap();
paths.push(p.to_string_lossy().to_string());
}
let mut s = Session::new().with_fs();
let ids = import(&mut s, paths);
let r = s.execute("merge.hdr", &json!({"ids": ids})).unwrap();
assert_eq!(r["reference"].as_u64(), Some(ids[1]), "{r}");
let merged = s.catalog.photo(lightcraft_catalog::PhotoId(r["id"].as_u64().unwrap())).unwrap().clone();
let reference = s.catalog.photo(lightcraft_catalog::PhotoId(ids[1])).unwrap().clone();
let expect = if orientation == Orientation::Normal { (452, 304) } else { (304, 452) };
assert_eq!(((merged.width, merged.height), (reference.width, reference.height)), (expect, expect));
let a = reference.embedded_lens.expect("the source's correction");
let b = merged.embedded_lens.expect("the merged DNG carries the reference frame's correction");
let (wa, wb) = (a.warp.unwrap(), b.warp.unwrap());
let (va, vb) = (a.vignette.unwrap(), b.vignette.unwrap());
let (w, h) = (expect.0 as f64, expect.1 as f64);
for (x, y) in [(0.0, 0.0), (w, h), (w, 0.0), (0.0, h), (w * 0.3, h * 0.7), (w * 0.5, h * 0.5)] {
let p = Point::new(x, y);
for ch in 0..3 {
let (pa, pb) = (embedded_warp(&wa, p, ch, w, h), embedded_warp(&wb, p, ch, w, h));
assert!((pa.x - pb.x).abs() < 1e-6 && (pa.y - pb.y).abs() < 1e-6, "{orientation:?} {p:?}: {pa:?} vs {pb:?}");
}
let (ga, gb) = (embedded_vignette_gain(&va, p, w, h), embedded_vignette_gain(&vb, p, w, h));
assert!((ga - gb).abs() < 1e-9, "{orientation:?} {p:?}: {ga} vs {gb}");
}
let _ = std::fs::remove_dir_all(&dir);
}
}
/// Raws without colour matrices of their own (CR2, NEF, ARW…) merge into a DNG with the colour they open with,
/// not with no colour at all (issue #965: an HDR merge of CR2s rendered green and dark).
#[test]