Skip to content

Commit 0a357b5

Browse files
light-leedgarriba
andauthored
Implement Box blur fast filter that could approximate gaussian filter (#223)
* implement box_blur_fast_kernels_1d with test * implement fast_horizontal_filter with test * implement box_blur_fast ops with tests * Apply docstring suggestions from code review Co-authored-by: Edgar Riba <[email protected]> * add more tests for box_blur_fast_kernels_1d * some fixes from suggestions * optimize calculation within loop and fmt fix * add bench for fast_box_blur * fmt fixes * simplify header doc --------- Co-authored-by: Edgar Riba <[email protected]>
1 parent 03a0b2c commit 0a357b5

File tree

4 files changed

+278
-3
lines changed

4 files changed

+278
-3
lines changed

crates/kornia-imgproc/benches/bench_filters.rs

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
22

33
use kornia_image::Image;
4-
use kornia_imgproc::filter::gaussian_blur;
4+
use kornia_imgproc::filter::{box_blur_fast, gaussian_blur};
55

66
use image::RgbImage;
77
use imageproc::filter::gaussian_blur_f32;
@@ -51,6 +51,15 @@ fn bench_filters(c: &mut Criterion) {
5151
b.iter(|| black_box(gaussian_blur_f32(&rgb_image, sigma)))
5252
},
5353
);
54+
55+
group.bench_with_input(
56+
BenchmarkId::new("box_blur_fast", &parameter_string),
57+
&(&image, &output),
58+
|b, i| {
59+
let (src, mut dst) = (i.0, i.1.clone());
60+
b.iter(|| black_box(box_blur_fast(src, &mut dst, (1.5, 1.5))))
61+
},
62+
);
5463
}
5564
}
5665

crates/kornia-imgproc/src/filter/kernels.rs

+41
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,37 @@ pub fn sobel_kernel_1d(kernel_size: usize) -> (Vec<f32>, Vec<f32>) {
6161
(kernel_x, kernel_y)
6262
}
6363

64+
/// Create list of optimized box blur kernels based on gaussian sigma
65+
///
66+
/// https://www.peterkovesi.com/papers/FastGaussianSmoothing.pdf
67+
/// # Arguments
68+
///
69+
/// * `sigma` = The sigma of the gaussian kernel
70+
/// * `kernels` = The number of times the box blur kernels would be applied, ideally from 3-5
71+
///
72+
/// # Returns
73+
///
74+
/// A kernels-sized vector of the kernels.
75+
pub fn box_blur_fast_kernels_1d(sigma: f32, kernels: u8) -> Vec<usize> {
76+
let n = kernels as f32;
77+
let ideal_size = (12.0 * sigma * sigma / n + 1.0).sqrt();
78+
let mut size_l = ideal_size.floor();
79+
size_l -= if size_l % 2.0 == 0.0 { 1.0 } else { 0.0 };
80+
let size_u = size_l + 2.0;
81+
82+
let ideal_m = (12.0 * sigma * sigma - n * size_l * size_l - 4.0 * n * size_l - 3.0 * n)
83+
/ (-4.0 * size_l - 4.0);
84+
let mut boxes = Vec::new();
85+
for i in 0..kernels {
86+
if i < ideal_m.round() as u8 {
87+
boxes.push(size_l as usize);
88+
} else {
89+
boxes.push(size_u as usize);
90+
}
91+
}
92+
boxes
93+
}
94+
6495
#[cfg(test)]
6596
mod tests {
6697
use super::*;
@@ -92,4 +123,14 @@ mod tests {
92123
assert_eq!(k, expected[i]);
93124
}
94125
}
126+
127+
#[test]
128+
fn test_box_blur_fast_kernels_1d() {
129+
assert_eq!(box_blur_fast_kernels_1d(0.5, 3), vec![1, 1, 1]);
130+
assert_eq!(box_blur_fast_kernels_1d(0.5, 4), vec![1, 1, 1, 1]);
131+
assert_eq!(box_blur_fast_kernels_1d(0.5, 5), vec![1, 1, 1, 1, 1]);
132+
133+
assert_eq!(box_blur_fast_kernels_1d(1.0, 3), vec![1, 1, 3]);
134+
assert_eq!(box_blur_fast_kernels_1d(1.0, 5), vec![1, 1, 1, 1, 3]);
135+
}
95136
}

crates/kornia-imgproc/src/filter/ops.rs

+107-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
use kornia_image::{Image, ImageError};
1+
use kornia_image::{Image, ImageError, ImageSize};
22

3-
use super::{kernels, separable_filter};
3+
use super::{fast_horizontal_filter, kernels, separable_filter};
44

55
/// Blur an image using a box blur filter
66
///
@@ -79,3 +79,108 @@ pub fn sobel<const C: usize>(
7979

8080
Ok(())
8181
}
82+
83+
/// Blur an image using a box blur filter multiple times to achieve a near gaussian blur
84+
///
85+
/// # Arguments
86+
///
87+
/// * `src` - The source image with shape (H, W, C).
88+
/// * `dst` - The destination image with shape (H, W, C).
89+
/// * `kernel_size` - The size of the kernel (kernel_x, kernel_y).
90+
/// * `sigma` - The sigma of the gaussian kernel, xy-ordered.
91+
///
92+
/// PRECONDITION: `src` and `dst` must have the same shape.
93+
pub fn box_blur_fast<const C: usize>(
94+
src: &Image<f32, C>,
95+
dst: &mut Image<f32, C>,
96+
sigma: (f32, f32),
97+
) -> Result<(), ImageError> {
98+
let half_kernel_x_sizes = kernels::box_blur_fast_kernels_1d(sigma.0, 3);
99+
let half_kernel_y_sizes = kernels::box_blur_fast_kernels_1d(sigma.1, 3);
100+
101+
let transposed_size = ImageSize {
102+
width: src.size().height,
103+
height: src.size().width,
104+
};
105+
106+
let mut input_img = src.clone();
107+
let mut transposed = Image::<f32, C>::from_size_val(transposed_size, 0.0)?;
108+
109+
for (half_kernel_x_size, half_kernel_y_size) in
110+
half_kernel_x_sizes.iter().zip(half_kernel_y_sizes.iter())
111+
{
112+
fast_horizontal_filter(&input_img, &mut transposed, *half_kernel_x_size)?;
113+
fast_horizontal_filter(&transposed, dst, *half_kernel_y_size)?;
114+
115+
input_img = dst.clone();
116+
}
117+
118+
Ok(())
119+
}
120+
121+
#[cfg(test)]
122+
mod tests {
123+
use super::*;
124+
125+
#[test]
126+
fn test_box_blur_fast() -> Result<(), ImageError> {
127+
let size = ImageSize {
128+
width: 5,
129+
height: 5,
130+
};
131+
132+
#[rustfmt::skip]
133+
let img = Image::new(
134+
size,
135+
(0..25).map(|x| x as f32).collect(),
136+
)?;
137+
let mut dst = Image::<_, 1>::from_size_val(size, 0.0)?;
138+
139+
box_blur_fast(&img, &mut dst, (0.5, 0.5))?;
140+
141+
#[rustfmt::skip]
142+
assert_eq!(
143+
dst.as_slice(),
144+
&[
145+
4.444444, 4.9259257, 5.7037034, 6.4814816, 6.962963,
146+
6.851851, 7.3333335, 8.111111, 8.888889, 9.370372,
147+
10.740741, 11.222222, 12.0, 12.777779, 13.259262,
148+
14.629628, 15.111112, 15.888888, 16.666666, 17.14815,
149+
17.037035, 17.518518, 18.296295, 19.074074, 19.555555,
150+
],
151+
);
152+
153+
Ok(())
154+
}
155+
156+
#[test]
157+
fn test_gaussian_blur() -> Result<(), ImageError> {
158+
let size = ImageSize {
159+
width: 5,
160+
height: 5,
161+
};
162+
163+
#[rustfmt::skip]
164+
let img = Image::new(
165+
size,
166+
(0..25).map(|x| x as f32).collect(),
167+
)?;
168+
169+
let mut dst = Image::<_, 1>::from_size_val(size, 0.0)?;
170+
171+
gaussian_blur(&img, &mut dst, (3, 3), (0.5, 0.5))?;
172+
173+
#[rustfmt::skip]
174+
assert_eq!(
175+
dst.as_slice(),
176+
&[
177+
0.57097936, 1.4260278, 2.3195207, 3.213014, 3.5739717,
178+
4.5739717, 5.999999, 7.0, 7.999999, 7.9349294,
179+
9.041435, 10.999999, 12.0, 12.999998, 12.402394,
180+
13.5089, 15.999998, 17.0, 17.999996, 16.86986,
181+
15.58594, 18.230816, 19.124311, 20.017801, 18.588936,
182+
]
183+
);
184+
Ok(())
185+
}
186+
}

crates/kornia-imgproc/src/filter/separable_filter.rs

+120
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,70 @@ pub fn separable_filter<const C: usize>(
8888
Ok(())
8989
}
9090

