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.
90 lines
3.4 KiB
Rust
90 lines
3.4 KiB
Rust
//! 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();
|
||
}
|