OpenGL.Shader:志哥教你写一个滤镜直播客户端(12)视觉滤镜:磨皮美白の双边滤波原理实现

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的调试技巧,把需要调试的值直接输出到gl_FragColor显示,效果用眼观察就最直接了。

gl_FragColor = vec4( min(distance(centralColor, sampleColor)*distanceNormalizationFactor, 1.0),   0.0, 0.0, 1.0);

// 调试.gif


(次重点)第三行代码,结合第二行的代码理解,利用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

我一步步的把核心值的权重提高,降低边缘的权重,想想是为什么?

猜你喜欢

转载自blog.csdn.net/a360940265a/article/details/109171054