gauss 函數

高斯濾波(高斯平滑)是圖像處理,計算機視覺裏面最常見的操作。平時,我們都是用matlab或者opencv的函數調用:imfilter或者cvSmooth,並不關心底層的實現。然而當開發者要實做高斯濾波的時候,往往就會很迷惘,往往會被以下幾個問題困擾:

  1. 給定sigma,即標準偏差,怎麼確定離散化後濾波器的窗口大小?
  2. 給定窗口大小,怎麼計算高斯核的sigma,即標準方差?
  3. 怎麼實現可分離濾波器?

我在google上搜了一下,還真沒幾個人把實現的細節講清楚了。這裏,我嘗試結合三份源碼,做個小小的總結。

三份源碼分別是:

  1. opencv 中的cvfilter.cpp
  2. autopano-sift-c 中的GaussianConvolution.c
  3. GIMP 中的blur-gauss.c和unsharp-mask.c

在圖像處理中,高斯濾波一般有兩種實現方式,一是用離散化窗口滑窗卷積,另一種通過傅里葉變換。最常見的就是第一種滑窗實現,只有當離散化的窗口非 常大,用滑窗計算量非常大(即使用可分離濾波器的實現)的情況下,可能會考慮基於傅里葉變化的實現方法。這裏我們只討論第一種方法。

二維高斯函數的形式是這樣的:

f(x,y) = A e^{- /left(/frac{(x-x_o)^2}{2/sigma_x^2} + /frac{(y-y_o)^2}{2/sigma_y^2} /right)}.

有着如下的形狀,形狀很激凸,怎麼會叫高斯平滑呢,分明是高斯激凸嘛:

基本上,離散化的主旨就是保留高斯函數中心能量最集中的中間部分,忽略四周能量很小的平坦區域。這只是個很感性的描述,具體實現起來,就會出現千奇百怪的版本。下面結合三份源碼,看看現實世界裏的高斯平滑到底長的什麼樣子。

首先是第一個問題:給定sigma,怎麼計算窗口大小?

直接上opencv的源碼,在cvFilter函數中:

param1 = cvRound(sigma1*(depth == CV_8U ? 3 : 4)*2 + 1)|1;

opencv認爲半徑爲3*sigma的窗口就是高斯函數能量最集中的區域。(爲什麼在圖像深度不是8U的時候,使用4*sigma半徑的窗口就不得而知了,有高人指點一下?)

autopan0-sift-c是圖像拼接軟件hugin裏面的sift實現,在實現DoG的時候需要做不同尺度的高斯平滑,實現如下,在GaussianConvolution_new1函數中:

dim = 1 + 2 * ((int) (3.0 * sigma));

可見autopano也是實現的3*sigma半徑的窗口。

在GIMP裏,實現比較奇特,在blur_gauss.c的make_rle_curve函數裏面,

const gdouble sigma2 = 2 * sigma * sigma;
const gdouble l = sqrt (-sigma2 * log (1.0 / 255.0));
int n = ceil (l) * 2;
if ((n % 2) == 0)
n += 1;

我是沒看懂那個 log (1.0 / 255.0)是幹嘛的。。。慚愧。效果來看,這個實現的窗口半徑是約等於2.2*sigma。

然後是第二個問題:給定窗口大小,怎麼計算sigma?

opencv的實現,在cvFilter.cpp的init_gaussian_kernel函數中:

sigmaX = sigma > 0 ? sigma : (n/2 – 1)*0.3 + 0.8;

再次不可解。。。乘以0.3還可以接受,加上0.8是爲嘛啊?

autopano沒有實現這個特性。

GIMP的實現:

/* we want to generate a matrix that goes out a certain radius
* from the center, so we have to go out ceil(rad-0.5) pixels,
* inlcuding the center pixel. Of course, that’s only in one direction,
* so we have to go the same amount in the other direction, but not count
* the center pixel again. So we double the previous result and subtract
* one.
* The radius parameter that is passed to this function is used as
* the standard deviation, and the radius of effect is the
* standard deviation * 2. It’s a little confusing.
*/
radius = fabs (radius) + 1.0;

std_dev = radius;
radius = std_dev * 2;
/* go out ‘radius’ in each direction */
matrix_length = 2 * ceil (radius – 0.5) + 1;

註釋講的很清楚了,基本上就是認爲sigma應該等於窗口半徑的一半。

看完這三分源碼,結論就是,關於sigma和半徑,你愛怎麼算就怎麼算吧,差不多就行。。。(額。。費了半天勁,就得到這麼一句廢話啊)。

第三個問題是可分離濾波器:

由於高斯函數可以寫成可分離的形式,因此可以採用可分離濾波器實現來加速。所謂的可分離濾波器,就是可以把多維的卷積化成多個一維卷積。具體到二維 的高斯濾波,就是指先對行做一維卷積,再對列做一維卷積。這樣就可以將計算複雜度從O(M*M*N*N)降到O(2*M*M*N),M,N分別是圖像和濾 波器的窗口大小。問題是實現時候怎麼計算一維的卷積核呢?

其實很簡單,按照前面計算出來的窗口大小,計算所有離散點上一維高斯函數的權值,最後別忘了將權值之和歸一化到1.
有碼有真相,來自opencv:

for( i = 0; i <= n/2; i++ )
{
double t = fixed_kernel ? (double)fixed_kernel[i] : exp(scale2X*i*i);
if( type == CV_32FC1 )
{
cf[(n/2+i)*step] = (float)t;
sum += cf[(n/2+i)*step]*2;
}
else
{
cd[(n/2+i)*step] = t;
sum += cd[(n/2+i)*step]*2;
}
}

sum = 1./sum;
for( i = 0; i <= n/2; i++ )
{
if( type == CV_32FC1 )
cf[(n/2+i)*step] = cf[(n/2-i)*step] = (float)(cf[(n/2+i)*step]*sum);
else
cd[(n/2+i)*step] = cd[(n/2-i)*step] = cd[(n/2+i)*step]*sum;
}

終於寫完了,希望對各位有所幫助。不對之處請指正哈。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章