1use crate::{AlignError, AlignResult, Point2D};
12use rayon::prelude::*;
13use std::f64::consts::PI;
14
15#[derive(Debug, Clone)]
17pub struct Keypoint {
18 pub point: Point2D,
20 pub scale: f32,
22 pub orientation: f32,
24 pub response: f32,
26}
27
28impl Keypoint {
29 #[must_use]
31 pub fn new(x: f64, y: f64, scale: f32, orientation: f32, response: f32) -> Self {
32 Self {
33 point: Point2D::new(x, y),
34 scale,
35 orientation,
36 response,
37 }
38 }
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct BinaryDescriptor {
44 pub data: [u8; 32],
46}
47
48impl BinaryDescriptor {
49 #[must_use]
51 pub fn new(data: [u8; 32]) -> Self {
52 Self { data }
53 }
54
55 #[must_use]
57 pub fn hamming_distance(&self, other: &Self) -> u32 {
58 self.data
59 .iter()
60 .zip(&other.data)
61 .map(|(a, b)| (a ^ b).count_ones())
62 .sum()
63 }
64
65 #[must_use]
67 pub fn zero() -> Self {
68 Self { data: [0; 32] }
69 }
70}
71
72#[must_use]
85pub fn hamming_distance_simd(a: &[u8], b: &[u8]) -> u32 {
86 assert_eq!(a.len(), b.len(), "descriptor slices must have equal length");
87
88 let mut total = 0u32;
89 let chunks = a.len() / 8;
90
91 for i in 0..chunks {
93 let offset = i * 8;
94 let au = u64::from_le_bytes([
95 a[offset],
96 a[offset + 1],
97 a[offset + 2],
98 a[offset + 3],
99 a[offset + 4],
100 a[offset + 5],
101 a[offset + 6],
102 a[offset + 7],
103 ]);
104 let bu = u64::from_le_bytes([
105 b[offset],
106 b[offset + 1],
107 b[offset + 2],
108 b[offset + 3],
109 b[offset + 4],
110 b[offset + 5],
111 b[offset + 6],
112 b[offset + 7],
113 ]);
114 total += (au ^ bu).count_ones();
115 }
116
117 let tail_start = chunks * 8;
119 for (&av, &bv) in a[tail_start..].iter().zip(&b[tail_start..]) {
120 total += (av ^ bv).count_ones();
121 }
122
123 total
124}
125
126#[derive(Debug, Clone)]
128pub struct MatchPair {
129 pub idx1: usize,
131 pub idx2: usize,
133 pub distance: u32,
135 pub point1: Point2D,
137 pub point2: Point2D,
139}
140
141impl MatchPair {
142 #[must_use]
144 pub fn new(idx1: usize, idx2: usize, distance: u32, point1: Point2D, point2: Point2D) -> Self {
145 Self {
146 idx1,
147 idx2,
148 distance,
149 point1,
150 point2,
151 }
152 }
153}
154
155pub struct FastDetector {
157 pub threshold: u8,
159 pub nms_window: usize,
161}
162
163impl Default for FastDetector {
164 fn default() -> Self {
165 Self {
166 threshold: 20,
167 nms_window: 3,
168 }
169 }
170}
171
172impl FastDetector {
173 #[must_use]
175 pub fn new(threshold: u8) -> Self {
176 Self {
177 threshold,
178 nms_window: 3,
179 }
180 }
181
182 pub fn detect(&self, image: &[u8], width: usize, height: usize) -> AlignResult<Vec<Keypoint>> {
187 if image.len() != width * height {
188 return Err(AlignError::InvalidConfig("Image size mismatch".to_string()));
189 }
190
191 let mut corners = Vec::new();
192 let radius = 3;
193
194 let circle = self.get_circle_offsets(width);
196
197 for y in radius..height - radius {
199 for x in radius..width - radius {
200 let idx = y * width + x;
201 let center = image[idx];
202
203 if self.is_corner(image, idx, center, &circle) {
204 let response = self.compute_response(image, idx, center, &circle);
205 corners.push(Keypoint::new(x as f64, y as f64, 1.0, 0.0, response));
206 }
207 }
208 }
209
210 let corners = self.non_maximum_suppression(&corners, width, height);
212
213 Ok(corners)
214 }
215
216 fn get_circle_offsets(&self, width: usize) -> Vec<isize> {
218 let w = width as isize;
219 vec![
220 -w * 3, -w * 3 + 1, -w * 2 + 2, -w + 3, 3, w + 3, w * 2 + 2, w * 3 + 1, w * 3, w * 3 - 1, w * 2 - 2, w - 3, -3, -w - 3, -w * 2 - 2, -w * 3 - 1, ]
237 }
238
239 fn is_corner(&self, image: &[u8], center_idx: usize, center_val: u8, circle: &[isize]) -> bool {
241 let threshold = i16::from(self.threshold);
242 let center = i16::from(center_val);
243
244 let mut brighter = 0;
246 let mut darker = 0;
247 let mut max_consecutive_brighter = 0;
248 let mut max_consecutive_darker = 0;
249
250 for &offset in circle {
251 let idx = (center_idx as isize + offset) as usize;
252 let val = i16::from(image[idx]);
253 let diff = val - center;
254
255 if diff > threshold {
256 brighter += 1;
257 darker = 0;
258 max_consecutive_brighter = max_consecutive_brighter.max(brighter);
259 } else if diff < -threshold {
260 darker += 1;
261 brighter = 0;
262 max_consecutive_darker = max_consecutive_darker.max(darker);
263 } else {
264 brighter = 0;
265 darker = 0;
266 }
267 }
268
269 max_consecutive_brighter >= 9 || max_consecutive_darker >= 9
271 }
272
273 fn compute_response(
275 &self,
276 image: &[u8],
277 center_idx: usize,
278 center_val: u8,
279 circle: &[isize],
280 ) -> f32 {
281 let center = i16::from(center_val);
282 let mut sum_abs_diff = 0i16;
283
284 for &offset in circle {
285 let idx = (center_idx as isize + offset) as usize;
286 let val = i16::from(image[idx]);
287 sum_abs_diff += (val - center).abs();
288 }
289
290 f32::from(sum_abs_diff)
291 }
292
293 fn non_maximum_suppression(
295 &self,
296 corners: &[Keypoint],
297 _width: usize,
298 _height: usize,
299 ) -> Vec<Keypoint> {
300 let mut suppressed = vec![false; corners.len()];
301 let window = self.nms_window;
302
303 for i in 0..corners.len() {
304 if suppressed[i] {
305 continue;
306 }
307
308 let ki = &corners[i];
309
310 for (j, kj) in corners.iter().enumerate().skip(i + 1) {
311 if suppressed[j] {
312 continue;
313 }
314
315 let dx = (ki.point.x - kj.point.x).abs();
316 let dy = (ki.point.y - kj.point.y).abs();
317
318 if dx < window as f64 && dy < window as f64 {
319 if ki.response > kj.response {
321 suppressed[j] = true;
322 } else {
323 suppressed[i] = true;
324 break;
325 }
326 }
327 }
328 }
329
330 corners
331 .iter()
332 .enumerate()
333 .filter(|(i, _)| !suppressed[*i])
334 .map(|(_, k)| k.clone())
335 .collect()
336 }
337}
338
339pub struct SubPixelRefiner {
347 pub half_window: usize,
349 pub max_iterations: usize,
351 pub epsilon: f64,
353}
354
355impl Default for SubPixelRefiner {
356 fn default() -> Self {
357 Self {
358 half_window: 3,
359 max_iterations: 10,
360 epsilon: 0.01,
361 }
362 }
363}
364
365impl SubPixelRefiner {
366 #[must_use]
368 pub fn new(half_window: usize, max_iterations: usize, epsilon: f64) -> Self {
369 Self {
370 half_window,
371 max_iterations,
372 epsilon,
373 }
374 }
375
376 pub fn refine(
390 &self,
391 image: &[u8],
392 width: usize,
393 height: usize,
394 keypoints: &[Keypoint],
395 ) -> AlignResult<Vec<Keypoint>> {
396 if image.len() != width * height {
397 return Err(AlignError::InvalidConfig("Image size mismatch".to_string()));
398 }
399
400 let mut refined = Vec::with_capacity(keypoints.len());
401 let hw = self.half_window as isize;
402
403 for kp in keypoints {
404 let ix = kp.point.x.round() as isize;
405 let iy = kp.point.y.round() as isize;
406
407 if ix < hw + 1
409 || iy < hw + 1
410 || ix >= (width as isize - hw - 1)
411 || iy >= (height as isize - hw - 1)
412 {
413 refined.push(kp.clone());
414 continue;
415 }
416
417 let mut cx = kp.point.x;
418 let mut cy = kp.point.y;
419
420 for _iter in 0..self.max_iterations {
421 let rx = cx.round() as isize;
422 let ry = cy.round() as isize;
423
424 let mut _s_x2_val = 0.0_f64; let mut _s_y2_val = 0.0_f64; let mut _s_xy_val = 0.0_f64; let mut _s_x_val = 0.0_f64; let mut _s_y_val = 0.0_f64; let mut _s_x2 = 0.0_f64;
439 let mut _s_y2 = 0.0_f64;
440 let mut _s_x4 = 0.0_f64;
441 let mut _s_y4 = 0.0_f64;
442 let mut _s_x2y2 = 0.0_f64;
443
444 let mut count = 0.0_f64;
445
446 for dy in -hw..=hw {
447 for dx in -hw..=hw {
448 let px = (rx + dx) as usize;
449 let py = (ry + dy) as usize;
450 let val = f64::from(image[py * width + px]);
451 let dxf = dx as f64;
452 let dyf = dy as f64;
453
454 _s_x2_val += dxf * dxf * val;
455 _s_y2_val += dyf * dyf * val;
456 _s_xy_val += dxf * dyf * val;
457 _s_x_val += dxf * val;
458 _s_y_val += dyf * val;
459
460 _s_x2 += dxf * dxf;
461 _s_y2 += dyf * dyf;
462 _s_x4 += dxf * dxf * dxf * dxf;
463 _s_y4 += dyf * dyf * dyf * dyf;
464 _s_x2y2 += dxf * dxf * dyf * dyf;
465
466 count += 1.0;
467 }
468 }
469
470 if count < 6.0 {
471 break;
472 }
473
474 let idx_c = ry as usize * width + rx as usize;
493 let gx = (f64::from(image[idx_c + 1]) - f64::from(image[idx_c - 1])) / 2.0;
494 let gy = (f64::from(image[(ry as usize + 1) * width + rx as usize])
495 - f64::from(image[(ry as usize - 1) * width + rx as usize]))
496 / 2.0;
497
498 let hxx = f64::from(image[idx_c + 1]) + f64::from(image[idx_c - 1])
500 - 2.0 * f64::from(image[idx_c]);
501 let hyy = f64::from(image[(ry as usize + 1) * width + rx as usize])
502 + f64::from(image[(ry as usize - 1) * width + rx as usize])
503 - 2.0 * f64::from(image[idx_c]);
504 let hxy = (f64::from(image[(ry as usize + 1) * width + rx as usize + 1])
505 - f64::from(image[(ry as usize + 1) * width + rx as usize - 1])
506 - f64::from(image[(ry as usize - 1) * width + rx as usize + 1])
507 + f64::from(image[(ry as usize - 1) * width + rx as usize - 1]))
508 / 4.0;
509
510 let det = hxx * hyy - hxy * hxy;
511 if det.abs() < 1e-10 {
512 break;
513 }
514
515 let shift_x = -(hyy * gx - hxy * gy) / det;
517 let shift_y = -(-hxy * gx + hxx * gy) / det;
518
519 if shift_x.abs() > 1.0 || shift_y.abs() > 1.0 {
521 break;
522 }
523
524 cx = rx as f64 + shift_x;
525 cy = ry as f64 + shift_y;
526
527 if shift_x * shift_x + shift_y * shift_y < self.epsilon * self.epsilon {
528 break;
529 }
530 }
531
532 cx = cx.clamp(0.0, (width - 1) as f64);
534 cy = cy.clamp(0.0, (height - 1) as f64);
535
536 refined.push(Keypoint::new(cx, cy, kp.scale, kp.orientation, kp.response));
537 }
538
539 Ok(refined)
540 }
541}
542
543#[allow(dead_code)]
545fn compute_sobel_gradients(image: &[u8], width: usize, height: usize) -> (Vec<f64>, Vec<f64>) {
546 let n = width * height;
547 let mut gx = vec![0.0_f64; n];
548 let mut gy = vec![0.0_f64; n];
549
550 for y in 1..height.saturating_sub(1) {
551 for x in 1..width.saturating_sub(1) {
552 let idx = y * width + x;
553
554 let i_tl = f64::from(image[(y - 1) * width + (x - 1)]);
555 let i_t = f64::from(image[(y - 1) * width + x]);
556 let i_tr = f64::from(image[(y - 1) * width + (x + 1)]);
557 let i_l = f64::from(image[y * width + (x - 1)]);
558 let i_r = f64::from(image[y * width + (x + 1)]);
559 let i_bl = f64::from(image[(y + 1) * width + (x - 1)]);
560 let i_b = f64::from(image[(y + 1) * width + x]);
561 let i_br = f64::from(image[(y + 1) * width + (x + 1)]);
562
563 gx[idx] = (-i_tl + i_tr - 2.0 * i_l + 2.0 * i_r - i_bl + i_br) / 8.0;
564 gy[idx] = (-i_tl - 2.0 * i_t - i_tr + i_bl + 2.0 * i_b + i_br) / 8.0;
565 }
566 }
567
568 (gx, gy)
569}
570
571pub struct BriefDescriptor {
573 pub patch_size: usize,
575 pattern: Vec<(isize, isize, isize, isize)>,
577}
578
579impl Default for BriefDescriptor {
580 fn default() -> Self {
581 Self::new(31)
582 }
583}
584
585impl BriefDescriptor {
586 #[must_use]
588 pub fn new(patch_size: usize) -> Self {
589 let pattern = Self::generate_pattern(patch_size);
590 Self {
591 patch_size,
592 pattern,
593 }
594 }
595
596 fn generate_pattern(patch_size: usize) -> Vec<(isize, isize, isize, isize)> {
598 let mut pattern = Vec::with_capacity(256);
599 let half = (patch_size / 2) as isize;
600
601 let mut seed = 42u32;
603 for _ in 0..256 {
604 let x1 = (Self::next_random(&mut seed) % patch_size as u32) as isize - half;
605 let y1 = (Self::next_random(&mut seed) % patch_size as u32) as isize - half;
606 let x2 = (Self::next_random(&mut seed) % patch_size as u32) as isize - half;
607 let y2 = (Self::next_random(&mut seed) % patch_size as u32) as isize - half;
608 pattern.push((x1, y1, x2, y2));
609 }
610
611 pattern
612 }
613
614 fn next_random(seed: &mut u32) -> u32 {
616 *seed = seed.wrapping_mul(1103515245).wrapping_add(12345);
617 (*seed / 65536) % 32768
618 }
619
620 pub fn extract(
625 &self,
626 image: &[u8],
627 width: usize,
628 height: usize,
629 keypoint: &Keypoint,
630 ) -> AlignResult<BinaryDescriptor> {
631 let x = keypoint.point.x as isize;
632 let y = keypoint.point.y as isize;
633 let half = (self.patch_size / 2) as isize;
634
635 if x < half || y < half || x >= (width as isize - half) || y >= (height as isize - half) {
637 return Err(AlignError::FeatureError(
638 "Keypoint too close to border".to_string(),
639 ));
640 }
641
642 let mut descriptor = [0u8; 32];
643
644 for (bit_idx, &(x1, y1, x2, y2)) in self.pattern.iter().enumerate() {
645 let px1 = (y + y1) as usize * width + (x + x1) as usize;
646 let px2 = (y + y2) as usize * width + (x + x2) as usize;
647
648 if image[px1] < image[px2] {
649 let byte_idx = bit_idx / 8;
650 let bit_pos = bit_idx % 8;
651 descriptor[byte_idx] |= 1 << bit_pos;
652 }
653 }
654
655 Ok(BinaryDescriptor::new(descriptor))
656 }
657
658 pub fn extract_batch(
663 &self,
664 image: &[u8],
665 width: usize,
666 height: usize,
667 keypoints: &[Keypoint],
668 ) -> AlignResult<Vec<BinaryDescriptor>> {
669 keypoints
670 .par_iter()
671 .map(|kp| self.extract(image, width, height, kp))
672 .collect()
673 }
674}
675
676pub struct OrbDetector {
678 fast: FastDetector,
680 brief: BriefDescriptor,
682 pub max_features: usize,
684}
685
686impl Default for OrbDetector {
687 fn default() -> Self {
688 Self::new(500)
689 }
690}
691
692impl OrbDetector {
693 #[must_use]
695 pub fn new(max_features: usize) -> Self {
696 Self {
697 fast: FastDetector::default(),
698 brief: BriefDescriptor::default(),
699 max_features,
700 }
701 }
702
703 pub fn detect_and_compute(
708 &self,
709 image: &[u8],
710 width: usize,
711 height: usize,
712 ) -> AlignResult<(Vec<Keypoint>, Vec<BinaryDescriptor>)> {
713 let mut keypoints = self.fast.detect(image, width, height)?;
715
716 for kp in &mut keypoints {
718 kp.orientation = self.compute_orientation(image, width, height, kp);
719 }
720
721 keypoints.sort_by(|a, b| {
723 b.response
724 .partial_cmp(&a.response)
725 .unwrap_or(std::cmp::Ordering::Equal)
726 });
727 keypoints.truncate(self.max_features);
728
729 let descriptors = self.brief.extract_batch(image, width, height, &keypoints)?;
731
732 Ok((keypoints, descriptors))
733 }
734
735 fn compute_orientation(
737 &self,
738 image: &[u8],
739 width: usize,
740 height: usize,
741 keypoint: &Keypoint,
742 ) -> f32 {
743 let x = keypoint.point.x as isize;
744 let y = keypoint.point.y as isize;
745 let radius = 15isize;
746
747 let mut m01 = 0i64;
748 let mut m10 = 0i64;
749
750 for dy in -radius..=radius {
751 for dx in -radius..=radius {
752 if dx * dx + dy * dy > radius * radius {
753 continue;
754 }
755
756 let px = x + dx;
757 let py = y + dy;
758
759 if px >= 0 && py >= 0 && px < width as isize && py < height as isize {
760 let idx = py as usize * width + px as usize;
761 let intensity = i64::from(image[idx]);
762
763 m01 += dy as i64 * intensity;
764 m10 += dx as i64 * intensity;
765 }
766 }
767 }
768
769 (m01 as f64).atan2(m10 as f64) as f32
770 }
771}
772
773pub struct FeatureMatcher {
775 pub max_distance: u32,
777 pub ratio_threshold: f32,
779}
780
781impl Default for FeatureMatcher {
782 fn default() -> Self {
783 Self {
784 max_distance: 50,
785 ratio_threshold: 0.8,
786 }
787 }
788}
789
790impl FeatureMatcher {
791 #[must_use]
793 pub fn new(max_distance: u32, ratio_threshold: f32) -> Self {
794 Self {
795 max_distance,
796 ratio_threshold,
797 }
798 }
799
800 #[must_use]
802 pub fn match_features(
803 &self,
804 keypoints1: &[Keypoint],
805 descriptors1: &[BinaryDescriptor],
806 keypoints2: &[Keypoint],
807 descriptors2: &[BinaryDescriptor],
808 ) -> Vec<MatchPair> {
809 descriptors1
810 .par_iter()
811 .enumerate()
812 .filter_map(|(i, desc1)| {
813 let mut best_dist = u32::MAX;
815 let mut second_best_dist = u32::MAX;
816 let mut best_idx = 0;
817
818 for (j, desc2) in descriptors2.iter().enumerate() {
819 let dist = desc1.hamming_distance(desc2);
820
821 if dist < best_dist {
822 second_best_dist = best_dist;
823 best_dist = dist;
824 best_idx = j;
825 } else if dist < second_best_dist {
826 second_best_dist = dist;
827 }
828 }
829
830 if best_dist <= self.max_distance {
832 let ratio = best_dist as f32 / second_best_dist as f32;
833 if ratio < self.ratio_threshold {
834 return Some(MatchPair::new(
835 i,
836 best_idx,
837 best_dist,
838 keypoints1[i].point,
839 keypoints2[best_idx].point,
840 ));
841 }
842 }
843
844 None
845 })
846 .collect()
847 }
848
849 #[must_use]
851 pub fn filter_matches_geometric(
852 &self,
853 matches: &[MatchPair],
854 threshold: f64,
855 ) -> Vec<MatchPair> {
856 if matches.len() < 4 {
857 return matches.to_vec();
858 }
859
860 let median_dx = Self::median(
862 &matches
863 .iter()
864 .map(|m| m.point2.x - m.point1.x)
865 .collect::<Vec<_>>(),
866 );
867 let median_dy = Self::median(
868 &matches
869 .iter()
870 .map(|m| m.point2.y - m.point1.y)
871 .collect::<Vec<_>>(),
872 );
873
874 matches
875 .iter()
876 .filter(|m| {
877 let dx = m.point2.x - m.point1.x;
878 let dy = m.point2.y - m.point1.y;
879 (dx - median_dx).abs() < threshold && (dy - median_dy).abs() < threshold
880 })
881 .cloned()
882 .collect()
883 }
884
885 fn median(values: &[f64]) -> f64 {
887 if values.is_empty() {
888 return 0.0;
889 }
890
891 let mut sorted = values.to_vec();
892 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
893
894 let mid = sorted.len() / 2;
895 if sorted.len() % 2 == 0 {
896 (sorted[mid - 1] + sorted[mid]) / 2.0
897 } else {
898 sorted[mid]
899 }
900 }
901}
902
903pub struct HarrisDetector {
905 pub threshold: f32,
907 pub window_size: usize,
909}
910
911impl Default for HarrisDetector {
912 fn default() -> Self {
913 Self {
914 threshold: 0.01,
915 window_size: 3,
916 }
917 }
918}
919
920impl HarrisDetector {
921 #[must_use]
923 pub fn new(threshold: f32) -> Self {
924 Self {
925 threshold,
926 window_size: 3,
927 }
928 }
929
930 pub fn detect(&self, image: &[u8], width: usize, height: usize) -> AlignResult<Vec<Keypoint>> {
935 if image.len() != width * height {
936 return Err(AlignError::InvalidConfig("Image size mismatch".to_string()));
937 }
938
939 let (grad_x, grad_y) = self.compute_gradients(image, width, height);
941
942 let mut corners = Vec::new();
944 let k = 0.04; for y in self.window_size..height - self.window_size {
947 for x in self.window_size..width - self.window_size {
948 let mut ixx = 0.0;
949 let mut iyy = 0.0;
950 let mut ixy = 0.0;
951
952 for dy in 0..self.window_size {
954 for dx in 0..self.window_size {
955 let idx = (y + dy - self.window_size / 2) * width
956 + (x + dx - self.window_size / 2);
957 let gx = grad_x[idx];
958 let gy = grad_y[idx];
959
960 ixx += gx * gx;
961 iyy += gy * gy;
962 ixy += gx * gy;
963 }
964 }
965
966 let det = ixx * iyy - ixy * ixy;
968 let trace = ixx + iyy;
969 let response = det - k * trace * trace;
970
971 if response > self.threshold {
972 corners.push(Keypoint::new(x as f64, y as f64, 1.0, 0.0, response));
973 }
974 }
975 }
976
977 Ok(corners)
978 }
979
980 fn compute_gradients(&self, image: &[u8], width: usize, height: usize) -> (Vec<f32>, Vec<f32>) {
982 let mut grad_x = vec![0.0; width * height];
983 let mut grad_y = vec![0.0; width * height];
984
985 for y in 1..height - 1 {
986 for x in 1..width - 1 {
987 let idx = y * width + x;
988
989 let gx = -f32::from(image[idx - width - 1])
991 - 2.0 * f32::from(image[idx - 1])
992 - f32::from(image[idx + width - 1])
993 + f32::from(image[idx - width + 1])
994 + 2.0 * f32::from(image[idx + 1])
995 + f32::from(image[idx + width + 1]);
996
997 let gy = -f32::from(image[idx - width - 1])
999 - 2.0 * f32::from(image[idx - width])
1000 - f32::from(image[idx - width + 1])
1001 + f32::from(image[idx + width - 1])
1002 + 2.0 * f32::from(image[idx + width])
1003 + f32::from(image[idx + width + 1]);
1004
1005 grad_x[idx] = gx / 8.0;
1006 grad_y[idx] = gy / 8.0;
1007 }
1008 }
1009
1010 (grad_x, grad_y)
1011 }
1012}
1013
1014pub struct FeaturePyramid {
1016 pub num_levels: usize,
1018 pub scale_factor: f32,
1020}
1021
1022impl Default for FeaturePyramid {
1023 fn default() -> Self {
1024 Self {
1025 num_levels: 4,
1026 scale_factor: 0.5,
1027 }
1028 }
1029}
1030
1031impl FeaturePyramid {
1032 #[must_use]
1034 pub fn new(num_levels: usize, scale_factor: f32) -> Self {
1035 Self {
1036 num_levels,
1037 scale_factor,
1038 }
1039 }
1040
1041 #[must_use]
1043 pub fn build_pyramid(
1044 &self,
1045 image: &[u8],
1046 width: usize,
1047 height: usize,
1048 ) -> Vec<(Vec<u8>, usize, usize)> {
1049 let mut pyramid = Vec::new();
1050 pyramid.push((image.to_vec(), width, height));
1051
1052 let mut current_image = image.to_vec();
1053 let mut current_width = width;
1054 let mut current_height = height;
1055
1056 for _ in 1..self.num_levels {
1057 let new_width = (current_width as f32 * self.scale_factor) as usize;
1058 let new_height = (current_height as f32 * self.scale_factor) as usize;
1059
1060 if new_width < 16 || new_height < 16 {
1061 break;
1062 }
1063
1064 let downsampled = self.downsample(
1065 ¤t_image,
1066 current_width,
1067 current_height,
1068 new_width,
1069 new_height,
1070 );
1071 pyramid.push((downsampled.clone(), new_width, new_height));
1072
1073 current_image = downsampled;
1074 current_width = new_width;
1075 current_height = new_height;
1076 }
1077
1078 pyramid
1079 }
1080
1081 fn downsample(
1083 &self,
1084 image: &[u8],
1085 src_width: usize,
1086 src_height: usize,
1087 dst_width: usize,
1088 dst_height: usize,
1089 ) -> Vec<u8> {
1090 let mut output = vec![0u8; dst_width * dst_height];
1091
1092 let scale_x = src_width as f32 / dst_width as f32;
1093 let scale_y = src_height as f32 / dst_height as f32;
1094
1095 for y in 0..dst_height {
1096 for x in 0..dst_width {
1097 let src_x = (x as f32 * scale_x) as usize;
1098 let src_y = (y as f32 * scale_y) as usize;
1099
1100 if src_x < src_width && src_y < src_height {
1101 output[y * dst_width + x] = image[src_y * src_width + src_x];
1102 }
1103 }
1104 }
1105
1106 output
1107 }
1108
1109 pub fn detect_multiscale(
1114 &self,
1115 image: &[u8],
1116 width: usize,
1117 height: usize,
1118 detector: &FastDetector,
1119 ) -> AlignResult<Vec<Keypoint>> {
1120 let pyramid = self.build_pyramid(image, width, height);
1121 let mut all_keypoints = Vec::new();
1122
1123 for (level, (img, w, h)) in pyramid.iter().enumerate() {
1124 let keypoints = detector.detect(img, *w, *h)?;
1125
1126 let scale = self.scale_factor.powi(level as i32);
1128 for mut kp in keypoints {
1129 kp.point.x /= f64::from(scale);
1130 kp.point.y /= f64::from(scale);
1131 kp.scale *= scale;
1132 all_keypoints.push(kp);
1133 }
1134 }
1135
1136 Ok(all_keypoints)
1137 }
1138}
1139
1140pub struct AdaptiveNMS {
1142 pub radius: f32,
1144 pub num_features: usize,
1146}
1147
1148impl AdaptiveNMS {
1149 #[must_use]
1151 pub fn new(radius: f32, num_features: usize) -> Self {
1152 Self {
1153 radius,
1154 num_features,
1155 }
1156 }
1157
1158 #[must_use]
1160 pub fn apply(&self, keypoints: &[Keypoint]) -> Vec<Keypoint> {
1161 if keypoints.len() <= self.num_features {
1162 return keypoints.to_vec();
1163 }
1164
1165 let mut result: Vec<Keypoint> = Vec::new();
1166 let mut sorted = keypoints.to_vec();
1167 sorted.sort_by(|a, b| {
1168 b.response
1169 .partial_cmp(&a.response)
1170 .unwrap_or(std::cmp::Ordering::Equal)
1171 });
1172
1173 for candidate in &sorted {
1174 if result.len() >= self.num_features {
1175 break;
1176 }
1177
1178 let mut too_close = false;
1179 for kept in &result {
1180 let dist = candidate.point.distance(&kept.point);
1181 if dist < f64::from(self.radius) {
1182 too_close = true;
1183 break;
1184 }
1185 }
1186
1187 if !too_close {
1188 result.push(candidate.clone());
1189 }
1190 }
1191
1192 result
1193 }
1194}
1195
1196pub struct OutlierFilter {
1198 pub threshold_multiplier: f32,
1200}
1201
1202impl Default for OutlierFilter {
1203 fn default() -> Self {
1204 Self {
1205 threshold_multiplier: 2.0,
1206 }
1207 }
1208}
1209
1210impl OutlierFilter {
1211 #[must_use]
1213 pub fn new(threshold_multiplier: f32) -> Self {
1214 Self {
1215 threshold_multiplier,
1216 }
1217 }
1218
1219 #[must_use]
1221 pub fn filter(&self, matches: &[MatchPair]) -> Vec<MatchPair> {
1222 if matches.len() < 3 {
1223 return matches.to_vec();
1224 }
1225
1226 let distances: Vec<f64> = matches
1228 .iter()
1229 .map(|m| m.point1.distance(&m.point2))
1230 .collect();
1231
1232 let mean = distances.iter().sum::<f64>() / distances.len() as f64;
1233
1234 let variance: f64 = distances
1235 .iter()
1236 .map(|&d| (d - mean) * (d - mean))
1237 .sum::<f64>()
1238 / distances.len() as f64;
1239
1240 let std_dev = variance.sqrt();
1241 let threshold = mean + std_dev * f64::from(self.threshold_multiplier);
1242
1243 matches
1244 .iter()
1245 .filter(|m| {
1246 let dist = m.point1.distance(&m.point2);
1247 dist <= threshold
1248 })
1249 .cloned()
1250 .collect()
1251 }
1252}
1253
1254pub struct CrossCheckMatcher {
1256 base_matcher: FeatureMatcher,
1258}
1259
1260impl Default for CrossCheckMatcher {
1261 fn default() -> Self {
1262 Self::new()
1263 }
1264}
1265
1266impl CrossCheckMatcher {
1267 #[must_use]
1269 pub fn new() -> Self {
1270 Self {
1271 base_matcher: FeatureMatcher::default(),
1272 }
1273 }
1274
1275 #[must_use]
1277 pub fn match_with_cross_check(
1278 &self,
1279 keypoints1: &[Keypoint],
1280 descriptors1: &[BinaryDescriptor],
1281 keypoints2: &[Keypoint],
1282 descriptors2: &[BinaryDescriptor],
1283 ) -> Vec<MatchPair> {
1284 let forward =
1286 self.base_matcher
1287 .match_features(keypoints1, descriptors1, keypoints2, descriptors2);
1288
1289 let backward =
1291 self.base_matcher
1292 .match_features(keypoints2, descriptors2, keypoints1, descriptors1);
1293
1294 let mut cross_checked = Vec::new();
1296
1297 for fwd in &forward {
1298 for bwd in &backward {
1299 if fwd.idx1 == bwd.idx2 && fwd.idx2 == bwd.idx1 {
1300 cross_checked.push(fwd.clone());
1301 break;
1302 }
1303 }
1304 }
1305
1306 cross_checked
1307 }
1308}
1309
1310pub struct FreakDescriptor {
1312 pub num_pairs: usize,
1314 pub pattern_scale: f32,
1316}
1317
1318impl Default for FreakDescriptor {
1319 fn default() -> Self {
1320 Self {
1321 num_pairs: 256,
1322 pattern_scale: 1.0,
1323 }
1324 }
1325}
1326
1327impl FreakDescriptor {
1328 #[must_use]
1330 pub fn new(num_pairs: usize, pattern_scale: f32) -> Self {
1331 Self {
1332 num_pairs,
1333 pattern_scale,
1334 }
1335 }
1336
1337 pub fn extract(
1342 &self,
1343 image: &[u8],
1344 width: usize,
1345 height: usize,
1346 keypoint: &Keypoint,
1347 ) -> AlignResult<BinaryDescriptor> {
1348 let x = keypoint.point.x as isize;
1349 let y = keypoint.point.y as isize;
1350 let radius = (20.0 * self.pattern_scale) as isize;
1351
1352 if x < radius
1353 || y < radius
1354 || x >= (width as isize - radius)
1355 || y >= (height as isize - radius)
1356 {
1357 return Err(AlignError::FeatureError(
1358 "Keypoint too close to border".to_string(),
1359 ));
1360 }
1361
1362 let mut descriptor = [0u8; 32];
1363
1364 let mut seed = 123u32;
1366 for bit_idx in 0..256 {
1367 let r1 = (Self::lcg(&mut seed) % (radius as u32)) as isize;
1368 let theta1 = (Self::lcg(&mut seed) as f32 / u32::MAX as f32) * 2.0 * PI as f32;
1369 let x1 = x + (r1 as f32 * theta1.cos()) as isize;
1370 let y1 = y + (r1 as f32 * theta1.sin()) as isize;
1371
1372 let r2 = (Self::lcg(&mut seed) % (radius as u32)) as isize;
1373 let theta2 = (Self::lcg(&mut seed) as f32 / u32::MAX as f32) * 2.0 * PI as f32;
1374 let x2 = x + (r2 as f32 * theta2.cos()) as isize;
1375 let y2 = y + (r2 as f32 * theta2.sin()) as isize;
1376
1377 if x1 >= 0
1378 && x1 < width as isize
1379 && y1 >= 0
1380 && y1 < height as isize
1381 && x2 >= 0
1382 && x2 < width as isize
1383 && y2 >= 0
1384 && y2 < height as isize
1385 {
1386 let idx1 = y1 as usize * width + x1 as usize;
1387 let idx2 = y2 as usize * width + x2 as usize;
1388
1389 if idx1 < image.len() && idx2 < image.len() && image[idx1] < image[idx2] {
1390 let byte_idx = bit_idx / 8;
1391 let bit_pos = bit_idx % 8;
1392 descriptor[byte_idx] |= 1 << bit_pos;
1393 }
1394 }
1395 }
1396
1397 Ok(BinaryDescriptor::new(descriptor))
1398 }
1399
1400 fn lcg(seed: &mut u32) -> u32 {
1402 *seed = seed.wrapping_mul(1103515245).wrapping_add(12345);
1403 (*seed / 65536) % 32768
1404 }
1405}
1406
1407pub struct DescriptorVarianceFilter {
1409 pub min_variance: f32,
1411}
1412
1413impl Default for DescriptorVarianceFilter {
1414 fn default() -> Self {
1415 Self { min_variance: 0.1 }
1416 }
1417}
1418
1419impl DescriptorVarianceFilter {
1420 #[must_use]
1422 pub fn new(min_variance: f32) -> Self {
1423 Self { min_variance }
1424 }
1425
1426 #[must_use]
1428 pub fn filter(
1429 &self,
1430 keypoints: &[Keypoint],
1431 descriptors: &[BinaryDescriptor],
1432 ) -> (Vec<Keypoint>, Vec<BinaryDescriptor>) {
1433 let mut filtered_kp = Vec::new();
1434 let mut filtered_desc = Vec::new();
1435
1436 for (kp, desc) in keypoints.iter().zip(descriptors.iter()) {
1437 let variance = self.compute_variance(desc);
1438 if variance >= self.min_variance {
1439 filtered_kp.push(kp.clone());
1440 filtered_desc.push(desc.clone());
1441 }
1442 }
1443
1444 (filtered_kp, filtered_desc)
1445 }
1446
1447 fn compute_variance(&self, descriptor: &BinaryDescriptor) -> f32 {
1449 let num_set_bits: u32 = descriptor.data.iter().map(|b| b.count_ones()).sum();
1450 let total_bits = descriptor.data.len() * 8;
1451
1452 let ratio = num_set_bits as f32 / total_bits as f32;
1453
1454 1.0 - (ratio - 0.5).abs() * 2.0
1456 }
1457}
1458
1459#[cfg(test)]
1460mod tests {
1461 use super::*;
1462
1463 #[test]
1464 fn test_binary_descriptor_hamming() {
1465 let desc1 = BinaryDescriptor::new([0xFF; 32]);
1466 let desc2 = BinaryDescriptor::new([0x00; 32]);
1467 assert_eq!(desc1.hamming_distance(&desc2), 256);
1468
1469 let desc3 = BinaryDescriptor::new([0xFF; 32]);
1470 assert_eq!(desc1.hamming_distance(&desc3), 0);
1471 }
1472
1473 #[test]
1474 fn test_keypoint_creation() {
1475 let kp = Keypoint::new(10.0, 20.0, 1.5, 0.5, 100.0);
1476 assert_eq!(kp.point.x, 10.0);
1477 assert_eq!(kp.point.y, 20.0);
1478 assert_eq!(kp.scale, 1.5);
1479 }
1480
1481 #[test]
1482 fn test_fast_detector() {
1483 let detector = FastDetector::new(20);
1484 let image = vec![128u8; 100 * 100];
1485 let result = detector.detect(&image, 100, 100);
1486 assert!(result.is_ok());
1487 }
1488
1489 #[test]
1490 fn test_brief_pattern_generation() {
1491 let brief = BriefDescriptor::new(31);
1492 assert_eq!(brief.pattern.len(), 256);
1493 }
1494
1495 #[test]
1496 fn test_feature_matcher() {
1497 let matcher = FeatureMatcher::default();
1498 assert_eq!(matcher.max_distance, 50);
1499 assert!((matcher.ratio_threshold - 0.8).abs() < f32::EPSILON);
1500 }
1501
1502 #[test]
1503 fn test_median_computation() {
1504 let values = vec![1.0, 3.0, 2.0, 5.0, 4.0];
1505 let median = FeatureMatcher::median(&values);
1506 assert_eq!(median, 3.0);
1507
1508 let values2 = vec![1.0, 2.0, 3.0, 4.0];
1509 let median2 = FeatureMatcher::median(&values2);
1510 assert_eq!(median2, 2.5);
1511 }
1512
1513 #[test]
1514 fn test_feature_pyramid() {
1515 let pyramid = FeaturePyramid::new(4, 0.5);
1516 assert_eq!(pyramid.num_levels, 4);
1517 assert_eq!(pyramid.scale_factor, 0.5);
1518 }
1519
1520 #[test]
1521 fn test_pyramid_building() {
1522 let pyramid = FeaturePyramid::default();
1523 let image = vec![128u8; 100 * 100];
1524 let levels = pyramid.build_pyramid(&image, 100, 100);
1525
1526 assert!(!levels.is_empty());
1527 assert_eq!(levels[0].1, 100); assert_eq!(levels[0].2, 100); }
1530
1531 #[test]
1532 fn test_adaptive_nms() {
1533 let nms = AdaptiveNMS::new(10.0, 5);
1534 let keypoints = vec![
1535 Keypoint::new(0.0, 0.0, 1.0, 0.0, 100.0),
1536 Keypoint::new(5.0, 5.0, 1.0, 0.0, 90.0),
1537 Keypoint::new(50.0, 50.0, 1.0, 0.0, 80.0),
1538 ];
1539
1540 let filtered = nms.apply(&keypoints);
1541 assert!(!filtered.is_empty());
1542 assert!(filtered.len() <= 5);
1543 }
1544
1545 #[test]
1546 fn test_outlier_filter() {
1547 let filter = OutlierFilter::default();
1548 assert_eq!(filter.threshold_multiplier, 2.0);
1549 }
1550
1551 #[test]
1552 fn test_cross_check_matcher() {
1553 let matcher = CrossCheckMatcher::new();
1554 let kp1 = vec![Keypoint::new(0.0, 0.0, 1.0, 0.0, 1.0)];
1555 let kp2 = vec![Keypoint::new(0.0, 0.0, 1.0, 0.0, 1.0)];
1556 let desc1 = vec![BinaryDescriptor::zero()];
1557 let desc2 = vec![BinaryDescriptor::zero()];
1558
1559 let matches = matcher.match_with_cross_check(&kp1, &desc1, &kp2, &desc2);
1560 assert_eq!(matches.len(), 1);
1561 }
1562
1563 #[test]
1564 fn test_freak_descriptor() {
1565 let freak = FreakDescriptor::default();
1566 assert_eq!(freak.num_pairs, 256);
1567 assert_eq!(freak.pattern_scale, 1.0);
1568 }
1569
1570 #[test]
1571 fn test_descriptor_variance_filter() {
1572 let filter = DescriptorVarianceFilter::new(0.1);
1573 assert_eq!(filter.min_variance, 0.1);
1574 }
1575
1576 #[test]
1577 fn test_descriptor_variance() {
1578 let filter = DescriptorVarianceFilter::default();
1579 let desc = BinaryDescriptor::new([0xAA; 32]); let variance = filter.compute_variance(&desc);
1581 assert!((variance - 1.0).abs() < 0.01);
1582 }
1583
1584 #[test]
1587 fn test_subpixel_refiner_default() {
1588 let r = SubPixelRefiner::default();
1589 assert_eq!(r.half_window, 3);
1590 assert_eq!(r.max_iterations, 10);
1591 }
1592
1593 #[test]
1594 fn test_subpixel_refiner_empty_keypoints() {
1595 let image = vec![128u8; 64 * 64];
1596 let refiner = SubPixelRefiner::default();
1597 let result = refiner.refine(&image, 64, 64, &[]).expect("should succeed");
1598 assert!(result.is_empty());
1599 }
1600
1601 #[test]
1602 fn test_subpixel_refiner_preserves_count() {
1603 let image = vec![128u8; 64 * 64];
1604 let refiner = SubPixelRefiner::default();
1605 let kps = vec![
1606 Keypoint::new(20.0, 20.0, 1.0, 0.0, 50.0),
1607 Keypoint::new(40.0, 40.0, 1.0, 0.0, 80.0),
1608 ];
1609 let result = refiner
1610 .refine(&image, 64, 64, &kps)
1611 .expect("should succeed");
1612 assert_eq!(result.len(), 2);
1613 }
1614
1615 #[test]
1616 fn test_subpixel_refiner_on_gaussian_peak() {
1617 let w = 128usize;
1621 let h = 128usize;
1622 let cx = 64.3_f64;
1623 let cy = 64.7_f64;
1624 let sigma = 8.0_f64;
1625
1626 let mut image = vec![0u8; w * h];
1627 for y in 0..h {
1628 for x in 0..w {
1629 let dx = x as f64 - cx;
1630 let dy = y as f64 - cy;
1631 let val = 255.0 * (-0.5 * (dx * dx + dy * dy) / (sigma * sigma)).exp();
1632 image[y * w + x] = val.round().min(255.0) as u8;
1633 }
1634 }
1635
1636 let refiner = SubPixelRefiner::new(5, 30, 0.001);
1638 let kps = vec![Keypoint::new(62.0, 63.0, 1.0, 0.0, 100.0)];
1639 let refined = refiner.refine(&image, w, h, &kps).expect("should succeed");
1640
1641 assert_eq!(refined.len(), 1);
1642 let rp = &refined[0];
1643 let dist_before = ((62.0 - cx).powi(2) + (63.0 - cy).powi(2)).sqrt();
1646 let dist_after = ((rp.point.x - cx).powi(2) + (rp.point.y - cy).powi(2)).sqrt();
1647 assert!(
1648 dist_after <= dist_before + 0.5,
1649 "refinement should improve or maintain: before={dist_before:.3}, after={dist_after:.3}"
1650 );
1651 }
1652
1653 #[test]
1654 fn test_subpixel_refiner_border_keypoint() {
1655 let image = vec![128u8; 32 * 32];
1657 let refiner = SubPixelRefiner::default();
1658 let kps = vec![Keypoint::new(1.0, 1.0, 1.0, 0.0, 50.0)];
1659 let result = refiner
1660 .refine(&image, 32, 32, &kps)
1661 .expect("should succeed");
1662 assert_eq!(result.len(), 1);
1663 assert!((result[0].point.x - 1.0).abs() < 1e-10);
1664 assert!((result[0].point.y - 1.0).abs() < 1e-10);
1665 }
1666
1667 #[test]
1668 fn test_subpixel_refiner_image_size_mismatch() {
1669 let refiner = SubPixelRefiner::default();
1670 let result = refiner.refine(&[0u8; 100], 20, 20, &[]);
1671 assert!(result.is_err());
1672 }
1673
1674 #[test]
1675 fn test_sobel_gradients_constant() {
1676 let image = vec![100u8; 32 * 32];
1677 let (gx, gy) = compute_sobel_gradients(&image, 32, 32);
1678 for y in 2..30 {
1679 for x in 2..30 {
1680 assert!(gx[y * 32 + x].abs() < 1e-10);
1681 assert!(gy[y * 32 + x].abs() < 1e-10);
1682 }
1683 }
1684 }
1685
1686 #[test]
1687 fn test_sobel_gradients_horizontal_ramp() {
1688 let w = 32usize;
1689 let h = 32usize;
1690 let mut image = vec![0u8; w * h];
1691 for y in 0..h {
1692 for x in 0..w {
1693 image[y * w + x] = (x * 8).min(255) as u8;
1694 }
1695 }
1696 let (gx, _gy) = compute_sobel_gradients(&image, w, h);
1697 let mid = 16 * w + 16;
1698 assert!(gx[mid] > 0.0, "horizontal ramp should produce positive gx");
1699 }
1700
1701 #[test]
1704 fn test_hamming_simd_identical() {
1705 let a = [0xAA_u8; 32];
1706 assert_eq!(hamming_distance_simd(&a, &a), 0);
1707 }
1708
1709 #[test]
1710 fn test_hamming_simd_all_differ() {
1711 let a = [0xFF_u8; 32];
1712 let b = [0x00_u8; 32];
1713 assert_eq!(hamming_distance_simd(&a, &b), 256);
1714 }
1715
1716 #[test]
1717 fn test_hamming_simd_known_value() {
1718 let mut a = [0u8; 32];
1719 let mut b = [0u8; 32];
1720 a[0] = 0b1111_0000; b[0] = 0b0000_1111; assert_eq!(hamming_distance_simd(&a, &b), 8);
1724 }
1725
1726 #[test]
1727 fn test_hamming_simd_single_bit() {
1728 let a = [0u8; 16];
1729 let mut b = [0u8; 16];
1730 b[15] = 1;
1731 assert_eq!(hamming_distance_simd(&a, &b), 1);
1732 }
1733
1734 #[test]
1735 fn test_hamming_simd_matches_byte_method() {
1736 let desc_a = BinaryDescriptor::new([0x5A; 32]);
1737 let desc_b = BinaryDescriptor::new([0xA5; 32]);
1738 let byte_result = desc_a.hamming_distance(&desc_b);
1739 let simd_result = hamming_distance_simd(&desc_a.data, &desc_b.data);
1740 assert_eq!(byte_result, simd_result);
1741 }
1742
1743 #[test]
1744 fn test_hamming_simd_non_multiple_of_8_length() {
1745 let a = vec![0xFF_u8; 11];
1747 let b = vec![0x00_u8; 11];
1748 assert_eq!(hamming_distance_simd(&a, &b), 88); }
1750
1751 #[test]
1752 fn test_hamming_simd_symmetry() {
1753 let a: Vec<u8> = (0..32).map(|i| i as u8).collect();
1754 let b: Vec<u8> = (0..32).map(|i| (i * 7 + 3) as u8).collect();
1755 assert_eq!(hamming_distance_simd(&a, &b), hamming_distance_simd(&b, &a));
1756 }
1757}