M4.4: Upright — Auto/Level/Vertical/Full from detected lines + Guided upright (up to 4 guides)

- pipeline::upright: own gradient-based line-segment detector (level-line region growing seeded from the
  strongest gradients, weighted principal-axis fit, elongation test); vanishing points by RANSAC over
  segment pairs + least-squares refinement (smallest eigenvector of Σ w·l·lᵀ)
- corrections as a virtual camera rotation K·R·K⁻¹ (vertical VP → y axis; Full also horizontal VP → x axis
  with focal length from VP orthogonality when plausible); Level = rotation; Auto = Full/Vertical when
  moderate, else Level; all re-centred
- Guided: guides stored in normalized lens-corrected coordinates; 2+ vertical/horizontal guides define the
  vanishing points, a single guide levels
- the analysis runs on the lens-corrected image at 1024 px and the result is stored in
  geometry.upright_transform (identical preview/export); settings naming a mode without a transform
  (XMP, CLI) are resolved at render time
- commands geometry.upright {mode} (Update = run again) and geometry.guides {guides, add}
- UI: Upright mode buttons, Update, Draw/Clear Guides in the crop panel; on-canvas guide drawing that
  follows the warp
- tests: segment detector on known lines; Vertical/Auto/Full fix converging verticals; Level fixes a tilted
  horizon; Full fixes both families; guided from two guides; analysis resolution independence; lazy vs
  stored transform; engine commands

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Brandon ThomasandClaude Opus 5.5 committed 2026-10-01 01:16:18 -04:00
1 parent 8cbdece6f2
commit b00dda4cec
10 files changed
+935 -12

No files matched your search

+8 -2
View File
@@ -1,7 +1,7 @@
//! The develop settings schema. Every field has a neutral default; serde uses `#[serde(default)]` so
//! older/newer files load (unknown fields are ignored, missing fields take defaults).
use lightcraft_geom::{CropGeometry, Orientation, Point};
use lightcraft_geom::{CropGeometry, Homography, Orientation, Point};
use serde::{Deserialize, Serialize};
pub const SCHEMA_VERSION: u32 = 1;
@@ -493,8 +493,13 @@ pub enum Upright {
#[serde(default)]
pub struct Geometry {
pub upright: Upright,
/// Guided Upright guides (normalized image coords), up to 4 line segments.
/// Guided Upright guides, up to 4 line segments, in normalized coordinates of the lens-corrected
/// (pre-perspective) oriented image.
pub guides: Vec<(Point, Point)>,
/// The automatic Upright correction found by analysis for `upright` (Auto/Level/Vertical/Full), as a
/// homography lens-corrected → transformed in centred coordinates (`(p − centre) / (long edge / 2)`).
/// Stored so previews and exports use the identical transform; recomputed by `geometry.upright`.
pub upright_transform: Option<Homography>,
pub vertical: f64,
pub horizontal: f64,
pub rotate: f64,
@@ -510,6 +515,7 @@ impl Default for Geometry {
Self {
upright: Upright::Off,
guides: Vec::new(),
upright_transform: None,
vertical: 0.0,
horizontal: 0.0,
rotate: 0.0,