Gaussian Blur

I wanted to implement a Gaussian Blur algorithm in Xojo. As an example I use this Swift code:

// Dependency: SwiftImage

// Usage: let outputImage = GaussianBlur.createBlurredImage(radius: 5, image: UIImage(named: "IMAGE_NAME")!)

final class GaussianBlur {

    /// Returns a gaussian blurred image

    /// - Parameters:

    ///   - radius: This relates to the strength of the blur. The kernel will be 1 + (2 * radius) in width/height to ensure center pixel exists.

    ///   - image: The source image

    /// - Returns: Returns a SwiftImage (UIImage can be extracted from this object)

    static func createBlurredImage(radius: Int, image: UIImage) -> Image<RGBA<UInt8>> {

        let inputImage = Image<RGBA<UInt8>>(uiImage: image)

        var outputImage = Image<RGBA<UInt8>>(uiImage: image)




        // We scale the sigma value in proportion to the radius

        // Setting the minimum standard deviation as a baseline

        let sigma = max(Double(radius / 2), 1)




        // Enforces odd width kernel which ensures a center pixel is always available

        let kernelWidth = (2 * radius) + 1




        // Initializing the 2D array for the kernel

        var kernel = Array(repeating: Array(repeating: 0.0, count: kernelWidth), count: kernelWidth)

        var sum = 0.0




        // Populate every position in the kernel with the respective Gaussian distribution value

        // Remember that x and y represent how far we are away from the CENTER pixel

        for x in -radius...radius {

            for y in -radius...radius {

                let exponentNumerator = Double(-(x * x + y * y))

                let exponentDenominator = (2 * sigma * sigma)




                let eExpression = pow(M_E, exponentNumerator / exponentDenominator)

                let kernelValue = (eExpression / (2 * Double.pi * sigma * sigma))




                // We add radius to the indices to prevent out of bound issues because x and y can be negative

                kernel[x + radius][y + radius] = kernelValue

                sum += kernelValue

            }

        }




        // Normalize the kernel

        // This ensures that all of the values in the kernel together add up to 1

        for x in 0..<kernelWidth {

            for y in 0..<kernelWidth {

                kernel[x][y] /= sum

            }

        }




        // Ignoring the edges for ease of implementation

        // This will cause a thin border around the image that won't be processed

        for x in radius..<(inputImage.width - radius) {

            for y in radius..<(inputImage.height - radius) {




                var redValue = 0.0

                var greenValue = 0.0

                var blueValue = 0.0




                // This is the convolution step

                // We run the kernel over this grouping of pixels centered around the pixel at (x,y)

                for kernelX in -radius...radius {

                    for kernelY in -radius...radius {




                        // Load the weight for this pixel from the convolution matrix

                        let kernelValue = kernel[kernelX + radius][kernelY + radius]




                        // Multiply each channel by the weight of the pixel as specified by the kernel

                        redValue += Double(inputImage[x - kernelX, y - kernelY].red) * kernelValue

                        greenValue += Double(inputImage[x - kernelX, y - kernelY].green) * kernelValue

                        blueValue += Double(inputImage[x - kernelX, y - kernelY].blue) * kernelValue

                    }

                }




                // New RGB value for output image at position (x,y)

                outputImage[x,y].red = UInt8(redValue)

                outputImage[x,y].green = UInt8((greenValue))

                outputImage[x,y].blue = UInt8(blueValue)

            }

        }




        return outputImage

    }

My Xojo port:

Function GBlur(Radius as Double, InputPicture as Picture) As Picture
  Const PI = 3.141592
  Const E = 2.71828
  
  Var InSurf As RGBSurface = InputPicture.RGBSurface
  
  Var OutputPicture As Picture
  
  OutputPicture = New Picture(InputPicture.Width, InputPicture.Height,32)
  
  Var OutSurf As RGBSurface = OutputPicture.RGBSurface
  
  Var Sigma As Double = Max(Radius / 2, 1)
  Var KernelWidth As Integer = ( 2 * Radius) + 1
  
  
  
  Var Kernel(-1,-1) As Double
  
  
  Kernel.ResizeTo(KernelWidth,KernelWidth)
  
  Var Sum As Double = 0.0
  
  For X As Integer = -Radius To Radius
    For Y As Integer = -Radius To Radius
      
      
      Var ExponentNumerator As Double = -(x * x + y * y)
      Var ExponentDenominator As Double = (2 * Sigma * Sigma)
      
      Var eExpression As Double = Pow(E, exponentNumerator / exponentDenominator)
      Var KernelValue As Double = (eExpression / (2 * PI * sigma * sigma))
      Kernel(X + Radius, Y + Radius) = KernelValue
      
      Sum = Sum + KernelValue
    Next
  Next
  
  For X As Integer = 0 To KernelWidth
    For Y As Integer = 0 To KernelWidth
      Kernel(X,Y) = Kernel(X,Y) / Sum
    Next
  Next
  
  For KernelX As Integer = Radius To InputPicture.Width - Radius
    For KernelY As Integer = Radius To InputPicture.Height - Radius
      Var RedValue As Double = 0.0
      Var GreenValue As Double = 0.0
      Var BlueValue As Double = 0.0
      
