使用OpenCV+Python進(jìn)行Canny邊緣檢測(cè)
點(diǎn)擊上方“小白學(xué)視覺(jué)”,選擇加"星標(biāo)"或“置頂”
重磅干貨,第一時(shí)間送達(dá)

如果我們環(huán)顧房間,我們會(huì)看到大量的物體,每一個(gè)都很容易區(qū)分,并有自己獨(dú)特的邊緣。我們區(qū)分物體的先天能力部分來(lái)自于我們的視覺(jué)系統(tǒng)檢測(cè)邊緣的能力。檢測(cè)邊緣是視覺(jué)的一項(xiàng)基本任務(wù),盡管沒(méi)有它我們不會(huì)完全失明,但以前區(qū)分物體的簡(jiǎn)單任務(wù)將變得非常具有挑戰(zhàn)性。電腦也是類(lèi)似的,計(jì)算機(jī)要檢測(cè)物體,首先需要識(shí)別邊緣。
接下來(lái)讓我們進(jìn)入 Canny 邊緣檢測(cè)器。
Canny 邊緣檢測(cè)器是一種用于識(shí)別圖像邊緣的算法,。它由 John F. Canny 于 1986 年開(kāi)發(fā),此后一直被廣泛使用。今天,我們將在開(kāi)放的 Python 計(jì)算機(jī)視覺(jué)庫(kù)(OpenCV-python)的幫助下,詳細(xì)探討 Canny 邊緣檢測(cè)器
讓我們首先從初始設(shè)置開(kāi)始。
向我們的 python 文件添加兩個(gè)依賴(lài)項(xiàng):
import cv2 as cvfrom matplotlib import pyplot as plot
第一個(gè)導(dǎo)入是 OpenCV python,這是我們將用來(lái)生成 Canny 邊緣和一些補(bǔ)充圖像的庫(kù)。我們還將使用 matplotlib 來(lái)顯示我們的圖像。
首先,這是我們將要查找的 Canny 邊緣的圖像:

Canny 邊緣算法需要灰度圖像才能正常運(yùn)行,我們可以在 OpenCV 中將 RGB 圖像讀取為灰度,如下所示:
# helper function to easily display our imagesdef img_show(title, image):plot.title(title)plot.xticks([])plot.yticks([])plot.imshow(image, cmap="gray")plot.show()# read in our original image as grayscaleimg = cv.imread("[email protected]", cv.IMREAD_GRAYSCALE)# show grayscale image using our helper functionimg_show("Grayscale Image", img)
我們來(lái)創(chuàng)建一個(gè)非常簡(jiǎn)單的輔助函數(shù),用于使用 matplotlib 顯示我們的圖像。使用這個(gè)函數(shù),我們可以查看我們的灰度圖像:

現(xiàn)在我們已經(jīng)完成了初始設(shè)置,接下來(lái)讓我們深入研究 Canny 邊緣算法!
Canny邊緣檢測(cè)算法包括五個(gè)步驟:
高斯濾波
確定強(qiáng)度梯度
非極大值抑制
雙閾值
滯后邊緣跟蹤
我將詳細(xì)解釋每個(gè)步驟。
我們可能聽(tīng)說(shuō)過(guò)正態(tài)分布或高斯分布,這種分布在自然界中始終存在,常用于表示實(shí)值隨機(jī)變量。
在圖像處理中,可以對(duì)圖像應(yīng)用高斯濾波器以減少噪聲,模糊的圖片可以直觀地觀察到這個(gè)效果。
由于 Canny 邊緣算法使用導(dǎo)數(shù)來(lái)尋找圖像的強(qiáng)度梯度,因此非常容易受到噪聲的影響。因此,我們通過(guò)對(duì)圖片應(yīng)用高斯濾波器來(lái)去除噪聲。如果我們不去除噪聲,算法可能會(huì)將圖像中的噪聲塊誤認(rèn)為邊緣并錯(cuò)誤地標(biāo)記它們。
OpenCV 使用 sigma = 1 的 5x5 高斯核作為降噪步驟。我已經(jīng)創(chuàng)建了這個(gè)內(nèi)核的 3D 可視化,可以在下面看到。當(dāng)應(yīng)用于我們的圖像時(shí),還包含了此過(guò)濾器的效果。


