1use 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#[derive(Copy, Clone, Debug, PartialEq, Eq)]
10pub enum MatchTemplateMethod {
11 SumOfSquaredErrors,
16 SumOfSquaredErrorsNormalized,
18 CrossCorrelation,
22 CrossCorrelationNormalized,
24}
25
26pub 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
115fn 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#[derive(Copy, Clone, Debug, PartialEq, Eq)]
135pub struct Extremes<T> {
136 pub max_value: T,
138 pub min_value: T,
140 pub max_value_location: (u32, u32),
142 pub min_value_location: (u32, u32),
144}
145
146pub 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}