摘要 : Canny 邊緣檢測算法由計算機科學(xué)家 John F. Canny 于 1986 年提出的。其不僅提供了算法,還帶來了一套邊緣檢測的理論,分階段的解釋如何實現(xiàn)邊緣檢測。Canny 檢測算法包含下面幾個階段:
- 圖像灰度化
- 高斯模糊處理
- 圖像梯度、梯度幅值、梯度方向計算
- NMS(非極大值抑制)
- 雙閾值的邊界選取
1、調(diào)用opencv進(jìn)行canny邊緣檢測
如果你只是想應(yīng)用canny得到圖片的邊緣的話,那么就沒有必要往下閱讀canny的具體原理與實現(xiàn)了。因為python的opencv庫中提供了很好的功能函數(shù)來實現(xiàn)這一功能。我們以一個示例來說明如何使用opencv進(jìn)行canny邊緣檢測:
這是原圖與使用opencv進(jìn)行canny邊緣檢測的結(jié)果圖:
這是代碼實現(xiàn):
import cv2 #導(dǎo)入opencv庫
#讀取圖片
img = cv2.imread("images/2007\_000032.jpg")
#進(jìn)行canny邊緣檢測
edge = cv2.Canny(img,50,150)
#保存結(jié)果
cv2.imwrite('test.jpg',edge)
這四行代碼的關(guān)鍵在于 cv2.Canny 函數(shù)。我們對它的參數(shù)進(jìn)行詳細(xì)的解讀。希望能對你有幫助。OpenCV-Python中Canny函數(shù)的原型為:
cv2.Canny(image, threshold1, threshold2[, edges[, apertureSize[, L2gradient]]])
必要參數(shù):
- 第一個參數(shù)是需要處理的原圖像,該圖像必須為單通道的灰度圖;
- 第二個參數(shù)是閾值1;
- 第三個參數(shù)是閾值2。
其中較大的閾值2用于檢測圖像中明顯的邊緣,但一般情況下檢測的效果不會那么完美,邊緣檢測出來是斷斷續(xù)續(xù)的。所以這時候用較小的第一個閾值用于將這些間斷的邊緣連接起來。
可選參數(shù)中apertureSize就是Sobel算子的大小。而L2gradient參數(shù)是一個布爾值,如果為真,則使用更精確的L2范數(shù)進(jìn)行計算(即兩個方向的倒數(shù)的平方和再開放),否則使用L1范數(shù)(直接將兩個方向?qū)?shù)的絕對值相加)。
到這其實已經(jīng)可以將canny邊緣檢測應(yīng)用到你的項目中了。當(dāng)然如果你想了解canny邊緣檢測的原理的話,請繼續(xù)往下閱讀。
2、圖像灰度化
對于一張圖片,當(dāng)我們只關(guān)心其邊界的時候,單通道的圖片已經(jīng)足夠提供檢測出邊界的信息。所以我們可以將R、G、B的3通道圖片乃至更高維的高光譜遙感圖像進(jìn)行灰度化?;叶然瘜嶋H上是一種降維操作,它減少了冗余數(shù)據(jù)從而降低了計算開銷。以下是對RGB圖片灰度化的方法:
# 灰度化
def gray(self, img\_path):
"""
計算公式:
Gray(i,j) = [R(i,j) + G(i,j) + B(i,j)] / 3
or :
Gray(i,j) = 0.299 \* R(i,j) + 0.587 \* G(i,j) + 0.114 \* B(i,j)
"""
# 讀取圖片
img = plt.imread(img_path)
# BGR 轉(zhuǎn)換成 RGB 格式
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# 灰度化
img_gray = np.dot(img_rgb[...,:3], [0.299, 0.587, 0.114])
return img_gray
3、高斯模糊處理
高斯模糊實際上是對灰度化后的圖像去噪。從數(shù)學(xué)的角度來看,圖像的高斯模糊過程就是圖像與正態(tài)分布做卷積。進(jìn)行高斯濾波之前,需要先得到一個高斯濾波器(kernel)。如何得到一個高斯濾波器呢?其實就是將高斯函數(shù)離散化,將濾波器中對應(yīng)的橫縱坐標(biāo)索引代入高斯函數(shù),即可得到對應(yīng)的值。不同尺寸的濾波器,得到的值也不同,下面是二維高斯函數(shù)與 (2k+1)x(2k+1) 濾波器的計算公式 :
高斯濾波常用尺寸為 5x5,σ=1.4 的高斯濾波器。下面是 5x5 高斯濾波器的實現(xiàn)代碼:
# 去除噪音 - 使用 5x5 的高斯濾波器
def smooth(self, img\_gray):
# 生成高斯濾波器
"""
要生成一個 (2k+1)x(2k+1) 的高斯濾波器,濾波器的各個元素計算公式如下:
H[i, j] = (1/(2\*pi\*sigma\*\*2))\*exp(-1/2\*sigma\*\*2((i-k-1)\*\*2 + (j-k-1)\*\*2))
"""
sigma1 = sigma2 = 1.4
gau_sum = 0
gaussian = np.zeros([5, 5])
for i in range(5):
for j in range(5):
gaussian[i, j] = math.exp((-1/(2*sigma1*sigma2))*(np.square(i-3)+ np.square(j-3)))/(2*math.pi*sigma1*sigma2)
gau_sum = gau_sum + gaussian[i, j]
# 歸一化處理
gaussian = gaussian / gau_sum
# 高斯濾波
W, H = img_gray.shape
new_gray = np.zeros([W-5, H-5])
for i in range(W-5):
for j in range(H-5):
new_gray[i, j] = np.sum(img_gray[i:i+5, j:j+5] * gaussian)
return new_gray
4、圖像梯度、梯度幅值、梯度方向計算
這個步驟的重要性不言而喻。直觀感受上來講我們知道一個圖像上處于邊界附近位置的像素值變化較大。而處于物體內(nèi)部位置的像素值大多相近。這樣我們可以計算當(dāng)前像素與其附近像素的像素值的差值判斷該像素處于物體內(nèi)部還是邊界。這個差值我們稱為圖像梯度。梯度幅值、梯度方向由圖像梯度計算而來。
具體而言,我們用一階導(dǎo)數(shù)來計算梯度:
對于上式,實際操作時就是用當(dāng)前像素的下一個像素減去當(dāng)前像素。此時 Δ x = 1 \Delta x=1 Δx=1;
梯度包含x方向的梯度與y方向的梯度。它們是兩個向量。梯度幅值是這兩個向量的向量和:
既然梯度幅值是一個向量,那么我們需要計算它的方向:
我們用如下代碼實現(xiàn):
# 計算梯度幅值
def gradients(self, new_gray):
"""
:type: image which after smooth
:rtype:
dx: gradient in the x direction
dy: gradient in the y direction
M: gradient magnitude
theta: gradient direction
"""
W, H = new_gray.shape
dx = np.zeros([W-1, H-1])
dy = np.zeros([W-1, H-1])
M = np.zeros([W-1, H-1])
theta = np.zeros([W-1, H-1])
for i in range(W-1):
for j in range(H-1):
dx[i, j] = new\_gray[i+1, j] - new\_gray[i, j]
dy[i, j] = new\_gray[i, j+1] - new\_gray[i, j]
# 圖像梯度幅值作為圖像強度值
M[i, j] = np.sqrt(np.square(dx[i, j]) + np.square(dy[i, j]))
# 計算 θ - artan(dx/dy)
theta[i, j] = math.atan(dx[i, j] / (dy[i, j] + 0.000000001))
return dx, dy, M, theta
在計算得到的梯度幅值中我們實際上已經(jīng)得到了圖像的邊界(即函數(shù)返回值中的M)。如下:
但是,很容易發(fā)現(xiàn)這個邊緣存在兩個問題:
- 邊緣較粗;
- 很多邊緣斷斷續(xù)續(xù)。
針對這兩個問題便有了以下兩個步驟NMS、雙閾值邊界選取。
5、NMS(非極大值抑制)
理想情況下,最終得到的邊緣應(yīng)該是很細(xì)的。因此,需要執(zhí)行非極大值抑制以使邊緣變細(xì)。原理很簡單:遍歷梯度矩陣上的所有點,并保留邊緣方向上具有極大值的像素。就像下面這幅圖一樣。圖中黑色和灰色表示邊界。我們通過NMS找出其中的局部最大值(也就是圖中的黑色)而把其他位置(也就是圖中的灰色)的值取0。
下面說說 NMS 的細(xì)節(jié)內(nèi)容。NMS在八個領(lǐng)域:上,下,左,右,左上,左下,右上,右下上進(jìn)行(當(dāng)然,比較的時候不需要將該點與其它八個點比較。只需要將其與其梯度方向上的點比較即可。這個很好理解。因為我們只需要當(dāng)前值在它所屬的邊緣上是局部最大值即可,而不需要它在其它邊緣上也是局部最大)如下圖所示,C 周圍的 8 個點就是其附近的八個領(lǐng)域。
NMS 是要找出局部最大值,因此,需要將當(dāng)前的像素的梯度,與其他方向進(jìn)行比較。如下圖所示,g1,g2,g3,g4 分別是 C 八個領(lǐng)域中的 4 個點,藍(lán)線是 C 的梯度方向。如果 C 是局部最大值的話,C 點的梯度幅值就要大于梯度方向直線與 g1g2,g4g3 兩個交點的梯度幅值,即大于點 dTemp1 和 dTemp2 的梯度幅值。上面提到這種方法無法達(dá)到最好的效果,因為 dTemp1 和 dTemp2 不是整像素,而是亞像素。亞像素的意思就是在兩個物理像素之間還有像素。那么,亞像素的梯度幅值怎么求?可以使用線性插值的方法,計算 dTemp1 在 g1,g2 之間的權(quán)重,就可以得到其梯度幅值。計算公式如下:
weight = |gx| / |gy| or |gy| / |gx|
dTemp1 = weight*g1 + (1-weight)*g2
dTemp2 = weight*g3 + (1-weight)*g4
計算時分兩種情況(都是比較當(dāng)前像素與dtemp1與dtemp2的大小,大于這兩個值則保留,小于其中任意一個則將其值取0):
-
下面兩幅圖是 y 方向梯度值比較大的情況,即梯度方向靠近 y 軸。所以,g2 和 g4 在 C 的上下位置,此時 weight = |gy| / |gx| 。左邊的圖是 x,y 方向梯度符號相同的情況,右邊是 x,y 方向梯度符號相反的情況。
-
下面兩幅圖是 x 方向梯度值比較大的情況,即梯度方向靠近 x 軸。所以,g2 和 g4 在 C 的左右位置,此時 weight = |gy| / |gx| 。左邊的圖是 x,y 方向梯度符號相同的情況,右邊是 x,y 方向梯度符號相反的情況。
代碼實現(xiàn)如下:
def NMS(self, M, dx, dy):
d = np.copy(M)
W, H = M.shape
NMS = np.copy(d)
NMS[0, :] = NMS[W-1, :] = NMS[:, 0] = NMS[:, H-1] = 0
for i in range(1, W-1):
for j in range(1, H-1):
# 如果當(dāng)前梯度為0,該點就不是邊緣點
if M[i, j] == 0:
NMS[i, j] = 0
else:
gradX = dx[i, j] # 當(dāng)前點 x 方向?qū)?shù)
gradY = dy[i, j] # 當(dāng)前點 y 方向?qū)?shù)
gradTemp = d[i, j] # 當(dāng)前梯度點
# 如果 y 方向梯度值比較大,說明導(dǎo)數(shù)方向趨向于 y 分量
if np.abs(gradY) > np.abs(gradX):
weight = np.abs(gradX) / np.abs(gradY) # 權(quán)重
grad2 = d[i-1, j]
grad4 = d[i+1, j]
# 如果 x, y 方向?qū)?shù)符號一致
# 像素點位置關(guān)系
# g1 g2
# c
# g4 g3
if gradX * gradY > 0:
grad1 = d[i-1, j-1]
grad3 = d[i+1, j+1]
# 如果 x,y 方向?qū)?shù)符號相反
# 像素點位置關(guān)系
# g2 g1
# c
# g3 g4
else:
grad1 = d[i-1, j+1]
grad3 = d[i+1, j-1]
# 如果 x 方向梯度值比較大
else:
weight = np.abs(gradY) / np.abs(gradX)
grad2 = d[i, j-1]
grad4 = d[i, j+1]
# 如果 x, y 方向?qū)?shù)符號一致
# 像素點位置關(guān)系
# g3
# g2 c g4
# g1
if gradX * gradY > 0:
grad1 = d[i+1, j-1]
grad3 = d[i-1, j+1]
# 如果 x,y 方向?qū)?shù)符號相反
# 像素點位置關(guān)系
# g1
# g2 c g4
# g3
else:
grad1 = d[i-1, j-1]
grad3 = d[i+1, j+1]
# 利用 grad1-grad4 對梯度進(jìn)行插值
gradTemp1 = weight \* grad1 + (1 - weight) \* grad2
gradTemp2 = weight \* grad3 + (1 - weight) \* grad4
# 當(dāng)前像素的梯度是局部的最大值,可能是邊緣點
if gradTemp >= gradTemp1 and gradTemp >= gradTemp2:
NMS[i, j] = gradTemp
else:
# 不可能是邊緣點
NMS[i, j] = 0
return NMS
6、雙閾值的邊界選取
這個階段決定哪些邊緣是真正的邊緣,哪些邊緣不是真正的邊緣。為此,需要設(shè)置兩個閾值,minVal 和 maxVal。梯度大于 maxVal 的任何邊緣肯定是真邊緣,而 minVal 以下的邊緣肯定是非邊緣,因此被丟棄。位于這兩個閾值之間的邊緣會基于其連通性而分類為邊緣或非邊緣,如果它們連接到"可靠邊緣"像素,則它們被視為邊緣的一部分。否則,也會被丟棄。代碼如下所示:
def double\_threshold(self, NMS):
W, H = NMS.shape
DT = np.zeros([W, H])
# 定義高低閾值
TL = 0.1 \* np.max(NMS)
TH = 0.3 \* np.max(NMS)
for i in range(1, W-1):
for j in range(1, H-1):
# 雙閾值選取
if (NMS[i, j] < TL):
DT[i, j] = 0
elif (NMS[i, j] > TH):
DT[i, j] = 1
# 連接
elif (NMS[i-1, j-1:j+1] < TH).any() or (NMS[i+1, j-1:j+1].any() or (NMS[i, [j-1, j+1]] < TH).any()):
DT[i, j] = 1
return DT
進(jìn)行完所有的步驟后,結(jié)果如下圖所示:文章來源:http://www.zghlxwxcb.cn/news/detail-443956.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-443956.html
到了這里,關(guān)于Canny 邊緣檢測算法-python實現(xiàn)(附代碼)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!