1. 什麼是邊緣檢測
邊緣檢測是影像處理與計算機視覺中的重要技術之一。其目的是檢測識別出影像中亮度變化劇烈的畫素點構成的集合。影像邊緣的正確檢測對於分析影像中的內容、實現影像中物體的分割、定位等具有重要的作用。邊緣檢測大大減少了源影像的資料量,剔除了與目標不相干的資訊,保留了影像重要的結構屬性。
影像的邊緣指的是影像中畫素灰度值突然發生變化的區域,如果將影像的每一行畫素和每一列畫素都描述成一個關於灰度值的函式,那麼影像的邊緣對應在灰度值函式中是函式值突然變大的區域。函式值的變化趨勢可以用函式的導數描述。當函式值突然變大時,導數也必然會變大,而函式值變化較為平緩區域,導數值也比較小,因此可以透過尋找導數值較大的區域去尋找函式中突然變化的區域,進而確定影像中的邊緣位置。
2 邊緣檢測的常用方法及Python應用
邊緣檢測的方法大致可分為兩類:基於搜尋和基於零交叉。
基於搜尋的邊緣檢測方法:首先計算邊緣強度,通常用一階導數表示,例如梯度模,然後,計算估計邊緣的區域性方向,通常採用梯度的方向,並利用此方向找到區域性梯度模的最大值。
基於零交叉的邊緣檢測方法:找到由影像得到的二階導數的零交叉點來定位邊緣,通常用拉普拉斯運算元或非線性微分方程的零交叉點。
濾波作為邊緣檢測的預處理通常是必要的,通常採用高斯濾波。
2.1 一階微分運算元
一階微分為基礎的邊緣檢測,透過計算影像的梯度值來檢測影像的邊緣,如Roberts運算元、Prewitt運算元和Sobel運算元等。
2.1.1 Roberts運算元
Roberts運算元是一種最簡單的運算元,它利用區域性差分運算元尋找邊緣。採用對角線相鄰兩畫素之差近似梯度幅值檢測邊緣,檢測垂直邊緣的效果比斜向邊緣要好,定位精度高,但對噪聲比較敏感,無法抑制噪聲的影響。
Roberts運算元是一個2x2的模板,採用的是對角方向相鄰的兩個畫素之差,如下的2個卷積核形成了Roberts運算元,影像中的每一個點都用這2個核做卷積:
若對於輸入影像f(x,y),使用Roberts運算元後輸出的目標影像為g(x,y),則
在Python中,Roberts運算元主要是透過Numpy定義模板,再呼叫OpenCV的filter2D()函式實現邊緣提取。該函式主要是利用核心實現對影像的卷積運算,其函式原型如下:
dst = filter2D(src, ddepth, kernel, dts, anchor,delta, borderType)
引數說明:
src:表示輸入影像;
ddepth: 表示目標影像所需的深度;
kernel: 表示卷積核,一個單通道浮點型矩陣;
anchor: 表示核心的基準點,其預設值為(-1, -1),位於中心位置;
delta:表示在儲存目標影像前可選的新增到畫素的值,預設值為0;
borderType:表示邊框模式。
實驗程式碼如下:
def Roberts(srcImg_path):
img = cv2.imread(srcImg_path)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
grayImage = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Roberts運算元
kernelx = np.array([[1, 0], [0, -1]], dtype=int)
kernely = np.array([[0, -1], [1, 0]], dtype=int)
x = cv2.filter2D(grayImage, cv2.CV_16S, kernelx)
y = cv2.filter2D(grayImage, cv2.CV_16S, kernely)
# 轉成uint8
absX = cv2.convertScaleAbs(x)
absY = cv2.convertScaleAbs(y)
Roberts = cv2.addWeighted(absX, 0.5, absY, 0.5, 0)
# 顯示圖形
titles = ["Original Image", "Roberts Image"]
images = [img, Roberts]
for i in range(2):
plt.subplot(1, 2, i+1)
plt.imshow(images[i], "gray")
plt.title(titles[i])
plt.axis('off')
plt.show()
效果如下:
2.1.2 Prewitt運算元
Prewitt是一種影像邊緣檢測的微分運算元,其原理是利用特定區域內畫素值產生的差分實現邊緣檢測。由於Prewitt運算元採用3x3模板對區域內的畫素值進行計算,而Roberts運算元的模板為2x2,故Prewitt運算元的邊緣檢測結果在水平和垂直方向均比Roberts運算元更加明顯。Prewitt運算元適合用來識別噪聲較多,灰度漸變的影像。
Prewitt運算元卷積核如下:
實驗程式碼如下:
def Prewitt(srcImg_path):
img = cv2.imread(srcImg_path)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
grayImage = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Prewitt運算元
kernelx = np.array([[1, 1, 1], [0, 0, 0], [-1, -1, -1]], dtype=int)
kernely = np.array([[-1, 0, 1], [-1, 0, 1], [-1, 0, 1]], dtype=int)
x = cv2.filter2D(grayImage, cv2.CV_16S, kernelx)
y = cv2.filter2D(grayImage, cv2.CV_16S, kernely)
# 轉成uint8
absX = cv2.convertScaleAbs(x)
absY = cv2.convertScaleAbs(y)
Prewitt = cv2.addWeighted(absX, 0.5, absY, 0.5, 0)
# 顯示圖形
titles = ["Original Image", "Prewitt Image"]
images = [img, Prewitt]
for i in range(2):
plt.subplot(1, 2, i+1)
plt.imshow(images[i], "gray")
plt.title(titles[i])
plt.axis('off')
plt.show()
效果如下:
2.1.3 Sobel運算元
在邊緣檢測中,常用的一種模板是Sobel運算元。Sobel運算元有兩個卷積核,一個是檢測水平邊緣的;另一個是檢測垂直邊緣的。與Prewitt運算元相比,Sobel運算元對於畫素的位置的影響做了加權,可以降低邊緣模糊程度,因此效果更好。
Sobel運算元卷積核如下:
在opencv-python中定義了Sobel運算元,其函式原型如下:
dst = Sobel(src, ddepth, dx, dy, dst,ksize, scale, delta, borderType)
引數說明:
src:表示輸入影像;
dst:表示輸出的邊緣圖,其大小和通道數與輸入影像相同;
ddepth:表示目標影像所需的深度,針對不同的輸入影像,輸出目標影像有不同的深度;
dx:表示x方向上的差分階數,取值1或0;
dy:表示y方向上的差分階數,取值1或0;
ksize:表示Sobel運算元的大小,其值必須是正數和奇數;
scale:表示縮放導數的比例常數,預設情況下沒有伸縮係數;
delta:表示將結果存入目標影像之前,新增到結果中的可選增量值。
實驗程式碼如下:
def Sobel_demo(srcImg_path):
img = cv2.imread(srcImg_path)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
grayImage = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Sobel運算元
x = cv2.Sobel(grayImage, cv2.CV_16S, 1, 0)
y = cv2.Sobel(grayImage, cv2.CV_16S, 0, 1)
# 轉成uint8
absX = cv2.convertScaleAbs(x)
absY = cv2.convertScaleAbs(y)
Sobel = cv2.addWeighted(absX, 0.5, absY, 0.5, 0)
# 顯示圖形
titles = ["Original Image", "Sobel Image"]
images = [img, Sobel]
for i in range(2):
plt.subplot(1, 2, i+1)
plt.imshow(images[i], "gray")
plt.title(titles[i])
plt.axis('off')
plt.show()
效果如下:
2.2 二階微分運算元
二階微分為基礎的邊緣檢測,透過尋求二階導數中的過零點來檢測邊緣,如Laplacian運算元和Canny運算元等。
2.2.1 Laplacian運算元
Laplacian運算元是n維歐幾里德空間中的一個二階微分運算元,常用於影像增強和邊緣提取。它透過灰度差分計算鄰域內的畫素,基本流程是:判斷影像中心畫素灰度值與它周圍其他畫素的灰度值,如果中心畫素的灰度更高,則提升中心畫素的灰度;反之降低中心畫素的灰度,從而實現影像銳化操作。在演算法實現過程中,Laplacian運算元透過對鄰域中心畫素的四方向或八方向求梯度,再將梯度相加起來判斷中心畫素灰度與鄰域內其他畫素灰度的關係,最後透過梯度運算的結果對畫素灰度進行調整。
在opencv-python中,Laplacian運算元封裝在Laplacian()函式中,其函式原型如下:
dst = Laplacian(src, ddepth[, dst[, ksize[, scale[, delta[, borderType]]]]])
引數說明:
src:表示輸入影像;
dst:表示輸出的邊緣圖,其大小和通道數與輸入影像相同;
ddepth:表示目標影像所需的深度;
ksize:表示用於計算二階導數的濾波器的孔徑大小,其值必須是正數和奇數,且預設值為1;
scale:表示計算拉普拉斯運算元值的可選比例因子,預設值為1;
delta:表示將結果存入目標影像之前,新增到結果中的可選增量值,預設值為0;
borderType:表示邊框模式。
當ksize=1時,Laplacian()函式採用3x3模板(四鄰域)進行變換處理。下面的實驗程式碼是採用ksize=3的Laplacian運算元進行影像銳化處理:
def Laplacian_demo(srcImg_path):
img = cv2.imread(srcImg_path)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
grayImage = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Laplacian運算元
Laplacian = cv2.Laplacian(grayImage, cv2.CV_16S, ksize=3)
# 轉成uint8
Laplacian = cv2.convertScaleAbs(Laplacian)
# 顯示圖形
titles = ["Original Image", "Laplacian Image"]
images = [img, Laplacian]
for i in range(2):
plt.subplot(1, 2, i+1)
plt.imshow(images[i], "gray")
plt.title(titles[i])
plt.axis('off')
plt.show()
效果如下:
2.2.2 Canny運算元
Canny運算元由John F. Canny在1986年提出,由於它出色的檢測和容錯能力,至今一直被廣泛使用。Canny邊緣檢測具有以下特點:
較低的錯誤率 - 只有真實存在的邊緣才會被檢測到。
較好的邊緣定位 - 檢測出來的結果和影像中真實的邊緣在距離上的誤差很小。
沒有重複的檢測 - 對於每一條邊緣,只會返回一個與之對應的結果。
Canny運算元的計算步驟大概分成以下幾步:
1.影像灰度化
2.用高斯濾波去噪:目的是平滑一些紋理較弱的非邊緣區域,以得到更準確的邊緣。
3.計算梯度方向和大小:影像梯度表達的是各個畫素點之間,畫素值大小的變化幅度大小,變化較大,則可以認為是處於邊緣位置。
4.非極大值抑制:在獲得梯度的方向和大小之後,應該對整幅影像做一個掃描,去除那些非邊界上的點,即對每一個畫素進行檢查,看這個點的梯度是不是周圍具有相同梯度方向的點中最大的。
5.雙閾值選取和滯後邊界跟蹤:確定哪些邊界才是真正的邊界。這時我們需要設定兩個閾值:minVal和maxVal。當影像的灰度梯度高於maxVal時被認為是真的邊界,那些低於minVal的邊界會被拋棄。如果介於兩者之間的話,就要看這個點是否與某個被確定為真正的邊界點相連,如果是就認為它也是邊界點,如果不是就拋棄。
在Python Opencv介面中,提供了Canny函式,其函式原型如下:
canny = cv2.Canny(image,threshold1,threshold2)
引數說明:
image:灰度圖;
threshold1:minval,較小的閾值將間斷的邊緣連線起來;
threshold2:maxval,較大的閾值檢測影像中明顯的邊緣。
實驗程式碼如下:
def Canny_demo(srcImg_path):
img = cv2.imread(srcImg_path)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 高斯濾波
img_GaussianBlur = cv2.GaussianBlur(gray, (3,3), 0)
# Canny運算元
Canny = cv2.Canny(img_GaussianBlur, 0, 100)
# 顯示圖形
titles = ["Original Image", "Canny Image"]
images = [img, Canny]
for i in range(2):
plt.subplot(1, 2, i+1)
plt.imshow(images[i], "gray")
plt.title(titles[i])
plt.axis('off')
plt.show()
效果如下: