diff --git a/core/dr-decode/examples/linear_dng.rs b/core/dr-decode/examples/linear_dng.rs index 922d93c..9fd1b21 100644 --- a/core/dr-decode/examples/linear_dng.rs +++ b/core/dr-decode/examples/linear_dng.rs @@ -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 { 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. { diff --git a/core/dr-decode/src/profile.rs b/core/dr-decode/src/profile.rs index 2235c04..bf45372 100644 --- a/core/dr-decode/src/profile.rs +++ b/core/dr-decode/src/profile.rs @@ -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) } diff --git a/core/dr-export/src/dng.rs b/core/dr-export/src/dng.rs index ce2df92..a867e4a 100644 --- a/core/dr-export/src/dng.rs +++ b/core/dr-export/src/dng.rs @@ -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( 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"); diff --git a/core/dr-export/src/inscribed.rs b/core/dr-export/src/inscribed.rs index 8f43129..54670c3 100644 --- a/core/dr-export/src/inscribed.rs +++ b/core/dr-export/src/inscribed.rs @@ -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. diff --git a/core/dr-export/src/lib.rs b/core/dr-export/src/lib.rs index 95056d5..87b2920 100644 --- a/core/dr-export/src/lib.rs +++ b/core/dr-export/src/lib.rs @@ -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; diff --git a/core/dr-face/src/align.rs b/core/dr-face/src/align.rs index c0add26..231ce37 100644 --- a/core/dr-face/src/align.rs +++ b/core/dr-face/src/align.rs @@ -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); diff --git a/core/dr-gpu/src/lib.rs b/core/dr-gpu/src/lib.rs index f52b4d3..3f4b661 100644 --- a/core/dr-gpu/src/lib.rs +++ b/core/dr-gpu/src/lib.rs @@ -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}; diff --git a/core/dr-gpu/src/merge.rs b/core/dr-gpu/src/merge.rs index 94f77f4..76cb764 100644 --- a/core/dr-gpu/src/merge.rs +++ b/core/dr-gpu/src/merge.rs @@ -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) -> Result<(), GpuError> { + fn resolve_chunk( + &mut self, + chunk: (u32, u32), + scale: f32, + out: &mut Vec, + ) -> 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 { diff --git a/core/dr-pano/examples/align.rs b/core/dr-pano/examples/align.rs index 5cf8184..681af68 100644 --- a/core/dr-pano/examples/align.rs +++ b/core/dr-pano/examples/align.rs @@ -32,11 +32,12 @@ fn main() { eprintln!("usage: align [out-prefix] ..."); 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 = 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; diff --git a/core/dr-pano/src/align.rs b/core/dr-pano/src/align.rs index 02a2b3a..0220773 100644 --- a/core/dr-pano/src/align.rs +++ b/core/dr-pano/src/align.rs @@ -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 { 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 Result 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> = 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, Cameras) { + fn synthetic_sweep( + n: usize, + step: f64, + f: f64, + w: usize, + h: usize, + ) -> (Vec, 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 = (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 = (0..DESCRIPTOR_LEN).map(|_| rnd() as f32).collect(); let norm = desc.iter().map(|v| v * v).sum::().sqrt(); let desc: Vec = 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(); diff --git a/core/dr-pano/src/bundle.rs b/core/dr-pano/src/bundle.rs index df3b236..e5deb00 100644 --- a/core/dr-pano/src/bundle.rs +++ b/core/dr-pano/src/bundle.rs @@ -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 diff --git a/core/dr-pano/src/homography.rs b/core/dr-pano/src/homography.rs index f061a91..da1e3ac 100644 --- a/core/dr-pano/src/homography.rs +++ b/core/dr-pano/src/homography.rs @@ -108,9 +108,7 @@ pub fn ransac_homography( let Some(h) = dlt(&sample.map(|k| pairs[k])) else { continue; }; - let inliers: Vec = (0..n) - .filter(|&k| agrees(&h, pairs[k], thr2)) - .collect(); + let inliers: Vec = (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); } } } diff --git a/core/dr-pano/src/image.rs b/core/dr-pano/src/image.rs index d53ab09..a1af61d 100644 --- a/core/dr-pano/src/image.rs +++ b/core/dr-pano/src/image.rs @@ -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| { diff --git a/core/dr-pano/src/linalg.rs b/core/dr-pano/src/linalg.rs index 9db7dcf..0c9c517 100644 --- a/core/dr-pano/src/linalg.rs +++ b/core/dr-pano/src/linalg.rs @@ -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 { diff --git a/core/dr-pano/src/matching.rs b/core/dr-pano/src/matching.rs index e0fc847..66437dc 100644 --- a/core/dr-pano/src/matching.rs +++ b/core/dr-pano/src/matching.rs @@ -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 = 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]; diff --git a/core/dr-pano/src/projection.rs b/core/dr-pano/src/projection.rs index c2bc08f..58a0278 100644 --- a/core/dr-pano/src/projection.rs +++ b/core/dr-pano/src/projection.rs @@ -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); } diff --git a/core/dr-pano/src/xfeat.rs b/core/dr-pano/src/xfeat.rs index 7acb07f..9419143 100644 --- a/core/dr-pano/src/xfeat.rs +++ b/core/dr-pano/src/xfeat.rs @@ -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 { + pub fn from_paths( + landscape: &std::path::Path, + portrait: &std::path::Path, + ) -> Result { 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]) diff --git a/core/dr-pipeline/src/sidecar.rs b/core/dr-pipeline/src/sidecar.rs index 79c2191..6778caa 100644 --- a/core/dr-pipeline/src/sidecar.rs +++ b/core/dr-pipeline/src/sidecar.rs @@ -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); diff --git a/core/dr-segment/examples/onnx_probe.rs b/core/dr-segment/examples/onnx_probe.rs index e659387..ea1c8ad 100644 --- a/core/dr-segment/examples/onnx_probe.rs +++ b/core/dr-segment/examples/onnx_probe.rs @@ -24,7 +24,11 @@ fn main() { }; let shape: Vec = 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()); diff --git a/ui/dr-ui/examples/merge.rs b/ui/dr-ui/examples/merge.rs index 47ac284..7bed683 100644 --- a/ui/dr-ui/examples/merge.rs +++ b/ui/dr-ui/examples/merge.rs @@ -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}"); diff --git a/ui/dr-ui/src/develop.rs b/ui/dr-ui/src/develop.rs index 1487933..dca9911 100644 --- a/ui/dr-ui/src/develop.rs +++ b/ui/dr-ui/src/develop.rs @@ -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, diff --git a/ui/dr-ui/src/merge.rs b/ui/dr-ui/src/merge.rs index 67653a3..c675f83 100644 --- a/ui/dr-ui/src/merge.rs +++ b/ui/dr-ui/src/merge.rs @@ -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 }, + Merge { + projection: Option, + }, /// 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 }, + Preview { + projection: Option, + }, 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 { 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 = rgba.chunks_exact(4).flat_map(|p| [p[0], p[1], p[2]]).collect(); + let rgb: Vec = 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> { @@ -722,7 +765,9 @@ fn gains(proxies: &[Gray], a: &Alignment) -> Vec { 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 { 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), diff --git a/ui/dr-ui/src/merge_ui.rs b/ui/dr-ui/src/merge_ui.rs index 9c3243f..cff05cb 100644 --- a/ui/dr-ui/src/merge_ui.rs +++ b/ui/dr-ui/src/merge_ui.rs @@ -146,7 +146,9 @@ pub fn wire( // bytes, which keeps it free of the library. let conn = context.conn.clone(); let fetch_names = names.clone(); - let fetch = move |tx: &Sender, cancel: &Cancel| -> Option> { + let fetch = move |tx: &Sender, + cancel: &Cancel| + -> Option> { 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( 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( if let (Some(gpu), Some(first)) = (gpu_for_start.clone(), paths.first()) { let names: Vec = 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, _: &Cancel| -> Option> { let mut frames = Vec::new(); @@ -214,7 +226,15 @@ pub fn wire( } 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( 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( 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::::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, on_done: &Rc { 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(),