Skip to main content

imageproc/filter/
mod.rs

1//! Functions for filtering images.
2
3pub 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
24/// Returns 2d correlation of an image. Intermediate calculations are performed
25/// at type K, and the results converted to pixel Q via f. Pads by continuity.
26///
27/// # Panics
28/// If `P::CHANNEL_COUNT != Q::CHANNEL_COUNT`
29pub 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    // Equation derived from <https://en.wikipedia.org/wiki/Gaussian_function>
128    (sigma * (2.0 * f32::consts::PI).sqrt()).recip() * (-x.powi(2) / (2.0 * sigma.powi(2))).exp()
129}
130
131/// Construct a one dimensional float-valued kernel for performing a Gaussian blur
132/// with standard deviation sigma.
133fn 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/// Blurs an image using a Gaussian of standard deviation sigma.
148/// The kernel used has type f32 and all intermediate calculations are performed
149/// at this type.
150///
151/// # Panics
152///
153/// Panics if `sigma <= 0.0`.
154// TODO: Integer type kernel, approximations via repeated box filter.
155#[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/// Returns 2d correlation of view with the outer product of the 1d
167/// kernels `h_kernel` and `v_kernel`.
168#[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/// Returns 2d correlation of an image with the outer product of the 1d
186/// kernel filter with itself.
187#[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
197/// Returns 2d correlation of an image with a row-major kernel. Intermediate calculations are
198/// performed at type K, and the results clamped to subpixel type S. Pads by continuity.
199///
200/// A parallelized version of this function exists with [`filter_clamped_parallel`] when
201/// the crate `rayon` feature is enabled.
202pub 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/// Returns horizontal correlations between an image and a 1d kernel.
229/// Pads by continuity. Intermediate calculations are performed at
230/// type K.
231#[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    // Don't replace this with a call to filter() without
239    // checking the benchmark results. At the time of writing this
240    // specialised implementation is faster.
241    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    // Typically the image side will be much larger than the kernel length.
249    // In that case we can remove a lot of bounds checks for most pixels.
250    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        // Left margin - need to check lower bound only
271        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        // Neither margin - don't need bounds check on either side
285        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        // Right margin - need to check upper bound only
299        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/// Returns horizontal correlations between an image and a 1d kernel.
317/// Pads by continuity.
318#[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    // Don't replace this with a call to filter() without
326    // checking the benchmark results. At the time of writing this
327    // specialised implementation is faster.
328    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    // Typically the image side will be much larger than the kernel length.
336    // In that case we can remove a lot of bounds checks for most pixels.
337    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    // Top margin - need to check lower bound only
357    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    // Neither margin - don't need bounds check on either side
373    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    // Right margin - need to check upper bound only
389    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/// Calculates the Laplacian of an image.
432///
433/// The Laplacian is computed by filtering the image using the following 3x3 kernel:
434/// ```notrust
435/// 0, 1, 0,
436/// 1,-4, 1,
437/// 0, 1, 0
438/// ```
439#[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        // Lazily copying the box_filter test case
487        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    /// Reference implementation of horizontal_filter. Used to validate
517    /// the (presumably faster) actual implementation.
518    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    /// Reference implementation of vertical_filter. Used to validate
547    /// the (presumably faster) actual implementation.
548    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                // I think the interesting edge cases here are determined entirely
582                // by the relative sizes of the kernel and the image side length, so
583                // I'm just enumerating over small values instead of generating random
584                // examples via proptesting.
585                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    // Simple reference implementation of 3x3 filtering on a grayscale image, to check that the generic filter
914    // functions we provide are at least as fast as doing the simplest thing.
915    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    // Timings for reference basic implementation
983    #[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    /// Baseline implementation of Gaussian blur is that provided by image::imageops.
1036    /// We can also use this to validate correctness of any implementations we add here.
1037    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] // Gives a baseline performance using code from another library
1046    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] // Gives a baseline performance using code from another library
1056    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] // Gives a baseline performance using code from another library
1066    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}