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.
This commit is contained in:
2026-09-27 16:29:39 -04:00
parent e98b1def98
commit 621a6b8313
9 changed files with 616 additions and 202 deletions
+52 -9
View File
@@ -7,7 +7,8 @@
//! fetches the selected originals on a worker, hands them to the job, and
//! opens the page; a timer drains the job's events into the page's
//! properties. When the alignment arrives the page shows it and waits.
//! "Merge" sends the decision; "Stop" or "Back" cancels. When the file is
//! A frame's box leaves it out or brings it back, and the job aligns again
//! over the rest. "Merge" sends the decision; "Stop" or "Back" cancels. When the file is
//! staged, the outbox drains and the library rescans, and the composite
//! appears in the grid beside its sources.
@@ -327,6 +328,37 @@ fn wire_decision(window: &AppWindow, ctl: &Rc<MergeController>) {
}
});
}
{
let weak = window.as_weak();
let ctl = ctl.clone();
window.on_merge_frame_toggled(move |i, on| {
let Some(w) = weak.upgrade() else { return };
// The set the alignment on screen was solved over, with this
// frame flipped. The box is controlled: it moves when the job's
// report comes back with the frame in or out.
let Some(mut keep) = ctl.report.borrow().as_ref().map(|r| r.included.clone()) else {
return;
};
let Some(slot) = keep.get_mut(i as usize) else {
return;
};
*slot = on;
if keep.iter().filter(|k| **k).count() < 2 {
w.set_merge_status("A panorama needs at least two frames.".into());
return;
}
if let Some(job) = ctl.job.borrow().as_ref() {
let _ = job.decide.send(Decision::Frames {
keep,
projection: chip_projection(ctl.projection.get()),
fill: ctl.fill_choice(),
});
}
w.set_merge_running(true);
w.set_merge_stage("Aligning".into());
w.set_merge_status("".into());
});
}
{
let weak = window.as_weak();
let ctl = ctl.clone();
@@ -561,10 +593,11 @@ fn drain(window: &AppWindow, ctl: &Rc<MergeController>, on_done: &Rc<impl Fn(&Ap
window.set_merge_running(false);
window.set_merge_stage("".into());
window.set_merge_fraction(0.0);
window.set_merge_status(if report.frames.iter().all(Result::is_ok) {
"Check the alignment, then press Merge.".into()
window.set_merge_status(if report.is_complete() {
"Check the alignment, then press Merge. Untick a frame to leave it out.".into()
} else {
"Not every frame could be placed. Remove the ones named and try again.".into()
"Not every frame could be placed. Untick the ones named to merge without them."
.into()
});
*ctl.report.borrow_mut() = Some(report);
}
@@ -632,28 +665,31 @@ fn show_report(window: &AppWindow, report: &AlignmentReport, names: &[String]) {
.get(k)
.cloned()
.unwrap_or_else(|| format!("frame {k}"));
let included = report.included.get(k).copied().unwrap_or(true);
match f {
Ok((yaw, pitch)) => MergeFrameRow {
name: name.into(),
detail: format!("yaw {yaw:.1}° · pitch {pitch:.1}°").into(),
aligned: true,
included,
},
Err(why) => MergeFrameRow {
name: name.into(),
detail: why.clone().into(),
aligned: false,
included,
},
}
})
.collect();
let complete = report.frames.iter().all(Result::is_ok);
let complete = report.is_complete();
window.set_merge_frames(slint::ModelRc::new(slint::VecModel::from(rows)));
window.set_merge_aligned(true);
window.set_merge_complete(complete);
window.set_merge_summary(
format!(
"{} frames, {} overlaps · {:.0} mm equivalent · {:?} · {}×{} px · fit {:.1} px",
report.frames.len(),
report.included.iter().filter(|k| **k).count(),
report.links,
report.focal_mm,
report.projection,
@@ -663,9 +699,16 @@ fn show_report(window: &AppWindow, report: &AlignmentReport, names: &[String]) {
)
.into(),
);
if let Some((w, h, rgba)) = &report.preview {
let buffer = slint::SharedPixelBuffer::<slint::Rgba8Pixel>::clone_from_slice(rgba, *w, *h);
window.set_merge_preview(slint::Image::from_rgba8(buffer));
// No preview is no picture, not the last one: after a frame is
// unticked, the old composite would show a set that is not the one
// being confirmed.
match &report.preview {
Some((w, h, rgba)) => {
let buffer =
slint::SharedPixelBuffer::<slint::Rgba8Pixel>::clone_from_slice(rgba, *w, *h);
window.set_merge_preview(slint::Image::from_rgba8(buffer));
}
None => window.set_merge_preview(slint::Image::default()),
}
window.set_merge_preview_filled(report.filled);
match &report.filler {