Skip to content

Gaussian Blur is not actually Gaussian #786

Closed

Description

@lemoncove
using (var image = new Image<Rgba32>(128, 128))
{
	image.Mutate(ctx => ctx
		.Fill(Rgba32.Black)
		.Fill(Rgba32.White, new EllipsePolygon(64, 64, 64, 64)));

	image.Mutate(ctx => ctx.GaussianBlur(12));

	using (var stream = new FileStream("image.png", FileMode.Create))
	{
		image.SaveAsPng(stream);
	}
}

Mutating an image with a Gaussian blur yields a blur that isn't quite Gaussian, much more closely resembling a box blur. I implemented a mathematically accurate Gaussian blur that factors in every pixel in the image (obviously overkill and inefficient in a 0-255 color space, but completely correct), and the results are as follows:

image

Note the similarity between ImageSharp's Gaussian blur and its box blur.

I would fix this myself and submit a PR but I genuinely have no clue why this happens. All of the processing regarding the generation of the kernel and the underlying Gaussian function seems to be correct and yields the same values as my own implementation.

Notes: Using ImageSharp v1.0.0-beta0005 on .NET Framework 4.7.1 on a Windows 10 64-bit OS.

Activity

  1. JimBobSquarePants commented on Dec 10, 2018

    @JimBobSquarePants
    Member

    Hi there,

    Can you do me a favour and compare against the latest dev build? We’ve updated the convolution code there so it would be good to see if anything has changed.

    Cheers!

  2. lemoncove commented on Dec 10, 2018

    @lemoncove
    ContributorAuthor

    Unfortunately, results seem to be identical in v1.0.0-dev002237.

  3. JimBobSquarePants commented on Dec 10, 2018

    @JimBobSquarePants
    Member

    Would it be possible to share your code?

  4. lemoncove commented on Dec 10, 2018

    @lemoncove
    ContributorAuthor

    Certainly, but it's messy and doesn't use any of ImageSharp's DenseMatrix or convolution features.

    internal class MathematicalGaussianBlurProcessor : IImageProcessor<Rgba32>
    {
        public float Sigma { get; }
    
        public MathematicalGaussianBlurProcessor(float sigma) => this.Sigma = sigma;
    
        /// <summary>A function in the form ae^(-(x - b)² / 2c²).</summary>
        /// <param name="a">The height of the curve's peak.</param>
        /// <param name="b">The position of the center of the curve's peak (or the mean in a normal distribution).</param>
        /// <param name="c">The standard deviation of the curve, affecting the width of the curve.</param>
        private double GaussianFunction(double x, double a, double b, double c)
            => a * Math.Exp(-(x - b) * (x - b) / (2 * c * c));
    
        public void Apply(Image<Rgba32> source, Rectangle sourceRectangle)
        {
            // Generate a table for the sake of speed.
            // The table will only need to be as large as the largest dimension of the image because the Gaussian function is symmetrical.
            double[] gaussianTable = new double[Math.Max(source.Width, source.Height)];
    
            for (int i = 0; i < gaussianTable.Length; i++)
            {
                // a is 1 because the weights will be normalized to sum to 1 when convoluting anyway.
                // The normalization will differ depending on the actual values so pre-calculating it is useless.
                gaussianTable[i] = this.GaussianFunction(i, 1, 0, this.Sigma);
            }
    
            // Clone the source for read-only access to the original pixels.
            Image<Rgba32> input = source.Clone();
            Process(input, source, gaussianTable, false);
    
            // Apply the convolution again in the horizontal direction.
            // This is possible because the Gaussian function is directionally seperable.
            input = source.Clone();
            Process(input, source, gaussianTable, true);
        }
    
        private void Process(Image<Rgba32> input, Image<Rgba32> output, double[] gaussianTable, bool horizontal)
        {
            int iSize = horizontal ? input.Height : input.Width;
            int jSize = horizontal ? input.Width : input.Height;
    
            for (int i = 0; i < iSize; i++)
            {
                for (int j = 0; j < jSize; j++)
                {
                    // Doubles are used instead of floats for accuracy.
    
                    double weightSum = 0;
    
                    double r = 0;
                    double g = 0;
                    double b = 0;
                    double a = 0;
    
                    for (int k = 0; k < jSize; k++)
                    {
                        // Reflect k along i.
                        double weight = gaussianTable[Math.Abs(j - k)];
    
                        weightSum += weight;
    
                        Rgba32 pixel;
                        if (horizontal)
                        {
                            pixel = input[k, i];
                        }
                        else
                        {
                            pixel = input[i, k];
                        }
    
                        r += weight * pixel.R;
                        g += weight * pixel.G;
                        b += weight * pixel.B;
                        a += weight * pixel.A;
                    }
    
                    // Normalize all weights such that they sum to 1, ergo the channel value is normalized to 255.
                    // Make sure to round, otherwise there will be intense noise from floating point rounding errors.
                    output[i, j] = new Rgba32(
                        (byte)Math.Round(r / weightSum),
                        (byte)Math.Round(g / weightSum),
                        (byte)Math.Round(b / weightSum),
                        (byte)Math.Round(a / weightSum));
                }
            }
        }
    }

    Again, this doesn't use a kernel, instead factoring every relevant pixel.

    Also note that the Gaussian function in my implementation is different than that of ImageSharp's. Compare the two:

    mine vs. ImageSharp

    ImageSharp's special a calculation yields a probability density function which has an area of 1, but this is negated in this particular use of the Gaussian function thanks to the normalization (see here for comparison).

  5. JimBobSquarePants commented on Dec 12, 2018

    @JimBobSquarePants
    Member

    Thanks for that, appreciate it..

    Reading through your code it looks to me that the difference is simply the sampling radius per pixel. You sample the entire image while we sample (2 * sigma) + 1.

    Btw I'm not very good at maths so you lost me with the graphs.

    This was a good read and helped make sense of what you are saying. http://dev.theomader.com/gaussian-kernel-calculator/

  6. iamcarbon commented on Dec 12, 2018

    @iamcarbon
    Contributor

    What does SkiaSharp / CSS do?

  7. JimBobSquarePants commented on Dec 12, 2018

    @JimBobSquarePants
    Member

    CSS makes an absolute meal of it, oversampling edge pixels but produces a similar blur to @Poyo-SSB method.

  8. lemoncove commented on Dec 12, 2018

    @lemoncove
    ContributorAuthor

    I do sample the entire image, but that's not what yields the difference in output—using the Gaussian blur function in an application like Photoshop or Paint.NET yields a similar result to mine. It appears that the shape of the actual kernel is different despite what the code seems to output.

  9. JimBobSquarePants commented on Dec 12, 2018

    @JimBobSquarePants
    Member

    It appears that the shape of the actual kernel is different despite what the code seems to output.

    I think you're correct. The calculated kernel dimensions are too small. I rewrote the kernel generation code based on the guide here http://chemaguerra.com/gaussian-filter-radius/ to preserve a minimum kernel radius of Ceiling(sigma * 3) and here is the result.

    imageshouldapplygaussianblur_rgba32_solid128x128_ 0 0 0 255

  10. lemoncove commented on Dec 12, 2018

    @lemoncove
    ContributorAuthor

    Ah, brilliant! Looks like that's the problem solved, then.

  11. JimBobSquarePants commented on Dec 12, 2018

    @JimBobSquarePants
    Member

    I’ll create a PR shortly

  12. added a commit that references this issue on Dec 12, 2018
    1b2e794
  13. added a commit that references this issue on Dec 16, 2018
    cbfb8dc
  14. Alerinos commented on Feb 11, 2023

    @Alerinos

    @JimBobSquarePants I have a similar problem, the blur in the browser looks completely different.

    image.Image.Mutate(x => x.GaussianBlur(20));
    

    image
    css filter: blur(20px);
    image

    In the browser, the effect looks smoother.
    I have the latest version, is there any solution for this?


    Edit:
    I found the problem, I used the webp file format.
    Webp reduces file size by 10 times. Is it possible to apply an effect so that the blur quality is nice and the file weighs less?
    Unfortunately, mobile devices have a blur performance problem at 20px.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions