1use core::ffi::c_char;
9use core::ptr;
10use std::ffi::CString;
11use std::path::Path;
12
13use crate::error::{from_swift, VisionError};
14use crate::ffi;
15use crate::recognize_text::BoundingBox;
16use crate::request_base::{ImageBasedRequest, RequestRevisionProviding};
17use crate::sdk::PointsClassification;
18
19#[derive(Debug, Clone, Copy, PartialEq)]
22pub struct LandmarkPoint {
23 pub x: f64,
24 pub y: f64,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29#[repr(usize)]
30pub enum RequestFaceLandmarksConstellation {
31 NotDefined = 0,
32 Points65 = 1,
33 Points76 = 2,
34}
35
36impl RequestFaceLandmarksConstellation {
37 pub const ALL: &'static [Self] = &[Self::NotDefined, Self::Points65, Self::Points76];
38
39 #[must_use]
40 pub const fn as_raw(self) -> usize {
41 self as usize
42 }
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct FaceLandmarkRegion {
48 pub point_count: usize,
49 pub request_revision: Option<usize>,
50}
51
52impl RequestRevisionProviding for FaceLandmarkRegion {
53 fn request_revision(&self) -> Option<usize> {
54 self.request_revision
55 }
56}
57
58#[derive(Debug, Clone, PartialEq)]
60pub struct FaceLandmarkRegion2D {
61 pub region: FaceLandmarkRegion,
62 pub normalized_points: Vec<LandmarkPoint>,
63 pub precision_estimates_per_point: Vec<f32>,
64 pub points_classification: Option<PointsClassification>,
65}
66
67impl FaceLandmarkRegion2D {
68 #[must_use]
69 pub fn points_in_image_of_size(&self, image_size: (f64, f64)) -> Vec<LandmarkPoint> {
70 self.normalized_points
71 .iter()
72 .map(|point| LandmarkPoint {
73 x: point.x * image_size.0,
74 y: point.y * image_size.1,
75 })
76 .collect()
77 }
78}
79
80#[derive(Debug, Clone, PartialEq)]
82pub struct FaceLandmarks {
83 pub confidence: f32,
84 pub request_revision: Option<usize>,
85}
86
87impl RequestRevisionProviding for FaceLandmarks {
88 fn request_revision(&self) -> Option<usize> {
89 self.request_revision
90 }
91}
92
93#[derive(Debug, Clone, PartialEq)]
95pub struct FaceLandmarks2D {
96 pub landmarks: FaceLandmarks,
97 pub all_points: Option<FaceLandmarkRegion2D>,
98 pub face_contour: Option<FaceLandmarkRegion2D>,
99 pub left_eye: Option<FaceLandmarkRegion2D>,
100 pub right_eye: Option<FaceLandmarkRegion2D>,
101 pub left_eyebrow: Option<FaceLandmarkRegion2D>,
102 pub right_eyebrow: Option<FaceLandmarkRegion2D>,
103 pub nose: Option<FaceLandmarkRegion2D>,
104 pub nose_crest: Option<FaceLandmarkRegion2D>,
105 pub median_line: Option<FaceLandmarkRegion2D>,
106 pub outer_lips: Option<FaceLandmarkRegion2D>,
107 pub inner_lips: Option<FaceLandmarkRegion2D>,
108 pub left_pupil: Option<FaceLandmarkRegion2D>,
109 pub right_pupil: Option<FaceLandmarkRegion2D>,
110}
111
112pub trait FaceObservationAccepting {
114 fn input_face_observations(&self) -> &[BoundingBox];
115}
116
117#[derive(Debug, Clone, PartialEq, Default)]
119pub struct FaceLandmarksRequest {
120 image_based_request: ImageBasedRequest,
121 constellation: Option<RequestFaceLandmarksConstellation>,
122 input_face_observations: Vec<BoundingBox>,
123}
124
125impl FaceLandmarksRequest {
126 #[must_use]
127 pub const fn new() -> Self {
128 Self {
129 image_based_request: ImageBasedRequest::new(),
130 constellation: None,
131 input_face_observations: Vec::new(),
132 }
133 }
134
135 #[must_use]
136 pub const fn with_image_based_request(
137 mut self,
138 image_based_request: ImageBasedRequest,
139 ) -> Self {
140 self.image_based_request = image_based_request;
141 self
142 }
143
144 #[must_use]
145 pub const fn with_constellation(
146 mut self,
147 constellation: RequestFaceLandmarksConstellation,
148 ) -> Self {
149 self.constellation = Some(constellation);
150 self
151 }
152
153 #[must_use]
154 pub fn with_input_face_observations(
155 mut self,
156 input_face_observations: Vec<BoundingBox>,
157 ) -> Self {
158 self.input_face_observations = input_face_observations;
159 self
160 }
161
162 #[must_use]
163 pub const fn image_based_request(&self) -> &ImageBasedRequest {
164 &self.image_based_request
165 }
166
167 #[must_use]
168 pub const fn constellation(&self) -> Option<RequestFaceLandmarksConstellation> {
169 self.constellation
170 }
171
172 #[must_use]
173 pub const fn supports_constellation(
174 request_revision: usize,
175 constellation: RequestFaceLandmarksConstellation,
176 ) -> bool {
177 matches!(
178 (request_revision, constellation),
179 (_, RequestFaceLandmarksConstellation::NotDefined)
180 | (1 | 2, RequestFaceLandmarksConstellation::Points65)
181 | (
182 3,
183 RequestFaceLandmarksConstellation::Points65
184 | RequestFaceLandmarksConstellation::Points76,
185 )
186 )
187 }
188}
189
190impl FaceObservationAccepting for FaceLandmarksRequest {
191 fn input_face_observations(&self) -> &[BoundingBox] {
192 &self.input_face_observations
193 }
194}
195
196impl RequestRevisionProviding for FaceLandmarksRequest {
197 fn request_revision(&self) -> Option<usize> {
198 self.image_based_request.revision()
199 }
200}
201
202#[derive(Debug, Clone, PartialEq)]
205pub struct FaceWithLandmarks {
206 pub bounding_box: BoundingBox,
207 pub confidence: f32,
208 pub roll: Option<f32>,
209 pub yaw: Option<f32>,
210 pub pitch: Option<f32>,
211 pub face_contour: Vec<LandmarkPoint>,
212 pub left_eye: Vec<LandmarkPoint>,
213 pub right_eye: Vec<LandmarkPoint>,
214 pub left_eyebrow: Vec<LandmarkPoint>,
215 pub right_eyebrow: Vec<LandmarkPoint>,
216 pub nose: Vec<LandmarkPoint>,
217 pub nose_crest: Vec<LandmarkPoint>,
218 pub median_line: Vec<LandmarkPoint>,
219 pub outer_lips: Vec<LandmarkPoint>,
220 pub inner_lips: Vec<LandmarkPoint>,
221 pub left_pupil: Vec<LandmarkPoint>,
222 pub right_pupil: Vec<LandmarkPoint>,
223}
224
225impl FaceWithLandmarks {
226 #[must_use]
227 pub fn landmarks_2d(&self) -> FaceLandmarks2D {
228 let all_points = [
229 &self.face_contour,
230 &self.left_eye,
231 &self.right_eye,
232 &self.left_eyebrow,
233 &self.right_eyebrow,
234 &self.nose,
235 &self.nose_crest,
236 &self.median_line,
237 &self.outer_lips,
238 &self.inner_lips,
239 &self.left_pupil,
240 &self.right_pupil,
241 ]
242 .into_iter()
243 .flat_map(|region| region.iter().copied())
244 .collect::<Vec<_>>();
245
246 FaceLandmarks2D {
247 landmarks: FaceLandmarks {
248 confidence: self.confidence,
249 request_revision: None,
250 },
251 all_points: region_from_points(&all_points),
252 face_contour: region_from_points(&self.face_contour),
253 left_eye: region_from_points(&self.left_eye),
254 right_eye: region_from_points(&self.right_eye),
255 left_eyebrow: region_from_points(&self.left_eyebrow),
256 right_eyebrow: region_from_points(&self.right_eyebrow),
257 nose: region_from_points(&self.nose),
258 nose_crest: region_from_points(&self.nose_crest),
259 median_line: region_from_points(&self.median_line),
260 outer_lips: region_from_points(&self.outer_lips),
261 inner_lips: region_from_points(&self.inner_lips),
262 left_pupil: region_from_points(&self.left_pupil),
263 right_pupil: region_from_points(&self.right_pupil),
264 }
265 }
266}
267
268unsafe fn copy_region(ptr: *mut f64, n: usize) -> Vec<LandmarkPoint> {
275 if ptr.is_null() || n == 0 {
276 return Vec::new();
277 }
278 let mut v = Vec::with_capacity(n);
279 for i in 0..n {
280 let x = unsafe { *ptr.add(i * 2) };
282 let y = unsafe { *ptr.add(i * 2 + 1) };
284 v.push(LandmarkPoint { x, y });
285 }
286 v
287}
288
289fn region_from_points(points: &[LandmarkPoint]) -> Option<FaceLandmarkRegion2D> {
290 if points.is_empty() {
291 None
292 } else {
293 Some(FaceLandmarkRegion2D {
294 region: FaceLandmarkRegion {
295 point_count: points.len(),
296 request_revision: None,
297 },
298 normalized_points: points.to_vec(),
299 precision_estimates_per_point: Vec::new(),
300 points_classification: None,
301 })
302 }
303}
304
305pub fn detect_face_landmarks_in_path(
312 path: impl AsRef<Path>,
313) -> Result<Vec<FaceWithLandmarks>, VisionError> {
314 let path_str = path
315 .as_ref()
316 .to_str()
317 .ok_or_else(|| VisionError::InvalidArgument("non-UTF-8 path".into()))?;
318 let path_c = CString::new(path_str)
319 .map_err(|e| VisionError::InvalidArgument(format!("path NUL byte: {e}")))?;
320
321 let mut out_array: *mut core::ffi::c_void = ptr::null_mut();
322 let mut out_count: usize = 0;
323 let mut err_msg: *mut c_char = ptr::null_mut();
324
325 let status = unsafe {
327 ffi::vn_detect_face_landmarks_in_path(
328 path_c.as_ptr(),
329 &mut out_array,
330 &mut out_count,
331 &mut err_msg,
332 )
333 };
334 if status != ffi::status::OK {
335 return Err(unsafe { from_swift(status, err_msg) });
337 }
338 if out_array.is_null() || out_count == 0 {
339 return Ok(Vec::new());
340 }
341 let typed = out_array.cast::<ffi::FaceLandmarksRaw>();
342 let mut faces = Vec::with_capacity(out_count);
343 for i in 0..out_count {
344 let raw = unsafe { &*typed.add(i) };
346 faces.push(FaceWithLandmarks {
347 bounding_box: BoundingBox {
348 x: raw.bbox_x,
349 y: raw.bbox_y,
350 width: raw.bbox_w,
351 height: raw.bbox_h,
352 },
353 confidence: raw.confidence,
354 roll: if raw.roll.is_nan() {
355 None
356 } else {
357 Some(raw.roll)
358 },
359 yaw: if raw.yaw.is_nan() {
360 None
361 } else {
362 Some(raw.yaw)
363 },
364 pitch: if raw.pitch.is_nan() {
365 None
366 } else {
367 Some(raw.pitch)
368 },
369 face_contour: unsafe { copy_region(raw.face_contour, raw.face_contour_count) },
371 left_eye: unsafe { copy_region(raw.left_eye, raw.left_eye_count) },
373 right_eye: unsafe { copy_region(raw.right_eye, raw.right_eye_count) },
375 left_eyebrow: unsafe { copy_region(raw.left_eyebrow, raw.left_eyebrow_count) },
377 right_eyebrow: unsafe { copy_region(raw.right_eyebrow, raw.right_eyebrow_count) },
379 nose: unsafe { copy_region(raw.nose, raw.nose_count) },
381 nose_crest: unsafe { copy_region(raw.nose_crest, raw.nose_crest_count) },
383 median_line: unsafe { copy_region(raw.median_line, raw.median_line_count) },
385 outer_lips: unsafe { copy_region(raw.outer_lips, raw.outer_lips_count) },
387 inner_lips: unsafe { copy_region(raw.inner_lips, raw.inner_lips_count) },
389 left_pupil: unsafe { copy_region(raw.left_pupil, raw.left_pupil_count) },
391 right_pupil: unsafe { copy_region(raw.right_pupil, raw.right_pupil_count) },
393 });
394 }
395 unsafe { ffi::vn_face_landmarks_free(out_array, out_count) };
397 Ok(faces)
398}