Skip to main content

warden_vision/core/
keypoints.rs

1//! Keypoint (pose/landmark) data structures, mirroring the role of
2//! [`crate::core::Detections`] but for per-object sets of named joints
3//! rather than a single bounding box.
4
5/// A single detected joint.
6#[derive(Debug, Clone, Copy, PartialEq)]
7pub struct Keypoint {
8    /// X coordinate, in absolute pixel space.
9    pub x: f32,
10    /// Y coordinate, in absolute pixel space.
11    pub y: f32,
12    /// Detector confidence for this specific joint, typically in `[0, 1]`.
13    pub confidence: f32,
14}
15
16impl Keypoint {
17    /// Creates a new keypoint.
18    pub fn new(x: f32, y: f32, confidence: f32) -> Self {
19        Self { x, y, confidence }
20    }
21}
22
23/// The joints detected for a single object instance.
24///
25/// `points[i]` is `None` when joint `i` (indexed per whatever skeleton
26/// layout the caller is using, e.g. [`COCO_17_EDGES`]) was not detected for
27/// this instance, keeping the slot present so joint index stays meaningful
28/// even when some joints are missing.
29#[derive(Debug, Clone, Default, PartialEq)]
30pub struct KeypointSet {
31    /// Per-joint keypoints, indexed by joint id.
32    pub points: Vec<Option<Keypoint>>,
33    /// Index of the predicted class (e.g. which kind of skeleton this is).
34    pub class_id: usize,
35}
36
37impl KeypointSet {
38    /// Creates a new keypoint set.
39    pub fn new(points: Vec<Option<Keypoint>>, class_id: usize) -> Self {
40        Self { points, class_id }
41    }
42
43    /// Number of joint slots (detected or not).
44    pub fn len(&self) -> usize {
45        self.points.len()
46    }
47
48    /// Whether this set has no joint slots at all.
49    pub fn is_empty(&self) -> bool {
50        self.points.is_empty()
51    }
52
53    /// The keypoint at `index`, if that joint was detected.
54    pub fn get(&self, index: usize) -> Option<Keypoint> {
55        self.points.get(index).copied().flatten()
56    }
57}
58
59/// A collection of [`KeypointSet`]s produced for a single frame.
60#[derive(Debug, Clone, Default, PartialEq)]
61pub struct KeyPoints {
62    /// The underlying keypoint sets, one per detected object instance.
63    pub keypoint_sets: Vec<KeypointSet>,
64}
65
66impl KeyPoints {
67    /// Creates a `KeyPoints` from an existing vector.
68    pub fn new(keypoint_sets: Vec<KeypointSet>) -> Self {
69        Self { keypoint_sets }
70    }
71
72    /// An empty `KeyPoints`.
73    pub fn empty() -> Self {
74        Self::default()
75    }
76
77    /// Number of object instances.
78    pub fn len(&self) -> usize {
79        self.keypoint_sets.len()
80    }
81
82    /// Whether there are no keypoint sets.
83    pub fn is_empty(&self) -> bool {
84        self.keypoint_sets.is_empty()
85    }
86
87    /// Iterates over keypoint sets by reference.
88    pub fn iter(&self) -> std::slice::Iter<'_, KeypointSet> {
89        self.keypoint_sets.iter()
90    }
91
92    /// Iterates over keypoint sets by mutable reference.
93    pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, KeypointSet> {
94        self.keypoint_sets.iter_mut()
95    }
96
97    /// Returns a new `KeyPoints` with the same instances, but with any
98    /// individual joint below `threshold` confidence (or missing) cleared
99    /// to `None`. Unlike [`crate::core::Detections::filter_by_confidence`],
100    /// this filters per-joint rather than dropping whole instances, since a
101    /// single low-confidence joint (e.g. an occluded wrist) shouldn't
102    /// discard an otherwise good pose estimate.
103    pub fn filter_by_confidence(&self, threshold: f32) -> KeyPoints {
104        let sets = self
105            .keypoint_sets
106            .iter()
107            .map(|set| {
108                let points = set
109                    .points
110                    .iter()
111                    .map(|p| p.filter(|kp| kp.confidence >= threshold))
112                    .collect();
113                KeypointSet::new(points, set.class_id)
114            })
115            .collect();
116        KeyPoints::new(sets)
117    }
118}
119
120impl IntoIterator for KeyPoints {
121    type Item = KeypointSet;
122    type IntoIter = std::vec::IntoIter<KeypointSet>;
123
124    fn into_iter(self) -> Self::IntoIter {
125        self.keypoint_sets.into_iter()
126    }
127}
128
129impl<'a> IntoIterator for &'a KeyPoints {
130    type Item = &'a KeypointSet;
131    type IntoIter = std::slice::Iter<'a, KeypointSet>;
132
133    fn into_iter(self) -> Self::IntoIter {
134        self.keypoint_sets.iter()
135    }
136}
137
138/// The 19 bone connections of the standard 17-joint COCO pose skeleton, as
139/// `(joint_index, joint_index)` pairs into a 0-indexed
140/// `[nose, left_eye, right_eye, left_ear, right_ear, left_shoulder,
141/// right_shoulder, left_elbow, right_elbow, left_wrist, right_wrist,
142/// left_hip, right_hip, left_knee, right_knee, left_ankle, right_ankle]`
143/// joint layout (COCO's official 1-indexed `skeleton` field, shifted down
144/// by one).
145pub const COCO_17_EDGES: &[(usize, usize)] = &[
146    (15, 13),
147    (13, 11),
148    (16, 14),
149    (14, 12),
150    (11, 12),
151    (5, 11),
152    (6, 12),
153    (5, 6),
154    (5, 7),
155    (6, 8),
156    (7, 9),
157    (8, 10),
158    (1, 2),
159    (0, 1),
160    (0, 2),
161    (1, 3),
162    (2, 4),
163    (3, 5),
164    (4, 6),
165];
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn keypoint_set_get_returns_none_for_missing_joint() {
173        let set = KeypointSet::new(vec![Some(Keypoint::new(1.0, 2.0, 0.9)), None], 0);
174        assert!(set.get(0).is_some());
175        assert!(set.get(1).is_none());
176        assert!(set.get(5).is_none());
177    }
178
179    #[test]
180    fn filter_by_confidence_clears_low_confidence_joints_only() {
181        let sets = vec![KeypointSet::new(
182            vec![
183                Some(Keypoint::new(0.0, 0.0, 0.9)),
184                Some(Keypoint::new(1.0, 1.0, 0.1)),
185                None,
186            ],
187            0,
188        )];
189        let filtered = KeyPoints::new(sets).filter_by_confidence(0.5);
190        assert!(filtered.keypoint_sets[0].get(0).is_some());
191        assert!(filtered.keypoint_sets[0].get(1).is_none());
192        assert!(filtered.keypoint_sets[0].get(2).is_none());
193        // Instance itself is retained even though one joint was cleared.
194        assert_eq!(filtered.len(), 1);
195    }
196
197    #[test]
198    fn coco_17_edges_reference_valid_joint_indices() {
199        for &(a, b) in COCO_17_EDGES {
200            assert!(a < 17);
201            assert!(b < 17);
202        }
203        assert_eq!(COCO_17_EDGES.len(), 19);
204    }
205}