I've implemented the stackblur algorithm (by Mario Klingemann) in Rust, and rewrote the horizontal pass to use iterators rather than indexing. However, the blur needs to be run twice to achieve full strength, comparing against GIMP. Doubling the radius introduces halo-ing.
/// Performs a horizontal pass of stackblur.
/// Input is expected to be in linear RGB color space.
/// Needs to be ran twice for full effect!
pub fn blur_horiz(src: &mut [u32], width: NonZeroUsize, radius: NonZeroU8) {
let width = width.get();
let radius = u32::from(min(radius.get() | 1, 255));
let r = radius as usize;
src.chunks_exact_mut(width).for_each(|row| {
let first = *row.first().unwrap();
let mut last = *row.last().unwrap();
let mut queue_r = VecDeque::with_capacity(r);
let mut queue_g = VecDeque::with_capacity(r);
let mut queue_b = VecDeque::with_capacity(r);
// fill with left edge pixel
for v in iter::repeat(first).take(r / 2 + 1) {
queue_r.push_back(red(v));
queue_g.push_back(green(v));
queue_b.push_back(blue(v));
}
// fill with starting pixels
for v in row.iter().copied().chain(iter::repeat(last)).take(r / 2) {
queue_r.push_back(red(v));
queue_g.push_back(green(v));
queue_b.push_back(blue(v));
}
debug_assert_eq!(queue_r.len(), r);
let mut row_iter = peek_nth(row.iter_mut());
while let Some(px) = row_iter.next() {
// set pixel
*px = pixel(
queue_r.iter().sum::<u32>() / radius,
queue_g.iter().sum::<u32>() / radius,
queue_b.iter().sum::<u32>() / radius,
);
// drop left edge of kernel
let _ = queue_r.pop_front();
let _ = queue_g.pop_front();
let _ = queue_b.pop_front();
// add right edge of kernel
let next = **row_iter.peek_nth(r / 2).unwrap_or(&&mut last);
queue_r.push_back(red(next));
queue_g.push_back(green(next));
queue_b.push_back(blue(next));
}
});
}
Result after running blur_horiz twice with radius=15:
Result after running blur_horiz once with radius=30:
Note that what you have implemented is a regular box filter, not stackblur (which uses a triangle filter). Also, filtering twice with a box of radius R
is equivalent to filtering once with a triangle of radius 2*R
, which explains why you get the expected result when running blur_horiz
twice.