canny邊緣檢測見OpenCV 【七】————邊緣提取算子(圖像邊緣提取)——canny算法的原理及實現
1 Sobel 導數
1.1.1 原因
-
上面兩節我們已經學習了卷積操作。一個最重要的卷積運算就是導數的計算(或者近似計算).
-
爲什麼對圖像進行求導是重要的呢? 假設我們需要檢測圖像中的 邊緣 ,如下圖:
你可以看到在 邊緣 ,相素值顯著的 改變 了。表示這一 改變 的一個方法是使用 導數 。 梯度值的大變預示着圖像中內容的顯著變化。
-
用更加形象的圖像來解釋,假設我們有一張一維圖形。下圖中灰度值的”躍升”表示邊緣的存在
-
使用一階微分求導我們可以更加清晰的看到邊緣”躍升”的存在(這裏顯示爲高峯值)
-
從上例中我們可以推論檢測邊緣可以通過定位梯度值大於鄰域的相素的方法找到(或者推廣到大於一個閥值).
1.1.2 原因
假設被作用圖像爲 :
-
在兩個方向求導:
-
水平變化: 將 與一個奇數大小的內核 進行卷積。比如,當內核大小爲3時, 的計算結果爲:
-
垂直變化: 將:math:I 與一個奇數大小的內核 進行卷積。比如,當內核大小爲3時, 的計算結果爲:
-
-
在圖像的每一點,結合以上兩個結果求出近似 梯度:
有時也用下面更簡單公式代替:
Note
當內核大小爲 時, 以上Sobel內核可能產生比較明顯的誤差(畢竟,Sobel算子只是求取了導數的近似值)。 爲解決這一問題,OpenCV提供了 Scharr 函數,但該函數僅作用於大小爲3的內核。該函數的運算與Sobel函數一樣快,但結果卻更加精確,其內核爲:
關於( Scharr )的更多信息請參考OpenCV文檔。在下面的示例代碼中,你會發現在 Sobel 函數調用的上面有被註釋掉的 Scharr 函數調用。 反註釋Scharr調用 (當然也要相應的註釋掉Sobel調用),看看該函數是如何工作的。
1.2 代碼
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <stdlib.h>
#include <stdio.h>
using namespace cv;
/** @function main */
int main(int argc, char** argv)
{
Mat src, src_gray;
Mat grad;
char* window_name = "Sobel Demo - Simple Edge Detector";
int scale = 1;
int delta = 0;
int ddepth = CV_16S;
int c;
/// 裝載圖像
src = imread("C:\\Users\\guoqi\\Desktop\\ch7\\4.jpg", 1);
if (!src.data)
{
return -1;
}
GaussianBlur(src, src, Size(3, 3), 0, 0, BORDER_DEFAULT);
/// 轉換爲灰度圖
cvtColor(src, src_gray, CV_RGB2GRAY);
/// 創建顯示窗口
namedWindow(window_name, CV_WINDOW_AUTOSIZE);
/// 創建 grad_x 和 grad_y 矩陣
Mat grad_x, grad_y;
Mat abs_grad_x, abs_grad_y;
/// 求 X方向梯度
//Scharr( src_gray, grad_x, ddepth, 1, 0, scale, delta, BORDER_DEFAULT );
Sobel(src_gray, grad_x, ddepth, 1, 0, 3, scale, delta, BORDER_DEFAULT);
convertScaleAbs(grad_x, abs_grad_x);//將中間結果轉換到 CV_8U:
/// 求Y方向梯度
//Scharr( src_gray, grad_y, ddepth, 0, 1, scale, delta, BORDER_DEFAULT );
Sobel(src_gray, grad_y, ddepth, 0, 1, 3, scale, delta, BORDER_DEFAULT);
convertScaleAbs(grad_y, abs_grad_y);
/// 合併梯度(近似)
addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0, grad);
imshow(window_name, grad);
waitKey(0);
return 0;
}
1.3 實現結果
2 Laplace 算子
2.1 原理
-
前一節我們學習了 Sobel 算子 ,其基礎來自於一個事實,即在邊緣部分,像素值出現”跳躍“或者較大的變化。如果在此邊緣部分求取一階導數,你會看到極值的出現。正如下圖所示:
-
如果在邊緣部分求二階導數會出現什麼情況?
你會發現在一階導數的極值位置,二階導數爲0。所以我們也可以用這個特點來作爲檢測圖像邊緣的方法。 但是, 二階導數的0值不僅僅出現在邊緣(它們也可能出現在無意義的位置),但是我們可以過濾掉這些點。
Laplacian 算子
-
從以上分析中,我們推論二階導數可以用來 檢測邊緣 。 因爲圖像是 “2維”, 我們需要在兩個方向求導。使用Laplacian算子將會使求導過程變得簡單。
-
Laplacian 算子 的定義:
-
OpenCV函數 Laplacian 實現了Laplacian算子。 實際上,由於 Laplacian使用了圖像梯度,它內部調用了 Sobel 算子。
Laplacian( src_gray, dst, ddepth, kernel_size, scale, delta, BORDER_DEFAULT );
函數接受了以下參數:
-
src_gray: 輸入圖像。
-
dst: 輸出圖像
-
ddepth: 輸出圖像的深度。 因爲輸入圖像的深度是 CV_8U ,這裏我們必須定義 ddepth = CV_16S 以避免外溢。
-
kernel_size: 內部調用的 Sobel算子的內核大小,此例中設置爲3。
-
scale, delta 和 BORDER_DEFAULT: 使用默認值。
2.2 代碼
-
裝載圖像
-
使用高斯平滑消除噪聲, 將圖像轉換到灰度空間。
-
使用Laplacian算子作用於灰度圖像,並保存輸出圖像。
-
輸出結果。
#include "opencv2/imgproc/imgproc.hpp" #include "opencv2/highgui/highgui.hpp" #include <stdlib.h> #include <stdio.h> using namespace cv; /** @函數 main */ int main( int argc, char** argv ) { Mat src, src_gray, dst; int kernel_size = 3; int scale = 1; int delta = 0; int ddepth = CV_16S; char* window_name = "Laplace Demo"; int c; /// 裝載圖像 src = imread( argv[1] ); if( !src.data ) { return -1; } /// 使用高斯濾波消除噪聲 GaussianBlur( src, src, Size(3,3), 0, 0, BORDER_DEFAULT ); /// 轉換爲灰度圖 cvtColor( src, src_gray, CV_RGB2GRAY ); /// 創建顯示窗口 namedWindow( window_name, CV_WINDOW_AUTOSIZE ); /// 使用Laplace函數 Mat abs_dst; Laplacian( src_gray, dst, ddepth, kernel_size, scale, delta, BORDER_DEFAULT ); convertScaleAbs( dst, abs_dst ); /// 顯示結果 imshow( window_name, abs_dst ); waitKey(0); return 0; }
2.3 實現結果
3 Canny 邊緣檢測¶
3.1代碼
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <stdlib.h>
#include <stdio.h>
using namespace cv;
/// 全局變量
Mat src, src_gray;
Mat dst, detected_edges;
int edgeThresh = 1;
int lowThreshold;
int const max_lowThreshold = 100;
int ratio = 3;
int kernel_size = 3;
char* window_name = "Edge Map";
/**
* @函數 CannyThreshold
* @簡介: trackbar 交互回調 - Canny閾值輸入比例1:3
*/
void CannyThreshold(int, void*)
{
/// 使用 3x3內核降噪
blur( src_gray, detected_edges, Size(3,3) );
/// 運行Canny算子
Canny( detected_edges, detected_edges, lowThreshold, lowThreshold*ratio, kernel_size );
/// 使用 Canny算子輸出邊緣作爲掩碼顯示原圖像
dst = Scalar::all(0);
src.copyTo( dst, detected_edges);
imshow( window_name, dst );
}
/** @函數 main */
int main( int argc, char** argv )
{
/// 裝載圖像
src = imread( argv[1] );
if( !src.data )
{ return -1; }
/// 創建與src同類型和大小的矩陣(dst)
dst.create( src.size(), src.type() );
/// 原圖像轉換爲灰度圖像
cvtColor( src, src_gray, CV_BGR2GRAY );
/// 創建顯示窗口
namedWindow( window_name, CV_WINDOW_AUTOSIZE );
/// 創建trackbar
createTrackbar( "Min Threshold:", window_name, &lowThreshold, max_lowThreshold, CannyThreshold );
/// 顯示圖像
CannyThreshold(0, 0);
/// 等待用戶反應
waitKey(0);
return 0;
}