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
@@ -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)+ }) }) }) };@@ -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 }@@ -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);@@ -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