部分 IV
OpenCV 中的圖像處理
16 圖像平滑
目標
• 學習使用不同的低通濾波器對圖像進行模糊
• 使用自定義的濾波器對圖像進行卷積(2D 卷積)
2D 卷積
與一維信號一樣,我們也可以對 2D 圖像實施低通濾波(LPF),高通濾波(HPF)等。LPF 幫助我們去除噪音,模糊圖像。HPF 幫助我們找到圖像的邊緣
OpenCV 提供的函數 cv.filter2D() 可以讓我們對一幅圖像進行卷積操作。下面我們將對一幅圖像使用平均濾波器。下面是一個 5x5 的平均濾波器核:
操作如下:將核放在圖像的一個像素 A 上,求與核對應的圖像上 25(5x5)個像素的和,在取平均數,用這個平均數替代像素 A 的值。重復以上操作直到將圖像的每一個像素值都更新一邊。代碼如下,運行一下吧。
import cv2 import numpy as np from matplotlib import pyplot as plt img = cv2.imread('opencv_logo.png') kernel = np.ones((5,5),np.float32)/25 dst = cv2.filter2D(img,-1,kernel) plt.subplot(121),plt.imshow(img),plt.title('Original') plt.xticks([]), plt.yticks([]) plt.subplot(122),plt.imshow(dst),plt.title('Averaging') plt.xticks([]), plt.yticks([]) plt.show()
結果:
圖像模糊(圖像平滑)
使用低通濾波器可以達到圖像模糊的目的。這對與去除噪音很有幫助。其實就是去除圖像中的高頻成分(比如:噪音,邊界)。所以邊界也會被模糊一點。(當然,也有一些模糊技術不會模糊掉邊界)。OpenCV 提供了四種模糊技術。
16.1 平均
這是由一個歸一化卷積框完成的。他只是用卷積框覆蓋區域所有像素的平均值來代替中心元素。可以使用函數 cv2.blur() 和 cv2.boxFilter() 來完這個任務。可以同看查看文檔了解更多卷積框的細節。我們需要設定卷積框的寬和高。下面是一個 3x3 的歸一化卷積框:
注意:如果你不想使用歸一化卷積框,你應該使用 cv2.boxFilter(),這時要傳入參數 normalize=False。
下面與第一部分一樣的一個例子:
import cv2 import numpy as np from matplotlib import pyplot as plt img = cv2.imread('opencv_logo.png') blur = cv2.blur(img,(5,5)) plt.subplot(121),plt.imshow(img),plt.title('Original') plt.xticks([]), plt.yticks([]) plt.subplot(122),plt.imshow(blur),plt.title('Blurred') plt.xticks([]), plt.yticks([]) plt.show()
結果:
16.2 高斯模糊
現在把卷積核換成高斯核(簡單來說,方框不變,將原來每個方框的值是相等的,現在里面的值是符合高斯分布的,方框中心的值最大,其余方框根據距離中心元素的距離遞減,構成一個高斯小山包。原來的求平均數現在變成求加權平均數,全就是方框里的值)。實現的函數是 cv2.GaussianBlur()。我們需要指定高斯核的寬和高(必須是奇數)。以及高斯函數沿 X,Y 方向的標准差。如果我們只指定了 X 方向的的標准差,Y 方向也會取相同值。如果兩個標准差都是 0,那么函數會根據核函數的大小自己計算。高斯濾波可以有效的從圖像中去除高斯噪音。
如果你願意的話,你也可以使用函數 cv2.getGaussianKernel() 自己構建一個高斯核。
如果要使用高斯模糊的話,上邊的代碼應該寫成:
#0 是指根據窗口大小( 5,5 )來計算高斯函數標准差 blur = cv2.GaussianBlur(img,(5,5),0)
結果:
16.3 中值模糊
顧名思義就是用與卷積框對應像素的中值來替代中心像素的值。這個濾波器經常用來去除椒鹽噪聲。前面的濾波器都是用計算得到的一個新值來取代中心像素的值,而中值濾波是用中心像素周圍(也可以使他本身)的值來取代他。他能有效的去除噪聲。卷積核的大小也應該是一個奇數。
在這個例子中,我們給原始圖像加上 50% 的噪聲然后再使用中值模糊。
代碼:
median = cv2.medianBlur(img,5)
# 運行有問題,出不了如下結果,下圖為照搬的
結果:
16.4 雙邊濾波
函數 cv2.bilateralFilter() 能在保持邊界清晰的情況下有效的去除噪音。但是這種操作與其他濾波器相比會比較慢。我們已經知道高斯濾波器是求中心點鄰近區域像素的高斯加權平均值。這種高斯濾波器只考慮像素之間的空間關系,而不會考慮像素值之間的關系(像素的相似度)。所以這種方法不會考慮一個像素是否位於邊界。因此邊界也會別模糊掉,而這正不是我們想要。雙邊濾波在同時使用空間高斯權重和灰度值相似性高斯權重。空間高斯函數確保只有鄰近區域的像素對中心點有影響,灰度值相似性高斯函數確保只有與中心像素灰度值相近的才會被用來做模糊運算。所以這種方法會確保邊界不會被模糊掉,因為邊界處的灰度值變化比較大。
進行雙邊濾波的代碼如下:
#cv2.bilateralFilter(src, d, sigmaColor, sigmaSpace) #d – Diameter of each pixel neighborhood that is used during filtering. # If it is non-positive, it is computed from sigmaSpace #9 鄰域直徑,兩個 75 分別是空間高斯函數標准差,灰度值相似性高斯函數標准差 blur = cv2.bilateralFilter(img,9,75,75)
# 運行有問題,出不了如下結果,下圖為照搬的
結果:
看見了把,上圖中的紋理被模糊掉了,但是邊界還在。
17 形態學轉換
目標
• 學習不同的形態學操作,例如腐蝕,膨脹,開運算,閉運算等
• 我們要學習的函數有:cv2.erode(),cv2.dilate(),cv2.morphologyEx()
等
原理
形態學操作是根據圖像形狀進行的簡單操作。一般情況下對二值化圖像進行的操作。需要輸入兩個參數,一個是原始圖像,第二個被稱為結構化元素或核,它是用來決定操作的性質的。兩個基本的形態學操作是腐蝕和膨脹。他們的變體構成了開運算,閉運算,梯度等。我們會以下圖為例逐一介紹它們。
17.1 腐蝕
就像土壤侵蝕一樣,這個操作會把前景物體的邊界腐蝕掉(但是前景仍然是白色)。這是怎么做到的呢?卷積核沿着圖像滑動,如果與卷積核對應的原圖像的所有像素值都是 1,那么中心元素就保持原來的像素值,否則就變為零。
這會產生什么影響呢?根據卷積核的大小靠近前景的所有像素都會被腐蝕掉(變為 0),所以前景物體會變小,整幅圖像的白色區域會減少。這對於去除白噪聲很有用,也可以用來斷開兩個連在一塊的物體等。
這里我們有一個例子,使用一個 5x5 的卷積核,其中所有的值都是以。讓我們看看他是如何工作的:
import cv2 import numpy as np img = cv2.imread('j.png',0) kernel = np.ones((5,5),np.uint8) erosion = cv2.erode(img,kernel,iterations = 1)
結果:
17.2 膨脹
與腐蝕相反,與卷積核對應的原圖像的像素值中只要有一個是 1,中心元素的像素值就是 1。所以這個操作會增加圖像中的白色區域(前景)。一般在去噪聲時先用腐蝕再用膨脹。因為腐蝕在去掉白噪聲的同時,也會使前景對象變小。所以我們再對他進行膨脹。這時噪聲已經被去除了,不會再回來了,但是前景還在並會增加。膨脹也可以用來連接兩個分開的物體。
dilation = cv2.dilate(img,kernel,iterations = 1)
結果:
17.3 開運算
先進性腐蝕再進行膨脹就叫做開運算。就像我們上面介紹的那樣,它被用來去除噪聲。這里我們用到的函數是 cv2.morphologyEx()。
opening = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)
結果:
17.4 閉運算
先膨脹再腐蝕。它經常被用來填充前景物體中的小洞,或者前景物體上的小黑點。
closing = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)
結果:
17.5 形態學梯度
其實就是一幅圖像膨脹與腐蝕的差別。
結果看上去就像前景物體的輪廓。
gradient = cv2.morphologyEx(img, cv2.MORPH_GRADIENT, kernel)
結果:
17.6 禮帽
原始圖像與進行開運算之后得到的圖像的差。下面的例子是用一個 9x9 的核進行禮帽操作的結果。
tophat = cv2.morphologyEx(img, cv2.MORPH_TOPHAT, kernel)
結果:
17.7 黑帽
進行閉運算之后得到的圖像與原始圖像的差。
tophat = cv2.morphologyEx(img, cv2.MORPH_TOPHAT, kernel)
結果:
17.8 形態學操作之間的關系
我們把以上集中形態學操作之間的關系列出來以供大家參考:
結構化元素
在前面的例子中我們使用 Numpy 構建了結構化元素,它是正方形的。但有時我們需要構建一個橢圓形/圓形的核。為了實現這種要求,提供了 OpenCV函數 cv2.getStructuringElement()。你只需要告訴他你需要的核的形狀和大小。
# Rectangular Kernel >>> cv2.getStructuringElement(cv2.MORPH_RECT,(5,5)) array([[1, 1, 1, 1, 1], [1, 1, 1, 1, 1], [1, 1, 1, 1, 1], [1, 1, 1, 1, 1], [1, 1, 1, 1, 1]], dtype=uint8) # Elliptical Kernel >>> cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(5,5)) array([[0, 0, 1, 0, 0], [1, 1, 1, 1, 1], [1, 1, 1, 1, 1], [1, 1, 1, 1, 1], [0, 0, 1, 0, 0]], dtype=uint8) # Cross-shaped Kernel >>> cv2.getStructuringElement(cv2.MORPH_CROSS,(5,5)) array([[0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [1, 1, 1, 1, 1], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0]], dtype=uint8)
18 圖像梯度
目標
• 圖像梯度,圖像邊界等
• 使用到的函數有:cv2.Sobel(),cv2.Schar(),cv2.Laplacian() 等
原理
梯度簡單來說就是求導。
OpenCV 提供了三種不同的梯度濾波器,或者說高通濾波器:Sobel,Scharr 和 Laplacian。我們會一一介紹他們。
Sobel,Scharr 其實就是求一階或二階導數。Scharr 是對 Sobel(使用小的卷積核求解求解梯度角度時)的優化。Laplacian 是求二階導數。
18.1 Sobel 算子和 Scharr 算子
Sobel 算子是高斯平滑與微分操作的結合體,所以它的抗噪聲能力很好。你可以設定求導的方向(xorder 或 yorder)。還可以設定使用的卷積核的大小(ksize)。如果 ksize=-1,會使用 3x3 的 Scharr 濾波器,它的的效果要比 3x3 的 Sobel 濾波器好(而且速度相同,所以在使用 3x3 濾波器時應該盡量使用 Scharr 濾波器)。3x3 的 Scharr 濾波器卷積核如下:
18.2 Laplacian 算子
拉普拉斯算子可以使用二階導數的形式定義,可假設其離散實現類似於二階 Sobel 導數,事實上,OpenCV 在計算拉普拉斯算子時直接調用 Sobel 算子。計算公式如下:
拉普拉斯濾波器使用的卷積核:
代碼
下面的代碼分別使用以上三種濾波器對同一幅圖進行操作。使用的卷積核都是 5x5 的。
import cv2 import numpy as np from matplotlib import pyplot as plt img = cv2.imread('dave.jpg',0) laplacian = cv2.Laplacian(img,cv2.CV_64F) sobelx = cv2.Sobel(img,cv2.CV_64F,1,0,ksize=5) sobely = cv2.Sobel(img,cv2.CV_64F,0,1,ksize=5) plt.subplot(2,2,1),plt.imshow(img,cmap = 'gray') plt.title('Original'), plt.xticks([]), plt.yticks([]) plt.subplot(2,2,2),plt.imshow(laplacian,cmap = 'gray') plt.title('Laplacian'), plt.xticks([]), plt.yticks([]) plt.subplot(2,2,3),plt.imshow(sobelx,cmap = 'gray') plt.title('Sobel X'), plt.xticks([]), plt.yticks([]) plt.subplot(2,2,4),plt.imshow(sobely,cmap = 'gray') plt.title('Sobel Y'), plt.xticks([]), plt.yticks([]) plt.show()
結果:
一個重要的事!
在查看上面這個例子的注釋時不知道你有沒有注意到:當我們可以通過參數 -1 來設定輸出圖像的深度(數據類型)與原圖像保持一致,但是我們在代碼中使用的卻是 cv2.CV_64F。這是為什么呢?想象一下一個從黑到白的邊界的導數是整數,而一個從白到黑的邊界點導數卻是負數。如果原圖像的深度是np.int8 時,所有的負值都會被截斷變成 0,換句話說就是把把邊界丟失掉。所以如果這兩種邊界你都想檢測到,最好的的辦法就是將輸出的數據類型設置的更高,比如 cv2.CV_16S,cv2.CV_64F 等。取絕對值然后再把它轉回到 cv2.CV_8U。下面的示例演示了輸出圖片的深度不同造成的不同效果。
import cv2 import numpy as np from matplotlib import pyplot as plt img = cv2.imread('box.png',0) # Output dtype = cv2.CV_8U sobelx8u = cv2.Sobel(img,cv2.CV_8U,1,0,ksize=5) # Output dtype = cv2.CV_64F. Then take its absolute and convert to cv2.CV_8U sobelx64f = cv2.Sobel(img,cv2.CV_64F,1,0,ksize=5) abs_sobel64f = np.absolute(sobelx64f) sobel_8u = np.uint8(abs_sobel64f) plt.subplot(1,3,1),plt.imshow(img,cmap = 'gray') plt.title('Original'), plt.xticks([]), plt.yticks([]) plt.subplot(1,3,2),plt.imshow(sobelx8u,cmap = 'gray') plt.title('Sobel CV_8U'), plt.xticks([]), plt.yticks([]) plt.subplot(1,3,3),plt.imshow(sobel_8u,cmap = 'gray') plt.title('Sobel abs(CV_64F)'), plt.xticks([]), plt.yticks([]) plt.show()
結果: