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:
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user