高斯模糊的 GLSL 实现

共 4489字,需浏览 9分钟

 ·

2021-07-07 23:18

本文是 OpenGL 4.0 Shading Language Cookbook 的学习笔记。


主要介绍使用GLSL来实现高斯模糊。


模糊过滤可以有效减少图像中的噪点数量。前面提到,在边缘检测之前使用模糊过滤可以减少图像的高频变化。


模糊过滤的基本原理是对附近像素进行加权和来混合当前像素颜色。通常使用的权重随距离减小(二维屏幕空间距离),距离当前像素较远的像素贡献较小。


高斯模糊使用二维高斯函数来计算附近像素的权重:

sigma 的平方项是高斯函数的方差,它决定了高斯曲面的宽度。高斯函数在(0,0)处取得最大值,对应需要模糊处理的像素,随着x或y坐标的增加,高斯函数值减小。


下图显示了  平方为4.0时的二维高斯函数的变化趋势。

下图显示了高斯模糊前后的画面,左边还没有进行高斯模糊,右边是进行高斯模糊后的图像:

使用高斯模糊,对于每一个像素,我们需要计算所有像素的加权和。这一算法存在下面这些问题:


  • 算法复杂度是大 On 的平方,对于实时渲染来说太慢了。

  • 为了避免改变图像整体亮度,所有权重相加的和必须为1。


由于我们是在离散的坐标上使用高斯函数采样,我们的权重和也不等于1。


我们可以通过限制采样的像素数目,然后对高斯函数的值进行规范化来解决上面的两个问题。在这里,我们使用9x9大小的高斯模糊过滤器,在这种情况下,对于每个像素,我们只需要采样81个像素就可以完成模糊操作。


这个方法需要为每个像素在片段着色器中访问纹理元素81次。一张800x600大小的图像,需要访问纹理元素800x600x81=38,880,000次。看起来访问次数非常多?我们可以通过进行两遍高斯模糊来减小访问次数。


上面公式表明,我们可以使用两遍处理来实现高斯模糊。在第一遍处理时,我们计算上面公式中与j相关的和,将结果存储在临时的纹理中。在第二遍处理时,我们使用临时纹理计算与i相关的和。


现在,还有一个重要的问题要处理。之前说过,我们的权值和必须等于1,所以,我们改写我们的等式为下面的形式来规范化权值:

让我们把重点转移到代码上。


我们在这里通过三遍处理和两张纹理实现高斯模糊。第一遍处理时,我们渲染整个场景到纹理中。在第二遍处理时,我们使用上一遍处理得到的纹理计算垂直和。最后一遍处理,我们进行水平和的计算,然后将结果输出到默认的帧缓冲中。


创建两个帧缓冲对象,以及它们绑定的纹理。由于我们在第一遍时需要绘制三维场景,第一个帧缓冲对象还需要绑定一个深度缓冲。第二遍和第三遍处理,我们只需要在片段着色器中绘制二维屏幕,不需要深度缓冲。


我们将使用子程序来绑定每一遍处理使用的着色器功能。我们的OpenGL程序需要对下面的Uniform变量进行设置:


  • Width:以像素为单位的屏幕宽度。

  • Height:以像素为单位的屏幕高度。

  • Weight[]:规范化后的高斯权重数组。

  • Texture0:设置为对应0号纹理单元。

  • PixOffset[]:需要模糊处理的像素的位置偏移。


我们采用下面的步骤实现高斯模糊:

1. 使用我们之前介绍的边缘检测技术使用的顶点着色器。

2. 使用下面的代码作为片段着色器:


#version 400
in vec3 Position; // Vertex position
in vec3 Normal; // Vertex normal
in vec2 TexCoord; // Texture coordinate
uniform sampler2D Texture0;
uniform int Width; // Width of the screen in pixels
uniform int Height; // Height of the screen in pixels
subroutine vec4 RenderPassType();
subroutine uniform RenderPassType RenderPass;
// Other uniform variables for
// the Phong reflection model can
// be placed here
layout( location = 0 ) out vec4 FragColor;
uniform float PixOffset[5]=float[](0.0,1.0,2.0,3.0,
4.0);
uniform float Weight[5];
vec3 phongModel( vec3 pos, vec3 norm )
{
// The code for the Phong reflection model
// goes here
}
subroutine (RenderPassType)
vec4 pass1()
{
return vec4(phongModel( Position,Normal ),1.0);
}
subroutine( RenderPassType )
vec4 pass2()
{
float dy = 1.0 / float(Height);
vec4 sum = texture(Texture0, TexCoord) *
Weight[0];
for( int i = 1; i < 5; i++ )
{
sum += texture( Texture0,
TexCoord +vec2(0.0,
PixOffset[i]) * dy )
* Weight[i];
sum += texture( Texture0,
TexCoord - vec2(0.0,
PixOffset[i]) * dy )
* Weight[i];
}
return sum;
}