      Var KernelValue As Double = Kernel(KernelX + radius, KernelY + radius)
      
      RedValue = RedValue + InSurf.Pixel(KernelX, KernelY).Red * KernelValue
      GreenValue = GreenValue + InSurf.Pixel(KernelX, KernelY).Green * KernelValue
      BlueValue = BlueValue + InSurf.Pixel(KernelX, KernelY).Blue * KernelValue
      
      
      OutSurf.Pixel(KernelX, KernelY) = RGB(RedValue,GreenValue,BlueValue)
      
    Next
  Next
  
  Return OutputPicture
  
  
End Function

This code looks at a specific pixel (center pixel) and calculates the color values of the output pixel by examining the pixels around the center pixel. This is the Radius parameter. A Radius of 5 means that X±5 to X+5 and Y-5 to Y+5 gets examined (this is the ‘-Radius to Radius’ statement). The multidim Kernel array keeps track of this. I know that some vars should be integer instead of double but I will fix this later.

My Xojo code compiles… but I get an ‘out of bounds’ error while iterating over the kernel array in the last for-next loop. I can’t find where it goes wrong. I know this is a bit complex but anybody who can help me out?

First of all… Oh boy, this is inefficient, look into separable kernels for things like this (I had a form of blur that would take close to 10 minutes on a 20 megapixel image), once I’d switched over to separable, it took less than 2 seconds. There was still room for improvement!

Secondly, I believe that your outofbounds is because the process is in reverse, you want to loop through each pixel of the image and pick out the surrounding values, adding them to the central pixel using a predetermined weight.

Look at your code, it seems like your loop round the kernelSize and then trying to read all the pixels of the image, for which there are not enough weights.

I can’t say it is inefficient or not - IMHO for a blur you need to look at an individual pixel and its neighbors, and determine the weight, there is no other way or is it?

As Sam wrote, this is really slow.

Anyway, you missed the inner loops; this should work:

Const PI = 3.141592
Const E = 2.71828

Var InSurf As RGBSurface = InputPicture.RGBSurface

Var OutputPicture As Picture

OutputPicture = New Picture(InputPicture.Width, InputPicture.Height,32)

Var OutSurf As RGBSurface = OutputPicture.RGBSurface

Var Sigma As Double = Max(Radius / 2, 1)
Var KernelWidth As Integer = ( 2 * Radius) + 1



Var Kernel(-1,-1) As Double


Kernel.ResizeTo(KernelWidth,KernelWidth)

Var Sum As Double = 0.0

For X As Integer = -Radius To Radius
  For Y As Integer = -Radius To Radius
    
    
    Var ExponentNumerator As Double = -(x * x + y * y)
    Var ExponentDenominator As Double = (2 * Sigma * Sigma)
    
    Var eExpression As Double = Pow(E, exponentNumerator / exponentDenominator)
    Var KernelValue As Double = (eExpression / (2 * PI * sigma * sigma))
    Kernel(X + Radius, Y + Radius) = KernelValue
    
    Sum = Sum + KernelValue
  Next
Next

For X As Integer = 0 To KernelWidth
  For Y As Integer = 0 To KernelWidth
    Kernel(X,Y) = Kernel(X,Y) / Sum
  Next
Next



Var RedValue   As Double = 0.0
Var GreenValue As Double = 0.0
Var BlueValue  As Double = 0.0

Var KernelValue As Double

For X As Integer = Radius To InputPicture.Width - Radius
  For Y As Integer = Radius To InputPicture.Height - Radius
    
    RedValue   = 0.0
    GreenValue = 0.0
    BlueValue  = 0.0
    
    For KernelX As Integer = -Radius To Radius
      For KernelY As Integer = -Radius To Radius
        
        KernelValue = Kernel(KernelX + radius, KernelY + radius)
        
        RedValue   = RedValue   + InSurf.Pixel(x - KernelX, y - KernelY).Red   * KernelValue
        GreenValue = GreenValue + InSurf.Pixel(x - KernelX, y - KernelY).Green * KernelValue
        BlueValue  = BlueValue  + InSurf.Pixel(x - KernelX, y - KernelY).Blue  * KernelValue
      Next
    Next
    
    OutSurf.Pixel(x, y) = RGB(RedValue, GreenValue, BlueValue)
  Next
Next

Return OutputPicture

1 Like

Ah, I see. Thanks. My purpose with this code was to see how Gaussian blur actually works. What would be a better approach?

For learning this is perfectly fine.

A box blur is much faster to compute than a gaussian blur and can produce a gaussian blur effect by performing the blur 3 times using 1/3 of the radius size.

The Gaussian blur took on a 1100*600 image with radius 3 16,3 secs, the box blur with radius 3 1,1 sec…

Did you perform the box blur once with a radius of 3 or did you perform a box blur three times with a radius of 1?

Once with radius of 3

That will look quite different to a gaussian blur.

It will look more like a gaussian blur if you perform the box blur 3 times with the gaussian radius / 3.

I wrote HDRtist NX - Powerful HDR software for your Mac in Xojo, the image manipulation is mostly done in CIKL, so I have a little experience with Gaussian filtering of images.

Hence why I suggested you research separable kernels, Intel has some good documentation on this process.