圖像平滑去噪之高斯濾波器


高斯濾波器是根據高斯函數來選擇權值的線性平滑濾波器,對隨機分布和服從正態分布的噪聲有很好地濾除效果。本文從opencv內置的高斯濾波函數入手,深入介紹高斯濾波器的原理與實現。

 

一、高斯分布函數與高斯卷積核

 


 

高斯分布函數指的就是概率論中的正態分布的概率密度函數,均值μ=0時的一維形式和二維形式如下。 其中σ為正態分布的標准偏差,其值決定了函數的衰減快慢。

         

 

 

從這兩個公式不難看出,二維公式其實等於兩個一維函數相乘。從概率論角度看,因為隨機變量X,Y是相互獨立的,那么他們的聯合概率密度就等於邊緣概率密度之積。這個特性是非常重要的,后面將再次提及。現在讓我們先看一下高斯函數的圖像分布與二維高斯卷積核的樣子:

           

 

圖像上,靠近原點的位置地勢高,距離原點越遠則地勢越低。相應地,卷積核也是中心數值最大,並向四周減小,減小的幅度並不是隨意的,而是要求整個卷積核近似高斯函數的圖像。由於高斯濾波實質是一種加權平均濾波,為了實現平均,核還帶有一個系數,例如上圖中的十六分之一、八十四分之一。這些系數等於矩陣中所有數值之和的倒數。

 

 

 

 

二、高斯濾波

 


 

在實際濾波中,將當前像素作為核中心,利用卷積核對周圍鄰域像素作加權平均,其值作為當前像素的新值。這個過程實質上也是矩陣的卷積(矩陣卷積要求其中一個矩陣要旋轉180度,在圖像處理時,卷積核可以認為是已經過旋轉的矩陣),因此才有卷積核這樣的稱呼。對於圖像邊界點,有多種處理方法,如對圖像進行擴展、縮小高斯核等。OpenCV中提供了高斯濾波函數:

 

 

src為輸入的圖像,可以是單通道灰度圖像,也可以是三通道RGB圖像;ksize是卷積核大小,接受tuple類型;sigmaX是水平方向的標准差,sigmaY是垂直方向的標准差,這兩個參數可以設置為0,函數內部會根據ksize自動計算標准差,標准差越大,核就越大,核變化趨勢就越緩;borderType是邊界的處理方式,采取默認值即可。以下是OpenCV-Python的濾波實例:

import cv2

img = cv2.imread('D:/programe/matlab/img/man.jpg')
gaussian = cv2.GaussianBlur(img,ksize=(5,5),sigmaX=0,sigmaY=0)
cv2.imshow('img',img)
cv2.imshow('gaussian',gaussian)
cv2.waitKey(0)

 

濾波前后對比如下圖。因為高斯濾波是以空間距離來確定權重大小的,但並沒有考慮用顏色距離來確定權重,這樣就導致了高斯濾波在去除噪聲的同時,也一定程度上模糊了邊界。

  

 

 

 

 

三、高斯卷積核的構造方法

 


 OpenCV本身就提供了高斯核的生成函數,不過它只能構建一維的核,但是根據上面我們提到過的一維乘積性質,我們可以通過兩個一維核向量相乘得到一個二維核,該函數接口和代碼如下:

#二維高斯卷積核
def GetGaussianKernel2D(ksize,sigma):
    kernelX = cv2.getGaussianKernel(ksize[0],sigma).reshape(ksize[0],1)#行向量
    kernelY = cv2.getGaussianKernel(ksize[1],sigma).reshape(1,ksize[1])#列向量
    kernel =  kernelX*kernelY
    return kernel
print(GetGaussianKernel2D((3,3),0.8))
#[[0.05711826 0.12475775 0.05711826]
# [0.12475775 0.27249597 0.12475775]
# [0.05711826 0.12475775 0.05711826]]

 

這里sigma是根據經驗指定的,實際上更多地是讓函數自動計算,標准差的計算公式為:

由於我們需要的是二維核,故可以設置兩個標准差,分別表示X方向和Y方向,這兩個標准差默認采用公式計算。最終獲得的卷積核是小數形式,但是我們像素卷積需要整數,同時為了減小計算開銷,我們希望核的值為較小的整數。因此,最常用的做法是將角點化為1,其它點相應乘以相同倍數再四舍五入或者直接去除小數部分,這樣使得整個核的值較小,可以加快計算。由於標准差指定可能不同,因此還是會有很多相同大小但值不同的核存在,這並不需要驚訝,因為這些核都符合高斯分布,只是在實際濾波時效果有略微差異罷了。

#二維高斯卷積核
def GetGaussianKernel2D(ksize,sigmaX=None,sigmaY=None):
    if sigmaX is None:
        sigmaX = 0.3*((ksize[0]-1)*0.5-1)+0.8
    if sigmaY is None:
        sigmaY = 0.3 * ((ksize[1] - 1) * 0.5 - 1) + 0.8
    kernelX = cv2.getGaussianKernel(ksize[0],sigmaX).reshape(ksize[0],1)
    kernelY = cv2.getGaussianKernel(ksize[1],sigmaY).reshape(1,ksize[1])
    kernel =  kernelX*kernelY
    kernel =  kernelX*kernelY
    k = 1/kernel[0,0]
    kernel = np.int32(kernel*k+0.5)
    return kernel
print(GetGaussianKernel2D((3,3)))
#[[1 2 1]
# [2 5 2]
# [1 2 1]]

 

 

 

 

四、高斯卷積核的構造原理

 


 

我們也可以自己構造核,但首先我們要先知道高斯核與高斯函數之間的關系。以核中心為平面中心,則核的每個元素都對應一個坐標(x,y)如下圖所示:

將每個元素對應的坐標代入到二維高斯函數中,得到的函數值作為該位置的值,再經歸一化和整數化后就可以得到最終所要的卷積核。

首先構造高斯函數,Python中我沒找到可以直接用的接口,所以就自定義算了,注意二維可以不用再寫一遍公式,可以通過一維高斯獲取:

#一維高斯函數
def Gaussian(x,sigma=1.0):
    result = 1/(np.sqrt(2*np.pi)*sigma)*np.exp(-(x/sigma)**2/2)
    return result

#二維高斯函數
def Gaussian2D(x,y,sigmaX=1.0,sigmaY=1.0):
    result = Gaussian(x,sigmaX)*Gaussian(y,sigmaY)
    return result

 

根據高斯核與高斯函數的關系,定義一維高斯核構造函數:

#一維高斯卷積核
def GaussianKernel(length,sigma=None):
    radius = length//2
    kernel = np.zeros(length,dtype=np.float32)
    if sigma is None:
        sigma = 0.3*((length-1)*0.5-1)+0.8
    for i in range(length):
        kernel.itemset(i,Gaussian(i-radius,sigma=sigma))
    #歸一化
    sum = np.sum(kernel)
    kernel = kernel/sum
    return kernel

 

上面的函數就類似於OpenCV的getGaussianKernel。因此二維核也采用相同方法創建即可:

#二維高斯卷積核
def GaussianKernel2D(ksize,sigmaX=None,sigmaY=None):
    kernelX = GaussianKernel(ksize[0],sigmaX).reshape(ksize[0],1)
    kernelY = GaussianKernel(ksize[1],sigmaY).reshape(1,ksize[1])
    kernel =  kernelX*kernelY
    #整數化
    k = 1 / kernel[0, 0]
    kernel = np.int32(kernel * k + 0.5)
    return kernel

 

 

 

 

五、卷積可分離性


 

