圖像邊緣信息主要集中在高頻段,一般說圖像銳化或檢測邊緣,實質就是高頻濾波。咱們知道微分運算是求信號的變化率,具備增強高頻份量的做用。在空域運算中來講,對圖像的銳化就是計算微分。因爲數字圖像的離散信號,微分運算就變成計算差分或梯度。圖像處理中有多種邊緣檢測(梯度)算子,經常使用的包括普通一階差分,Robert算子(交叉差分),Sobel算子等等,是基於尋找梯度強度。拉普拉斯算子(二階差分)是基於過零點檢測。經過計算梯度,設置閥值,獲得邊緣圖像。算法
Canny邊緣檢測算子是一種多級檢測算法。1986年由John F. Canny提出,同時提出了邊緣檢測的三大準則:ide
Canny算法出現之後一直是做爲一種標準的邊緣檢測算法,此後也出現了各類基於Canny算法的改進算法。時至今日,Canny算法及其各類變種依舊是一種優秀的邊緣檢測算法。並且除非前提條件很適合,你很難找到一種邊緣檢測算子能顯著地比Canny算子作的更好。url
關於各類差分算子,還有Canny算子的簡單介紹,這裏就不羅嗦了,網上均可以找獲得。直接進入Canny算法的實現。Canny算法分爲幾步。spa
1. 高斯模糊。3d
這一步很簡單,相似於LoG算子(Laplacian of Gaussian)做高斯模糊同樣,主要做用就是去除噪聲。由於噪聲也集中於高頻信號,很容易被識別爲僞邊緣。應用高斯模糊去除噪聲,下降僞邊緣的識別。可是因爲圖像邊緣信息也是高頻信號,高斯模糊的半徑選擇很重要,過大的半徑很容易讓一些弱邊緣檢測不到。code
Lena原圖 Lena高斯模糊,半徑2blog
2. 計算梯度幅值和方向。隊列
圖像的邊緣能夠指向不一樣方向,所以經典Canny算法用了四個梯度算子來分別計算水平,垂直和對角線方向的梯度。可是一般都不用四個梯度算子來分別計算四個方向。經常使用的邊緣差分算子(如Rober,Prewitt,Sobel)計算水平和垂直方向的差分Gx和Gy。這樣就能夠以下計算梯度模和方向:ip
梯度角度θ範圍從弧度-π到π,而後把它近似到四個方向,分別表明水平,垂直和兩個對角線方向(0°,45°,90°,135°)。能夠以±iπ/8(i=1,3,5,7)分割,落在每一個區域的梯度角給一個特定值,表明四個方向之一。ci
這裏我選擇Sobel算子計算梯度。Sobel算法很簡單,處處均可以找到,就不列出代碼來了。相對於其餘邊緣算子,Sobel算子得出來的邊緣粗大明亮。
下圖是對上面半徑2的高斯模糊圖像L通道(HSL)應用Sobel算子的梯度模圖,沒有施加任何閥值。
Sobel算子,無閥值
3. 非最大值抑制。
非最大值抑制是一種邊緣細化方法。一般得出來的梯度邊緣不止一個像素寬,而是多個像素寬。就像咱們所說Sobel算子得出來的邊緣粗大而明亮,從上面Lena圖的Sobel結果能夠看得出來。所以這樣的梯度圖仍是很「模糊」。而準則3要求,邊緣只有一個精確的點寬度。非最大值抑制能幫助保留局部最大梯度而抑制全部其餘梯度值。這意味着只保留了梯度變化中最銳利的位置。算法以下:
注意,方向的正負是不起做用的,好比東南方向和西北方向是同樣的,都認爲是對角線的一個方向。前面咱們把梯度方向近似到水平,垂直和兩個對角線四個方向,因此每一個像素根據自身方向在這四個方向之一進行比較,決定是否保留。這一部分的代碼也很簡單,列出以下。pModule,pDirection分別記錄了上一步梯度模值和梯度方向。
pmoddrow = pModule + Width + 1; pdirdrow = pDirection + Width + 1; pstrongdrow = pStrong + Width + 1; for (i = 1; i < Hend - 1; i++) { pstrongd = pstrongdrow; pmodd = pmoddrow; pdird = pdirdrow; for (j = 1; j < Wend - 1; j++) { switch (*pdird) { case 0: // x direction
case 4: if (*pmodd > *(pmodd - 1) && *pmodd > *(pmodd + 1)) *pstrongd = 255; break; case 1: // northeast-southwest direction. Notice the data order on y direction of bmp data
case 5: if (*pmodd > *(pmodd + Width + 1) && *pmodd > *(pmodd - Width - 1)) *pstrongd = 255; break; case 2: // y direction
case 6: if (*pmodd > *(pmodd - Width) && *pmodd > *(pmodd + Width)) *pstrongd = 255; break; case 3: // northwest-southeast direction. Notice the data order on y direction of bmp data
case 7: if (*pmodd > *(pmodd + Width - 1) && *pmodd > *(pmodd - Width + 1)) *pstrongd = 255; break; default: ASSERT(0); break; } pstrongd++; pmodd++; pdird++; } pstrongdrow += Width; pmoddrow += Width; pdirdrow += Width; }
下圖是非最大值抑制的結果。可見邊緣寬度已經大大減少。可是這個圖像中由於沒有應用任何閥值,還含有大量小梯度模值的點,也就是圖中很暗的地方。下面,閥值要上場了。
非最大值抑制結果
4. 雙閥值。
通常的邊緣檢測算法用一個閥值來濾除噪聲或顏色變化引發的小的梯度值,而保留大的梯度值。Canny算法應用雙閥值,即一個高閥值和一個低閥值來區分邊緣像素。若是邊緣像素點梯度值大於高閥值,則被認爲是強邊緣點。若是邊緣梯度值小於高閥值,大於低閥值,則標記爲弱邊緣點。小於低閥值的點則被抑制掉。這一步算法很簡單。
5. 滯後邊界跟蹤。
至此,強邊緣點能夠認爲是真的邊緣。弱邊緣點則多是真的邊緣,也多是噪聲或顏色變化引發的。爲獲得精確的結果,後者引發的弱邊緣點應該去掉。一般認爲真實邊緣引發的弱邊緣點和強邊緣點是連通的,而又噪聲引發的弱邊緣點則不會。所謂的滯後邊界跟蹤算法檢查一個弱邊緣點的8連通領域像素,只要有強邊緣點存在,那麼這個弱邊緣點被認爲是真是邊緣保留下來。
這個算法搜索全部連通的弱邊緣,若是一條連通的弱邊緣的任何一個點和強邊緣點連通,則保留這條弱邊緣,不然抑制這條弱邊緣。搜索時能夠用廣度優先或者深度優先算法,我在這裏實現了應該是最容易的深度優先算法。一次連通一條邊緣的深度優先算法以下:
// 5. Edge tracking by hysteresis
stack<CPoint> s; queue<CPoint> q; BOOL connected = FALSE; long row_idx = Width; for (i = 1; i < Height - 1; i++, row_idx += Width) { for (j = 1; j < Width - 1; j++) { pweakd = pWeak + row_idx + j; if (*pweakd == 255) { s.push(CPoint(j, i)); q.push(CPoint(j, i)); *pweakd = 1; // Label it
while (!s.empty()) { CPoint p = s.top(); s.pop(); // Search weak edge 8-point neighborhood
pweakd = pWeak + p.y*Width + p.x; if (*(pweakd - Width - 1) == 255) { CPoint np = CPoint(p.x - 1, p.y - 1); s.push(np); q.push(np); *(pweakd - Width - 1) = 1; // Label it
} if (*(pweakd - Width) == 255) { CPoint np = CPoint(p.x, p.y - 1); s.push(np); q.push(np); *(pweakd - Width) = 1; // Label it
} if (*(pweakd - Width + 1) == 255) { CPoint np = CPoint(p.x + 1, p.y - 1); s.push(np); q.push(np); *(pweakd - Width + 1) = 1; // Label it
} if (*(pweakd - 1) == 255) { CPoint np = CPoint(p.x - 1, p.y); s.push(np); q.push(np); *(pweakd - 1) = 1; // Label it
} if (*(pweakd + 1) == 255) { CPoint np = CPoint(p.x + 1, p.y); s.push(np); q.push(np); *(pweakd + 1) = 1; // Label it
} if (*(pweakd + Width - 1) == 255) { CPoint np = CPoint(p.x - 1, p.y + 1); s.push(np); q.push(np); *(pweakd + Width - 1) = 1; // Label it
} if (*(pweakd + Width) == 255) { CPoint np = CPoint(p.x, p.y + 1); s.push(np); q.push(np); *(pweakd + Width) = 1; // Label it
} if (*(pweakd + Width + 1) == 255) { CPoint np = CPoint(p.x + 1, p.y + 1); s.push(np); q.push(np); *(pweakd + Width + 1) = 1; // Label it
} // Search strong edge 8-point neighborhood
if (connected == FALSE) { pstrongd = pStrong + p.y*Width + p.x; for (int m = -1; m <= 1; m++) { for (int n = -1; n <= 1; n++) { if (*(pstrongd + m*Width + n) == 255) { connected = TRUE; goto next; } } } } next: continue; } // No more element in the stack
if (connected == FALSE) { // The weak edge is not connected to any strong edge. Suppress it.
while (!q.empty()) { CPoint p = q.front(); q.pop(); pWeak[p.y*Width + p.x] = 0; } } else { // Clean the queue
while (!q.empty()) q.pop(); connected = FALSE; } } } } // Add the connected weak edges (labeled) into strong edge image. // All strong edge pixels are labeled 255, otherwise 0.
for (i = 0; i < len; i++) { if (pWeak[i] == 1) pStrong[i] = 255; }
下面是對Lena圖計算Canny邊緣檢測的梯度模圖和二值化圖,高斯半徑2,高閥值100,低閥值50。
Canny檢測梯度模圖 Canny檢測梯度二值圖
做爲對比,下面是用一階差分和Sobel算子對原圖計算的結果,閥值100。因爲一階差分的梯度值相對較小,我對一階差分的梯度值放大了必定倍數,使得它和Sobel的梯度值保持一樣的水平。
一階差分梯度模圖 一階差分梯度二值圖
Sobel梯度模圖 Sobel梯度二值圖
很明顯,Canny邊緣檢測的效果是很顯著的。相比普通的梯度算法大大抑制了噪聲引發的僞邊緣,並且是邊緣細化,易於後續處理。對於對比度較低的圖像,經過調節參數,Canny算法也能有很好的效果。
原圖 Canny梯度模,高斯半徑2,低閥值30,高閥值100 Canny梯度二值化圖,高斯半徑2,低閥值30,高閥值100
原圖 Canny梯度模,高斯半徑1,低閥值40,高閥值80 Canny梯度二值化圖,高斯半徑1,低閥值40,高閥值80
參考