subroutine( RenderPassType )
vec4 pass3()
{
float dx = 1.0 / float(Width);
vec4 sum = texture(Texture0, TexCoord) *
Weight[0];
for( int i = 1; i < 5; i++ )
{
sum += texture( Texture0,
TexCoord + vec2(PixOffset[i],
0.0) * dx )
* Weight[i];
sum += texture( Texture0,
TexCoord - vec2(PixOffset[i],
0.0) * dx )
* Weight[i];
}
return sum;
}
void main()
{
// This will call either pass1(),
// pass2(), or pass3()
FragColor = RenderPass();
}


3. 在OpenGL程序中,我们使用PixOffset进行坐标偏移计算高斯权重,然后将结果存储在Weight数组中。代码如下:


char uniName[20];
float weights[5], sum, sigma2 = 4.0f;
// Compute and sum the weights
weights[0] = gauss(0,sigma2); // The 1-D Gaussian
// function
sum = weights[0];
for( int i = 1; i < 5; i++ ) {
weights[i] = gauss(i, sigma2);
sum += 2 * weights[i];
}
// Normalize the weights and set the uniform
for( int i = 0; i < 5; i++ ) {
snprintf(uniName, 20, "Weight[%d]", i);
prog.setUniform(uniName, weights[i] / sum);
}


对于第一遍处理,我们采取以下步骤:

1. 选择进行渲染的帧缓冲,启用深度测试,清除颜色/深度缓冲区。

2. 选择执行pass1子程序。

3. 绘制场景。


对于第二遍处理,我们采取以下步骤:

1. 选择中转使用的帧缓冲,关闭深度测试,清除颜色缓冲区。

2. 选择执行pass2子程序。

3. 设置视图,投影和模型矩阵为单位矩阵。

4. 绑定第一遍处理生成的纹理到0号纹理单元。

5. 绘制中间纹理。


对于第三遍处理,我们采取以下步骤:

1. 解除绑定中转帧缓冲(设置为默认的帧缓冲),清除颜色缓冲区。

2. 选择执行pass3子程序。

3. 绑定第二遍处理生成的纹理到0号纹理单元。

4. 绘制纹理到屏幕。

原理

步骤3中使用的gauss函数的第一个参数是x坐标,第二个参数是 sigma 的平方。gauss函数关于坐标0进行偏移,我们只需要使用偏移量作参数即可。在这里,我们只使用正整数来进行偏移,我们需要对非0数值的结果乘以2(对应整数和负数偏移)。


第一遍处理(pass1子程序),我们使用Phong反射模型将场景渲染到纹理中。


第二遍处理进行垂直权重和高斯模糊操作,并将结果存储在另一张纹理中。我们使用PixOffset数组来进行垂直方向的坐标偏移求出权重和。(dy项是一个纹理元素在纹理坐标下的高度)我们同时累加两个方向(距离要模糊的像素垂直方向正反两边四个像素内的像素)的权重。


第三遍处理和第二遍处理类似。我们计算水平方向的权重和。这一遍处理后,我们将结果直接输出到默认的帧缓冲。

优化

我们还可以继续优化将访问纹理的次数减少一半。

如果我们好好利用纹理访问时自动进行的线性插值(纹理的缩放模式为GL_LINEAR),我们就可以一次获得两个纹理元素的信息!Daniel Rákos的博客详细描述了这一技术(rastergrid.com/blog/201)。


我们可以通过增加Weight和PixOffset数组的大小来增大模糊效果。还可以使用不同的sigma的平方值来改变高斯函数的趋势。


来源:https://zhuanlan.zhihu.com/p/60824750

作者:fangcun


推荐:

Android FFmpeg 实现带滤镜的微信小视频录制功能

全网最全的 Android 音视频和 OpenGL ES 干货,都在这了

一文掌握 YUV 图像的基本处理

Android FFmpeg 流媒体边播放边录制功能

FFmpeg + Android AudioRecorder 音频录制编码

利用 FFmpeg 和 ANativeWindow 实现视频解码播放

FFmpeg、x264以及fdk-aac 编译整合

浏览 97
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报