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
我一步步的把核心值的權重提高,降低邊緣的權重,想想是為什么?
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/193058.html
標籤:其他
上一篇:子查詢(內查詢)
下一篇:降雨繪圖(JAVA實作)
