Shader 高斯模糊(Gaussion Blur)

7,052 阅读5分钟

高斯模糊的算法可以看阮一峰写的这篇文章,高斯模糊之所以叫高斯模糊,是因为它运用了高斯的正态分布的密度函数:

一维形式:

二维形式:

相关计算步骤:

我们在 shader 中还原一个拼多多版简易版的高斯模糊算法:

void main() {
    vec2 st = gl_FragCoord / iResulution;

    vec4 color = vec4(0.0);
    const int coreSize = 3;         // 取 3x3 像素网格
    vec2 texelOffset = vec2(1.)/vec2(375., 667.);  // 每个素纹的间距

    float kernel[9];                // 卷积核,每个像素点的权重
    kernel[0] = 1.; kernel[1] = 2.; kernel[2] = 1.;
    kernel[3] = 2.; kernel[4] = 4.; kernel[5] = 2.;
    kernel[6] = 1.; kernel[7] = 2.; kernel[8] = 1.;

    int index = 0;
    for(int y=0; y<coreSize; y++)
    {
        for(int x = 0; x<coreSize; x++)
        {
            // 这是核心,依次取9个像素点的色值
            vec4 currentColor = texture2D(inputImageTexture, st + vec2(float(-1+x)*texelOffset.x, float(-1+y)*texelOffset.y));

            // 将颜色值和权重相乘,就是卷积运算
            if (index == 0) { color += currentColor * kernel[0]; }
            else if (index == 1) { color += currentColor * kernel[1]; }
            else if (index == 2) { color += currentColor * kernel[2]; }
            else if (index == 3) { color += currentColor * kernel[3]; }
            else if (index == 4) { color += currentColor * kernel[4]; }
            else if (index == 5) { color += currentColor * kernel[5]; }
            else if (index == 6) { color += currentColor * kernel[6]; }
            else if (index == 7) { color += currentColor * kernel[7]; }
            else if (index == 8) { color += currentColor * kernel[8]; }

            index++;
        }
    }
    
    // 除以权重总和
    color /= 16.0;
    gl_FragColor=color;
}

效果不明显,因为我们的权重没有很准确,而且我们的图片尺寸比较大(750x1334),如果只对 3x3 的网格进行模糊,效果不够明显,下面优化下:

void main() {
    vec2 st = gl_FragCoord / iResulution;

    vec4 color = vec4(0.0);
    const int coreSize = 3;
    
    // 把素纹间隔放大,这里做法比较粗暴。合理的做法是把 3x3 变成 9x9 或更大
    vec2 texelOffset = vec2(2.)/vec2(375., 667.);

    // 代入高斯正态分布函数计算出来的权重值
    float kernel[9];
    kernel[0] = .0947416; kernel[1] = .118318; kernel[2] = .0947416;
    kernel[3] = .118318; kernel[4] = .147761; kernel[5] = .118318;
    kernel[6] = .0947416; kernel[7] = .118318; kernel[8] = .0947416;

    int index = 0;
    for(int y=0; y<coreSize; y++)
    {
        for(int x = 0; x<coreSize; x++)
        {
            vec4 currentColor = texture2D(inputImageTexture, st + vec2(float(-1+x)*texelOffset.x, float(-1+y)*texelOffset.y));

            if (index == 0) { color += currentColor * kernel[0]; }
            else if (index == 1) { color += currentColor * kernel[1]; }
            else if (index == 2) { color += currentColor * kernel[2]; }
            else if (index == 3) { color += currentColor * kernel[3]; }
            else if (index == 4) { color += currentColor * kernel[4]; }
            else if (index == 5) { color += currentColor * kernel[5]; }
            else if (index == 6) { color += currentColor * kernel[6]; }
            else if (index == 7) { color += currentColor * kernel[7]; }
            else if (index == 8) { color += currentColor * kernel[8]; }

            index++;
        }
    }
    // 上面的权重已经进行了加权平均,所以这一步不需要了
    // color /= 16.0;
    gl_FragColor = color;
}

只要我们把 3x3 的网格放大,如 9x9 / 16x16,或者直接放大像素间距,都可以增加模糊效果。

然而上面的实现方式性能是比较差的。因为遍历的成本太高了。通常拆成两个一维向量,这样时间复杂度就由NxNxWxH下降为2xNxWxH(W为图像的宽,H为图像的高)。

我们以 5x5 的卷积核,垂直方向取样为例:

void main() {
    vec2 iResolution = vec2(375., 667.);

    float offset[6];
    offset[1] = 0.; offset[2] = 1.; offset[3] = 2.; offset[4] = 3.; offset[5] = 4.;

    float weight[6];
    weight[1] = 0.2270270270; weight[2] = 0.1945945946; weight[3] = 0.1216216216;
    weight[4] = 0.0540540541; weight[5] = 0.0162162162; 

    vec4 color = texture2D(inputImageTexture, vec2(gl_FragCoord)/iResolution) * weight[0];

    for (int i=1; i<=5; i++) {
        color +=
            texture2D(inputImageTexture, (vec2(gl_FragCoord)+vec2(0.0, offset[i]))/iResolution)
                * weight[i];
        color +=
            texture2D(inputImageTexture, (vec2(gl_FragCoord)-vec2(0.0, offset[i]))/iResolution)
                * weight[i];
    }

    gl_FragColor = color;
}

水平方向也是同理的,不过上述方式依旧需要进行 9 次的texture2D()纹理采样操作,这篇文章中介绍了一种线性采样的方式,通过对权重和间距的处理,把 9 次纹理采样操作减少到 5 次:

void main() {
    vec2 iResolution = vec2(375., 667.);

    float offset[4];
    offset[1] = 0.; offset[2] = 1.3846153846; offset[3] = 3.2307692308;

    float weight[4];
    weight[1] = 0.2270270270; weight[2] = 0.3162162162; weight[3] = 0.0702702703;

    vec4 color = texture2D(inputImageTexture, vec2(gl_FragCoord)/iResolution) * weight[0];

    // 垂直
    for (int i=1; i<=3; i++) {
        color +=
            texture2D(inputImageTexture, (vec2(gl_FragCoord)+vec2(0.0, offset[i]))/iResolution)
                * weight[i];
        color +=
            texture2D(inputImageTexture, (vec2(gl_FragCoord)-vec2(0.0, offset[i]))/iResolution)
                * weight[i];
    }

    vec4 color2 = texture2D(inputImageTexture, vec2(gl_FragCoord)/iResolution) * weight[0];

    // 水平
    for (int i=1; i<=3; i++) {
        color2 +=
            texture2D(inputImageTexture, (vec2(gl_FragCoord)+vec2(offset[i], 0.0))/iResolution)
                * weight[i];
        color2 +=
            texture2D(inputImageTexture, (vec2(gl_FragCoord)-vec2(offset[i], 0.0))/iResolution)
                * weight[i];
    }

    gl_FragColor = mix(color, color2, .5);
}

肉眼是无法区分出差异的,但性能会提升:

让图片更加模糊的另外一种方式是通过对 framebuffer 多次应用模糊函数来加强模糊效果。


当然,关于高斯模糊的实现,还有很多其他的实现方式:
// 来自 https://github.com/Jam3/glsl-fast-gaussian-blur
// 只包含一个方向,需要自己叠加
// 3x3
vec4 blur5(sampler2D image, vec2 uv, vec2 resolution, vec2 direction) {
  vec4 color = vec4(0.0);
  vec2 off1 = vec2(1.3333333333333333) * direction;
  color += texture2D(image, uv) * 0.29411764705882354;
  color += texture2D(image, uv + (off1 / resolution)) * 0.35294117647058826;
  color += texture2D(image, uv - (off1 / resolution)) * 0.35294117647058826;
  return color; 
}
// 5x5
vec4 blur9(sampler2D image, vec2 uv, vec2 resolution, vec2 direction) {
  vec4 color = vec4(0.0);
  vec2 off1 = vec2(1.3846153846) * direction;
  vec2 off2 = vec2(3.2307692308) * direction;
  color += texture2D(image, uv) * 0.2270270270;
  color += texture2D(image, uv + (off1 / resolution)) * 0.3162162162;
  color += texture2D(image, uv - (off1 / resolution)) * 0.3162162162;
  color += texture2D(image, uv + (off2 / resolution)) * 0.0702702703;
  color += texture2D(image, uv - (off2 / resolution)) * 0.0702702703;
  return color;
}
// 7x7
vec4 blur13(sampler2D image, vec2 uv, vec2 resolution, vec2 direction) {
  vec4 color = vec4(0.0);
  vec2 off1 = vec2(1.411764705882353) * direction;
  vec2 off2 = vec2(3.2941176470588234) * direction;
  vec2 off3 = vec2(5.176470588235294) * direction;
  color += texture2D(image, uv) * 0.1964825501511404;
  color += texture2D(image, uv + (off1 / resolution)) * 0.2969069646728344;
  color += texture2D(image, uv - (off1 / resolution)) * 0.2969069646728344;
  color += texture2D(image, uv + (off2 / resolution)) * 0.09447039785044732;
  color += texture2D(image, uv - (off2 / resolution)) * 0.09447039785044732;
  color += texture2D(image, uv + (off3 / resolution)) * 0.010381362401148057;
  color += texture2D(image, uv - (off3 / resolution)) * 0.010381362401148057;
  return color;
}
// 来自 https://www.shadertoy.com/view/XdfGDH
// 正态分布概率密度函数
float normpdf(in float x, in float sigma) {
	return 0.39894*exp(-0.5*x*x/(sigma*sigma))/sigma;
}
vec3 gaussianblur(int size, sampler2D texture, vec2 resolution) {
    //declare stuff
    const int mSize = size;
    const int kSize = (mSize-1)/2;
    float kernel[mSize];
    vec3 final_colour = vec3(0.0);
    
    //create the 1-D kernel
    float sigma = 7.0;
    float Z = 0.0;
    for (int j = 0; j <= kSize; ++j)
    {
        kernel[kSize+j] = kernel[kSize-j] = normpdf(float(j), sigma);
    }
    
    //get the normalization factor (as the gaussian has been clamped)
    for (int j = 0; j < mSize; ++j)
    {
        Z += kernel[j];
    }
    
    //read out the texels
    for (int i=-kSize; i <= kSize; ++i)
    {
        for (int j=-kSize; j <= kSize; ++j)
        {
            final_colour += kernel[kSize+j]*kernel[kSize+i]*texture2D(texture, (gl_FragCoord.xy+vec2(float(i),float(j))) / resolution.xy).rgb;
        }
    }

    return final_colour/(Z*Z);
}
// 来自:https://gl-transitions.com/editor/LinearBlur
vec4 blur(vec2 _uv, sampler2D texture) {
    float disp = 0.;
    float intensity = .2;
    const int passes = 6;
    vec4 c1 = vec4(0.0);
    disp = intensity*(0.5-distance(0.5, .1));
  
    for (int xi=0; xi<passes; xi++) {
        float x = float(xi) / float(passes) - 0.5;
        for (int yi=0; yi<passes; yi++) {
            float y = float(yi) / float(passes) - 0.5;
            vec2 v = vec2(x, y);
            float d = disp;
            c1 += texture2D(texture, _uv + d*v);
        }
    }
    c1 /= float(passes*passes);
    return c1;
}

下一篇 Shader 运动模糊

相关链接: