OpenCV 4.12.0
开源计算机视觉
加载中...
搜索中...
无匹配项
Canny 边缘检测器

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

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

目标

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

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

理论

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

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

步骤

  1. 滤除噪声。高斯滤波器用于此目的。下面显示了一个可能使用的 \(size = 5\) 的高斯核示例:

    \[K = \dfrac{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. 找到图像的强度梯度。为此,我们遵循类似于 Sobel 的步骤:
    1. 应用一对卷积核(在 \(x\) 和 \(y\) 方向):

      \[G_{x} = \begin{bmatrix} -1 & 0 & +1 \\ -2 & 0 & +2 \\ -1 & 0 & +1 \end{bmatrix}\]

      \[G_{y} = \begin{bmatrix} -1 & -2 & -1 \\ 0 & 0 & 0 \\ +1 & +2 & +1 \end{bmatrix}\]

    2. 用以下公式找到梯度强度和方向:

      \[\begin{array}{l} G = \sqrt{ G_{x}^{2} + G_{y}^{2} } \\ \theta = \arctan(\dfrac{ G_{y} }{ G_{x} }) \end{array}\]

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

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

    Canny 建议阈值与阈值的比率在 2:1 到 3:1 之间。

  5. 更多细节,您可以查阅您喜欢的计算机视觉书籍。

代码

  • 此程序的作用是什么?
    • 要求用户输入一个数值来设置我们的 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. 我们设置 Sobel 运算的核大小为 \(3\)(Canny 函数内部会执行这些运算)。
      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 << "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. 创建一个滚动条,供用户输入 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:在程序中设置为低阈值的三倍(遵循 Canny 的建议)
        • kernel_size:我们将其定义为 3(内部使用的 Sobel 核的大小)
    8. 我们将 dst 图像填充为零(意味着图像完全是黑色的)。
      dst = Scalar::all(0);
    9. 最后,我们将使用函数 cv::Mat::copyTo 来仅映射图像中被识别为边缘的区域(在黑色背景上)。cv::Mat::copyTosrc 图像复制到 dst。但是,它只会复制像素值非零位置的像素。由于 Canny 检测器的输出是黑色背景上的边缘轮廓,因此生成的 dst 除了检测到的边缘外,所有区域都将是黑色的。
      src.copyTo( dst, detected_edges);
    10. 我们显示结果
      imshow( window_name, dst );

    结果

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