在高斯卷積應用中,可能會用到較大的卷積核,此時程序性能就不太令人滿意了。卷積可分離性就是解決這種問題的一種優化手段。對於高斯卷積來說,我們可以將圖像先與一維水平卷積核相卷積,再與一維垂直方向的卷積核相卷積,所得結果跟直接用二維核來卷積相同,這就是高斯卷積核的可分離性。利用該分離性,卷積復雜度大大降低,並且我們也不需要生成二維卷積核,而只要生成一維核即可,這也減少了計算量。在實際應用中,甚至還可以直接存儲常用維數的高斯核數值來提升性能。

 

下面談一談卷積可分離性的原理,不感興趣的讀者可直接閱讀下一節。

假設A為n維列向量,B為n維行向量,則有A*B(卷積)=B*A=A·B。例如n=3時,如下圖所示:

        

 

 A對B的卷積(A作為核)需要注意先將A向量旋轉180度,再做加權求和操作,過程如下:

 

前面說過高斯核Kernel2D=KernelX·KernelY,那么卷積過程就可以表示為:Dst=Src*Kernel2D=(Src*KernelX)*KernelY=(Src*KernelY)*KernelX。一般來說,無論對於何種卷積濾波,只要其卷積核可以拆解為兩個行向量與列向量的乘積,那么就算卷積可分離。

 

 

 

 

 

六、優化的高斯濾波實現

 


 這一節綜合了上面所有代碼,並且添加了優化的自定義高斯濾波函數:

import numpy as np
import cv2


#一維高斯函數
def Gaussian(x,sigma=1.0):
    result = 1/(np.sqrt(2*np.pi)*sigma)*np.exp(-(x/sigma)**2/2)
    return result

#二維高斯函數
def Gaussian2D(x,y,sigmaX=1.0,sigmaY=1.0):
    result = Gaussian(x,sigmaX)*Gaussian(y,sigmaY)
    return result

#一維高斯卷積核
def GaussianKernel(length,sigma=None):
    radius = length//2
    kernel = np.zeros(length,dtype=np.float32)
    if sigma is None:
        sigma = 0.3*((length-1)*0.5-1)+0.8
    for i in range(length):
        kernel.itemset(i,Gaussian(i-radius,sigma=sigma))
    #歸一化
    sum = np.sum(kernel)
    kernel = kernel/sum
    return kernel

#二維高斯卷積核
def GaussianKernel2D(ksize,sigmaX=None,sigmaY=None):
    kernelX = GaussianKernel(ksize[0],sigmaX).reshape(ksize[0],1)
    kernelY = GaussianKernel(ksize[1],sigmaY).reshape(1,ksize[1])
    kernel =  kernelX*kernelY
    k = 1 / kernel[0, 0]
    kernel = np.int32(kernel * k + 0.5)
    return kernel

#二維高斯卷積核
def GetGaussianKernel2D(ksize,sigmaX=None,sigmaY=None):
    if sigmaX is None:
        sigmaX = 0.3*((ksize[0]-1)*0.5-1)+0.8
    if sigmaY is None:
        sigmaY = 0.3 * ((ksize[1] - 1) * 0.5 - 1) + 0.8
    kernelX = cv2.getGaussianKernel(ksize[0],sigmaX).reshape(ksize[0],1)
    kernelY = cv2.getGaussianKernel(ksize[1],sigmaY).reshape(1,ksize[1])
    kernel =  kernelX*kernelY
    k = 1/kernel[0,0]
    kernel = np.int32(kernel*k+0.5)
    return kernel

#優化的高斯濾波
def GaussianFilter(src,ksize):
    #卷積分離
    kernelX = GaussianKernel(ksize[0])
    kernelX = np.resize(kernelX,(ksize[0],1))
    kernelY = GaussianKernel(ksize[1])
    kernelX = np.resize(kernelX, (1,ksize[1]))
    dst = cv2.filter2D(src,-1,kernelX)
    dst = cv2.filter2D(dst,-1,kernelY)
    return dst


if __name__ == '__main__':
    src=cv2.imread('man.jpg')
    dst = GaussianFilter(src,(5,5))
    cv2.imshow('src',src)
    cv2.imshow('dst',dst)
    cv2.waitKey(0)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM