Files
DarkRoom/ui/dr-ui/examples/merge.rs
T
dtourolle 621a6b8313 Leave a panorama frame out without leaving the page
A frame that did not fit ended the job with its name, and the only way
on was Back, a smaller selection and every frame read, demosaiced and
searched for keypoints again. Each row on the page now has a box. An
unticked frame is left out and the rest are solved again from what the
first pass measured.

dr_pano::align is split for it: match_pairs does the matching and the
pairwise RANSAC once over every frame (about 5 s for twelve), and solve
takes a subset of the frames and uses only the links among them (about
0.1 s). Solving a subset by re-aligning it also moved every RANSAC seed,
which are keyed on frame position, and on the fixture set that was
enough to lose a marginal link and strand a neighbour of the frame left
out.

A frame that cannot be placed no longer stops the job either. Its row
names it and Merge stays off until it is unticked; the headless example
leaves such frames out the same way, and takes --leave-out N to try it.
2026-09-27 16:29:39 -04:00

205 lines
8.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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 --fill frame1.CR2 …
//! cargo run -p dr-ui --example merge --release -- --leave-out 3 fixtures/pano/2025-08-05/*.CR2
//! ```
//!
//! `--leave-out N` unticks frame `N` once the first alignment is in, as the
//! page's box would, so the job aligns again without it.
//!
//! 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;
let mut fill = false;
let mut wait_engines = false;
let mut leave_out: Option<usize> = None;
loop {
match args.first().map(String::as_str) {
Some("--out") => {
args.remove(0);
out_dir = Some(PathBuf::from(args.remove(0)));
}
Some("--fill") => {
args.remove(0);
fill = true;
}
Some("--leave-out") => {
args.remove(0);
leave_out = args.remove(0).parse().ok();
}
Some("--wait-engines") => {
args.remove(0);
wait_engines = true;
}
_ => break,
}
}
if args.len() < 2 {
eprintln!("usage: merge [--out DIR] [--fill] [--wait-engines] <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);
};
// The inference engine as the desktop app starts it, so the border fill
// runs where the app would run it — TensorRT, ONNX Runtime's CPU pool —
// rather than on tract, which is a hundred times slower for this model.
// `DARKROOM_ORT_DIR` first, as the desktop app takes it: a runtime with
// the TensorRT provider where the distribution's has none.
let mut runtime_dirs = Vec::new();
if let Some(dir) = std::env::var_os("DARKROOM_ORT_DIR") {
runtime_dirs.push(std::path::PathBuf::from(dir));
}
runtime_dirs.push(std::path::PathBuf::from("/usr/lib/darkroom"));
runtime_dirs.push(std::path::PathBuf::from("/usr/lib"));
dr_ui::inference::init(runtime_dirs);
// A compiling rung (TensorRT) serves models from the fallback until
// their engines are built, in the background, on first run; the app
// lives long enough for that, this example does not. `--wait-engines`
// waits for the cache to fill so the fill's timing is the rung's.
if wait_engines {
loop {
let s = dr_inference_engine::status();
eprintln!(
"inference: {} ({}), engines {}/{}",
s.line(),
s.reason,
s.engines.0,
s.engines.1
);
if !s.probing && s.engines.0 >= s.engines.1 {
break;
}
std::thread::sleep(std::time::Duration::from_secs(5));
}
}
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 mut request =
dr_ui::merge::MergeRequest::new(frames, dr_ui::merge::MergeDestination::Local(dir));
// The filler from the shared models directory, as the app finds it.
request.inpaint_model = dr_ui::merge::inpaint_model_path();
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) if !r.included[k] => println!(" frame {k}: {why}"),
Err(why) => println!(" frame {k}: NOT ALIGNED — {why}"),
}
}
println!(
" border fill: {}",
match &r.filler {
Ok(rung) => format!("available, runs on {rung}"),
Err(why) => why.clone(),
}
);
if let Some(k) = leave_out.take() {
let mut keep = r.included.clone();
if let Some(slot) = keep.get_mut(k) {
*slot = false;
}
println!(" aligning again without frame {k}");
let _ = decide.send(dr_ui::merge::Decision::Frames {
keep,
projection: None,
fill: None,
});
continue;
}
// Headless: what a photographer would untick — the frames
// that could not be placed — then the confirmation.
if !r.is_complete() {
let keep: Vec<bool> = r
.frames
.iter()
.zip(&r.included)
.map(|(f, &kept)| kept && f.is_ok())
.collect();
if keep == r.included || keep.iter().filter(|k| **k).count() < 2 {
eprintln!("not enough frames can be placed to merge");
let _ = decide.send(dr_ui::merge::Decision::Abandon);
} else {
println!(" aligning again without the frames that could not be placed");
let _ = decide.send(dr_ui::merge::Decision::Frames {
keep,
projection: None,
fill: None,
});
}
continue;
}
let _ = decide.send(dr_ui::merge::Decision::Merge {
projection: None,
fill: fill.then(dr_ui::merge::FillSettings::default),
});
}
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();
}