immich-automated-selfie-tim.../src/pipeline/types.rs
2026-02-12 20:20:12 +01:00

210 lines
5.5 KiB
Rust

//! Types for image processing.
use serde::{Deserialize, Serialize};
/// A 2D point.
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct Point {
pub x: f32,
pub y: f32,
}
impl Point {
pub fn new(x: f32, y: f32) -> Self {
Self { x, y }
}
}
/// Bounding box for a face.
#[derive(Debug, Clone, Copy)]
pub struct BoundingBox {
pub x1: f32,
pub y1: f32,
pub x2: f32,
pub y2: f32,
}
impl BoundingBox {
/// Create a new bounding box, ensuring x1 <= x2 and y1 <= y2.
pub fn new(x1: f32, y1: f32, x2: f32, y2: f32) -> Self {
Self {
x1: x1.min(x2),
y1: y1.min(y2),
x2: x1.max(x2),
y2: y1.max(y2),
}
}
pub fn width(&self) -> f32 {
self.x2 - self.x1
}
pub fn height(&self) -> f32 {
self.y2 - self.y1
}
pub fn center(&self) -> Point {
Point::new((self.x1 + self.x2) / 2.0, (self.y1 + self.y2) / 2.0)
}
}
/// Facial landmarks (68-point model).
///
/// This struct requires exactly 68 landmark points following the standard
/// dlib/iBUG 68-point face landmark format. Use `Landmarks::new()` to
/// construct with validation.
#[derive(Debug, Clone)]
pub struct Landmarks {
/// All 68 landmark points (validated on construction).
points: [Point; 68],
}
/// Expected number of landmark points.
pub const LANDMARK_COUNT: usize = 68;
impl Landmarks {
/// Create a new Landmarks from a vector of points.
///
/// Returns an error if the vector doesn't contain exactly 68 points.
pub fn new(points: Vec<Point>) -> Result<Self, String> {
let len = points.len();
let points: [Point; LANDMARK_COUNT] = points.try_into().map_err(|_| {
format!(
"Expected exactly {} landmark points, got {}",
LANDMARK_COUNT, len
)
})?;
Ok(Self { points })
}
/// Get all landmark points.
pub fn points(&self) -> &[Point; 68] {
&self.points
}
/// Left eye center (average of points 36-41).
pub fn left_eye_center(&self) -> Point {
let eye_points = &self.points[36..42];
let x = eye_points.iter().map(|p| p.x).sum::<f32>() / 6.0;
let y = eye_points.iter().map(|p| p.y).sum::<f32>() / 6.0;
Point::new(x, y)
}
/// Right eye center (average of points 42-47).
pub fn right_eye_center(&self) -> Point {
let eye_points = &self.points[42..48];
let x = eye_points.iter().map(|p| p.x).sum::<f32>() / 6.0;
let y = eye_points.iter().map(|p| p.y).sum::<f32>() / 6.0;
Point::new(x, y)
}
/// Nose tip (point 30).
pub fn nose_tip(&self) -> Point {
self.points[30]
}
/// Chin (point 8).
pub fn chin(&self) -> Point {
self.points[8]
}
/// Left mouth corner (point 48).
pub fn left_mouth(&self) -> Point {
self.points[48]
}
/// Right mouth corner (point 54).
pub fn right_mouth(&self) -> Point {
self.points[54]
}
/// Calculate Eye Aspect Ratio (EAR) for blink detection.
///
/// EAR is computed as:
/// EAR = (||p2-p6|| + ||p3-p5||) / (2 * ||p1-p4||)
///
/// Where p1-p6 are the 6 eye landmark points.
/// For left eye: points 36-41
/// For right eye: points 42-47
pub fn eye_aspect_ratio(&self) -> EyeAspectRatio {
let left_ear = Self::compute_ear(&self.points[36..42]);
let right_ear = Self::compute_ear(&self.points[42..48]);
EyeAspectRatio {
left: left_ear,
right: right_ear,
}
}
/// Compute EAR for a single eye given 6 landmark points.
fn compute_ear(eye: &[Point]) -> f32 {
if eye.len() != 6 {
return 0.0;
}
// Vertical distances
let v1 = Self::distance(&eye[1], &eye[5]); // p2-p6
let v2 = Self::distance(&eye[2], &eye[4]); // p3-p5
// Horizontal distance
let h = Self::distance(&eye[0], &eye[3]); // p1-p4
if h == 0.0 {
return 0.0;
}
(v1 + v2) / (2.0 * h)
}
/// Euclidean distance between two points.
fn distance(p1: &Point, p2: &Point) -> f32 {
let dx = p2.x - p1.x;
let dy = p2.y - p1.y;
(dx * dx + dy * dy).sqrt()
}
/// Get the angle (in radians) to rotate the face so eyes are horizontal.
pub fn eye_rotation_angle(&self) -> f32 {
let left_eye = self.left_eye_center();
let right_eye = self.right_eye_center();
let dy = right_eye.y - left_eye.y;
let dx = right_eye.x - left_eye.x;
dy.atan2(dx)
}
/// Get the distance between eye centers.
pub fn inter_eye_distance(&self) -> f32 {
Self::distance(&self.left_eye_center(), &self.right_eye_center())
}
}
/// Head pose angles.
#[derive(Debug, Clone, Copy)]
pub struct HeadPose {
/// Pitch (up/down tilt) in degrees.
pub pitch: f32,
/// Yaw (left/right turn) in degrees.
pub yaw: f32,
/// Roll (head tilt) in degrees.
pub roll: f32,
}
impl HeadPose {
/// Check if the pose is within acceptable thresholds.
pub fn is_frontal(&self, yaw_threshold: f32) -> bool {
self.yaw.abs() <= yaw_threshold
}
}
/// Eye Aspect Ratio for blink detection.
#[derive(Debug, Clone, Copy)]
pub struct EyeAspectRatio {
pub left: f32,
pub right: f32,
}
impl EyeAspectRatio {
/// Check if eyes are sufficiently open.
pub fn eyes_open(&self, threshold: f32) -> bool {
self.left >= threshold && self.right >= threshold
}
}