上一篇 OpenCV 之 圖像平滑 中,提到的圖像平滑,從信號處理的角度來看,其實是一種「低通濾波器」。html
本篇中,數字圖像的邊緣,一般都是像素值變化劇烈的區域 (「高頻」),故可將邊緣檢測視爲一種 「高通濾波器」。算法
現實中,對應於像素值變化劇烈的狀況以下:app
1) 深度的不連續 (物體處在不一樣的物平面上)ide
2) 表面方向的不連續 (例如,正方體的不一樣的兩個面)函數
3) 物體材料不一樣 (光的反射係數也不一樣)post
4) 場景中光照不一樣 (例如,有樹蔭的路面)ui
OpenCV 中,邊緣檢測經常使用的是索貝爾算子 (Sobel) 和拉普拉斯算子 (Laplace),分別是對圖像求一階導和二階導。url
假定輸入圖像矩陣爲 I,卷積核大小爲 3x3,則水平一階導數 Gx 和垂直一階導數 Gy 分別爲:spa
$\quad G_x = \begin{bmatrix} -1 & 0 & 1 \\ -2 & 0 & 2 \\ -1 & 0 & 1 \\ \end{bmatrix} * I \qquad G_y = \begin{bmatrix} -1 & -2 & -1 \\ 0 & 0 & 0 \\ 1 & 2 & 1 \\ \end{bmatrix} * I $code
輸出的圖像矩陣 G 爲:
$\quad G = \sqrt{G_{x}^2 + G_{y}^2 } \qquad \text{或簡化爲} \qquad G = |G_x| + |G_y| $
OpenCV 中,Sobel 函數以下:
void cv::Sobel ( InputArray src, // 輸入圖像 OutputArray dst, // 輸出圖像 int ddepth, // 輸出圖像深度,-1 表示等於 src.depth() int dx, // 水平方向的階數 int dy, // 垂直方向的階數 int ksize = 3, // 卷積核的大小,常取 1, 3, 5, 7 等奇數 double scale = 1, // 縮放因子,應用於計算結果 double delta = 0, // 增量數值,應用於計算結果
int borderType = BORDER_DEFAULT // 邊界處理模式
)
dx 和 dy 表示階數,通常取 0 或 1,但不超過 2;scale = 1,表示計算結果不縮放;delat = 0,表示計算結果無增量。
當卷積核大小爲 3x3 時,使用 sobel 卷積核來計算並非很精確,此時經常使用 Scharr 卷積核來代替,以下:
$\quad K_x = \begin{bmatrix} -3 & 0 & 3 \\ -10 & 0 & 10 \\ -3 & 0 & 3 \\ \end{bmatrix}\qquad K_y = \begin{bmatrix} -3 & -10 & -3 \\ 0 & 0 & 0 \\ 3 & 10 & 3 \\ \end{bmatrix} $
而 Sharr 函數,本質上就是令 ksize = 3 且使用 Scharr 卷積核的 Sobel 函數。
void cv::Scharr ( InputArray src, OutputArray dst, int ddepth, int dx, int dy, double scale = 1, double delta = 0, int borderType = BORDER_DEFAULT )
對於 Scharr 函數,要求 dx 和 dy 都 >= 0 且 dx + dy == 1,假如 dx 和 dy 都設爲 1,則會拋出異常。
所以,對於 Sobel 和 Scharr 函數,一般各自求其 x 和 y 方向的導數,而後經過加權來進行邊緣檢測。
// Gradient X Scharr( src_gray, grad_x, ddepth, 1, 0, scale, delta, BORDER_DEFAULT ); convertScaleAbs( grad_x, abs_grad_x ); // Gradient Y Scharr( src_gray, grad_y, ddepth, 0, 1, scale, delta, BORDER_DEFAULT );
convertScaleAbs( grad_y, abs_grad_y ); // Total Gradient (approximate) addWeighted( abs_grad_x, 0.5, abs_grad_y, 0.5, 0, grad );
索貝爾算子 (Sobel) 和拉普拉斯算子 (Laplace) 都是用來對圖像進行邊緣檢測的,不一樣之處在於,前者是求一階導,後者是求二階導。
$\quad Laplace(f) = \frac{\partial^2f}{\partial x^2} + \frac{\partial^2f}{\partial y^2} = f(x+1, y) + f(x-1, y) + f(x, y+1) + f(x, y-1) - 4f(x, y)$
OpenCV 中對應的函數爲 Laplacian
void cv::Laplacian ( InputArray src, OutputArray dst, int ddepth, int ksize = 1, double scale = 1, double delta = 0, int borderType = BORDER_DEFAULT )
Canny 邊緣檢測算子,其算法步驟大致以下:
1) 用高斯濾波器對輸入圖像作平滑處理 (大小爲 5x5 的高斯核)
$\quad K = \frac{1}{159} \begin{bmatrix} 2 & 4 & 5 & 4 & 2 \\ 4 & 9 & 12 & 9 & 4 \\ 5 & 12 & 15 & 12 & 5 \\ 4 & 9 & 12 & 9 & 4 \\ 2 & 4 & 5 & 4 & 2 \end{bmatrix}$
2) 計算圖像的梯度強度和角度方向 ( x 和 y 方向上的卷積核)
$\quad K_x = \begin{bmatrix} -1 & 0 & 1 \\ -2 & 0 & 2 \\ -1 & 0 & 1 \\ \end{bmatrix} \qquad K_y = \begin{bmatrix} -1 & -2 & -1 \\ 0 & 0 & 0 \\ 1 & 2 & 1 \\ \end{bmatrix} $
$\quad G = \sqrt{G_{x}^2 + G_{y}^2 } \qquad \theta = \arctan(\dfrac{ G_y }{ G_x }) $
角度方向近似爲四個可能值,即 0, 45, 90, 135
3) 對圖像的梯度強度進行非極大抑制
可看作邊緣細化:只有候選邊緣點被保留,其他的點被移除
4) 利用雙閾值檢測和鏈接邊緣
若候選邊緣點大於上閾值,則被保留;小於下閾值,則被捨棄;處於兩者之間,須視其所鏈接的像素點,大於上閾值則被保留,反之捨棄
OpenCV 中的 Canny 函數以下所示:
void cv::Canny ( InputArray image, // 輸入圖像 (8位) OutputArray edges, // 輸出圖像 (單通道,8位) double threshold1, // 下閾值 double threshold2, // 上閾值 int apertureSize = 3, bool L2gradient = false )
通常 上閾值 / 下閾值 = 2 ~ 3
L2gradient 默認 flase,表示圖像梯度強度的計算採用近似形式;若爲 true,則表示採用更精確的形式。
Sobel 或 Scharr 示例中,使用 addWeighted 函數,來加權合成 x 和 y 方向上各自的一階導數
#include "opencv2/imgproc/imgproc.hpp" #include "opencv2/imgcodecs.hpp" #include "opencv2/highgui/highgui.hpp" #include <stdlib.h> #include <stdio.h> using namespace cv; int main( int, char** argv ) { Mat src, src_gray; Mat grad; const char* window_name = "Sobel Demo - Simple Edge Detector"; int scale = 1; int delta = 0; int ddepth = CV_16S; /// Load an image src = imread( argv[1] ); if( src.empty() ) { return -1; } GaussianBlur( src, src, Size(3,3), 0, 0, BORDER_DEFAULT ); /// Convert it to gray cvtColor( src, src_gray, COLOR_RGB2GRAY ); /// Create window namedWindow( window_name, WINDOW_AUTOSIZE ); /// Generate grad_x and grad_y Mat grad_x, grad_y; Mat abs_grad_x, abs_grad_y; /// Gradient 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 ); /// Gradient 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 ); /// Total Gradient (approximate) addWeighted( abs_grad_x, 0.5, abs_grad_y, 0.5, 0, grad ); imshow( window_name, grad ); waitKey(0); return 0; }
Laplacion 示例中,利用了高斯濾波函數來下降噪聲
#include "opencv2/imgproc/imgproc.hpp" #include "opencv2/imgcodecs.hpp" #include "opencv2/highgui/highgui.hpp" using namespace cv; int main( int, char** argv ) { Mat src, src_gray, dst; int kernel_size = 3; int scale = 1; int delta = 0; int ddepth = CV_16S; const char* window_name = "Laplace Demo"; // 讀圖 src = imread("camera1.bmp"); if( src.empty()) return -1; // 高斯濾波 GaussianBlur( src, src, Size(3,3), 0, 0, BORDER_DEFAULT ); // 灰度圖 cvtColor( src, src_gray, COLOR_RGB2GRAY ); // 窗體 namedWindow( window_name, 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); }
在 Canny 函數以前,也須要 blur 函數,來進行降噪處理
#include "opencv2/imgproc/imgproc.hpp" #include "opencv2/imgcodecs.hpp" #include "opencv2/highgui/highgui.hpp" #include <stdlib.h> #include <stdio.h> using namespace cv; /// Global variables 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; const char* window_name = "Edge Map"; /** * @function CannyThreshold * @brief Trackbar callback - Canny thresholds input with a ratio 1:3 */ static void CannyThreshold(int, void*) { /// Reduce noise with a kernel 3x3 blur( src_gray, detected_edges, Size(3,3) ); /// Canny detector Canny( detected_edges, detected_edges, lowThreshold, lowThreshold*ratio, kernel_size ); /// Using Canny's output as a mask, we display our result dst = Scalar::all(0); src.copyTo( dst, detected_edges); imshow( window_name, dst ); } int main( int, char** argv ) { /// Load an image src = imread( argv[1] ); if( src.empty() ) { return -1; } /// Create a matrix of the same type and size as src (for dst) dst.create( src.size(), src.type() ); /// Convert the image to grayscale cvtColor( src, src_gray, COLOR_BGR2GRAY ); /// Create a window namedWindow( window_name, WINDOW_AUTOSIZE ); /// Create a Trackbar for user to enter threshold createTrackbar( "Min Threshold:", window_name, &lowThreshold, max_lowThreshold, CannyThreshold ); /// Show the image CannyThreshold(0, 0); /// Wait until user exit program by pressing a key waitKey(0); return 0; }
在進行 Sobel,Laplacian 和 Canny 邊緣檢測以前,統一調用 GaussianBlur 來下降圖像噪聲
#include "opencv2/imgproc/imgproc.hpp" #include "opencv2/highgui/highgui.hpp" using namespace std; using namespace cv; int main() { Mat src, src_gray, dst; src = imread("E:/Edge/bird.jpg"); if(src.empty()) return -1;
namedWindow("Original", CV_WINDOW_AUTOSIZE); namedWindow("Sobel", CV_WINDOW_AUTOSIZE); namedWindow("Laplace", CV_WINDOW_AUTOSIZE); namedWindow("Canny", CV_WINDOW_AUTOSIZE); imshow("Original", src); Mat grad_x, grad_y, abs_grad_x, abs_grad_y; GaussianBlur(src, src, Size(3,3),0); cvtColor(src,src_gray,COLOR_BGR2GRAY); Sobel(src_gray, grad_x,CV_16S,0,1); // use CV_16S to avoid overflow convertScaleAbs( grad_x, abs_grad_x ); Sobel(src_gray, grad_y,CV_16S,1,0); // use CV_16S to avoid overflow convertScaleAbs( grad_y, abs_grad_y ); addWeighted( abs_grad_x, 0.5, abs_grad_y, 0.5, 0, dst ); imshow("Sobel", dst); imwrite("Sobel.jpg",dst); Laplacian(src_gray,dst,-1,3); imshow("Laplace", dst); imwrite("Laplace.jpg",dst); Canny(src_gray,dst,100,300); imshow("Canny",dst); imwrite("Canny.jpg",dst); waitKey(0);
}
三種邊緣檢測的效果圖以下:
<Learning OpenCV_2nd>
<OpenCV Tutorials> imgproc module