1pub mod bilateral;
4mod median;
5pub use self::bilateral::bilateral_filter;
6pub use self::median::median_filter;
7
8mod sharpen;
9pub use self::sharpen::*;
10
11mod box_filter;
12pub use self::box_filter::box_filter;
13
14use image::{GenericImageView, GrayImage, Luma, Pixel, Primitive};
15
16use crate::definitions::{Clamp, Image};
17use crate::kernel::{self, Kernel};
18use crate::map::{ChannelMap, WithChannel};
19use num::Num;
20
21use std::cmp::{max, min};
22use std::f32;
23
24pub fn filter<P, K, F, Q>(image: &Image<P>, kernel: Kernel<K>, mut f: F) -> Image<Q>
30where
31 P: Pixel,
32 <P as Pixel>::Subpixel: Into<K>,
33 Q: Pixel,
34 F: FnMut(K) -> Q::Subpixel,
35 K: Copy + Num,
36{
37 assert_eq!(P::CHANNEL_COUNT, Q::CHANNEL_COUNT);
38 let num_channels = P::CHANNEL_COUNT as usize;
39 let zero = K::zero();
40
41 let (width, height) = image.dimensions();
42 let mut out = Image::<Q>::new(width, height);
43 let mut acc = vec![zero; num_channels];
44 let (k_width, k_height) = (kernel.width as i64, kernel.height as i64);
45 let (width, height) = (width as i64, height as i64);
46
47 for y in 0..height {
48 for x in 0..width {
49 for k_y in 0..k_height {
50 let y_p = min(height - 1, max(0, y + k_y - k_height / 2)) as u32;
51 for k_x in 0..k_width {
52 let x_p = min(width - 1, max(0, x + k_x - k_width / 2)) as u32;
53
54 debug_assert!(image.in_bounds(x_p, y_p));
55 let pixel = unsafe { image.unsafe_get_pixel(x_p, y_p) };
56 accumulate(&mut acc, &pixel, unsafe {
57 kernel.get_unchecked(k_x as u32, k_y as u32)
58 });
59 }
60 }
61 let out_channels = out.get_pixel_mut(x as u32, y as u32).channels_mut();
62 for (a, c) in acc.iter_mut().zip(out_channels) {
63 *c = f(*a);
64 *a = zero;
65 }
66 }
67 }
68 out
69}
70
71#[cfg(feature = "rayon")]
72#[doc = generate_parallel_doc_comment!("filter")]
73pub fn filter_parallel<P, K, F, Q>(image: &Image<P>, kernel: Kernel<K>, f: F) -> Image<Q>
74where
75 P: Pixel + Sync,
76 P::Subpixel: Into<K> + Sync,
77 Q: Pixel + Send,
78 F: Sync + Fn(K) -> Q::Subpixel,
79 K: Copy + Num + Sync,
80{
81 use num::Zero;
82 use rayon::prelude::*;
83
84 assert_eq!(P::CHANNEL_COUNT, Q::CHANNEL_COUNT);
85 let num_channels = P::CHANNEL_COUNT as usize;
86
87 let (k_width, k_height) = (kernel.width as i64, kernel.height as i64);
88 let (width, height) = (image.width() as i64, image.height() as i64);
89
90 let out_rows: Vec<Vec<_>> = (0..height)
91 .into_par_iter()
92 .map(|y| {
93 let mut out_row = Vec::with_capacity(image.width() as usize);
94 let mut out_pixel = vec![Q::Subpixel::zero(); num_channels];
95 let mut acc = vec![K::zero(); num_channels];
96
97 for x in 0..width {
98 for k_y in 0..k_height {
99 let y_p = min(height - 1, max(0, y + k_y - k_height / 2)) as u32;
100 for k_x in 0..k_width {
101 let x_p = min(width - 1, max(0, x + k_x - k_width / 2)) as u32;
102
103 debug_assert!(image.in_bounds(x_p, y_p));
104 let pixel = unsafe { image.unsafe_get_pixel(x_p, y_p) };
105 accumulate(&mut acc, &pixel, unsafe {
106 kernel.get_unchecked(k_x as u32, k_y as u32)
107 });
108 }
109 }
110 for (a, c) in acc.iter_mut().zip(out_pixel.iter_mut()) {
111 *c = f(*a);
112 *a = K::zero();
113 }
114 out_row.push(*Q::from_slice(&out_pixel));
115 }
116 out_row
117 })
118 .collect();
119
120 Image::from_fn(image.width(), image.height(), |x, y| {
121 out_rows[y as usize][x as usize]
122 })
123}
124
125#[inline]
126fn gaussian_pdf(x: f32, sigma: f32) -> f32 {
127 (sigma * (2.0 * f32::consts::PI).sqrt()).recip() * (-x.powi(2) / (2.0 * sigma.powi(2))).exp()
129}
130
131fn gaussian_kernel_f32(sigma: f32) -> Vec<f32> {
134 let kernel_radius = (2.0 * sigma).ceil() as usize;
135 let mut kernel_data = vec![0.0; 2 * kernel_radius + 1];
136 for i in 0..kernel_radius + 1 {
137 let value = gaussian_pdf(i as f32, sigma);
138 kernel_data[kernel_radius + i] = value;
139 kernel_data[kernel_radius - i] = value;
140 }
141 let sum: f32 = kernel_data.iter().sum();
142 kernel_data.iter_mut().for_each(|x| *x /= sum);
143
144 kernel_data
145}
146
147#[must_use = "the function does not modify the original image"]
156pub fn gaussian_blur_f32<P>(image: &Image<P>, sigma: f32) -> Image<P>
157where
158 P: Pixel,
159 <P as Pixel>::Subpixel: Into<f32> + Clamp<f32>,
160{
161 assert!(sigma > 0.0, "sigma must be > 0.0");
162 let kernel = gaussian_kernel_f32(sigma);
163 separable_filter_equal(image, &kernel)
164}
165
166#[must_use = "the function does not modify the original image"]
169pub fn separable_filter<P, K>(image: &Image<P>, h_kernel: &[K], v_kernel: &[K]) -> Image<P>
170where
171 P: Pixel,
172 <P as Pixel>::Subpixel: Into<K> + Clamp<K>,
173 K: Num + Copy,
174{
175 assert_eq!(
176 h_kernel.len(),
177 v_kernel.len(),
178 "the two 1D kernels must be the same length"
179 );
180
181 let h = horizontal_filter(image, h_kernel);
182 vertical_filter(&h, v_kernel)
183}
184
185#[must_use = "the function does not modify the original image"]
188pub fn separable_filter_equal<P, K>(image: &Image<P>, kernel: &[K]) -> Image<P>
189where
190 P: Pixel,
191 <P as Pixel>::Subpixel: Into<K> + Clamp<K>,
192 K: Num + Copy,
193{
194 separable_filter(image, kernel, kernel)
195}
196
197pub fn filter_clamped<P, K, S>(image: &Image<P>, kernel: Kernel<K>) -> Image<ChannelMap<P, S>>
203where
204 P: WithChannel<S>,
205 P::Subpixel: Into<K>,
206 K: Num + Copy + From<<P as image::Pixel>::Subpixel>,
207 S: Clamp<K> + Primitive,
208{
209 filter(image, kernel, S::clamp)
210}
211
212#[cfg(feature = "rayon")]
213#[doc = generate_parallel_doc_comment!("filter_clamped")]
214pub fn filter_clamped_parallel<P, K, S>(
215 image: &Image<P>,
216 kernel: Kernel<K>,
217) -> Image<ChannelMap<P, S>>
218where
219 P: Sync + WithChannel<S>,
220 P::Subpixel: Into<K> + Sync,
221 <P as WithChannel<S>>::Pixel: Send,
222 K: Num + Copy + From<<P as image::Pixel>::Subpixel> + Sync,
223 S: Clamp<K> + Primitive + Send,
224{
225 filter_parallel(image, kernel, S::clamp)
226}
227
228#[must_use = "the function does not modify the original image"]
232pub fn horizontal_filter<P, K>(image: &Image<P>, kernel: &[K]) -> Image<P>
233where
234 P: Pixel,
235 <P as Pixel>::Subpixel: Into<K> + Clamp<K>,
236 K: Num + Copy,
237{
238 let (width, height) = image.dimensions();
242 let mut out = Image::<P>::new(width, height);
243 let zero = K::zero();
244 let mut acc = vec![zero; P::CHANNEL_COUNT as usize];
245 let k_width = kernel.len() as i32;
246 let half_k = k_width / 2;
247
248 if k_width >= width as i32 {
251 for y in 0..height {
252 for x in 0..width {
253 for (i, k) in kernel.iter().enumerate() {
254 let x_unchecked = (x as i32) + i as i32 - half_k;
255 let x_p = max(0, min(x_unchecked, width as i32 - 1)) as u32;
256 debug_assert!(image.in_bounds(x_p, y));
257 let p = unsafe { image.unsafe_get_pixel(x_p, y) };
258 accumulate(&mut acc, &p, *k);
259 }
260
261 let out_channels = out.get_pixel_mut(x, y).channels_mut();
262 clamp_and_reset::<P, K>(&mut acc, out_channels, zero);
263 }
264 }
265
266 return out;
267 }
268
269 for y in 0..height {
270 for x in 0..half_k {
272 for (i, k) in kernel.iter().enumerate() {
273 let x_unchecked = x + i as i32 - half_k;
274 let x_p = max(0, x_unchecked) as u32;
275 debug_assert!(image.in_bounds(x_p, y));
276 let p = unsafe { image.unsafe_get_pixel(x_p, y) };
277 accumulate(&mut acc, &p, *k);
278 }
279
280 let out_channels = out.get_pixel_mut(x as u32, y).channels_mut();
281 clamp_and_reset::<P, K>(&mut acc, out_channels, zero);
282 }
283
284 for x in half_k..(width as i32 - half_k) {
286 for (i, k) in kernel.iter().enumerate() {
287 let x_unchecked = x + i as i32 - half_k;
288 let x_p = x_unchecked as u32;
289 debug_assert!(image.in_bounds(x_p, y));
290 let p = unsafe { image.unsafe_get_pixel(x_p, y) };
291 accumulate(&mut acc, &p, *k);
292 }
293
294 let out_channels = out.get_pixel_mut(x as u32, y).channels_mut();
295 clamp_and_reset::<P, K>(&mut acc, out_channels, zero);
296 }
297
298 for x in (width as i32 - half_k)..(width as i32) {
300 for (i, k) in kernel.iter().enumerate() {
301 let x_unchecked = x + i as i32 - half_k;
302 let x_p = min(x_unchecked, width as i32 - 1) as u32;
303 debug_assert!(image.in_bounds(x_p, y));
304 let p = unsafe { image.unsafe_get_pixel(x_p, y) };
305 accumulate(&mut acc, &p, *k);
306 }
307
308 let out_channels = out.get_pixel_mut(x as u32, y).channels_mut();
309 clamp_and_reset::<P, K>(&mut acc, out_channels, zero);
310 }
311 }
312
313 out
314}
315
316#[must_use = "the function does not modify the original image"]
319pub fn vertical_filter<P, K>(image: &Image<P>, kernel: &[K]) -> Image<P>
320where
321 P: Pixel,
322 <P as Pixel>::Subpixel: Into<K> + Clamp<K>,
323 K: Num + Copy,
324{
325 let (width, height) = image.dimensions();
329 let mut out = Image::<P>::new(width, height);
330 let zero = K::zero();
331 let mut acc = vec![zero; P::CHANNEL_COUNT as usize];
332 let k_height = kernel.len() as i32;
333 let half_k = k_height / 2;
334
335 if k_height >= height as i32 {
338 for y in 0..height {
339 for x in 0..width {
340 for (i, k) in kernel.iter().enumerate() {
341 let y_unchecked = (y as i32) + i as i32 - half_k;
342 let y_p = max(0, min(y_unchecked, height as i32 - 1)) as u32;
343 debug_assert!(image.in_bounds(x, y_p));
344 let p = unsafe { image.unsafe_get_pixel(x, y_p) };
345 accumulate(&mut acc, &p, *k);
346 }
347
348 let out_channels = out.get_pixel_mut(x, y).channels_mut();
349 clamp_and_reset::<P, K>(&mut acc, out_channels, zero);
350 }
351 }
352
353 return out;
354 }
355
356 for y in 0..half_k {
358 for x in 0..width {
359 for (i, k) in kernel.iter().enumerate() {
360 let y_unchecked = y + i as i32 - half_k;
361 let y_p = max(0, y_unchecked) as u32;
362 debug_assert!(image.in_bounds(x, y_p));
363 let p = unsafe { image.unsafe_get_pixel(x, y_p) };
364 accumulate(&mut acc, &p, *k);
365 }
366
367 let out_channels = out.get_pixel_mut(x, y as u32).channels_mut();
368 clamp_and_reset::<P, K>(&mut acc, out_channels, zero);
369 }
370 }
371
372 for y in half_k..(height as i32 - half_k) {
374 for x in 0..width {
375 for (i, k) in kernel.iter().enumerate() {
376 let y_unchecked = y + i as i32 - half_k;
377 let y_p = y_unchecked as u32;
378 debug_assert!(image.in_bounds(x, y_p));
379 let p = unsafe { image.unsafe_get_pixel(x, y_p) };
380 accumulate(&mut acc, &p, *k);
381 }
382
383 let out_channels = out.get_pixel_mut(x, y as u32).channels_mut();
384 clamp_and_reset::<P, K>(&mut acc, out_channels, zero);
385 }
386 }
387
388 for y in (height as i32 - half_k)..(height as i32) {
390 for x in 0..width {
391 for (i, k) in kernel.iter().enumerate() {
392 let y_unchecked = y + i as i32 - half_k;
393 let y_p = min(y_unchecked, height as i32 - 1) as u32;
394 debug_assert!(image.in_bounds(x, y_p));
395 let p = unsafe { image.unsafe_get_pixel(x, y_p) };
396 accumulate(&mut acc, &p, *k);
397 }
398
399 let out_channels = out.get_pixel_mut(x, y as u32).channels_mut();
400 clamp_and_reset::<P, K>(&mut acc, out_channels, zero);
401 }
402 }
403
404 out
405}
406
407fn accumulate<P, K>(acc: &mut [K], pixel: &P, weight: K)
408where
409 P: Pixel,
410 P::Subpixel: Into<K>,
411 K: Num + Copy,
412{
413 acc.iter_mut().zip(pixel.channels()).for_each(|(a, &c)| {
414 *a = *a + c.into() * weight;
415 });
416}
417
418fn clamp_and_reset<P, K>(acc: &mut [K], out_channels: &mut [P::Subpixel], zero: K)
419where
420 P: Pixel,
421 <P as Pixel>::Subpixel: Into<K> + Clamp<K>,
422 K: Num + Copy,
423{
424 for (accumulator, pixel_channel) in acc.iter_mut().zip(out_channels.iter_mut()) {
425 let clamped_pixel = P::Subpixel::clamp(*accumulator);
426 *pixel_channel = clamped_pixel;
427 *accumulator = zero;
428 }
429}
430
431#[must_use = "the function does not modify the original image"]
440pub fn laplacian_filter(image: &GrayImage) -> Image<Luma<i16>> {
441 filter_clamped(image, kernel::LAPLACIAN_3X3)
442}
443
444#[must_use = "the function does not modify the original image"]
445#[cfg(feature = "rayon")]
446#[doc = generate_parallel_doc_comment!("laplacian_filter")]
447pub fn laplacian_filter_parallel(image: &GrayImage) -> Image<Luma<i16>> {
448 filter_clamped_parallel(image, kernel::LAPLACIAN_3X3)
449}
450
451#[cfg(test)]
452mod tests {
453 use super::*;
454 use crate::definitions::{Clamp, Image};
455 use crate::utils::gray_bench_image;
456 use image::{GrayImage, Luma};
457 use std::cmp::{max, min};
458 use test::black_box;
459
460 #[test]
461 fn test_laplacian() {
462 let image = gray_image!(
463 1, 2, 3;
464 4, 5, 6;
465 7, 8, 9);
466
467 #[rustfmt::skip]
468 let laplacian = Kernel::new(&[
469 0, 1, 0,
470 1, -4, 1,
471 0, 1, 0
472 ], 3, 3);
473
474 let actual = laplacian_filter(&image);
475 let expected = filter_clamped::<_, _, i16>(&image, laplacian);
476 assert_eq!(actual.as_ref(), expected.as_ref());
477 }
478
479 #[test]
480 fn test_separable_filter() {
481 let image = gray_image!(
482 1, 2, 3;
483 4, 5, 6;
484 7, 8, 9);
485
486 let expected = gray_image!(
488 2, 3, 3;
489 4, 5, 5;
490 6, 7, 7);
491
492 let kernel = [1f32 / 3f32; 3];
493 let filtered = separable_filter_equal(&image, &kernel);
494
495 assert_pixels_eq!(filtered, expected);
496 }
497
498 #[test]
499 fn test_separable_filter_integer_kernel() {
500 let image = gray_image!(
501 1, 2, 3;
502 4, 5, 6;
503 7, 8, 9);
504
505 let expected = gray_image!(
506 21, 27, 33;
507 39, 45, 51;
508 57, 63, 69);
509
510 let kernel = [1i32; 3];
511 let filtered = separable_filter_equal(&image, &kernel);
512
513 assert_pixels_eq!(filtered, expected);
514 }
515
516 fn horizontal_filter_reference(image: &GrayImage, kernel: &[f32]) -> GrayImage {
519 let (width, height) = image.dimensions();
520 let mut out = GrayImage::new(width, height);
521
522 for y in 0..height {
523 for x in 0..width {
524 let mut acc = 0f32;
525
526 for (i, k) in kernel.iter().enumerate() {
527 let mut x_unchecked = x as i32 + i as i32 - (kernel.len() / 2) as i32;
528 x_unchecked = max(0, x_unchecked);
529 x_unchecked = min(x_unchecked, width as i32 - 1);
530
531 let x_checked = x_unchecked as u32;
532 let color = image.get_pixel(x_checked, y)[0];
533 let weight = k;
534
535 acc += color as f32 * weight;
536 }
537
538 let clamped = <u8 as Clamp<f32>>::clamp(acc);
539 out.put_pixel(x, y, Luma([clamped]));
540 }
541 }
542
543 out
544 }
545
546 fn vertical_filter_reference(image: &GrayImage, kernel: &[f32]) -> GrayImage {
549 let (width, height) = image.dimensions();
550 let mut out = GrayImage::new(width, height);
551
552 for y in 0..height {
553 for x in 0..width {
554 let mut acc = 0f32;
555
556 for (i, k) in kernel.iter().enumerate() {
557 let mut y_unchecked = y as i32 + i as i32 - (kernel.len() / 2) as i32;
558 y_unchecked = max(0, y_unchecked);
559 y_unchecked = min(y_unchecked, height as i32 - 1);
560
561 let y_checked = y_unchecked as u32;
562 let color = image.get_pixel(x, y_checked)[0];
563 let weight = k;
564
565 acc += color as f32 * weight;
566 }
567
568 let clamped = <u8 as Clamp<f32>>::clamp(acc);
569 out.put_pixel(x, y, Luma([clamped]));
570 }
571 }
572
573 out
574 }
575
576 macro_rules! test_against_reference_implementation {
577 ($test_name:ident, $under_test:ident, $reference_impl:ident) => {
578 #[cfg_attr(miri, ignore = "slow")]
579 #[test]
580 fn $test_name() {
581 for height in 0..5 {
586 for width in 0..5 {
587 for kernel_length in 0..15 {
588 let image = gray_bench_image(width, height);
589 let kernel: Vec<f32> =
590 (0..kernel_length).map(|i| i as f32 % 1.35).collect();
591
592 let expected = $reference_impl(&image, &kernel);
593 let actual = $under_test(&image, &kernel);
594
595 assert_pixels_eq!(actual, expected);
596 }
597 }
598 }
599 }
600 };
601 }
602
603 test_against_reference_implementation!(
604 test_horizontal_filter_matches_reference_implementation,
605 horizontal_filter,
606 horizontal_filter_reference
607 );
608
609 test_against_reference_implementation!(
610 test_vertical_filter_matches_reference_implementation,
611 vertical_filter,
612 vertical_filter_reference
613 );
614
615 #[test]
616 fn test_horizontal_filter() {
617 let image = gray_image!(
618 1, 4, 1;
619 4, 7, 4;
620 1, 4, 1);
621
622 let expected = gray_image!(
623 2, 2, 2;
624 5, 5, 5;
625 2, 2, 2);
626
627 let kernel = [1f32 / 3f32; 3];
628 let filtered = horizontal_filter(&image, &kernel);
629
630 assert_pixels_eq!(filtered, expected);
631 }
632
633 #[test]
634 fn test_horizontal_filter_with_kernel_wider_than_image_does_not_panic() {
635 let image = gray_image!(
636 1, 4, 1;
637 4, 7, 4;
638 1, 4, 1);
639
640 let kernel = [1f32 / 10f32; 10];
641 black_box(horizontal_filter(&image, &kernel));
642 }
643 #[test]
644 fn test_vertical_filter() {
645 let image = gray_image!(
646 1, 4, 1;
647 4, 7, 4;
648 1, 4, 1);
649
650 let expected = gray_image!(
651 2, 5, 2;
652 2, 5, 2;
653 2, 5, 2);
654
655 let kernel = [1f32 / 3f32; 3];
656 let filtered = vertical_filter(&image, &kernel);
657
658 assert_pixels_eq!(filtered, expected);
659 }
660
661 #[test]
662 fn test_vertical_filter_with_kernel_taller_than_image_does_not_panic() {
663 let image = gray_image!(
664 1, 4, 1;
665 4, 7, 4;
666 1, 4, 1);
667
668 let kernel = [1f32 / 10f32; 10];
669 black_box(vertical_filter(&image, &kernel));
670 }
671 #[test]
672 fn test_filter_clamped_with_results_outside_input_channel_range() {
673 #[rustfmt::skip]
674 let kernel: Kernel<i32> = Kernel::new(&[
675 -1, 0, 1,
676 -2, 0, 2,
677 -1, 0, 1
678 ], 3, 3);
679
680 let image = gray_image!(
681 3, 2, 1;
682 6, 5, 4;
683 9, 8, 7);
684
685 let expected = gray_image!(type: i16,
686 -4, -8, -4;
687 -4, -8, -4;
688 -4, -8, -4
689 );
690
691 let filtered = filter_clamped(&image, kernel);
692 assert_pixels_eq!(filtered, expected);
693 }
694
695 #[test]
696 #[cfg(feature = "rayon")]
697 fn test_filter_clamped_parallel_with_results_outside_input_channel_range() {
698 #[rustfmt::skip]
699 let kernel: Kernel<i32> = Kernel::new(&[
700 -1, 0, 1,
701 -2, 0, 2,
702 -1, 0, 1
703 ], 3, 3);
704
705 let image = gray_image!(
706 3, 2, 1;
707 6, 5, 4;
708 9, 8, 7);
709
710 let expected = gray_image!(type: i16,
711 -4, -8, -4;
712 -4, -8, -4;
713 -4, -8, -4
714 );
715
716 let filtered = filter_clamped_parallel(&image, kernel);
717 assert_pixels_eq!(filtered, expected);
718 }
719
720 #[test]
721 #[should_panic]
722 fn test_kernel_must_be_nonempty() {
723 let k: &[u8] = &[];
724 let _ = Kernel::new(k, 0, 0);
725 }
726
727 #[test]
728 fn test_kernel_filter_with_even_kernel_side() {
729 let image = gray_image!(
730 3, 2;
731 4, 1);
732
733 let k = &[1u8, 2u8];
734 let kernel = Kernel::new(k, 2, 1);
735 let filtered = filter(&image, kernel, |x| x);
736
737 let expected = gray_image!(
738 9, 7;
739 12, 6);
740
741 assert_pixels_eq!(filtered, expected);
742 }
743
744 #[test]
745 fn test_kernel_filter_with_empty_image() {
746 let image = gray_image!();
747
748 let k = &[2u8];
749 let kernel = Kernel::new(k, 1, 1);
750 let filtered = filter(&image, kernel, |x| x);
751
752 let expected = gray_image!();
753 assert_pixels_eq!(filtered, expected);
754 }
755
756 #[test]
757 fn test_kernel_filter_with_kernel_dimensions_larger_than_image() {
758 let image = gray_image!(
759 9, 4;
760 8, 1);
761
762 #[rustfmt::skip]
763 let k: &[f32] = &[
764 0.1, 0.2, 0.1,
765 0.2, 0.4, 0.2,
766 0.1, 0.2, 0.1
767 ];
768 let kernel = Kernel::new(k, 3, 3);
769 let filtered: Image<Luma<u8>> = filter(&image, kernel, <u8 as Clamp<f32>>::clamp);
770
771 let expected = gray_image!(
772 11, 7;
773 10, 5);
774
775 assert_pixels_eq!(filtered, expected);
776 }
777 #[test]
778 #[should_panic]
779 fn test_gaussian_blur_f32_rejects_zero_sigma() {
780 let image = gray_image!(
781 1, 2, 3;
782 4, 5, 6;
783 7, 8, 9
784 );
785 let _ = gaussian_blur_f32(&image, 0.0);
786 }
787
788 #[test]
789 #[should_panic]
790 fn test_gaussian_blur_f32_rejects_negative_sigma() {
791 let image = gray_image!(
792 1, 2, 3;
793 4, 5, 6;
794 7, 8, 9
795 );
796 let _ = gaussian_blur_f32(&image, -0.5);
797 }
798
799 #[cfg_attr(miri, ignore = "assert fails")]
800 #[test]
801 fn test_gaussian_on_u8_white_idempotent() {
802 let image = Image::<Luma<u8>>::from_pixel(12, 12, Luma([255]));
803 let image2 = gaussian_blur_f32(&image, 6f32);
804 assert_pixels_eq_within!(image2, image, 0);
805 }
806
807 #[test]
808 fn test_gaussian_on_f32_white_idempotent() {
809 let image = Image::<Luma<f32>>::from_pixel(12, 12, Luma([1.0]));
810 let image2 = gaussian_blur_f32(&image, 6f32);
811 assert_pixels_eq_within!(image2, image, 1e-6);
812 }
813}
814
815#[cfg(not(miri))]
816#[cfg(test)]
817mod proptests {
818 use super::*;
819 use crate::proptest_utils::arbitrary_image;
820 use proptest::prelude::*;
821
822 proptest! {
823 #[test]
824 fn proptest_gaussian_blur_f32(
825 img in arbitrary_image::<Luma<u8>>(0..20, 0..20),
826 sigma in (0.0..150f32).prop_filter("contract", |&x| x > 0.0),
827 ) {
828 let out = gaussian_blur_f32(&img, sigma);
829 assert_eq!(out.dimensions(), img.dimensions());
830 }
831
832 #[test]
833 fn proptest_kernel_luma_f32(
834 img in arbitrary_image::<Luma<f32>>(0..30, 0..30),
835 ker in arbitrary_image::<Luma<f32>>(1..20, 1..20),
836 ) {
837 let kernel = Kernel::new(&ker, ker.width(), ker.height());
838 let out: Image<Luma<f32>> = filter(&img, kernel, |x| {
839 x
840 });
841 assert_eq!(out.dimensions(), img.dimensions());
842 }
843
844 #[test]
845 fn proptest_horizontal_filter_luma_f32(
846 img in arbitrary_image::<Luma<f32>>(0..50, 0..50),
847 ker in proptest::collection::vec(any::<f32>(), 0..50),
848 ) {
849 let out = horizontal_filter(&img, &ker);
850 assert_eq!(out.dimensions(), img.dimensions());
851 }
852
853 #[test]
854 fn proptest_vertical_filter_luma_f32(
855 img in arbitrary_image::<Luma<f32>>(0..50, 0..50),
856 ker in proptest::collection::vec(any::<f32>(), 0..50),
857 ) {
858 let out = vertical_filter(&img, &ker);
859 assert_eq!(out.dimensions(), img.dimensions());
860 }
861
862 #[test]
863 fn proptest_laplacian_filter(
864 img in arbitrary_image::<Luma<u8>>(0..120, 0..120),
865 ) {
866 let out = laplacian_filter(&img);
867 assert_eq!(out.dimensions(), img.dimensions());
868 }
869 }
870}
871
872#[cfg(not(miri))]
873#[cfg(test)]
874mod benches {
875 use super::*;
876 use crate::definitions::Image;
877 use crate::utils::{gray_bench_image, rgb_bench_image};
878 use image::imageops::blur;
879 use image::{GenericImage, Luma, Rgb};
880 use test::{Bencher, black_box};
881
882 #[bench]
883 fn bench_separable_filter(b: &mut Bencher) {
884 let image = gray_bench_image(300, 300);
885 let h_kernel = [1f32 / 5f32; 5];
886 let v_kernel = [0.1f32, 0.4f32, 0.3f32, 0.1f32, 0.1f32];
887 b.iter(|| {
888 let filtered = separable_filter(&image, &h_kernel, &v_kernel);
889 black_box(filtered);
890 });
891 }
892
893 #[bench]
894 fn bench_horizontal_filter(b: &mut Bencher) {
895 let image = gray_bench_image(500, 500);
896 let kernel = [1f32 / 5f32; 5];
897 b.iter(|| {
898 let filtered = horizontal_filter(&image, &kernel);
899 black_box(filtered);
900 });
901 }
902
903 #[bench]
904 fn bench_vertical_filter(b: &mut Bencher) {
905 let image = gray_bench_image(500, 500);
906 let kernel = [1f32 / 5f32; 5];
907 b.iter(|| {
908 let filtered = vertical_filter(&image, &kernel);
909 black_box(filtered);
910 });
911 }
912
913 fn filter3x3_reference(image: &GrayImage, kernel: Kernel<i32>) -> Image<Luma<i16>> {
916 let (width, height) = image.dimensions();
917 let mut out: Image<Luma<i16>> = image::ImageBuffer::new(width, height);
918
919 for y in 0..height {
920 let y_prev = std::cmp::max(1, y) - 1;
921 let y_next = std::cmp::min(height - 2, y) + 1;
922
923 for x in 0..width {
924 let x_prev = std::cmp::max(1, x) - 1;
925 let x_next = std::cmp::min(width - 2, x) + 1;
926
927 let mut acc = 0i32;
928
929 unsafe {
930 acc += image.unsafe_get_pixel(x_prev, y_prev)[0] as i32 * kernel.data[0];
931 acc += image.unsafe_get_pixel(x, y_prev)[0] as i32 * kernel.data[1];
932 acc += image.unsafe_get_pixel(x_next, y_prev)[0] as i32 * kernel.data[2];
933 acc += image.unsafe_get_pixel(x_prev, y)[0] as i32 * kernel.data[3];
934 acc += image.unsafe_get_pixel(x, y)[0] as i32 * kernel.data[4];
935 acc += image.unsafe_get_pixel(x_next, y)[0] as i32 * kernel.data[5];
936 acc += image.unsafe_get_pixel(x_prev, y_next)[0] as i32 * kernel.data[6];
937 acc += image.unsafe_get_pixel(x, y_next)[0] as i32 * kernel.data[7];
938 acc += image.unsafe_get_pixel(x_next, y_next)[0] as i32 * kernel.data[8];
939 }
940
941 let acc = <i16 as Clamp<i32>>::clamp(acc);
942 *out.get_pixel_mut(x, y) = Luma([acc]);
943 }
944 }
945
946 out
947 }
948
949 macro_rules! bench_filter_clamped_gray {
950 ($name:ident, $kernel_size:expr, $image_size:expr, $function_name:ident) => {
951 #[bench]
952 fn $name(b: &mut Bencher) {
953 let image = gray_bench_image($image_size, $image_size);
954 let kernel: Vec<i32> = (0..$kernel_size * $kernel_size).collect();
955 let kernel = Kernel::new(&kernel, $kernel_size, $kernel_size);
956 b.iter(|| {
957 let filtered: Image<Luma<i16>> = $function_name::<_, _, i16>(&image, kernel);
958 black_box(filtered);
959 });
960 }
961 };
962 }
963 macro_rules! bench_filter_clamped_rgb {
964 ($name:ident, $kernel_size:expr, $image_size:expr, $function_name:ident) => {
965 #[bench]
966 fn $name(b: &mut Bencher) {
967 let image = rgb_bench_image($image_size, $image_size);
968 let kernel: Vec<i32> = (0..$kernel_size * $kernel_size).collect();
969 let kernel = Kernel::new(&kernel, $kernel_size, $kernel_size);
970 b.iter(|| {
971 let filtered: Image<Rgb<i16>> = $function_name::<_, _, i16>(&image, kernel);
972 black_box(filtered);
973 });
974 }
975 };
976 }
977
978 bench_filter_clamped_gray!(bench_filter_clamped_gray_3x3, 3, 300, filter_clamped);
979 bench_filter_clamped_gray!(bench_filter_clamped_gray_5x5, 5, 300, filter_clamped);
980 bench_filter_clamped_gray!(bench_filter_clamped_gray_7x7, 7, 300, filter_clamped);
981
982 #[bench]
984 fn bench_filter_clamped_gray_3x3_ref(b: &mut Bencher) {
985 let image = gray_bench_image(300, 300);
986 let kernel: Vec<i32> = (0..9).collect();
987 let kernel = Kernel::new(&kernel, 3, 3);
988 b.iter(|| {
989 let filtered = filter3x3_reference(&image, kernel);
990 black_box(filtered);
991 });
992 }
993
994 bench_filter_clamped_rgb!(bench_filter_clamped_rgb_3x3, 3, 300, filter_clamped);
995 bench_filter_clamped_rgb!(bench_filter_clamped_rgb_5x5, 5, 300, filter_clamped);
996 bench_filter_clamped_rgb!(bench_filter_clamped_rgb_7x7, 7, 300, filter_clamped);
997
998 bench_filter_clamped_gray!(
999 bench_filter_clamped_parallel_gray_3x3,
1000 3,
1001 300,
1002 filter_clamped_parallel
1003 );
1004 bench_filter_clamped_gray!(
1005 bench_filter_clamped_parallel_gray_5x5,
1006 5,
1007 300,
1008 filter_clamped_parallel
1009 );
1010 bench_filter_clamped_gray!(
1011 bench_filter_clamped_parallel_gray_7x7,
1012 7,
1013 300,
1014 filter_clamped_parallel
1015 );
1016 bench_filter_clamped_rgb!(
1017 bench_filter_clamped_parallel_rgb_3x3,
1018 3,
1019 300,
1020 filter_clamped_parallel
1021 );
1022 bench_filter_clamped_rgb!(
1023 bench_filter_clamped_parallel_rgb_5x5,
1024 5,
1025 300,
1026 filter_clamped_parallel
1027 );
1028 bench_filter_clamped_rgb!(
1029 bench_filter_clamped_parallel_rgb_7x7,
1030 7,
1031 300,
1032 filter_clamped_parallel
1033 );
1034
1035 fn gaussian_baseline_rgb<I>(image: &I, stdev: f32) -> Image<Rgb<u8>>
1038 where
1039 I: GenericImage<Pixel = Rgb<u8>>,
1040 {
1041 blur(image, stdev)
1042 }
1043
1044 #[bench]
1045 #[ignore] fn bench_baseline_gaussian_stdev_1(b: &mut Bencher) {
1047 let image = rgb_bench_image(100, 100);
1048 b.iter(|| {
1049 let blurred = gaussian_baseline_rgb(&image, 1f32);
1050 black_box(blurred);
1051 });
1052 }
1053
1054 #[bench]
1055 #[ignore] fn bench_baseline_gaussian_stdev_3(b: &mut Bencher) {
1057 let image = rgb_bench_image(100, 100);
1058 b.iter(|| {
1059 let blurred = gaussian_baseline_rgb(&image, 3f32);
1060 black_box(blurred);
1061 });
1062 }
1063
1064 #[bench]
1065 #[ignore] fn bench_baseline_gaussian_stdev_10(b: &mut Bencher) {
1067 let image = rgb_bench_image(100, 100);
1068 b.iter(|| {
1069 let blurred = gaussian_baseline_rgb(&image, 10f32);
1070 black_box(blurred);
1071 });
1072 }
1073
1074 #[bench]
1075 fn bench_gaussian_f32_stdev_1(b: &mut Bencher) {
1076 let image = rgb_bench_image(100, 100);
1077 b.iter(|| {
1078 let blurred = gaussian_blur_f32(&image, 1f32);
1079 black_box(blurred);
1080 });
1081 }
1082
1083 #[bench]
1084 fn bench_gaussian_f32_stdev_3(b: &mut Bencher) {
1085 let image = rgb_bench_image(100, 100);
1086 b.iter(|| {
1087 let blurred = gaussian_blur_f32(&image, 3f32);
1088 black_box(blurred);
1089 });
1090 }
1091
1092 #[bench]
1093 fn bench_gaussian_f32_stdev_10(b: &mut Bencher) {
1094 let image = rgb_bench_image(100, 100);
1095 b.iter(|| {
1096 let blurred = gaussian_blur_f32(&image, 10f32);
1097 black_box(blurred);
1098 });
1099 }
1100}