1use crate::{VisionError, VisionResult};
4
5#[derive(Clone, Copy, Debug, PartialEq)]
7pub struct Keypoint2 {
8 x: f32,
9 y: f32,
10 size: f32,
11 angle_degrees: Option<f32>,
12 response: f32,
13 octave: i32,
14 class_id: Option<i32>,
15}
16
17impl Keypoint2 {
18 pub fn try_new(x: f32, y: f32, response: f32) -> VisionResult<Self> {
20 if !x.is_finite() || !y.is_finite() || !response.is_finite() {
21 return Err(VisionError::InvalidParameter(
22 "keypoint coordinates and response must be finite".into(),
23 ));
24 }
25 Ok(Self { x, y, size: 1.0, angle_degrees: None, response, octave: 0, class_id: None })
26 }
27
28 pub fn with_size(mut self, size: f32) -> VisionResult<Self> {
30 if !size.is_finite() || size <= 0.0 {
31 return Err(VisionError::InvalidParameter(
32 "keypoint size must be positive and finite".into(),
33 ));
34 }
35 self.size = size;
36 Ok(self)
37 }
38
39 pub fn with_angle_degrees(mut self, angle: f32) -> VisionResult<Self> {
41 if !angle.is_finite() {
42 return Err(VisionError::InvalidParameter("keypoint angle must be finite".into()));
43 }
44 self.angle_degrees = Some(angle.rem_euclid(360.0));
45 Ok(self)
46 }
47
48 #[must_use]
50 pub const fn with_octave(mut self, octave: i32) -> Self {
51 self.octave = octave;
52 self
53 }
54
55 #[must_use]
57 pub const fn with_class_id(mut self, class_id: i32) -> Self {
58 self.class_id = Some(class_id);
59 self
60 }
61
62 pub const fn x(self) -> f32 {
64 self.x
65 }
66
67 pub const fn y(self) -> f32 {
69 self.y
70 }
71
72 pub const fn size(self) -> f32 {
74 self.size
75 }
76
77 pub const fn angle_degrees(self) -> Option<f32> {
79 self.angle_degrees
80 }
81
82 pub const fn response(self) -> f32 {
84 self.response
85 }
86
87 pub const fn octave(self) -> i32 {
89 self.octave
90 }
91
92 pub const fn class_id(self) -> Option<i32> {
94 self.class_id
95 }
96}
97
98#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
100pub enum DescriptorKind {
101 Binary,
103 Float32,
105}
106
107#[derive(Clone, Debug, PartialEq)]
108enum DescriptorStorage {
109 Binary(Vec<u8>),
110 Float32(Vec<f32>),
111}
112
113#[derive(Clone, Debug, PartialEq)]
115pub struct DescriptorBuffer {
116 rows: usize,
117 width: usize,
118 storage: DescriptorStorage,
119}
120
121impl DescriptorBuffer {
122 pub fn try_binary(rows: usize, bytes_per_row: usize, data: Vec<u8>) -> VisionResult<Self> {
124 Self::validate_layout(rows, bytes_per_row, data.len())?;
125 Ok(Self { rows, width: bytes_per_row, storage: DescriptorStorage::Binary(data) })
126 }
127
128 pub fn try_float32(rows: usize, values_per_row: usize, data: Vec<f32>) -> VisionResult<Self> {
130 if data.iter().any(|value| !value.is_finite()) {
131 return Err(VisionError::InvalidParameter(
132 "float descriptors must contain only finite values".into(),
133 ));
134 }
135 Self::validate_layout(rows, values_per_row, data.len())?;
136 Ok(Self { rows, width: values_per_row, storage: DescriptorStorage::Float32(data) })
137 }
138
139 fn validate_layout(rows: usize, width: usize, actual: usize) -> VisionResult<()> {
140 if width == 0 {
141 return Err(VisionError::InvalidParameter("descriptor width must be positive".into()));
142 }
143 let expected = rows
144 .checked_mul(width)
145 .ok_or_else(|| VisionError::InvalidParameter("descriptor layout overflows".into()))?;
146 if actual != expected {
147 return Err(VisionError::DescriptorLayout { rows, width, expected, actual });
148 }
149 Ok(())
150 }
151
152 pub const fn kind(&self) -> DescriptorKind {
154 match self.storage {
155 DescriptorStorage::Binary(_) => DescriptorKind::Binary,
156 DescriptorStorage::Float32(_) => DescriptorKind::Float32,
157 }
158 }
159
160 pub const fn len(&self) -> usize {
162 self.rows
163 }
164
165 pub const fn is_empty(&self) -> bool {
167 self.rows == 0
168 }
169
170 pub const fn width(&self) -> usize {
172 self.width
173 }
174
175 pub fn binary_row(&self, index: usize) -> Option<&[u8]> {
177 let DescriptorStorage::Binary(data) = &self.storage else {
178 return None;
179 };
180 let start = index.checked_mul(self.width)?;
181 data.get(start..start + self.width)
182 }
183
184 pub fn float32_row(&self, index: usize) -> Option<&[f32]> {
186 let DescriptorStorage::Float32(data) = &self.storage else {
187 return None;
188 };
189 let start = index.checked_mul(self.width)?;
190 data.get(start..start + self.width)
191 }
192
193 pub fn binary_data(&self) -> Option<&[u8]> {
195 match &self.storage {
196 DescriptorStorage::Binary(data) => Some(data),
197 DescriptorStorage::Float32(_) => None,
198 }
199 }
200
201 pub fn float32_data(&self) -> Option<&[f32]> {
203 match &self.storage {
204 DescriptorStorage::Float32(data) => Some(data),
205 DescriptorStorage::Binary(_) => None,
206 }
207 }
208}
209
210#[derive(Clone, Debug, PartialEq)]
212pub struct FeatureSet2 {
213 keypoints: Vec<Keypoint2>,
214 descriptors: DescriptorBuffer,
215}
216
217impl FeatureSet2 {
218 pub fn try_new(keypoints: Vec<Keypoint2>, descriptors: DescriptorBuffer) -> VisionResult<Self> {
220 if keypoints.len() != descriptors.len() {
221 return Err(VisionError::FeatureCountMismatch {
222 keypoints: keypoints.len(),
223 descriptors: descriptors.len(),
224 });
225 }
226 Ok(Self { keypoints, descriptors })
227 }
228
229 pub fn keypoints(&self) -> &[Keypoint2] {
231 &self.keypoints
232 }
233
234 pub const fn descriptors(&self) -> &DescriptorBuffer {
236 &self.descriptors
237 }
238}
239
240#[derive(Clone, Copy, Debug, PartialEq)]
242pub struct FeatureMatch {
243 query_index: usize,
244 train_index: usize,
245 distance: f32,
246}
247
248impl FeatureMatch {
249 pub fn try_new(query_index: usize, train_index: usize, distance: f32) -> VisionResult<Self> {
251 if !distance.is_finite() || distance < 0.0 {
252 return Err(VisionError::InvalidParameter(
253 "feature-match distance must be finite and non-negative".into(),
254 ));
255 }
256 Ok(Self { query_index, train_index, distance })
257 }
258
259 pub fn validate(self, query_count: usize, train_count: usize) -> VisionResult<Self> {
261 if self.query_index >= query_count || self.train_index >= train_count {
262 return Err(VisionError::MatchIndexOutOfBounds {
263 query: self.query_index,
264 queries: query_count,
265 train: self.train_index,
266 trains: train_count,
267 });
268 }
269 Ok(self)
270 }
271
272 pub const fn query_index(self) -> usize {
274 self.query_index
275 }
276
277 pub const fn train_index(self) -> usize {
279 self.train_index
280 }
281
282 pub const fn distance(self) -> f32 {
284 self.distance
285 }
286}
287
288#[cfg(test)]
289mod tests {
290 use super::{DescriptorBuffer, DescriptorKind, FeatureMatch, FeatureSet2, Keypoint2};
291 use crate::VisionError;
292
293 #[test]
294 fn keypoint_normalizes_orientation_and_rejects_non_finite_values() {
295 let keypoint = Keypoint2::try_new(2.5, 3.5, -0.2)
296 .unwrap()
297 .with_size(7.0)
298 .unwrap()
299 .with_angle_degrees(-45.0)
300 .unwrap()
301 .with_octave(2)
302 .with_class_id(9);
303 assert_eq!(keypoint.angle_degrees(), Some(315.0));
304 assert_eq!((keypoint.x(), keypoint.y(), keypoint.size()), (2.5, 3.5, 7.0));
305 assert_eq!(
306 (keypoint.response(), keypoint.octave(), keypoint.class_id()),
307 (-0.2, 2, Some(9))
308 );
309 assert!(Keypoint2::try_new(f32::NAN, 0.0, 1.0).is_err());
310 }
311
312 #[test]
313 fn descriptor_layout_and_kind_are_checked() {
314 let binary = DescriptorBuffer::try_binary(2, 4, (0..8).collect()).unwrap();
315 assert_eq!(binary.kind(), DescriptorKind::Binary);
316 assert_eq!(binary.binary_row(1), Some(&[4, 5, 6, 7][..]));
317 assert!(binary.float32_row(0).is_none());
318 assert!(matches!(
319 DescriptorBuffer::try_float32(2, 3, vec![0.0; 5]),
320 Err(VisionError::DescriptorLayout { expected: 6, actual: 5, .. })
321 ));
322 assert!(DescriptorBuffer::try_float32(1, 1, vec![f32::NAN]).is_err());
323 }
324
325 #[test]
326 fn feature_rows_and_match_indices_are_checked() {
327 let keypoint = Keypoint2::try_new(1.0, 2.0, 3.0).unwrap();
328 let descriptors = DescriptorBuffer::try_binary(1, 2, vec![0xaa, 0x55]).unwrap();
329 let features = FeatureSet2::try_new(vec![keypoint], descriptors).unwrap();
330 assert_eq!(features.keypoints(), &[keypoint]);
331 assert_eq!(features.descriptors().len(), 1);
332 let correspondence = FeatureMatch::try_new(0, 0, 4.0).unwrap().validate(1, 1).unwrap();
333 assert_eq!((correspondence.query_index(), correspondence.train_index()), (0, 0));
334 assert_eq!(correspondence.distance(), 4.0);
335 assert!(FeatureMatch::try_new(0, 0, f32::INFINITY).is_err());
336 assert!(matches!(
337 FeatureMatch::try_new(1, 0, 0.0).unwrap().validate(1, 1),
338 Err(VisionError::MatchIndexOutOfBounds { .. })
339 ));
340 }
341}