1

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

  • 2
    Note that what you have implemented is a regular box filter, not stackblur (which uses a triangle filter). – Jmb May 19 '21 at 07:58
  • 1
    What does “achieve full strength” mean? Could you please clarify what you mean? – Cris Luengo May 19 '21 at 14:00
  • @CrisLuengo When compared to gaussian blur in e.g. GIMP. I'll edit to add clarification. – owenthewizard May 19 '21 at 15:03
  • @Jmb Oh, I think that explains it then? Since repeating passes of box blur approximate gaussian blur. – owenthewizard May 19 '21 at 15:05
  • Gimp probably sizes its filter according to a sigma parameter, not a radius. Also, you create a filter whose full size is "radius", so your "radius" parameter is really the full size of the filter, not an actual radius. – Cris Luengo May 19 '21 at 15:57
  • Also, 2x a box filter is a triangle filter. So running your filter twice should produce the same results as stackblur (with 2*radius-1 as the parameter). – Cris Luengo May 19 '21 at 16:06
  • @Jmb I've corrected my code to use a triangle filter and it produces the correct output now. You can leave your comment as an answer and I'll mark it as resolved. – owenthewizard May 19 '21 at 17:26

1 Answers1

0

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.

Jmb
  • 18,893
  • 2
  • 28
  • 55