盡管高斯濾波圖像可能與原始灰度圖像相同,但仔細(xì)觀察會(huì)發(fā)現(xiàn)輕微的模糊,尤其是在棕櫚葉的邊緣周?chē)N覀儾幌脒^(guò)多地模糊圖像;否則,我們可能會(huì)丟失圖像的細(xì)節(jié)。
我們可以使用以下代碼輕松生成此圖像:
# blurring the image with a 5x5, sigma = 1 Guassian kernelimg_blur = cv.GaussianBlur(img, (5, 5), 1)
在對(duì)圖像進(jìn)行平滑處理后,Canny 邊緣算法的第二步是找到圖片的強(qiáng)度梯度。
盡管“強(qiáng)度梯度”這個(gè)名詞可能聽(tīng)起來(lái)很復(fù)雜,但它只是指邊緣的方向。一條邊實(shí)際上可以指向任何方向,但該算法只查看四個(gè)方向以簡(jiǎn)化事情。方向是水平、垂直和兩個(gè)對(duì)角線方向。在數(shù)學(xué)中,我們將其寫(xiě)為 [0?° , 90?° , 45 ° , 135 °?]。
OpenCV 使用 3x3 Sobel 內(nèi)核來(lái)確定水平方向的導(dǎo)數(shù),然后將其轉(zhuǎn)置以確定垂直方向的導(dǎo)數(shù),這些導(dǎo)數(shù)可用于在所需的四個(gè)方向上找到我們的邊緣。
與高斯核一樣,我們也可以在 3D 中可視化 Sobel 核。下邊還包括了 Sobel 過(guò)濾圖像。


