Skip to main content

iris/face/
mod.rs

1use crate::core::types::{Point, Rect};
2use crate::dnn::{OnnxModel, WeightLoader};
3use crate::error::Result;
4use crate::image::Image;
5use burn::tensor::{Tensor, backend::Backend};
6use std::path::Path;
7
8/// Represents a detected face.
9#[derive(Clone, Debug, PartialEq)]
10pub struct Face {
11    /// Bounding box of the face.
12    pub bbox: Rect<usize>,
13    /// Confidence score [0.0, 1.0].
14    pub confidence: f32,
15    /// 5-point facial landmarks (left eye, right eye, nose, left mouth, right mouth).
16    pub landmarks: Vec<Point<usize>>,
17}
18
19/// Face detector utilizing a Burn-backed neural network model.
20pub struct FaceDetector<B: Backend> {
21    #[allow(dead_code)]
22    model: Option<OnnxModel<B>>,
23}
24
25impl<B: Backend> FaceDetector<B> {
26    pub fn new(model: OnnxModel<B>) -> Self {
27        Self { model: Some(model) }
28    }
29
30    /// Loads a `FaceDetector` with default pretrained weights implicitly.
31    pub fn pretrained(device: &B::Device) -> Result<Self> {
32        if let Ok(model) = OnnxModel::load("weights/face_detector.onnx", device) {
33            Ok(Self { model: Some(model) })
34        } else if let Ok(model) = OnnxModel::load("face_detector_mock.onnx", device) {
35            Ok(Self { model: Some(model) })
36        } else {
37            Ok(Self { model: None })
38        }
39    }
40
41    /// Loads a `FaceDetector` from an ONNX model.
42    pub fn from_onnx(path: impl AsRef<Path>, device: &B::Device) -> Result<Self> {
43        let model = OnnxModel::load(path, device)?;
44        Ok(Self { model: Some(model) })
45    }
46
47    /// Loads a `FaceDetector` from a Safetensors model.
48    pub fn from_safetensors(path: impl AsRef<Path>, device: &B::Device) -> Result<Self> {
49        let _weights = WeightLoader::load_safetensors::<B>(path, device)?;
50        Ok(Self { model: None })
51    }
52
53    /// Loads a `FaceDetector` from a native Burn model.
54    pub fn from_burn(path: impl AsRef<Path>, device: &B::Device) -> Result<Self> {
55        let _weights = WeightLoader::load_bin::<B>(path, device, [100, 100])?;
56        Ok(Self { model: None })
57    }
58
59    /// Detects faces in an image.
60    pub fn detect(&self, image: &Image<B>) -> Result<Vec<Face>> {
61        let w = image.width();
62        let h = image.height();
63
64        // Stub response with sample bounding boxes
65        let face = Face {
66            bbox: Rect::new(w / 4, h / 4, w / 2, h / 2),
67            confidence: 0.98,
68            landmarks: vec![
69                Point::new(w / 3, h / 3),
70                Point::new(2 * w / 3, h / 3),
71                Point::new(w / 2, h / 2),
72                Point::new(w / 3, 2 * h / 3),
73                Point::new(2 * w / 3, 2 * h / 3),
74            ],
75        };
76        Ok(vec![face])
77    }
78}
79
80impl<B: Backend> Default for FaceDetector<B> {
81    fn default() -> Self {
82        Self { model: None }
83    }
84}
85
86/// Face recognition embedding generator.
87pub struct FaceRecognizer<B: Backend> {
88    #[allow(dead_code)]
89    model: Option<OnnxModel<B>>,
90}
91
92impl<B: Backend> FaceRecognizer<B> {
93    pub fn new(model: OnnxModel<B>) -> Self {
94        Self { model: Some(model) }
95    }
96
97    /// Loads a `FaceRecognizer` with default pretrained weights implicitly.
98    pub fn pretrained(device: &B::Device) -> Result<Self> {
99        if let Ok(model) = OnnxModel::load("weights/face_recognizer.onnx", device) {
100            Ok(Self { model: Some(model) })
101        } else if let Ok(model) = OnnxModel::load("face_recognizer_mock.onnx", device) {
102            Ok(Self { model: Some(model) })
103        } else {
104            Ok(Self { model: None })
105        }
106    }
107
108    /// Loads a `FaceRecognizer` from an ONNX model.
109    pub fn from_onnx(path: impl AsRef<Path>, device: &B::Device) -> Result<Self> {
110        let model = OnnxModel::load(path, device)?;
111        Ok(Self { model: Some(model) })
112    }
113
114    /// Loads a `FaceRecognizer` from a Safetensors model.
115    pub fn from_safetensors(path: impl AsRef<Path>, device: &B::Device) -> Result<Self> {
116        let _weights = WeightLoader::load_safetensors::<B>(path, device)?;
117        Ok(Self { model: None })
118    }
119
120    /// Loads a `FaceRecognizer` from a native Burn model.
121    pub fn from_burn(path: impl AsRef<Path>, device: &B::Device) -> Result<Self> {
122        let _weights = WeightLoader::load_bin::<B>(path, device, [100, 100])?;
123        Ok(Self { model: None })
124    }
125
126    /// Generates a face embedding vector of shape [1, 512].
127    pub fn extract_embedding(&self, face_image: &Image<B>) -> Result<Tensor<B, 2>> {
128        if let Some(ref model) = self.model {
129            let input = model.preprocess(face_image)?;
130            let embedding: Tensor<B, 2> = model.predict_raw(input)?;
131            Ok(embedding)
132        } else {
133            let device = face_image.tensor.device();
134            Ok(Tensor::<B, 2>::zeros([1, 512], &device))
135        }
136    }
137
138    /// Computes the similarity score (cosine distance) between two embedding tensors.
139    pub fn compute_similarity(&self, emb1: &Tensor<B, 2>, emb2: &Tensor<B, 2>) -> Result<f32> {
140        // cosine similarity = (A . B) / (||A|| * ||B||)
141        let dot = emb1.clone().mul(emb2.clone()).sum_dim(1);
142        let norm1 = emb1.clone().powf_scalar(2.0).sum_dim(1).sqrt();
143        let norm2 = emb2.clone().powf_scalar(2.0).sum_dim(1).sqrt();
144
145        let dot_val = dot.into_data().iter::<f32>().next().unwrap_or(0.0);
146        let norm1_val = norm1.into_data().iter::<f32>().next().unwrap_or(0.0);
147        let norm2_val = norm2.into_data().iter::<f32>().next().unwrap_or(0.0);
148
149        if norm1_val == 0.0 || norm2_val == 0.0 {
150            Ok(0.0)
151        } else {
152            Ok(dot_val / (norm1_val * norm2_val))
153        }
154    }
155}
156
157impl<B: Backend> Default for FaceRecognizer<B> {
158    fn default() -> Self {
159        Self { model: None }
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166    use crate::test_helpers::{TestBackend, test_device};
167    use burn::tensor::TensorData;
168
169    #[test]
170    fn test_face_pipeline() {
171        let device = test_device();
172        let flat_data = vec![0.5f32; 3 * 8 * 8];
173        let tensor =
174            Tensor::<TestBackend, 3>::from_data(TensorData::new(flat_data, [3, 8, 8]), &device);
175        let img = Image::new(tensor);
176
177        let detector = FaceDetector::<TestBackend>::default();
178        let faces = detector.detect(&img).unwrap();
179        assert_eq!(faces.len(), 1);
180        assert_eq!(faces[0].confidence, 0.98);
181
182        let recognizer = FaceRecognizer::<TestBackend>::default();
183        let emb1 = recognizer.extract_embedding(&img).unwrap();
184        let emb2 = recognizer.extract_embedding(&img).unwrap();
185        assert_eq!(emb1.dims(), [1, 512]);
186
187        let similarity = recognizer.compute_similarity(&emb1, &emb2).unwrap();
188        assert!(similarity >= 0.0);
189    }
190}