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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum TemplateMatchMethod {
13 TmSqdiff,
15 TmSqdiffNormed,
17 TmCcorr,
19 TmCcorrNormed,
21 TmCcoeff,
23 TmCcoeffNormed,
25}
26
27pub 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 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 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 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 _ => {
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#[derive(Clone, Debug, PartialEq)]
177pub struct KeyPoint {
178 pub pt: Point<f64>,
180 pub size: f64,
182 pub angle: f64,
184 pub response: f64,
186 pub octave: i32,
188 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
206pub 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 #[must_use]
231 pub fn with_max_features(mut self, max: usize) -> Self {
232 self.max_features = max;
233 self
234 }
235
236 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; 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 let threshold = 10.0f32 / 255.0;
273 let n_points = 9; for y in border..(h - border) {
276 for x in border..(w - border) {
277 let center = flat_vals[y * w + x];
278
279 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 let mut max_arc = 0;
304 let mut current_arc = 0;
305
306 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 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 keypoints.sort_by(|a, b| b.response.partial_cmp(&a.response).unwrap());
347 keypoints.truncate(self.max_features);
348
349 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 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; let mut descriptors = vec![0u8; n * descriptor_dim];
385
386 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 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); let cos_a = angle.cos();
399 let sin_a = angle.sin();
400
401 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 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 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
439fn 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
464fn generate_brief_pattern() -> [(i32, i32, i32, i32); 256] {
466 let mut pattern = [(0i32, 0i32, 0i32, 0i32); 256];
467 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 let mut flat_data = vec![0.0f32; 3 * 100 * 100];
495 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 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 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 let mut src_data = vec![0.0f32; 3 * 6 * 6];
535 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 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 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 assert!(
573 vals[0] < 0.01,
574 "Expected near-zero at (0,0), got {}",
575 vals[0]
576 );
577
578 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}