高斯模糊的 GLSL 实现
本文是 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 音视频和 OpenGL ES 干货,都在这了
FFmpeg + Android AudioRecorder 音频录制编码