image icon indicating copy to clipboard operation
image copied to clipboard

Blur for images with alpha is incorrect, bleeds color from transparent pixels

Open Shnatsel opened this issue 1 year ago • 5 comments

With this test image the blur implementation shows artifacts:

blur-test-input

It's just a white rectangle in the middle, surrounded by fully transparent pixels.

Gaussian blur in image bleeds color from fully transparent pixels into the white rectangle, resulting in the following image:

blur-test-input fast

The fully transparent pixels have R set to 255 while all the other channels are set to 0, and that red color bleeds into the white rectangle.

The expected blur as produced by GIMP is like this - blurred white rectangle without any red color:

blurred-gimp-20px

To avoid such artifacts, the pixel's contribution to the color change should be weighted by its alpha channel value. Fully transparent pixels should get multiplied by 0 and not contribute to the changes in non-alpha channels at all.

Code used for testing:

use std::env;
use std::error::Error;
use std::path::Path;
use std::process;

fn main() -> Result<(), Box<dyn Error>> {
    // Collect command-line arguments
    let args: Vec<String> = env::args().collect();

    // Ensure that we have 2 CLI arguments
    if args.len() != 3 {
        eprintln!("Usage: {} <path> <radius>", args[0]);
        process::exit(1);
    }

    let radius: f32 = args[2].parse()?;

    // Load the input image
    let path_str = &args[1];
    let path = Path::new(path_str);
    let input = image::open(path)?;

    // Correct but slow Gaussian blur
    let mut out_path_gauss = path.to_owned();
    out_path_gauss.set_extension("gaussian.png");

    let blurred = input.blur(radius);
    blurred.save(out_path_gauss)?;

    Ok(())
}

Originally found in https://github.com/image-rs/image/pull/2302#issuecomment-2346108041, moving it to the issue tracker so that it doesn't get lost.

Tested on image from git on commit 98ceb7197f5d8469b9b2364fbf9d671ce161ae97

Shnatsel avatar Sep 13 '24 13:09 Shnatsel

Wow, good catch! I think this is a general issue coming from image::imageops::sample::horizontal_sample which basically affects all filters defined by convolution with 2d functions. I think your suggested fix (using the alpha channel as weight) is correct and I am willing to implement this as long as this is the suggested fix.

torfmaster avatar Sep 16 '24 09:09 torfmaster

I am not 100% certain this is the correct fix. I am not a blur expert and I may be missing something. It's probably worth researching this.

Or, if you want a shortcut on the research, here's what ChatGPT says about it: https://chatgpt.com/share/66e8069d-6198-800f-8ca4-dfd8fb5360ae

Shnatsel avatar Sep 16 '24 10:09 Shnatsel

This StackOverflow answer seems to suggest the same thing, with the contributions being weighted by alpha naturally if the RBG values are multiplied by alpha first: https://computergraphics.stackexchange.com/a/5517

Shnatsel avatar Sep 16 '24 10:09 Shnatsel

Yes, premultiplied alpha color space is the right answer here.

kornelski avatar Sep 16 '24 13:09 kornelski

I've added utilities for premultiplying by alpha:

https://github.com/image-rs/image/blob/ee7c5d979cb503ed0a78b984542af990dd7ecd87/src/imageops/resize.rs#L156-L197

However, they currently only work for DynamicImage because Subpixel trait is not expressive enough and does not provide the necessary bounds.

Shnatsel avatar Nov 18 '25 19:11 Shnatsel