Skip to main content

apple_vision/hand_pose/
mod.rs

1//! Human hand pose detection (`VNDetectHumanHandPoseRequest`).
2//!
3//! Re-uses [`crate::body_pose::DetectedBodyPose`] / [`JointPoint`] as the
4//! result shape — only the joint names differ (Apple's
5//! `VNHumanHandPoseObservationJointName`: `"wrist_joint"`,
6//! `"thumb_tip_joint"`, `"index_pip_joint"`, …).
7
8use core::ffi::c_char;
9use core::ptr;
10use std::ffi::CString;
11use std::path::Path;
12
13use crate::body_pose::{collect, DetectedBodyPose};
14pub use crate::body_pose::{DetectedBodyPose as DetectedHandPose, JointPoint};
15use crate::error::{from_swift, VisionError};
16use crate::ffi;
17use crate::recognized_points::{RecognizedPointsObservation, VisionRecognizedPoint};
18
19macro_rules! string_enum {
20    (
21        $(#[$meta:meta])*
22        pub enum $name:ident {
23            $( $variant:ident => $value:literal ),+ $(,)?
24        }
25    ) => {
26        $(#[$meta])*
27        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28        pub enum $name {
29            $( $variant ),+
30        }
31
32        impl $name {
33            pub const ALL: &'static [Self] = &[
34                $( Self::$variant ),+
35            ];
36
37            #[must_use]
38            pub const fn as_str(self) -> &'static str {
39                match self {
40                    $( Self::$variant => $value ),+
41                }
42            }
43
44            #[allow(clippy::should_implement_trait)]
45            #[must_use]
46            pub fn from_str(value: &str) -> Option<Self> {
47                match value {
48                    $( $value => Some(Self::$variant), )+
49                    _ => None,
50                }
51            }
52        }
53    };
54}
55
56string_enum! {
57    /// Mirrors `VNHumanHandPoseObservationJointName`.
58    pub enum HumanHandPoseJointName {
59        Wrist => "VNHLKWRI",
60        ThumbCmc => "VNHLKTCMC",
61        ThumbMp => "VNHLKTMP",
62        ThumbIp => "VNHLKTIP",
63        ThumbTip => "VNHLKTTIP",
64        IndexMcp => "VNHLKIMCP",
65        IndexPip => "VNHLKIPIP",
66        IndexDip => "VNHLKIDIP",
67        IndexTip => "VNHLKITIP",
68        MiddleMcp => "VNHLKMMCP",
69        MiddlePip => "VNHLKMPIP",
70        MiddleDip => "VNHLKMDIP",
71        MiddleTip => "VNHLKMTIP",
72        RingMcp => "VNHLKRMCP",
73        RingPip => "VNHLKRPIP",
74        RingDip => "VNHLKRDIP",
75        RingTip => "VNHLKRTIP",
76        LittleMcp => "VNHLKPMCP",
77        LittlePip => "VNHLKPPIP",
78        LittleDip => "VNHLKPDIP",
79        LittleTip => "VNHLKPTIP",
80    }
81}
82
83string_enum! {
84    /// Mirrors `VNHumanHandPoseObservationJointsGroupName`.
85    pub enum HumanHandPoseJointGroupName {
86        Thumb => "VNHLRKT",
87        IndexFinger => "VNHLRKI",
88        MiddleFinger => "VNHLRKM",
89        RingFinger => "VNHLRKR",
90        LittleFinger => "VNHLRKP",
91        All => "VNIPOAll",
92    }
93}
94
95/// The `VNChirality` reported by `VNHumanHandPoseObservation`.
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
97pub enum HandChirality {
98    Unknown,
99    Left,
100    Right,
101}
102
103/// A dedicated `VNHumanHandPoseObservation` wrapper.
104#[derive(Debug, Clone, PartialEq)]
105pub struct HumanHandPoseObservation {
106    pub recognized_points: RecognizedPointsObservation,
107    pub available_joint_names: Vec<String>,
108    pub available_joint_group_names: Vec<String>,
109    pub chirality: HandChirality,
110}
111
112const SUPPORTED_JOINT_NAMES: &[&str] = &[
113    HumanHandPoseJointName::Wrist.as_str(),
114    HumanHandPoseJointName::ThumbCmc.as_str(),
115    HumanHandPoseJointName::ThumbMp.as_str(),
116    HumanHandPoseJointName::ThumbIp.as_str(),
117    HumanHandPoseJointName::ThumbTip.as_str(),
118    HumanHandPoseJointName::IndexMcp.as_str(),
119    HumanHandPoseJointName::IndexPip.as_str(),
120    HumanHandPoseJointName::IndexDip.as_str(),
121    HumanHandPoseJointName::IndexTip.as_str(),
122    HumanHandPoseJointName::MiddleMcp.as_str(),
123    HumanHandPoseJointName::MiddlePip.as_str(),
124    HumanHandPoseJointName::MiddleDip.as_str(),
125    HumanHandPoseJointName::MiddleTip.as_str(),
126    HumanHandPoseJointName::RingMcp.as_str(),
127    HumanHandPoseJointName::RingPip.as_str(),
128    HumanHandPoseJointName::RingDip.as_str(),
129    HumanHandPoseJointName::RingTip.as_str(),
130    HumanHandPoseJointName::LittleMcp.as_str(),
131    HumanHandPoseJointName::LittlePip.as_str(),
132    HumanHandPoseJointName::LittleDip.as_str(),
133    HumanHandPoseJointName::LittleTip.as_str(),
134];
135
136const SUPPORTED_JOINT_GROUP_NAMES: &[&str] = &[
137    HumanHandPoseJointGroupName::Thumb.as_str(),
138    HumanHandPoseJointGroupName::IndexFinger.as_str(),
139    HumanHandPoseJointGroupName::MiddleFinger.as_str(),
140    HumanHandPoseJointGroupName::RingFinger.as_str(),
141    HumanHandPoseJointGroupName::LittleFinger.as_str(),
142    HumanHandPoseJointGroupName::All.as_str(),
143];
144
145impl HumanHandPoseObservation {
146    #[must_use]
147    pub const fn supported_joint_name_keys() -> &'static [HumanHandPoseJointName] {
148        HumanHandPoseJointName::ALL
149    }
150
151    #[must_use]
152    pub const fn supported_joint_names() -> &'static [&'static str] {
153        SUPPORTED_JOINT_NAMES
154    }
155
156    #[must_use]
157    pub const fn supported_joint_group_name_keys() -> &'static [HumanHandPoseJointGroupName] {
158        HumanHandPoseJointGroupName::ALL
159    }
160
161    #[must_use]
162    pub const fn supported_joint_group_names() -> &'static [&'static str] {
163        SUPPORTED_JOINT_GROUP_NAMES
164    }
165
166    #[must_use]
167    pub fn recognized_point(
168        &self,
169        joint_name: HumanHandPoseJointName,
170    ) -> Option<VisionRecognizedPoint> {
171        self.recognized_points.recognized_point(joint_name.as_str())
172    }
173
174    #[must_use]
175    pub fn into_detected_hand_pose(self) -> DetectedHandPose {
176        self.into()
177    }
178}
179
180impl From<DetectedHandPose> for HumanHandPoseObservation {
181    fn from(value: DetectedHandPose) -> Self {
182        let body = crate::body_pose::HumanBodyPoseObservation::from(value);
183        Self {
184            recognized_points: body.recognized_points,
185            available_joint_names: body.available_joint_names,
186            available_joint_group_names: Self::supported_joint_group_names()
187                .iter()
188                .map(|name| (*name).to_string())
189                .collect(),
190            chirality: HandChirality::Unknown,
191        }
192    }
193}
194
195impl From<HumanHandPoseObservation> for DetectedHandPose {
196    fn from(value: HumanHandPoseObservation) -> Self {
197        crate::body_pose::HumanBodyPoseObservation {
198            recognized_points: value.recognized_points,
199            available_joint_names: value.available_joint_names,
200            available_joint_group_names: value.available_joint_group_names,
201        }
202        .into()
203    }
204}
205
206/// Detect up to `max_hands` (use `0` for "no limit") in the image at
207/// `path`.
208///
209/// # Errors
210///
211/// Returns [`VisionError::ImageLoadFailed`] / [`VisionError::RequestFailed`].
212pub fn detect_human_hand_pose_in_path(
213    path: impl AsRef<Path>,
214    max_hands: usize,
215) -> Result<Vec<DetectedBodyPose>, VisionError> {
216    let path_str = path
217        .as_ref()
218        .to_str()
219        .ok_or_else(|| VisionError::InvalidArgument("non-UTF-8 path".into()))?;
220    let path_c = CString::new(path_str)
221        .map_err(|e| VisionError::InvalidArgument(format!("path NUL byte: {e}")))?;
222
223    let mut out_array: *mut core::ffi::c_void = ptr::null_mut();
224    let mut out_count: usize = 0;
225    let mut err_msg: *mut c_char = ptr::null_mut();
226    // SAFETY: all pointer arguments are valid stack locations or null-initialised out-params; strings are valid C strings for the duration of the call.
227    let status = unsafe {
228        ffi::vn_detect_human_hand_pose_in_path(
229            path_c.as_ptr(),
230            max_hands,
231            &mut out_array,
232            &mut out_count,
233            &mut err_msg,
234        )
235    };
236    if status != ffi::status::OK {
237        // SAFETY: the error pointer is either null or a bridge-allocated C string; `from_swift` frees it.
238        return Err(unsafe { from_swift(status, err_msg) });
239    }
240    // SAFETY: the pointer/count pair comes directly from the bridge and `collect` consumes it exactly once.
241    Ok(unsafe { collect(out_array, out_count) })
242}
243
244/// Detect dedicated `VNHumanHandPoseObservation` wrappers in the image at `path`.
245///
246/// # Errors
247///
248/// Returns [`VisionError::ImageLoadFailed`] / [`VisionError::RequestFailed`].
249pub fn detect_human_hand_pose_observations_in_path(
250    path: impl AsRef<Path>,
251    max_hands: usize,
252) -> Result<Vec<HumanHandPoseObservation>, VisionError> {
253    detect_human_hand_pose_in_path(path, max_hands)
254        .map(|poses| poses.into_iter().map(Into::into).collect())
255}