imageproc/filter/
mod.rs

1//! Functions for filtering images.
2
3mod median;
4pub use self::median::median_filter;
5
6mod sharpen;
7pub use self::sharpen::*;
8
9use image::{GenericImage, GenericImageView, GrayImage, ImageBuffer, Luma, Pixel, Primitive};
10
11use crate::definitions::{Clamp, Image};
12use crate::integral_image::{column_running_sum, row_running_sum};
13use crate::map::{ChannelMap, WithChannel};
14use num::{abs, pow, Num};
15
16use crate::math::cast;
17use conv::ValueInto;
18use std::cmp::{max, min};
19use std::f32;
20
21/// Denoise 8-bit grayscale image using bilateral filtering.
22///
23/// # Arguments
24///
25/// * `image` - Grayscale image to be filtered.
26/// * `window_size` - Window size for filtering.
27/// * `sigma_color` - Standard deviation for grayscale distance. A larger value results
28///     in averaging of pixels with larger grayscale differences.
29/// * `sigma_spatial` - Standard deviation for range distance. A larger value results in
30///     averaging of pixels separated by larger distances.
31///
32/// This is a denoising filter designed to preserve edges. It averages pixels based on their spatial
33/// closeness and radiometric similarity [1]. Spatial closeness is measured by the Gaussian function
34/// of the Euclidean distance between two pixels with user-specified standard deviation
35/// (`sigma_spatial`). Radiometric similarity is measured by the Gaussian function of the difference
36/// between two grayscale values with user-specified standard deviation (`sigma_color`).
37///
38/// # References
39///
40///   [1] C. Tomasi and R. Manduchi. "Bilateral Filtering for Gray and Color
41///        Images." IEEE International Conference on Computer Vision (1998)
42///        839-846. DOI: 10.1109/ICCV.1998.710815
43///
44/// # Examples
45///
46/// ```
47/// use imageproc::filter::bilateral_filter;
48/// use imageproc::utils::gray_bench_image;
49/// let image = gray_bench_image(500, 500);
50/// let filtered = bilateral_filter(&image, 10, 10., 3.);
51/// ```
52#[must_use = "the function does not modify the original image"]
53pub fn bilateral_filter(
54    image: &GrayImage,
55    window_size: u32,
56    sigma_color: f32,
57    sigma_spatial: f32,
58) -> Image<Luma<u8>> {
59    /// Un-normalized Gaussian weights for look-up tables.
60    fn gaussian_weight(x: f32, sigma_squared: f32) -> f32 {
61        (-0.5 * x.powi(2) / sigma_squared).exp()
62    }
63
64    /// Effectively a meshgrid command with flattened outputs.
65    fn window_coords(window_size: u32) -> (Vec<i32>, Vec<i32>) {
66        let window_start = (-(window_size as f32) / 2.0).floor() as i32;
67        let window_end = (window_size as f32 / 2.0).floor() as i32 + 1;
68        let window_range = window_start..window_end;
69        let v = window_range.collect::<Vec<i32>>();
70        let cc: Vec<i32> = v
71            .iter()
72            .cycle()
73            .take(v.len() * v.len())
74            .into_iter()
75            .cloned()
76            .collect();
77        let mut rr = Vec::new();
78        let window_range = window_start..window_end;
79        for i in window_range {
80            rr.append(&mut vec![i; (window_size + 1) as usize]);
81        }
82        (rr, cc)
83    }
84
85    /// Create look-up table of Gaussian weights for color dimension.
86    fn compute_color_lut(bins: u32, sigma: f32, max_value: f32) -> Vec<f32> {
87        let v = (0..bins as i32).collect::<Vec<i32>>();
88        let step_size = max_value / bins as f32;
89        let vals = v.iter().map(|&x| x as f32 * step_size).collect::<Vec<_>>();
90        let sigma_squared = sigma.powi(2);
91        let gauss_weights = vals
92            .iter()
93            .map(|&x| gaussian_weight(x, sigma_squared))
94            .collect::<Vec<_>>();
95        gauss_weights
96    }
97
98    /// Create look-up table of weights corresponding to flattened 2-D Gaussian kernel.
99    fn compute_spatial_lut(window_size: u32, sigma: f32) -> Vec<f32> {
100        let (rr, cc) = window_coords(window_size);
101        let mut gauss_weights = Vec::new();
102        let it = rr.iter().zip(cc.iter());
103        let sigma_squared = sigma.powi(2);
104        for (r, c) in it {
105            let dist = f32::sqrt(pow(*r as f32, 2) + pow(*c as f32, 2));
106            gauss_weights.push(gaussian_weight(dist, sigma_squared));
107        }
108        gauss_weights
109    }
110
111    let (width, height) = image.dimensions();
112    let mut out = ImageBuffer::new(width, height);
113    let max_value = *image.iter().max().unwrap() as f32;
114    let n_bins: u32 = 255; // for color or > 8-bit, make n_bins a user input for tuning accuracy.
115    let color_lut = compute_color_lut(n_bins, sigma_color, max_value);
116    let color_dist_scale = n_bins as f32 / max_value;
117    let max_color_bin = (n_bins - 1) as usize;
118    let range_lut = compute_spatial_lut(window_size, sigma_spatial);
119    let window_size = window_size as i32;
120    let window_extent = (window_size - 1) / 2;
121    let height = height as i32;
122    let width = width as i32;
123    for row in 0..height {
124        for col in 0..width {
125            let mut total_val: f32 = 0.;
126            let mut total_weight: f32 = 0.;
127            let window_center_val = image.get_pixel(col as u32, row as u32)[0] as i32;
128            for window_row in -window_extent..window_extent + 1 {
129                let window_row_abs: i32 = row + window_row;
130                let window_row_abs: i32 = min(height - 1, max(0, window_row_abs)); // Wrap to edge.
131                let kr: i32 = window_row + window_extent;
132                for window_col in -window_extent..window_extent + 1 {
133                    let window_col_abs: i32 = col + window_col;
134                    let window_col_abs: i32 = min(width - 1, max(0, window_col_abs)); // Wrap to edge.
135                    let kc: i32 = window_col + window_extent;
136                    let range_bin = (kr * window_size + kc) as usize;
137                    let range_weight: f32 = range_lut[range_bin];
138                    let val: i32 =
139                        image.get_pixel(window_col_abs as u32, window_row_abs as u32)[0] as i32;
140                    let color_dist: i32 = abs(window_center_val - val);
141                    let color_bin = (color_dist as f32 * color_dist_scale) as usize;
142                    let color_bin: usize = min(color_bin, max_color_bin);
143                    let color_weight: f32 = color_lut[color_bin];
144                    let weight: f32 = range_weight * color_weight;
145                    total_val += val as f32 * weight;
146                    total_weight += weight;
147                }
148            }
149            let new_val = (total_val / total_weight).round() as u8;
150            out.put_pixel(col as u32, row as u32, Luma([new_val]));
151        }
152    }
153    out
154}
155
156/// Convolves an 8bpp grayscale image with a kernel of width (2 * `x_radius` + 1)
157/// and height (2 * `y_radius` + 1) whose entries are equal and
158/// sum to one. i.e. each output pixel is the unweighted mean of
159/// a rectangular region surrounding its corresponding input pixel.
160/// We handle locations where the kernel would extend past the image's
161/// boundary by treating the image as if its boundary pixels were
162/// repeated indefinitely.
163// TODO: for small kernels we probably want to do the convolution
164// TODO: directly instead of using an integral image.
165// TODO: more formats!
166#[must_use = "the function does not modify the original image"]
167pub fn box_filter(image: &GrayImage, x_radius: u32, y_radius: u32) -> Image<Luma<u8>> {
168    let (width, height) = image.dimensions();
169    let mut out = ImageBuffer::new(width, height);
170    if width == 0 || height == 0 {
171        return out;
172    }
173
174    let kernel_width = 2 * x_radius + 1;
175    let kernel_height = 2 * y_radius + 1;
176
177    let mut row_buffer = vec![0; (width + 2 * x_radius) as usize];
178    for y in 0..height {
179        row_running_sum(image, y, &mut row_buffer, x_radius);
180        let val = row_buffer[(2 * x_radius) as usize] / kernel_width;
181        unsafe {
182            out.unsafe_put_pixel(0, y, Luma([val as u8]));
183        }
184        for x in 1..width {
185            // TODO: This way we pay rounding errors for each of the
186            // TODO: x and y convolutions. Is there a better way?
187            let u = (x + 2 * x_radius) as usize;
188            let l = (x - 1) as usize;
189            let val = (row_buffer[u] - row_buffer[l]) / kernel_width;
190            unsafe {
191                out.unsafe_put_pixel(x, y, Luma([val as u8]));
192            }
193        }
194    }
195
196    let mut col_buffer = vec![0; (height + 2 * y_radius) as usize];
197    for x in 0..width {
198        column_running_sum(&out, x, &mut col_buffer, y_radius);
199        let val = col_buffer[(2 * y_radius) as usize] / kernel_height;
200        unsafe {
201            out.unsafe_put_pixel(x, 0, Luma([val as u8]));
202        }
203        for y in 1..height {
204            let u = (y + 2 * y_radius) as usize;
205            let l = (y - 1) as usize;
206            let val = (col_buffer[u] - col_buffer[l]) / kernel_height;
207            unsafe {
208                out.unsafe_put_pixel(x, y, Luma([val as u8]));
209            }
210        }
211    }
212
213    out
214}
215
216/// A 2D kernel, used to filter images via convolution.
217pub struct Kernel<'a, K> {
218    data: &'a [K],
219    width: u32,
220    height: u32,
221}
222
223impl<'a, K: Num + Copy + 'a> Kernel<'a, K> {
224    /// Construct a kernel from a slice and its dimensions. The input slice is
225    /// in row-major form.
226    pub fn new(data: &'a [K], width: u32, height: u32) -> Kernel<'a, K> {
227        assert!(width > 0 && height > 0, "width and height must be non-zero");
228        assert!(
229            width * height == data.len() as u32,
230            "Invalid kernel len: expecting {}, found {}",
231            width * height,
232            data.len()
233        );
234        Kernel {
235            data,
236            width,
237            height,
238        }
239    }
240
241    /// Returns 2d correlation of an image. Intermediate calculations are performed
242    /// at type K, and the results converted to pixel Q via f. Pads by continuity.
243    pub fn filter<P, F, Q>(&self, image: &Image<P>, mut f: F) -> Image<Q>
244    where
245        P: Pixel,
246        <P as Pixel>::Subpixel: ValueInto<K>,
247        Q: Pixel,
248        F: FnMut(&mut Q::Subpixel, K),
249    {
250        let (width, height) = image.dimensions();
251        let mut out = Image::<Q>::new(width, height);
252        let num_channels = P::CHANNEL_COUNT as usize;
253        let zero = K::zero();
254        let mut acc = vec![zero; num_channels];
255        let (k_width, k_height) = (self.width as i64, self.height as i64);
256        let (width, height) = (width as i64, height as i64);
257
258        for y in 0..height {
259            for x in 0..width {
260                for k_y in 0..k_height {
261                    let y_p = min(height - 1, max(0, y + k_y - k_height / 2)) as u32;
262                    for k_x in 0..k_width {
263                        let x_p = min(width - 1, max(0, x + k_x - k_width / 2)) as u32;
264                        accumulate(
265                            &mut acc,
266                            unsafe { &image.unsafe_get_pixel(x_p, y_p) },
267                            unsafe { *self.data.get_unchecked((k_y * k_width + k_x) as usize) },
268                        );
269                    }
270                }
271                let out_channels = out.get_pixel_mut(x as u32, y as u32).channels_mut();
272                for (a, c) in acc.iter_mut().zip(out_channels.iter_mut()) {
273                    f(c, *a);
274                    *a = zero;
275                }
276            }
277        }
278
279        out
280    }
281}
282
283#[inline]
284fn gaussian(x: f32, r: f32) -> f32 {
285    ((2.0 * f32::consts::PI).sqrt() * r).recip() * (-x.powi(2) / (2.0 * r.powi(2))).exp()
286}
287
288/// Construct a one dimensional float-valued kernel for performing a Gaussian blur
289/// with standard deviation sigma.
290fn gaussian_kernel_f32(sigma: f32) -> Vec<f32> {
291    let kernel_radius = (2.0 * sigma).ceil() as usize;
292    let mut kernel_data = vec![0.0; 2 * kernel_radius + 1];
293    for i in 0..kernel_radius + 1 {
294        let value = gaussian(i as f32, sigma);
295        kernel_data[kernel_radius + i] = value;
296        kernel_data[kernel_radius - i] = value;
297    }
298    kernel_data
299}
300
301/// Blurs an image using a Gaussian of standard deviation sigma.
302/// The kernel used has type f32 and all intermediate calculations are performed
303/// at this type.
304///
305/// # Panics
306///
307/// Panics if `sigma <= 0.0`.
308// TODO: Integer type kernel, approximations via repeated box filter.
309#[must_use = "the function does not modify the original image"]
310pub fn gaussian_blur_f32<P>(image: &Image<P>, sigma: f32) -> Image<P>
311where
312    P: Pixel,
313    <P as Pixel>::Subpixel: ValueInto<f32> + Clamp<f32>,
314{
315    assert!(sigma > 0.0, "sigma must be > 0.0");
316    let kernel = gaussian_kernel_f32(sigma);
317    separable_filter_equal(image, &kernel)
318}
319
320/// Returns 2d correlation of view with the outer product of the 1d
321/// kernels `h_kernel` and `v_kernel`.
322#[must_use = "the function does not modify the original image"]
323pub fn separable_filter<P, K>(image: &Image<P>, h_kernel: &[K], v_kernel: &[K]) -> Image<P>
324where
325    P: Pixel,
326    <P as Pixel>::Subpixel: ValueInto<K> + Clamp<K>,
327    K: Num + Copy,
328{
329    let h = horizontal_filter(image, h_kernel);
330    vertical_filter(&h, v_kernel)
331}
332
333/// Returns 2d correlation of an image with the outer product of the 1d
334/// kernel filter with itself.
335#[must_use = "the function does not modify the original image"]
336pub fn separable_filter_equal<P, K>(image: &Image<P>, kernel: &[K]) -> Image<P>
337where
338    P: Pixel,
339    <P as Pixel>::Subpixel: ValueInto<K> + Clamp<K>,
340    K: Num + Copy,
341{
342    separable_filter(image, kernel, kernel)
343}
344
345/// Returns 2d correlation of an image with a 3x3 row-major kernel. Intermediate calculations are
346/// performed at type K, and the results clamped to subpixel type S. Pads by continuity.
347#[must_use = "the function does not modify the original image"]
348pub fn filter3x3<P, K, S>(image: &Image<P>, kernel: &[K]) -> Image<ChannelMap<P, S>>
349where
350    P::Subpixel: ValueInto<K>,
351    S: Clamp<K> + Primitive,
352    P: WithChannel<S>,
353    K: Num + Copy,
354{
355    let kernel = Kernel::new(kernel, 3, 3);
356    kernel.filter(image, |channel, acc| *channel = S::clamp(acc))
357}
358
359/// Returns horizontal correlations between an image and a 1d kernel.
360/// Pads by continuity. Intermediate calculations are performed at
361/// type K.
362#[must_use = "the function does not modify the original image"]
363pub fn horizontal_filter<P, K>(image: &Image<P>, kernel: &[K]) -> Image<P>
364where
365    P: Pixel,
366    <P as Pixel>::Subpixel: ValueInto<K> + Clamp<K>,
367    K: Num + Copy,
368{
369    // Don't replace this with a call to Kernel::filter without
370    // checking the benchmark results. At the time of writing this
371    // specialised implementation is faster.
372    let (width, height) = image.dimensions();
373    let mut out = Image::<P>::new(width, height);
374    let zero = K::zero();
375    let mut acc = vec![zero; P::CHANNEL_COUNT as usize];
376    let k_width = kernel.len() as i32;
377
378    // Typically the image side will be much larger than the kernel length.
379    // In that case we can remove a lot of bounds checks for most pixels.
380    if k_width >= width as i32 {
381        for y in 0..height {
382            for x in 0..width {
383                for (i, k) in kernel.iter().enumerate() {
384                    let x_unchecked = (x as i32) + i as i32 - k_width / 2;
385                    let x_p = max(0, min(x_unchecked, width as i32 - 1)) as u32;
386                    let p = unsafe { image.unsafe_get_pixel(x_p, y) };
387                    accumulate(&mut acc, &p, *k);
388                }
389
390                let out_channels = out.get_pixel_mut(x, y).channels_mut();
391                for (a, c) in acc.iter_mut().zip(out_channels.iter_mut()) {
392                    *c = <P as Pixel>::Subpixel::clamp(*a);
393                    *a = zero;
394                }
395            }
396        }
397
398        return out;
399    }
400
401    let half_k = k_width / 2;
402
403    for y in 0..height {
404        // Left margin - need to check lower bound only
405        for x in 0..half_k {
406            for (i, k) in kernel.iter().enumerate() {
407                let x_unchecked = (x as i32) + i as i32 - k_width / 2;
408                let x_p = max(0, x_unchecked) as u32;
409                let p = unsafe { image.unsafe_get_pixel(x_p, y) };
410                accumulate(&mut acc, &p, *k);
411            }
412
413            let out_channels = out.get_pixel_mut(x as u32, y).channels_mut();
414            for (a, c) in acc.iter_mut().zip(out_channels.iter_mut()) {
415                *c = <P as Pixel>::Subpixel::clamp(*a);
416                *a = zero;
417            }
418        }
419
420        // Neither margin - don't need bounds check on either side
421        for x in half_k..(width as i32 - half_k) {
422            for (i, k) in kernel.iter().enumerate() {
423                let x_unchecked = (x as i32) + i as i32 - k_width / 2;
424                let x_p = x_unchecked as u32;
425                let p = unsafe { image.unsafe_get_pixel(x_p, y) };
426                accumulate(&mut acc, &p, *k);
427            }
428
429            let out_channels = out.get_pixel_mut(x as u32, y).channels_mut();
430            for (a, c) in acc.iter_mut().zip(out_channels.iter_mut()) {
431                *c = <P as Pixel>::Subpixel::clamp(*a);
432                *a = zero;
433            }
434        }
435
436        // Right margin - need to check upper bound only
437        for x in (width as i32 - half_k)..(width as i32) {
438            for (i, k) in kernel.iter().enumerate() {
439                let x_unchecked = (x as i32) + i as i32 - k_width / 2;
440                let x_p = min(x_unchecked, width as i32 - 1) as u32;
441                let p = unsafe { image.unsafe_get_pixel(x_p, y) };
442                accumulate(&mut acc, &p, *k);
443            }
444
445            let out_channels = out.get_pixel_mut(x as u32, y).channels_mut();
446            for (a, c) in acc.iter_mut().zip(out_channels.iter_mut()) {
447                *c = <P as Pixel>::Subpixel::clamp(*a);
448                *a = zero;
449            }
450        }
451    }
452
453    out
454}
455
456/// Returns horizontal correlations between an image and a 1d kernel.
457/// Pads by continuity.
458#[must_use = "the function does not modify the original image"]
459pub fn vertical_filter<P, K>(image: &Image<P>, kernel: &[K]) -> Image<P>
460where
461    P: Pixel,
462    <P as Pixel>::Subpixel: ValueInto<K> + Clamp<K>,
463    K: Num + Copy,
464{
465    // Don't replace this with a call to Kernel::filter without
466    // checking the benchmark results. At the time of writing this
467    // specialised implementation is faster.
468    let (width, height) = image.dimensions();
469    let mut out = Image::<P>::new(width, height);
470    let zero = K::zero();
471    let mut acc = vec![zero; P::CHANNEL_COUNT as usize];
472    let k_height = kernel.len() as i32;
473
474    // Typically the image side will be much larger than the kernel length.
475    // In that case we can remove a lot of bounds checks for most pixels.
476    if k_height >= height as i32 {
477        for y in 0..height {
478            for x in 0..width {
479                for (i, k) in kernel.iter().enumerate() {
480                    let y_unchecked = (y as i32) + i as i32 - k_height / 2;
481                    let y_p = max(0, min(y_unchecked, height as i32 - 1)) as u32;
482                    let p = unsafe { image.unsafe_get_pixel(x, y_p) };
483                    accumulate(&mut acc, &p, *k);
484                }
485
486                let out_channels = out.get_pixel_mut(x, y).channels_mut();
487                for (a, c) in acc.iter_mut().zip(out_channels.iter_mut()) {
488                    *c = <P as Pixel>::Subpixel::clamp(*a);
489                    *a = zero;
490                }
491            }
492        }
493
494        return out;
495    }
496
497    let half_k = k_height / 2;
498
499    // Top margin - need to check lower bound only
500    for y in 0..half_k {
501        for x in 0..width {
502            for (i, k) in kernel.iter().enumerate() {
503                let y_unchecked = (y as i32) + i as i32 - k_height / 2;
504                let y_p = max(0, y_unchecked) as u32;
505                let p = unsafe { image.unsafe_get_pixel(x, y_p) };
506                accumulate(&mut acc, &p, *k);
507            }
508
509            let out_channels = out.get_pixel_mut(x, y as u32).channels_mut();
510            for (a, c) in acc.iter_mut().zip(out_channels.iter_mut()) {
511                *c = <P as Pixel>::Subpixel::clamp(*a);
512                *a = zero;
513            }
514        }
515    }
516
517    // Neither margin - don't need bounds check on either side
518    for y in half_k..(height as i32 - half_k) {
519        for x in 0..width {
520            for (i, k) in kernel.iter().enumerate() {
521                let y_unchecked = (y as i32) + i as i32 - k_height / 2;
522                let y_p = y_unchecked as u32;
523                let p = unsafe { image.unsafe_get_pixel(x, y_p) };
524                accumulate(&mut acc, &p, *k);
525            }
526
527            let out_channels = out.get_pixel_mut(x, y as u32).channels_mut();
528            for (a, c) in acc.iter_mut().zip(out_channels.iter_mut()) {
529                *c = <P as Pixel>::Subpixel::clamp(*a);
530                *a = zero;
531            }
532        }
533    }
534
535    // Right margin - need to check upper bound only
536    for y in (height as i32 - half_k)..(height as i32) {
537        for x in 0..width {
538            for (i, k) in kernel.iter().enumerate() {
539                let y_unchecked = (y as i32) + i as i32 - k_height / 2;
540                let y_p = min(y_unchecked, height as i32 - 1) as u32;
541                let p = unsafe { image.unsafe_get_pixel(x, y_p) };
542                accumulate(&mut acc, &p, *k);
543            }
544
545            let out_channels = out.get_pixel_mut(x, y as u32).channels_mut();
546            for (a, c) in acc.iter_mut().zip(out_channels.iter_mut()) {
547                *c = <P as Pixel>::Subpixel::clamp(*a);
548                *a = zero;
549            }
550        }
551    }
552
553    out
554}
555
556fn accumulate<P, K>(acc: &mut [K], pixel: &P, weight: K)
557where
558    P: Pixel,
559    <P as Pixel>::Subpixel: ValueInto<K>,
560    K: Num + Copy,
561{
562    for i in 0..(P::CHANNEL_COUNT as usize) {
563        acc[i as usize] = acc[i as usize] + cast(pixel.channels()[i]) * weight;
564    }
565}
566
567#[cfg(test)]
568mod tests {
569    use super::*;
570    use crate::definitions::{Clamp, Image};
571    use crate::utils::{gray_bench_image, rgb_bench_image};
572    use image::imageops::blur;
573    use image::{GenericImage, GrayImage, ImageBuffer, Luma, Rgb};
574    use std::cmp::{max, min};
575    use test::{black_box, Bencher};
576
577    #[bench]
578    fn bench_bilateral_filter(b: &mut Bencher) {
579        let image = gray_bench_image(500, 500);
580        b.iter(|| {
581            let filtered = bilateral_filter(&image, 10, 10., 3.);
582            black_box(filtered);
583        });
584    }
585
586    #[test]
587    fn test_box_filter_handles_empty_images() {
588        let _ = box_filter(&GrayImage::new(0, 0), 3, 3);
589        let _ = box_filter(&GrayImage::new(1, 0), 3, 3);
590        let _ = box_filter(&GrayImage::new(0, 1), 3, 3);
591    }
592
593    #[test]
594    fn test_box_filter() {
595        let image = gray_image!(
596            1, 2, 3;
597            4, 5, 6;
598            7, 8, 9);
599
600        // For this image we get the same answer from the two 1d
601        // convolutions as from doing the 2d convolution in one step
602        // (but we needn't in general, as in the former case we're
603        // clipping to an integer value twice).
604        let expected = gray_image!(
605            2, 3, 3;
606            4, 5, 5;
607            6, 7, 7);
608
609        assert_pixels_eq!(box_filter(&image, 1, 1), expected);
610    }
611
612    #[bench]
613    fn bench_box_filter(b: &mut Bencher) {
614        let image = gray_bench_image(500, 500);
615        b.iter(|| {
616            let filtered = box_filter(&image, 7, 7);
617            black_box(filtered);
618        });
619    }
620
621    #[test]
622    fn test_separable_filter() {
623        let image = gray_image!(
624            1, 2, 3;
625            4, 5, 6;
626            7, 8, 9);
627
628        // Lazily copying the box_filter test case
629        let expected = gray_image!(
630            2, 3, 3;
631            4, 5, 5;
632            6, 7, 7);
633
634        let kernel = vec![1f32 / 3f32; 3];
635        let filtered = separable_filter_equal(&image, &kernel);
636
637        assert_pixels_eq!(filtered, expected);
638    }
639
640    #[test]
641    fn test_separable_filter_integer_kernel() {
642        let image = gray_image!(
643            1, 2, 3;
644            4, 5, 6;
645            7, 8, 9);
646
647        let expected = gray_image!(
648            21, 27, 33;
649            39, 45, 51;
650            57, 63, 69);
651
652        let kernel = vec![1i32; 3];
653        let filtered = separable_filter_equal(&image, &kernel);
654
655        assert_pixels_eq!(filtered, expected);
656    }
657
658    #[bench]
659    fn bench_separable_filter(b: &mut Bencher) {
660        let image = gray_bench_image(300, 300);
661        let h_kernel = vec![1f32 / 5f32; 5];
662        let v_kernel = vec![0.1f32, 0.4f32, 0.3f32, 0.1f32, 0.1f32];
663        b.iter(|| {
664            let filtered = separable_filter(&image, &h_kernel, &v_kernel);
665            black_box(filtered);
666        });
667    }
668
669    /// Reference implementation of horizontal_filter. Used to validate
670    /// the (presumably faster) actual implementation.
671    fn horizontal_filter_reference(image: &GrayImage, kernel: &[f32]) -> GrayImage {
672        let (width, height) = image.dimensions();
673        let mut out = GrayImage::new(width, height);
674
675        for y in 0..height {
676            for x in 0..width {
677                let mut acc = 0f32;
678
679                for k in 0..kernel.len() {
680                    let mut x_unchecked = x as i32 + k as i32 - (kernel.len() / 2) as i32;
681                    x_unchecked = max(0, x_unchecked);
682                    x_unchecked = min(x_unchecked, width as i32 - 1);
683
684                    let x_checked = x_unchecked as u32;
685                    let color = image.get_pixel(x_checked, y)[0];
686                    let weight = kernel[k];
687
688                    acc += color as f32 * weight;
689                }
690
691                let clamped = <u8 as Clamp<f32>>::clamp(acc);
692                out.put_pixel(x, y, Luma([clamped]));
693            }
694        }
695
696        out
697    }
698
699    /// Reference implementation of vertical_filter. Used to validate
700    /// the (presumably faster) actual implementation.
701    fn vertical_filter_reference(image: &GrayImage, kernel: &[f32]) -> GrayImage {
702        let (width, height) = image.dimensions();
703        let mut out = GrayImage::new(width, height);
704
705        for y in 0..height {
706            for x in 0..width {
707                let mut acc = 0f32;
708
709                for k in 0..kernel.len() {
710                    let mut y_unchecked = y as i32 + k as i32 - (kernel.len() / 2) as i32;
711                    y_unchecked = max(0, y_unchecked);
712                    y_unchecked = min(y_unchecked, height as i32 - 1);
713
714                    let y_checked = y_unchecked as u32;
715                    let color = image.get_pixel(x, y_checked)[0];
716                    let weight = kernel[k];
717
718                    acc += color as f32 * weight;
719                }
720
721                let clamped = <u8 as Clamp<f32>>::clamp(acc);
722                out.put_pixel(x, y, Luma([clamped]));
723            }
724        }
725
726        out
727    }
728
729    macro_rules! test_against_reference_implementation {
730        ($test_name:ident, $under_test:ident, $reference_impl:ident) => {
731            #[test]
732            fn $test_name() {
733                // I think the interesting edge cases here are determined entirely
734                // by the relative sizes of the kernel and the image side length, so
735                // I'm just enumerating over small values instead of generating random
736                // examples via quickcheck.
737                for height in 0..5 {
738                    for width in 0..5 {
739                        for kernel_length in 0..15 {
740                            let image = gray_bench_image(width, height);
741                            let kernel: Vec<f32> =
742                                (0..kernel_length).map(|i| i as f32 % 1.35).collect();
743
744                            let expected = $reference_impl(&image, &kernel);
745                            let actual = $under_test(&image, &kernel);
746
747                            assert_pixels_eq!(actual, expected);
748                        }
749                    }
750                }
751            }
752        };
753    }
754
755    test_against_reference_implementation!(
756        test_horizontal_filter_matches_reference_implementation,
757        horizontal_filter,
758        horizontal_filter_reference
759    );
760
761    test_against_reference_implementation!(
762        test_vertical_filter_matches_reference_implementation,
763        vertical_filter,
764        vertical_filter_reference
765    );
766
767    #[test]
768    fn test_horizontal_filter() {
769        let image = gray_image!(
770            1, 4, 1;
771            4, 7, 4;
772            1, 4, 1);
773
774        let expected = gray_image!(
775            2, 2, 2;
776            5, 5, 5;
777            2, 2, 2);
778
779        let kernel = vec![1f32 / 3f32; 3];
780        let filtered = horizontal_filter(&image, &kernel);
781
782        assert_pixels_eq!(filtered, expected);
783    }
784
785    #[test]
786    fn test_horizontal_filter_with_kernel_wider_than_image_does_not_panic() {
787        let image = gray_image!(
788            1, 4, 1;
789            4, 7, 4;
790            1, 4, 1);
791
792        let kernel = vec![1f32 / 10f32; 10];
793        black_box(horizontal_filter(&image, &kernel));
794    }
795
796    #[bench]
797    fn bench_horizontal_filter(b: &mut Bencher) {
798        let image = gray_bench_image(500, 500);
799        let kernel = vec![1f32 / 5f32; 5];
800        b.iter(|| {
801            let filtered = horizontal_filter(&image, &kernel);
802            black_box(filtered);
803        });
804    }
805
806    #[test]
807    fn test_vertical_filter() {
808        let image = gray_image!(
809            1, 4, 1;
810            4, 7, 4;
811            1, 4, 1);
812
813        let expected = gray_image!(
814            2, 5, 2;
815            2, 5, 2;
816            2, 5, 2);
817
818        let kernel = vec![1f32 / 3f32; 3];
819        let filtered = vertical_filter(&image, &kernel);
820
821        assert_pixels_eq!(filtered, expected);
822    }
823
824    #[test]
825    fn test_vertical_filter_with_kernel_taller_than_image_does_not_panic() {
826        let image = gray_image!(
827            1, 4, 1;
828            4, 7, 4;
829            1, 4, 1);
830
831        let kernel = vec![1f32 / 10f32; 10];
832        black_box(vertical_filter(&image, &kernel));
833    }
834
835    #[bench]
836    fn bench_vertical_filter(b: &mut Bencher) {
837        let image = gray_bench_image(500, 500);
838        let kernel = vec![1f32 / 5f32; 5];
839        b.iter(|| {
840            let filtered = vertical_filter(&image, &kernel);
841            black_box(filtered);
842        });
843    }
844
845    #[test]
846    fn test_filter3x3_with_results_outside_input_channel_range() {
847        #[rustfmt::skip]
848        let kernel: Vec<i32> = vec![
849            -1, 0, 1,
850            -2, 0, 2,
851            -1, 0, 1
852        ];
853
854        let image = gray_image!(
855            3, 2, 1;
856            6, 5, 4;
857            9, 8, 7);
858
859        let expected = gray_image!(type: i16,
860            -4, -8, -4;
861            -4, -8, -4;
862            -4, -8, -4
863        );
864
865        let filtered = filter3x3(&image, &kernel);
866        assert_pixels_eq!(filtered, expected);
867    }
868
869    #[test]
870    #[should_panic]
871    fn test_kernel_must_be_nonempty() {
872        let k: Vec<u8> = Vec::new();
873        let _ = Kernel::new(&k, 0, 0);
874    }
875
876    #[test]
877    fn test_kernel_filter_with_even_kernel_side() {
878        let image = gray_image!(
879            3, 2;
880            4, 1);
881
882        let k = vec![1u8, 2u8];
883        let kernel = Kernel::new(&k, 2, 1);
884        let filtered = kernel.filter(&image, |c, a| *c = a);
885
886        let expected = gray_image!(
887             9,  7;
888            12,  6);
889
890        assert_pixels_eq!(filtered, expected);
891    }
892
893    #[test]
894    fn test_kernel_filter_with_empty_image() {
895        let image = gray_image!();
896
897        let k = vec![2u8];
898        let kernel = Kernel::new(&k, 1, 1);
899        let filtered = kernel.filter(&image, |c, a| *c = a);
900
901        let expected = gray_image!();
902        assert_pixels_eq!(filtered, expected);
903    }
904
905    #[test]
906    fn test_kernel_filter_with_kernel_dimensions_larger_than_image() {
907        let image = gray_image!(
908            9, 4;
909            8, 1);
910
911        #[rustfmt::skip]
912        let k: Vec<f32> = vec![
913            0.1, 0.2, 0.1,
914            0.2, 0.4, 0.2,
915            0.1, 0.2, 0.1
916        ];
917        let kernel = Kernel::new(&k, 3, 3);
918        let filtered: Image<Luma<u8>> =
919            kernel.filter(&image, |c, a| *c = <u8 as Clamp<f32>>::clamp(a));
920
921        let expected = gray_image!(
922            11,  7;
923            10,  5);
924
925        assert_pixels_eq!(filtered, expected);
926    }
927
928    #[bench]
929    fn bench_filter3x3_i32_filter(b: &mut Bencher) {
930        let image = gray_bench_image(500, 500);
931        #[rustfmt::skip]
932        let kernel: Vec<i32> = vec![
933            -1, 0, 1,
934            -2, 0, 2,
935            -1, 0, 1
936        ];
937
938        b.iter(|| {
939            let filtered: ImageBuffer<Luma<i16>, Vec<i16>> =
940                filter3x3::<_, _, i16>(&image, &kernel);
941            black_box(filtered);
942        });
943    }
944
945    /// Baseline implementation of Gaussian blur is that provided by image::imageops.
946    /// We can also use this to validate correctnes of any implementations we add here.
947    fn gaussian_baseline_rgb<I>(image: &I, stdev: f32) -> Image<Rgb<u8>>
948    where
949        I: GenericImage<Pixel = Rgb<u8>>,
950    {
951        blur(image, stdev)
952    }
953
954    #[bench]
955    #[ignore] // Gives a baseline performance using code from another library
956    fn bench_baseline_gaussian_stdev_1(b: &mut Bencher) {
957        let image = rgb_bench_image(100, 100);
958        b.iter(|| {
959            let blurred = gaussian_baseline_rgb(&image, 1f32);
960            black_box(blurred);
961        });
962    }
963
964    #[bench]
965    #[ignore] // Gives a baseline performance using code from another library
966    fn bench_baseline_gaussian_stdev_3(b: &mut Bencher) {
967        let image = rgb_bench_image(100, 100);
968        b.iter(|| {
969            let blurred = gaussian_baseline_rgb(&image, 3f32);
970            black_box(blurred);
971        });
972    }
973
974    #[bench]
975    #[ignore] // Gives a baseline performance using code from another library
976    fn bench_baseline_gaussian_stdev_10(b: &mut Bencher) {
977        let image = rgb_bench_image(100, 100);
978        b.iter(|| {
979            let blurred = gaussian_baseline_rgb(&image, 10f32);
980            black_box(blurred);
981        });
982    }
983
984    #[bench]
985    fn bench_gaussian_f32_stdev_1(b: &mut Bencher) {
986        let image = rgb_bench_image(100, 100);
987        b.iter(|| {
988            let blurred = gaussian_blur_f32(&image, 1f32);
989            black_box(blurred);
990        });
991    }
992
993    #[bench]
994    fn bench_gaussian_f32_stdev_3(b: &mut Bencher) {
995        let image = rgb_bench_image(100, 100);
996        b.iter(|| {
997            let blurred = gaussian_blur_f32(&image, 3f32);
998            black_box(blurred);
999        });
1000    }
1001
1002    #[bench]
1003    fn bench_gaussian_f32_stdev_10(b: &mut Bencher) {
1004        let image = rgb_bench_image(100, 100);
1005        b.iter(|| {
1006            let blurred = gaussian_blur_f32(&image, 10f32);
1007            black_box(blurred);
1008        });
1009    }
1010
1011    #[test]
1012    #[should_panic]
1013    fn test_gaussian_blur_f32_rejects_zero_sigma() {
1014        let image = gray_image!(
1015            1, 2, 3;
1016            4, 5, 6;
1017            7, 8, 9
1018        );
1019        let _ = gaussian_blur_f32(&image, 0.0);
1020    }
1021
1022    #[test]
1023    #[should_panic]
1024    fn test_gaussian_blur_f32_rejects_negative_sigma() {
1025        let image = gray_image!(
1026            1, 2, 3;
1027            4, 5, 6;
1028            7, 8, 9
1029        );
1030        let _ = gaussian_blur_f32(&image, -0.5);
1031    }
1032}