如果使用過PS之類的圖像處理軟件娱局,相信對于模糊濾鏡不會陌生,圖像處理軟件提供了眾多的模糊算法泪漂。高斯模糊是其中的一種廊营。
在我們的智慧樓宇的項(xiàng)目中,要求對樓宇實(shí)現(xiàn)樓宇發(fā)光的效果萝勤。 比如如下圖所示的簡單樓宇效果:
樓宇發(fā)光效果需要用的算法之一就是高斯模糊露筒。
高斯模糊簡介
高斯模糊算法是計算機(jī)圖形學(xué)領(lǐng)域中一種使用廣泛的技術(shù), 是一種圖像空間效果,用于對圖像進(jìn)行模糊處理敌卓,創(chuàng)建原始圖像的柔和模糊版本慎式。
使用高斯模糊的效果,結(jié)合一些其他的算法趟径,還可以產(chǎn)生發(fā)光瘪吏,光暈,景深蜗巧,熱霧和模糊玻璃效果掌眠。
高斯模糊的原理說明
圖像模糊的原理,簡單而言幕屹,就是針對圖像的每一個像素蓝丙,其顏色取其周邊像素的平均值。不同的模糊算法望拖,對周邊的定義不一樣渺尘,平均的算法也不一樣。 比如之前寫#過的一篇文章说敏,webgl實(shí)現(xiàn)徑向模糊,就是模糊算法中的一種鸥跟。
均值模糊
在理解高斯模糊之前,我們先理解比較容易的均值模糊像云。所謂均值模糊
其原理就是取像素點(diǎn)周圍(上下左右)像素的平均值(其中也會包括自身)锌雀。如下圖所示:
可以看出蚂夕,對于某個像素點(diǎn)迅诬,當(dāng)搜索半徑為1的時候,影響其顏色值的像素是9個像素(包括自己和周邊的8個像素)婿牍。假設(shè)每個像素對于中心像素的影響都是一樣的侈贷,那么每個像素的影響度就是1/9。如下圖所示:
上面這個3*3的影響度的數(shù)字矩陣,通常稱之為卷積核俏蛮。
那么最終中心點(diǎn)的值的求和如下圖所示:
最終的值是:
(8 * 1 + 1 * 2 / (8 + 1) ) = 10/9
當(dāng)計算像素的顏色時候撑蚌,對于像素的RGB每一個通道都進(jìn)行的上述平均計算即可。
上面的計算過程就是一種卷積濾鏡搏屑。所謂卷積濾鏡争涌,通俗來說,就是一種組合一組數(shù)值的算法辣恋。
如果搜索半徑變成2亮垫,則會變成25個像素的平均,搜索半徑越大伟骨,就會越模糊饮潦。像素個數(shù)與搜索半徑的關(guān)系如下:
(1 + r * 2)的平方 // r = 1,結(jié)果為9携狭,r=2继蜡,結(jié)果為25,r=3 結(jié)果為49.
通常 NxN會被稱之卷積核的大小逛腿。比如3x3稀并,5x5。
在均值模糊的計算中单默,參與的每個像素稻轨,對中心像素的貢獻(xiàn)值都是一樣的,這是均值模糊的特點(diǎn)雕凹。也就是殴俱,每個像素的權(quán)重都是一樣的。
正態(tài)分布
如果使用簡單平均枚抵,顯然不是很合理线欲,因?yàn)閳D像都是連續(xù)的,越靠近的點(diǎn)關(guān)系越密切汽摹,越遠(yuǎn)離的點(diǎn)關(guān)系越疏遠(yuǎn)李丰。因此,加權(quán)平均更合理逼泣,距離越近的點(diǎn)權(quán)重越大趴泌,距離越遠(yuǎn)的點(diǎn)權(quán)重越小。
正態(tài)分布整好滿足上述的的分布需求拉庶,如下圖所示:
可以看出嗜憔,正態(tài)分布是一種鐘形曲線,越接近中心氏仗,取值越大吉捶,越遠(yuǎn)離中心,取值越小。
在計算平均值的時候呐舔,我們只需要將"中心點(diǎn)"作為原點(diǎn)币励,其他點(diǎn)按照其在正態(tài)曲線上的位置,分配權(quán)重珊拼,就可以得到一個加權(quán)平均值食呻。
高斯函數(shù)
高斯函數(shù)是描述正態(tài)分布的數(shù)學(xué)公式。公式如下:
其中澎现,μ是x的均值搁进,可以理解為正態(tài)分布的中心位置,σ是x的方差昔头。因?yàn)橛嬎闫骄档臅r候饼问,中心點(diǎn)就是原點(diǎn),所以μ等于0揭斧。
如果是二維莱革,則有:
可以看出二維高斯函數(shù)中,x和y相對是獨(dú)立的。也就是說:
G(x,y) = G(x) + G(y)
這個特性的好處是讹开,可以把二維的高斯函數(shù)盅视,拆解成兩個獨(dú)立的一維高斯函數(shù)∧只鳎可以提高效率。實(shí)際上,高斯模糊運(yùn)用的一維高斯函數(shù),而不是使用二維。
高斯模糊
高斯模糊的原理和前面介紹的均值模糊的原理基本上一樣,只是均值模糊在計算平均值的時候,周邊像素的權(quán)重都是一樣的芝发。而高斯模糊下独悴,周邊像素的權(quán)重值卻使用高斯函數(shù)進(jìn)行計算坟奥,這也是高斯模糊的之所以被稱為高斯模糊的原因孝偎。
比如當(dāng)σ取值為則模糊半徑為1的權(quán)重矩陣如下:
這9個點(diǎn)的權(quán)重總和等于0.4787147迅涮,如果只計算這9個點(diǎn)的加權(quán)平均据悔,還必須讓它們的權(quán)重之和等于1菠隆,因此上面9個值還要分別除以0.4787147兵琳,得到最終的權(quán)重矩陣狂秘。
渲染流程
了解了高斯模糊的基本原理之后,來看看高斯模糊在webgl中基本渲染流程:
- 首先躯肌,按照正常流程把場景或者圖像渲染到一個紋理對象上面者春,需要使用FrameBuffer功能。
- 對紋理對象進(jìn)行施加高斯模糊算法清女,得到最終的高斯模糊的紋理對象钱烟。
上面第二部,施加高斯模糊算法嫡丙,一般又會分成兩步:
- 先施加垂直方向的高斯模糊算法拴袭;
- 在垂直模糊的基礎(chǔ)上進(jìn)行水平方向的高斯模糊算法。
當(dāng)然曙博,也可以先水平后垂直,結(jié)果是一樣的父泳。 分兩步高斯模糊算法和一步進(jìn)行兩個方向的高斯模糊算法的結(jié)果基本是一致的泰佳,但是卻可以提高算法的效率。 有人可能說尘吗,多模糊了一步逝她,為啥還提高了效率。 這么來說吧睬捶,如果是3x3大小的高斯模糊:
分兩步要獲取的像素數(shù)量是 3 + 3 = 6黔宛; 而一步卻是3 x 3 = 9。 如果是5x5大小的高斯模糊:分兩步要獲取的像素數(shù)量是 5+5=10擒贸; 而一步卻是5 x 5=25 臀晃。顯然可以算法執(zhí)行效率。
渲染流程代碼
對于第一步介劫,首先是渲染到紋理對象徽惋,這輸入渲染到紋理的知識,此處不再贅述座韵,大致大代碼結(jié)構(gòu)如下:
···
frameBuffer.bind();
renderScene();
frameBuffer.unbind();
···
把renderScene放到frameBuffer.bind之后险绘,會把場景繪制到frameBuffer關(guān)聯(lián)的紋理對象上面。
然后是第二步誉碴,執(zhí)行高斯模糊算法進(jìn)行
pass(params={},count = 1,inputFrameBuffer){
let {options,fullScreen } = this;
inputFrameBuffer = inputFrameBuffer || this.inputFrameBuffer;
let {gl,gaussianBlurProgram,verticalBlurFrameBuffer,horizontalBlurFrameBuffer} = this;
let {width,height} = options;
gl.useProgram(gaussianBlurProgram);
if(width == null){
width = verticalBlurFrameBuffer.width;
height = verticalBlurFrameBuffer.height;
}
verticalBlurFrameBuffer.bind();
fullScreen.enable(gaussianBlurProgram,true);
gl.activeTexture(gl.TEXTURE0 + inputFrameBuffer.textureUnit); // 激活gl.TEXTURE0
gl.bindTexture(gl.TEXTURE_2D, inputFrameBuffer.colorTexture); // 綁定貼圖對象
gl.uniform1i(gaussianBlurProgram.uColorTexture, inputFrameBuffer.textureUnit);
gl.uniform2fv(gaussianBlurProgram.uTexSize, [width,height]);
gl.uniform2fv(gaussianBlurProgram.uDirection,[0,1]); // 垂直方向
gl.uniform1f(gaussianBlurProgram.uExposure,params.exposure || 3);
gl.uniform1f(gaussianBlurProgram.uRadius,params.radius || 5);
gl.uniform1f(gaussianBlurProgram.uUseLinear,params.useLinear || 0.0);
fullScreen.draw();
verticalBlurFrameBuffer.unbind();
if(horizontalBlurFrameBuffer){ // renderToScreen
horizontalBlurFrameBuffer.bind(gl);
}
gl.activeTexture(gl.TEXTURE0 + verticalBlurFrameBuffer.textureUnit); // 激活gl.TEXTURE0
gl.bindTexture(gl.TEXTURE_2D, verticalBlurFrameBuffer.colorTexture); // 綁定貼圖對象
gl.uniform1i(gaussianBlurProgram.uColorTexture, verticalBlurFrameBuffer.textureUnit);
gl.uniform2fv(gaussianBlurProgram.uTexSize, [width,height]);
gl.uniform2fv(gaussianBlurProgram.uDirection,[1,0]); // 水平方向
gl.uniform1f(gaussianBlurProgram.uExposure,params.exposure || 2);
gl.uniform1f(gaussianBlurProgram.uRadius,params.radius || 5);
gl.uniform1f(gaussianBlurProgram.uUseLinear,params.useLinear || 0.0);
fullScreen.draw();
if(horizontalBlurFrameBuffer){
horizontalBlurFrameBuffer.unbind();
}
if(count > 1){
this.pass(params,count - 1,this.horizontalBlurFrameBuffer);
}
return horizontalBlurFrameBuffer;
}
其中inputFrameBuffer 是第一步渲染時候的frameBuffer對象宦棺,作為輸入?yún)?shù)傳遞過來。 然后開始執(zhí)行垂直方向的高斯模糊算法,
verticalBlurFrameBuffer.bind();
fullScreen.enable(gaussianBlurProgram,true);
gl.activeTexture(gl.TEXTURE0 + inputFrameBuffer.textureUnit); // 激活gl.TEXTURE0
gl.bindTexture(gl.TEXTURE_2D, inputFrameBuffer.colorTexture); // 綁定貼圖對象
gl.uniform1i(gaussianBlurProgram.uColorTexture, inputFrameBuffer.textureUnit);
gl.uniform2fv(gaussianBlurProgram.uTexSize, [width,height]);
gl.uniform2fv(gaussianBlurProgram.uDirection,[0,1]); // 垂直方向
gl.uniform1f(gaussianBlurProgram.uExposure,params.exposure || 3);
gl.uniform1f(gaussianBlurProgram.uRadius,params.radius || 5);
gl.uniform1f(gaussianBlurProgram.uUseLinear,params.useLinear || 0.0);
fullScreen.draw();
verticalBlurFrameBuffer.unbind();
在之后執(zhí)行水平方向的模糊算法:
if(horizontalBlurFrameBuffer){ // renderToScreen
horizontalBlurFrameBuffer.bind(gl);
}
gl.activeTexture(gl.TEXTURE0 + verticalBlurFrameBuffer.textureUnit); // 激活gl.TEXTURE0
gl.bindTexture(gl.TEXTURE_2D, verticalBlurFrameBuffer.colorTexture); // 綁定貼圖對象
gl.uniform1i(gaussianBlurProgram.uColorTexture, verticalBlurFrameBuffer.textureUnit);
gl.uniform2fv(gaussianBlurProgram.uTexSize, [width,height]);
gl.uniform2fv(gaussianBlurProgram.uDirection,[1,0]); // 水平方向
gl.uniform1f(gaussianBlurProgram.uExposure,params.exposure || 2);
gl.uniform1f(gaussianBlurProgram.uRadius,params.radius || 5);
gl.uniform1f(gaussianBlurProgram.uUseLinear,params.useLinear || 0.0);
fullScreen.draw();
if(horizontalBlurFrameBuffer){
horizontalBlurFrameBuffer.unbind();
}
shader 代碼
shader 代碼分成兩部分黔帕,一個頂點(diǎn)著色器代碼:
const gaussianBlurVS = `
attribute vec3 aPosition;
attribute vec2 aUv;
varying vec2 vUv;
void main() {
vUv = aUv;
gl_Position = vec4(aPosition, 1.0);
}
`;
另外一個是片元著色器代碼:
const gaussianBlurFS = `
precision highp float;
precision highp int;
#define HIGH_PRECISION
#define SHADER_NAME ShaderMaterial
#define MAX_KERNEL_RADIUS 49
#define SIGMA 11
varying vec2 vUv;
uniform sampler2D uColorTexture;
uniform vec2 uTexSize;
uniform vec2 uDirection;
uniform float uExposure;
uniform bool uUseLinear;
uniform float uRadius;
float gaussianPdf(in float x, in float sigma) {
return 0.39894 * exp( -0.5 * x * x/( sigma * sigma))/sigma;
}
void main() {
vec2 invSize = 1.0 / uTexSize;
float fSigma = float(SIGMA);
float weightSum = gaussianPdf(0.0, fSigma);
vec4 diffuseSum = texture2D( uColorTexture, vUv).rgba * weightSum;
float radius = uRadius;
for( int i = 1; i < MAX_KERNEL_RADIUS; i ++ ) {
float x = float(i);
if(x > radius){
break;
}
float gaussianPdf(x, fSigma),t = x;
vec2 uvOffset = uDirection * invSize * t;
vec4 sample1 = texture2D( uColorTexture, vUv + uvOffset).rgba;
vec4 sample2 = texture2D( uColorTexture, vUv - uvOffset).rgba;
diffuseSum += (sample1 + sample2) * w;
weightSum += 2.0 * w;
}
vec4 result = vec4(1.0) - exp(-diffuseSum/weightSum * uExposure);
gl_FragColor = result;
}
`
最終渲染的效果如下:
應(yīng)用案例
目前用到的主要是發(fā)光樓宇的效果代咸。 下面是幾個案例圖,分享給大家看看:
參考文檔
http://www.ruanyifeng.com/blog/2012/11/gaussian_blur.html
如果對可視化感興趣成黄,可以和我交流呐芥,微信541002349. 另外關(guān)注公眾號“ITMan彪叔” 可以及時收到更多有價值的文章逻杖。