Search code examples
javaopenglimage-processingblurgaussianblur

Blur kernel separation for two pass rendering


It is necessary for me to calculate the kernel for two-pass blur. Suppose there is a blur kernel 5x5, which is generated as follows:

public static float[][] getKernelf(int size, float sigma) {
    float[][] kernel = new float[size][size];
    int mean = size / 2;
    float sum = 0; // For accumulating the kernel values
    for (int x = 0; x < size; x++)  {
        for (int y = 0; y < size; y++) {
            kernel[x][y] = (float) (Math.exp(-0.5 * (Math.pow((x - mean) / sigma, 2.0) + 
                    Math.pow((y - mean) / sigma, 2.0))) / (2 * Geometry.PI * sigma * sigma));
            // Accumulate the kernel values
            sum += kernel[x][y];
        }
    }

    // Normalize the kernel
    for (int x = 0; x < size; x++) 
        for (int y = 0; y < size; y++)
            kernel[x][y] /= sum;

    return kernel;
}

With sigma = 1 and size = 5 we have the following kernel:

0.0029690173  0.013306212  0.021938235  0.013306212  0.0029690173
0.013306212   0.059634306  0.09832035   0.059634306  0.013306212
0.021938235   0.09832035   0.16210285   0.09832035   0.021938235
0.013306212   0.059634306  0.09832035   0.059634306  0.013306212
0.0029690173  0.013306212  0.021938235  0.013306212  0.0029690173

The question is how to bring this kernel into a view suitable for two-pass rendering (horizontally and vertically, real time rendering in OpenGL)

EDIT:

Kernel given by book: 0.227027 0.1945946 0.1216216 0.054054 0.016216

My full fragment_blur_shader.glsl:

#version 330

out vec4 fragColor;
in vec2 texCoords;

uniform sampler2D image;
uniform bool isHorizontal;
uniform float weight[5] = float[] (0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216);

void main() {
    vec2 texOffset = 1.0 / textureSize(image, 0); // gets size of single texel
    vec3 result = texture(image, texCoords).rgb * weight[0]; // current fragment’s contribution
    if(isHorizontal) {
        for(int i = 1; i < 5; i++) {
            result += texture(image, texCoords + vec2(texOffset.x * i, 0.0)).rgb * weight[i];
            result += texture(image, texCoords - vec2(texOffset.x * i, 0.0)).rgb * weight[i];
        }
    } else {
        for(int i = 1; i < 5; ++i) {
            result += texture(image, texCoords + vec2(0.0, texOffset.y * i)).rgb * weight[i];
            result += texture(image, texCoords - vec2(0.0, texOffset.y * i)).rgb * weight[i];
        }
    }
    fragColor = vec4(result, 1.0);
}

Also I found the following picture demonstrating the receipt of a 2D kernel from 1D kernel for a two-stage rendering:

Example of a 7x7 convolution kernel done using the two pass separable filter approach

But I have no idea how to get this 1D core. I am hope for your help.

EDIT:

I understood how to get the kernel I needed, but I still do not understand why the book gives this kernel in this form


Solution

  • To go from the 2D Gaussian kernel that you have:

    float[][] kernel = new float[size][size];
    int mean = size / 2;
    float sum = 0; // For accumulating the kernel values
    for (int x = 0; x < size; x++)  {
        for (int y = 0; y < size; y++) {
            kernel[x][y] = (float) (Math.exp(-0.5 * (Math.pow((x - mean) / sigma, 2.0) + 
                    Math.pow((y - mean) / sigma, 2.0))) / (2 * Geometry.PI * sigma * sigma));
            // Accumulate the kernel values
            sum += kernel[x][y];
        }
    }
    
    // Normalize the kernel
    for (int x = 0; x < size; x++) 
        for (int y = 0; y < size; y++)
            kernel[x][y] /= sum;
    

    to a 1D Gaussian kernel, simply remove the loops over y (and all further references to y):

    float[] kernel = new float[size];
    int mean = size / 2;
    float sum = 0; // For accumulating the kernel values
    for (int x = 0; x < size; x++)  {
        kernel[x] = (float) Math.exp(-0.5 * Math.pow((x - mean) / sigma, 2.0));
        // Accumulate the kernel values
        sum += kernel[x];
    }
    
    // Normalize the kernel
    for (int x = 0; x < size; x++) 
        kernel[x] /= sum;
    

    Some more hints:

    • As you already noticed, your shader uses only half of this kernel (the part for x-mean >= 0). Since it is symmetric, the other half is redundant.

    • No need to scale the kernel values with 2 * Geometry.PI * sigma * sigma, because you normalize the kernel later, this scaling is irrelevant.

    • Multiplying this kernel with its transposed yields the 2D kernel that the first bit of code produces (as shown in the figure that you included in the question).