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:
@@ -71,7 +71,7 @@ fn main() {
|
||||
// The pixel at (1, 0) was written as (1000, 2000, 3000): if the samples
|
||||
// come back interleaved in that order, cpp == 3 means what it says.
|
||||
if let rawler::RawImageData::Integer(v) = &image.data {
|
||||
let i = image.cpp * 1;
|
||||
let i = image.cpp;
|
||||
println!(" pixel (1,0) = {:?}", &v[i..i + image.cpp.min(3)]);
|
||||
}
|
||||
|
||||
@@ -167,6 +167,7 @@ fn write_linear_dng(w: u32, h: u32) -> Vec<u8> {
|
||||
entries.push((50707, 1, 4, [1, 4, 0, 0])); // DNGBackwardVersion
|
||||
entries.push(ascii(&mut extra, extra_off, 50708, "DarkRoom Panorama")); // UniqueCameraModel
|
||||
entries.push(long(50717, 65535)); // WhiteLevel
|
||||
|
||||
// ColorMatrix1: XYZ → camera, 9 SRATIONALs. A plausible sRGB-ish matrix
|
||||
// (the inverse of the sRGB D65 primaries), scaled to integers.
|
||||
{
|
||||
|
||||
@@ -567,7 +567,11 @@ fn illuminant_code(temperature: f32) -> u16 {
|
||||
];
|
||||
TABLE
|
||||
.iter()
|
||||
.min_by(|a, b| (a.0 - temperature).abs().total_cmp(&(b.0 - temperature).abs()))
|
||||
.min_by(|a, b| {
|
||||
(a.0 - temperature)
|
||||
.abs()
|
||||
.total_cmp(&(b.0 - temperature).abs())
|
||||
})
|
||||
.map(|(_, code)| *code)
|
||||
.unwrap_or(255)
|
||||
}
|
||||
|
||||
@@ -65,6 +65,10 @@ pub struct DngProfile {
|
||||
/// rectangle the frames covered, found while the strips went by
|
||||
/// (`Inscribed`) — becomes `DefaultCropOrigin`/`DefaultCropSize`
|
||||
/// (FR-MRG-4): the file opens on the picture, and the border is still in it.
|
||||
// Eight arguments, and each is a different thing: the sink, three
|
||||
// dimensions, the profile, the header, the strip source and the crop. A
|
||||
// struct for them would be a struct with one caller.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn write_linear_dng<W, F, C>(
|
||||
out: W,
|
||||
width: u32,
|
||||
@@ -119,7 +123,10 @@ where
|
||||
.map_err(enc)?;
|
||||
image
|
||||
.encoder()
|
||||
.write_tag(Tag::Unknown(tag::DEFAULT_CROP_SIZE), &[r.width, r.height][..])
|
||||
.write_tag(
|
||||
Tag::Unknown(tag::DEFAULT_CROP_SIZE),
|
||||
&[r.width, r.height][..],
|
||||
)
|
||||
.map_err(enc)?;
|
||||
}
|
||||
image.finish().map_err(enc)
|
||||
@@ -136,8 +143,14 @@ where
|
||||
dir.write_tag(Tag::Orientation, 1u16)?;
|
||||
dir.write_tag(Tag::Unknown(tag::DNG_VERSION), &[1u8, 4, 0, 0][..])?;
|
||||
dir.write_tag(Tag::Unknown(tag::DNG_BACKWARD_VERSION), &[1u8, 4, 0, 0][..])?;
|
||||
dir.write_tag(Tag::Unknown(tag::UNIQUE_CAMERA_MODEL), Ascii(&profile.unique_model))?;
|
||||
dir.write_tag(Tag::Unknown(tag::WHITE_LEVEL), &[profile.white_level; 3][..])?;
|
||||
dir.write_tag(
|
||||
Tag::Unknown(tag::UNIQUE_CAMERA_MODEL),
|
||||
Ascii(&profile.unique_model),
|
||||
)?;
|
||||
dir.write_tag(
|
||||
Tag::Unknown(tag::WHITE_LEVEL),
|
||||
&[profile.white_level; 3][..],
|
||||
)?;
|
||||
dir.write_tag(Tag::Unknown(tag::BLACK_LEVEL), &[0u32; 3][..])?;
|
||||
|
||||
for (slot, (illuminant, matrix)) in profile.calibrations.iter().take(2).enumerate() {
|
||||
@@ -281,7 +294,7 @@ mod tests {
|
||||
let i = (12 * 20 + 19) * 3;
|
||||
assert_eq!(data[i], (19 + 12 * 20) as u16);
|
||||
// The profile came through as the camera's.
|
||||
assert!(image.camera.color_matrix.len() >= 1);
|
||||
assert!(!image.camera.color_matrix.is_empty());
|
||||
assert_eq!(image.model, "Canon EOS 6D");
|
||||
// The default crop is what the decoder reports as the picture.
|
||||
let crop = image.crop_area.expect("a crop");
|
||||
|
||||
@@ -126,12 +126,7 @@ mod tests {
|
||||
fn ragged_edges_are_cut_off() {
|
||||
// A cylinder's footprint: narrower at top and bottom.
|
||||
let r = from_art(&[
|
||||
"..####..",
|
||||
".######.",
|
||||
"########",
|
||||
"########",
|
||||
".######.",
|
||||
"..####..",
|
||||
"..####..", ".######.", "########", "########", ".######.", "..####..",
|
||||
]);
|
||||
// 6 wide × 4 tall = 24 beats 8 × 2 = 16 and 4 × 6 = 24 ties; the
|
||||
// first found wins a tie, which is the wider one here.
|
||||
|
||||
@@ -26,18 +26,18 @@ use dr_types::{ColourSpace, ExportFormat, ExportSettings};
|
||||
|
||||
mod dng;
|
||||
mod encode;
|
||||
mod inscribed;
|
||||
mod error;
|
||||
mod exif;
|
||||
pub mod icc;
|
||||
mod inscribed;
|
||||
mod metadata;
|
||||
mod name;
|
||||
mod sharpen;
|
||||
mod size;
|
||||
|
||||
pub use dng::{write_linear_dng, DngProfile};
|
||||
pub use inscribed::{Inscribed, Rect};
|
||||
pub use error::ExportError;
|
||||
pub use inscribed::{Inscribed, Rect};
|
||||
pub use metadata::SourceMetadata;
|
||||
pub use name::{resolve_name, NameContext};
|
||||
pub use size::target_size;
|
||||
|
||||
@@ -465,7 +465,7 @@ pub fn eye_box(contour: &[(f32, f32)]) -> Option<(f32, f32, f32, f32)> {
|
||||
y1 = y1.max(y);
|
||||
}
|
||||
let w = x1 - x0;
|
||||
if contour.is_empty() || !(w > 0.0) {
|
||||
if contour.is_empty() || w <= 0.0 || w.is_nan() {
|
||||
return None;
|
||||
}
|
||||
let h = (y1 - y0).max(w * EYE_BOX_MIN_ASPECT);
|
||||
|
||||
@@ -38,10 +38,10 @@ pub use adjust::AdjustPass;
|
||||
// unclipped, full internal precision (FR-DEV-2), and anyone reasoning about
|
||||
// VRAM at 24 MP needs to know what an intermediate costs.
|
||||
pub use demosaic::{DemosaicedImage, Demosaicer};
|
||||
pub use merge::{Band, MergeFrame, MergeOutput, MergePass};
|
||||
pub use detail::INTERMEDIATE_FORMAT as DETAIL_INTERMEDIATE_FORMAT;
|
||||
pub use error::GpuError;
|
||||
pub use focus::{FocusPeakPass, FocusPeaking, PeakColour, PeakSensitivity};
|
||||
pub use merge::{Band, MergeFrame, MergeOutput, MergePass};
|
||||
// Renamed on the way out: `BINS` says enough inside `histogram`, and nothing
|
||||
// at all at a crate root shared with demosaic and segmentation.
|
||||
pub use histogram::{Histogram, HistogramPass, BINS as HISTOGRAM_BINS};
|
||||
|
||||
@@ -179,12 +179,12 @@ impl MergePass {
|
||||
storage(2, false),
|
||||
],
|
||||
});
|
||||
let resolve_layout = ctx
|
||||
.device
|
||||
.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("merge-resolve-bgl"),
|
||||
entries: &[uniform(0), storage(1, true), storage(2, false)],
|
||||
});
|
||||
let resolve_layout =
|
||||
ctx.device
|
||||
.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("merge-resolve-bgl"),
|
||||
entries: &[uniform(0), storage(1, true), storage(2, false)],
|
||||
});
|
||||
let pipeline = |name: &str, layout: &wgpu::BindGroupLayout| {
|
||||
let pl = ctx
|
||||
.device
|
||||
@@ -418,7 +418,12 @@ impl MergePass {
|
||||
self.ctx.queue.submit(Some(enc.finish()));
|
||||
}
|
||||
|
||||
fn resolve_chunk(&mut self, chunk: (u32, u32), scale: f32, out: &mut Vec<u32>) -> Result<(), GpuError> {
|
||||
fn resolve_chunk(
|
||||
&mut self,
|
||||
chunk: (u32, u32),
|
||||
scale: f32,
|
||||
out: &mut Vec<u32>,
|
||||
) -> Result<(), GpuError> {
|
||||
let params = ResolveParams {
|
||||
chunk_size: [chunk.0, chunk.1],
|
||||
scale,
|
||||
@@ -522,7 +527,10 @@ fn source_rect(
|
||||
// The interior too, coarsely: a chunk can contain a frame entirely.
|
||||
for i in 1..4 {
|
||||
for j in 1..4 {
|
||||
visit(origin.0 + w * f64::from(i) / 4.0, origin.1 + h * f64::from(j) / 4.0);
|
||||
visit(
|
||||
origin.0 + w * f64::from(i) / 4.0,
|
||||
origin.1 + h * f64::from(j) / 4.0,
|
||||
);
|
||||
}
|
||||
}
|
||||
if !any {
|
||||
|
||||
@@ -32,11 +32,12 @@ fn main() {
|
||||
eprintln!("usage: align [out-prefix] <frame>...");
|
||||
std::process::exit(2);
|
||||
}
|
||||
let prefix = if args[0].ends_with(".CR2") || args[0].ends_with(".dng") || args[0].ends_with(".jpg") {
|
||||
"align".to_string()
|
||||
} else {
|
||||
args.remove(0)
|
||||
};
|
||||
let prefix =
|
||||
if args[0].ends_with(".CR2") || args[0].ends_with(".dng") || args[0].ends_with(".jpg") {
|
||||
"align".to_string()
|
||||
} else {
|
||||
args.remove(0)
|
||||
};
|
||||
let paths: Vec<PathBuf> = args.iter().map(PathBuf::from).collect();
|
||||
|
||||
// Previews, oriented, at proxy size.
|
||||
@@ -46,17 +47,25 @@ fn main() {
|
||||
let bytes = std::fs::read(p).expect("read");
|
||||
let preview = dr_decode::extract_preview(&bytes, dr_decode::PreviewSize::Full)
|
||||
.expect("embedded preview");
|
||||
let orientation = dr_decode::orientation(&bytes[..bytes.len().min(dr_decode::HEADER_BYTES as usize)])
|
||||
.unwrap_or(dr_types::Orientation::NORMAL);
|
||||
let orientation =
|
||||
dr_decode::orientation(&bytes[..bytes.len().min(dr_decode::HEADER_BYTES as usize)])
|
||||
.unwrap_or(dr_types::Orientation::NORMAL);
|
||||
let tag = match orientation.quarter_turns {
|
||||
1 => 6,
|
||||
2 => 3,
|
||||
3 => 8,
|
||||
_ => 1,
|
||||
};
|
||||
let gray = Gray::from_rgba8(&preview.rgba, preview.width as usize, preview.height as usize)
|
||||
.oriented(tag);
|
||||
let (fitted, _) = gray.fitted(dr_pano::xfeat::INPUT_LONG_EDGE, dr_pano::xfeat::INPUT_LONG_EDGE);
|
||||
let gray = Gray::from_rgba8(
|
||||
&preview.rgba,
|
||||
preview.width as usize,
|
||||
preview.height as usize,
|
||||
)
|
||||
.oriented(tag);
|
||||
let (fitted, _) = gray.fitted(
|
||||
dr_pano::xfeat::INPUT_LONG_EDGE,
|
||||
dr_pano::xfeat::INPUT_LONG_EDGE,
|
||||
);
|
||||
println!(
|
||||
"{:<14} preview {}×{} orientation {} → proxy {}×{}",
|
||||
p.file_name().unwrap().to_string_lossy(),
|
||||
@@ -80,7 +89,11 @@ fn main() {
|
||||
for (i, f) in features.iter().enumerate() {
|
||||
println!("frame {i}: {} keypoints", f.len());
|
||||
}
|
||||
println!("detection in {:?} ({:?} per frame)", t.elapsed(), t.elapsed() / proxies.len() as u32);
|
||||
println!(
|
||||
"detection in {:?} ({:?} per frame)",
|
||||
t.elapsed(),
|
||||
t.elapsed() / proxies.len() as u32
|
||||
);
|
||||
|
||||
// Alignment.
|
||||
let t = Instant::now();
|
||||
@@ -95,7 +108,10 @@ fn main() {
|
||||
alignment.rms_px
|
||||
);
|
||||
for l in &alignment.links {
|
||||
println!(" link {}–{}: {} inliers of {} matches", l.i, l.j, l.inliers, l.matches);
|
||||
println!(
|
||||
" link {}–{}: {} inliers of {} matches",
|
||||
l.i, l.j, l.inliers, l.matches
|
||||
);
|
||||
}
|
||||
for (k, why) in &alignment.unaligned {
|
||||
println!(" UNALIGNED frame {k}: {why}");
|
||||
@@ -143,7 +159,9 @@ fn main() {
|
||||
let v = bounds.min_v + (oy as f64 + 0.5) * px;
|
||||
let d = Projection::Cylindrical.to_direction(scale, u, v);
|
||||
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 + g.width as f64 / 2.0, y + g.height as f64 / 2.0);
|
||||
if x < 0.0 || y < 0.0 || x >= g.width as f64 - 1.0 || y >= g.height as f64 - 1.0 {
|
||||
continue;
|
||||
|
||||
+39
-16
@@ -115,7 +115,11 @@ impl Alignment {
|
||||
/// when [`Self::is_complete`].
|
||||
pub fn cameras(&self) -> Cameras {
|
||||
Cameras {
|
||||
rotations: self.rotations.iter().map(|r| r.unwrap_or(Mat3::IDENTITY)).collect(),
|
||||
rotations: self
|
||||
.rotations
|
||||
.iter()
|
||||
.map(|r| r.unwrap_or(Mat3::IDENTITY))
|
||||
.collect(),
|
||||
focal: self.focal,
|
||||
}
|
||||
}
|
||||
@@ -130,7 +134,9 @@ impl Alignment {
|
||||
pub fn align(frames: &[Features], opts: &AlignOptions) -> Result<Alignment, PanoError> {
|
||||
let n = frames.len();
|
||||
if n < 2 {
|
||||
return Err(PanoError::Input("a panorama needs at least two frames".into()));
|
||||
return Err(PanoError::Input(
|
||||
"a panorama needs at least two frames".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let centre = |k: usize, i: usize| -> (f64, f64) {
|
||||
@@ -177,10 +183,7 @@ pub fn align(frames: &[Features], opts: &AlignOptions) -> Result<Alignment, Pano
|
||||
continue;
|
||||
};
|
||||
let needed = (8.0 + 0.3 * matches.len() as f64).ceil() as usize;
|
||||
log::debug!(
|
||||
"pair {i}-{j}: {} inliers, {needed} needed",
|
||||
inliers.len()
|
||||
);
|
||||
log::debug!("pair {i}-{j}: {} inliers, {needed} needed", inliers.len());
|
||||
if inliers.len() <= needed || inliers.len() < opts.min_inliers {
|
||||
continue;
|
||||
}
|
||||
@@ -218,7 +221,10 @@ pub fn align(frames: &[Features], opts: &AlignOptions) -> Result<Alignment, Pano
|
||||
.filter_map(|l| homography::focal_from_homography(&l.h))
|
||||
.filter(|f| f.is_finite() && *f > 0.0)
|
||||
.collect();
|
||||
let longest = frames.iter().map(|f| f.width.max(f.height) as f64).fold(0.0, f64::max);
|
||||
let longest = frames
|
||||
.iter()
|
||||
.map(|f| f.width.max(f.height) as f64)
|
||||
.fold(0.0, f64::max);
|
||||
let focal = if !estimates.is_empty() {
|
||||
estimates.sort_by(f64::total_cmp);
|
||||
let median = estimates[estimates.len() / 2];
|
||||
@@ -236,10 +242,10 @@ pub fn align(frames: &[Features], opts: &AlignOptions) -> Result<Alignment, Pano
|
||||
let mut rotations: Vec<Option<Mat3>> = vec![None; n];
|
||||
let mut unaligned = Vec::new();
|
||||
if links.is_empty() {
|
||||
for k in 0..n {
|
||||
for (k, &matched) in matched_any.iter().enumerate() {
|
||||
unaligned.push((
|
||||
k,
|
||||
if matched_any[k] {
|
||||
if matched {
|
||||
Unaligned::NoOverlap
|
||||
} else {
|
||||
Unaligned::NoMatches
|
||||
@@ -341,7 +347,13 @@ mod tests {
|
||||
|
||||
/// Frames of a synthetic sweep: world directions with random unit
|
||||
/// descriptors, each frame seeing the ones in its field of view.
|
||||
fn synthetic_sweep(n: usize, step: f64, f: f64, w: usize, h: usize) -> (Vec<Features>, Cameras) {
|
||||
fn synthetic_sweep(
|
||||
n: usize,
|
||||
step: f64,
|
||||
f: f64,
|
||||
w: usize,
|
||||
h: usize,
|
||||
) -> (Vec<Features>, Cameras) {
|
||||
let mut seed = 777u64;
|
||||
let mut rnd = || {
|
||||
seed = seed
|
||||
@@ -355,7 +367,10 @@ mod tests {
|
||||
* Mat3::rotation(Vec3::new(1.0, 0.0, 0.0), 0.02 * ((k % 3) as f64 - 1.0))
|
||||
})
|
||||
.collect();
|
||||
let truth = Cameras { rotations, focal: f };
|
||||
let truth = Cameras {
|
||||
rotations,
|
||||
focal: f,
|
||||
};
|
||||
let total = step * (n as f64 - 1.0);
|
||||
let mut frames: Vec<Features> = (0..n)
|
||||
.map(|_| Features {
|
||||
@@ -368,20 +383,24 @@ mod tests {
|
||||
for _ in 0..600 * n {
|
||||
let yaw = rnd() * (total + 0.8) + total / 2.0;
|
||||
let pitch = rnd() * 0.5;
|
||||
let d = Vec3::new(yaw.sin() * pitch.cos(), pitch.sin(), yaw.cos() * pitch.cos());
|
||||
let d = Vec3::new(
|
||||
yaw.sin() * pitch.cos(),
|
||||
pitch.sin(),
|
||||
yaw.cos() * pitch.cos(),
|
||||
);
|
||||
let desc: Vec<f32> = (0..DESCRIPTOR_LEN).map(|_| rnd() as f32).collect();
|
||||
let norm = desc.iter().map(|v| v * v).sum::<f32>().sqrt();
|
||||
let desc: Vec<f32> = desc.iter().map(|v| v / norm).collect();
|
||||
for k in 0..n {
|
||||
for (k, frame) in frames.iter_mut().enumerate() {
|
||||
if let Some(p) = truth.project(k, d) {
|
||||
let (x, y) = (p.0 + w as f64 / 2.0, p.1 + h as f64 / 2.0);
|
||||
if x >= 0.0 && x < w as f64 && y >= 0.0 && y < h as f64 {
|
||||
frames[k].keypoints.push(Keypoint {
|
||||
frame.keypoints.push(Keypoint {
|
||||
x: (x + rnd() * 0.6) as f32,
|
||||
y: (y + rnd() * 0.6) as f32,
|
||||
score: 1.0,
|
||||
});
|
||||
frames[k].descriptors.extend_from_slice(&desc);
|
||||
frame.descriptors.extend_from_slice(&desc);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -402,7 +421,11 @@ mod tests {
|
||||
assert!((out.focal - 1400.0).abs() < 15.0, "focal {}", out.focal);
|
||||
assert!(out.rms_px < 1.0, "rms {}", out.rms_px);
|
||||
// Relative rotations match the truth's, whichever frame is the root.
|
||||
let root = out.rotations.iter().position(|r| *r == Some(Mat3::IDENTITY)).unwrap();
|
||||
let root = out
|
||||
.rotations
|
||||
.iter()
|
||||
.position(|r| *r == Some(Mat3::IDENTITY))
|
||||
.unwrap();
|
||||
for k in 0..6 {
|
||||
let rel_truth = truth.rotations[root].transpose() * truth.rotations[k];
|
||||
let rel_out = out.rotations[k].unwrap();
|
||||
|
||||
@@ -325,7 +325,10 @@ mod tests {
|
||||
* Mat3::rotation(Vec3::new(0.0, 0.0, 1.0), roll);
|
||||
rotations.push(r);
|
||||
}
|
||||
let truth = Cameras { rotations, focal: f };
|
||||
let truth = Cameras {
|
||||
rotations,
|
||||
focal: f,
|
||||
};
|
||||
|
||||
// World directions: a fan across the whole sweep.
|
||||
let mut obs = Vec::new();
|
||||
@@ -340,8 +343,12 @@ mod tests {
|
||||
for _ in 0..400 * n {
|
||||
let yaw = rnd() * (total + 0.8) + total / 2.0;
|
||||
let pitch = rnd() * 0.5;
|
||||
let d = Vec3::new(yaw.sin() * pitch.cos(), pitch.sin(), yaw.cos() * pitch.cos())
|
||||
.normalised();
|
||||
let d = Vec3::new(
|
||||
yaw.sin() * pitch.cos(),
|
||||
pitch.sin(),
|
||||
yaw.cos() * pitch.cos(),
|
||||
)
|
||||
.normalised();
|
||||
// Visible in which frames? Within ±0.35 f of centre.
|
||||
let mut seen: Vec<(usize, Point)> = Vec::new();
|
||||
for k in 0..n {
|
||||
@@ -383,7 +390,11 @@ mod tests {
|
||||
let before = rms(&start, &obs);
|
||||
let out = adjust(start, &obs, &AdjustOptions::default()).expect("solvable");
|
||||
assert!(out.rms_px < 1e-3, "rms {} (was {before})", out.rms_px);
|
||||
assert!((out.cameras.focal - 1400.0).abs() < 0.5, "focal {}", out.cameras.focal);
|
||||
assert!(
|
||||
(out.cameras.focal - 1400.0).abs() < 0.5,
|
||||
"focal {}",
|
||||
out.cameras.focal
|
||||
);
|
||||
for k in 0..6 {
|
||||
let err = angle_between(out.cameras.rotations[k], truth.rotations[k]);
|
||||
assert!(err < 1e-5, "frame {k} off by {err} rad");
|
||||
@@ -395,7 +406,8 @@ mod tests {
|
||||
let (truth, obs) = sweep(8, 0.25, 1400.0, 1.0);
|
||||
let mut start = truth.clone();
|
||||
for k in 1..8 {
|
||||
start.rotations[k] = Mat3::exp(Vec3::new(0.0, 0.004 * k as f64, 0.0)) * start.rotations[k];
|
||||
start.rotations[k] =
|
||||
Mat3::exp(Vec3::new(0.0, 0.004 * k as f64, 0.0)) * start.rotations[k];
|
||||
}
|
||||
let out = adjust(start, &obs, &AdjustOptions::default()).expect("solvable");
|
||||
// ±0.5 px of uniform noise on every coordinate has an RMS of 0.41 px
|
||||
|
||||
@@ -108,9 +108,7 @@ pub fn ransac_homography(
|
||||
let Some(h) = dlt(&sample.map(|k| pairs[k])) else {
|
||||
continue;
|
||||
};
|
||||
let inliers: Vec<usize> = (0..n)
|
||||
.filter(|&k| agrees(&h, pairs[k], thr2))
|
||||
.collect();
|
||||
let inliers: Vec<usize> = (0..n).filter(|&k| agrees(&h, pairs[k], thr2)).collect();
|
||||
if best.as_ref().is_none_or(|(b, _)| inliers.len() > b.len()) {
|
||||
// Adapt: enough iterations to have drawn one all-inlier sample
|
||||
// with probability 0.99, given the ratio seen so far.
|
||||
@@ -333,7 +331,11 @@ mod tests {
|
||||
pairs.push((p, ((rng.below(1000) as f64 - 500.0) / 1000.0, 0.1)));
|
||||
}
|
||||
let robust = ransac_homography(&pairs, 0.003, 500, 3).expect("found");
|
||||
assert!(robust.inliers.len() >= 55, "{} inliers", robust.inliers.len());
|
||||
assert!(
|
||||
robust.inliers.len() >= 55,
|
||||
"{} inliers",
|
||||
robust.inliers.len()
|
||||
);
|
||||
assert!(robust.inliers.iter().all(|&k| k < 60));
|
||||
}
|
||||
|
||||
@@ -351,9 +353,9 @@ mod tests {
|
||||
let est = focal_from_homography(&h).expect("estimable");
|
||||
assert!((est - f).abs() / f < 1e-6, "{est}");
|
||||
let back = rotation_from_homography(&h, f);
|
||||
for i in 0..3 {
|
||||
for j in 0..3 {
|
||||
assert!((back.0[i][j] - m[i][j]).abs() < 1e-9);
|
||||
for (row, truth) in back.0.iter().zip(&m) {
|
||||
for (a, b) in row.iter().zip(truth) {
|
||||
assert!((a - b).abs() < 1e-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,10 @@ impl Gray {
|
||||
/// frames because they were converted differently.
|
||||
pub fn from_rgba8(rgba: &[u8], width: usize, height: usize) -> Gray {
|
||||
let n = width * height;
|
||||
assert!(rgba.len() >= n * 4, "rgba buffer is short for {width}×{height}");
|
||||
assert!(
|
||||
rgba.len() >= n * 4,
|
||||
"rgba buffer is short for {width}×{height}"
|
||||
);
|
||||
let data = rgba[..n * 4]
|
||||
.chunks_exact(4)
|
||||
.map(|p| {
|
||||
|
||||
@@ -339,7 +339,12 @@ mod tests {
|
||||
#[test]
|
||||
fn cholesky_solves_a_small_spd_system() {
|
||||
// A = Bᵀ B for a random-ish B is SPD by construction.
|
||||
let b = [[2.0, 1.0, 0.0], [1.0, 3.0, 1.0], [0.0, 1.0, 4.0], [1.0, 1.0, 1.0]];
|
||||
let b = [
|
||||
[2.0, 1.0, 0.0],
|
||||
[1.0, 3.0, 1.0],
|
||||
[0.0, 1.0, 4.0],
|
||||
[1.0, 1.0, 1.0],
|
||||
];
|
||||
let mut a = DMat::zeros(3);
|
||||
for i in 0..3 {
|
||||
for j in 0..3 {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
use crate::features::{Features, DESCRIPTOR_LEN};
|
||||
|
||||
const _: () = assert!(DESCRIPTOR_LEN % 8 == 0);
|
||||
const _: () = assert!(DESCRIPTOR_LEN.is_multiple_of(8));
|
||||
|
||||
/// A correspondence: keypoint `a` in the first image matches keypoint `b`
|
||||
/// in the second, with the cosine similarity of their descriptors.
|
||||
@@ -64,9 +64,10 @@ pub fn match_features(a: &Features, b: &Features, min_similarity: f32) -> Vec<Ma
|
||||
let best_ab: Vec<(usize, f32)> = sim
|
||||
.chunks_exact(nb)
|
||||
.map(|row| {
|
||||
row.iter()
|
||||
.enumerate()
|
||||
.fold((0usize, f32::MIN), |acc, (j, &s)| if s > acc.1 { (j, s) } else { acc })
|
||||
row.iter().enumerate().fold(
|
||||
(0usize, f32::MIN),
|
||||
|acc, (j, &s)| if s > acc.1 { (j, s) } else { acc },
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let mut best_ba = vec![(0usize, f32::MIN); nb];
|
||||
|
||||
@@ -151,18 +151,29 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn to_and_from_direction_are_inverses() {
|
||||
for proj in [Projection::Perspective, Projection::Cylindrical, Projection::Spherical] {
|
||||
for proj in [
|
||||
Projection::Perspective,
|
||||
Projection::Cylindrical,
|
||||
Projection::Spherical,
|
||||
] {
|
||||
for (u, v) in [(0.0, 0.0), (300.0, -200.0), (-900.0, 450.0)] {
|
||||
let d = proj.to_direction(1000.0, u, v);
|
||||
let (bu, bv) = proj.from_direction(1000.0, d).expect("in front");
|
||||
assert!((bu - u).abs() < 1e-9 && (bv - v).abs() < 1e-9, "{proj:?} {u} {v}");
|
||||
assert!(
|
||||
(bu - u).abs() < 1e-9 && (bv - v).abs() < 1e-9,
|
||||
"{proj:?} {u} {v}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_origin_looks_down_z_in_every_projection() {
|
||||
for proj in [Projection::Perspective, Projection::Cylindrical, Projection::Spherical] {
|
||||
for proj in [
|
||||
Projection::Perspective,
|
||||
Projection::Cylindrical,
|
||||
Projection::Spherical,
|
||||
] {
|
||||
let d = proj.to_direction(500.0, 0.0, 0.0);
|
||||
assert!((d.z() - 1.0).abs() < 1e-12);
|
||||
}
|
||||
|
||||
@@ -43,7 +43,10 @@ impl XFeat {
|
||||
}
|
||||
|
||||
/// From the two exports on disk.
|
||||
pub fn from_paths(landscape: &std::path::Path, portrait: &std::path::Path) -> Result<Self, PanoError> {
|
||||
pub fn from_paths(
|
||||
landscape: &std::path::Path,
|
||||
portrait: &std::path::Path,
|
||||
) -> Result<Self, PanoError> {
|
||||
let l = std::fs::read(landscape).map_err(PanoError::ModelRead)?;
|
||||
let p = std::fs::read(portrait).map_err(PanoError::ModelRead)?;
|
||||
Self::from_bytes(&l, &p)
|
||||
@@ -80,8 +83,9 @@ impl XFeat {
|
||||
let (fitted, scale) = image.fitted(in_w, in_h);
|
||||
let padded = fitted.padded(in_w, in_h);
|
||||
|
||||
let input = ndarray::Array::from_shape_vec(ndarray::IxDyn(&[1, 1, in_h, in_w]), padded.data)
|
||||
.expect("shape matches the buffer by construction");
|
||||
let input =
|
||||
ndarray::Array::from_shape_vec(ndarray::IxDyn(&[1, 1, in_h, in_w]), padded.data)
|
||||
.expect("shape matches the buffer by construction");
|
||||
let tensor = ort::value::Tensor::from_array(input).map_err(PanoError::Inference)?;
|
||||
let outputs = session
|
||||
.run(ort::inputs![tensor])
|
||||
|
||||
@@ -2243,8 +2243,14 @@ mod tests {
|
||||
merge = panorama cylindrical 49.7mm 2 frames\n\n[version u1]\nname = Default\n\
|
||||
revision = 1\nmodified = 0\n";
|
||||
let sidecar = Sidecar::parse(text).expect("parses");
|
||||
assert_eq!(sidecar.derived_from, vec!["2025/_MG_8320.CR2", "2025/_MG_8321.CR2"]);
|
||||
assert_eq!(sidecar.merge.as_deref(), Some("panorama cylindrical 49.7mm 2 frames"));
|
||||
assert_eq!(
|
||||
sidecar.derived_from,
|
||||
vec!["2025/_MG_8320.CR2", "2025/_MG_8321.CR2"]
|
||||
);
|
||||
assert_eq!(
|
||||
sidecar.merge.as_deref(),
|
||||
Some("panorama cylindrical 49.7mm 2 frames")
|
||||
);
|
||||
let out = sidecar.to_text();
|
||||
assert!(out.starts_with("drsc 1\nderived_from = 2025/_MG_8320.CR2\n"));
|
||||
assert_eq!(Sidecar::parse(&out).expect("re-parses"), sidecar);
|
||||
|
||||
@@ -24,7 +24,11 @@ fn main() {
|
||||
};
|
||||
let shape: Vec<usize> = args
|
||||
.next()
|
||||
.map(|s| s.split('x').map(|d| d.parse().expect("dimension")).collect())
|
||||
.map(|s| {
|
||||
s.split('x')
|
||||
.map(|d| d.parse().expect("dimension"))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_else(|| vec![1, 1, 768, 1024]);
|
||||
|
||||
let bytes = std::fs::read(&path).expect("read model");
|
||||
@@ -32,13 +36,14 @@ fn main() {
|
||||
|
||||
let _ = ort::set_api(ort_tract::api());
|
||||
let t = Instant::now();
|
||||
let mut session = match ort::session::Session::builder().and_then(|mut b| b.commit_from_memory(&bytes)) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
println!("FAIL load: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
let mut session =
|
||||
match ort::session::Session::builder().and_then(|mut b| b.commit_from_memory(&bytes)) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
println!("FAIL load: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
println!("ok loaded in {:?}", t.elapsed());
|
||||
for i in session.inputs().iter() {
|
||||
println!(" input {} {:?}", i.name(), i.dtype());
|
||||
|
||||
@@ -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}");
|
||||
|
||||
@@ -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
@@ -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),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user