From aa50768de36e098b9284174f06914f42f0b0af18 Mon Sep 17 00:00:00 2001 From: Phil Ellison Date: Mon, 27 May 2024 17:46:32 +0100 Subject: [PATCH] Move box_filter into a separate file --- src/filter/box_filter.rs | 136 +++++++++++++++++++++++++++++++++++++++ src/filter/mod.rs | 114 ++------------------------------ 2 files changed, 140 insertions(+), 110 deletions(-) create mode 100644 src/filter/box_filter.rs diff --git a/src/filter/box_filter.rs b/src/filter/box_filter.rs new file mode 100644 index 00000000..16a36bf0 --- /dev/null +++ b/src/filter/box_filter.rs @@ -0,0 +1,136 @@ +use image::{GenericImage, GenericImageView, GrayImage, Luma}; + +use crate::{definitions::Image, integral_image::{column_running_sum, row_running_sum}}; + +/// Convolves an 8bpp grayscale image with a kernel of width (2 * `x_radius` + 1) +/// and height (2 * `y_radius` + 1) whose entries are equal and +/// sum to one. i.e. each output pixel is the unweighted mean of +/// a rectangular region surrounding its corresponding input pixel. +/// We handle locations where the kernel would extend past the image's +/// boundary by treating the image as if its boundary pixels were +/// repeated indefinitely. +// TODO: for small kernels we probably want to do the convolution +// TODO: directly instead of using an integral image. +// TODO: more formats! +#[must_use = "the function does not modify the original image"] +pub fn box_filter(image: &GrayImage, x_radius: u32, y_radius: u32) -> Image> { + let (width, height) = image.dimensions(); + let mut out = Image::new(width, height); + if width == 0 || height == 0 { + return out; + } + + let kernel_width = 2 * x_radius + 1; + let kernel_height = 2 * y_radius + 1; + + let mut row_buffer = vec![0; (width + 2 * x_radius) as usize]; + for y in 0..height { + row_running_sum(image, y, &mut row_buffer, x_radius); + let val = row_buffer[(2 * x_radius) as usize] / kernel_width; + unsafe { + debug_assert!(out.in_bounds(0, y)); + out.unsafe_put_pixel(0, y, Luma([val as u8])); + } + for x in 1..width { + // TODO: This way we pay rounding errors for each of the + // TODO: x and y convolutions. Is there a better way? + let u = (x + 2 * x_radius) as usize; + let l = (x - 1) as usize; + let val = (row_buffer[u] - row_buffer[l]) / kernel_width; + unsafe { + debug_assert!(out.in_bounds(x, y)); + out.unsafe_put_pixel(x, y, Luma([val as u8])); + } + } + } + + let mut col_buffer = vec![0; (height + 2 * y_radius) as usize]; + for x in 0..width { + column_running_sum(&out, x, &mut col_buffer, y_radius); + let val = col_buffer[(2 * y_radius) as usize] / kernel_height; + unsafe { + debug_assert!(out.in_bounds(x, 0)); + out.unsafe_put_pixel(x, 0, Luma([val as u8])); + } + for y in 1..height { + let u = (y + 2 * y_radius) as usize; + let l = (y - 1) as usize; + let val = (col_buffer[u] - col_buffer[l]) / kernel_height; + unsafe { + debug_assert!(out.in_bounds(x, y)); + out.unsafe_put_pixel(x, y, Luma([val as u8])); + } + } + } + + out +} + +#[cfg(test)] +mod tests { + use super::*; + use image::GrayImage; + + #[test] + fn test_box_filter_handles_empty_images() { + let _ = box_filter(&GrayImage::new(0, 0), 3, 3); + let _ = box_filter(&GrayImage::new(1, 0), 3, 3); + let _ = box_filter(&GrayImage::new(0, 1), 3, 3); + } + + #[test] + fn test_box_filter() { + let image = gray_image!( + 1, 2, 3; + 4, 5, 6; + 7, 8, 9); + + // For this image we get the same answer from the two 1d + // convolutions as from doing the 2d convolution in one step + // (but we needn't in general, as in the former case we're + // clipping to an integer value twice). + let expected = gray_image!( + 2, 3, 3; + 4, 5, 5; + 6, 7, 7); + + assert_pixels_eq!(box_filter(&image, 1, 1), expected); + } +} + +#[cfg(not(miri))] +#[cfg(test)] +mod proptests { + use super::*; + use crate::proptest_utils::arbitrary_image; + use proptest::prelude::*; + + proptest! { + #[test] + fn proptest_box_filter( + img in arbitrary_image::>(0..200, 0..200), + x_radius in 0..100u32, + y_radius in 0..100u32, + ) { + let out = box_filter(&img, x_radius, y_radius); + assert_eq!(out.dimensions(), img.dimensions()); + } + } +} + +#[cfg(not(miri))] +#[cfg(test)] +mod benches { + use super::*; + use crate::utils::gray_bench_image; + use test::{black_box, Bencher}; + + #[bench] + fn bench_box_filter(b: &mut Bencher) { + let image = gray_bench_image(500, 500); + b.iter(|| { + let filtered = box_filter(&image, 7, 7); + black_box(filtered); + }); + } +} diff --git a/src/filter/mod.rs b/src/filter/mod.rs index 726ba77b..f04abbce 100644 --- a/src/filter/mod.rs +++ b/src/filter/mod.rs @@ -8,10 +8,12 @@ pub use self::median::median_filter; mod sharpen; pub use self::sharpen::*; -use image::{GenericImage, GenericImageView, GrayImage, Luma, Pixel, Primitive}; +mod box_filter; +pub use self::box_filter::box_filter; + +use image::{GenericImageView, GrayImage, Luma, Pixel, Primitive}; use crate::definitions::{Clamp, Image}; -use crate::integral_image::{column_running_sum, row_running_sum}; use crate::kernel::{self, Kernel}; use crate::map::{ChannelMap, WithChannel}; use num::Num; @@ -19,70 +21,6 @@ use num::Num; use std::cmp::{max, min}; use std::f32; -/// Convolves an 8bpp grayscale image with a kernel of width (2 * `x_radius` + 1) -/// and height (2 * `y_radius` + 1) whose entries are equal and -/// sum to one. i.e. each output pixel is the unweighted mean of -/// a rectangular region surrounding its corresponding input pixel. -/// We handle locations where the kernel would extend past the image's -/// boundary by treating the image as if its boundary pixels were -/// repeated indefinitely. -// TODO: for small kernels we probably want to do the convolution -// TODO: directly instead of using an integral image. -// TODO: more formats! -#[must_use = "the function does not modify the original image"] -pub fn box_filter(image: &GrayImage, x_radius: u32, y_radius: u32) -> Image> { - let (width, height) = image.dimensions(); - let mut out = Image::new(width, height); - if width == 0 || height == 0 { - return out; - } - - let kernel_width = 2 * x_radius + 1; - let kernel_height = 2 * y_radius + 1; - - let mut row_buffer = vec![0; (width + 2 * x_radius) as usize]; - for y in 0..height { - row_running_sum(image, y, &mut row_buffer, x_radius); - let val = row_buffer[(2 * x_radius) as usize] / kernel_width; - unsafe { - debug_assert!(out.in_bounds(0, y)); - out.unsafe_put_pixel(0, y, Luma([val as u8])); - } - for x in 1..width { - // TODO: This way we pay rounding errors for each of the - // TODO: x and y convolutions. Is there a better way? - let u = (x + 2 * x_radius) as usize; - let l = (x - 1) as usize; - let val = (row_buffer[u] - row_buffer[l]) / kernel_width; - unsafe { - debug_assert!(out.in_bounds(x, y)); - out.unsafe_put_pixel(x, y, Luma([val as u8])); - } - } - } - - let mut col_buffer = vec![0; (height + 2 * y_radius) as usize]; - for x in 0..width { - column_running_sum(&out, x, &mut col_buffer, y_radius); - let val = col_buffer[(2 * y_radius) as usize] / kernel_height; - unsafe { - debug_assert!(out.in_bounds(x, 0)); - out.unsafe_put_pixel(x, 0, Luma([val as u8])); - } - for y in 1..height { - let u = (y + 2 * y_radius) as usize; - let l = (y - 1) as usize; - let val = (col_buffer[u] - col_buffer[l]) / kernel_height; - unsafe { - debug_assert!(out.in_bounds(x, y)); - out.unsafe_put_pixel(x, y, Luma([val as u8])); - } - } - } - - out -} - /// Returns 2d correlation of an image. Intermediate calculations are performed /// at type K, and the results converted to pixel Q via f. Pads by continuity. /// @@ -540,31 +478,6 @@ mod tests { assert_eq!(actual.as_ref(), expected.as_ref()); } - #[test] - fn test_box_filter_handles_empty_images() { - let _ = box_filter(&GrayImage::new(0, 0), 3, 3); - let _ = box_filter(&GrayImage::new(1, 0), 3, 3); - let _ = box_filter(&GrayImage::new(0, 1), 3, 3); - } - - #[test] - fn test_box_filter() { - let image = gray_image!( - 1, 2, 3; - 4, 5, 6; - 7, 8, 9); - - // For this image we get the same answer from the two 1d - // convolutions as from doing the 2d convolution in one step - // (but we needn't in general, as in the former case we're - // clipping to an integer value twice). - let expected = gray_image!( - 2, 3, 3; - 4, 5, 5; - 6, 7, 7); - - assert_pixels_eq!(box_filter(&image, 1, 1), expected); - } #[test] fn test_separable_filter() { let image = gray_image!( @@ -907,16 +820,6 @@ mod proptests { use proptest::prelude::*; proptest! { - #[test] - fn proptest_box_filter( - img in arbitrary_image::>(0..200, 0..200), - x_radius in 0..100u32, - y_radius in 0..100u32, - ) { - let out = box_filter(&img, x_radius, y_radius); - assert_eq!(out.dimensions(), img.dimensions()); - } - #[test] fn proptest_gaussian_blur_f32( img in arbitrary_image::>(0..20, 0..20), @@ -976,15 +879,6 @@ mod benches { use image::{GenericImage, Luma, Rgb}; use test::{black_box, Bencher}; - #[bench] - fn bench_box_filter(b: &mut Bencher) { - let image = gray_bench_image(500, 500); - b.iter(|| { - let filtered = box_filter(&image, 7, 7); - black_box(filtered); - }); - } - #[bench] fn bench_separable_filter(b: &mut Bencher) { let image = gray_bench_image(300, 300);