Skip to main content

iris/features/
mod.rs

1pub mod matching;
2
3pub use matching::{BFMatcher, DMatch, FlannMatcher, MatchDrawer};
4
5use crate::core::types::Point;
6use crate::error::{IrisError, Result};
7use crate::image::Image;
8use burn::tensor::{Tensor, backend::Backend};
9
10/// Template matching method.
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum TemplateMatchMethod {
13    /// Sum of squared differences (unnormalized).
14    TmSqdiff,
15    /// Sum of squared differences (normalized).
16    TmSqdiffNormed,
17    /// Cross-correlation (unnormalized).
18    TmCcorr,
19    /// Cross-correlation (normalized).
20    TmCcorrNormed,
21    /// Cross-correlation coefficient (unnormalized).
22    TmCcoeff,
23    /// Cross-correlation coefficient (normalized).
24    TmCcoeffNormed,
25}
26
27/// Performs template matching using sliding window correlation.
28///
29/// Returns a 2D tensor of shape `[H - th + 1, W - tw + 1]` where `(th, tw)` is the template size.
30pub fn template_match<B: Backend>(
31    source: &Image<B>,
32    template: &Image<B>,
33    method: TemplateMatchMethod,
34) -> crate::error::Result<Tensor<B, 2>> {
35    let src_dims = source.tensor.dims();
36    let tpl_dims = template.tensor.dims();
37
38    let src_h = src_dims[1];
39    let src_w = src_dims[2];
40    let tpl_h = tpl_dims[1];
41    let tpl_w = tpl_dims[2];
42
43    if tpl_h > src_h || tpl_w > src_w {
44        return Err(IrisError::DimensionMismatch {
45            expected: vec![src_h, src_w],
46            actual: vec![tpl_h, tpl_w],
47        });
48    }
49
50    let src_data = source.tensor.clone().into_data();
51    let tpl_data = template.tensor.clone().into_data();
52    let src_flat: Vec<f32> = src_data.iter::<f32>().collect();
53    let tpl_flat: Vec<f32> = tpl_data.iter::<f32>().collect();
54
55    let src_channels = src_dims[0];
56    let tpl_channels = tpl_dims[0];
57
58    let out_h = src_h - tpl_h + 1;
59    let out_w = src_w - tpl_w + 1;
60    let mut result = vec![0.0f32; out_h * out_w];
61
62    // Compute template mean for CCOEFF methods
63    let tpl_mean: f32 = tpl_flat.iter().sum::<f32>() / tpl_flat.len() as f32;
64    let tpl_sub: Vec<f32> = tpl_flat.iter().map(|&v| v - tpl_mean).collect();
65    let tpl_norm: f32 = tpl_sub.iter().map(|v| v * v).sum::<f32>().sqrt();
66
67    for oy in 0..out_h {
68        for ox in 0..out_w {
69            let mut sum = 0.0f32;
70
71            // Compute correlation/difference
72            for c in 0..src_channels.min(tpl_channels) {
73                for ty in 0..tpl_h {
74                    for tx in 0..tpl_w {
75                        let si = c * src_h * src_w + (oy + ty) * src_w + (ox + tx);
76                        let ti = c * tpl_h * tpl_w + ty * tpl_w + tx;
77                        let sv = src_flat[si];
78                        let tv = tpl_flat[ti];
79
80                        match method {
81                            TemplateMatchMethod::TmSqdiff | TemplateMatchMethod::TmSqdiffNormed => {
82                                let diff = sv - tv;
83                                sum += diff * diff;
84                            }
85                            TemplateMatchMethod::TmCcorr | TemplateMatchMethod::TmCcorrNormed => {
86                                sum += sv * tv;
87                            }
88                            TemplateMatchMethod::TmCcoeff | TemplateMatchMethod::TmCcoeffNormed => {
89                                let src_sub = sv - {
90                                    let mut region_sum = 0.0f32;
91                                    for rty in 0..tpl_h {
92                                        for rtx in 0..tpl_w {
93                                            let ri =
94                                                c * src_h * src_w + (oy + rty) * src_w + (ox + rtx);
95                                            region_sum += src_flat[ri];
96                                        }
97                                    }
98                                    region_sum / (tpl_h * tpl_w) as f32
99                                };
100                                sum += src_sub * tpl_sub[c * tpl_h * tpl_w + ty * tpl_w + tx];
101                            }
102                        }
103                    }
104                }
105            }
106
107            // Normalize if needed
108            match method {
109                TemplateMatchMethod::TmSqdiffNormed => {
110                    let mut src_sum_sq = 0.0f32;
111                    for c in 0..src_channels.min(tpl_channels) {
112                        for ty in 0..tpl_h {
113                            for tx in 0..tpl_w {
114                                let si = c * src_h * src_w + (oy + ty) * src_w + (ox + tx);
115                                let v = src_flat[si];
116                                src_sum_sq += v * v;
117                            }
118                        }
119                    }
120                    let denom = (src_sum_sq * tpl_flat.iter().map(|v| v * v).sum::<f32>()).sqrt();
121                    if denom > 1e-10 {
122                        result[oy * out_w + ox] = sum / denom;
123                    }
124                }
125                TemplateMatchMethod::TmCcorrNormed => {
126                    let mut src_norm = 0.0f32;
127                    for c in 0..src_channels.min(tpl_channels) {
128                        for ty in 0..tpl_h {
129                            for tx in 0..tpl_w {
130                                let si = c * src_h * src_w + (oy + ty) * src_w + (ox + tx);
131                                let v = src_flat[si];
132                                src_norm += v * v;
133                            }
134                        }
135                    }
136                    let denom = src_norm.sqrt() * tpl_norm;
137                    if denom > 1e-10 {
138                        result[oy * out_w + ox] = sum / denom;
139                    }
140                }
141                TemplateMatchMethod::TmCcoeffNormed => {
142                    let mut src_sum = 0.0f32;
143                    let mut src_sum_sq = 0.0f32;
144                    let count = (src_channels.min(tpl_channels) * tpl_h * tpl_w) as f32;
145                    for c in 0..src_channels.min(tpl_channels) {
146                        for ty in 0..tpl_h {
147                            for tx in 0..tpl_w {
148                                let si = c * src_h * src_w + (oy + ty) * src_w + (ox + tx);
149                                let v = src_flat[si];
150                                src_sum += v;
151                                src_sum_sq += v * v;
152                            }
153                        }
154                    }
155                    let src_mean = src_sum / count;
156                    let src_var = src_sum_sq - count * src_mean * src_mean;
157                    let denom = (src_var.max(0.0)).sqrt() * tpl_norm;
158                    if denom > 1e-10 {
159                        result[oy * out_w + ox] = sum / denom;
160                    }
161                }
162                // Unnormalized methods keep sum as-is
163                _ => {
164                    result[oy * out_w + ox] = sum;
165                }
166            }
167        }
168    }
169
170    let device = source.tensor.device();
171    let data = burn::tensor::TensorData::new(result, [out_h, out_w]);
172    Ok(Tensor::<B, 2>::from_data(data, &device))
173}
174
175/// Represents a detected keypoint in an image.
176#[derive(Clone, Debug, PartialEq)]
177pub struct KeyPoint {
178    /// Coordinates of the keypoint.
179    pub pt: Point<f64>,
180    /// Diameter of the meaningful keypoint neighborhood.
181    pub size: f64,
182    /// Computed orientation of the keypoint (degrees).
183    pub angle: f64,
184    /// Strength/response of the keypoint.
185    pub response: f64,
186    /// Octave (pyramid layer) from which the keypoint was extracted.
187    pub octave: i32,
188    /// Object class ID.
189    pub class_id: i32,
190}
191
192impl KeyPoint {
193    #[must_use]
194    pub fn new(x: f64, y: f64, size: f64) -> Self {
195        Self {
196            pt: Point::new(x, y),
197            size,
198            angle: -1.0,
199            response: 0.0,
200            octave: 0,
201            class_id: -1,
202        }
203    }
204}
205
206/// Feature detector type.
207pub enum FeatureType {
208    ORB,
209    BRISK,
210    AKAZE,
211    SIFT,
212}
213
214pub struct FeatureDetector {
215    #[allow(dead_code)]
216    detector_type: FeatureType,
217    max_features: usize,
218}
219
220impl FeatureDetector {
221    #[must_use]
222    pub fn new(detector_type: FeatureType) -> Self {
223        Self {
224            detector_type,
225            max_features: 500,
226        }
227    }
228
229    /// Sets the maximum number of features to detect.
230    #[must_use]
231    pub fn with_max_features(mut self, max: usize) -> Self {
232        self.max_features = max;
233        self
234    }
235
236    /// Detects keypoints in an image using the FAST corner detector.
237    /// FAST checks a circle of 16 pixels around each candidate point
238    /// and requires at least 12 contiguous pixels to be brighter or darker.
239    pub fn detect<B: Backend>(&self, image: &Image<B>) -> Result<Vec<KeyPoint>> {
240        let gray = image.grayscale()?;
241        let dims = gray.tensor.dims();
242        let h = dims[1];
243        let w = dims[2];
244
245        let tensor_data = gray.tensor.clone().into_data();
246        let flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
247
248        let mut keypoints = Vec::new();
249        let border = 3; // FAST radius = 3
250
251        // FAST circle offsets (16 points around center)
252        let circle: [(i32, i32); 16] = [
253            (0, -3),
254            (1, -3),
255            (2, -2),
256            (3, -1),
257            (3, 0),
258            (3, 1),
259            (2, 2),
260            (1, 3),
261            (0, 3),
262            (-1, 3),
263            (-2, 2),
264            (-3, 1),
265            (-3, 0),
266            (-3, -1),
267            (-2, -2),
268            (-1, -3),
269        ];
270
271        // Threshold for brightness difference (relative to center pixel)
272        let threshold = 10.0f32 / 255.0;
273        let n_points = 9; // Require 9 of 16 contiguous pixels
274
275        for y in border..(h - border) {
276            for x in border..(w - border) {
277                let center = flat_vals[y * w + x];
278
279                // Quick check: pixels 0, 4, 8, 12 must all be brighter or darker
280                let p0 = flat_vals
281                    [(y as i32 + circle[0].1) as usize * w + (x as i32 + circle[0].0) as usize];
282                let p4 = flat_vals
283                    [(y as i32 + circle[4].1) as usize * w + (x as i32 + circle[4].0) as usize];
284                let p8 = flat_vals
285                    [(y as i32 + circle[8].1) as usize * w + (x as i32 + circle[8].0) as usize];
286                let p12 = flat_vals
287                    [(y as i32 + circle[12].1) as usize * w + (x as i32 + circle[12].0) as usize];
288
289                let all_bright = p0 > center + threshold
290                    && p4 > center + threshold
291                    && p8 > center + threshold
292                    && p12 > center + threshold;
293                let all_dark = p0 < center - threshold
294                    && p4 < center - threshold
295                    && p8 < center - threshold
296                    && p12 < center - threshold;
297
298                if !all_bright && !all_dark {
299                    continue;
300                }
301
302                // Check full circle for contiguous arc of n_points
303                let mut max_arc = 0;
304                let mut current_arc = 0;
305
306                // Read all 16 circle pixels
307                let mut circle_vals = [0.0f32; 16];
308                for i in 0..16 {
309                    let nx = x as i32 + circle[i].0;
310                    let ny = y as i32 + circle[i].1;
311                    circle_vals[i] = flat_vals[ny as usize * w + nx as usize];
312                }
313
314                for i in 0..32 {
315                    let val = circle_vals[i % 16];
316                    if (all_bright && val > center + threshold)
317                        || (all_dark && val < center - threshold)
318                    {
319                        current_arc += 1;
320                        if current_arc > max_arc {
321                            max_arc = current_arc;
322                        }
323                    } else {
324                        current_arc = 0;
325                    }
326                }
327
328                if max_arc >= n_points {
329                    // Compute response using intensity difference in center of arc
330                    let mut sum_diff = 0.0f32;
331                    for i in 0..16 {
332                        let diff = (circle_vals[i] - center).abs();
333                        sum_diff += diff;
334                    }
335                    let response = sum_diff / 16.0;
336
337                    let mut kp = KeyPoint::new(x as f64, y as f64, 3.0);
338                    kp.response = response as f64;
339                    kp.octave = 0;
340                    keypoints.push(kp);
341                }
342            }
343        }
344
345        // Sort by response strength and keep top N
346        keypoints.sort_by(|a, b| b.response.partial_cmp(&a.response).unwrap());
347        keypoints.truncate(self.max_features);
348
349        // Non-maximum suppression: remove keypoints too close together
350        let mut suppressed = Vec::new();
351        let min_dist = 7.0;
352        for kp in &keypoints {
353            let too_close = suppressed.iter().any(|other: &KeyPoint| {
354                let dx = kp.pt.x - other.pt.x;
355                let dy = kp.pt.y - other.pt.y;
356                (dx * dx + dy * dy).sqrt() < min_dist
357            });
358            if !too_close {
359                suppressed.push(kp.clone());
360            }
361        }
362
363        Ok(suppressed)
364    }
365
366    /// Computes ORB descriptors for detected keypoints.
367    /// ORB uses BRIEF descriptors with rotation invariance via intensity centroid.
368    /// Returns a descriptor tensor of shape [`NumKeyPoints`, `DescriptorDim`].
369    pub fn compute<B: Backend>(
370        &self,
371        image: &Image<B>,
372        keypoints: &[KeyPoint],
373    ) -> Result<Tensor<B, 2>> {
374        let gray = image.grayscale()?;
375        let dims = gray.tensor.dims();
376        let h = dims[1];
377        let w = dims[2];
378
379        let tensor_data = gray.tensor.clone().into_data();
380        let flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
381
382        let n = keypoints.len();
383        let descriptor_dim = 32; // ORB uses 256-bit descriptors = 32 bytes
384        let mut descriptors = vec![0u8; n * descriptor_dim];
385
386        // Fixed random sampling pattern for BRIEF (deterministic, 256 pairs)
387        let pattern: [(i32, i32, i32, i32); 256] = generate_brief_pattern();
388
389        for (ki, kp) in keypoints.iter().enumerate() {
390            let cx = kp.pt.x as i32;
391            let cy = kp.pt.y as i32;
392
393            // Compute intensity centroid angle for rotation invariance
394            let m10 = compute_moment(&flat_vals, w, h, cx, cy, 1, 0);
395            let m01 = compute_moment(&flat_vals, w, h, cx, cy, 0, 1);
396            let angle = m01.atan2(m10); // radians
397
398            let cos_a = angle.cos();
399            let sin_a = angle.sin();
400
401            // Sample 256 bit pairs
402            for byte_idx in 0..descriptor_dim {
403                let mut byte_val = 0u8;
404                for bit_idx in 0..8 {
405                    let pair_idx = byte_idx * 8 + bit_idx;
406                    let (dx1, dy1, dx2, dy2) = pattern[pair_idx];
407
408                    // Rotate sampling pattern
409                    let rx1 = (dx1 as f64 * cos_a - dy1 as f64 * sin_a) as i32;
410                    let ry1 = (dx1 as f64 * sin_a + dy1 as f64 * cos_a) as i32;
411                    let rx2 = (dx2 as f64 * cos_a - dy2 as f64 * sin_a) as i32;
412                    let ry2 = (dx2 as f64 * sin_a + dy2 as f64 * cos_a) as i32;
413
414                    let px1 = (cx + rx1).clamp(0, w as i32 - 1) as usize;
415                    let py1 = (cy + ry1).clamp(0, h as i32 - 1) as usize;
416                    let px2 = (cx + rx2).clamp(0, w as i32 - 1) as usize;
417                    let py2 = (cy + ry2).clamp(0, h as i32 - 1) as usize;
418
419                    let val1 = flat_vals[py1 * w + px1];
420                    let val2 = flat_vals[py2 * w + px2];
421
422                    if val1 < val2 {
423                        byte_val |= 1 << bit_idx;
424                    }
425                }
426                descriptors[ki * descriptor_dim + byte_idx] = byte_val;
427            }
428        }
429
430        // Convert u8 descriptors to float tensor for Burn compatibility
431        let float_desc: Vec<f32> = descriptors.iter().map(|&b| b as f32).collect();
432        let device = image.tensor.device();
433        let data = burn::tensor::TensorData::new(float_desc, [n, descriptor_dim]);
434        let tensor = Tensor::<B, 2>::from_data(data, &device);
435        Ok(tensor)
436    }
437}
438
439/// Compute a spatial moment of image pixels around a point.
440fn compute_moment(
441    flat_vals: &[f32],
442    w: usize,
443    h: usize,
444    cx: i32,
445    cy: i32,
446    px: i32,
447    py: i32,
448) -> f64 {
449    let radius = 15;
450    let mut sum = 0.0f64;
451    for dy in -radius..=radius {
452        for dx in -radius..=radius {
453            let nx = cx + dx;
454            let ny = cy + dy;
455            if nx >= 0 && nx < w as i32 && ny >= 0 && ny < h as i32 {
456                let val = flat_vals[ny as usize * w + nx as usize] as f64;
457                sum += val * (dx as f64).powi(px) * (dy as f64).powi(py);
458            }
459        }
460    }
461    sum
462}
463
464/// Generates a fixed pseudo-random sampling pattern for BRIEF descriptors.
465fn generate_brief_pattern() -> [(i32, i32, i32, i32); 256] {
466    let mut pattern = [(0i32, 0i32, 0i32, 0i32); 256];
467    // Simple deterministic pattern using linear congruential generator
468    let mut seed: u32 = 42;
469    for i in 0..256 {
470        seed = seed.wrapping_mul(1103515245).wrapping_add(12345);
471        let x1 = ((seed >> 16) as i32 % 31) - 15;
472        seed = seed.wrapping_mul(1103515245).wrapping_add(12345);
473        let y1 = ((seed >> 16) as i32 % 31) - 15;
474        seed = seed.wrapping_mul(1103515245).wrapping_add(12345);
475        let x2 = ((seed >> 16) as i32 % 31) - 15;
476        seed = seed.wrapping_mul(1103515245).wrapping_add(12345);
477        let y2 = ((seed >> 16) as i32 % 31) - 15;
478        pattern[i] = (x1, y1, x2, y2);
479    }
480    pattern
481}
482
483#[cfg(test)]
484mod tests {
485    use super::*;
486    use crate::test_helpers::{TestBackend, test_device};
487    use burn::tensor::TensorData;
488
489    #[test]
490    fn test_orb_feature_detection() {
491        let device = test_device();
492        // Create an image with strong edges that produce FAST corners
493        // Use a cross pattern: vertical and horizontal bars
494        let mut flat_data = vec![0.0f32; 3 * 100 * 100];
495        // Vertical bar
496        for y in 0..100 {
497            for x in 45..55 {
498                flat_data[y * 100 + x] = 1.0;
499                flat_data[10000 + y * 100 + x] = 1.0;
500                flat_data[20000 + y * 100 + x] = 1.0;
501            }
502        }
503        // Horizontal bar
504        for y in 45..55 {
505            for x in 0..100 {
506                flat_data[y * 100 + x] = 1.0;
507                flat_data[10000 + y * 100 + x] = 1.0;
508                flat_data[20000 + y * 100 + x] = 1.0;
509            }
510        }
511        let tensor =
512            Tensor::<TestBackend, 3>::from_data(TensorData::new(flat_data, [3, 100, 100]), &device);
513        let img = Image::new(tensor);
514
515        let detector = FeatureDetector::new(FeatureType::ORB).with_max_features(50);
516        let keypoints = detector.detect(&img).unwrap();
517
518        // Verify API correctness: returns correct type and bounds
519        for kp in &keypoints {
520            assert!(kp.pt.x >= 0.0 && kp.pt.x < 100.0);
521            assert!(kp.pt.y >= 0.0 && kp.pt.y < 100.0);
522            assert!(kp.size > 0.0);
523            assert!(kp.response >= 0.0);
524        }
525
526        let descriptors = detector.compute(&img, &keypoints).unwrap();
527        assert_eq!(descriptors.dims(), [keypoints.len(), 32]);
528    }
529
530    #[test]
531    fn test_template_match() {
532        let device = test_device();
533        // Create a 6x6 image with a white block in the corner
534        let mut src_data = vec![0.0f32; 3 * 6 * 6];
535        // White block at top-left 3x3
536        for c in 0..3 {
537            for y in 0..3 {
538                for x in 0..3 {
539                    src_data[c * 36 + y * 6 + x] = 1.0;
540                }
541            }
542        }
543        let src_tensor = Tensor::<TestBackend, 3>::from_data(
544            TensorData::new(src_data.clone(), [3, 6, 6]),
545            &device,
546        );
547        let src_img = Image::new(src_tensor);
548
549        // Template: same 3x3 white block
550        let mut tpl_data = vec![0.0f32; 3 * 3 * 3];
551        for c in 0..3 {
552            for y in 0..3 {
553                for x in 0..3 {
554                    tpl_data[c * 9 + y * 3 + x] = 1.0;
555                }
556            }
557        }
558        let tpl_tensor =
559            Tensor::<TestBackend, 3>::from_data(TensorData::new(tpl_data, [3, 3, 3]), &device);
560        let tpl_img = Image::new(tpl_tensor);
561
562        // TM_SQDIFF: minimum should be at (0,0) where template matches perfectly
563        let result = src_img
564            .template_match(&tpl_img, TemplateMatchMethod::TmSqdiff)
565            .unwrap();
566        assert_eq!(result.dims(), [4, 4]);
567
568        let result_data = result.into_data();
569        let vals: Vec<f32> = result_data.iter::<f32>().collect();
570
571        // Position (0,0) should have zero difference (perfect match)
572        assert!(
573            vals[0] < 0.01,
574            "Expected near-zero at (0,0), got {}",
575            vals[0]
576        );
577
578        // TM_CCORR: maximum should be at (0,0)
579        let result_corr = src_img
580            .template_match(&tpl_img, TemplateMatchMethod::TmCcorr)
581            .unwrap();
582        let corr_data = result_corr.into_data();
583        let corr_vals: Vec<f32> = corr_data.iter::<f32>().collect();
584        assert!(
585            corr_vals[0] > corr_vals[1],
586            "Expected (0,0) to have higher correlation"
587        );
588    }
589}