OpenGL.Shader:志哥教你写一个滤镜直播客户端(12)
工作生活安排得太满,放空了博客一段时间,最近才有时间继续整理滤镜的学习。这篇带来的是时下比较热门的一个滤镜效果磨皮——双边滤波的简单学习。
一、何为双边滤波?
我们先来看看比较官方的解释:双边滤波(Bilateral filter)是一种非线性的滤波方法,是结合图像的空间邻近度和像素值相似度的一种折衷处理,同时考虑空域信息和灰度相似性,达到保边去噪的目的。具有简单、非迭代、局部的特点。
第一次看可能不太理解 “非线性” 。其实在前面介绍的均值滤波 / 高斯滤波 都是属于线性滤波,简单理解为:针对一张图片的所有像素值,都是使用同一个滤波矩阵,按照固定比例的权重系数做卷积。非线性滤波就是增加一个参数,判断临近像素是否相似,使得卷积过程中的权重系数不再是固定比例,而是动态按需调整。
结合以上这个白话文认识之后,接下来按照国际惯例,以双边滤波的公式入手。
g(i, j) 代表输出结果;
S(i, j)的是指以(i,j)为中心的(2N+1)(2N+1)的卷积运算;N为卷积矩阵的半径长度
(k, l)代表范围内的(多个)输入点;f(k, l) 是点(k, l)对应的值。
w(i, j, k, l)代表经过两个高斯函数计算出的值 (注意:这里不是最终权值)
上述公式我们进行转化,假设公式中w(i,j,k,l)为m,则有
设 m1+m2+m3 … +mn = M,则有
此时可以看到,这明显是图像矩阵与核的卷积运算了。其中m1/M代表的第一个点(或最后一个点,看后面如何实现)的权值,而图像矩阵与核通过卷积算子作加权和,最终得到输出值。
接下来我们来讨论最关键的w(i, j, k, l),其实w(i, j, k, l) = ws * wr。
先说WS,空间临近高斯函数,也叫定义域,仔细观察其实就是高斯滤波的正太分布模型。代表的是其在特定空间所执行的卷积的数学模型,示意图如下,如果我们在整个图像上都执行这个数学模型的卷积,就是普通的高斯滤波。
再说WR,像素值相似度高斯函数,也叫值域或频域,逻辑示意图如下,其意思表示当前输入点(k,l)的值f(k,l) 和 输出点(i,j)的值f(i,j)的差值。差值越大,wr越小趋向于0;差值越小,wr越大趋向于1;如果 f(i,j) = f(k,l),wr=1。
重点理解:是比较当前点的值f(k,l)和输入点的值f(i,j)的差值,在图像处理当中,就是比较坐标为(i,j)的像素值和坐标为(k,l)的像素值,从而判断其边缘是否相似。
二、GL当中的双边滤波
在OpenGL.Shader上实现双边滤波不在于算法,是在于思想。上篇介绍了多重FBO实现高斯滤波降维的运算,可能比较难理解。这一次实现双边滤波就从简单中来,简单中去。力求让大家能明白其中的思想,拿到代码后是能 “自己改得动的”。
首先是顶点着色器:
attribute vec4 position;
attribute vec4 inputTextureCoordinate;
const int GAUSSIAN_SAMPLES = 9;
uniform vec2 singleStepOffset;
varying vec2 textureCoordinate;
varying vec2 blurCoordinates[GAUSSIAN_SAMPLES];
void main()
{
gl_Position = position;
textureCoordinate = inputTextureCoordinate.xy;
int multiplier = 0;
vec2 blurStep;
for (int i = 0; i < GAUSSIAN_SAMPLES; i++)
{
multiplier = (i - ((GAUSSIAN_SAMPLES - 1) / 2));
blurStep = float(multiplier) * singleStepOffset;
blurCoordinates[i] = inputTextureCoordinate.xy + blurStep;
}
}
双边滤波和之前的高斯滤波基本一样,也是取9个采样点, 直接声名一个vec2的singleStepOffset偏移步长。计算输入点前四步和后四步的顶点。没啥好说的,接着重点看片元着色器。
uniform sampler2D SamplerY;
uniform sampler2D SamplerU;
uniform sampler2D SamplerV;
uniform sampler2D SamplerRGB;
mat3 colorConversionMatrix = mat3(
1.0, 1.0, 1.0,
0.0, -0.39465, 2.03211,
1.13983, -0.58060, 0.0);
vec3 yuv2rgb(vec2 pos)
{
vec3 yuv;
yuv.x = texture2D(SamplerY, pos).r;
yuv.y = texture2D(SamplerU, pos).r - 0.5;
yuv.z = texture2D(SamplerV, pos).r - 0.5;
return colorConversionMatrix * yuv;
}
// yuv转rgb
const lowp int GAUSSIAN_SAMPLES = 9;
varying highp vec2 textureCoordinate;
varying highp vec2 blurCoordinates[GAUSSIAN_SAMPLES];
uniform mediump float distanceNormalizationFactor;
void main()
{
lowp vec4 centralColor; // 输入中心点像素值
lowp float gaussianWeightTotal; // 高斯权重集合
lowp vec4 sampleSum; // 卷积和
lowp vec4 sampleColor; // 采样点像素值
lowp float gaussianWeight; // 采样点高斯权重
lowp float distanceFromCentralColor;
centralColor = vec4(yuv2rgb(blurCoordinates[4]), 1.0);
gaussianWeightTotal = 0.22;
sampleSum = centralColor * 0.22;
sampleColor = vec4(yuv2rgb(blurCoordinates[0]), 1.0);
distanceFromCentralColor = min(distance(centralColor, sampleColor) * distanceNormalizationFactor, 1.0);
gaussianWeight = 0.03 * (1.0 - distanceFromCentralColor);
gaussianWeightTotal += gaussianWeight;
sampleSum += sampleColor * gaussianWeight;
sampleColor = vec4(yuv2rgb(blurCoordinates[1]), 1.0);
distanceFromCentralColor = min(distance(centralColor, sampleColor) * distanceNormalizationFactor, 1.0);
gaussianWeight = 0.07 * (1.0 - distanceFromCentralColor);
gaussianWeightTotal += gaussianWeight;
sampleSum += sampleColor * gaussianWeight;
sampleColor = vec4(yuv2rgb(blurCoordinates[2]), 1.0);
distanceFromCentralColor = min(distance(centralColor, sampleColor) * distanceNormalizationFactor, 1.0);
gaussianWeight = 0.12 * (1.0 - distanceFromCentralColor);
gaussianWeightTotal += gaussianWeight;
sampleSum += sampleColor * gaussianWeight;
sampleColor = vec4(yuv2rgb(blurCoordinates[3]), 1.0);
distanceFromCentralColor = min(distance(centralColor, sampleColor) * distanceNormalizationFactor, 1.0);
gaussianWeight = 0.17 * (1.0 - distanceFromCentralColor);
gaussianWeightTotal += gaussianWeight;
sampleSum += sampleColor * gaussianWeight;
sampleColor = vec4(yuv2rgb(blurCoordinates[5]), 1.0);
distanceFromCentralColor = min(distance(centralColor, sampleColor) * distanceNormalizationFactor, 1.0);
gaussianWeight = 0.17 * (1.0 - distanceFromCentralColor);
gaussianWeightTotal += gaussianWeight;
sampleSum += sampleColor * gaussianWeight;
sampleColor = vec4(yuv2rgb(blurCoordinates[6]), 1.0);
distanceFromCentralColor = min(distance(centralColor, sampleColor) * distanceNormalizationFactor, 1.0);
gaussianWeight = 0.12 * (1.0 - distanceFromCentralColor);
gaussianWeightTotal += gaussianWeight;
sampleSum += sampleColor * gaussianWeight;
sampleColor = vec4(yuv2rgb(blurCoordinates[7]), 1.0);
distanceFromCentralColor = min(distance(centralColor, sampleColor) * distanceNormalizationFactor, 1.0);
gaussianWeight = 0.07 * (1.0 - distanceFromCentralColor);
gaussianWeightTotal += gaussianWeight;
sampleSum += sampleColor * gaussianWeight;
sampleColor = vec4(yuv2rgb(blurCoordinates[8]), 1.0);
distanceFromCentralColor = min(distance(centralColor, sampleColor) * distanceNormalizationFactor, 1.0);
gaussianWeight = 0.03 * (1.0 - distanceFromCentralColor);
gaussianWeightTotal += gaussianWeight;
sampleSum += sampleColor * gaussianWeight;
gl_FragColor = sampleSum / gaussianWeightTotal;
}
看着很长的一段shader代码,其实都是一套模板代码。 先看头尾两部分代码:
centralColor = vec4(yuv2rgb(blurCoordinates[4]), 1.0);
gaussianWeightTotal = 0.22;
sampleSum = centralColor * 0.22;
// ... ...
gl_FragColor = sampleSum / gaussianWeightTotal
如果我们略去外围8个采样点的卷积,输出值=输入中心点的像素值,保持原输入的图像效果。接着我们加入采样点卷积,以中心点外一个singleStepOffset的blurCoordinates[3] 和 blurCoordinates[5]为例,分析采样点代码逻辑:
sampleColor = vec4(yuv2rgb(blurCoordinates[3]), 1.0);
distanceFromCentralColor = min(distance(centralColor, sampleColor) * distanceNormalizationFactor, 1.0);
gaussianWeight = 0.17 * (1.0 - distanceFromCentralColor);
gaussianWeightTotal += gaussianWeight;
sampleSum += sampleColor * gaussianWeight;
第一行代码,获取采样点像素值。
(重点)第二行代码 利用GLSL内置函数disatance计算两个vec2/3/4的距离,也可以理解为两个变量的差值,distance函数可以用来计算两个颜色的相似程度,结果越大,两个颜色间的差异越大,结果越小,两个颜色间的差异越小。然后乘以自定义的distanceNormalizationFactor差值量化因子。怎么理解这个因子?其实就是一个修正参数,使其可以动态改变其效果范围,不明白可以看以下的动图。
这里说一个GLSL的调试技巧,把需要调试的值拼装成一个vec4,直接输出到gl_FragColor显示,效果用眼观察就最直接了。
譬如distanceNormalizationFactor,通过调试发现,当其取值越大,显示红色区域越大,证明distanceFromCentralColor = min(distance(centralColor, sampleColor) * distanceNormalizationFactor, 1.0)运算值取1,distanceFromCentralColor = 1时,不参与其中的高斯卷积运算,由此总结论1,红色区域是指不参与高斯卷积的部分;结论2,要突出高斯模糊的边缘效果,就要加大红色区域,distanceNormalizationFactor要越大。
(次重点)第三行代码,结合第二行的代码理解,利用GLSL内置函数min,根据双边滤波的数学意义,求出采样点和中心输入点的值的差值后,归一化为一个相似度distanceFromCentralColor,但是注意一点的是,采样点和中心输入点的像素值越接近,distanceFromCentralColor越趋向于0,高斯权重越接近原始值。
所以参与卷积的权重 = 原高斯权重*(1.0 - distanceFromCentralColor)
第四第五行代码,把参与卷积的权重归并,进行卷积运算。
剩下就是复写几个GpuBaseFilter的函数,详情请参考 https://github.com/MrZhaozhirong/NativeCppApp /src/main/cpp/gpufilter/filter/GpuBilateralBlurFilter.hpp
void setAdjustEffect(float percent) {
// 动态调整色值阈值参数
mThreshold_ColorDistanceNormalization = range(percent*100.0f, 10.0f, 1.0f);
}
void onDraw(GLuint SamplerY_texId, GLuint SamplerU_texId, GLuint SamplerV_texId,
void* positionCords, void* textureCords)
{
if (!mIsInitialized)
return;
glUseProgram(getProgram());
// 把step offset的步伐直接用vec2表示,其值直接输入1/w,1/h
glUniform2f(mSingleStepOffsetLocation, 1.0f/mOutputWidth, 1.0f/mOutputHeight);
glUniform1f(mColorDisNormalFactorLocation, mThreshold_ColorDistanceNormalization);
// 绘制的模板代码
glVertexAttribPointer(mGLAttribPosition, 2, GL_FLOAT, GL_FALSE, 0, positionCords);
glEnableVertexAttribArray(mGLAttribPosition);
glVertexAttribPointer(mGLAttribTextureCoordinate, 2, GL_FLOAT, GL_FALSE, 0, textureCords);
glEnableVertexAttribArray(mGLAttribTextureCoordinate);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, SamplerY_texId);
glUniform1i(mGLUniformSampleY, 0);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, SamplerU_texId);
glUniform1i(mGLUniformSampleU, 1);
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, SamplerV_texId);
glUniform1i(mGLUniformSampleV, 2);
// onDrawArraysPre
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glDisableVertexAttribArray(mGLAttribPosition);
glDisableVertexAttribArray(mGLAttribTextureCoordinate);
glBindTexture(GL_TEXTURE_2D, 0);
}
再说一个知识点:
在GpuGaussianBlurFilter的时候,高斯卷积核是从外部代码传入到Shader的,其值是
convolutionKernel = new GLfloat[9]{
0.0947416f, 0.118318f, 0.0947416f,
0.118318f, 0.147761f, 0.118318f,
0.0947416f, 0.118318f, 0.0947416f,
};
在 GpuGaussianBlurFilter2,高斯核不在由外部传入,是直接写在Shader当中,其值是:
0.05,0.09,0.12,0.15,0.18,0.15,0.12,0.09,0.05
在这次GpuBilateralBlurFilter,不知道小伙伴有没留意,其高斯核也是直接写在Shader当中,其值是:
0.03,0.07,0.12,0.17,0.22,0.17,0.12,0.07,0.03
我一步步的把核心值的权重提高,降低边缘的权重,想想是为什么?