1use crate::error::{Result, VisionError};
7use image::{DynamicImage, GenericImageView, GrayImage, Rgb, RgbImage};
8use scirs2_core::ndarray::ArrayStatCompat;
9use scirs2_core::ndarray::{s, Array2};
10use scirs2_core::parallel_ops::*;
11use statrs::statistics::Statistics;
12
13#[derive(Debug, Clone, Copy)]
15pub enum MatchMethod {
16 SumSquaredDiff,
18 NormalizedSumSquaredDiff,
20 CrossCorrelation,
22 NormalizedCrossCorrelation,
24 CorrelationCoeff,
26 NormalizedCorrelationCoeff,
28}
29
30#[derive(Debug, Clone)]
32pub struct MatchResult {
33 pub x: u32,
35 pub y: u32,
37 pub score: f32,
39}
40
41#[allow(dead_code)]
67pub fn template_match(
68 img: &DynamicImage,
69 template: &DynamicImage,
70 method: MatchMethod,
71) -> Result<Array2<f32>> {
72 let gray_img = img.to_luma8();
73 let gray_template = template.to_luma8();
74
75 match method {
76 MatchMethod::SumSquaredDiff => match_ssd(&gray_img, &gray_template),
77 MatchMethod::NormalizedSumSquaredDiff => match_normalized_ssd(&gray_img, &gray_template),
78 MatchMethod::CrossCorrelation => match_cross_correlation(&gray_img, &gray_template),
79 MatchMethod::NormalizedCrossCorrelation => match_ncc(&gray_img, &gray_template),
80 MatchMethod::CorrelationCoeff => match_correlation_coeff(&gray_img, &gray_template),
81 MatchMethod::NormalizedCorrelationCoeff => {
82 match_normalized_correlation_coeff(&gray_img, &gray_template)
83 }
84 }
85}
86
87#[allow(dead_code)]
89fn match_ssd(img: &GrayImage, template: &GrayImage) -> Result<Array2<f32>> {
90 let (img_width, img_height) = img.dimensions();
91 let (tmpl_width, tmpl_height) = template.dimensions();
92
93 if tmpl_width > img_width || tmpl_height > img_height {
94 return Err(VisionError::InvalidParameter(
95 "Template larger than image".to_string(),
96 ));
97 }
98
99 let result_width = (img_width - tmpl_width + 1) as usize;
100 let result_height = (img_height - tmpl_height + 1) as usize;
101 let mut result = Array2::zeros((result_height, result_width));
102
103 let img_array = image_to_array(img);
105 let tmpl_array = image_to_array(template);
106
107 let scores: Vec<_> = (0..result_height)
109 .into_par_iter()
110 .flat_map(|y| {
111 let img_slice = img_array.view();
112 let tmpl_slice = tmpl_array.view();
113 (0..result_width)
114 .into_par_iter()
115 .map(move |x| {
116 let mut ssd = 0.0f32;
117 for ty in 0..tmpl_height as usize {
118 for tx in 0..tmpl_width as usize {
119 let img_val = img_slice[[y + ty, x + tx]];
120 let tmpl_val = tmpl_slice[[ty, tx]];
121 let diff = img_val - tmpl_val;
122 ssd += diff * diff;
123 }
124 }
125 (y, x, ssd)
126 })
127 .collect::<Vec<_>>()
128 })
129 .collect();
130
131 for (y, x, score) in scores {
133 result[[y, x]] = score;
134 }
135
136 Ok(result)
137}
138
139#[allow(dead_code)]
141fn match_normalized_ssd(img: &GrayImage, template: &GrayImage) -> Result<Array2<f32>> {
142 let ssd_result = match_ssd(img, template)?;
143 let (height, width) = ssd_result.dim();
144
145 let tmpl_array = image_to_array(template);
147 let tmpl_norm: f32 = tmpl_array.iter().map(|&v| v * v).sum();
148
149 let mut result = Array2::zeros((height, width));
150
151 for y in 0..height {
152 for x in 0..width {
153 if tmpl_norm > 0.0 {
154 result[[y, x]] = ssd_result[[y, x]] / tmpl_norm.sqrt();
155 }
156 }
157 }
158
159 Ok(result)
160}
161
162#[allow(dead_code)]
164fn match_cross_correlation(img: &GrayImage, template: &GrayImage) -> Result<Array2<f32>> {
165 let (img_width, img_height) = img.dimensions();
166 let (tmpl_width, tmpl_height) = template.dimensions();
167
168 if tmpl_width > img_width || tmpl_height > img_height {
169 return Err(VisionError::InvalidParameter(
170 "Template larger than image".to_string(),
171 ));
172 }
173
174 let result_width = (img_width - tmpl_width + 1) as usize;
175 let result_height = (img_height - tmpl_height + 1) as usize;
176
177 let img_array = image_to_array(img);
178 let tmpl_array = image_to_array(template);
179
180 let scores: Vec<_> = (0..result_height)
182 .into_par_iter()
183 .flat_map(|y| {
184 let img_slice = img_array.view();
185 let tmpl_slice = tmpl_array.view();
186 (0..result_width)
187 .into_par_iter()
188 .map(move |x| {
189 let mut correlation = 0.0f32;
190 for ty in 0..tmpl_height as usize {
191 for tx in 0..tmpl_width as usize {
192 correlation += img_slice[[y + ty, x + tx]] * tmpl_slice[[ty, tx]];
193 }
194 }
195 (y, x, correlation)
196 })
197 .collect::<Vec<_>>()
198 })
199 .collect();
200
201 let mut result = Array2::zeros((result_height, result_width));
202 for (y, x, score) in scores {
203 result[[y, x]] = score;
204 }
205
206 Ok(result)
207}
208
209#[allow(dead_code)]
211fn match_ncc(img: &GrayImage, template: &GrayImage) -> Result<Array2<f32>> {
212 let (img_width, img_height) = img.dimensions();
213 let (tmpl_width, tmpl_height) = template.dimensions();
214
215 if tmpl_width > img_width || tmpl_height > img_height {
216 return Err(VisionError::InvalidParameter(
217 "Template larger than image".to_string(),
218 ));
219 }
220
221 let result_width = (img_width - tmpl_width + 1) as usize;
222 let result_height = (img_height - tmpl_height + 1) as usize;
223
224 let img_array = image_to_array(img);
225 let tmpl_array = image_to_array(template);
226
227 let tmpl_mean: f32 = tmpl_array.mean_or(0.0);
229 let tmpl_norm: f32 = tmpl_array
230 .iter()
231 .map(|&v| {
232 let diff = v - tmpl_mean;
233 diff * diff
234 })
235 .sum::<f32>()
236 .sqrt();
237
238 let scores: Vec<_> = (0..result_height)
240 .into_par_iter()
241 .flat_map(|y| {
242 let img_slice = img_array.view();
243 let tmpl_slice = tmpl_array.view();
244 (0..result_width)
245 .into_par_iter()
246 .map(move |x| {
247 let patch = img_slice
249 .slice(s![y..y + tmpl_height as usize, x..x + tmpl_width as usize]);
250 let patch_mean: f32 = patch.mean_or(0.0);
251
252 let mut correlation = 0.0f32;
253 let mut patch_norm = 0.0f32;
254
255 for ty in 0..tmpl_height as usize {
256 for tx in 0..tmpl_width as usize {
257 let img_val = img_slice[[y + ty, x + tx]] - patch_mean;
258 let tmpl_val = tmpl_slice[[ty, tx]] - tmpl_mean;
259 correlation += img_val * tmpl_val;
260 patch_norm += img_val * img_val;
261 }
262 }
263
264 patch_norm = patch_norm.sqrt();
265
266 let ncc = if patch_norm > 0.0 && tmpl_norm > 0.0 {
267 correlation / (patch_norm * tmpl_norm)
268 } else {
269 0.0
270 };
271
272 (y, x, ncc)
273 })
274 .collect::<Vec<_>>()
275 })
276 .collect();
277
278 let mut result = Array2::zeros((result_height, result_width));
279 for (y, x, score) in scores {
280 result[[y, x]] = score;
281 }
282
283 Ok(result)
284}
285
286#[allow(dead_code)]
288fn match_correlation_coeff(img: &GrayImage, template: &GrayImage) -> Result<Array2<f32>> {
289 match_ncc(img, template)
290}
291
292#[allow(dead_code)]
294fn match_normalized_correlation_coeff(
295 img: &GrayImage,
296 template: &GrayImage,
297) -> Result<Array2<f32>> {
298 let ncc_result = match_ncc(img, template)?;
299
300 let mut result = ncc_result.clone();
303 result.mapv_inplace(|v| (v + 1.0) / 2.0);
304
305 Ok(result)
306}
307
308#[allow(dead_code)]
319pub fn find_best_match(scores: &Array2<f32>, method: MatchMethod) -> MatchResult {
320 let (height, width) = scores.dim();
321 let mut best_score = match method {
322 MatchMethod::SumSquaredDiff | MatchMethod::NormalizedSumSquaredDiff => f32::INFINITY,
323 _ => f32::NEG_INFINITY,
324 };
325 let mut best_x = 0;
326 let mut best_y = 0;
327
328 for y in 0..height {
329 for x in 0..width {
330 let score = scores[[y, x]];
331 let is_better = match method {
332 MatchMethod::SumSquaredDiff | MatchMethod::NormalizedSumSquaredDiff => {
333 score < best_score
334 }
335 _ => score > best_score,
336 };
337
338 if is_better {
339 best_score = score;
340 best_x = x as u32;
341 best_y = y as u32;
342 }
343 }
344 }
345
346 MatchResult {
347 x: best_x,
348 y: best_y,
349 score: best_score,
350 }
351}
352
353#[allow(dead_code)]
366pub fn find_matches(
367 scores: &Array2<f32>,
368 method: MatchMethod,
369 threshold: f32,
370 min_distance: u32,
371) -> Vec<MatchResult> {
372 let (height, width) = scores.dim();
373 let mut matches = Vec::new();
374
375 let mut scores_copy = scores.clone();
377
378 loop {
379 let best = find_best_match(&scores_copy, method);
380
381 let meets_threshold = match method {
383 MatchMethod::SumSquaredDiff | MatchMethod::NormalizedSumSquaredDiff => {
384 best.score <= threshold
385 }
386 _ => best.score >= threshold,
387 };
388
389 if !meets_threshold {
390 break;
391 }
392
393 matches.push(best.clone());
394
395 let y_start = best.y.saturating_sub(min_distance) as usize;
397 let y_end = (best.y + min_distance + 1).min(height as u32) as usize;
398 let x_start = best.x.saturating_sub(min_distance) as usize;
399 let x_end = (best.x + min_distance + 1).min(width as u32) as usize;
400
401 for y in y_start..y_end {
402 for x in x_start..x_end {
403 scores_copy[[y, x]] = match method {
404 MatchMethod::SumSquaredDiff | MatchMethod::NormalizedSumSquaredDiff => {
405 f32::INFINITY
406 }
407 _ => f32::NEG_INFINITY,
408 };
409 }
410 }
411 }
412
413 matches
414}
415
416#[allow(dead_code)]
418pub fn draw_match(
419 img: &DynamicImage,
420 template: &DynamicImage,
421 match_result: &MatchResult,
422) -> RgbImage {
423 let mut result = img.to_rgb8();
424 let (tmpl_width, tmpl_height) = template.dimensions();
425
426 let color = Rgb([0, 255, 0]);
428 draw_rectangle(
429 &mut result,
430 match_result.x,
431 match_result.y,
432 tmpl_width,
433 tmpl_height,
434 color,
435 );
436
437 result
438}
439
440#[allow(dead_code)]
442fn draw_rectangle(img: &mut RgbImage, x: u32, y: u32, width: u32, height: u32, color: Rgb<u8>) {
443 let (img_width, img_height) = img.dimensions();
444
445 for dx in 0..width {
447 let px = x + dx;
448 if px < img_width {
449 if y < img_height {
450 img.put_pixel(px, y, color);
451 }
452 if y + height - 1 < img_height {
453 img.put_pixel(px, y + height - 1, color);
454 }
455 }
456 }
457
458 for dy in 0..height {
460 let py = y + dy;
461 if py < img_height {
462 if x < img_width {
463 img.put_pixel(x, py, color);
464 }
465 if x + width - 1 < img_width {
466 img.put_pixel(x + width - 1, py, color);
467 }
468 }
469 }
470}
471
472#[allow(dead_code)]
474fn image_to_array(img: &GrayImage) -> Array2<f32> {
475 let (width, height) = img.dimensions();
476 let mut array = Array2::zeros((height as usize, width as usize));
477
478 for y in 0..height {
479 for x in 0..width {
480 array[[y as usize, x as usize]] = img.get_pixel(x, y)[0] as f32 / 255.0;
481 }
482 }
483
484 array
485}
486
487#[cfg(test)]
488mod tests {
489 use super::*;
490 use scirs2_core::ndarray::Array2;
491
492 #[test]
493 fn test_template_match_basic() {
494 let img = DynamicImage::new_luma8(50, 50);
495 let template = DynamicImage::new_luma8(10, 10);
496
497 let result = template_match(&img, &template, MatchMethod::CrossCorrelation);
498 assert!(result.is_ok());
499
500 let scores = result.expect("Operation failed");
501 assert_eq!(scores.dim(), (41, 41));
502 }
503
504 #[test]
505 fn test_template_too_large() {
506 let img = DynamicImage::new_luma8(10, 10);
507 let template = DynamicImage::new_luma8(20, 20);
508
509 let result = template_match(&img, &template, MatchMethod::CrossCorrelation);
510 assert!(result.is_err());
511 }
512
513 #[test]
514 fn test_find_best_match() {
515 let mut scores = Array2::zeros((10, 10));
516 scores[[5, 5]] = 0.9;
517
518 let best = find_best_match(&scores, MatchMethod::CrossCorrelation);
519 assert_eq!(best.x, 5);
520 assert_eq!(best.y, 5);
521 assert_eq!(best.score, 0.9);
522 }
523
524 #[test]
525 fn test_find_multiple_matches() {
526 let mut scores = Array2::zeros((20, 20));
527 scores[[5, 5]] = 0.9;
528 scores[[15, 15]] = 0.8;
529 scores[[5, 15]] = 0.7;
530
531 let matches = find_matches(&scores, MatchMethod::CrossCorrelation, 0.6, 3);
532 assert_eq!(matches.len(), 3);
533 }
534}