原文鏈接:http://www.juzicode.com/opencv-python-gaussianblur-bilateralfilter
OpenCV-Python教程:均值平滑、中值平滑 一文中介紹了在滑動窗口內均值的方式進行平滑處理,這時窗口中心點和窗口領域內的所有像素的加權系數都是一樣的,中值平滑提取中位數時滑動窗口內任一像素出現中值的概率也是相同的。本文要介紹的高斯平滑則根據距離中心點的間距遠近其權重會不同,這種方式看起來更符合”慣例”:身邊的人對你影響會更大。
1、高斯平滑GaussianBlur()
所謂高斯平滑,聞其名就知道和”高斯”這位大神有關,這里實際用到的是高斯分布。我們先來看下一維高斯分布(正態分布)的概率密度函數:
這是一個以自然常數e為底的二次指數函數,其中μ是均值,σ是標准差,我們可以用matplotlib繪圖,設置μ=0,σ=0.8:
#juzicode.com/vx:桔子code
import numpy as np
import matplotlib.pyplot as plt
plt.rc('font',family='Youyuan',size='9')
plt.rc('axes',unicode_minus='False')
mu=0 #均值
sigma=0.8 #標准差
x=np.linspace(-3,3,60)
y=np.exp((-(x-mu)**2)/(2*(sigma**2)))/(np.sqrt(2*np.pi)*sigma)
plt.plot(x,y,"b-",)
plt.grid(True)
因為在OpenCV的高斯平滑中用到的正態分布都是取μ=0,這里就不對比u值差異,只看下取不同的σ的曲線,
#juzicode.com/vx:桔子code
import numpy as np
import matplotlib.pyplot as plt
plt.rc('font',family='Youyuan',size='9')
plt.rc('axes',unicode_minus='False')
x=np.linspace(-3,3,100)
mu=0 # 均值μ
sigma=0.3 # 標准差σ
y=np.exp((-(x-mu)**2)/(2*(sigma**2)))/(np.sqrt(2*np.pi)*sigma)
sigma=0.5
y2=np.exp((-(x-mu)**2)/(2*(sigma**2)))/(np.sqrt(2*np.pi)*sigma)
sigma=1.1
y3=np.exp((-(x-mu)**2)/(2*(sigma**2)))/(np.sqrt(2*np.pi)*sigma)
plt.plot(x,y,"b-",)
plt.plot(x,y2,"g-",)
plt.plot(x,y3,"r-",)
plt.grid(True)
從高斯分布的曲線可以看到(μ=0時):
- 當x=0時,f(x)的值最大,當x向兩邊變化時,f(x)的值越來越小;
- 這個曲線在x=0的2側是對稱的,f(-1)=f(1),f(-2)=f(2);
- σ越大,曲線越平坦,x=0時的取值越低。
因為圖像是二維的,實際上在OpenCV中要用到二維高斯分布,可以從一維高斯分布推導出二維高斯分布(這里假設x,y不相關),其中x表示鄰域像素距離中心點水平方向的間距,y表示垂直方向的間距:
用matplotlib繪制二維高斯分布:
#juzicode.com/vx:桔子code
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import cm
from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure()
ax = Axes3D(fig)
X = np.arange(-3, 3.1, 0.1,dtype=np.float64).reshape(-1,1)
Y = np.arange(-3, 3.1, 0.1,dtype=np.float64)
mux,muy=0,0
sigmax,sigmay = 0.8,0.8
expont = -0.5*(((X-mux)/sigmax)**2 + ((Y-muy)/sigmay)**2)
Z=np.exp(expont)/(2*np.pi*sigmax*sigmay)
ax.plot_surface(X, Y, Z, rstride=2, cstride=2, cmap=cm.viridis)
plt.show()
二維高斯分布也具備一維高斯分布類似的特點:在f(0,0)處取值最大(μ=0時),σ越大曲線越平坦。
有了前面繪制曲線得到的直觀感受,下面我們用numpy構造一個5×5大小的高斯核(滑動窗口):
#juzicode.com/vx:桔子code
import numpy as np
X = np.arange(-2, 3, 1,dtype=np.float64).reshape(-1,1)#轉置
Y = np.arange(-2, 3, 1,dtype=np.float64) #[-2. -1. 0. 1. 2.]
mux,muy=0,0
sigmax,sigmay = 0.8,0.8
expont = -0.5*(((X-mux)/sigmax)**2 + ((Y-muy)/sigmay)**2)
Z=np.exp(expont)/(2*np.pi*sigmax*sigmay)
Z=Z/np.sum(Z) #歸一化
print(Z)
[[0.00048091 0.00501119 0.01094545 0.00501119 0.00048091]
[0.00501119 0.0522178 0.11405416 0.0522178 0.00501119]
[0.01094545 0.11405416 0.2491172 0.11405416 0.01094545]
[0.00501119 0.0522178 0.11405416 0.0522178 0.00501119]
[0.00048091 0.00501119 0.01094545 0.00501119 0.00048091]]
從這個5×5大小的矩陣也能看到,位於正中心的點取值為0.2491172是最大值的點,緊鄰着正中心距離為1的周圍4個點的值0.11405416是第2大的點,依次越往外取值越小。
GaussianBlur()的接口形式:
dst=cv2.GaussianBlur(src, ksize, sigmaX[, dst[, sigmaY[, borderType]]])
- 參數含義:
- src:通道數任意,實際處理是分通道處理;圖像深度只能是CV_8U, CV_16U, CV_16S, CV_32F or CV_64F;
- ksize:元組類型,窗口大小,寬度和高度可以不一樣,但是必須是正的奇數;如果設置為0,則根據sigma計算得到。
- sigmaX:圖像X方向的標准差,對應前述二維高斯分布的σ1;
- sigmaY:圖像Y方向的標准差,對應前述二維高斯分布的σ2,如果傳入0,會等於sigmaX,如果sigmaX和sigmaY都傳入0,sigmaX和sigmaX則根據ksize計算;
- borderType:邊界處理方式;
注意:GaussianBlur的首字母是大寫的G,和其他大多數OpenCV函數名稱采用小駝峰命名風格稍有差異。
因為都是以滑動窗口中心點為原點,為了保證中心點(x,y)=(0,0)的權重為最大值,所以在OpenCV的高斯平滑中μ1和μ2都設置為0,這樣在調用高斯平滑函數時只需要傳入σ1(sigmaX)和σ2(sigmaY)。
下面是用不同的ksize進行高斯平滑的例子:
import matplotlib.pyplot as plt
import cv2
print('VX公眾號: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__)
plt.rc('font',family='Youyuan',size='9')
img = cv2.imread('..\\lena.jpg')
img_ret1 = cv2.GaussianBlur(img,(3,3),0)
img_ret2 = cv2.GaussianBlur(img,(5,5),0)
img_ret3 = cv2.GaussianBlur(img,(11,11),0)
#顯示圖像
fig,ax = plt.subplots(2,2)
ax[0,0].set_title('VX:桔子code 原圖')
ax[0,0].imshow(cv2.cvtColor(img,cv2.COLOR_BGR2RGB)) #matplotlib顯示圖像為rgb格式
ax[0,1].set_title('GaussianBlur ksize=3')
ax[0,1].imshow(cv2.cvtColor(img_ret1,cv2.COLOR_BGR2RGB))
ax[1,0].set_title('GaussianBlur ksize=5')
ax[1,0].imshow(cv2.cvtColor(img_ret2,cv2.COLOR_BGR2RGB))
ax[1,1].set_title('GaussianBlur ksize=11')
ax[1,1].imshow(cv2.cvtColor(img_ret3,cv2.COLOR_BGR2RGB))
ax[0,0].axis('off');ax[0,1].axis('off');ax[1,0].axis('off');ax[1,1].axis('off')#關閉坐標軸顯示
plt.show()
從運行結果看,ksize越大,圖像越模糊。
下面是ksize保持不變,sigmaX變化的例子(sigmaY不傳入默認等於sigmaX):
import matplotlib.pyplot as plt
import cv2
print('VX公眾號: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__)
plt.rc('font',family='Youyuan',size='9')
img = cv2.imread('..\\lena.jpg')
img_ret1 = cv2.GaussianBlur(img,(5,5),0.5)
img_ret2 = cv2.GaussianBlur(img,(5,5),10)
img_ret3 = cv2.GaussianBlur(img,(5,5),25)
#顯示圖像
fig,ax = plt.subplots(2,2)
ax[0,0].set_title('VX:桔子code 原圖')
ax[0,0].imshow(cv2.cvtColor(img,cv2.COLOR_BGR2RGB)) #matplotlib顯示圖像為rgb格式
ax[0,1].set_title('GaussianBlur ksize=5 sigma=0.5')
ax[0,1].imshow(cv2.cvtColor(img_ret1,cv2.COLOR_BGR2RGB))
ax[1,0].set_title('GaussianBlur ksize=5 sigma=10')
ax[1,0].imshow(cv2.cvtColor(img_ret2,cv2.COLOR_BGR2RGB))
ax[1,1].set_title('GaussianBlur ksize=5 sigma=25')
ax[1,1].imshow(cv2.cvtColor(img_ret3,cv2.COLOR_BGR2RGB))
ax[0,0].axis('off');ax[0,1].axis('off');ax[1,0].axis('off');ax[1,1].axis('off')#關閉坐標軸顯示
plt.show()
對應到高斯分布曲線的特性,當sigma越大時,原點的取值越小,周圍點的取值更大,對應到圖像上中心點的權重越低,周圍點權重越高,所以sigma越大圖像越模糊。
2、雙邊平滑bilateralFilter()
均值、中值、高斯平滑的去躁是一種“無差別攻擊”,所有的像素都受到同一個加權系數的影響,所以在平滑過程中也會影響到圖像的邊沿(像素值突變的地方),接下來要介紹的雙邊濾波則可以在去除噪聲的同時又能保持圖像的邊沿,也就是傳說中的”去噪保邊”。
雙邊平滑使用的加權系數如下:
第1個exp()函數是空間距離加權系數的簡化,1)前面的1/2πσ1σ2系數沒有了,因為該系數在σ1和σ2確定后最后歸一化的時候是會被消除掉的;2)雙邊平滑x和y方向用的σ1=σ2,所以用一個σs代替;3)u值在高斯平滑中也一直設置為0,這里也直接去掉了。其中x和y分別表示鄰域像素和中心點X和Y方向的距離。
第2個exp()函數也是高斯函數,v(x0,y0)表示中心點的像素值,v(x,y)表示距離中心點距離為(x,y)的像素值,所以這部分的值就和像素差值有關。如果像素差值為0,這部分系數的值就為1,所以就等價於高斯平滑的系數;如果2個像素差值非常大,這個部分exp()系數就向0靠攏,最后的f(x,y)就向0靠攏,這時該像素值對中心點的影響就非常小。所以如果2個像素值差異非常大時,這時新生成圖像的像素值就不會被這個差異極大的像素受影響,從而保持該差異,達到“保邊”的效果。
2個exp()函數就是雙邊平滑的“雙邊”的含義,它並不是指圖像的X和Y(行、列)2個方向,而是指在像素差和空間距離的2個“邊”。
bilateralFilter()的接口形式:
dst=cv2.bilateralFilter(src, d, sigmaColor, sigmaSpace[, dst[, borderType]])
- 參數含義:
- src:8bit或浮點類型;1或3通道;
- d:窗口大小,如果為非正數,根據sigmaSpace計算;d>5時速度會比較慢,當噪聲比較嚴重時可以選擇d>=9,但是此時不適合對時間敏感的處理;
- sigmaColor:亮度差的sigma參數;
- sigmaSpace:空間距離的sigma參數,同時作用於圖像的X和Y(行、列)2個方向;
- borderType:邊界處理方式;
在表示滑動窗口大小的參數里,高斯平滑的ksize參數是一個寬高可以不等的元組,但是雙邊平滑用的參數d是一個整型類型,這也決定了其滑動窗口寬高是相等的。另外d的大小在源碼中是這樣設置的:
if( d <= 0 )
radius = cvRound(sigma_space*1.5);
else
radius = d/2;
radius = MAX(radius, 1);
d = radius*2 + 1;
如果d小於0,用sigma_space*1.5后取整得到半徑,如果d大於0則先除以2得到半徑radius,然后用半徑radius和1比較取其中更大的值,最后乘以2加1,這樣的得到的d可以保證是不小於3的奇數。
sigmaColor和sigmaSpace參數比較小的時候(<10),平滑的效果不是很明顯,當參數比較大或者多次平滑后的圖像看起來會比較卡通化,詳情可閱讀:論如何把自己變成卡通人物(OpenCV制作卡通化頭像)。
下面的是一個高斯平滑和雙邊平滑對比的例子,取相同的滑動窗口大小以及相同的sigma值,觀察平滑后的差異,以及在平滑后取ksize=3計算sobel邊沿:
import matplotlib.pyplot as plt
import cv2
print('VX公眾號: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__)
plt.rc('font',family='Youyuan',size='9')
img = cv2.imread('..\\samples\\picture\\streak.jpeg')
img_ret1 = cv2.GaussianBlur(img,(7,7),10)
img_ret2 = cv2.bilateralFilter(img,7,10,10)
img_ret3 = cv2.bilateralFilter(img,7,25,25)
fig,ax = plt.subplots(2,2)
ax[0,0].set_title('VX:桔子code 原圖')
ax[0,0].imshow(cv2.cvtColor(img,cv2.COLOR_BGR2RGB)) #matplotlib顯示圖像為rgb格式
ax[0,1].set_title('GaussianBlur ksize=7 sigma=10')
ax[0,1].imshow(cv2.cvtColor(img_ret1,cv2.COLOR_BGR2RGB))
ax[1,0].set_title('bilateralFilter ksize=7 sigma=10')
ax[1,0].imshow(cv2.cvtColor(img_ret2,cv2.COLOR_BGR2RGB))
ax[1,1].set_title('bilateralFilter ksize=7 sigma=25')
ax[1,1].imshow(cv2.cvtColor(img_ret3,cv2.COLOR_BGR2RGB))
ax[0,0].axis('off');ax[0,1].axis('off');ax[1,0].axis('off');ax[1,1].axis('off')#關閉坐標軸顯示
img_edge1=cv2.Sobel(img_ret1,cv2.CV_8U,1,0,ksize=3)
img_edge2=cv2.Sobel(img_ret2,cv2.CV_8U,1,0,ksize=3)
img_edge3=cv2.Sobel(img_ret3,cv2.CV_8U,1,0,ksize=3)
fig2,ax2 = plt.subplots(2,2)
ax2[0,0].set_title('VX:桔子code 原圖')
ax2[0,0].imshow(cv2.cvtColor(img,cv2.COLOR_BGR2RGB)) #matplotlib顯示圖像為rgb格式
ax2[0,1].set_title('GaussianBlur ksize=7 sigma=10')
ax2[0,1].imshow(cv2.cvtColor(img_edge1,cv2.COLOR_BGR2RGB))
ax2[1,0].set_title('bilateralFilter ksize=7 sigma=10')
ax2[1,0].imshow(cv2.cvtColor(img_edge2,cv2.COLOR_BGR2RGB))
ax2[1,1].set_title('bilateralFilter ksize=7 sigma=25')
ax2[1,1].imshow(cv2.cvtColor(img_edge3,cv2.COLOR_BGR2RGB))
ax2[0,0].axis('off');ax2[0,1].axis('off');ax2[1,0].axis('off');ax2[1,1].axis('off')#關閉坐標軸顯示
plt.show()
平滑后的對比:
取sobel邊沿對比:
從運行結果可看到,在顏色突然變化的地方(邊沿),高斯平滑只保留了一半左右的邊界,而雙邊平滑幾乎將所有的邊界保留下來,而且高斯平滑的邊界亮度平均值也沒有雙邊平滑高。
小結:高斯平滑對比均值和中值平滑其取值更符合“慣例”,在空間距離上距離越近的像素用來計算新像素的值其權重越大。均值平滑、中值平滑和高斯平滑會對整幅圖像實現無差別的平滑,一個固定系數的滑動窗口作用於整個圖像,所以平滑后的圖像雖然處理掉了噪聲,但是邊沿部分也會被削弱。而雙邊平滑在高斯平滑使用的系數基礎上乘以像素差值的高斯函數,和中心點像素差值越大整個系數值越小,最后就能達到去躁保邊的效果。