2

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

congard
  • 945
  • 2
  • 10
  • 28
  • Looks like you are trying implement the shader code using Java and CPU. – Victor Gubin Jun 19 '18 at 09:02
  • @VictorGubin, no, it's just a blur kernel generation for the shader, written in glsl – congard Jun 19 '18 at 09:04
  • 1
    `float mean = size / 2;` is an invitation for trouble (should use float arithmetic here) – Gyro Gearless Jun 19 '18 at 09:12
  • @GyroGearless, i did not notice, thanks – congard Jun 19 '18 at 09:16
  • The question is still open – congard Jun 19 '18 at 12:08
  • 1
    Not sure I'm following, you just need to generate a 1D vector of length `5` corresponding to the middle column or middle row of your array. Then you can use that as kernel for your two (horizontal and vertical) rendering passes. – filippo Jun 19 '18 at 13:03
  • 1
    or in other words you need to calculate a 1D gaussian, should be pretty simple if you already did the 2D version – filippo Jun 19 '18 at 13:05
  • Also, in your shader, you can exploit the symmetry better: Instead of `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];` you can do `result += ( texture(image, texCoords + vec2(texOffset.x * i, 0.0)).rgb + texture(image, texCoords - vec2(texOffset.x * i, 0.0)).rgb ) * weight[i];` – Cris Luengo Jun 19 '18 at 17:23
  • 1
    Regarding your last question, here I explain about Gaussian filtering and about separating the 2D kernel into 1D kernels: https://www.crisluengo.net/index.php/archives/22 – Cris Luengo Jun 19 '18 at 17:25

2 Answers2

4

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).

Cris Luengo
  • 55,762
  • 10
  • 62
  • 120
1

So, I came to the final answer. Thanks to the picture I found and your advices, I finally wrote the final function for getting 1D two-pass kernel:

public static float[] getTwoPassKernelf(int size, float sigma) {
    float[] kernel = new float[size];
    float[][] fkernel = getKernelf(size, sigma);

    for (int i = 0; i < size; i++) {
        kernel[i] = (float) Math.sqrt(fkernel[i][i]);
    }

    return kernel;
}

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;
}

In conclusion, the shader needs to transfer not all the kernel, but its part, including the center (in my case). Thus, the core that I cited in the question itself was not 5x5 but 10x10.

For example: getTwoPassKernelf(10, 2f) returns

0.008890575, 0.027384898, 0.065692954, 0.1227306, 0.17857197, 0.20234855, 0.17857197, 0.1227306, 0.065692954, 0.027384898

but in my case I must took only 0.20234855, 0.17857197, 0.1227306, 0.065692954, 0.027384898 - this is just about what the book gives

A picture explaining the essence of obtaining a kernel for a two-pass rendering

A picture explaining the essence of obtaining a kernel for a two-pass rendering

Selected - the squares of the elements of the kernel

I hope the answer will be useful for someone

congard
  • 945
  • 2
  • 10
  • 28
  • 2
    OK but that's pretty mysterious, why not just make a 1D Gaussian kernel in the first place? The whole 2D step is unnecessary. It's *easier* to make the 1D kernel – harold Jun 19 '18 at 18:25
  • 2
    This makes no sense. The diagonal is a 1D kernel, but so is the horizontal or vertical. The 2D Gaussian is perfectly isotropic. The samples along the diagonal, since they are spaced further away, will lead to a 1D kernel with a smaller sigma (factor `sqrt(2)`). – Cris Luengo Jun 19 '18 at 18:56