在代碼中,我們可以按如下方式生成 Sobel 過(guò)濾后的圖像:
# obtaining a horizontal and vertical Sobel filtering of the imageimg_sobelx = cv.Sobel(img_blur, cv.CV_64F, 1, 0, ksize=3)img_sobely = cv.Sobel(img_blur, cv.CV_64F, 0, 1, ksize=3)# image with both horizontal and vertical Sobel kernels appliedimg_sobelxy?=?cv.addWeighted(cv.convertScaleAbs(img_sobelx),?0.5,?cv.convertScaleAbs(img_sobely),?0.5,?0)
一旦我們找到了強(qiáng)度梯度,我們就可以使用它們來(lái)細(xì)化我們的邊緣。在這一步之后,結(jié)果是一個(gè)二值圖像,這意味著圖像將只包含兩種顏色,黑色和白色。同樣,非最大抑制這個(gè)名字聽(tīng)起來(lái)很復(fù)雜,實(shí)際上這是一個(gè)簡(jiǎn)單的操作。
我們通過(guò)檢查每個(gè)像素在其梯度方向上的相鄰像素來(lái)確定它是否具有最大強(qiáng)度,從而對(duì)每個(gè)像素應(yīng)用非最大抑制。如果像素是最大的,那么我們將其值設(shè)置為 1。如果不是,這意味著像素的相鄰像素具有更高的強(qiáng)度,我們將其值設(shè)置為 0(抑制它)。
有一個(gè)小問(wèn)題:并非所有邊緣都準(zhǔn)確地代表了圖像的真實(shí)邊緣。許多假邊緣是由噪聲和輕微的顏色變化造成的。盡管該算法的第一步是去除噪聲,但并非所有噪聲都被去除,這是因?yàn)檫x擇 5x5 高斯濾波器是一種折中處理。過(guò)濾器去除了大部分明顯的噪聲,但不會(huì)去除太多。這就是雙重閾值發(fā)揮作用的地方。
我們首先選擇兩個(gè)閾值:最小值和最大值。這就是我們所謂的雙閾值。接下來(lái),我們將每個(gè)邊緣的強(qiáng)度梯度與兩個(gè)閾值進(jìn)行比較,如果邊緣的強(qiáng)度梯度大于最大閾值,則將其標(biāo)記為強(qiáng)(或確定)邊緣;相反,如果邊緣的強(qiáng)度梯度小于最小閾值,則將其丟棄。但是,如果邊緣的強(qiáng)度梯度介于最小和最大閾值之間,則將其標(biāo)記為弱邊緣。
請(qǐng)參考下圖:
閾值區(qū)域圖
綠色區(qū)域是強(qiáng)度梯度高于最大閾值的地方,這意味著該區(qū)域內(nèi)的任何邊緣都被歸類(lèi)為強(qiáng)邊緣。類(lèi)似地,可以在藍(lán)色區(qū)域內(nèi)找到弱邊緣,因?yàn)樵搮^(qū)域位于我們的兩個(gè)閾值之間。紅色區(qū)域代表梯度低于我們的最小閾值的邊緣,因此,該區(qū)域內(nèi)的任何邊緣都將被丟棄。
到目前為止,我們已經(jīng)確定了兩種類(lèi)型的邊緣:弱邊和強(qiáng)弱。我們知道強(qiáng)邊緣是我們選擇最大閾值的最終結(jié)果的一部分,但是,我們不太確定如何處理弱邊緣。Canny 邊緣算法使用假設(shè)來(lái)簡(jiǎn)化事情,它假設(shè)連接到強(qiáng)邊的弱邊本身就是強(qiáng)邊,同樣,未連接到強(qiáng)邊緣的弱邊緣是噪聲或顏色變化。在閾值化的上下文中,這就是滯后的含義。
現(xiàn)在,讓我們?cè)诘?4 步的圖表中添加兩條曲線,每條曲線代表一條邊的梯度值。
添加了漸變曲線的閾值區(qū)域圖
由線段 A 和 C 組成的頂部曲線穿過(guò)綠色和藍(lán)色區(qū)域,我們知道綠色區(qū)域的邊緣強(qiáng),藍(lán)色區(qū)域的邊緣弱。因此,段A是強(qiáng)邊緣,段C是弱邊緣。通過(guò)應(yīng)用滯后邊緣跟蹤,我們將 C 標(biāo)記為強(qiáng)邊緣,因?yàn)樗B接到 A。
現(xiàn)在讓我們來(lái)看底部曲線,該曲線由段 B 和 D 組成。由于 B 低于最小閾值,它應(yīng)該已經(jīng)被抑制了。然而,D 是一個(gè)弱邊緣,這意味著它需要進(jìn)行判斷。通過(guò)使用滯后邊緣跟蹤,我們可以看到 D 沒(méi)有連接到任何通過(guò)綠色區(qū)域的線段,這意味著它沒(méi)有連接到任何強(qiáng)邊,因此它被丟棄。
這很好,但是算法如何準(zhǔn)確地知道弱邊是否連接到強(qiáng)邊?Canny 邊緣算法通過(guò)考慮每個(gè)弱邊緣像素及其周?chē)?8 個(gè)相鄰像素來(lái)確定這一點(diǎn)。如果其相鄰像素中的任何一個(gè)是強(qiáng)邊緣的一部分,則認(rèn)為它連接到強(qiáng)邊緣。因此,該像素保留在我們的最終結(jié)果中。相反,如果相鄰像素都不是強(qiáng)邊緣,則假定它不是強(qiáng)邊緣的一部分,因此被抑制。
對(duì)我們來(lái)說(shuō)非常容易的是,我們實(shí)際上不需要執(zhí)行任何這些步驟來(lái)生成我們的 Canny 邊,OpenCV 將它們捆綁到一個(gè)名為 Canny() 的函數(shù)中。
通過(guò)編寫(xiě)以下代碼,我們可以很容易地在圖像上調(diào)用此函數(shù):
# finally, generate canny edges# extreme examples: high threshold [900, 1000]; low threshold [1, 10]img_edges = cv.Canny(img, 50, 100)
經(jīng)過(guò)一些實(shí)驗(yàn),我們分別選擇50和100作為我們的低閾值和高閾值。但是,我們還可以嘗試不同的方法,也許會(huì)發(fā)現(xiàn)其他閾值會(huì)產(chǎn)生更好的結(jié)果!

我們最后的 Canny 邊緣
參考文獻(xiàn):
https://en.wikipedia.org/wiki/Canny_edge_detector
https://docs.opencv.org/master/da/d22/tutorial_py_canny.html
https://scikit-image.org/docs/dev/auto_examples/filters/plot_hysteresis.html
https://unsplash.com/photos/Ow-MNAWRBW4
Github代碼連接:
https://github.com/SeanDuttonJones/opencv-canny-edge
交流群
歡迎加入公眾號(hào)讀者群一起和同行交流,目前有SLAM、三維視覺(jué)、傳感器、自動(dòng)駕駛、計(jì)算攝影、檢測(cè)、分割、識(shí)別、醫(yī)學(xué)影像、GAN、算法競(jìng)賽等微信群(以后會(huì)逐漸細(xì)分),請(qǐng)掃描下面微信號(hào)加群,備注:”昵稱(chēng)+學(xué)校/公司+研究方向“,例如:”張三?+?上海交大?+?視覺(jué)SLAM“。請(qǐng)按照格式備注,否則不予通過(guò)。添加成功后會(huì)根據(jù)研究方向邀請(qǐng)進(jìn)入相關(guān)微信群。請(qǐng)勿在群內(nèi)發(fā)送廣告,否則會(huì)請(qǐng)出群,謝謝理解~

