Skip to main content

oximedia_align/
features.rs

1//! Patent-free feature detection and matching.
2//!
3//! This module provides implementations of feature detection and description
4//! algorithms that are free from patent restrictions:
5//!
6//! - FAST corner detection
7//! - BRIEF binary descriptors
8//! - ORB (Oriented FAST and Rotated BRIEF)
9//! - Brute-force matching with Hamming distance
10
11use crate::{AlignError, AlignResult, Point2D};
12use rayon::prelude::*;
13use std::f64::consts::PI;
14
15/// A detected keypoint feature
16#[derive(Debug, Clone)]
17pub struct Keypoint {
18    /// Location of the keypoint
19    pub point: Point2D,
20    /// Scale (size) of the feature
21    pub scale: f32,
22    /// Orientation in radians
23    pub orientation: f32,
24    /// Response (strength) of the feature
25    pub response: f32,
26}
27
28impl Keypoint {
29    /// Create a new keypoint
30    #[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/// Binary descriptor (256 bits = 32 bytes)
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct BinaryDescriptor {
44    /// Binary descriptor data
45    pub data: [u8; 32],
46}
47
48impl BinaryDescriptor {
49    /// Create a new binary descriptor
50    #[must_use]
51    pub fn new(data: [u8; 32]) -> Self {
52        Self { data }
53    }
54
55    /// Compute Hamming distance to another descriptor
56    #[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    /// Create zero descriptor
66    #[must_use]
67    pub fn zero() -> Self {
68        Self { data: [0; 32] }
69    }
70}
71
72/// Compute Hamming distance between two byte slices using u64 popcount batching.
73///
74/// Processes 8 bytes at a time by casting to `u64` and using the hardware
75/// `POPCNT` instruction via `u64::count_ones()`. This is significantly faster
76/// than byte-by-byte XOR when matching large numbers of BRIEF descriptors.
77///
78/// The slices must be the same length; any trailing bytes (if length is not a
79/// multiple of 8) are handled byte-by-byte to avoid out-of-bounds access.
80///
81/// # Panics
82///
83/// Panics if `a.len() != b.len()`.
84#[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    // Process 8-byte (u64) chunks — maps to POPCNT on x86-64 / AArch64.
92    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    // Handle the remaining bytes (tail) if length % 8 != 0.
118    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/// A pair of matched features
127#[derive(Debug, Clone)]
128pub struct MatchPair {
129    /// Index in first image
130    pub idx1: usize,
131    /// Index in second image
132    pub idx2: usize,
133    /// Hamming distance
134    pub distance: u32,
135    /// Point in first image
136    pub point1: Point2D,
137    /// Point in second image
138    pub point2: Point2D,
139}
140
141impl MatchPair {
142    /// Create a new match pair
143    #[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
155/// FAST corner detector
156pub struct FastDetector {
157    /// Threshold for corner detection
158    pub threshold: u8,
159    /// Non-maximum suppression window size
160    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    /// Create a new FAST detector
174    #[must_use]
175    pub fn new(threshold: u8) -> Self {
176        Self {
177            threshold,
178            nms_window: 3,
179        }
180    }
181
182    /// Detect FAST corners in a grayscale image
183    ///
184    /// # Errors
185    /// Returns error if image dimensions are invalid
186    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        // FAST-9 circle offsets (16 pixels in a circle of radius 3)
195        let circle = self.get_circle_offsets(width);
196
197        // Detect corners (skip border)
198        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        // Apply non-maximum suppression
211        let corners = self.non_maximum_suppression(&corners, width, height);
212
213        Ok(corners)
214    }
215
216    /// Get FAST-9 circle offsets
217    fn get_circle_offsets(&self, width: usize) -> Vec<isize> {
218        let w = width as isize;
219        vec![
220            -w * 3,     // 0: top
221            -w * 3 + 1, // 1
222            -w * 2 + 2, // 2
223            -w + 3,     // 3: right
224            3,          // 4
225            w + 3,      // 5
226            w * 2 + 2,  // 6
227            w * 3 + 1,  // 7: bottom
228            w * 3,      // 8
229            w * 3 - 1,  // 9
230            w * 2 - 2,  // 10
231            w - 3,      // 11: left
232            -3,         // 12
233            -w - 3,     // 13
234            -w * 2 - 2, // 14
235            -w * 3 - 1, // 15
236        ]
237    }
238
239    /// Check if pixel is a corner
240    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        // Count consecutive brighter or darker pixels
245        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        // Need at least 9 consecutive pixels (FAST-9)
270        max_consecutive_brighter >= 9 || max_consecutive_darker >= 9
271    }
272
273    /// Compute corner response (strength)
274    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    /// Non-maximum suppression
294    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                    // Keep the one with higher response
320                    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
339/// Sub-pixel corner refinement using parabolic fitting.
340///
341/// After FAST detects corners at integer-pixel positions, this refiner
342/// fits a 2-D parabola to the corner response in a small neighbourhood and
343/// finds the sub-pixel maximum.  This improves localisation accuracy from
344/// ~0.5 px to ~0.05 px, which is critical for high-quality homography
345/// estimation and camera calibration.
346pub struct SubPixelRefiner {
347    /// Half-size of the refinement window.
348    pub half_window: usize,
349    /// Maximum number of Newton iterations.
350    pub max_iterations: usize,
351    /// Convergence threshold (pixels).
352    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    /// Create a new sub-pixel refiner.
367    #[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    /// Refine a set of keypoint positions to sub-pixel accuracy.
377    ///
378    /// Uses a quadratic surface fit: within the window around each keypoint,
379    /// fit `I(x,y) = a*x^2 + b*y^2 + c*x*y + d*x + e*y + f` and find the
380    /// extremum at `(-dI/dx, -dI/dy) / Hessian`.  This works for both
381    /// corners and peaks/blobs, unlike the structure tensor method which
382    /// only converges for true corners.
383    ///
384    /// Keypoints too close to the image border are left unmodified.
385    ///
386    /// # Errors
387    ///
388    /// Returns an error if the image dimensions are invalid.
389    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            // Skip if too close to border
408            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                // Fit quadratic surface I = a*dx^2 + b*dy^2 + c*dx*dy + d*dx + e*dy + f
425                // using least-squares over the window, where dx = x - rx, dy = y - ry.
426                //
427                // We only need the gradient (d, e) and Hessian (2a, c; c, 2b) to find
428                // the sub-pixel shift: delta = -H^{-1} * grad.
429                //
430                // Normal equations for [a, b, c, d, e, f]:
431                // Use the closed-form expressions from sums of monomials.
432                let mut _s_x2_val = 0.0_f64; // sum(dx^2 * I)
433                let mut _s_y2_val = 0.0_f64; // sum(dy^2 * I)
434                let mut _s_xy_val = 0.0_f64; // sum(dx*dy * I)
435                let mut _s_x_val = 0.0_f64; // sum(dx * I)
436                let mut _s_y_val = 0.0_f64; // sum(dy * I)
437
438                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                // For a symmetric window centred at origin, odd-power sums
475                // vanish (sum(x)=0, sum(x^3)=0, sum(x*y^2)=0, etc.).
476                // The normal equations decouple:
477                //
478                // For coefficient d: d = s_x_val / s_x2
479                // For coefficient e: e = s_y_val / s_y2
480                // For coefficients a, b: solve from s_x4, s_x2y2, s_y4
481                //   a*s_x4 + b*s_x2y2 = s_x2_val - f*s_x2
482                //   a*s_x2y2 + b*s_y4 = s_y2_val - f*s_y2
483                // where f = (sum(I) - a*s_x2 - b*s_y2) / count
484                //
485                // For our purposes we only need d and e (first-order) and
486                // a, b, c (second-order) to compute the Hessian.
487                //
488                // Simplified: just use the 3-point formula for the Hessian
489                // and gradient at the integer location for robustness.
490
491                // Gradient via central differences at (rx, ry)
492                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                // Hessian via central differences
499                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                // Newton step: delta = -H^{-1} * grad
516                let shift_x = -(hyy * gx - hxy * gy) / det;
517                let shift_y = -(-hxy * gx + hxx * gy) / det;
518
519                // Reject shifts larger than 1 pixel (non-convergent)
520                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            // Clamp to image bounds
533            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/// Compute Sobel gradients (f64 output) for sub-pixel refinement.
544/// Only used from tests; annotated to silence dead_code in non-test builds.
545#[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
572/// BRIEF descriptor extractor
573pub struct BriefDescriptor {
574    /// Patch size
575    pub patch_size: usize,
576    /// Sampling pattern
577    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    /// Create a new BRIEF descriptor extractor
588    #[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    /// Generate sampling pattern (deterministic)
598    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        // Use a deterministic pattern based on a simple PRNG
603        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    /// Simple LCG random number generator
616    fn next_random(seed: &mut u32) -> u32 {
617        *seed = seed.wrapping_mul(1103515245).wrapping_add(12345);
618        (*seed / 65536) % 32768
619    }
620
621    /// Extract BRIEF descriptor at a keypoint
622    ///
623    /// # Errors
624    /// Returns error if keypoint is too close to image border
625    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        // Check bounds
637        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    /// Extract descriptors for multiple keypoints in parallel
660    ///
661    /// # Errors
662    /// Returns error if any extraction fails
663    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
677/// ORB feature detector and descriptor
678pub struct OrbDetector {
679    /// FAST detector
680    fast: FastDetector,
681    /// BRIEF descriptor
682    brief: BriefDescriptor,
683    /// Number of features to retain
684    pub max_features: usize,
685}
686
687impl Default for OrbDetector {
688    fn default() -> Self {
689        Self::new(500)
690    }
691}
692
693impl OrbDetector {
694    /// Create a new ORB detector
695    #[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    /// Detect and describe ORB features
705    ///
706    /// # Errors
707    /// Returns error if detection or description fails
708    pub fn detect_and_compute(
709        &self,
710        image: &[u8],
711        width: usize,
712        height: usize,
713    ) -> AlignResult<(Vec<Keypoint>, Vec<BinaryDescriptor>)> {
714        // Detect FAST corners
715        let mut keypoints = self.fast.detect(image, width, height)?;
716
717        // Compute orientation for each keypoint
718        for kp in &mut keypoints {
719            kp.orientation = self.compute_orientation(image, width, height, kp);
720        }
721
722        // Keep top N features by response
723        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        // Extract BRIEF descriptors
731        let descriptors = self.brief.extract_batch(image, width, height, &keypoints)?;
732
733        Ok((keypoints, descriptors))
734    }
735
736    /// Compute orientation using intensity centroid
737    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
774/// Brute-force feature matcher
775pub struct FeatureMatcher {
776    /// Maximum Hamming distance for a valid match
777    pub max_distance: u32,
778    /// Ratio test threshold (Lowe's ratio)
779    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    /// Create a new feature matcher
793    #[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    /// Match features using brute-force with ratio test
802    #[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                // Find two best matches
815                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                // Apply distance threshold and ratio test
832                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    /// Filter matches using geometric consistency
851    #[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        // Simple geometric filter: check consistency with median displacement
862        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    /// Compute median of a slice
887    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// ── Summed-Area Table (integral image) ────────────────────────────────────────
905
906/// Summed-area table (SAT) for O(1) rectangular sum queries over a grayscale image.
907///
908/// Built in O(W×H) via the 2-D prefix-sum recurrence; each query is O(1) using
909/// the four-corner formula.  Storage is `i64` to prevent overflow for large images.
910#[derive(Debug, Clone)]
911pub struct SummedAreaTable {
912    /// Row-major SAT; size `(width+1) × (height+1)` with zero-padded borders.
913    data: Vec<i64>,
914    /// Width of the source image in pixels.
915    pub width: usize,
916    /// Height of the source image in pixels.
917    pub height: usize,
918}
919
920impl SummedAreaTable {
921    /// Build the SAT from an 8-bit grayscale image.  `gray` must have `width * height` elements.
922    ///
923    /// # Panics
924    /// Panics in debug builds if `gray.len() != width * height`.
925    #[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                // Indices into the padded (stride × (height+1)) array:
943                //   sat row y+1, column x+1
944                let above = data[y * stride + (x + 1)]; // sat[y][x+1]   — row y in padded
945                let left = data[(y + 1) * stride + x]; // sat[y+1][x]   — col x in padded
946                let diag = data[y * stride + x]; // sat[y][x]     — diag
947
948                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    /// Return the sum of pixels in `[x1, x2) × [y1, y2)` (O(1)).
960    /// Coordinates are clamped; returns 0 for empty rectangles.
961    #[must_use]
962    pub fn rect_sum(&self, x1: usize, y1: usize, x2: usize, y2: usize) -> i64 {
963        // Clamp to valid range.
964        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    /// Mean pixel value in the rectangle, or `None` if the area is zero.
983    #[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
996/// Harris corner detector (alternative to FAST)
997pub struct HarrisDetector {
998    /// Threshold for corner detection
999    pub threshold: f32,
1000    /// Window size for gradient computation
1001    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    /// Create a new Harris detector
1015    #[must_use]
1016    pub fn new(threshold: f32) -> Self {
1017        Self {
1018            threshold,
1019            window_size: 3,
1020        }
1021    }
1022
1023    /// Detect Harris corners.
1024    ///
1025    /// Uses a `SummedAreaTable` for accelerated NMS: after computing the Harris
1026    /// response for every candidate pixel, we build a SAT over an 8-bit
1027    /// quantisation of the response magnitude.  For each candidate corner we
1028    /// query the neighbourhood box sum in O(1); if the corner's response is not
1029    /// the strict maximum in its `nms_half × nms_half` neighbourhood (determined
1030    /// by comparing the box average to the corner's own value), the candidate is
1031    /// suppressed.  This replaces the O(k²) per-candidate neighbourhood scan with
1032    /// an O(1) SAT lookup.
1033    ///
1034    /// # Errors
1035    /// Returns error if image dimensions are invalid
1036    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        // Compute gradients
1042        let (grad_x, grad_y) = self.compute_gradients(image, width, height);
1043
1044        // ── Pass 1: compute Harris response for every interior pixel ──────────
1045        let k = 0.04_f32; // Harris parameter
1046        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        // ── Pass 2: SAT-accelerated NMS ───────────────────────────────────────
1080        //
1081        // Quantise the response map to u8 (0–255) so we can build a SAT over it.
1082        // The peak response value is used for normalisation.
1083        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        // NMS half-window.  Use at least 1 pixel.
1103        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                // Candidate's own quantised value.
1112                let own_q = quantised[cy * width + cx] as i64;
1113
1114                // Neighbourhood rectangle [x1, x2) × [y1, y2).
1115                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                // Keep this corner only if its value is strictly greater than
1128                // the neighbourhood average.  This ensures the local maximum
1129                // is retained while suppressing weaker neighbours.
1130                own_q * area > box_sum
1131            })
1132            .collect();
1133
1134        Ok(corners)
1135    }
1136
1137    /// Compute image gradients using Sobel operator
1138    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                // Sobel X
1147                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                // Sobel Y
1155                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
1171/// Feature pyramid for multi-scale feature detection
1172pub struct FeaturePyramid {
1173    /// Number of pyramid levels
1174    pub num_levels: usize,
1175    /// Scale factor between levels
1176    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    /// Create a new feature pyramid
1190    #[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    /// Build image pyramid
1199    #[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                &current_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    /// Downsample image
1239    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    /// Detect features at all pyramid levels
1267    ///
1268    /// # Errors
1269    /// Returns error if detection fails
1270    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            // Rescale keypoints to original image coordinates
1284            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
1297/// Adaptive non-maximum suppression for better feature distribution
1298pub struct AdaptiveNMS {
1299    /// Radius for NMS
1300    pub radius: f32,
1301    /// Number of features to retain
1302    pub num_features: usize,
1303}
1304
1305impl AdaptiveNMS {
1306    /// Create a new adaptive NMS
1307    #[must_use]
1308    pub fn new(radius: f32, num_features: usize) -> Self {
1309        Self {
1310            radius,
1311            num_features,
1312        }
1313    }
1314
1315    /// Apply adaptive NMS to keypoints
1316    #[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
1353/// Outlier filter using statistical methods
1354pub struct OutlierFilter {
1355    /// Distance threshold multiplier
1356    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    /// Create a new outlier filter
1369    #[must_use]
1370    pub fn new(threshold_multiplier: f32) -> Self {
1371        Self {
1372            threshold_multiplier,
1373        }
1374    }
1375
1376    /// Filter outlier matches
1377    #[must_use]
1378    pub fn filter(&self, matches: &[MatchPair]) -> Vec<MatchPair> {
1379        if matches.len() < 3 {
1380            return matches.to_vec();
1381        }
1382
1383        // Compute distance statistics
1384        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
1411/// Cross-check matcher for bidirectional matching
1412pub struct CrossCheckMatcher {
1413    /// Base matcher
1414    base_matcher: FeatureMatcher,
1415}
1416
1417impl Default for CrossCheckMatcher {
1418    fn default() -> Self {
1419        Self::new()
1420    }
1421}
1422
1423impl CrossCheckMatcher {
1424    /// Create a new cross-check matcher
1425    #[must_use]
1426    pub fn new() -> Self {
1427        Self {
1428            base_matcher: FeatureMatcher::default(),
1429        }
1430    }
1431
1432    /// Match with cross-check (mutual nearest neighbors)
1433    #[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        // Forward matches (1 -> 2)
1442        let forward =
1443            self.base_matcher
1444                .match_features(keypoints1, descriptors1, keypoints2, descriptors2);
1445
1446        // Backward matches (2 -> 1)
1447        let backward =
1448            self.base_matcher
1449                .match_features(keypoints2, descriptors2, keypoints1, descriptors1);
1450
1451        // Keep only mutual matches
1452        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
1467/// Feature descriptor with FREAK-like pattern
1468pub struct FreakDescriptor {
1469    /// Number of sampling pairs
1470    pub num_pairs: usize,
1471    /// Pattern scale
1472    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    /// Create a new FREAK descriptor
1486    #[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    /// Extract descriptor (simplified FREAK-like)
1495    ///
1496    /// # Errors
1497    /// Returns error if extraction fails
1498    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        // Simplified retina-inspired sampling pattern
1522        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    /// Simple linear congruential generator
1558    fn lcg(seed: &mut u32) -> u32 {
1559        *seed = seed.wrapping_mul(1103515245).wrapping_add(12345);
1560        (*seed / 65536) % 32768
1561    }
1562}
1563
1564/// Descriptor variance filter to remove low-quality features
1565pub struct DescriptorVarianceFilter {
1566    /// Minimum variance threshold
1567    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    /// Create a new variance filter
1578    #[must_use]
1579    pub fn new(min_variance: f32) -> Self {
1580        Self { min_variance }
1581    }
1582
1583    /// Filter keypoints by descriptor variance
1584    #[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    /// Compute descriptor variance (ratio of set bits)
1605    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        // Variance is highest when ratio is close to 0.5
1612        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); // First level width
1685        assert_eq!(levels[0].2, 100); // First level height
1686    }
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]); // 50% set bits
1737        let variance = filter.compute_variance(&desc);
1738        assert!((variance - 1.0).abs() < 0.01);
1739    }
1740
1741    // ── SubPixelRefiner ─────────────────────────────────────────────────────
1742
1743    #[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        // Create a 128x128 image with a Gaussian peak centred at (64.3, 64.7)
1775        // Use a larger image and wider Gaussian for stable gradients with 8-bit
1776        // quantisation.
1777        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        // Start 2 pixels away from the true centre
1794        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        // The refinement should at least not diverge wildly -- allow up to 0.5
1801        // pixel degradation due to 8-bit quantisation artefacts.
1802        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        // Keypoint at the border should be returned unmodified
1813        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    // -- hamming_distance_simd ------------------------------------------------
1859
1860    #[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; // 4 set bits
1878        b[0] = 0b0000_1111; // 4 set bits, all different positions
1879                            // XOR = 0b1111_1111 = 8 differing bits
1880        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        // 11 bytes — not a multiple of 8 — exercises the tail handling path.
1903        let a = vec![0xFF_u8; 11];
1904        let b = vec![0x00_u8; 11];
1905        assert_eq!(hamming_distance_simd(&a, &b), 88); // 11 × 8 bits
1906    }
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    // ── SummedAreaTable ────────────────────────────────────────────────────────
1916
1917    /// Verify `rect_sum` against brute-force sum for a 10×10 image.
1918    #[test]
1919    fn test_sat_rect_sum_correctness() {
1920        // Build a 10×10 image where pixel[y*10+x] = (x + y) as u8 (values 0..18)
1921        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        // Check several rectangles against brute-force.
1930        let cases: &[(usize, usize, usize, usize)] = &[
1931            (0, 0, 10, 10), // full image
1932            (2, 3, 7, 8),   // interior rectangle
1933            (0, 0, 1, 1),   // top-left single pixel
1934            (9, 9, 10, 10), // bottom-right single pixel
1935            (0, 5, 10, 10), // bottom half
1936            (3, 0, 6, 4),   // tall narrow strip
1937        ];
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    /// Verify that the SAT handles a large all-255 image without overflow.
1953    ///
1954    /// A 256×256 image filled with 255 must yield a full-image rect_sum of
1955    /// `256 * 256 * 255 = 16,711,680`, well within `i64` range.
1956    #[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    /// Verify that `rect_mean` returns the correct mean for a uniform image.
1973    #[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    /// Verify that `SummedAreaTable` works correctly for a single-pixel image.
1984    #[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); // empty x-range
1990    }
1991}