高斯濾波器是根據高斯函數來選擇權值的線性平滑濾波器,對隨機分布和服從正態分布的噪聲有很好地濾除效果。本文從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)