1use crate::error::{Result, VisionError};
15use crate::features::orb_like::{hamming_distance, OrbLikeDescriptor, DESC_WORDS};
16use crate::features::sift_like::SIFTDescriptor;
17
18#[derive(Debug, Clone)]
22pub enum MatchMethod {
23 BruteForce,
25 FlannLike {
27 num_trees: usize,
29 checks: usize,
31 },
32 RatioTest {
34 ratio: f64,
36 },
37}
38
39impl Default for MatchMethod {
40 fn default() -> Self {
41 MatchMethod::RatioTest { ratio: 0.75 }
42 }
43}
44
45pub fn match_descriptors(
59 desc1: &[SIFTDescriptor],
60 desc2: &[SIFTDescriptor],
61 method: &MatchMethod,
62) -> Result<Vec<(usize, usize, f64)>> {
63 if desc1.is_empty() || desc2.is_empty() {
64 return Ok(Vec::new());
65 }
66
67 let vecs1: Vec<&[f32]> = desc1.iter().map(|d| d.descriptor.as_slice()).collect();
69 let vecs2: Vec<&[f32]> = desc2.iter().map(|d| d.descriptor.as_slice()).collect();
70
71 let dim = vecs1[0].len();
72 for v in vecs1.iter().chain(vecs2.iter()) {
73 if v.len() != dim {
74 return Err(VisionError::InvalidParameter(format!(
75 "Descriptor dimension mismatch: expected {dim}, got {}",
76 v.len()
77 )));
78 }
79 }
80
81 let matches = match method {
82 MatchMethod::BruteForce => brute_force_float(&vecs1, &vecs2),
83 MatchMethod::FlannLike { num_trees, checks } => {
84 flann_like_float(&vecs1, &vecs2, *num_trees, *checks)
85 }
86 MatchMethod::RatioTest { ratio } => ratio_test_float(&vecs1, &vecs2, *ratio),
87 };
88
89 Ok(matches)
90}
91
92fn brute_force_float(q: &[&[f32]], t: &[&[f32]]) -> Vec<(usize, usize, f64)> {
94 let mut out = Vec::with_capacity(q.len());
95 for (i, qi) in q.iter().enumerate() {
96 let mut best_dist = f64::MAX;
97 let mut best_j = 0usize;
98 for (j, tj) in t.iter().enumerate() {
99 let d = l2_distance_f32(qi, tj);
100 if d < best_dist {
101 best_dist = d;
102 best_j = j;
103 }
104 }
105 out.push((i, best_j, best_dist));
106 }
107 out.sort_unstable_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal));
108 out
109}
110
111fn flann_like_float(
116 q: &[&[f32]],
117 t: &[&[f32]],
118 num_trees: usize,
119 checks: usize,
120) -> Vec<(usize, usize, f64)> {
121 if t.len() < 16 {
122 return brute_force_float(q, t);
123 }
124
125 let trees: Vec<KdNode> = (0..num_trees.max(1))
127 .map(|seed| build_kdtree(t, seed as u64))
128 .collect();
129
130 let mut out = Vec::with_capacity(q.len());
131 for (i, qi) in q.iter().enumerate() {
132 let mut best_dist = f64::MAX;
133 let mut best_j = 0usize;
134
135 for tree in &trees {
136 let (j, d) = search_kdtree(tree, qi, t, checks);
137 if d < best_dist {
138 best_dist = d;
139 best_j = j;
140 }
141 }
142 out.push((i, best_j, best_dist));
143 }
144 out.sort_unstable_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal));
145 out
146}
147
148fn ratio_test_float(q: &[&[f32]], t: &[&[f32]], ratio: f64) -> Vec<(usize, usize, f64)> {
150 let mut out = Vec::new();
151 for (i, qi) in q.iter().enumerate() {
152 let mut first = (f64::MAX, 0usize);
153 let mut second = f64::MAX;
154
155 for (j, tj) in t.iter().enumerate() {
156 let d = l2_distance_f32(qi, tj);
157 if d < first.0 {
158 second = first.0;
159 first = (d, j);
160 } else if d < second {
161 second = d;
162 }
163 }
164
165 if second > 0.0 && first.0 / second < ratio {
166 out.push((i, first.1, first.0));
167 }
168 }
169 out.sort_unstable_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal));
170 out
171}
172
173pub fn match_binary_descriptors(
188 desc1: &[OrbLikeDescriptor],
189 desc2: &[OrbLikeDescriptor],
190 method: &MatchMethod,
191) -> Result<Vec<(usize, usize, f64)>> {
192 if desc1.is_empty() || desc2.is_empty() {
193 return Ok(Vec::new());
194 }
195
196 let refs1: Vec<&[u32; DESC_WORDS]> = desc1.iter().map(|d| &d.descriptor).collect();
197 let refs2: Vec<&[u32; DESC_WORDS]> = desc2.iter().map(|d| &d.descriptor).collect();
198
199 let matches = match method {
200 MatchMethod::BruteForce => brute_force_binary(&refs1, &refs2),
201 MatchMethod::FlannLike { checks, .. } => {
202 lsh_binary(&refs1, &refs2, *checks)
204 }
205 MatchMethod::RatioTest { ratio } => ratio_test_binary(&refs1, &refs2, *ratio),
206 };
207
208 Ok(matches)
209}
210
211fn brute_force_binary(
213 q: &[&[u32; DESC_WORDS]],
214 t: &[&[u32; DESC_WORDS]],
215) -> Vec<(usize, usize, f64)> {
216 let mut out = Vec::with_capacity(q.len());
217 for (i, qi) in q.iter().enumerate() {
218 let mut best = (u32::MAX, 0usize);
219 for (j, tj) in t.iter().enumerate() {
220 let d = hamming_distance(qi, tj);
221 if d < best.0 {
222 best = (d, j);
223 }
224 }
225 out.push((i, best.1, best.0 as f64));
226 }
227 out.sort_unstable_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal));
228 out
229}
230
231fn lsh_binary(
235 q: &[&[u32; DESC_WORDS]],
236 t: &[&[u32; DESC_WORDS]],
237 checks: usize,
238) -> Vec<(usize, usize, f64)> {
239 if t.len() <= checks {
240 return brute_force_binary(q, t);
241 }
242
243 let bucket_count = (t.len() / 4).next_power_of_two().max(64);
246 let mask = bucket_count - 1;
247
248 let mut buckets: Vec<Vec<usize>> = vec![Vec::new(); bucket_count];
250 for (j, tj) in t.iter().enumerate() {
251 let hash = (tj[0] ^ tj[1].rotate_left(8) ^ tj[2].rotate_left(16)) as usize & mask;
252 buckets[hash].push(j);
253 }
254
255 let mut out = Vec::with_capacity(q.len());
256 for (i, qi) in q.iter().enumerate() {
257 let query_hash = (qi[0] ^ qi[1].rotate_left(8) ^ qi[2].rotate_left(16)) as usize & mask;
258
259 let mut candidates: Vec<usize> = buckets[query_hash].clone();
260 let nb1 = (query_hash + 1) & mask;
262 let nb2 = (query_hash + bucket_count - 1) & mask;
263 candidates.extend_from_slice(&buckets[nb1]);
264 candidates.extend_from_slice(&buckets[nb2]);
265
266 if candidates.len() < checks {
268 let step = t.len() / (checks - candidates.len()).max(1);
269 for k in (0..t.len()).step_by(step.max(1)) {
270 candidates.push(k);
271 }
272 }
273
274 candidates.sort_unstable();
275 candidates.dedup();
276
277 let mut best = (u32::MAX, 0usize);
278 for j in candidates {
279 if j < t.len() {
280 let d = hamming_distance(qi, t[j]);
281 if d < best.0 {
282 best = (d, j);
283 }
284 }
285 }
286
287 out.push((i, best.1, best.0 as f64));
288 }
289
290 out.sort_unstable_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal));
291 out
292}
293
294fn ratio_test_binary(
296 q: &[&[u32; DESC_WORDS]],
297 t: &[&[u32; DESC_WORDS]],
298 ratio: f64,
299) -> Vec<(usize, usize, f64)> {
300 let mut out = Vec::new();
301 for (i, qi) in q.iter().enumerate() {
302 let mut first = (u32::MAX, 0usize);
303 let mut second = u32::MAX;
304
305 for (j, tj) in t.iter().enumerate() {
306 let d = hamming_distance(qi, tj);
307 if d < first.0 {
308 second = first.0;
309 first = (d, j);
310 } else if d < second {
311 second = d;
312 }
313 }
314
315 if second > 0 {
316 let r = first.0 as f64 / second as f64;
317 if r < ratio {
318 out.push((i, first.1, first.0 as f64));
319 }
320 }
321 }
322 out.sort_unstable_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal));
323 out
324}
325
326pub fn symmetric_filter(
334 desc1: &[SIFTDescriptor],
335 desc2: &[SIFTDescriptor],
336 method: &MatchMethod,
337) -> Result<Vec<(usize, usize, f64)>> {
338 let fwd = match_descriptors(desc1, desc2, method)?;
339 let rev = match_descriptors(desc2, desc1, method)?;
340
341 let mut rev_map: std::collections::HashMap<usize, usize> =
343 std::collections::HashMap::with_capacity(rev.len());
344 for (j, i, _) in &rev {
345 rev_map.insert(*j, *i);
346 }
347
348 let symmetric: Vec<(usize, usize, f64)> = fwd
349 .into_iter()
350 .filter(|(i, j, _)| rev_map.get(j).is_some_and(|&ri| ri == *i))
351 .collect();
352
353 Ok(symmetric)
354}
355
356enum KdNode {
360 Leaf {
361 indices: Vec<usize>,
362 },
363 Internal {
364 axis: usize,
365 split_val: f32,
366 left: Box<KdNode>,
367 right: Box<KdNode>,
368 },
369}
370
371fn build_kdtree(vecs: &[&[f32]], seed: u64) -> KdNode {
373 let indices: Vec<usize> = (0..vecs.len()).collect();
374 build_kdtree_rec(&indices, vecs, seed, 0)
375}
376
377fn build_kdtree_rec(indices: &[usize], vecs: &[&[f32]], seed: u64, depth: usize) -> KdNode {
378 const LEAF_SIZE: usize = 8;
379 if indices.len() <= LEAF_SIZE {
380 return KdNode::Leaf {
381 indices: indices.to_vec(),
382 };
383 }
384
385 let dim = vecs[0].len();
386 let axis = choose_split_axis(indices, vecs, seed, depth, dim);
389
390 let mut vals: Vec<f32> = indices.iter().map(|&i| vecs[i][axis]).collect();
392 vals.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
393 let split_val = vals[vals.len() / 2];
394
395 let left_idx: Vec<usize> = indices
396 .iter()
397 .copied()
398 .filter(|&i| vecs[i][axis] < split_val)
399 .collect();
400 let right_idx: Vec<usize> = indices
401 .iter()
402 .copied()
403 .filter(|&i| vecs[i][axis] >= split_val)
404 .collect();
405
406 if left_idx.is_empty() || right_idx.is_empty() {
408 return KdNode::Leaf {
409 indices: indices.to_vec(),
410 };
411 }
412
413 KdNode::Internal {
414 axis,
415 split_val,
416 left: Box::new(build_kdtree_rec(&left_idx, vecs, seed, depth + 1)),
417 right: Box::new(build_kdtree_rec(&right_idx, vecs, seed, depth + 1)),
418 }
419}
420
421fn choose_split_axis(
422 indices: &[usize],
423 vecs: &[&[f32]],
424 seed: u64,
425 depth: usize,
426 dim: usize,
427) -> usize {
428 let n_sample = 5usize.min(dim);
430 let mut rng_state = seed.wrapping_add(depth as u64 * 6_364_136_223_846_793_005);
431
432 let sample_n = indices.len().min(32);
433
434 let mut best_axis = 0usize;
435 let mut best_var = f64::NEG_INFINITY;
436
437 for _ in 0..n_sample {
438 rng_state = rng_state
439 .wrapping_mul(6_364_136_223_846_793_005)
440 .wrapping_add(1_442_695_040_888_963_407);
441 let axis = (rng_state >> 33) as usize % dim;
442
443 let step = indices.len() / sample_n + 1;
445 let sampled: Vec<f64> = indices
446 .iter()
447 .step_by(step)
448 .take(sample_n)
449 .map(|&i| vecs[i][axis] as f64)
450 .collect();
451
452 let n = sampled.len() as f64;
453 if n < 2.0 {
454 continue;
455 }
456 let mean = sampled.iter().sum::<f64>() / n;
457 let var = sampled.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n;
458
459 if var > best_var {
460 best_var = var;
461 best_axis = axis;
462 }
463 }
464
465 best_axis
466}
467
468fn search_kdtree(root: &KdNode, query: &[f32], vecs: &[&[f32]], checks: usize) -> (usize, f64) {
470 let mut best = (f64::MAX, 0usize);
471 let mut evals = 0usize;
472 search_kdtree_rec(root, query, vecs, &mut best, &mut evals, checks);
473 (best.1, best.0)
474}
475
476fn search_kdtree_rec(
477 node: &KdNode,
478 query: &[f32],
479 vecs: &[&[f32]],
480 best: &mut (f64, usize),
481 evals: &mut usize,
482 max_evals: usize,
483) {
484 if *evals >= max_evals {
485 return;
486 }
487
488 match node {
489 KdNode::Leaf { indices } => {
490 for &i in indices {
491 *evals += 1;
492 let d = l2_distance_f32(query, vecs[i]);
493 if d < best.0 {
494 *best = (d, i);
495 }
496 if *evals >= max_evals {
497 return;
498 }
499 }
500 }
501 KdNode::Internal {
502 axis,
503 split_val,
504 left,
505 right,
506 } => {
507 let q_val = query[*axis];
508 let (near, far) = if q_val < *split_val {
509 (left.as_ref(), right.as_ref())
510 } else {
511 (right.as_ref(), left.as_ref())
512 };
513
514 search_kdtree_rec(near, query, vecs, best, evals, max_evals);
515
516 let plane_dist = (q_val - split_val).powi(2) as f64;
518 if plane_dist < best.0 && *evals < max_evals {
519 search_kdtree_rec(far, query, vecs, best, evals, max_evals);
520 }
521 }
522 }
523}
524
525fn l2_distance_f32(a: &[f32], b: &[f32]) -> f64 {
529 a.iter()
530 .zip(b.iter())
531 .map(|(&x, &y)| {
532 let d = x - y;
533 (d * d) as f64
534 })
535 .sum::<f64>()
536 .sqrt()
537}
538
539#[cfg(test)]
542mod tests {
543 use super::*;
544 use crate::features::sift_like::{Keypoint, SIFTDescriptor};
545
546 fn make_sift_desc(id: usize, perturb: f32) -> SIFTDescriptor {
547 let mut desc = vec![0.0f32; 128];
548 desc[id % 128] = 1.0;
549 desc[(id + 1) % 128] = perturb;
551 let norm: f32 = desc.iter().map(|v| v * v).sum::<f32>().sqrt();
553 for v in &mut desc {
554 *v /= norm;
555 }
556 SIFTDescriptor {
557 keypoint: Keypoint {
558 x: id as f64,
559 y: id as f64,
560 scale: 1.0,
561 orientation: 0.0,
562 response: 1.0,
563 octave: 0,
564 },
565 descriptor: desc,
566 }
567 }
568
569 fn make_orb_desc(id: usize) -> OrbLikeDescriptor {
570 let mut words = [0u32; DESC_WORDS];
571 words[id % DESC_WORDS] = (id as u32).wrapping_mul(0x12345678);
572 OrbLikeDescriptor {
573 keypoint: crate::features::orb_like::OrbKeypoint {
574 x: id as f64,
575 y: id as f64,
576 score: 1.0,
577 orientation: 0.0,
578 level: 0,
579 },
580 descriptor: words,
581 }
582 }
583
584 #[test]
585 fn test_brute_force_exact_match() {
586 let descs: Vec<SIFTDescriptor> = (0..5).map(|i| make_sift_desc(i, 0.0)).collect();
587 let matches = match_descriptors(&descs, &descs, &MatchMethod::BruteForce)
588 .expect("match_descriptors should succeed");
589 for (i, j, d) in &matches {
591 assert_eq!(i, j, "Self-match expected at index {i}");
592 assert!(*d < 1e-6, "Self-match distance should be ~0, got {d}");
593 }
594 }
595
596 #[test]
597 fn test_ratio_test_returns_matches() {
598 let q: Vec<SIFTDescriptor> = (0..4).map(|i| make_sift_desc(i, 0.01)).collect();
599 let t: Vec<SIFTDescriptor> = (0..8).map(|i| make_sift_desc(i, 0.01)).collect();
600 let m = match_descriptors(&q, &t, &MatchMethod::RatioTest { ratio: 0.9 })
601 .expect("match_descriptors with ratio test should succeed");
602 assert!(!m.is_empty());
604 }
605
606 #[test]
607 fn test_flann_like_consistent_with_brute_force_small() {
608 let descs: Vec<SIFTDescriptor> = (0..5).map(|i| make_sift_desc(i, 0.0)).collect();
610 let bf = match_descriptors(&descs, &descs, &MatchMethod::BruteForce)
611 .expect("brute force match should succeed");
612 let fl = match_descriptors(
613 &descs,
614 &descs,
615 &MatchMethod::FlannLike {
616 num_trees: 2,
617 checks: 50,
618 },
619 )
620 .expect("flann-like match should succeed");
621 assert_eq!(bf.len(), fl.len());
623 }
624
625 #[test]
626 fn test_binary_brute_force() {
627 let d: Vec<OrbLikeDescriptor> = (0..4).map(make_orb_desc).collect();
628 let matches = match_binary_descriptors(&d, &d, &MatchMethod::BruteForce)
629 .expect("match_binary_descriptors should succeed");
630 for (i, j, dist) in &matches {
631 assert_eq!(i, j, "Binary self-match expected");
632 assert_eq!(*dist, 0.0, "Hamming self-distance should be 0");
633 }
634 }
635
636 #[test]
637 fn test_empty_match_set() {
638 let empty: Vec<SIFTDescriptor> = Vec::new();
639 let q: Vec<SIFTDescriptor> = (0..3).map(|i| make_sift_desc(i, 0.0)).collect();
640 let m1 = match_descriptors(&empty, &q, &MatchMethod::BruteForce)
641 .expect("match_descriptors should succeed with empty query");
642 let m2 = match_descriptors(&q, &empty, &MatchMethod::BruteForce)
643 .expect("match_descriptors should succeed with empty target");
644 assert!(m1.is_empty());
645 assert!(m2.is_empty());
646 }
647
648 #[test]
649 fn test_symmetric_filter() {
650 let descs: Vec<SIFTDescriptor> = (0..6).map(|i| make_sift_desc(i, 0.0)).collect();
651 let sym = symmetric_filter(&descs, &descs, &MatchMethod::BruteForce)
652 .expect("symmetric_filter should succeed");
653 for (i, j, d) in &sym {
655 assert_eq!(i, j);
656 assert!(*d < 1e-6);
657 }
658 }
659}