此系列的其他文章:
OpenCV算法學(xué)習(xí)筆記之初識(shí)OpenCV
OpenCV算法學(xué)習(xí)筆記之幾何變換
OpenCV算法學(xué)習(xí)筆記之對(duì)比度增強(qiáng)
OpenCV算法學(xué)習(xí)筆記之平滑算法
OpenCV算法學(xué)習(xí)筆記之閾值分割
OpenCV算法學(xué)習(xí)筆記之形態(tài)學(xué)處理
OpenCV算法學(xué)習(xí)筆記之邊緣檢測(cè)(一)
OpenCV算法學(xué)習(xí)筆記之形狀檢測(cè)
更多文章可以訪問(wèn)我的博客Aengus | Blog
Canny邊緣檢測(cè)
原理
基于卷積運(yùn)算的邊緣檢測(cè)算法(即上一篇OpenCV筆記中提到的),有以下兩個(gè)缺點(diǎn):
(1)沒(méi)有充分利用邊緣的梯度方向;
(2)最后輸出的邊緣二值圖,只是簡(jiǎn)單地利用閾值進(jìn)行處理,邊緣信息損失程度和閾值相關(guān);
Canny算法基于這兩點(diǎn)做出了改進(jìn),主要為:
(1)基于邊緣梯度方向的非極大值抑制;
(2)雙閾值的滯后閾值處理;
Canny算法的步驟近似如下:
第一步:圖像矩陣分別與水平方向上的卷積核
和垂直方向上的卷積核
卷積得到
和
,然后利用平方和的開(kāi)方
得到邊緣強(qiáng)度,這一步和Sobel邊緣檢測(cè)一樣,也可以用Prewitt核代替。邊界的處理方式是補(bǔ)零。
假設(shè)有以下矩陣:

與sobel算子進(jìn)行卷積后,得到與
:

然后計(jì)算平方和并開(kāi)方,得到邊緣強(qiáng)度(梯度強(qiáng)度),這里只寫(xiě)整數(shù)部分:

Sobel邊緣檢測(cè)是對(duì)magnitude的結(jié)果大于255的值截?cái)酁?55,然后轉(zhuǎn)換為8位圖就得到了邊緣強(qiáng)度圖的灰度顯示。而Canny算法的處理方式與此不同。
第二步:利用第一步得到的與
,計(jì)算出梯度方向
,即對(duì)每一個(gè)位置
,
代表該位置的梯度方向,一般用角度表示,即
??梢岳肞ython的函數(shù)包math中的
atan2進(jìn)行求取,也可以利用Numpy的函數(shù)arctan2,代碼如下:
y = np.array([[1, 1], [-1, -1]])
x = np.array([[1, -1], [-1, 1]])
angle = np.arctan2(y, x)/np.pi*180
結(jié)果為:
array([[45., 135.],
[-135., -45.]])
用第一步得到的和
,計(jì)算得到每一個(gè)位置的梯度角:

第三步:對(duì)每一個(gè)位置進(jìn)行非極大值抑制處理,非極大值抑制處理操作操作返回的仍然是一個(gè)矩陣,假設(shè)為nonMaxSup。在上面對(duì)示例中,邊界的處理是補(bǔ)零,導(dǎo)致所得到對(duì)magnitude產(chǎn)生額外的邊緣響應(yīng);如果采用對(duì)是以邊界為對(duì)稱(chēng)的邊界擴(kuò)充方式,那么卷積結(jié)果的邊界全是0。在非極大值抑制這一步中,對(duì)邊界不進(jìn)行任何處理。
所謂對(duì)非極大值抑制,就是如果在沿著梯度方向
上的鄰域內(nèi)是最大的則將
設(shè)置為最大值,否則將其設(shè)置為0。
首先將nonMaxSup初始化為以下矩陣:

接下來(lái)以填充為例:首先在
上放置倒置對(duì)坐標(biāo)軸,然后對(duì)應(yīng)到
,發(fā)現(xiàn)
,再按照該梯度方向畫(huà)出直線,最后以
為中心對(duì)
鄰域內(nèi),大體定位出梯度方向上對(duì)領(lǐng)域,即右上方和左下方。如下圖所示:

