1. 程式人生 > >Canny邊緣檢測原理

Canny邊緣檢測原理

一. Canny基本思想

1. 邊緣檢測

解析:邊緣是物件和背景之間的邊界,還能表示重疊物件之間的邊界。邊緣檢測是影象分割的一部分,影象分割的目的是識別出影象中的區域。邊緣檢測是定位邊緣畫素的過程,而邊緣增強是增加邊緣和背景之間的對比度以便能夠更清楚地看清邊緣的過程。邊緣跟蹤是沿著邊緣進行跟蹤的過程,這個過程通常會把邊緣畫素採集到一個列表中,鏈碼演算法是邊緣跟蹤演算法的一個特例。

2. 最優邊緣準則 [1]

Canny邊緣檢測運算元是John F. Canny於1986年開發出來的一個多級邊緣檢測演算法。Canny的目標是找到一個最優的邊緣檢測演算法,最優邊緣檢測的含義,如下所示:

(1)最優檢測:演算法能夠儘可能多地標識出影象中的實際邊緣,漏檢真實邊緣的概率和誤檢非邊緣的概率都儘可能小;

(2)最優定位準則:檢測到的邊緣點的位置距離實際邊緣點的位置最近,或者是由於噪聲影響引起檢測出的邊緣偏離物體的真實邊緣的程度最小;

(3)檢測點與邊緣點一一對應:運算元檢測的邊緣點與實際邊緣點應該是一一對應。

二. Canny演算法實現

1. 高斯濾波影象去噪

垃圾進,垃圾出,資料預處理工作是非常重要的,影象處理也不例外。這裡使用高斯濾波進行影象去噪,比如blur = cv2.GaussianBlur(img, (5,5), 0),處理後的影象與原始影象相比稍微有些模糊。這樣單獨的一個畫素噪聲在經過高斯濾波的影象上變得幾乎沒有影響。

2. 計算影象梯度

Canny演算法的基本思想是找尋一幅圖相中灰度強度變化最強的位置。所謂變化最強,即指梯度方向。對平滑後的影象使用Sobel運算元計算水平方向和豎直方向的一階導數(影象梯度)(

)。根據得到的這兩幅梯度圖()找到邊界的梯度和方向。如下所示:

梯度的方向一般總是與邊界垂直。梯度方向被歸為四類:垂直,水平,和兩個對角線。

3. 非極大值抑制

在獲得梯度的方向和大小之後,應該對整幅影象做一個掃描,去除那些非邊界上的點。對每一個畫素進行檢查,看這個點的梯度是不是周圍具有相同梯度方向的點中最大的。如下所示:

上圖中的數字代表了畫素點的梯度強度,箭頭方向代表了梯度方向。以第二排第三個畫素點為例,由於梯度方向向上,則將這一點的強度(7)與其上下兩個畫素點的強度(5和4)比較,由於這一點強度最大,則保留。

4. 滯後閾值

現在要確定那些邊界才是真正的邊界。這時我們需要設定兩個閾值:minVal和maxVal。當影象的灰度梯度高於maxVal時被認為是真的邊界,那些低於minVal的邊界會被拋棄。如果介於兩者之間的話,就要看這個點是否與某個被確定為真正的邊界點相連,如果是就認為它也是邊界點,如果不是就拋棄。如下所示:

A高於閾值maxVal所以是真正的邊界點,C雖然低於maxVal但高於minVal並且與A相連,所以也被認為是真正的邊界點。而B就會被拋棄,因為它不僅低於maxVal而且不與真正的邊界點相連。所以選擇合適的maxVal和minVal對於能否得到好的結果非常重要。在這一步一些小的噪聲點也會被除去,因為我們假設邊界都是一些長的線段。

三. Canny程式碼應用

import cv2
import numpy as np

if __name__ == '__main__':

    def nothing(*arg):
        pass
    
    cv2.namedWindow('edge')
    cv2.createTrackbar('thrs1', 'edge', 2000, 5000, nothing)
    cv2.createTrackbar('thrs2', 'edge', 4000, 5000, nothing)

    cap = cv2.VideoCapture(0)
    while True:
        flag, img = cap.read()
        img = cv2.GaussianBlur(img, (3,3), 0) 
        
        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        thrs1 = cv2.getTrackbarPos('thrs1', 'edge')
        thrs2 = cv2.getTrackbarPos('thrs2', 'edge')
        edge = cv2.Canny(gray, thrs1, thrs2, apertureSize=5)
        vis = img.copy()
        vis = np.uint8(vis/2.)
        vis[edge != 0] = (0, 255, 0)
        cv2.imshow('edge', vis)
        ch = cv2.waitKey(5) & 0xFF
        if ch == 27:
            break
    cv2.destroyAllWindows()

解析:

1. cv2.Canny(image, threshold1, threshold2[, edges[, apertureSize[, L2gradient]]]) → edges

(1)其中較大的threshold2用於檢測影象中明顯的邊緣,但一般情況下檢測的效果不會那麼完美,邊緣檢測出來是斷斷續續的,所以這時候用較小的threshold1用於將這些間斷的邊緣連線起來。

(2)可選引數apertureSize是Sobel運算元的大小(預設值為3),而引數L2gradient是一個布林值,如果為真,則使用更精確的L2範數進行計算(即兩個方向的倒數的平方和再開方),否則使用L1範數(直接將兩個方向導數的絕對值相加)。

2. cv2.Sobel(src, ddepth, dx, dy[, dst[, ksize[, scale[, delta[, borderType]]]]]) → dst

(1)ddepth表示影象的深度,-1表示採用的是與原影象相同的深度。目標影象的深度必須大於等於原影象的深度;

(2)dx和dy表示求導的階數,0表示這個方向上沒有求導,一般為0、1、2。

(3)ksize表示Sobel運算元的大小,必須為1、3、5、7。

(4)scale:optional scale factor for the computed derivative values; by default, no scaling is applied。

(5)delta:optional delta value that is added to the results prior to storing them in dst.

(6)borderType:pixel extrapolation method.

3. cv2.convertScaleAbs(src[, dst[, alpha[, beta]]]) → dst

(1)Scales, calculates absolute values, and converts the result to 8-bit, namely dst = src*alpha + beta.
(2)alpha:optional scale factor.
(3)beta:optional delta added to the scaled values.

4. cv2.addWeighted(src1, alpha, src2, beta, gamma[, dst[, dtype]]) → dst

(1)alpha表示src1的權重。

(2)beta表示src2的權重。

(3)gamma:scalar added to each sum.

(4)dtype:optional depth of the output array; when both input arrays have the same depth, dtype can be set to -1, which will be equivalent to src1.depth().

cv2.addWeighted表示計算兩個陣列的權重和,即dst = src1*alpha + src2*beta + gamma。

說明:邊緣檢測常用方法:Canny運算元,Sobel運算元,Laplace運算元,Scharr濾波器。

參考文獻:

[2] OpenCV官方教程中文版