collective

Browse / github.com/storytold/lightcraft / patch

#2361: lens corrections crop inside the corrected photo

[Feature request] Add a button to automatically crop the image when manually correcting the distortion Like it is shown in the image, when I tried to correct the distortion of my lens manually, it didn't correctly crop the image, instead it left me with white borders <img width="2560" height="1440" alt="Image" src="https://github.com/user-attachments/assets/0947a4d0-01ee-4e62-8692-a2561ff212f0" />

Status
open
Score
0.00
Adopters
0 other operators
Author
emmertex (imported from pull request #3012, open)
Base
387bed199fca44d76af5aa1c24dd7cc3de4c8db2
Size
+57 −7 in 4 files
Review
pending
Shared
2026-10-28
Id
3443a345e335a9b46722f268

How it went

No other operator has reported using it yet.

Diff

raw · crates/pipeline/src/geometry.rs crates/pipeline/src/lib.rs crates/pipeline/src/tests_geometry.rs docs/whats-new.md

diff --git a/crates/pipeline/src/geometry.rs b/crates/pipeline/src/geometry.rs--- a/crates/pipeline/src/geometry.rs+++ b/crates/pipeline/src/geometry.rs@@ -77,15 +77,32 @@ impl Frame {             warp: (!warp.is_identity()).then_some(warp),             view: None,         };-        if apply_crop && s.geometry.constrain_crop {-            f.constrain_to_image();+        if apply_crop {+            f.constrain_to_image(s.geometry.constrain_crop);         }         f     } -    /// "Constrain Crop": shrink the crop about its centre until it contains no area outside the warped source.-    fn constrain_to_image(&mut self) {-        let Some(wp) = self.warp.as_ref().filter(|w| w.moves_pixels()) else { return };+    /// Shrink the crop about its centre until it contains no area outside the warped source. Lens+    /// corrections (profile, embedded camera warp, manual distortion, lateral CA) always do this, as+    /// a corrected lens never shows the blank past the photo's edge (issue #2361); perspective only+    /// with "Constrain Crop" (`with_persp`), so a keystone fix can keep its corners.+    pub(crate) fn constrain_to_image(&mut self, with_persp: bool) {+        let Some(full) = self.warp.as_ref().filter(|w| w.moves_pixels()) else { return };+        let optics;+        let wp = if with_persp || full.persp == Homography::IDENTITY {+            full+        } else {+            let mut o = full.clone();+            o.persp = Homography::IDENTITY;+            o.persp_inv = Homography::IDENTITY;+            optics = o;+            &optics+        };+        if !wp.moves_pixels() {+            return;+        }+        let planes: &[usize] = if wp.per_channel() { &[0, 1, 2] } else { &[1] };         let a = self.crop.output_to_source(self.ow, self.oh, 1.0, 1.0);         let (w, h) = (self.ow, self.oh);         let fits = |t: f64| {@@ -93,8 +110,10 @@ impl Frame {                 let u = i as f64 / 48.0;                 [(u, 0.0), (u, 1.0), (0.0, u), (1.0, u)].iter().all(|&(x, y)| {                     let p = Point::new(0.5 + t * (x - 0.5), 0.5 + t * (y - 0.5));-                    let s = wp.to_source(a.apply(p), 1);-                    s.x >= -1e-6 * w && s.y >= -1e-6 * h && s.x <= w * (1.0 + 1e-6) && s.y <= h * (1.0 + 1e-6)+                    planes.iter().all(|&ch| {+                        let s = wp.to_source(a.apply(p), ch);+                        s.x >= -1e-6 * w && s.y >= -1e-6 * h && s.x <= w * (1.0 + 1e-6) && s.y <= h * (1.0 + 1e-6)+                    })                 })             })         };diff --git a/crates/pipeline/src/lib.rs b/crates/pipeline/src/lib.rs--- a/crates/pipeline/src/lib.rs+++ b/crates/pipeline/src/lib.rs@@ -236,6 +236,10 @@ pub fn frame_for(src: &Rgb32f, info: &SourceInfo, s: &DevelopSettings, apply_cro     let mut frame = geometry::Frame::with_lens(src.width, src.height, s, apply_crop, info.lens.as_ref());     if s.optics.remove_ca && s.section_enabled("optics") {         frame.add_lateral_ca(optics::estimate_lateral_ca(src));+        if apply_crop {+            // The estimated CA moves the colour planes too: keep the crop inside every one of them.+            frame.constrain_to_image(s.geometry.constrain_crop);+        }     }     frame }diff --git a/crates/pipeline/src/tests_geometry.rs b/crates/pipeline/src/tests_geometry.rs--- a/crates/pipeline/src/tests_geometry.rs+++ b/crates/pipeline/src/tests_geometry.rs@@ -260,6 +260,26 @@ fn constrain_crop_removes_blank_areas() {     assert!(blank_fraction(&c) < 0.002, "constrained: {}", blank_fraction(&c)); } +/// Issue #2361: lens corrections never show the blank past the photo's edge, with or without+/// Constrain Crop, in every colour plane; perspective alone still needs Constrain Crop.+#[test]+fn lens_corrections_leave_no_blank_corners() {+    let src = Rgb32f::from_fn(300, 200, |x, y| [0.05 + 0.002 * x as f32, 0.1 + 0.002 * y as f32, 0.3]);+    for (distortion, ca) in [(-80.0, 0.0), (80.0, 0.0), (-40.0, 100.0), (0.0, -100.0)] {+        let mut s = DevelopSettings::default();+        s.optics.distortion = distortion;+        s.optics.ca_red = ca;+        s.optics.ca_blue = -ca;+        let r = render(&src, &SourceInfo::default(), &s, &RenderRequest::fit(300, 300)).image;+        assert!(blank_fraction(&r) < 0.002, "distortion {distortion}, CA {ca}: {}", blank_fraction(&r));+    }+    let mut s = DevelopSettings::default();+    s.optics.distortion = -60.0;+    s.geometry.vertical = -60.0;+    let r = render(&src, &SourceInfo::default(), &s, &RenderRequest::fit(300, 300)).image;+    assert!(blank_fraction(&r) > 0.03, "the keystone keeps its corners: {}", blank_fraction(&r));+}+ #[test] fn transforms_preview_matches_export() {     let src = lightcraft_scenes::demo_library()[4].render(960, 640);diff --git a/docs/whats-new.md b/docs/whats-new.md--- a/docs/whats-new.md+++ b/docs/whats-new.md@@ -1,5 +1,12 @@ # What's new in LightCraft +## 2026-10-26++### Lens corrections without grey corners+- Lens profiles, the camera's own distortion correction, manual Distortion and Chromatic Aberration now crop just+  inside the corrected photo, as in Lightroom, so no grey corners or edge marks show. Perspective corrections still+  need Constrain Crop to do the same.+ ## 2026-10-24  ### Relative-date filters and smart albums follow the clock