Skip to main content

scirs2_vision/feature/
template_matching.rs

1//! Template matching for object detection
2//!
3//! This module provides various template matching methods to find regions
4//! in an image that match a template image.
5
6use 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/// Template matching method
14#[derive(Debug, Clone, Copy)]
15pub enum MatchMethod {
16    /// Sum of Squared Differences (SSD)
17    SumSquaredDiff,
18    /// Normalized Sum of Squared Differences
19    NormalizedSumSquaredDiff,
20    /// Cross-Correlation
21    CrossCorrelation,
22    /// Normalized Cross-Correlation
23    NormalizedCrossCorrelation,
24    /// Correlation Coefficient
25    CorrelationCoeff,
26    /// Normalized Correlation Coefficient
27    NormalizedCorrelationCoeff,
28}
29
30/// Match result containing position and score
31#[derive(Debug, Clone)]
32pub struct MatchResult {
33    /// X coordinate of the match
34    pub x: u32,
35    /// Y coordinate of the match
36    pub y: u32,
37    /// Match score (higher is better)
38    pub score: f32,
39}
40
41/// Perform template matching
42///
43/// # Arguments
44///
45/// * `img` - Source image to search in
46/// * `template` - Template image to find
47/// * `method` - Matching method to use
48///
49/// # Returns
50///
51/// * Result containing array of match scores
52///
53/// # Example
54///
55/// ```rust
56/// use scirs2_vision::feature::{template_match, MatchMethod};
57/// use image::DynamicImage;
58///
59/// # fn main() -> scirs2_vision::error::Result<()> {
60/// let img = image::open("examples/input/input.jpg").expect("Operation failed");
61/// let template = img.crop_imm(50, 50, 30, 30);
62/// let scores = template_match(&img, &template, MatchMethod::NormalizedCrossCorrelation)?;
63/// # Ok(())
64/// # }
65/// ```
66#[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/// Sum of Squared Differences matching
88#[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    // Convert to arrays for faster access
104    let img_array = image_to_array(img);
105    let tmpl_array = image_to_array(template);
106
107    // Parallel computation
108    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    // Fill result array
132    for (y, x, score) in scores {
133        result[[y, x]] = score;
134    }
135
136    Ok(result)
137}
138
139/// Normalized Sum of Squared Differences
140#[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    // Compute template norm
146    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/// Cross-correlation matching
163#[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    // Parallel computation
181    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/// Normalized Cross-Correlation
210#[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    // Compute template mean and norm
228    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    // Parallel computation
239    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                    // Extract patch
248                    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/// Correlation coefficient matching
287#[allow(dead_code)]
288fn match_correlation_coeff(img: &GrayImage, template: &GrayImage) -> Result<Array2<f32>> {
289    match_ncc(img, template)
290}
291
292/// Normalized correlation coefficient
293#[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    // NCC already produces normalized values in [-1, 1]
301    // Transform to [0, 1] for consistency
302    let mut result = ncc_result.clone();
303    result.mapv_inplace(|v| (v + 1.0) / 2.0);
304
305    Ok(result)
306}
307
308/// Find best match location
309///
310/// # Arguments
311///
312/// * `scores` - Match scores array
313/// * `method` - Matching method (to determine if lower or higher is better)
314///
315/// # Returns
316///
317/// * Best match result
318#[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/// Find multiple matches above/below threshold
354///
355/// # Arguments
356///
357/// * `scores` - Match scores array
358/// * `method` - Matching method
359/// * `threshold` - Score threshold
360/// * `min_distance` - Minimum distance between matches
361///
362/// # Returns
363///
364/// * Vector of match results
365#[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    // Create a copy for non-maximum suppression
376    let mut scores_copy = scores.clone();
377
378    loop {
379        let best = find_best_match(&scores_copy, method);
380
381        // Check if match meets threshold
382        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        // Suppress nearby scores
396        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/// Draw match result on image
417#[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    // Draw rectangle around match
427    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/// Draw rectangle on image
441#[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    // Top and bottom edges
446    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    // Left and right edges
459    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/// Convert grayscale image to normalized array
473#[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}