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#[cfg_attr(not(test), allow(dead_code))]
546fn compute_sobel_gradients(image: &[u8], width: usize, height: usize) -> (Vec<f64>, Vec<f64>) {
547 let n = width * height;
548 let mut gx = vec![0.0_f64; n];
549 let mut gy = vec![0.0_f64; n];
550
551 for y in 1..height.saturating_sub(1) {
552 for x in 1..width.saturating_sub(1) {
553 let idx = y * width + x;
554
555 let i_tl = f64::from(image[(y - 1) * width + (x - 1)]);
556 let i_t = f64::from(image[(y - 1) * width + x]);
557 let i_tr = f64::from(image[(y - 1) * width + (x + 1)]);
558 let i_l = f64::from(image[y * width + (x - 1)]);
559 let i_r = f64::from(image[y * width + (x + 1)]);
560 let i_bl = f64::from(image[(y + 1) * width + (x - 1)]);
561 let i_b = f64::from(image[(y + 1) * width + x]);
562 let i_br = f64::from(image[(y + 1) * width + (x + 1)]);
563
564 gx[idx] = (-i_tl + i_tr - 2.0 * i_l + 2.0 * i_r - i_bl + i_br) / 8.0;
565 gy[idx] = (-i_tl - 2.0 * i_t - i_tr + i_bl + 2.0 * i_b + i_br) / 8.0;
566 }
567 }
568
569 (gx, gy)
570}
571
572pub struct BriefDescriptor {
574 pub patch_size: usize,
576 pattern: Vec<(isize, isize, isize, isize)>,
578}
579
580impl Default for BriefDescriptor {
581 fn default() -> Self {
582 Self::new(31)
583 }
584}
585
586impl BriefDescriptor {
587 #[must_use]
589 pub fn new(patch_size: usize) -> Self {
590 let pattern = Self::generate_pattern(patch_size);
591 Self {
592 patch_size,
593 pattern,
594 }
595 }
596
597 fn generate_pattern(patch_size: usize) -> Vec<(isize, isize, isize, isize)> {
599 let mut pattern = Vec::with_capacity(256);
600 let half = (patch_size / 2) as isize;
601
602 let mut seed = 42u32;
604 for _ in 0..256 {
605 let x1 = (Self::next_random(&mut seed) % patch_size as u32) as isize - half;
606 let y1 = (Self::next_random(&mut seed) % patch_size as u32) as isize - half;
607 let x2 = (Self::next_random(&mut seed) % patch_size as u32) as isize - half;
608 let y2 = (Self::next_random(&mut seed) % patch_size as u32) as isize - half;
609 pattern.push((x1, y1, x2, y2));
610 }
611
612 pattern
613 }
614
615 fn next_random(seed: &mut u32) -> u32 {
617 *seed = seed.wrapping_mul(1103515245).wrapping_add(12345);
618 (*seed / 65536) % 32768
619 }
620
621 pub fn extract(
626 &self,
627 image: &[u8],
628 width: usize,
629 height: usize,
630 keypoint: &Keypoint,
631 ) -> AlignResult<BinaryDescriptor> {
632 let x = keypoint.point.x as isize;
633 let y = keypoint.point.y as isize;
634 let half = (self.patch_size / 2) as isize;
635
636 if x < half || y < half || x >= (width as isize - half) || y >= (height as isize - half) {
638 return Err(AlignError::FeatureError(
639 "Keypoint too close to border".to_string(),
640 ));
641 }
642
643 let mut descriptor = [0u8; 32];
644
645 for (bit_idx, &(x1, y1, x2, y2)) in self.pattern.iter().enumerate() {
646 let px1 = (y + y1) as usize * width + (x + x1) as usize;
647 let px2 = (y + y2) as usize * width + (x + x2) as usize;
648
649 if image[px1] < image[px2] {
650 let byte_idx = bit_idx / 8;
651 let bit_pos = bit_idx % 8;
652 descriptor[byte_idx] |= 1 << bit_pos;
653 }
654 }
655
656 Ok(BinaryDescriptor::new(descriptor))
657 }
658
659 pub fn extract_batch(
664 &self,
665 image: &[u8],
666 width: usize,
667 height: usize,
668 keypoints: &[Keypoint],
669 ) -> AlignResult<Vec<BinaryDescriptor>> {
670 keypoints
671 .par_iter()
672 .map(|kp| self.extract(image, width, height, kp))
673 .collect()
674 }
675}
676
677pub struct OrbDetector {
679 fast: FastDetector,
681 brief: BriefDescriptor,
683 pub max_features: usize,
685}
686
687impl Default for OrbDetector {
688 fn default() -> Self {
689 Self::new(500)
690 }
691}
692
693impl OrbDetector {
694 #[must_use]
696 pub fn new(max_features: usize) -> Self {
697 Self {
698 fast: FastDetector::default(),
699 brief: BriefDescriptor::default(),
700 max_features,
701 }
702 }
703
704 pub fn detect_and_compute(
709 &self,
710 image: &[u8],
711 width: usize,
712 height: usize,
713 ) -> AlignResult<(Vec<Keypoint>, Vec<BinaryDescriptor>)> {
714 let mut keypoints = self.fast.detect(image, width, height)?;
716
717 for kp in &mut keypoints {
719 kp.orientation = self.compute_orientation(image, width, height, kp);
720 }
721
722 keypoints.sort_by(|a, b| {
724 b.response
725 .partial_cmp(&a.response)
726 .unwrap_or(std::cmp::Ordering::Equal)
727 });
728 keypoints.truncate(self.max_features);
729
730 let descriptors = self.brief.extract_batch(image, width, height, &keypoints)?;
732
733 Ok((keypoints, descriptors))
734 }
735
736 fn compute_orientation(
738 &self,
739 image: &[u8],
740 width: usize,
741 height: usize,
742 keypoint: &Keypoint,
743 ) -> f32 {
744 let x = keypoint.point.x as isize;
745 let y = keypoint.point.y as isize;
746 let radius = 15isize;
747
748 let mut m01 = 0i64;
749 let mut m10 = 0i64;
750
751 for dy in -radius..=radius {
752 for dx in -radius..=radius {
753 if dx * dx + dy * dy > radius * radius {
754 continue;
755 }
756
757 let px = x + dx;
758 let py = y + dy;
759
760 if px >= 0 && py >= 0 && px < width as isize && py < height as isize {
761 let idx = py as usize * width + px as usize;
762 let intensity = i64::from(image[idx]);
763
764 m01 += dy as i64 * intensity;
765 m10 += dx as i64 * intensity;
766 }
767 }
768 }
769
770 (m01 as f64).atan2(m10 as f64) as f32
771 }
772}
773
774pub struct FeatureMatcher {
776 pub max_distance: u32,
778 pub ratio_threshold: f32,
780}
781
782impl Default for FeatureMatcher {
783 fn default() -> Self {
784 Self {
785 max_distance: 50,
786 ratio_threshold: 0.8,
787 }
788 }
789}
790
791impl FeatureMatcher {
792 #[must_use]
794 pub fn new(max_distance: u32, ratio_threshold: f32) -> Self {
795 Self {
796 max_distance,
797 ratio_threshold,
798 }
799 }
800
801 #[must_use]
803 pub fn match_features(
804 &self,
805 keypoints1: &[Keypoint],
806 descriptors1: &[BinaryDescriptor],
807 keypoints2: &[Keypoint],
808 descriptors2: &[BinaryDescriptor],
809 ) -> Vec<MatchPair> {
810 descriptors1
811 .par_iter()
812 .enumerate()
813 .filter_map(|(i, desc1)| {
814 let mut best_dist = u32::MAX;
816 let mut second_best_dist = u32::MAX;
817 let mut best_idx = 0;
818
819 for (j, desc2) in descriptors2.iter().enumerate() {
820 let dist = desc1.hamming_distance(desc2);
821
822 if dist < best_dist {
823 second_best_dist = best_dist;
824 best_dist = dist;
825 best_idx = j;
826 } else if dist < second_best_dist {
827 second_best_dist = dist;
828 }
829 }
830
831 if best_dist <= self.max_distance {
833 let ratio = best_dist as f32 / second_best_dist as f32;
834 if ratio < self.ratio_threshold {
835 return Some(MatchPair::new(
836 i,
837 best_idx,
838 best_dist,
839 keypoints1[i].point,
840 keypoints2[best_idx].point,
841 ));
842 }
843 }
844
845 None
846 })
847 .collect()
848 }
849
850 #[must_use]
852 pub fn filter_matches_geometric(
853 &self,
854 matches: &[MatchPair],
855 threshold: f64,
856 ) -> Vec<MatchPair> {
857 if matches.len() < 4 {
858 return matches.to_vec();
859 }
860
861 let median_dx = Self::median(
863 &matches
864 .iter()
865 .map(|m| m.point2.x - m.point1.x)
866 .collect::<Vec<_>>(),
867 );
868 let median_dy = Self::median(
869 &matches
870 .iter()
871 .map(|m| m.point2.y - m.point1.y)
872 .collect::<Vec<_>>(),
873 );
874
875 matches
876 .iter()
877 .filter(|m| {
878 let dx = m.point2.x - m.point1.x;
879 let dy = m.point2.y - m.point1.y;
880 (dx - median_dx).abs() < threshold && (dy - median_dy).abs() < threshold
881 })
882 .cloned()
883 .collect()
884 }
885
886 fn median(values: &[f64]) -> f64 {
888 if values.is_empty() {
889 return 0.0;
890 }
891
892 let mut sorted = values.to_vec();
893 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
894
895 let mid = sorted.len() / 2;
896 if sorted.len() % 2 == 0 {
897 (sorted[mid - 1] + sorted[mid]) / 2.0
898 } else {
899 sorted[mid]
900 }
901 }
902}
903
904#[derive(Debug, Clone)]
911pub struct SummedAreaTable {
912 data: Vec<i64>,
914 pub width: usize,
916 pub height: usize,
918}
919
920impl SummedAreaTable {
921 #[must_use]
926 pub fn new(gray: &[u8], width: usize, height: usize) -> Self {
927 debug_assert_eq!(
928 gray.len(),
929 width * height,
930 "SAT: gray.len()={} != width*height={}",
931 gray.len(),
932 width * height
933 );
934
935 let stride = width + 1;
936 let mut data = vec![0i64; stride * (height + 1)];
937
938 for y in 0..height {
939 for x in 0..width {
940 let pixel = i64::from(gray[y * width + x]);
941
942 let above = data[y * stride + (x + 1)]; let left = data[(y + 1) * stride + x]; let diag = data[y * stride + x]; data[(y + 1) * stride + (x + 1)] = pixel + above + left - diag;
949 }
950 }
951
952 Self {
953 data,
954 width,
955 height,
956 }
957 }
958
959 #[must_use]
962 pub fn rect_sum(&self, x1: usize, y1: usize, x2: usize, y2: usize) -> i64 {
963 let x1 = x1.min(self.width);
965 let y1 = y1.min(self.height);
966 let x2 = x2.min(self.width);
967 let y2 = y2.min(self.height);
968
969 if x2 <= x1 || y2 <= y1 {
970 return 0;
971 }
972
973 let stride = self.width + 1;
974 let s_x2_y2 = self.data[y2 * stride + x2];
975 let s_x1_y2 = self.data[y2 * stride + x1];
976 let s_x2_y1 = self.data[y1 * stride + x2];
977 let s_x1_y1 = self.data[y1 * stride + x1];
978
979 s_x2_y2 - s_x1_y2 - s_x2_y1 + s_x1_y1
980 }
981
982 #[must_use]
984 pub fn rect_mean(&self, x1: usize, y1: usize, x2: usize, y2: usize) -> Option<f64> {
985 let (x2c, y2c) = (x2.min(self.width), y2.min(self.height));
986 let (x1c, y1c) = (x1.min(x2c), y1.min(y2c));
987 let area = (x2c - x1c) as i64 * (y2c - y1c) as i64;
988 if area == 0 {
989 None
990 } else {
991 Some(self.rect_sum(x1c, y1c, x2c, y2c) as f64 / area as f64)
992 }
993 }
994}
995
996pub struct HarrisDetector {
998 pub threshold: f32,
1000 pub window_size: usize,
1002}
1003
1004impl Default for HarrisDetector {
1005 fn default() -> Self {
1006 Self {
1007 threshold: 0.01,
1008 window_size: 3,
1009 }
1010 }
1011}
1012
1013impl HarrisDetector {
1014 #[must_use]
1016 pub fn new(threshold: f32) -> Self {
1017 Self {
1018 threshold,
1019 window_size: 3,
1020 }
1021 }
1022
1023 pub fn detect(&self, image: &[u8], width: usize, height: usize) -> AlignResult<Vec<Keypoint>> {
1037 if image.len() != width * height {
1038 return Err(AlignError::InvalidConfig("Image size mismatch".to_string()));
1039 }
1040
1041 let (grad_x, grad_y) = self.compute_gradients(image, width, height);
1043
1044 let k = 0.04_f32; let mut response_map = vec![0.0_f32; width * height];
1047 let mut raw_candidates: Vec<Keypoint> = Vec::new();
1048
1049 for y in self.window_size..height.saturating_sub(self.window_size) {
1050 for x in self.window_size..width.saturating_sub(self.window_size) {
1051 let mut ixx = 0.0_f32;
1052 let mut iyy = 0.0_f32;
1053 let mut ixy = 0.0_f32;
1054
1055 let half = self.window_size / 2;
1056 for dy in 0..self.window_size {
1057 for dx in 0..self.window_size {
1058 let idx =
1059 (y + dy).saturating_sub(half) * width + (x + dx).saturating_sub(half);
1060 let gx = grad_x[idx];
1061 let gy = grad_y[idx];
1062 ixx += gx * gx;
1063 iyy += gy * gy;
1064 ixy += gx * gy;
1065 }
1066 }
1067
1068 let det = ixx * iyy - ixy * ixy;
1069 let trace = ixx + iyy;
1070 let response = det - k * trace * trace;
1071
1072 if response > self.threshold {
1073 response_map[y * width + x] = response;
1074 raw_candidates.push(Keypoint::new(x as f64, y as f64, 1.0, 0.0, response));
1075 }
1076 }
1077 }
1078
1079 let max_response = response_map.iter().cloned().fold(0.0_f32, f32::max);
1084
1085 if max_response <= 0.0 {
1086 return Ok(raw_candidates);
1087 }
1088
1089 let quantised: Vec<u8> = response_map
1090 .iter()
1091 .map(|&r| {
1092 if r <= 0.0 {
1093 0u8
1094 } else {
1095 ((r / max_response) * 255.0).clamp(0.0, 255.0).round() as u8
1096 }
1097 })
1098 .collect();
1099
1100 let sat = SummedAreaTable::new(&quantised, width, height);
1101
1102 let nms_half = (self.window_size / 2).max(1);
1104
1105 let corners: Vec<Keypoint> = raw_candidates
1106 .into_iter()
1107 .filter(|kp| {
1108 let cx = kp.point.x as usize;
1109 let cy = kp.point.y as usize;
1110
1111 let own_q = quantised[cy * width + cx] as i64;
1113
1114 let x1 = cx.saturating_sub(nms_half);
1116 let y1 = cy.saturating_sub(nms_half);
1117 let x2 = (cx + nms_half + 1).min(width);
1118 let y2 = (cy + nms_half + 1).min(height);
1119
1120 let area = ((x2 - x1) * (y2 - y1)) as i64;
1121 if area == 0 {
1122 return true;
1123 }
1124
1125 let box_sum = sat.rect_sum(x1, y1, x2, y2);
1126
1127 own_q * area > box_sum
1131 })
1132 .collect();
1133
1134 Ok(corners)
1135 }
1136
1137 fn compute_gradients(&self, image: &[u8], width: usize, height: usize) -> (Vec<f32>, Vec<f32>) {
1139 let mut grad_x = vec![0.0; width * height];
1140 let mut grad_y = vec![0.0; width * height];
1141
1142 for y in 1..height - 1 {
1143 for x in 1..width - 1 {
1144 let idx = y * width + x;
1145
1146 let gx = -f32::from(image[idx - width - 1])
1148 - 2.0 * f32::from(image[idx - 1])
1149 - f32::from(image[idx + width - 1])
1150 + f32::from(image[idx - width + 1])
1151 + 2.0 * f32::from(image[idx + 1])
1152 + f32::from(image[idx + width + 1]);
1153
1154 let gy = -f32::from(image[idx - width - 1])
1156 - 2.0 * f32::from(image[idx - width])
1157 - f32::from(image[idx - width + 1])
1158 + f32::from(image[idx + width - 1])
1159 + 2.0 * f32::from(image[idx + width])
1160 + f32::from(image[idx + width + 1]);
1161
1162 grad_x[idx] = gx / 8.0;
1163 grad_y[idx] = gy / 8.0;
1164 }
1165 }
1166
1167 (grad_x, grad_y)
1168 }
1169}
1170
1171pub struct FeaturePyramid {
1173 pub num_levels: usize,
1175 pub scale_factor: f32,
1177}
1178
1179impl Default for FeaturePyramid {
1180 fn default() -> Self {
1181 Self {
1182 num_levels: 4,
1183 scale_factor: 0.5,
1184 }
1185 }
1186}
1187
1188impl FeaturePyramid {
1189 #[must_use]
1191 pub fn new(num_levels: usize, scale_factor: f32) -> Self {
1192 Self {
1193 num_levels,
1194 scale_factor,
1195 }
1196 }
1197
1198 #[must_use]
1200 pub fn build_pyramid(
1201 &self,
1202 image: &[u8],
1203 width: usize,
1204 height: usize,
1205 ) -> Vec<(Vec<u8>, usize, usize)> {
1206 let mut pyramid = Vec::new();
1207 pyramid.push((image.to_vec(), width, height));
1208
1209 let mut current_image = image.to_vec();
1210 let mut current_width = width;
1211 let mut current_height = height;
1212
1213 for _ in 1..self.num_levels {
1214 let new_width = (current_width as f32 * self.scale_factor) as usize;
1215 let new_height = (current_height as f32 * self.scale_factor) as usize;
1216
1217 if new_width < 16 || new_height < 16 {
1218 break;
1219 }
1220
1221 let downsampled = self.downsample(
1222 ¤t_image,
1223 current_width,
1224 current_height,
1225 new_width,
1226 new_height,
1227 );
1228 pyramid.push((downsampled.clone(), new_width, new_height));
1229
1230 current_image = downsampled;
1231 current_width = new_width;
1232 current_height = new_height;
1233 }
1234
1235 pyramid
1236 }
1237
1238 fn downsample(
1240 &self,
1241 image: &[u8],
1242 src_width: usize,
1243 src_height: usize,
1244 dst_width: usize,
1245 dst_height: usize,
1246 ) -> Vec<u8> {
1247 let mut output = vec![0u8; dst_width * dst_height];
1248
1249 let scale_x = src_width as f32 / dst_width as f32;
1250 let scale_y = src_height as f32 / dst_height as f32;
1251
1252 for y in 0..dst_height {
1253 for x in 0..dst_width {
1254 let src_x = (x as f32 * scale_x) as usize;
1255 let src_y = (y as f32 * scale_y) as usize;
1256
1257 if src_x < src_width && src_y < src_height {
1258 output[y * dst_width + x] = image[src_y * src_width + src_x];
1259 }
1260 }
1261 }
1262
1263 output
1264 }
1265
1266 pub fn detect_multiscale(
1271 &self,
1272 image: &[u8],
1273 width: usize,
1274 height: usize,
1275 detector: &FastDetector,
1276 ) -> AlignResult<Vec<Keypoint>> {
1277 let pyramid = self.build_pyramid(image, width, height);
1278 let mut all_keypoints = Vec::new();
1279
1280 for (level, (img, w, h)) in pyramid.iter().enumerate() {
1281 let keypoints = detector.detect(img, *w, *h)?;
1282
1283 let scale = self.scale_factor.powi(level as i32);
1285 for mut kp in keypoints {
1286 kp.point.x /= f64::from(scale);
1287 kp.point.y /= f64::from(scale);
1288 kp.scale *= scale;
1289 all_keypoints.push(kp);
1290 }
1291 }
1292
1293 Ok(all_keypoints)
1294 }
1295}
1296
1297pub struct AdaptiveNMS {
1299 pub radius: f32,
1301 pub num_features: usize,
1303}
1304
1305impl AdaptiveNMS {
1306 #[must_use]
1308 pub fn new(radius: f32, num_features: usize) -> Self {
1309 Self {
1310 radius,
1311 num_features,
1312 }
1313 }
1314
1315 #[must_use]
1317 pub fn apply(&self, keypoints: &[Keypoint]) -> Vec<Keypoint> {
1318 if keypoints.len() <= self.num_features {
1319 return keypoints.to_vec();
1320 }
1321
1322 let mut result: Vec<Keypoint> = Vec::new();
1323 let mut sorted = keypoints.to_vec();
1324 sorted.sort_by(|a, b| {
1325 b.response
1326 .partial_cmp(&a.response)
1327 .unwrap_or(std::cmp::Ordering::Equal)
1328 });
1329
1330 for candidate in &sorted {
1331 if result.len() >= self.num_features {
1332 break;
1333 }
1334
1335 let mut too_close = false;
1336 for kept in &result {
1337 let dist = candidate.point.distance(&kept.point);
1338 if dist < f64::from(self.radius) {
1339 too_close = true;
1340 break;
1341 }
1342 }
1343
1344 if !too_close {
1345 result.push(candidate.clone());
1346 }
1347 }
1348
1349 result
1350 }
1351}
1352
1353pub struct OutlierFilter {
1355 pub threshold_multiplier: f32,
1357}
1358
1359impl Default for OutlierFilter {
1360 fn default() -> Self {
1361 Self {
1362 threshold_multiplier: 2.0,
1363 }
1364 }
1365}
1366
1367impl OutlierFilter {
1368 #[must_use]
1370 pub fn new(threshold_multiplier: f32) -> Self {
1371 Self {
1372 threshold_multiplier,
1373 }
1374 }
1375
1376 #[must_use]
1378 pub fn filter(&self, matches: &[MatchPair]) -> Vec<MatchPair> {
1379 if matches.len() < 3 {
1380 return matches.to_vec();
1381 }
1382
1383 let distances: Vec<f64> = matches
1385 .iter()
1386 .map(|m| m.point1.distance(&m.point2))
1387 .collect();
1388
1389 let mean = distances.iter().sum::<f64>() / distances.len() as f64;
1390
1391 let variance: f64 = distances
1392 .iter()
1393 .map(|&d| (d - mean) * (d - mean))
1394 .sum::<f64>()
1395 / distances.len() as f64;
1396
1397 let std_dev = variance.sqrt();
1398 let threshold = mean + std_dev * f64::from(self.threshold_multiplier);
1399
1400 matches
1401 .iter()
1402 .filter(|m| {
1403 let dist = m.point1.distance(&m.point2);
1404 dist <= threshold
1405 })
1406 .cloned()
1407 .collect()
1408 }
1409}
1410
1411pub struct CrossCheckMatcher {
1413 base_matcher: FeatureMatcher,
1415}
1416
1417impl Default for CrossCheckMatcher {
1418 fn default() -> Self {
1419 Self::new()
1420 }
1421}
1422
1423impl CrossCheckMatcher {
1424 #[must_use]
1426 pub fn new() -> Self {
1427 Self {
1428 base_matcher: FeatureMatcher::default(),
1429 }
1430 }
1431
1432 #[must_use]
1434 pub fn match_with_cross_check(
1435 &self,
1436 keypoints1: &[Keypoint],
1437 descriptors1: &[BinaryDescriptor],
1438 keypoints2: &[Keypoint],
1439 descriptors2: &[BinaryDescriptor],
1440 ) -> Vec<MatchPair> {
1441 let forward =
1443 self.base_matcher
1444 .match_features(keypoints1, descriptors1, keypoints2, descriptors2);
1445
1446 let backward =
1448 self.base_matcher
1449 .match_features(keypoints2, descriptors2, keypoints1, descriptors1);
1450
1451 let mut cross_checked = Vec::new();
1453
1454 for fwd in &forward {
1455 for bwd in &backward {
1456 if fwd.idx1 == bwd.idx2 && fwd.idx2 == bwd.idx1 {
1457 cross_checked.push(fwd.clone());
1458 break;
1459 }
1460 }
1461 }
1462
1463 cross_checked
1464 }
1465}
1466
1467pub struct FreakDescriptor {
1469 pub num_pairs: usize,
1471 pub pattern_scale: f32,
1473}
1474
1475impl Default for FreakDescriptor {
1476 fn default() -> Self {
1477 Self {
1478 num_pairs: 256,
1479 pattern_scale: 1.0,
1480 }
1481 }
1482}
1483
1484impl FreakDescriptor {
1485 #[must_use]
1487 pub fn new(num_pairs: usize, pattern_scale: f32) -> Self {
1488 Self {
1489 num_pairs,
1490 pattern_scale,
1491 }
1492 }
1493
1494 pub fn extract(
1499 &self,
1500 image: &[u8],
1501 width: usize,
1502 height: usize,
1503 keypoint: &Keypoint,
1504 ) -> AlignResult<BinaryDescriptor> {
1505 let x = keypoint.point.x as isize;
1506 let y = keypoint.point.y as isize;
1507 let radius = (20.0 * self.pattern_scale) as isize;
1508
1509 if x < radius
1510 || y < radius
1511 || x >= (width as isize - radius)
1512 || y >= (height as isize - radius)
1513 {
1514 return Err(AlignError::FeatureError(
1515 "Keypoint too close to border".to_string(),
1516 ));
1517 }
1518
1519 let mut descriptor = [0u8; 32];
1520
1521 let mut seed = 123u32;
1523 for bit_idx in 0..256 {
1524 let r1 = (Self::lcg(&mut seed) % (radius as u32)) as isize;
1525 let theta1 = (Self::lcg(&mut seed) as f32 / u32::MAX as f32) * 2.0 * PI as f32;
1526 let x1 = x + (r1 as f32 * theta1.cos()) as isize;
1527 let y1 = y + (r1 as f32 * theta1.sin()) as isize;
1528
1529 let r2 = (Self::lcg(&mut seed) % (radius as u32)) as isize;
1530 let theta2 = (Self::lcg(&mut seed) as f32 / u32::MAX as f32) * 2.0 * PI as f32;
1531 let x2 = x + (r2 as f32 * theta2.cos()) as isize;
1532 let y2 = y + (r2 as f32 * theta2.sin()) as isize;
1533
1534 if x1 >= 0
1535 && x1 < width as isize
1536 && y1 >= 0
1537 && y1 < height as isize
1538 && x2 >= 0
1539 && x2 < width as isize
1540 && y2 >= 0
1541 && y2 < height as isize
1542 {
1543 let idx1 = y1 as usize * width + x1 as usize;
1544 let idx2 = y2 as usize * width + x2 as usize;
1545
1546 if idx1 < image.len() && idx2 < image.len() && image[idx1] < image[idx2] {
1547 let byte_idx = bit_idx / 8;
1548 let bit_pos = bit_idx % 8;
1549 descriptor[byte_idx] |= 1 << bit_pos;
1550 }
1551 }
1552 }
1553
1554 Ok(BinaryDescriptor::new(descriptor))
1555 }
1556
1557 fn lcg(seed: &mut u32) -> u32 {
1559 *seed = seed.wrapping_mul(1103515245).wrapping_add(12345);
1560 (*seed / 65536) % 32768
1561 }
1562}
1563
1564pub struct DescriptorVarianceFilter {
1566 pub min_variance: f32,
1568}
1569
1570impl Default for DescriptorVarianceFilter {
1571 fn default() -> Self {
1572 Self { min_variance: 0.1 }
1573 }
1574}
1575
1576impl DescriptorVarianceFilter {
1577 #[must_use]
1579 pub fn new(min_variance: f32) -> Self {
1580 Self { min_variance }
1581 }
1582
1583 #[must_use]
1585 pub fn filter(
1586 &self,
1587 keypoints: &[Keypoint],
1588 descriptors: &[BinaryDescriptor],
1589 ) -> (Vec<Keypoint>, Vec<BinaryDescriptor>) {
1590 let mut filtered_kp = Vec::new();
1591 let mut filtered_desc = Vec::new();
1592
1593 for (kp, desc) in keypoints.iter().zip(descriptors.iter()) {
1594 let variance = self.compute_variance(desc);
1595 if variance >= self.min_variance {
1596 filtered_kp.push(kp.clone());
1597 filtered_desc.push(desc.clone());
1598 }
1599 }
1600
1601 (filtered_kp, filtered_desc)
1602 }
1603
1604 fn compute_variance(&self, descriptor: &BinaryDescriptor) -> f32 {
1606 let num_set_bits: u32 = descriptor.data.iter().map(|b| b.count_ones()).sum();
1607 let total_bits = descriptor.data.len() * 8;
1608
1609 let ratio = num_set_bits as f32 / total_bits as f32;
1610
1611 1.0 - (ratio - 0.5).abs() * 2.0
1613 }
1614}
1615
1616#[cfg(test)]
1617mod tests {
1618 use super::*;
1619
1620 #[test]
1621 fn test_binary_descriptor_hamming() {
1622 let desc1 = BinaryDescriptor::new([0xFF; 32]);
1623 let desc2 = BinaryDescriptor::new([0x00; 32]);
1624 assert_eq!(desc1.hamming_distance(&desc2), 256);
1625
1626 let desc3 = BinaryDescriptor::new([0xFF; 32]);
1627 assert_eq!(desc1.hamming_distance(&desc3), 0);
1628 }
1629
1630 #[test]
1631 fn test_keypoint_creation() {
1632 let kp = Keypoint::new(10.0, 20.0, 1.5, 0.5, 100.0);
1633 assert_eq!(kp.point.x, 10.0);
1634 assert_eq!(kp.point.y, 20.0);
1635 assert_eq!(kp.scale, 1.5);
1636 }
1637
1638 #[test]
1639 fn test_fast_detector() {
1640 let detector = FastDetector::new(20);
1641 let image = vec![128u8; 100 * 100];
1642 let result = detector.detect(&image, 100, 100);
1643 assert!(result.is_ok());
1644 }
1645
1646 #[test]
1647 fn test_brief_pattern_generation() {
1648 let brief = BriefDescriptor::new(31);
1649 assert_eq!(brief.pattern.len(), 256);
1650 }
1651
1652 #[test]
1653 fn test_feature_matcher() {
1654 let matcher = FeatureMatcher::default();
1655 assert_eq!(matcher.max_distance, 50);
1656 assert!((matcher.ratio_threshold - 0.8).abs() < f32::EPSILON);
1657 }
1658
1659 #[test]
1660 fn test_median_computation() {
1661 let values = vec![1.0, 3.0, 2.0, 5.0, 4.0];
1662 let median = FeatureMatcher::median(&values);
1663 assert_eq!(median, 3.0);
1664
1665 let values2 = vec![1.0, 2.0, 3.0, 4.0];
1666 let median2 = FeatureMatcher::median(&values2);
1667 assert_eq!(median2, 2.5);
1668 }
1669
1670 #[test]
1671 fn test_feature_pyramid() {
1672 let pyramid = FeaturePyramid::new(4, 0.5);
1673 assert_eq!(pyramid.num_levels, 4);
1674 assert_eq!(pyramid.scale_factor, 0.5);
1675 }
1676
1677 #[test]
1678 fn test_pyramid_building() {
1679 let pyramid = FeaturePyramid::default();
1680 let image = vec![128u8; 100 * 100];
1681 let levels = pyramid.build_pyramid(&image, 100, 100);
1682
1683 assert!(!levels.is_empty());
1684 assert_eq!(levels[0].1, 100); assert_eq!(levels[0].2, 100); }
1687
1688 #[test]
1689 fn test_adaptive_nms() {
1690 let nms = AdaptiveNMS::new(10.0, 5);
1691 let keypoints = vec![
1692 Keypoint::new(0.0, 0.0, 1.0, 0.0, 100.0),
1693 Keypoint::new(5.0, 5.0, 1.0, 0.0, 90.0),
1694 Keypoint::new(50.0, 50.0, 1.0, 0.0, 80.0),
1695 ];
1696
1697 let filtered = nms.apply(&keypoints);
1698 assert!(!filtered.is_empty());
1699 assert!(filtered.len() <= 5);
1700 }
1701
1702 #[test]
1703 fn test_outlier_filter() {
1704 let filter = OutlierFilter::default();
1705 assert_eq!(filter.threshold_multiplier, 2.0);
1706 }
1707
1708 #[test]
1709 fn test_cross_check_matcher() {
1710 let matcher = CrossCheckMatcher::new();
1711 let kp1 = vec![Keypoint::new(0.0, 0.0, 1.0, 0.0, 1.0)];
1712 let kp2 = vec![Keypoint::new(0.0, 0.0, 1.0, 0.0, 1.0)];
1713 let desc1 = vec![BinaryDescriptor::zero()];
1714 let desc2 = vec![BinaryDescriptor::zero()];
1715
1716 let matches = matcher.match_with_cross_check(&kp1, &desc1, &kp2, &desc2);
1717 assert_eq!(matches.len(), 1);
1718 }
1719
1720 #[test]
1721 fn test_freak_descriptor() {
1722 let freak = FreakDescriptor::default();
1723 assert_eq!(freak.num_pairs, 256);
1724 assert_eq!(freak.pattern_scale, 1.0);
1725 }
1726
1727 #[test]
1728 fn test_descriptor_variance_filter() {
1729 let filter = DescriptorVarianceFilter::new(0.1);
1730 assert_eq!(filter.min_variance, 0.1);
1731 }
1732
1733 #[test]
1734 fn test_descriptor_variance() {
1735 let filter = DescriptorVarianceFilter::default();
1736 let desc = BinaryDescriptor::new([0xAA; 32]); let variance = filter.compute_variance(&desc);
1738 assert!((variance - 1.0).abs() < 0.01);
1739 }
1740
1741 #[test]
1744 fn test_subpixel_refiner_default() {
1745 let r = SubPixelRefiner::default();
1746 assert_eq!(r.half_window, 3);
1747 assert_eq!(r.max_iterations, 10);
1748 }
1749
1750 #[test]
1751 fn test_subpixel_refiner_empty_keypoints() {
1752 let image = vec![128u8; 64 * 64];
1753 let refiner = SubPixelRefiner::default();
1754 let result = refiner.refine(&image, 64, 64, &[]).expect("should succeed");
1755 assert!(result.is_empty());
1756 }
1757
1758 #[test]
1759 fn test_subpixel_refiner_preserves_count() {
1760 let image = vec![128u8; 64 * 64];
1761 let refiner = SubPixelRefiner::default();
1762 let kps = vec![
1763 Keypoint::new(20.0, 20.0, 1.0, 0.0, 50.0),
1764 Keypoint::new(40.0, 40.0, 1.0, 0.0, 80.0),
1765 ];
1766 let result = refiner
1767 .refine(&image, 64, 64, &kps)
1768 .expect("should succeed");
1769 assert_eq!(result.len(), 2);
1770 }
1771
1772 #[test]
1773 fn test_subpixel_refiner_on_gaussian_peak() {
1774 let w = 128usize;
1778 let h = 128usize;
1779 let cx = 64.3_f64;
1780 let cy = 64.7_f64;
1781 let sigma = 8.0_f64;
1782
1783 let mut image = vec![0u8; w * h];
1784 for y in 0..h {
1785 for x in 0..w {
1786 let dx = x as f64 - cx;
1787 let dy = y as f64 - cy;
1788 let val = 255.0 * (-0.5 * (dx * dx + dy * dy) / (sigma * sigma)).exp();
1789 image[y * w + x] = val.round().min(255.0) as u8;
1790 }
1791 }
1792
1793 let refiner = SubPixelRefiner::new(5, 30, 0.001);
1795 let kps = vec![Keypoint::new(62.0, 63.0, 1.0, 0.0, 100.0)];
1796 let refined = refiner.refine(&image, w, h, &kps).expect("should succeed");
1797
1798 assert_eq!(refined.len(), 1);
1799 let rp = &refined[0];
1800 let dist_before = ((62.0 - cx).powi(2) + (63.0 - cy).powi(2)).sqrt();
1803 let dist_after = ((rp.point.x - cx).powi(2) + (rp.point.y - cy).powi(2)).sqrt();
1804 assert!(
1805 dist_after <= dist_before + 0.5,
1806 "refinement should improve or maintain: before={dist_before:.3}, after={dist_after:.3}"
1807 );
1808 }
1809
1810 #[test]
1811 fn test_subpixel_refiner_border_keypoint() {
1812 let image = vec![128u8; 32 * 32];
1814 let refiner = SubPixelRefiner::default();
1815 let kps = vec![Keypoint::new(1.0, 1.0, 1.0, 0.0, 50.0)];
1816 let result = refiner
1817 .refine(&image, 32, 32, &kps)
1818 .expect("should succeed");
1819 assert_eq!(result.len(), 1);
1820 assert!((result[0].point.x - 1.0).abs() < 1e-10);
1821 assert!((result[0].point.y - 1.0).abs() < 1e-10);
1822 }
1823
1824 #[test]
1825 fn test_subpixel_refiner_image_size_mismatch() {
1826 let refiner = SubPixelRefiner::default();
1827 let result = refiner.refine(&[0u8; 100], 20, 20, &[]);
1828 assert!(result.is_err());
1829 }
1830
1831 #[test]
1832 fn test_sobel_gradients_constant() {
1833 let image = vec![100u8; 32 * 32];
1834 let (gx, gy) = compute_sobel_gradients(&image, 32, 32);
1835 for y in 2..30 {
1836 for x in 2..30 {
1837 assert!(gx[y * 32 + x].abs() < 1e-10);
1838 assert!(gy[y * 32 + x].abs() < 1e-10);
1839 }
1840 }
1841 }
1842
1843 #[test]
1844 fn test_sobel_gradients_horizontal_ramp() {
1845 let w = 32usize;
1846 let h = 32usize;
1847 let mut image = vec![0u8; w * h];
1848 for y in 0..h {
1849 for x in 0..w {
1850 image[y * w + x] = (x * 8).min(255) as u8;
1851 }
1852 }
1853 let (gx, _gy) = compute_sobel_gradients(&image, w, h);
1854 let mid = 16 * w + 16;
1855 assert!(gx[mid] > 0.0, "horizontal ramp should produce positive gx");
1856 }
1857
1858 #[test]
1861 fn test_hamming_simd_identical() {
1862 let a = [0xAA_u8; 32];
1863 assert_eq!(hamming_distance_simd(&a, &a), 0);
1864 }
1865
1866 #[test]
1867 fn test_hamming_simd_all_differ() {
1868 let a = [0xFF_u8; 32];
1869 let b = [0x00_u8; 32];
1870 assert_eq!(hamming_distance_simd(&a, &b), 256);
1871 }
1872
1873 #[test]
1874 fn test_hamming_simd_known_value() {
1875 let mut a = [0u8; 32];
1876 let mut b = [0u8; 32];
1877 a[0] = 0b1111_0000; b[0] = 0b0000_1111; assert_eq!(hamming_distance_simd(&a, &b), 8);
1881 }
1882
1883 #[test]
1884 fn test_hamming_simd_single_bit() {
1885 let a = [0u8; 16];
1886 let mut b = [0u8; 16];
1887 b[15] = 1;
1888 assert_eq!(hamming_distance_simd(&a, &b), 1);
1889 }
1890
1891 #[test]
1892 fn test_hamming_simd_matches_byte_method() {
1893 let desc_a = BinaryDescriptor::new([0x5A; 32]);
1894 let desc_b = BinaryDescriptor::new([0xA5; 32]);
1895 let byte_result = desc_a.hamming_distance(&desc_b);
1896 let simd_result = hamming_distance_simd(&desc_a.data, &desc_b.data);
1897 assert_eq!(byte_result, simd_result);
1898 }
1899
1900 #[test]
1901 fn test_hamming_simd_non_multiple_of_8_length() {
1902 let a = vec![0xFF_u8; 11];
1904 let b = vec![0x00_u8; 11];
1905 assert_eq!(hamming_distance_simd(&a, &b), 88); }
1907
1908 #[test]
1909 fn test_hamming_simd_symmetry() {
1910 let a: Vec<u8> = (0..32).map(|i| i as u8).collect();
1911 let b: Vec<u8> = (0..32).map(|i| (i * 7 + 3) as u8).collect();
1912 assert_eq!(hamming_distance_simd(&a, &b), hamming_distance_simd(&b, &a));
1913 }
1914
1915 #[test]
1919 fn test_sat_rect_sum_correctness() {
1920 let w = 10usize;
1922 let h = 10usize;
1923 let gray: Vec<u8> = (0..h)
1924 .flat_map(|y| (0..w).map(move |x| (x + y) as u8))
1925 .collect();
1926
1927 let sat = SummedAreaTable::new(&gray, w, h);
1928
1929 let cases: &[(usize, usize, usize, usize)] = &[
1931 (0, 0, 10, 10), (2, 3, 7, 8), (0, 0, 1, 1), (9, 9, 10, 10), (0, 5, 10, 10), (3, 0, 6, 4), ];
1938
1939 for &(x1, y1, x2, y2) in cases {
1940 let expected: i64 = (y1..y2)
1941 .flat_map(|y| (x1..x2).map(move |x| (y, x)))
1942 .map(|(y, x)| i64::from(gray[y * w + x]))
1943 .sum();
1944 let got = sat.rect_sum(x1, y1, x2, y2);
1945 assert_eq!(
1946 got, expected,
1947 "rect_sum({x1},{y1},{x2},{y2}): expected {expected}, got {got}"
1948 );
1949 }
1950 }
1951
1952 #[test]
1957 fn test_sat_overflow_safety() {
1958 let w = 256usize;
1959 let h = 256usize;
1960 let gray = vec![255u8; w * h];
1961
1962 let sat = SummedAreaTable::new(&gray, w, h);
1963 let expected: i64 = 256 * 256 * 255;
1964 let got = sat.rect_sum(0, 0, w, h);
1965
1966 assert_eq!(
1967 got, expected,
1968 "256×256 all-255 image: expected {expected}, got {got}"
1969 );
1970 }
1971
1972 #[test]
1974 fn test_sat_rect_mean_uniform() {
1975 let w = 8usize;
1976 let h = 8usize;
1977 let gray = vec![42u8; w * h];
1978 let sat = SummedAreaTable::new(&gray, w, h);
1979 let mean = sat.rect_mean(1, 1, 7, 7).expect("should be Some");
1980 assert!((mean - 42.0).abs() < 1e-10, "mean={mean}");
1981 }
1982
1983 #[test]
1985 fn test_sat_single_pixel() {
1986 let gray = vec![77u8];
1987 let sat = SummedAreaTable::new(&gray, 1, 1);
1988 assert_eq!(sat.rect_sum(0, 0, 1, 1), 77);
1989 assert_eq!(sat.rect_sum(0, 0, 0, 1), 0); }
1991}