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 实现结果
3Canny 边缘检测¶
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 );/// 创建trackbarcreateTrackbar( "Min Threshold:", window_name, &lowThreshold, max_lowThreshold, CannyThreshold );/// 显示图像CannyThreshold(0, 0);/// 等待用户反应waitKey(0);return 0;}