LR-EYE-RED: red eye correction with automatic pupil detection

pipeline::redeye: inside the user's ellipse the pupil is found on the source image (a fixed
grid in normalized coordinates, so previews and exports agree): weighted centroid and area of
red-dominant pixels (or, for pet eyes, of the brightest ones). Pupil Size scales the detected
disc; rendering in scene-linear light after white balance neutralizes the red where it is red
(iris and catchlights stay) and darkens it. CPU (lin stage) and GPU kernel redeye_k with an
equivalence test (both the GPU and the CPU scene-linear paths). Indexed controls
redEye.<i>.pupilSize/darken, redeye.add/delete commands, the Red Eye panel (selected eye's
sliders, delete) and the loupe tool (drag an ellipse, click to select).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Brandon ThomasandClaude Opus 5.5 committed 2026-10-01 10:37:02 -04:00
1 parent 8b7fb306dc
commit ebcc41825b
14 files changed
+614 -12

No files matched your search

+34 -1
View File
@@ -27,6 +27,7 @@ pub enum Section {
Profile,
Calibration,
PointColor,
RedEye,
}
impl Section {
@@ -47,6 +48,7 @@ impl Section {
Section::Profile => "Profile",
Section::Calibration => "Calibration",
Section::PointColor => "Point Color",
Section::RedEye => "Red Eye",
}
}
}
@@ -335,6 +337,28 @@ pub static INDEXED: &[ControlSpec] = &[
decimals: 0,
track: Gradient { from: "#202020", to: "#f0f0f0" },
},
ControlSpec {
id: "redEye.pupilSize",
label: "Pupil Size",
section: Section::RedEye,
min: 0.0,
max: 100.0,
default: 50.0,
step: 1.0,
decimals: 0,
track: Plain,
},
ControlSpec {
id: "redEye.darken",
label: "Darken",
section: Section::RedEye,
min: 0.0,
max: 100.0,
default: 50.0,
step: 1.0,
decimals: 0,
track: Gradient { from: "#d0d0d0", to: "#101010" },
},
];
/// `pointColor.3.hueShift` → (`pointColor`, 3, `hueShift`).
@@ -361,13 +385,21 @@ fn indexed_field<'a>(s: &'a mut DevelopSettings, id: &str) -> Option<&'a mut f64
_ => return None,
})
}
"redEye" => {
let e = s.red_eye.get_mut(i)?;
Some(match field {
"pupilSize" => &mut e.pupil_size,
"darken" => &mut e.darken,
_ => return None,
})
}
_ => None,
}
}
fn indexed_get(s: &DevelopSettings, id: &str) -> Option<f64> {
// (a clone keeps one accessor for get and set; these lists are tiny)
let mut c = DevelopSettings { point_colors: s.point_colors.clone(), ..DevelopSettings::default() };
let mut c = DevelopSettings { point_colors: s.point_colors.clone(), red_eye: s.red_eye.clone(), ..DevelopSettings::default() };
indexed_field(&mut c, id).map(|v| *v)
}
@@ -398,6 +430,7 @@ pub fn indexed_instances(s: &DevelopSettings) -> Vec<(String, &'static ControlSp
let Some((fam, field)) = spec.id.split_once('.') else { continue };
let n = match fam {
"pointColor" => s.point_colors.len(),
"redEye" => s.red_eye.len(),
_ => 0,
};
for i in 0..n {
+13
View File
@@ -842,14 +842,27 @@ impl Default for Spot {
}
}
/// A red eye / pet eye correction: the user's ellipse (centre normalized, radii as fractions of
/// the long edge); the pupil inside it is found automatically.
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct RedEye {
pub center: Point,
pub rx: f64,
pub ry: f64,
/// 0..100 (50 = the detected pupil).
pub pupil_size: f64,
/// 0..100.
pub darken: f64,
pub pet: bool,
/// Pet eye catchlight: offset of its centre from the pupil centre, in pupil radii.
#[serde(default)]
pub catchlight: Option<Point>,
}
impl Default for RedEye {
fn default() -> Self {
Self { center: Point::new(0.5, 0.5), rx: 0.02, ry: 0.015, pupil_size: 50.0, darken: 50.0, pet: false, catchlight: None }
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]