91+
/// Apply a fast filter horizontally using cumulative kernel
92+
///
93+
/// # Arguments
94+
///
95+
/// * `src` - The source image with shape (H, W, C).
96+
/// * `dst` - The destination image with transposed shape (W, H, C).
97+
/// * `half_kernel_x_size` - Half of the kernel at weight 1. The total size would be 2*this+1
98+
pub(crate) fn fast_horizontal_filter<const C: usize>(
99+
src: &Image<f32, C>,
100+
dst: &mut Image<f32, C>,
101+
half_kernel_x_size: usize,
102+
) -> Result<(), ImageError> {
103+
let src_data = src.as_slice();
104+
let dst_data = dst.as_slice_mut();
105+
let mut row_acc = [0.0; C];
106+
107+
let mut leftmost_pixel = [0.0; C];
108+
let mut rightmost_pixel = [0.0; C];
109+
110+
let pixels_between_first_last_cols = (src.cols() - 1) * C;
111+
let kernel_pix_offset_diffs: Vec<usize> =
112+
(0..half_kernel_x_size).map(|p| (p + 1) * C).collect();
113+
for (pix_offset, source_pixel) in src_data.iter().enumerate() {
114+
let ch = pix_offset % C;
115+
let rc = pix_offset / C;
116+
let c = rc % src.cols();
117+
let r = rc / src.cols();
118+
119+
let transposed_r = c;
120+
let transposed_c = r;
121+
let transposed_pix_offset = transposed_r * src.rows() * C + transposed_c * C + ch;
122+
123+
if c == 0 {
124+
row_acc[ch] = *source_pixel * (half_kernel_x_size + 1) as f32;
125+
for pix_diff in &kernel_pix_offset_diffs {
126+
row_acc[ch] += src_data[pix_offset + pix_diff]
127+
}
128+
leftmost_pixel[ch] = *source_pixel;
129+
rightmost_pixel[ch] = src_data[pix_offset + pixels_between_first_last_cols];
130+
} else {
131+
row_acc[ch] -= match c.checked_sub(half_kernel_x_size + 1) {
132+
Some(_) => {
133+
let prv_leftmost_pix_offset = pix_offset - C * (half_kernel_x_size + 1);
134+
src_data[prv_leftmost_pix_offset]
135+
}
136+
None => leftmost_pixel[ch],
137+
};
138+
139+
let rightmost_x = c + half_kernel_x_size;
140+
141+
row_acc[ch] += match rightmost_x {
142+
x if x < src.cols() => {
143+
let rightmost_pix_offset = pix_offset + C * half_kernel_x_size;
144+
src_data[rightmost_pix_offset]
145+
}
146+
_ => rightmost_pixel[ch],
147+
};
148+
}
149+
dst_data[transposed_pix_offset] = row_acc[ch] / (half_kernel_x_size * 2 + 1) as f32;
150+
}
151+
152+
Ok(())
153+
}
154+
91155
#[cfg(test)]
92156
mod tests {
93157
use super::*;
@@ -134,4 +198,60 @@ mod tests {
134198

135199
Ok(())
136200
}
201+
202+
#[test]
203+
fn test_fast_horizontal_filter() -> Result<(), ImageError> {
204+
let size = ImageSize {
205+
width: 5,
206+
height: 5,
207+
};
208+
209+
#[rustfmt::skip]
210+
let img = Image::new(
211+
size,
212+
vec![
213+
0.0, 0.0, 0.0, 0.0, 0.0,
214+
0.0, 0.0, 0.0, 0.0, 0.0,
215+
0.0, 0.0, 9.0, 0.0, 0.0,
216+
0.0, 0.0, 0.0, 0.0, 0.0,
217+
0.0, 0.0, 0.0, 0.0, 0.0,
218+
],
219+
)?;
220+
221+
let mut transposed = Image::<_, 1>::from_size_val(size, 0.0)?;
222+
223+
fast_horizontal_filter(&img, &mut transposed, 1)?;
224+
225+
#[rustfmt::skip]
226+
assert_eq!(
227+
transposed.as_slice(),
228+
&[
229+
0.0, 0.0, 0.0, 0.0, 0.0,
230+
0.0, 0.0, 3.0, 0.0, 0.0,
231+
0.0, 0.0, 3.0, 0.0, 0.0,
232+
0.0, 0.0, 3.0, 0.0, 0.0,
233+
0.0, 0.0, 0.0, 0.0, 0.0,
234+
]
235+
);
236+
237+
let mut dst = Image::<_, 1>::from_size_val(size, 0.0)?;
238+
239+
fast_horizontal_filter(&transposed, &mut dst, 1)?;
240+
241+
#[rustfmt::skip]
242+
assert_eq!(
243+
dst.as_slice(),
244+
&[
245+
0.0, 0.0, 0.0, 0.0, 0.0,
246+
0.0, 1.0, 1.0, 1.0, 0.0,
247+
0.0, 1.0, 1.0, 1.0, 0.0,
248+
0.0, 1.0, 1.0, 1.0, 0.0,
249+
0.0, 0.0, 0.0, 0.0, 0.0,
250+
]
251+
);
252+
let xsum = dst.as_slice().iter().sum::<f32>();
253+
assert_eq!(xsum, 9.0);
254+
255+
Ok(())
256+
}
137257
}

0 commit comments

Comments
 (0)