工作生活安排得太滿,放空了部落格一段時間,最近才有時間繼續整理濾鏡的學習。這篇帶來的是時下比較熱門的一個濾鏡效果磨皮——雙邊濾波的簡單學習。
我們先來看看比較官方的解釋:雙邊濾波(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)的畫素值,從而判斷其邊緣是否相似。
在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差值量化因子。怎麼理解這個因子?其實就是一個修正引數,使其可以動態改變其效果範圍,不明白可以看以下的動圖。
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
我一步步的把核心值的權重提高,降低邊緣的權重,想想是為什麼?