Eye alignment now only needs one setting (inter-eye distance)

This commit is contained in:
Arnaud_Cayrol 2026-02-07 21:39:42 +01:00
parent 621b93c599
commit 250cbd491a
3 changed files with 51 additions and 64 deletions

View file

@ -38,8 +38,7 @@
keep_intermediates: false,
},
alignment: {
left_eye_y_position: 0.4,
left_eye_x_position: 0.35,
eye_distance: 0.3,
},
},
video: {
@ -116,8 +115,7 @@
keep_intermediates: false,
},
alignment: {
left_eye_y_position: 0.4,
left_eye_x_position: 0.35,
eye_distance: 0.3,
},
},
video: {
@ -353,35 +351,18 @@
<div class="setting-row sub-setting">
<label>
<span class="setting-label">Left Eye Y Position</span>
<span class="setting-hint">Vertical position of left eye (% from top)</span>
<span class="setting-label">Eye Distance</span>
<span class="setting-hint">Distance between eyes as % of image width (larger = zoom in)</span>
</label>
<div class="setting-control">
<input
type="range"
bind:value={config.processing.alignment.left_eye_y_position}
min="0.2"
max="0.6"
bind:value={config.processing.alignment.eye_distance}
min="0.1"
max="0.5"
step="0.01"
/>
<span class="value">{(config.processing.alignment.left_eye_y_position * 100).toFixed(0)}%</span>
</div>
</div>
<div class="setting-row sub-setting">
<label>
<span class="setting-label">Left Eye X Position</span>
<span class="setting-hint">Horizontal position of left eye (% from left)</span>
</label>
<div class="setting-control">
<input
type="range"
bind:value={config.processing.alignment.left_eye_x_position}
min="0.25"
max="0.45"
step="0.01"
/>
<span class="value">{(config.processing.alignment.left_eye_x_position * 100).toFixed(0)}%</span>
<span class="value">{(config.processing.alignment.eye_distance * 100).toFixed(0)}%</span>
</div>
</div>
</div>

View file

@ -299,63 +299,66 @@ impl EyeFilterConfig {
/// Aligns faces based on eye positions detected from facial landmarks,
/// ensuring consistent eye placement across all images for smoother timelapses.
///
/// Uses a single `eye_distance` parameter to control framing. Eye positions
/// are derived by estimating the visual face center (approximately at the nose
/// tip) from standard face proportions, then placing that point at the image
/// center with a small downward offset to show more neck.
///
/// Note: Face alignment is always enabled and is a core part of the pipeline.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlignmentConfig {
/// Target Y position for left eye as percentage from top (0.0-1.0).
/// Default 0.4 places left eye at 40% from the top.
/// Both eyes are positioned at the same vertical level.
pub left_eye_y_position: f32,
/// Target X position for left eye as percentage from left (0.0-1.0).
/// Default 0.35 places left eye at 35% from the left edge.
/// Right eye will be placed at (1.0 - left_eye_x_position).
pub left_eye_x_position: f32,
/// Distance between left and right eye centers as a fraction of output
/// image width (0.0-1.0). Default 0.3 means the eyes span 30% of the
/// output width. Larger values zoom in (bigger face), smaller values
/// zoom out (more background).
pub eye_distance: f32,
}
impl Default for AlignmentConfig {
fn default() -> Self {
Self {
left_eye_y_position: 0.4,
left_eye_x_position: 0.35,
eye_distance: 0.3,
}
}
}
impl AlignmentConfig {
/// Offset from the eye line to the approximate visual face center
/// (with a slight neck bias), expressed as a multiple of the inter-eye
/// distance. The visible face center (hairline-to-chin midpoint) is
/// about 0.4× IPD below the eyes; adding a small neck offset brings
/// this to 0.5× IPD. This matches the standard used by dlib and OpenCV
/// face alignment (desiredLeftEye = (0.35, 0.35) with IPD = 0.3).
/// This point is placed at the image center (0.5, 0.5).
const FACE_CENTER_BELOW_EYES: f32 = 0.5;
/// Validate the configuration values.
pub fn validate(&self) -> Result<()> {
if self.left_eye_y_position <= 0.0 || self.left_eye_y_position >= 1.0 {
if self.eye_distance <= 0.0 || self.eye_distance >= 1.0 {
return Err(Error::Config(
"Alignment left_eye_y_position must be between 0.0 and 1.0 (exclusive)".to_string(),
"Alignment eye_distance must be between 0.0 and 1.0 (exclusive)".to_string(),
));
}
if self.left_eye_y_position < 0.2 || self.left_eye_y_position > 0.6 {
if self.eye_distance < 0.1 || self.eye_distance > 0.5 {
return Err(Error::Config(
"Alignment left_eye_y_position should be between 0.2 and 0.6 for best results"
.to_string(),
));
}
if self.left_eye_x_position <= 0.0 || self.left_eye_x_position >= 0.5 {
return Err(Error::Config(
"Alignment left_eye_x_position must be between 0.0 and 0.5 (left eye must be in left half)"
.to_string(),
));
}
if self.left_eye_x_position < 0.25 || self.left_eye_x_position > 0.45 {
return Err(Error::Config(
"Alignment left_eye_x_position should be between 0.25 and 0.45 for best results"
"Alignment eye_distance should be between 0.1 and 0.5 for best results"
.to_string(),
));
}
Ok(())
}
/// Calculate the target inter-eye distance as a fraction of output width.
/// Since left eye is at left_eye_x_position and right eye is at (1 - left_eye_x_position),
/// the distance between them is: (1 - left_eye_x_position) - left_eye_x_position = 1 - 2*left_eye_x_position
pub fn inter_eye_distance(&self) -> f32 {
1.0 - 2.0 * self.left_eye_x_position
/// Target X position for the left eye as a fraction of output width.
/// Eyes are centered horizontally: `left_eye_x = (1 - eye_distance) / 2`.
pub fn left_eye_x(&self) -> f32 {
(1.0 - self.eye_distance) / 2.0
}
/// Target Y position for both eyes as a fraction of output height.
/// Derived by placing the estimated face center at the image center:
/// `eye_y = 0.5 - eye_distance * FACE_CENTER_BELOW_EYES`.
pub fn left_eye_y(&self) -> f32 {
0.5 - self.eye_distance * Self::FACE_CENTER_BELOW_EYES
}
}

View file

@ -174,14 +174,17 @@ fn calculate_eye_alignment_transform(
) -> AffineMatrix {
let output_size_f = output_size as f32;
// Calculate target eye positions
// Calculate target eye positions from the single eye_distance setting
let left_eye_x = alignment_config.left_eye_x();
let left_eye_y = alignment_config.left_eye_y();
let left_eye_target = Point::new(
output_size_f * alignment_config.left_eye_x_position,
output_size_f * alignment_config.left_eye_y_position,
output_size_f * left_eye_x,
output_size_f * left_eye_y,
);
let right_eye_target = Point::new(
output_size_f * (1.0 - alignment_config.left_eye_x_position),
output_size_f * alignment_config.left_eye_y_position,
output_size_f * (1.0 - left_eye_x),
output_size_f * left_eye_y,
);
// Calculate angles