mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-27 03:27:14 -06:00
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8cbdece6f2
commit
b00dda4cec
@@ -1,7 +1,7 @@
|
||||
//! Develop commands. All operate on the active photo unless `ids` are given (sync).
|
||||
|
||||
use lightcraft_catalog::{Op, PhotoId, Version};
|
||||
use lightcraft_develop::{DevelopSettings, Preset, Section, SettingsGroup, Treatment, WbMode, controls};
|
||||
use lightcraft_develop::{DevelopSettings, Preset, Section, SettingsGroup, Treatment, Upright, WbMode, controls};
|
||||
use lightcraft_geom::{CropGeometry, Point, Rect, crop_fit_angle};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -382,6 +382,66 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
Ok(())
|
||||
})
|
||||
}),
|
||||
// ---- geometry: Upright
|
||||
cmd!(
|
||||
"geometry.upright",
|
||||
"Upright",
|
||||
[],
|
||||
None,
|
||||
"{mode: off|auto|guided|level|vertical|full} — analyses the photo (line segments → vanishing points) and stores the correction; run again to update after lens changes",
|
||||
has_active,
|
||||
|s, p| {
|
||||
let mode: Upright =
|
||||
serde_json::from_value(p.get("mode").cloned().unwrap_or(json!("auto"))).map_err(|e| bad("geometry.upright", e.to_string()))?;
|
||||
let id = active(s, "geometry.upright")?;
|
||||
let d = s.develop_of(id).unwrap_or_default();
|
||||
let transform = match mode {
|
||||
Upright::Off | Upright::Guided => None,
|
||||
m => {
|
||||
let src = s.source_now(id, SourceLevel::Preview).map_err(|e| bad("geometry.upright", e))?;
|
||||
let info = s.catalog.photo(id).map(|p| crate::media::source_info(p)).unwrap_or_default();
|
||||
Some(lightcraft_pipeline::upright::auto_transform(&src, &info, &d, m))
|
||||
}
|
||||
};
|
||||
edit(s, "geometry.upright", "Upright", |d| {
|
||||
d.geometry.upright = mode;
|
||||
d.geometry.upright_transform = transform;
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(json!({"mode": mode, "transform": transform}))
|
||||
}
|
||||
),
|
||||
cmd!(
|
||||
"geometry.guides",
|
||||
"Guided Upright Guides",
|
||||
[],
|
||||
None,
|
||||
"{guides: [[x0,y0,x1,y1], …] (≤ 4, normalized coords of the lens-corrected image), add?: bool (append one guide, dropping the oldest beyond 4)} — switches Upright to Guided",
|
||||
has_active,
|
||||
|s, p| {
|
||||
let parse = |v: &Value| -> Option<(Point, Point)> {
|
||||
let a: Vec<f64> = v.as_array()?.iter().filter_map(Value::as_f64).collect();
|
||||
(a.len() == 4 && a.iter().all(|x| x.is_finite())).then(|| (Point::new(a[0], a[1]), Point::new(a[2], a[3])))
|
||||
};
|
||||
let guides: Vec<(Point, Point)> =
|
||||
p.get("guides").and_then(Value::as_array).map(|a| a.iter().filter_map(parse).collect()).unwrap_or_default();
|
||||
let add = bool_or(p, "add", false);
|
||||
edit(s, "geometry.guides", "Guided Upright", |d| {
|
||||
if add {
|
||||
d.geometry.guides.extend(guides);
|
||||
} else {
|
||||
d.geometry.guides = guides;
|
||||
}
|
||||
let n = d.geometry.guides.len();
|
||||
if n > 4 {
|
||||
d.geometry.guides.drain(..n - 4);
|
||||
}
|
||||
d.geometry.upright = Upright::Guided;
|
||||
d.geometry.upright_transform = None;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
),
|
||||
// ---- copy / paste / sync
|
||||
cmd!(
|
||||
"develop.copy",
|
||||
|
||||
Reference in New Issue
Block a user