use core::ffi::c_char;
use core::ptr;
use std::ffi::CString;
use std::path::Path;
use crate::error::{from_swift, VisionError};
use crate::ffi;
use crate::recognize_text::BoundingBox;
use crate::request_base::{ImageBasedRequest, RequestRevisionProviding};
use crate::sdk::PointsClassification;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LandmarkPoint {
pub x: f64,
pub y: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(usize)]
pub enum RequestFaceLandmarksConstellation {
NotDefined = 0,
Points65 = 1,
Points76 = 2,
}
impl RequestFaceLandmarksConstellation {
pub const ALL: &'static [Self] = &[Self::NotDefined, Self::Points65, Self::Points76];
#[must_use]
pub const fn as_raw(self) -> usize {
self as usize
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FaceLandmarkRegion {
pub point_count: usize,
pub request_revision: Option<usize>,
}
impl RequestRevisionProviding for FaceLandmarkRegion {
fn request_revision(&self) -> Option<usize> {
self.request_revision
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct FaceLandmarkRegion2D {
pub region: FaceLandmarkRegion,
pub normalized_points: Vec<LandmarkPoint>,
pub precision_estimates_per_point: Vec<f32>,
pub points_classification: Option<PointsClassification>,
}
impl FaceLandmarkRegion2D {
#[must_use]
pub fn points_in_image_of_size(&self, image_size: (f64, f64)) -> Vec<LandmarkPoint> {
self.normalized_points
.iter()
.map(|point| LandmarkPoint {
x: point.x * image_size.0,
y: point.y * image_size.1,
})
.collect()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct FaceLandmarks {
pub confidence: f32,
pub request_revision: Option<usize>,
}
impl RequestRevisionProviding for FaceLandmarks {
fn request_revision(&self) -> Option<usize> {
self.request_revision
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct FaceLandmarks2D {
pub landmarks: FaceLandmarks,
pub all_points: Option<FaceLandmarkRegion2D>,
pub face_contour: Option<FaceLandmarkRegion2D>,
pub left_eye: Option<FaceLandmarkRegion2D>,
pub right_eye: Option<FaceLandmarkRegion2D>,
pub left_eyebrow: Option<FaceLandmarkRegion2D>,
pub right_eyebrow: Option<FaceLandmarkRegion2D>,
pub nose: Option<FaceLandmarkRegion2D>,
pub nose_crest: Option<FaceLandmarkRegion2D>,
pub median_line: Option<FaceLandmarkRegion2D>,
pub outer_lips: Option<FaceLandmarkRegion2D>,
pub inner_lips: Option<FaceLandmarkRegion2D>,
pub left_pupil: Option<FaceLandmarkRegion2D>,
pub right_pupil: Option<FaceLandmarkRegion2D>,
}
pub trait FaceObservationAccepting {
fn input_face_observations(&self) -> &[BoundingBox];
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct FaceLandmarksRequest {
image_based_request: ImageBasedRequest,
constellation: Option<RequestFaceLandmarksConstellation>,
input_face_observations: Vec<BoundingBox>,
}
impl FaceLandmarksRequest {
#[must_use]
pub const fn new() -> Self {
Self {
image_based_request: ImageBasedRequest::new(),
constellation: None,
input_face_observations: Vec::new(),
}
}
#[must_use]
pub const fn with_image_based_request(
mut self,
image_based_request: ImageBasedRequest,
) -> Self {
self.image_based_request = image_based_request;
self
}
#[must_use]
pub const fn with_constellation(
mut self,
constellation: RequestFaceLandmarksConstellation,
) -> Self {
self.constellation = Some(constellation);
self
}
#[must_use]
pub fn with_input_face_observations(
mut self,
input_face_observations: Vec<BoundingBox>,
) -> Self {
self.input_face_observations = input_face_observations;
self
}
#[must_use]
pub const fn image_based_request(&self) -> &ImageBasedRequest {
&self.image_based_request
}
#[must_use]
pub const fn constellation(&self) -> Option<RequestFaceLandmarksConstellation> {
self.constellation
}
#[must_use]
pub const fn supports_constellation(
request_revision: usize,
constellation: RequestFaceLandmarksConstellation,
) -> bool {
matches!(
(request_revision, constellation),
(_, RequestFaceLandmarksConstellation::NotDefined)
| (1 | 2, RequestFaceLandmarksConstellation::Points65)
| (
3,
RequestFaceLandmarksConstellation::Points65
| RequestFaceLandmarksConstellation::Points76,
)
)
}
}
impl FaceObservationAccepting for FaceLandmarksRequest {
fn input_face_observations(&self) -> &[BoundingBox] {
&self.input_face_observations
}
}
impl RequestRevisionProviding for FaceLandmarksRequest {
fn request_revision(&self) -> Option<usize> {
self.image_based_request.revision()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct FaceWithLandmarks {
pub bounding_box: BoundingBox,
pub confidence: f32,
pub roll: Option<f32>,
pub yaw: Option<f32>,
pub pitch: Option<f32>,
pub face_contour: Vec<LandmarkPoint>,
pub left_eye: Vec<LandmarkPoint>,
pub right_eye: Vec<LandmarkPoint>,
pub left_eyebrow: Vec<LandmarkPoint>,
pub right_eyebrow: Vec<LandmarkPoint>,
pub nose: Vec<LandmarkPoint>,
pub nose_crest: Vec<LandmarkPoint>,
pub median_line: Vec<LandmarkPoint>,
pub outer_lips: Vec<LandmarkPoint>,
pub inner_lips: Vec<LandmarkPoint>,
pub left_pupil: Vec<LandmarkPoint>,
pub right_pupil: Vec<LandmarkPoint>,
}
impl FaceWithLandmarks {
#[must_use]
pub fn landmarks_2d(&self) -> FaceLandmarks2D {
let all_points = [
&self.face_contour,
&self.left_eye,
&self.right_eye,
&self.left_eyebrow,
&self.right_eyebrow,
&self.nose,
&self.nose_crest,
&self.median_line,
&self.outer_lips,
&self.inner_lips,
&self.left_pupil,
&self.right_pupil,
]
.into_iter()
.flat_map(|region| region.iter().copied())
.collect::<Vec<_>>();
FaceLandmarks2D {
landmarks: FaceLandmarks {
confidence: self.confidence,
request_revision: None,
},
all_points: region_from_points(&all_points),
face_contour: region_from_points(&self.face_contour),
left_eye: region_from_points(&self.left_eye),
right_eye: region_from_points(&self.right_eye),
left_eyebrow: region_from_points(&self.left_eyebrow),
right_eyebrow: region_from_points(&self.right_eyebrow),
nose: region_from_points(&self.nose),
nose_crest: region_from_points(&self.nose_crest),
median_line: region_from_points(&self.median_line),
outer_lips: region_from_points(&self.outer_lips),
inner_lips: region_from_points(&self.inner_lips),
left_pupil: region_from_points(&self.left_pupil),
right_pupil: region_from_points(&self.right_pupil),
}
}
}
unsafe fn copy_region(ptr: *mut f64, n: usize) -> Vec<LandmarkPoint> {
if ptr.is_null() || n == 0 {
return Vec::new();
}
let mut v = Vec::with_capacity(n);
for i in 0..n {
let x = unsafe { *ptr.add(i * 2) };
let y = unsafe { *ptr.add(i * 2 + 1) };
v.push(LandmarkPoint { x, y });
}
v
}
fn region_from_points(points: &[LandmarkPoint]) -> Option<FaceLandmarkRegion2D> {
if points.is_empty() {
None
} else {
Some(FaceLandmarkRegion2D {
region: FaceLandmarkRegion {
point_count: points.len(),
request_revision: None,
},
normalized_points: points.to_vec(),
precision_estimates_per_point: Vec::new(),
points_classification: None,
})
}
}
pub fn detect_face_landmarks_in_path(
path: impl AsRef<Path>,
) -> Result<Vec<FaceWithLandmarks>, VisionError> {
let path_str = path
.as_ref()
.to_str()
.ok_or_else(|| VisionError::InvalidArgument("non-UTF-8 path".into()))?;
let path_c = CString::new(path_str)
.map_err(|e| VisionError::InvalidArgument(format!("path NUL byte: {e}")))?;
let mut out_array: *mut core::ffi::c_void = ptr::null_mut();
let mut out_count: usize = 0;
let mut err_msg: *mut c_char = ptr::null_mut();
let status = unsafe {
ffi::vn_detect_face_landmarks_in_path(
path_c.as_ptr(),
&mut out_array,
&mut out_count,
&mut err_msg,
)
};
if status != ffi::status::OK {
return Err(unsafe { from_swift(status, err_msg) });
}
if out_array.is_null() || out_count == 0 {
return Ok(Vec::new());
}
let typed = out_array.cast::<ffi::FaceLandmarksRaw>();
let mut faces = Vec::with_capacity(out_count);
for i in 0..out_count {
let raw = unsafe { &*typed.add(i) };
faces.push(FaceWithLandmarks {
bounding_box: BoundingBox {
x: raw.bbox_x,
y: raw.bbox_y,
width: raw.bbox_w,
height: raw.bbox_h,
},
confidence: raw.confidence,
roll: if raw.roll.is_nan() {
None
} else {
Some(raw.roll)
},
yaw: if raw.yaw.is_nan() {
None
} else {
Some(raw.yaw)
},
pitch: if raw.pitch.is_nan() {
None
} else {
Some(raw.pitch)
},
face_contour: unsafe { copy_region(raw.face_contour, raw.face_contour_count) },
left_eye: unsafe { copy_region(raw.left_eye, raw.left_eye_count) },
right_eye: unsafe { copy_region(raw.right_eye, raw.right_eye_count) },
left_eyebrow: unsafe { copy_region(raw.left_eyebrow, raw.left_eyebrow_count) },
right_eyebrow: unsafe { copy_region(raw.right_eyebrow, raw.right_eyebrow_count) },
nose: unsafe { copy_region(raw.nose, raw.nose_count) },
nose_crest: unsafe { copy_region(raw.nose_crest, raw.nose_crest_count) },
median_line: unsafe { copy_region(raw.median_line, raw.median_line_count) },
outer_lips: unsafe { copy_region(raw.outer_lips, raw.outer_lips_count) },
inner_lips: unsafe { copy_region(raw.inner_lips, raw.inner_lips_count) },
left_pupil: unsafe { copy_region(raw.left_pupil, raw.left_pupil_count) },
right_pupil: unsafe { copy_region(raw.right_pupil, raw.right_pupil_count) },
});
}
unsafe { ffi::vn_face_landmarks_free(out_array, out_count) };
Ok(faces)
}