高斯模糊的 GLSL 實(shí)現(xiàn)
本文是 OpenGL 4.0 Shading Language Cookbook 的學(xué)習(xí)筆記。
主要介紹使用GLSL來實(shí)現(xiàn)高斯模糊。
模糊過濾可以有效減少圖像中的噪點(diǎn)數(shù)量。前面提到,在邊緣檢測之前使用模糊過濾可以減少圖像的高頻變化。
模糊過濾的基本原理是對附近像素進(jìn)行加權(quán)和來混合當(dāng)前像素顏色。通常使用的權(quán)重隨距離減小(二維屏幕空間距離),距離當(dāng)前像素較遠(yuǎn)的像素貢獻(xiàn)較小。
高斯模糊使用二維高斯函數(shù)來計(jì)算附近像素的權(quán)重:

sigma 的平方項(xiàng)是高斯函數(shù)的方差,它決定了高斯曲面的寬度。高斯函數(shù)在(0,0)處取得最大值,對應(yīng)需要模糊處理的像素,隨著x或y坐標(biāo)的增加,高斯函數(shù)值減小。
下圖顯示了
平方為4.0時(shí)的二維高斯函數(shù)的變化趨勢。

下圖顯示了高斯模糊前后的畫面,左邊還沒有進(jìn)行高斯模糊,右邊是進(jìn)行高斯模糊后的圖像:

使用高斯模糊,對于每一個(gè)像素,我們需要計(jì)算所有像素的加權(quán)和。這一算法存在下面這些問題:
算法復(fù)雜度是大 On 的平方,對于實(shí)時(shí)渲染來說太慢了。
為了避免改變圖像整體亮度,所有權(quán)重相加的和必須為1。
由于我們是在離散的坐標(biāo)上使用高斯函數(shù)采樣,我們的權(quán)重和也不等于1。
我們可以通過限制采樣的像素?cái)?shù)目,然后對高斯函數(shù)的值進(jìn)行規(guī)范化來解決上面的兩個(gè)問題。在這里,我們使用9x9大小的高斯模糊過濾器,在這種情況下,對于每個(gè)像素,我們只需要采樣81個(gè)像素就可以完成模糊操作。
這個(gè)方法需要為每個(gè)像素在片段著色器中訪問紋理元素81次。一張800x600大小的圖像,需要訪問紋理元素800x600x81=38,880,000次。看起來訪問次數(shù)非常多?我們可以通過進(jìn)行兩遍高斯模糊來減小訪問次數(shù)。

上面公式表明,我們可以使用兩遍處理來實(shí)現(xiàn)高斯模糊。在第一遍處理時(shí),我們計(jì)算上面公式中與j相關(guān)的和,將結(jié)果存儲(chǔ)在臨時(shí)的紋理中。在第二遍處理時(shí),我們使用臨時(shí)紋理計(jì)算與i相關(guān)的和。
現(xiàn)在,還有一個(gè)重要的問題要處理。之前說過,我們的權(quán)值和必須等于1,所以,我們改寫我們的等式為下面的形式來規(guī)范化權(quán)值:

讓我們把重點(diǎn)轉(zhuǎn)移到代碼上。
我們在這里通過三遍處理和兩張紋理實(shí)現(xiàn)高斯模糊。第一遍處理時(shí),我們渲染整個(gè)場景到紋理中。在第二遍處理時(shí),我們使用上一遍處理得到的紋理計(jì)算垂直和。最后一遍處理,我們進(jìn)行水平和的計(jì)算,然后將結(jié)果輸出到默認(rèn)的幀緩沖中。
創(chuàng)建兩個(gè)幀緩沖對象,以及它們綁定的紋理。由于我們在第一遍時(shí)需要繪制三維場景,第一個(gè)幀緩沖對象還需要綁定一個(gè)深度緩沖。第二遍和第三遍處理,我們只需要在片段著色器中繪制二維屏幕,不需要深度緩沖。
我們將使用子程序來綁定每一遍處理使用的著色器功能。我們的OpenGL程序需要對下面的Uniform變量進(jìn)行設(shè)置:
Width:以像素為單位的屏幕寬度。
Height:以像素為單位的屏幕高度。
Weight[]:規(guī)范化后的高斯權(quán)重?cái)?shù)組。
Texture0:設(shè)置為對應(yīng)0號(hào)紋理單元。
PixOffset[]:需要模糊處理的像素的位置偏移。
我們采用下面的步驟實(shí)現(xiàn)高斯模糊:
1. 使用我們之前介紹的邊緣檢測技術(shù)使用的頂點(diǎn)著色器。
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進(jìn)行坐標(biāo)偏移計(jì)算高斯權(quán)重,然后將結(jié)果存儲(chǔ)在Weight數(shù)組中。代碼如下:
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. 選擇進(jìn)行渲染的幀緩沖,啟用深度測試,清除顏色/深度緩沖區(qū)。
2. 選擇執(zhí)行pass1子程序。
3. 繪制場景。
對于第二遍處理,我們采取以下步驟:
1. 選擇中轉(zhuǎn)使用的幀緩沖,關(guān)閉深度測試,清除顏色緩沖區(qū)。
2. 選擇執(zhí)行pass2子程序。
3. 設(shè)置視圖,投影和模型矩陣為單位矩陣。
4. 綁定第一遍處理生成的紋理到0號(hào)紋理單元。
5. 繪制中間紋理。
對于第三遍處理,我們采取以下步驟:
1. 解除綁定中轉(zhuǎn)幀緩沖(設(shè)置為默認(rèn)的幀緩沖),清除顏色緩沖區(qū)。
2. 選擇執(zhí)行pass3子程序。
3. 綁定第二遍處理生成的紋理到0號(hào)紋理單元。
4. 繪制紋理到屏幕。
原理
步驟3中使用的gauss函數(shù)的第一個(gè)參數(shù)是x坐標(biāo),第二個(gè)參數(shù)是 sigma 的平方。gauss函數(shù)關(guān)于坐標(biāo)0進(jìn)行偏移,我們只需要使用偏移量作參數(shù)即可。在這里,我們只使用正整數(shù)來進(jìn)行偏移,我們需要對非0數(shù)值的結(jié)果乘以2(對應(yīng)整數(shù)和負(fù)數(shù)偏移)。
第一遍處理(pass1子程序),我們使用Phong反射模型將場景渲染到紋理中。
第二遍處理進(jìn)行垂直權(quán)重和高斯模糊操作,并將結(jié)果存儲(chǔ)在另一張紋理中。我們使用PixOffset數(shù)組來進(jìn)行垂直方向的坐標(biāo)偏移求出權(quán)重和。(dy項(xiàng)是一個(gè)紋理元素在紋理坐標(biāo)下的高度)我們同時(shí)累加兩個(gè)方向(距離要模糊的像素垂直方向正反兩邊四個(gè)像素內(nèi)的像素)的權(quán)重。
第三遍處理和第二遍處理類似。我們計(jì)算水平方向的權(quán)重和。這一遍處理后,我們將結(jié)果直接輸出到默認(rèn)的幀緩沖。
優(yōu)化
我們還可以繼續(xù)優(yōu)化將訪問紋理的次數(shù)減少一半。
如果我們好好利用紋理訪問時(shí)自動(dòng)進(jìn)行的線性插值(紋理的縮放模式為GL_LINEAR),我們就可以一次獲得兩個(gè)紋理元素的信息!Daniel Rákos的博客詳細(xì)描述了這一技術(shù)(rastergrid.com/blog/201)。
我們可以通過增加Weight和PixOffset數(shù)組的大小來增大模糊效果。還可以使用不同的sigma的平方值來改變高斯函數(shù)的趨勢。
來源:https://zhuanlan.zhihu.com/p/60824750
作者:fangcun
推薦:
全網(wǎng)最全的 Android 音視頻和 OpenGL ES 干貨,都在這了
FFmpeg + Android AudioRecorder 音頻錄制編碼
