Python-計算機視覺中的Canny邊緣檢測方法
今天的想法是用Canny邊緣檢測算法,建立一種可以勾畫出圖像上任何物體的邊緣的算法。
首先,我們來描述一下Canny邊緣檢測器:
Canny邊緣檢測算子是一種邊緣檢測算子,它採用多級算法檢測圖像中廣泛的邊緣。它是由John F. Canny在1986年開發的。Canny還提出了邊緣檢測的計算理論,解釋了該技術的工作原理。
Canny邊緣檢測算法由5個步驟組成:
- 降噪;
- 梯度計算;
- 非最大抑制;
- 雙閾值;
- 滯後邊緣跟蹤。
應用這些步驟後,您將能夠獲得以下結果:
最後值得一提的是,該算法是基於灰度圖像的。因此,在進行上述步驟之前,首先要將圖像轉換爲灰度。
降噪
由於場景背後涉及的數學主要基於導數(參見步驟2:梯度計算),邊緣檢測結果對圖像噪聲高度敏感。
消除圖像噪聲的一種方法是使用高斯模糊平滑圖像。爲此,圖像卷積技術應用高斯核(3x3, 5x5, 7x7等)。核大小取決於預期的模糊效果。基本上,核越小,模糊就越不明顯。在我們的例子中,我們將使用一個5×5的高斯核函數。
大小爲(2k+1)×(2k+1)的高斯濾波核的方程爲:
用於生成Gaussian 5x5內核的Python代碼:
import numpy as np
def gaussian_kernel(size, sigma=1):
size = int(size) // 2
x, y = np.mgrid[-size:size+1, -size:size+1]
normal = 1 / (2.0 * np.pi * sigma**2)
g = np.exp(-((x**2 + y**2) / (2.0*sigma**2))) * normal
return g
應用高斯模糊後,我們得到以下結果:
原始圖像(左) - 帶有高斯濾波器的模糊圖像(sigma = 1.4,核大小爲5x5)
梯度計算
梯度計算步驟通過使用邊緣檢測算子計算圖像的梯度來檢測邊緣強度和方向。
邊緣對應於像素強度的變化。要檢測它,最簡單的方法是應用filters,在兩個方向上突出這種強度變化:水平(x)和垂直(y)
當平滑圖像時,計算導數Ix和Iy。它可以通過分別用Sobel kernels Kx和Ky分別卷積I來實現:
然後,梯度的幅度G和斜率θ計算如下:
下面是Sobel濾鏡應用於圖像的方法,以及如何獲得強度和邊緣方向矩陣,Python代碼如下:
from scipy import ndimage
def sobel_filters(img):
Kx = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], np.float32)
Ky = np.array([[1, 2, 1], [0, 0, 0], [-1, -2, -1]], np.float32)
Ix = ndimage.filters.convolve(img, Kx)
Iy = ndimage.filters.convolve(img, Ky)
G = np.hypot(Ix, Iy)
G = G / G.max() * 255
theta = np.arctan2(Iy, Ix)
return (G, theta)
結果幾乎是預期的,我們可以看到,一些邊緣是厚的,另一些是薄的。非最大抑制步驟將有助於我們減輕厚的。
此外,梯度強度水平在0到255之間,這是不均勻的。最終結果的邊緣應具有相同的強度(即白色像素= 255)。
非最大抑制
理想情況下,最終的圖像應該有細邊。因此,我們必須執行非最大抑制以使邊緣變細。
原理很簡單:算法遍歷梯度強度矩陣上的所有點,並找到邊緣方向上具有最大值的像素。
讓我們舉一個簡單的例子:
上圖左上角的紅色框表示被處理的梯度強度矩陣的一個強度像素。對應的邊緣方向由橙色箭頭表示,其角度爲-pi弧度(+/- 180度)。
邊緣方向是橙色虛線(從左到右水平)。該算法的目的是檢查在相同方向上的像素是否比被處理的像素強度高或低。在上面的例子中,正在處理像素(i,j),相同方向上的像素用藍色(i, j-1)和(i, j+1)高亮顯示。如果這兩個像素中的一個比正在處理的那個更強,那麼只保留更強的那個。像素(i, j-1)似乎更強,因爲它是白色的(值255)。因此,當前像素(i, j)的強度值設置爲0。如果邊緣方向上沒有具有更強值的像素,則保留當前像素的值。
現在讓我們關注另一個例子:
在這種情況下,方向是橙色虛線對角線。因此,該方向上最強的像素是像素(i-1,j + 1)。
讓我們總結一下。每個像素有2個主要標準(弧度的邊緣方向和像素強度(0-255之間))。基於這些輸入,非最大抑制步驟是:
- 創建一個初始化爲0的矩陣,該矩陣與原始梯度強度矩陣的大小相同;
- 根據角度矩陣的角度值識別邊緣方向;
- 檢查相同方向的像素是否具有比當前處理的像素更高的強度;
- 返回使用非最大抑制算法處理的圖像。
Python代碼如下:
def non_max_suppression(img, D):
M, N = img.shape
Z = np.zeros((M,N), dtype=np.int32)
angle = D * 180. / np.pi
angle[angle < 0] += 180
for i in range(1,M-1):
for j in range(1,N-1):
try:
q = 255
r = 255
#angle 0
if (0 <= angle[i,j] < 22.5) or (157.5 <= angle[i,j] <= 180):
q = img[i, j+1]
r = img[i, j-1]
#angle 45
elif (22.5 <= angle[i,j] < 67.5):
q = img[i+1, j-1]
r = img[i-1, j+1]
#angle 90
elif (67.5 <= angle[i,j] < 112.5):
q = img[i+1, j]
r = img[i-1, j]
#angle 135
elif (112.5 <= angle[i,j] < 157.5):
q = img[i-1, j-1]
r = img[i+1, j+1]
if (img[i,j] >= q) and (img[i,j] >= r):
Z[i,j] = img[i,j]
else:
Z[i,j] = 0
except IndexError as e:
pass
return Z
結果是相同的圖像,但邊緣更薄。然而,我們仍然可以注意到邊緣亮度的一些變化:一些像素似乎比其他像素更亮,我們將嘗試在最後兩個步驟中彌補這一缺陷。
雙閾值
雙閾值步驟旨在識別3種像素:強,弱和不相關:
- 強像素是指像素的強度如此之高,以至於我們確信它們有助於最終的邊緣。
- 弱像素是具有不足以被視爲強的強度值的像素,但是還不足以被認爲與邊緣檢測不相關。
- 其他像素被認爲與邊緣無關。
現在你可以看到這兩個閾值代表什麼:
- 高閾值用於識別強像素(強度高於高閾值)
- 低閾值用於識別不相關的像素(強度低於低閾值)
- 具有兩個閾值之間的強度的所有像素被標記爲弱,滯後機制(下一步驟)將幫助我們識別可被視爲強的那些和被認爲是不相關的那些。
def threshold(img, lowThresholdRatio=0.05, highThresholdRatio=0.09):
highThreshold = img.max() * highThresholdRatio;
lowThreshold = highThreshold * lowThresholdRatio;
M, N = img.shape
res = np.zeros((M,N), dtype=np.int32)
weak = np.int32(25)
strong = np.int32(255)
strong_i, strong_j = np.where(img >= highThreshold)
zeros_i, zeros_j = np.where(img < lowThreshold)
weak_i, weak_j = np.where((img <= highThreshold) & (img >= lowThreshold))
res[strong_i, strong_j] = strong
res[weak_i, weak_j] = weak
return (res, weak, strong)
此步驟的結果是隻有2個像素強度值(強弱)的圖像:
滯後邊緣跟蹤
根據閾值結果,當且僅當被處理像素周圍至少有一個像素爲強像素時,滯後由弱像素轉換爲強像素構成,如下所述:
def hysteresis(img, weak, strong=255):
M, N = img.shape
for i in range(1, M-1):
for j in range(1, N-1):
if (img[i,j] == weak):
try:
if ((img[i+1, j-1] == strong) or (img[i+1, j] == strong) or (img[i+1, j+1] == strong)
or (img[i, j-1] == strong) or (img[i, j+1] == strong)
or (img[i-1, j-1] == strong) or (img[i-1, j] == strong) or (img[i-1, j+1] == strong)):
img[i, j] = strong
else:
img[i, j] = 0
except IndexError as e:
pass
return img