The merge job and its page: a selection to a panorama DNG, confirmed first

dr_ui::merge is the orchestration with no interface in it: decode each
frame to sensor data and build its graph as a session would (orientation,
lens profile); render each through the camera-space tap at proxy size and
detect keypoints there, so the alignment is measured in the undistorted
frame the tiles are rendered in; align; solve one gain per frame from the
proxies' overlaps; draw the aligned set in colour for the page; then wait.
Nothing is written until a Decision arrives (FR-MRG-1). The merge writes
a linear DNG through the outbox with a destination record, so the drain
puts it beside its sources on a folder library and a server alike, and
the library rescans (FR-MRG-3).

merge.slint is the page, on the import page's model: the alignment
table with a failed frame named on its row and the button held off
(FR-MRG-5), the preview, the projection choice, Stop and Back. A
"Merge to panorama" button joins the grid's selection bar at two frames.

Headless, the example produces the fixture's 22 993 x 5 980 DNG in 45 s
on the reference desktop, exposures balanced across the stop of drift.
This commit is contained in:
2026-09-19 15:24:20 +02:00
parent 44ea763c61
commit 2e9a1eb0f0
12 changed files with 1654 additions and 105 deletions
+89
View File
@@ -0,0 +1,89 @@
//! Merge a set of frames into a panorama DNG, headless.
//!
//! ```sh
//! cargo run -p dr-ui --example merge --release -- fixtures/pano/2025-08-05/*.CR2
//! cargo run -p dr-ui --example merge --release -- --out /var/tmp frame1.CR2 …
//! ```
//!
//! The job the interface runs (`dr_ui::merge`), driven from a loop instead
//! of a timer: the same code, the same events, a file at the end. What it
//! proves that the alignment example cannot is the second half — the tap,
//! the chunked warp, the resolve to sixteen bits and the DNG — and that the
//! result opens in the application as one RAW.
use std::path::PathBuf;
use std::time::Instant;
fn main() {
env_logger::init();
let mut args: Vec<String> = std::env::args().skip(1).collect();
let mut out_dir = None;
if args.first().map(String::as_str) == Some("--out") {
args.remove(0);
out_dir = Some(PathBuf::from(args.remove(0)));
}
if args.len() < 2 {
eprintln!("usage: merge [--out DIR] <frame> <frame>...");
std::process::exit(2);
}
let Ok(gpu) = pollster::block_on(dr_gpu::GpuContext::new_headless()) else {
eprintln!("no GPU");
std::process::exit(1);
};
let frames: Vec<dr_ui::merge::MergeInput> = args
.iter()
.map(|a| dr_ui::merge::MergeInput::read(std::path::Path::new(a)).expect("read"))
.collect();
let dir = out_dir.unwrap_or_else(|| {
PathBuf::from(&args[0])
.parent()
.map(std::path::Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."))
});
let request = dr_ui::merge::MergeRequest::new(frames, dr_ui::merge::MergeDestination::Local(dir));
let (tx, rx) = std::sync::mpsc::channel();
let (decide, decision) = std::sync::mpsc::channel();
let cancel = dr_ui::merge::Cancel::default();
let t = Instant::now();
let worker = std::thread::spawn(move || dr_ui::merge::run(gpu, request, tx, decision, cancel));
let mut last_stage = "";
for event in rx {
use dr_ui::merge::MergeEvent::*;
match event {
Progress { stage, done, total } => {
if stage != last_stage {
println!("{stage}…");
last_stage = stage;
}
if done == total || done % 4 == 0 {
println!(" {done}/{total}");
}
}
Aligned(r) => {
println!(
"aligned: {:.1} mm equivalent, rms {:.2} px, {} links, {:?} {}×{}",
r.focal_mm, r.rms_px, r.links, r.projection, r.width, r.height
);
for (k, f) in r.frames.iter().enumerate() {
match f {
Ok((yaw, pitch)) => println!(" frame {k}: yaw {yaw:7.2}° pitch {pitch:6.2}°"),
Err(why) => println!(" frame {k}: NOT ALIGNED — {why}"),
}
}
// Headless: the confirmation the dialog would ask for.
let _ = decide.send(dr_ui::merge::Decision::Merge { projection: None });
}
Done { path, width, height, .. } => {
println!("wrote {} ({width}×{height}) in {:?}", path.display(), t.elapsed());
}
Failed(e) => {
eprintln!("failed: {e}");
std::process::exit(1);
}
Cancelled => println!("cancelled"),
}
}
let _ = worker.join();
}