OpenCV
开源计算机视觉库
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Modules Pages
Canny 边缘检测器

上一篇教程: 拉普拉斯算子
下一篇教程: 霍夫线变换

原作者Ana Huamán
兼容性OpenCV >= 3.0

目标

在本教程中,您将学习如何

  • 使用 OpenCV 函数 cv::Canny 实现 Canny 边缘检测器。

理论

Canny 边缘检测器 [48] 由 John F. Canny 于 1986 年开发。也被许多人称为最佳检测器,Canny 算法旨在满足三个主要标准

  • 低错误率:意味着只对存在的边缘进行良好的检测。
  • 良好的定位:检测到的边缘像素与真实边缘像素之间的距离必须最小化。
  • 最小响应:每个边缘只有一个检测器响应。

步骤

  1. 滤除任何噪声。为此使用高斯滤波器。下面显示了一个可能使用的尺寸为 size=5 的高斯核的示例

    K=1159[245424912945121512549129424542]

  2. 查找图像的强度梯度。为此,我们遵循类似于 Sobel 的过程
    1. 应用一对卷积掩码(在 xy 方向)

      Gx=[10+120+210+1]

      Gy=[121000+1+2+1]

    2. 使用以下公式查找梯度强度和方向

      G=Gx2+Gy2θ=arctan(GyGx)

      方向四舍五入为四个可能的角度之一(即 0、45、90 或 135)
  3. 应用非最大值抑制。这将删除不被认为是边缘一部分的像素。因此,只有细线(候选边缘)将保留。
  4. 滞后:最后一步。Canny 使用两个阈值(上限和下限)

    1. 如果像素梯度高于上限阈值,则该像素被接受为边缘
    2. 如果像素梯度值低于下限阈值,则将其拒绝。
    3. 如果像素梯度介于两个阈值之间,则只有当它连接到高于上限阈值的像素时,才会被接受。

    Canny 建议上限下限比率在 2:1 和 3:1 之间。

  5. 有关更多详细信息,您始终可以查阅您最喜欢的计算机视觉书籍。

代码

  • 下面显示了教程代码。您也可以从 此处 下载它
    #include <iostream>
    using namespace cv;
    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";
    static void CannyThreshold(int, void*)
    {
    blur( src_gray, detected_edges, Size(3,3) );
    Canny( detected_edges, detected_edges, lowThreshold, lowThreshold*ratio, kernel_size );
    dst = Scalar::all(0);
    src.copyTo( dst, detected_edges);
    imshow( window_name, dst );
    }
    int main( int argc, char** argv )
    {
    CommandLineParser parser( argc, argv, "{@input | fruits.jpg | input image}" );
    src = imread( samples::findFile( parser.get<String>( "@input" ) ), IMREAD_COLOR ); // 加载图像
    if( src.empty() )
    {
    std::cout << "无法打开或找到图像!\n" << std::endl;
    std::cout << "用法:" << argv[0] << " <输入图像>" << std::endl;
    return -1;
    }
    dst.create( src.size(), src.type() );
    cvtColor( src, src_gray, COLOR_BGR2GRAY );
    namedWindow( window_name, WINDOW_AUTOSIZE );
    createTrackbar( "最小阈值:", window_name, &lowThreshold, max_lowThreshold, CannyThreshold );
    CannyThreshold(0, 0);
    waitKey(0);
    return 0;
    }
    用于命令行解析。
    定义 utility.hpp:890
    n 维密集数组类
    定义 mat.hpp:829
    MatSize size
    定义 mat.hpp:2177
    void copyTo(OutputArray m) const
    将矩阵复制到另一个矩阵。
    void create(int rows, int cols, int type)
    根据需要分配新的数组数据。
    bool empty() const
    如果数组没有元素,则返回 true。
    int type() const
    返回矩阵元素的类型。
    用于指定图像或矩形大小的模板类。
    定义 types.hpp:335
    std::string String
    定义 cvstd.hpp:151
    int main(int argc, char *argv[])
    定义 highgui_qt.cpp:3
    定义 core.hpp:107
  • 这个程序的功能是什么?
    • 要求用户输入一个数值来设置我们*Canny边缘检测器*的下阈值(通过轨迹条)。
    • 应用*Canny检测器*并生成一个**掩码**(在黑色背景上显示代表边缘的亮线)。
    • 将获得的掩码应用于原始图像并在窗口中显示。
  • 解释 (C++ 代码)

    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";

      注意以下几点

      1. 我们建立了下阈值:上阈值的比例为 3:1(使用变量 *ratio*)。
      2. 我们将内核大小设置为 3(用于Canny函数内部执行的Sobel运算)。
      3. 我们将下阈值的最大值设置为 100。
    2. 加载源图像
      CommandLineParser parser( argc, argv, "{@input | fruits.jpg | input image}" );
      src = imread( samples::findFile( parser.get<String>( "@input" ) ), IMREAD_COLOR ); // 加载图像
      if( src.empty() )
      {
      std::cout << "无法打开或找到图像!\n" << std::endl;
      std::cout << "用法:" << argv[0] << " <输入图像>" << 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. 创建一个轨迹条,供用户输入Canny检测器的下阈值
      createTrackbar( "Min Threshold:", window_name, &lowThreshold, max_lowThreshold, CannyThreshold );
      观察以下内容
      1. 轨迹条控制的变量是 *lowThreshold*,其限制为 *max_lowThreshold*(我们之前将其设置为 100)
      2. 每次轨迹条注册一个操作时,都会调用回调函数 *CannyThreshold*。
    7. 让我们一步一步检查 *CannyThreshold* 函数
      1. 首先,我们使用大小为 3 的滤波器模糊图像
        blur( src_gray, detected_edges, Size(3,3) );
      2. 其次,我们应用OpenCV函数 cv::Canny
        Canny( detected_edges, detected_edges, lowThreshold, lowThreshold*ratio, kernel_size );
        其中参数为
        • *detected_edges*:源图像,灰度图像
        • *detected_edges*:检测器的输出(可以与输入相同)
        • *lowThreshold*:用户移动轨迹条输入的值
        • *highThreshold*:程序中设置为下阈值的3倍(遵循Canny的建议)
        • *kernel_size*:我们将其定义为 3(内部使用的Sobel内核的大小)
    8. 我们用零填充 *dst* 图像(这意味着图像完全是黑色的)。
      dst = Scalar::all(0);
    9. 最后,我们将使用函数 cv::Mat::copyTo 仅映射被识别为边缘的图像区域(在黑色背景上)。cv::Mat::copyTo 将 *src* 图像复制到 *dst*。但是,它只会复制在非零值位置的像素。由于Canny检测器的输出是在黑色背景上的边缘轮廓,因此生成的 *dst* 在所有区域都将是黑色,除了检测到的边缘。
      src.copyTo( dst, detected_edges);
    10. 我们显示结果
      imshow( window_name, dst );

    结果

    • 编译上面的代码后,我们可以运行它,并将图像路径作为参数传递。例如,使用以下图像作为输入
    • 移动滑块,尝试不同的阈值,我们得到以下结果
    • 注意图像如何在边缘区域叠加在黑色背景上。