imageproc/
template_matching.rs

1//! Functions for performing template matching.
2use crate::definitions::Image;
3use crate::integral_image::{integral_squared_image, sum_image_pixels};
4use crate::rect::Rect;
5use image::Primitive;
6use image::{GrayImage, Luma};
7
8/// Method used to compute the matching score between a template and an image region.
9#[derive(Copy, Clone, Debug, PartialEq, Eq)]
10pub enum MatchTemplateMethod {
11    /// Sum of the squares of the difference between image and template pixel
12    /// intensities.
13    ///
14    /// Smaller values are better.
15    SumOfSquaredErrors,
16    /// Divides the sum computed using `SumOfSquaredErrors` by a normalization term.
17    SumOfSquaredErrorsNormalized,
18    /// Cross Correlation
19    ///
20    /// Higher values are better.
21    CrossCorrelation,
22    /// Divides the sum computed using `CrossCorrelation` by a normalization term.
23    CrossCorrelationNormalized,
24}
25
26/// Slides a `template` over an `image` and scores the match at each point using
27/// the requested `method`.
28///
29/// The returned image has dimensions `image.width() - template.width() + 1` by
30/// `image.height() - template.height() + 1`.
31///
32/// # Panics
33///
34/// If either dimension of `template` is not strictly less than the corresponding dimension
35/// of `image`.
36pub fn match_template(
37    image: &GrayImage,
38    template: &GrayImage,
39    method: MatchTemplateMethod,
40) -> Image<Luma<f32>> {
41    use image::GenericImageView;
42
43    let (image_width, image_height) = image.dimensions();
44    let (template_width, template_height) = template.dimensions();
45
46    assert!(
47        image_width >= template_width,
48        "image width must be greater than or equal to template width"
49    );
50    assert!(
51        image_height >= template_height,
52        "image height must be greater than or equal to template height"
53    );
54
55    let should_normalize = matches! { method,
56    MatchTemplateMethod::SumOfSquaredErrorsNormalized
57    | MatchTemplateMethod::CrossCorrelationNormalized };
58    let image_squared_integral = if should_normalize {
59        Some(integral_squared_image(image))
60    } else {
61        None
62    };
63    let template_squared_sum = if should_normalize {
64        Some(sum_squares(template))
65    } else {
66        None
67    };
68
69    let mut result = Image::new(
70        image_width - template_width + 1,
71        image_height - template_height + 1,
72    );
73
74    for y in 0..result.height() {
75        for x in 0..result.width() {
76            let mut score = 0f32;
77
78            for dy in 0..template_height {
79                for dx in 0..template_width {
80                    let image_value = unsafe { image.unsafe_get_pixel(x + dx, y + dy)[0] as f32 };
81                    let template_value = unsafe { template.unsafe_get_pixel(dx, dy)[0] as f32 };
82
83                    use MatchTemplateMethod::*;
84
85                    score += match method {
86                        SumOfSquaredErrors | SumOfSquaredErrorsNormalized => {
87                            (image_value - template_value).powf(2.0)
88                        }
89                        CrossCorrelation | CrossCorrelationNormalized => {
90                            image_value * template_value
91                        }
92                    };
93                }
94            }
95
96            if let (&Some(ref i), &Some(t)) = (&image_squared_integral, &template_squared_sum) {
97                let region = Rect::at(x as i32, y as i32).of_size(template_width, template_height);
98                let norm = normalization_term(i, t, region);
99                if norm > 0.0 {
100                    score /= norm;
101                }
102            }
103
104            result.put_pixel(x, y, Luma([score]));
105        }
106    }
107
108    result
109}
110
111fn sum_squares(template: &GrayImage) -> f32 {
112    template.iter().map(|p| *p as f32 * *p as f32).sum()
113}
114
115/// Returns the square root of the product of the sum of squares of
116/// pixel intensities in template and the provided region of image.
117fn normalization_term(
118    image_squared_integral: &Image<Luma<u64>>,
119    template_squared_sum: f32,
120    region: Rect,
121) -> f32 {
122    let image_sum = sum_image_pixels(
123        image_squared_integral,
124        region.left() as u32,
125        region.top() as u32,
126        region.right() as u32,
127        region.bottom() as u32,
128    )[0] as f32;
129    (image_sum * template_squared_sum).sqrt()
130}
131
132/// The largest and smallest values in an image,
133/// together with their locations.
134#[derive(Copy, Clone, Debug, PartialEq, Eq)]
135pub struct Extremes<T> {
136    /// The largest value in an image.
137    pub max_value: T,
138    /// The smallest value in an image.
139    pub min_value: T,
140    /// The coordinates of the largest value in an image.
141    pub max_value_location: (u32, u32),
142    /// The coordinates of the smallest value in an image.
143    pub min_value_location: (u32, u32),
144}
145
146/// Finds the largest and smallest values in an image and their locations.
147/// If there are multiple such values then the lexicographically smallest is returned.
148pub fn find_extremes<T>(image: &Image<Luma<T>>) -> Extremes<T>
149where
150    T: Primitive,
151{
152    assert!(
153        image.width() > 0 && image.height() > 0,
154        "image must be non-empty"
155    );
156
157    let mut min_value = image.get_pixel(0, 0)[0];
158    let mut max_value = image.get_pixel(0, 0)[0];
159
160    let mut min_value_location = (0, 0);
161    let mut max_value_location = (0, 0);
162
163    for (x, y, p) in image.enumerate_pixels() {
164        if p[0] < min_value {
165            min_value = p[0];
166            min_value_location = (x, y);
167        }
168        if p[0] > max_value {
169            max_value = p[0];
170            max_value_location = (x, y);
171        }
172    }
173
174    Extremes {
175        max_value,
176        min_value,
177        max_value_location,
178        min_value_location,
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185    use crate::utils::gray_bench_image;
186    use image::GrayImage;
187    use test::{black_box, Bencher};
188
189    #[test]
190    #[should_panic]
191    fn match_template_panics_if_image_width_does_is_less_than_template_width() {
192        let _ = match_template(
193            &GrayImage::new(5, 5),
194            &GrayImage::new(6, 5),
195            MatchTemplateMethod::SumOfSquaredErrors,
196        );
197    }
198
199    #[test]
200    #[should_panic]
201    fn match_template_panics_if_image_height_is_less_than_template_height() {
202        let _ = match_template(
203            &GrayImage::new(5, 5),
204            &GrayImage::new(5, 6),
205            MatchTemplateMethod::SumOfSquaredErrors,
206        );
207    }
208
209    #[test]
210    fn match_template_handles_template_of_same_size_as_image() {
211        assert_pixels_eq!(
212            match_template(
213                &GrayImage::new(5, 5),
214                &GrayImage::new(5, 5),
215                MatchTemplateMethod::SumOfSquaredErrors
216            ),
217            gray_image!(type: f32, 0.0)
218        );
219    }
220
221    #[test]
222    fn match_template_normalization_handles_zero_norm() {
223        assert_pixels_eq!(
224            match_template(
225                &GrayImage::new(1, 1),
226                &GrayImage::new(1, 1),
227                MatchTemplateMethod::SumOfSquaredErrorsNormalized
228            ),
229            gray_image!(type: f32, 0.0)
230        );
231    }
232
233    #[test]
234    fn match_template_sum_of_squared_errors() {
235        let image = gray_image!(
236            1, 4, 2;
237            2, 1, 3;
238            3, 3, 4
239        );
240        let template = gray_image!(
241            1, 2;
242            3, 4
243        );
244
245        let actual = match_template(&image, &template, MatchTemplateMethod::SumOfSquaredErrors);
246        let expected = gray_image!(type: f32,
247            14.0, 14.0;
248            3.0, 1.0
249        );
250
251        assert_pixels_eq!(actual, expected);
252    }
253
254    #[test]
255    fn match_template_sum_of_squared_errors_normalized() {
256        let image = gray_image!(
257            1, 4, 2;
258            2, 1, 3;
259            3, 3, 4
260        );
261        let template = gray_image!(
262            1, 2;
263            3, 4
264        );
265
266        let actual = match_template(
267            &image,
268            &template,
269            MatchTemplateMethod::SumOfSquaredErrorsNormalized,
270        );
271        let tss = 30f32;
272        let expected = gray_image!(type: f32,
273            14.0 / (22.0 * tss).sqrt(), 14.0 / (30.0 * tss).sqrt();
274            3.0 / (23.0 * tss).sqrt(), 1.0 / (35.0 * tss).sqrt()
275        );
276
277        assert_pixels_eq!(actual, expected);
278    }
279
280    #[test]
281    fn match_template_cross_correlation() {
282        let image = gray_image!(
283            1, 4, 2;
284            2, 1, 3;
285            3, 3, 4
286        );
287        let template = gray_image!(
288            1, 2;
289            3, 4
290        );
291
292        let actual = match_template(&image, &template, MatchTemplateMethod::CrossCorrelation);
293        let expected = gray_image!(type: f32,
294            19.0, 23.0;
295            25.0, 32.0
296        );
297
298        assert_pixels_eq!(actual, expected);
299    }
300
301    #[test]
302    fn match_template_cross_correlation_normalized() {
303        let image = gray_image!(
304            1, 4, 2;
305            2, 1, 3;
306            3, 3, 4
307        );
308        let template = gray_image!(
309            1, 2;
310            3, 4
311        );
312
313        let actual = match_template(
314            &image,
315            &template,
316            MatchTemplateMethod::CrossCorrelationNormalized,
317        );
318        let tss = 30f32;
319        let expected = gray_image!(type: f32,
320            19.0 / (22.0 * tss).sqrt(), 23.0 / (30.0 * tss).sqrt();
321            25.0 / (23.0 * tss).sqrt(), 32.0 / (35.0 * tss).sqrt()
322        );
323
324        assert_pixels_eq!(actual, expected);
325    }
326
327    macro_rules! bench_match_template {
328        ($name:ident, image_size: $s:expr, template_size: $t:expr, method: $m:expr) => {
329            #[bench]
330            fn $name(b: &mut Bencher) {
331                let image = gray_bench_image($s, $s);
332                let template = gray_bench_image($t, $t);
333                b.iter(|| {
334                    let result =
335                        match_template(&image, &template, MatchTemplateMethod::SumOfSquaredErrors);
336                    black_box(result);
337                })
338            }
339        };
340    }
341
342    bench_match_template!(
343        bench_match_template_s100_t1_sse,
344        image_size: 100,
345        template_size: 1,
346        method: MatchTemplateMethod::SumOfSquaredErrors);
347
348    bench_match_template!(
349        bench_match_template_s100_t4_sse,
350        image_size: 100,
351        template_size: 4,
352        method: MatchTemplateMethod::SumOfSquaredErrors);
353
354    bench_match_template!(
355        bench_match_template_s100_t16_sse,
356        image_size: 100,
357        template_size: 16,
358        method: MatchTemplateMethod::SumOfSquaredErrors);
359
360    bench_match_template!(
361        bench_match_template_s100_t1_sse_norm,
362        image_size: 100,
363        template_size: 1,
364        method: MatchTemplateMethod::SumOfSquaredErrorsNormalized);
365
366    bench_match_template!(
367        bench_match_template_s100_t4_sse_norm,
368        image_size: 100,
369        template_size: 4,
370        method: MatchTemplateMethod::SumOfSquaredErrorsNormalized);
371
372    bench_match_template!(
373        bench_match_template_s100_t16_sse_norm,
374        image_size: 100,
375        template_size: 16,
376        method: MatchTemplateMethod::SumOfSquaredErrorsNormalized);
377
378    #[test]
379    fn test_find_extremes() {
380        let image = gray_image!(
381            10,  7,  8,  1;
382             9, 15,  4,  2
383        );
384
385        let expected = Extremes {
386            max_value: 15,
387            min_value: 1,
388            max_value_location: (1, 1),
389            min_value_location: (3, 0),
390        };
391
392        assert_eq!(find_extremes(&image), expected);
393    }
394}