Goal
在本教程中,您将学习如何:
使用 OpenCV 函数 cv::Canny 来实现 Canny 边缘检测器。
Theory
Canny 边缘检测器 [42] 由 John F. Canny 于 1986 年开发。Canny 算法也被许多人称为最佳检测器,旨在满足三个主要标准:
低错误率Low error rate:意味着对仅存在的边缘的良好检测。
良好的定位Good localization:必须最小化检测到的边缘像素和真实边缘像素之间的距离。
最小响应Minimal response:每个边缘只有一个检测器响应。
Steps
1. 过滤掉任何噪音。 高斯滤波器用于此目的。 可能使用的大小为 5 的高斯核的示例如下所示:
2.找到图像的强度梯度。 为此,我们遵循类似于 Sobel 的程序:
a. 应用一对卷积掩码(在 x 和 y 方向:
2. 找到梯度强度和方向:
方向四舍五入到四个可能的角度之一(即 0、45、90 或 135)
3. Non-maximum应用非最大抑制。 这会删除不被视为边缘一部分的像素。 因此,只保留细线(候选边缘)。
4. Hysteresis滞后:最后一步。 Canny 确实使用了两个阈值(上限和下限):
如果像素梯度高于上限阈值,则该像素被接受为边缘
如果像素梯度值低于下阈值,则将其拒绝。
如果像素梯度在两个阈值之间,那么只有当它连接到高于上限阈值的像素时才会被接受。
Canny 推荐了 2:1 和 3:1 之间的上下比例。
5. 有关更多详细信息,您可以随时查阅您最喜欢的计算机视觉书籍。
Code
教程代码如下所示。 你也可以从这里opencv/CannyDetector_Demo.cpp at 4.x · opencv/opencv · GitHub 下载
/**
* @file CannyDetector_Demo.cpp
* @brief Sample code showing how to detect edges using the Canny Detector
* @author OpenCV team
*/
#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"
#include <iostream>
using namespace cv;
//![variables]
Mat src, src_gray;
Mat dst, detected_edges;
int lowThreshold = 0;//阈值下限
const int max_lowThreshold = 100;//阈值下限最大值
const int ratio = 3;//阈值上限计算系数
const int kernel_size = 3;//sobel内核尺寸
const char* window_name = "Edge Map";
//![variables]
/**
* @function CannyThreshold
* @brief Trackbar callback - Canny thresholds input with a ratio 1:3
* Trackbar 回调 - Canny 阈值输入,比率为 1:3
*/
static void CannyThreshold(int, void*)
{
//![reduce_noise]
/// 使用 3x3 内核降低噪音
blur( src_gray, detected_edges, Size(3,3) );
//![reduce_noise]
//![canny]
/// Canny 边缘检测
Canny( detected_edges, detected_edges, lowThreshold, lowThreshold*ratio, kernel_size );
//![canny]
/// 使用 Canny 的输出作为掩码,我们显示我们的结果Using Canny's output as a mask, we display our result
//![fill]
dst = Scalar::all(0);
//![fill]
//![copyto]
src.copyTo( dst, detected_edges);
//![copyto]
//![display]
imshow( window_name, dst );
//![display]
}
/**
* @function main
*/
int main( int argc, char** argv )
{
//![load]
CommandLineParser parser( argc, argv, "{@input | fruits.jpg | input image}" );
src = imread( samples::findFile( parser.get<String>( "@input" ) ), IMREAD_COLOR ); // 加载图像
if( src.empty() )
{
std::cout << "Could not open or find the image!\n" << std::endl;
std::cout << "Usage: " << argv[0] << " <Input image>" << std::endl;
return -1;
}
//![load]
//![create_mat]
/// 创建与 src 相同类型和大小的矩阵(用于 dst)
dst.create( src.size(), src.type() );//创建目标矩阵
//![create_mat]
//![convert_to_gray]
cvtColor( src, src_gray, COLOR_BGR2GRAY );//灰度图
//![convert_to_gray]
//![create_window]
namedWindow( window_name, WINDOW_AUTOSIZE );//创建窗口
//![create_window]
//![create_trackbar]
/// 为用户创建一个 Trackbar 以输入阈值
createTrackbar( "Min Threshold:", window_name, &lowThreshold, max_lowThreshold, CannyThreshold );
//![create_trackbar]
/// 显示图像
CannyThreshold(0, 0);
/// 等到用户通过按键退出程序 Wait until user exit program by pressing a key
waitKey(0);
return 0;
}
这个程序有什么作用?
要求用户输入数值以设置我们的 Canny 边缘检测器的下限阈值(通过 Trackbar 的方式)。
应用 Canny Detector 并生成一个mask(代表黑色背景上的边缘的亮线)。
在原始图像上应用获得的mask并在窗口中显示。
Explanation (C++ code)
1. 创建一些需要的变量
Mat src, src_gray;
Mat dst, detected_edges;
int lowThreshold = 0;
const int max_lowThreshold = 100;
const int ratio = 3;
const int kernel_size = 3;
const char* window_name = "Edge Map";
请注意以下事项:
我们建立了一个 3:1 的下限:上限阈值的比率(具有可变比率)。
我们将内核大小设置为 3(Sobel 操作由 Canny 函数在内部执行)。
我们为下限阈值设置最大值 100
2. 加载源图像:
CommandLineParser parser( argc, argv, "{@input | fruits.jpg | input image}" );
src = imread( samples::findFile( parser.get<String>( "@input" ) ), IMREAD_COLOR ); // Load an image
if( src.empty() )
{
std::cout << "Could not open or find the image!\n" << std::endl;
std::cout << "Usage: " << argv[0] << " <Input image>" << std::endl;
return -1;
}
3. 创建一个与src 类型和大小相同的矩阵dst
dst.create( src.size(), src.type() );
4. 将图像转换为灰度(使用函数 cv::cvtColor ):
cvtColor( src, src_gray, COLOR_BGR2GRAY );
5.创建一个窗口来显示结果
namedWindow( window_name, WINDOW_AUTOSIZE );
6.为用户创建一个 Trackbar 以输入我们的 Canny 检测器的下限阈值
createTrackbar( "Min Threshold:", window_name, &lowThreshold, max_lowThreshold, CannyThreshold );
请注意以下事项:
Trackbar 控制的变量是 lowThreshold,限制为 max_lowThreshold(我们之前设置为 100)
每次Trackbar注册一个动作,都会调用回调函数CannyThreshold。
7. 让我们一步一步检查 CannyThreshold 函数
首先,我们使用内核大小为 3 的过滤器对图像进行模糊处理
blur( src_gray, detected_edges, Size(3,3) );
其次,我们应用 OpenCV 函数 cv::Canny :
Canny( detected_edges, detected_edges, lowThreshold, lowThreshold*ratio, kernel_size );
参数是:
detected_edges:源图像,灰度
detected_edges:检测器的输出(可以和输入一样)
lowThreshold:用户移动Trackbar输入的值
highThreshold:在程序中设置为下限阈值的三倍(按照 Canny 的推荐)
kernel_size:我们定义为3(内部使用的Sobel内核的大小)
8. 我们用零填充 dst 图像(意味着图像完全是黑色的)
dst = Scalar::all(0);
9. 最后,我们将使用函数 cv::Mat::copyTo 仅映射图像中被识别为边缘的区域(在黑色背景上)。 cv::Mat::copy 将 src 图像复制到 dst。 但是,它只会复制具有非零值位置的像素。 由于 Canny 检测器的输出是黑色背景上的边缘轮廓,因此除了检测到的边缘之外,所得的 dst 在所有区域中都是黑色的。
src.copyTo( dst, detected_edges);
10.我们展示我们的结果
imshow( window_name, dst );
Result
编译上面的代码后,我们可以运行它,将图像的路径作为参数。 例如,使用以下图像作为输入:
移动滑块,尝试不同的阈值,我们得到以下结果:
注意图像是如何叠加到边缘区域的黑色背景上的。