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

    結果

    • 編譯上述程式碼後,我們可以透過提供影像路徑作為引數來執行它。例如,使用以下影像作為輸入:
    • 移動滑塊,嘗試不同的閾值,我們得到以下結果:
    • 請注意影像是如何在邊緣區域疊加到黑色背景上的。