Search code examples
image-processingrustblurgaussianblur

Why is my gaussian blur approximation half strength?


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));
        }
    });
}

[Full Code]

Result after running blur_horiz twice with radius=15: Image

Result after running blur_horiz once with radius=30: Image


Solution

  • 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.