//! ADR-117 P2 — PyO3 bindings for `wifi_densepose_core::Keypoint` + //! `KeypointType` + `Confidence`. //! //! Design notes (consequential for the Python API surface): //! //! 1. **`Confidence` is NOT bound as a separate Python class.** End //! users hate having to construct a wrapper just to pass a float. //! Python-side, confidence is just an `f32` in `[0.0, 1.0]`; the //! binding validates on the way in. //! //! 2. **`KeypointType` is bound as a `#[pyclass]` enum** (PyO3 0.22 //! supports `#[pyclass(eq, eq_int)]` for C-like enums). Python-side //! it surfaces as `wifi_densepose.KeypointType.Nose`, etc. //! //! 3. **`Keypoint` constructor accepts `z` as `Optional[float]`** so //! Python users can pass `Keypoint(KeypointType.Nose, 0.5, 0.3, //! 0.95)` for 2D or `Keypoint(..., z=0.1)` for 3D. use pyo3::prelude::*; use wifi_densepose_core::{Confidence, Keypoint, KeypointType}; // ─── KeypointType ──────────────────────────────────────────────────── /// COCO-17 keypoint identifier — re-export of the Rust core enum. /// /// Python: /// ```python /// from wifi_densepose import KeypointType /// kp = KeypointType.Nose /// print(kp.name) # "Nose" /// ``` // `hash` makes the enum hashable in Python (usable as dict keys + set // members) — derived from `Hash` on the Rust side. `frozen` is a // hard requirement for `hash` per pyo3 contract. #[pyclass(eq, eq_int, hash, frozen, name = "KeypointType")] #[derive(Clone, Copy, PartialEq, Eq, Hash)] pub enum PyKeypointType { Nose = 0, LeftEye = 1, RightEye = 2, LeftEar = 3, RightEar = 4, LeftShoulder = 5, RightShoulder = 6, LeftElbow = 7, RightElbow = 8, LeftWrist = 9, RightWrist = 10, LeftHip = 11, RightHip = 12, LeftKnee = 13, RightKnee = 14, LeftAnkle = 15, RightAnkle = 16, } #[pymethods] impl PyKeypointType { /// Lowercase snake_case name (matches the COCO standard). #[getter] fn snake_name(&self) -> &'static str { self.as_rust().name() } /// Integer index 0–16 (COCO ordering). #[getter] fn index(&self) -> u8 { (*self).into() } /// True if this keypoint is on the face (nose, eyes, ears). fn is_face(&self) -> bool { self.as_rust().is_face() } /// True if this keypoint is in the upper body (shoulders, elbows, wrists). fn is_upper_body(&self) -> bool { self.as_rust().is_upper_body() } /// All 17 keypoint types in COCO order. Useful for Jupyter /// enumeration: `for kp in KeypointType.all(): ...`. #[staticmethod] fn all() -> Vec { KeypointType::all().iter().map(|k| PyKeypointType::from_rust(*k)).collect() } fn __repr__(&self) -> String { format!("KeypointType.{:?}", self.as_rust()) } } impl PyKeypointType { pub(crate) fn as_rust(&self) -> KeypointType { // SAFETY equivalent: the enum variants line up 1:1 with the // Rust enum's `#[repr(u8)]` discriminants. The match below is // exhaustive on both sides so a future addition to either side // fails to compile until the other is updated. match self { Self::Nose => KeypointType::Nose, Self::LeftEye => KeypointType::LeftEye, Self::RightEye => KeypointType::RightEye, Self::LeftEar => KeypointType::LeftEar, Self::RightEar => KeypointType::RightEar, Self::LeftShoulder => KeypointType::LeftShoulder, Self::RightShoulder => KeypointType::RightShoulder, Self::LeftElbow => KeypointType::LeftElbow, Self::RightElbow => KeypointType::RightElbow, Self::LeftWrist => KeypointType::LeftWrist, Self::RightWrist => KeypointType::RightWrist, Self::LeftHip => KeypointType::LeftHip, Self::RightHip => KeypointType::RightHip, Self::LeftKnee => KeypointType::LeftKnee, Self::RightKnee => KeypointType::RightKnee, Self::LeftAnkle => KeypointType::LeftAnkle, Self::RightAnkle => KeypointType::RightAnkle, } } pub(crate) fn from_rust(k: KeypointType) -> Self { match k { KeypointType::Nose => Self::Nose, KeypointType::LeftEye => Self::LeftEye, KeypointType::RightEye => Self::RightEye, KeypointType::LeftEar => Self::LeftEar, KeypointType::RightEar => Self::RightEar, KeypointType::LeftShoulder => Self::LeftShoulder, KeypointType::RightShoulder => Self::RightShoulder, KeypointType::LeftElbow => Self::LeftElbow, KeypointType::RightElbow => Self::RightElbow, KeypointType::LeftWrist => Self::LeftWrist, KeypointType::RightWrist => Self::RightWrist, KeypointType::LeftHip => Self::LeftHip, KeypointType::RightHip => Self::RightHip, KeypointType::LeftKnee => Self::LeftKnee, KeypointType::RightKnee => Self::RightKnee, KeypointType::LeftAnkle => Self::LeftAnkle, KeypointType::RightAnkle => Self::RightAnkle, } } } impl From for u8 { fn from(k: PyKeypointType) -> u8 { k as u8 } } impl PyKeypoint { /// Rust-side accessor for the inner Keypoint (used by pose.rs). /// Not exposed to Python — Python users go through the /// #[pymethods] getters above. pub(crate) fn inner(&self) -> &Keypoint { &self.inner } /// Rust-side constructor from a core Keypoint (used by pose.rs /// when re-wrapping outputs of PersonPose methods). pub(crate) fn from_rust(k: Keypoint) -> Self { Self { inner: k } } } // ─── Keypoint ──────────────────────────────────────────────────────── /// Single skeletal joint with COCO type, 2D-or-3D position, and a /// confidence score in [0.0, 1.0]. /// /// Python: /// ```python /// from wifi_densepose import Keypoint, KeypointType /// /// kp = Keypoint(KeypointType.Nose, 0.5, 0.3, 0.95) /// print(kp.x, kp.y, kp.confidence, kp.is_visible) /// /// kp_3d = Keypoint(KeypointType.LeftWrist, 0.2, 0.4, 0.8, z=0.1) /// print(kp_3d.position_3d) # (0.2, 0.4, 0.1) /// ``` #[pyclass(frozen, name = "Keypoint")] #[derive(Clone)] pub struct PyKeypoint { inner: Keypoint, } #[pymethods] impl PyKeypoint { /// Construct a new keypoint. Confidence must be in [0.0, 1.0]. /// `z` is optional — omit for a 2D keypoint, supply for 3D. #[new] #[pyo3(signature = (keypoint_type, x, y, confidence, *, z=None))] fn new( keypoint_type: PyKeypointType, x: f32, y: f32, confidence: f32, z: Option, ) -> PyResult { let conf = Confidence::new(confidence).map_err(|e| { pyo3::exceptions::PyValueError::new_err(e.to_string()) })?; let inner = match z { Some(zv) => Keypoint::new_3d(keypoint_type.as_rust(), x, y, zv, conf), None => Keypoint::new(keypoint_type.as_rust(), x, y, conf), }; Ok(Self { inner }) } /// COCO keypoint type. #[getter] fn keypoint_type(&self) -> PyKeypointType { PyKeypointType::from_rust(self.inner.keypoint_type) } /// X coordinate. #[getter] fn x(&self) -> f32 { self.inner.x } /// Y coordinate. #[getter] fn y(&self) -> f32 { self.inner.y } /// Z coordinate, or None for 2D keypoints. #[getter] fn z(&self) -> Option { self.inner.z } /// Detection confidence in [0.0, 1.0]. #[getter] fn confidence(&self) -> f32 { self.inner.confidence.value() } /// True if this keypoint clears the default visibility threshold /// (`confidence >= 0.5`). #[getter] fn is_visible(&self) -> bool { self.inner.is_visible() } /// 2D position as a tuple `(x, y)`. #[getter] fn position_2d(&self) -> (f32, f32) { self.inner.position_2d() } /// 3D position as a tuple `(x, y, z)`, or None for 2D keypoints. #[getter] fn position_3d(&self) -> Option<(f32, f32, f32)> { self.inner.position_3d() } /// Euclidean distance to another keypoint. If both are 3D the /// distance includes the z-axis; otherwise it's 2D only. fn distance_to(&self, other: &PyKeypoint) -> f32 { self.inner.distance_to(&other.inner) } fn __repr__(&self) -> String { match self.inner.z { Some(z) => format!( "Keypoint(KeypointType.{:?}, x={}, y={}, z={}, confidence={:.4})", self.inner.keypoint_type, self.inner.x, self.inner.y, z, self.inner.confidence.value() ), None => format!( "Keypoint(KeypointType.{:?}, x={}, y={}, confidence={:.4})", self.inner.keypoint_type, self.inner.x, self.inner.y, self.inner.confidence.value() ), } } fn __eq__(&self, other: &PyKeypoint) -> bool { self.inner.keypoint_type == other.inner.keypoint_type && self.inner.x == other.inner.x && self.inner.y == other.inner.y && self.inner.z == other.inner.z && (self.inner.confidence.value() - other.inner.confidence.value()).abs() < f32::EPSILON } } /// Register the binding types with the `_native` PyModule. pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; Ok(()) }