由于且
,故令
。
一般可以將梯度離散為以下幾種情況:
-
;
-
;
-
;
-
;
對(duì)應(yīng)的領(lǐng)域如下所示:

第四步:雙閾值對(duì)滯后閾值處理。經(jīng)非極大值抑制后得到的邊緣強(qiáng)度圖一般需要閾值化處理,常用對(duì)是全局閾值分割和局部自適應(yīng)閾值分割。還有另外一種方法:滯后閾值處理,它使用高閾值和低閾值,按照下列規(guī)則進(jìn)行閾值化處理:
- 邊緣強(qiáng)度大于高閾值對(duì)那些點(diǎn)作為確定邊緣點(diǎn);
- 邊緣強(qiáng)度比低閾值小的那些點(diǎn)立即被剔除;
- 處于高低閾值之間對(duì)那些點(diǎn),只有這些點(diǎn)能按某一路徑與確認(rèn)邊緣點(diǎn)相連時(shí),才可以作為邊緣點(diǎn)被接受。組成這一路徑的所有點(diǎn)都要比低閾值大。實(shí)際中,可以先選定邊緣強(qiáng)度大于高閾值的所有確定邊緣點(diǎn),然后在邊緣強(qiáng)度大于低閾值的情況下盡可能的延長(zhǎng)邊緣;
Python實(shí)現(xiàn)
最好利用dx與dy的平方和開(kāi)方對(duì)形式來(lái)衡量邊緣強(qiáng)度。
def non_maximum_suppression(dx, dy):
"""
非極大值抑制
:param dx: 與水平卷積核卷積運(yùn)算后的結(jié)果
:param dy: 與垂直卷積核卷積運(yùn)算后對(duì)結(jié)果
:return 非極大值抑制后的矩陣
"""
# 邊緣強(qiáng)度
edge_mag = np.sqrt(np.power(dx, 2.0) + np.power(dy, 2.0))
# 高、寬
rows, cols = dx.shape
# 梯度方向
grad_direction = np.zeros(dx.shape)
# 邊緣強(qiáng)度的非極大值抑制
edge_mag_non_max_sup = np.zeros(dx.shape)
for r in range(1, rows-1):
for c in range(1, cols-1):
# angle的范圍[0, 180],[-180, 0]
angle = math.atan2(dy[r][c], dx[r][c]) / math.pi*180
grad_direction[r][c] = angle
# 左右方向
if abs(angle) < 22.5 or abs(angle) > 157.5:
if edge_mag[r][c] > edge_mag[r][c-1] and edge_mag[r][c] > edge_mag[r][c+1]:
edge_mag_non_max_sup[r][c] = edge_mag[r][c]
# 左上右下方向
if (abs(angle) >= 22.5 or abs(angle) < 67.5) or (-angle > 112.5 and -angle <= 157.5):
if edge_mag[r][c] > edge_mag[r-1][c-1] and edge_mag[r][c] > edge_mag[r+1][c+1]:
edge_mag_non_max_sup[r][c] = edge_mag[r][c]
# 上下方向
if abs(angle) >= 67.5 and abs(angle) <= 112.5:
if edge_mag[r][c] > edge_mag[r-1][c] and edge_mag[r][c] > edge_mag[r+1][c]:
edge_mag_non_max_sup[r][c] = edge_mag[r][c]
# 右上左下方向
if (abs(angle) >= 112.5 or abs(angle) <= 157.5) or (-angle >= 22.5 and -angle < 67.5):
if edge_mag[r][c] > edge_mag[r-1][c+1] and edge_mag[r][c] > edge_mag[r+1][c-1]:
edge_mag_non_max_sup[r][c] = edge_mag[r][c]
return edge_mag_non_max_sup
接下來(lái)是滯后閾值處理:
def check_in_range(r, c, rows, cols):
"""
判斷一個(gè)點(diǎn)的坐標(biāo)是否在圖像范圍內(nèi)
"""
if (r >= 0 and r < rows) and (c >= 0 and c < cols):
return True
else:
return False
def trace(edge_mag_non_max_sup, edge, lower_thresh, r, c, rows, cols):
"""
滯后閾值處理對(duì)第四步:延長(zhǎng)邊緣
"""
# 大于高閾值的點(diǎn)為確定邊緣點(diǎn)
if edge[r][c] == 0:
edge[r][c] = 255
for i in range(-1, 2):
for j in range(-1, 2):
if check_in_range(r+i, c+j, rows, cols) and edge_mag_non_max_sup[r+i][c+j] >= lower_thresh:
tarce(edge_mag_non_max_sup, edge, lower_thresh, r+i, c+j, rows, cols)
def hysteresis_threshold(edge_non_max_sup, lower_thresh, upper_thresh):
"""
滯后閾值處理
:param edge_non_max_sup: 待處理邊緣強(qiáng)度圖
:param lower_thresh: 低閾值
:param upper_thresh: 高閾值
:return 處理后對(duì)邊緣強(qiáng)度圖
"""
# 高寬
rows, cols = edge_non_max_sup.shape
edge = np.zeros(edge_non_max_sup.shape, np.uint8)
for r in range(1, rows-1):
for c in range(1, cols-1):
# 大于高閾值的點(diǎn)為確定邊緣點(diǎn),而且以該點(diǎn)為起始點(diǎn)延長(zhǎng)邊緣
if edge_non_max_sup[r][c] >= upper_thresh:
trace(edge_non_max_sup, edge, lower_thresh, r, c, rows, cols)
# 小于低閾值的被剔除
if edge_non_max_sup[r][c] < lower_thresh:
edge[r][c] = 0
return edge
OpenCV提供函數(shù)void Canny(InputArray image, OutputArray edges, double threshold1, double threshold2, int apertureSize=3, bool L2gradient=false),其中threshold1代表低閾值,threshold2代表高閾值,apertureSize代表Sobel核的窗口尺寸,L2gradient代表計(jì)算邊緣強(qiáng)度時(shí)使用的方式,值等于true時(shí)用的是平方和形式,否則使用的是絕對(duì)值和的形式。
Laplacian邊緣檢測(cè)
原理
二維的拉普拉斯變換由以下公式定義:
推廣到二維矩陣,即矩陣進(jìn)行拉普拉斯變換,也就是與以下卷積核進(jìn)行卷積(這兩種都可以):
錨點(diǎn)在中心位置。圖像與拉普拉斯核進(jìn)行卷積運(yùn)算的本質(zhì)是計(jì)算任意位置的值與其在水平方向和垂直方向上四個(gè)相鄰點(diǎn)平均值之間的差值(只是相差一個(gè)4的倍數(shù))。
拉普拉斯邊緣檢測(cè)沒(méi)有進(jìn)行平滑處理,所以對(duì)噪點(diǎn)比較敏感;優(yōu)點(diǎn)是只有一個(gè)卷積核,運(yùn)算量比較低。
拉普拉斯核還有以下幾種形式:
拉普拉斯核內(nèi)所有元素的和必須為0,這樣就使得在恒等灰度值區(qū)域不會(huì)產(chǎn)生錯(cuò)誤的邊緣。上述的拉普拉斯核均是不可分離的。
Python實(shí)現(xiàn)
def laplacian(image, _boundary='fill', _fillvalue=0):
# 第一步:圖像與拉普拉斯核卷積
laplacian_kernel = np.array([[0, -1, 0], [-1, 4, -1], [0, -1, 0]], np.float32)
# 卷積
i_conv_lap = signal.convolve2d(image, laplacian_kernel, mode='same', boundary=_boundary, fillvalue=_fillvalue)
# 第二步:進(jìn)行閾值化處理
i_conv_lap[i_conv_lap > 0] = 255
i_conv_lap[i_conv_lap <= 0] = 0
i_conv_lap.astype(np.uint8)
return i_conv_lap
在閾值化處理時(shí),也可以用以下公式進(jìn)行處理以得到水墨畫(huà)效果:
OpenCV提供函數(shù)void Laplacian(InputArray src, OutputArray dst, int ddepth, int ksize=-1, double scale=1, double delta=0, int borderType=BORDER_DEFAULT)實(shí)現(xiàn)拉普拉斯變換,參數(shù)解釋如下所示:
| 參數(shù) | 解釋 |
|---|---|
| src | 輸入矩陣 |
| dst | 輸出矩陣 |
| ddepth | 輸出矩陣的數(shù)據(jù)類(lèi)型(位深) |
| ksize | 拉普拉斯核的類(lèi)型 |
| scale | 比例系數(shù) |
| delta | 平移系數(shù) |
| borderType | 邊界擴(kuò)充類(lèi)型 |
參數(shù)ksize等于1時(shí)采用的是形式的拉普拉斯核,等于3時(shí)采用的是
形式的核。
高斯拉普拉斯(LoG)邊緣檢測(cè)
由于拉普拉斯變換對(duì)于噪點(diǎn)比較敏感,在進(jìn)行變換前先進(jìn)行高斯平滑再進(jìn)行卷積,這就是LoG邊緣檢測(cè)。雖然平滑和變換需要兩次卷積運(yùn)算,但是這里我們可以利用一次卷積運(yùn)算即可完成。
原理
二維高斯函數(shù)為
其拉普拉斯變化如下(省略化簡(jiǎn)過(guò)程):
其步驟如下:
第一步:構(gòu)建、標(biāo)準(zhǔn)差為
的LoG卷積核:
其中均為奇數(shù),且一般
,錨點(diǎn)位置在
。
第二步:圖像與進(jìn)行卷積,結(jié)果記為
i_cov_log;
第三步:對(duì)i_cov_log進(jìn)行二值化處理,閾值為0;
由于
故可以將高斯拉普拉斯核分離為兩個(gè)卷積核,運(yùn)算時(shí),圖像先與水平方向核卷積,再與垂直卷積核卷積得到結(jié)果1;然后圖像先與垂直卷積核卷積,再與水平卷積核卷積得到結(jié)果2,結(jié)果1與結(jié)果2相加再進(jìn)行閾值化處理得到最終結(jié)果。
Python實(shí)現(xiàn)
這里給出非分離的邊緣檢測(cè),可以把前面的系數(shù)
去掉。
def create_LoG_kernel(sigma, size):
h, w = size
r, c = np.mgrid[0:h:1, 0:w:1]
r -= (h-1) / 2
c -= (w-1) / 2
# 方差
sigma2 = pow(sigma, 2.0)
# 高斯拉普拉斯核
norm2 = np.power(r, 2.0) + np.power(c, 2.0)
log_kernel = (norm2/sigma2 - 2) * np.exp(-norm2/(2*sigma2))
return log_kernel
def LoG(image, sigma, size, _boundary='symm'):
"""
:param _boundary: symm代表反射擴(kuò)充
"""
log_kernel = create_LoG_kernel(sigma, size)
# 卷積
img_conv_log = signal.convolve2d(image, log_kernel, 'same', boundary=_boundary)
return img_conv_log
對(duì)于高斯拉普拉斯核的尺寸,一般取,即大于
的最小奇數(shù),這樣得到的邊緣效果會(huì)比較好。隨著尺度(標(biāo)準(zhǔn)差)的增大,得到的邊緣尺度也越來(lái)越大,越來(lái)越失去邊緣的細(xì)節(jié)。
高斯差分(DoG)邊緣檢測(cè)
可以用差分代替卷積來(lái)近似高斯拉普拉斯變換以減小計(jì)算量。
原理
由于
以及一階導(dǎo)數(shù)的定義:
可以得到高斯拉普拉斯的近似值:
該近似值常稱(chēng)為高斯差分。
步驟如下:
第一步:構(gòu)建、標(biāo)準(zhǔn)差為
的DoG卷積核:
其中均為奇數(shù),且一般
,錨點(diǎn)位置在
。
第二步:圖像與進(jìn)行卷積,結(jié)果記為
i_cov_dog;
第三步:對(duì)i_cov_dog進(jìn)行二值化處理,閾值為0;
參考
《OpenCV算法精解——基于Python和C++》(張平)第八章