Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Box blur fast filter that could approximate gaussian filter #223

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
43 changes: 43 additions & 0 deletions crates/kornia-imgproc/src/filter/kernels.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,38 @@ pub fn sobel_kernel_1d(kernel_size: usize) -> (Vec<f32>, Vec<f32>) {
(kernel_x, kernel_y)
}


/// Create list of optimized box blur kernels based on gaussian sigma
light-le marked this conversation as resolved.
Show resolved Hide resolved
///
/// https://www.peterkovesi.com/papers/FastGaussianSmoothing.pdf
/// # Arguments
///
/// * `sigma` = The sigma of the gaussian kernel
/// * `kernels` = The number of times the box blur kernels would be applied, ideally from 3-5
light-le marked this conversation as resolved.
Show resolved Hide resolved
///
/// # Returns
///
/// A kernels-sized vector of the kernels.
pub fn box_blur_fast_kernels_1d(sigma: f32, kernels: u8) -> Vec<usize> {
let n = kernels as f32;
let ideal_size = (12.0*sigma*sigma/n+1.0).sqrt();
let mut size_l = ideal_size.floor();
size_l -= if size_l % 2.0 == 0.0 {1.0} else {0.0};
let size_u = size_l + 2.0;

let ideal_m = (12.0*sigma*sigma-n*size_l*size_l-4.0*n*size_l-3.0*n) / (-4.0*size_l - 4.0);
let mut boxes = Vec::new();
for i in 0..kernels {
if i < ideal_m.round() as u8 {
boxes.push(size_l as usize);
} else {
boxes.push(size_u as usize);
}
}
boxes
}


#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -92,4 +124,15 @@ mod tests {
assert_eq!(k, expected[i]);
}
}

#[test]
light-le marked this conversation as resolved.
Show resolved Hide resolved
fn test_box_blur_fast_kernels_1d() {

assert_eq!(box_blur_fast_kernels_1d(0.5, 3), vec![1, 1, 1]);
assert_eq!(box_blur_fast_kernels_1d(0.5, 4), vec![1, 1, 1, 1]);
assert_eq!(box_blur_fast_kernels_1d(0.5, 5), vec![1, 1, 1, 1, 1]);

assert_eq!(box_blur_fast_kernels_1d(1.0, 3), vec![1, 1, 3]);
assert_eq!(box_blur_fast_kernels_1d(1.0, 5), vec![1, 1, 1, 1, 3]);
}
}
108 changes: 106 additions & 2 deletions crates/kornia-imgproc/src/filter/ops.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use kornia_image::{Image, ImageError};
use kornia_image::{Image, ImageError, ImageSize};

use super::{kernels, separable_filter};
use super::{kernels, separable_filter, fast_horizontal_filter};

/// Blur an image using a box blur filter
///
Expand Down Expand Up @@ -79,3 +79,107 @@ pub fn sobel<const C: usize>(

Ok(())
}



/// Blur an image using a box blur filter multiple times to achieve a near gaussian blur
///
/// # Arguments
///
/// * `src` - The source image with shape (H, W, C).
/// * `dst` - The destination image with shape (H, W, C).
/// * `kernel_size` - The size of the kernel (kernel_x, kernel_y).
/// * `sigma` - The sigma of the gaussian kernel, xy-ordered.
///
/// PRECONDITION: `src` and `dst` must have the same shape.
pub fn box_blur_fast<const C: usize>(
src: &Image<f32, C>,
dst: &mut Image<f32, C>,
sigma: (f32, f32),
) -> Result<(), ImageError> {
let half_kernel_x_sizes = kernels::box_blur_fast_kernels_1d(sigma.0, 3);
let half_kernel_y_sizes = kernels::box_blur_fast_kernels_1d(sigma.1, 3);

let transposed_size = ImageSize {
width: src.size().height,
height: src.size().width,
};

let mut input_img = src.clone();
for (half_kernel_x_size, half_kernel_y_size) in half_kernel_x_sizes.iter().zip(half_kernel_y_sizes.iter()) {
let mut transposed = Image::<f32, C>::from_size_val(transposed_size, 0.0)?;
light-le marked this conversation as resolved.
Show resolved Hide resolved

fast_horizontal_filter(&input_img, &mut transposed, *half_kernel_x_size)?;
fast_horizontal_filter(&transposed, dst, *half_kernel_y_size)?;

input_img = dst.clone();
}

Ok(())
}


#[cfg(test)]
mod tests {
use super::*;

#[test]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would some simple numbers test too similar to the other functions to verify that’s doing the right thing

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I added 2 tests here: test_box_blur_fast() and test_gaussian_blur(). Both has the same input (0..25) to show that the outputs are not that much different.

I did attempt to use the same input (all 0.0, 9.0 in the middle) as test_fast_horizontal_filter(), if that's how you mean by this comment. The result was a little disappointing as there's a big difference between the outputs of the 2 methods. I figured it's because the test input was odd. It might be fitting for test_fast_horizontal_filter() but not for these. Therefore I went with something more randomized.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why should be different? The test you describe should give you a box of ones, right ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really. The test that gives me a box of ones was because the fast_horizontal_filter() was applied twice. But an actual fast_box_blur() test has the filter applied 6 times (or more). The numbers are a lot more spread out.

fn test_box_blur_fast() -> Result<(), ImageError> {
let size = ImageSize {
width: 5,
height: 5,
};

#[rustfmt::skip]
let img = Image::new(
size,
(0..25).map(|x| x as f32).collect(),
)?;
let mut dst = Image::<_, 1>::from_size_val(size, 0.0)?;

box_blur_fast(&img, &mut dst, (0.5, 0.5))?;

assert_eq!(
dst.as_slice(),
&[
4.444444, 4.9259257, 5.7037034, 6.4814816, 6.962963,
6.851851, 7.3333335, 8.111111, 8.888889, 9.370372,
10.740741, 11.222222, 12.0, 12.777779, 13.259262,
14.629628, 15.111112, 15.888888, 16.666666, 17.14815,
17.037035, 17.518518, 18.296295, 19.074074, 19.555555,
],
);

Ok(())
}

#[test]
fn test_gaussian_blur() -> Result<(), ImageError> {
let size = ImageSize {
width: 5,
height: 5,
};

#[rustfmt::skip]
let img = Image::new(
size,
(0..25).map(|x| x as f32).collect(),
)?;

let mut dst = Image::<_, 1>::from_size_val(size, 0.0)?;

gaussian_blur(&img, &mut dst, (3, 3), (0.5, 0.5))?;

assert_eq!(
dst.as_slice(),
&[
0.57097936, 1.4260278, 2.3195207, 3.213014, 3.5739717,
4.5739717, 5.999999, 7.0, 7.999999, 7.9349294,
9.041435, 10.999999, 12.0, 12.999998, 12.402394,
13.5089, 15.999998, 17.0, 17.999996, 16.86986,
15.58594, 18.230816, 19.124311, 20.017801, 18.588936,
]
);
Ok(())
}
}
120 changes: 120 additions & 0 deletions crates/kornia-imgproc/src/filter/separable_filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,72 @@ pub fn separable_filter<const C: usize>(
Ok(())
}


/// Apply a fast filter horizontally, take advantage of property where all
light-le marked this conversation as resolved.
Show resolved Hide resolved
/// weights are equal to filter an image at O(n) time
///
/// # Arguments
///
/// * `src` - The source image with shape (H, W, C).
/// * `dst` - The destination image with transposed shape (W, H, C).
/// * `half_kernel_x_size` - Half of the kernel at weight 1. The total size would be 2*this+1
pub(crate) fn fast_horizontal_filter<const C: usize>(
src: &Image<f32, C>,
dst: &mut Image<f32, C>,
half_kernel_x_size: usize,
) -> Result<(), ImageError> {
let src_data = src.as_slice();
let dst_data = dst.as_slice_mut();
let mut row_acc = [0.0; C];

let mut leftmost_pixel = [0.0; C];
let mut rightmost_pixel = [0.0; C];
for (pix_offset, source_pixel) in src_data.iter().enumerate() {
let ch = pix_offset % C;
let rc = pix_offset / C;
let c = rc % src.cols();
let r = rc / src.cols();

let transposed_r = c;
let transposed_c = r;
let transposed_pix_offset = transposed_r*src.rows()*C + transposed_c*C + ch;

if c == 0 {
row_acc[ch] = *source_pixel * (half_kernel_x_size+1) as f32;
let mut kernel_pix_offset = pix_offset;
light-le marked this conversation as resolved.
Show resolved Hide resolved
for _ in 0..half_kernel_x_size {
kernel_pix_offset += C;
row_acc[ch] += src_data[kernel_pix_offset];
}
leftmost_pixel[ch] = *source_pixel;
rightmost_pixel[ch] = src_data[pix_offset+((src.cols()-1)*C)];
light-le marked this conversation as resolved.
Show resolved Hide resolved
} else {
row_acc[ch] -= match c.checked_sub(half_kernel_x_size+1) {
Some(_) => {
let prv_leftmost_pix_offset = pix_offset - C*(half_kernel_x_size+1);
src_data[prv_leftmost_pix_offset]
},
None => leftmost_pixel[ch],
};

let rightmost_x = c + half_kernel_x_size;

row_acc[ch] += match rightmost_x {
x if x < src.cols() => {
let rightmost_pix_offset = pix_offset + C*half_kernel_x_size;
src_data[rightmost_pix_offset]
},
_ => { rightmost_pixel[ch] },
};

}
dst_data[transposed_pix_offset] = row_acc[ch] / (half_kernel_x_size*2+1) as f32;
}

Ok(())
}


#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -134,4 +200,58 @@ mod tests {

Ok(())
}


#[test]
fn test_fast_horizontal_filter() -> Result<(), ImageError> {
let size = ImageSize {
width: 5,
height: 5,
};

let img = Image::new(
size,
vec![
0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 9.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0,
],
)?;

let mut transposed = Image::<_, 1>::from_size_val(size, 0.0)?;

fast_horizontal_filter(&img, &mut transposed, 1)?;

assert_eq!(
transposed.as_slice(),
&[
0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 3.0, 0.0, 0.0,
0.0, 0.0, 3.0, 0.0, 0.0,
0.0, 0.0, 3.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0,
]
);

let mut dst = Image::<_, 1>::from_size_val(size, 0.0)?;

fast_horizontal_filter(&transposed, &mut dst, 1)?;

assert_eq!(
dst.as_slice(),
&[
0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 1.0, 1.0, 1.0, 0.0,
0.0, 1.0, 1.0, 1.0, 0.0,
0.0, 1.0, 1.0, 1.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0,
]
);
let xsum = dst.as_slice().iter().sum::<f32>();
assert_eq!(xsum, 9.0);

Ok(())
}
}