cargo fmt and clippy across the panorama work, and one lint master carried

The dr-face comparison is master's: a negated partial-order test on the
eye box's width, rewritten as the two conditions it meant.
This commit is contained in:
2026-09-19 15:53:06 +02:00
parent 67f225beba
commit 42d11d919b
23 changed files with 330 additions and 123 deletions
+16 -4
View File
@@ -41,7 +41,8 @@ fn main() {
.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 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();
@@ -68,15 +69,26 @@ fn main() {
);
for (k, f) in r.frames.iter().enumerate() {
match f {
Ok((yaw, pitch)) => println!(" frame {k}: yaw {yaw:7.2}° pitch {pitch:6.2}°"),
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());
Done {
path,
width,
height,
..
} => {
println!(
"wrote {} ({width}×{height}) in {:?}",
path.display(),
t.elapsed()
);
}
Failed(e) => {
eprintln!("failed: {e}");
+4
View File
@@ -8722,6 +8722,10 @@ mod tests {
wb_coeffs: [1.0, 1.0, 1.0, 1.0],
color_matrix: None,
base_curve: dr_decode::BaseCurve::IDENTITY,
samples_per_pixel: 1,
profile: None,
make: String::new(),
model: String::new(),
crop: dr_decode::CropRect {
x: 0,
y: 0,
+72 -23
View File
@@ -34,7 +34,9 @@ use std::sync::Arc;
use std::time::Instant;
use dr_decode::RawImage;
use dr_gpu::{AdjustPass, DemosaicedImage, Demosaicer, GpuContext, MergeFrame, MergeOutput, MergePass};
use dr_gpu::{
AdjustPass, DemosaicedImage, Demosaicer, GpuContext, MergeFrame, MergeOutput, MergePass,
};
use dr_pano::bundle::Cameras;
use dr_pano::projection::{self, Projection};
use dr_pano::{Alignment, Gray};
@@ -109,11 +111,15 @@ impl MergeRequest {
/// never automatic).
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Decision {
Merge { projection: Option<Projection> },
Merge {
projection: Option<Projection>,
},
/// Draw the alignment again on another surface, and report it again.
/// The page's projection chips: what a choice looks like before it is
/// confirmed, at proxy cost rather than the merge's.
Preview { projection: Option<Projection> },
Preview {
projection: Option<Projection>,
},
Abandon,
}
@@ -221,7 +227,11 @@ fn run_inner(
graph.set_orientation(orientation);
graph.set_lens_profile(crate::develop::DevelopSession::profile_for(&meta));
let (w, h) = (raw.crop.width, raw.crop.height);
let size = if orientation.quarter_turns % 2 == 1 { (h, w) } else { (w, h) };
let size = if orientation.quarter_turns % 2 == 1 {
(h, w)
} else {
(w, h)
};
frames.push(Loaded {
raw,
meta,
@@ -255,8 +265,8 @@ fn run_inner(
// 3. Alignment.
let t = Instant::now();
progress("Aligning", 0, 1);
let alignment = dr_pano::align(&features, &dr_pano::AlignOptions::default())
.map_err(|e| e.to_string())?;
let alignment =
dr_pano::align(&features, &dr_pano::AlignOptions::default()).map_err(|e| e.to_string())?;
log::info!(
"merge: aligned in {:?}, focal {:.1} px, rms {:.2} px, {} links",
t.elapsed(),
@@ -294,9 +304,16 @@ fn run_inner(
let report = |projection: Projection| -> Result<AlignmentReport, String> {
let bounds = projection::bounds(projection, focal_full, &cameras, frame_size)
.ok_or("the frames project nowhere")?;
let preview = alignment
.is_complete()
.then(|| preview(&colour, &proxies, &alignment, &gains, projection, &frames[0].raw));
let preview = alignment.is_complete().then(|| {
preview(
&colour,
&proxies,
&alignment,
&gains,
projection,
&frames[0].raw,
)
});
Ok(AlignmentReport {
focal_mm: focal_full * 36.0 / full_long,
rms_px: alignment.rms_px,
@@ -316,7 +333,10 @@ fn run_inner(
.iter()
.map(|(k, why)| format!("{}: {why}", request.frames[*k].name))
.collect();
return Err(format!("not every frame could be placed — {}", names.join("; ")));
return Err(format!(
"not every frame could be placed — {}",
names.join("; ")
));
}
// Never automatic (FR-MRG-1): nothing is written until the alignment
@@ -400,7 +420,8 @@ fn run_inner(
// once the last strip is in, by which time every band has been seen.
let inscribed = Arc::new(std::sync::Mutex::new(dr_export::Inscribed::new(out_w)));
let inscribed_for_writer = inscribed.clone();
let file = std::fs::File::create(&out_path).map_err(|e| format!("{}: {e}", out_path.display()))?;
let file =
std::fs::File::create(&out_path).map_err(|e| format!("{}: {e}", out_path.display()))?;
let writer = std::thread::spawn(move || -> Result<(), String> {
let mut file = std::io::BufWriter::new(file);
dr_export::write_linear_dng(
@@ -411,9 +432,9 @@ fn run_inner(
&profile,
Some(&carried),
|_, buf| {
let band = band_rx
.recv()
.map_err(|_| dr_export::ExportError::Encode("the merge stopped early".into()))?;
let band = band_rx.recv().map_err(|_| {
dr_export::ExportError::Encode("the merge stopped early".into())
})?;
buf.extend_from_slice(&band);
Ok(())
},
@@ -458,7 +479,9 @@ fn run_inner(
|| cancel.is_cancelled(),
);
drop(band_tx);
let written = writer.join().unwrap_or_else(|_| Err("the writer panicked".into()));
let written = writer
.join()
.unwrap_or_else(|_| Err("the writer panicked".into()));
match merged {
Ok(()) => {}
Err(e) if cancel.is_cancelled() => {
@@ -475,7 +498,13 @@ fn run_inner(
cleanup(&out_path);
return Err(e);
}
log::info!("merge: {}×{} written to {} in {:?}", out_w, out_h, out_path.display(), t.elapsed());
log::info!(
"merge: {}×{} written to {} in {:?}",
out_w,
out_h,
out_path.display(),
t.elapsed()
);
// TRACES: FR-MRG-6
// Provenance, beside the composite: what it was merged from, in order,
@@ -547,7 +576,10 @@ fn camera_proxy(
.chunks_exact(4)
.map(|p| ((p[0] + p[1] + p[2]) / 3.0).clamp(0.0, 1.0).powf(1.0 / 2.2))
.collect();
let rgb: Vec<f32> = rgba.chunks_exact(4).flat_map(|p| [p[0], p[1], p[2]]).collect();
let rgb: Vec<f32> = rgba
.chunks_exact(4)
.flat_map(|p| [p[0], p[1], p[2]])
.collect();
Ok((
Gray {
width: rw as usize,
@@ -595,7 +627,9 @@ fn preview(
let mut sum = [0.0f32; 3];
let mut n = 0u32;
for (k, g) in proxies.iter().enumerate() {
let Some((x, y)) = cameras.project(k, d) else { continue };
let Some((x, y)) = cameras.project(k, d) else {
continue;
};
let (x, y) = (x + fw / 2.0, y + fh / 2.0);
if x < 0.0 || y < 0.0 || x >= fw - 1.0 || y >= fh - 1.0 {
continue;
@@ -611,7 +645,11 @@ fn preview(
out[o + 3] = 255;
continue;
}
let cam = [sum[0] / n as f32 * wb[0], sum[1] / n as f32 * wb[1], sum[2] / n as f32 * wb[2]];
let cam = [
sum[0] / n as f32 * wb[0],
sum[1] / n as f32 * wb[1],
sum[2] / n as f32 * wb[2],
];
for c in 0..3 {
let lin = m[c * 3] * cam[0] + m[c * 3 + 1] * cam[1] + m[c * 3 + 2] * cam[2];
out[o + c] = (lin.clamp(0.0, 1.0).powf(1.0 / 2.2) * 255.0) as u8;
@@ -637,7 +675,9 @@ fn preview(
(fw / 2.0 - 1.0, -fh / 2.0 + fh * t),
] {
let d = cameras.bearing(k, p);
let Some((u, v)) = projection.from_direction(scale, d) else { continue };
let Some((u, v)) = projection.from_direction(scale, d) else {
continue;
};
let x = ((u - bounds.min_u) / px) as i64;
let y = ((v - bounds.min_v) / px) as i64;
for (dx, dy) in [(0, 0), (1, 0), (0, 1), (1, 1)] {
@@ -677,7 +717,10 @@ fn field_of_view(cameras: &Cameras, frame: (f64, f64)) -> (f64, f64) {
}
fn report_size(b: &projection::Bounds) -> (u32, u32) {
(b.width().ceil().max(1.0) as u32, b.height().ceil().max(1.0) as u32)
(
b.width().ceil().max(1.0) as u32,
b.height().ceil().max(1.0) as u32,
)
}
fn describe(a: &Alignment) -> Vec<Result<(f64, f64), String>> {
@@ -722,7 +765,9 @@ fn gains(proxies: &[Gray], a: &Alignment) -> Vec<f32> {
for x in (0..proxies[l.i].width).step_by(step) {
let p = (x as f64 - w / 2.0, y as f64 - h / 2.0);
let d = cameras.bearing(l.i, p);
let Some((qx, qy)) = cameras.project(l.j, d) else { continue };
let Some((qx, qy)) = cameras.project(l.j, d) else {
continue;
};
let (qx, qy) = (qx + w / 2.0, qy + h / 2.0);
if qx < 0.0 || qy < 0.0 || qx >= w - 1.0 || qy >= h - 1.0 {
continue;
@@ -771,7 +816,11 @@ fn gains(proxies: &[Gray], a: &Alignment) -> Vec<f32> {
fn dng_profile(first: &Loaded, white_level: u32) -> dr_export::DngProfile {
let wb = first.raw.wb_coeffs;
let neutral_from_wb = [1.0 / wb[0].max(1e-3), 1.0 / wb[1].max(1e-3), 1.0 / wb[2].max(1e-3)];
let neutral_from_wb = [
1.0 / wb[0].max(1e-3),
1.0 / wb[1].max(1e-3),
1.0 / wb[2].max(1e-3),
];
let (calibrations, as_shot_neutral) = match &first.raw.profile {
Some(p) => (p.dng_calibrations(), p.neutral().unwrap_or(neutral_from_wb)),
None => (Vec::new(), neutral_from_wb),
+40 -9
View File
@@ -146,7 +146,9 @@ pub fn wire<S, C, F>(
// bytes, which keeps it free of the library.
let conn = context.conn.clone();
let fetch_names = names.clone();
let fetch = move |tx: &Sender<MergeEvent>, cancel: &Cancel| -> Option<Vec<MergeInput>> {
let fetch = move |tx: &Sender<MergeEvent>,
cancel: &Cancel|
-> Option<Vec<MergeInput>> {
let mut frames = Vec::with_capacity(sources.len());
for (i, (path, cache)) in sources.into_iter().enumerate() {
if cancel.is_cancelled() {
@@ -162,7 +164,10 @@ pub fn wire<S, C, F>(
let bytes = match rx.recv() {
Ok(Ok(bytes)) => bytes,
Ok(Err(e)) => {
let _ = tx.send(MergeEvent::Failed(format!("{}: {}", fetch_names[i], e.message)));
let _ = tx.send(MergeEvent::Failed(format!(
"{}: {}",
fetch_names[i], e.message
)));
return None;
}
Err(_) => {
@@ -197,9 +202,16 @@ pub fn wire<S, C, F>(
if let (Some(gpu), Some(first)) = (gpu_for_start.clone(), paths.first()) {
let names: Vec<String> = paths
.iter()
.map(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default())
.map(|p| {
p.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default()
})
.collect();
let dir = first.parent().map(std::path::Path::to_path_buf).unwrap_or_default();
let dir = first
.parent()
.map(std::path::Path::to_path_buf)
.unwrap_or_default();
let read_paths = paths.clone();
let fetch = move |tx: &Sender<MergeEvent>, _: &Cancel| -> Option<Vec<MergeInput>> {
let mut frames = Vec::new();
@@ -214,7 +226,15 @@ pub fn wire<S, C, F>(
}
Some(frames)
};
start(window, &ctl, gpu, names, MergeDestination::Local(dir), fetch, &on_done);
start(
window,
&ctl,
gpu,
names,
MergeDestination::Local(dir),
fetch,
&on_done,
);
}
}
@@ -309,7 +329,9 @@ fn start<Fetch>(
let cancel = cancel.clone();
let tx = tx.clone();
std::thread::spawn(move || {
let Some(frames) = fetch(&tx, &cancel) else { return };
let Some(frames) = fetch(&tx, &cancel) else {
return;
};
let request = MergeRequest::new(frames, destination);
merge::run(gpu, request, tx, decision, cancel);
});
@@ -334,7 +356,9 @@ fn start<Fetch>(
window.set_merge_aligned(false);
window.set_merge_complete(false);
window.set_merge_summary("".into());
window.set_merge_frames(slint::ModelRc::new(slint::VecModel::from(Vec::<MergeFrameRow>::new())));
window.set_merge_frames(slint::ModelRc::new(slint::VecModel::from(Vec::<
MergeFrameRow,
>::new())));
window.set_merge_preview(slint::Image::default());
window.set_merge_projection_selected(0);
window.set_merge_done(false);
@@ -379,7 +403,11 @@ fn drain(window: &AppWindow, ctl: &Rc<MergeController>, on_done: &Rc<impl Fn(&Ap
MergeEvent::Progress { stage, done, total } => {
window.set_merge_stage(stage.into());
window.set_merge_running(true);
window.set_merge_fraction(if total > 0 { done as f32 / total as f32 } else { 0.0 });
window.set_merge_fraction(if total > 0 {
done as f32 / total as f32
} else {
0.0
});
if let Some(job) = ctl.job.borrow().as_ref() {
job.activity.detail(stage.to_lowercase());
job.activity.progress(done, total.max(1));
@@ -463,7 +491,10 @@ fn show_report(window: &AppWindow, report: &AlignmentReport, names: &[String]) {
.iter()
.enumerate()
.map(|(k, f)| {
let name = names.get(k).cloned().unwrap_or_else(|| format!("frame {k}"));
let name = names
.get(k)
.cloned()
.unwrap_or_else(|| format!("frame {k}"));
match f {
Ok((yaw, pitch)) => MergeFrameRow {
name: name.into(),