1. 程式人生 > >【OpenCV入門教程之十二】OpenCV邊緣檢測 Canny運算元 Sobel運算元 Laplace運算元 Scharr濾波

【OpenCV入門教程之十二】OpenCV邊緣檢測 Canny運算元 Sobel運算元 Laplace運算元 Scharr濾波

               

本篇文章中,我們將一起學習OpenCV中邊緣檢測的各種運算元和濾波器——Canny運算元,Sobel運算元,Laplace運算元以及Scharr濾波器。文章中包含了五個淺墨為大家準備的詳細註釋的博文配套原始碼。在介紹四塊知識點的時候分別一個,以及最後的綜合示例中的一個。文章末尾提供配套原始碼的下載。

依然是是放出一些程式執行截圖吧:

 

 效果圖看完,我們來嘮嘮嗑。

首先需要說明的是,淺墨這篇文章最後的示例程式碼是採用兩週前剛剛釋出的2.4.9來書寫的。裡面的lib都已經改成了2.4.9版本的。如果大家需要執行的話,要麼配置好2.4.9.要麼把淺墨在工程中包含的末尾數字為249的各種lib改成之前的248或者你對應的OpenCV版本。

不然會提示: LINK : fatal error LNK1181: 無法開啟輸入檔案“opencv_calib3d248.lib”之類的錯誤。

OpenCV 2.4.9的配置和之前的2.4.8差不多,如果還是不太清楚,具體可以參考淺墨修改過的對應2.4.9版的配置文章:

第二,給大家分享一個OpenCV中寫程式碼時節約時間的小常識。其實OpenCV中,不用namedWindow,直接imshow就可以顯示出視窗。大家看下文的示例程式碼就可以發現,淺墨在寫程式碼的時候並沒有用namedWindow,遇到想顯示出來的Mat變數直接imshow。我們一般是為了規範,才先用namedWindow建立視窗,再imshow出它來,因為我們還有需要用到指定視窗名稱的地方,比如用到trackbar的時候。而一般情況想顯示一個Mat變數的圖片的話,直接imshow就可以啦。

OK,開始正文吧~

一、關於邊緣檢測

在具體介紹之前,先來一起看看邊緣檢測的一般步驟吧。

1)濾波:邊緣檢測的演算法主要是基於影象強度的一階和二階導數,但導數通常對噪聲很敏感,因此必須採用濾波器來改善與噪聲有關的邊緣檢測器的效能。常見的濾波方法主要有高斯濾波,即採用離散化的高斯函式產生一組歸一化的高斯核(具體見“高斯濾波原理及其程式設計離散化實現方法”一文),然後基於高斯核函式對影象灰度矩陣的每一點進行加權求和(具體程式實現見下文)。

  2)增強:增強邊緣的基礎是確定影象各點鄰域強度的變化值。增強演算法可以將影象灰度點鄰域強度值有顯著變化的點凸顯出來。在具體程式設計實現時,可通過計算梯度幅值來確定。

3)檢測:經過增強的影象,往往鄰域中有很多點的梯度值比較大,而在特定的應用中,這些點並不是我們要找的邊緣點,所以應該採用某種方法來對這些點進行取捨。實際工程中,常用的方法是通過閾值化方法來檢測。

另外,需要注意,下文中講到的Laplace運算元,sobel運算元和Scharr運算元都是帶方向的,所以,示例中我們分別寫了X方向,Y方向和最終合成的的效果圖。

OK,正餐開始,召喚canny運算元。:)

二、canny運算元篇

2.1 canny運算元相關理論與概念講解

2.1.1 canny運算元簡介

Canny邊緣檢測運算元是John F.Canny於 1986 年開發出來的一個多級邊緣檢測演算法。更為重要的是 Canny 創立了邊緣檢測計算理論(Computational theory ofedge detection),解釋了這項技術是如何工作的。Canny邊緣檢測演算法以Canny的名字命名,被很多人推崇為當今最優的邊緣檢測的演算法。

其中,Canny 的目標是找到一個最優的邊緣檢測演算法,讓我們看一下最優邊緣檢測的三個主要評價標準:

1.低錯誤率: 標識出盡可能多的實際邊緣,同時儘可能的減少噪聲產生的誤報。

2.高定位性: 標識出的邊緣要與影象中的實際邊緣儘可能接近。

3.最小響應: 影象中的邊緣只能標識一次,並且可能存在的影象噪聲不應標識為邊緣。

為了滿足這些要求 Canny 使用了變分法,這是一種尋找滿足特定功能的函式的方法。最優檢測使用四個指數函式項的和表示,但是它非常近似於高斯函式的一階導數。

2.1.2Canny 邊緣檢測的步驟

1.消除噪聲。 一般情況下,使用高斯平滑濾波器卷積降噪。 如下顯示了一個 size = 5 的高斯核心示例:

2.計算梯度幅值和方向。 此處,按照Sobel濾波器的步驟。

Ⅰ.運用一對卷積陣列 (分別作用於 x 和 y 方向):

 

Ⅱ.使用下列公式計算梯度幅值和方向:

梯度方向近似到四個可能角度之一(一般為0, 45, 90, 135)

3.非極大值抑制。 這一步排除非邊緣畫素, 僅僅保留了一些細線條(候選邊緣)。

4.滯後閾值。最後一步,Canny 使用了滯後閾值,滯後閾值需要兩個閾值(高閾值和低閾值):

Ⅰ.如果某一畫素位置的幅值超過 高 閾值, 該畫素被保留為邊緣畫素。

Ⅱ.如果某一畫素位置的幅值小於 低 閾值, 該畫素被排除。

.如果某一畫素位置的幅值在兩個閾值之間,該畫素僅僅在連線到一個高於 高 閾值的畫素時被保留。

tips:對於Canny函式的使用,推薦的高低閾值比在2:1到3:1之間。

2.2 OpenCV中Canny函式詳解

Canny函式利用Canny演算法來進行影象的邊緣檢測。

C++: void Canny(InputArray image,OutputArray edges, double threshold1, double threshold2, int apertureSize=3,bool L2gradient=false )
  • 第一個引數,InputArray型別的image,輸入影象,即源影象,填Mat類的物件即可,且需為單通道8點陣圖像。
  • 第二個引數,OutputArray型別的edges,輸出的邊緣圖,需要和源圖片有一樣的尺寸和型別。
  • 第三個引數,double型別的threshold1,第一個滯後性閾值。
  • 第四個引數,double型別的threshold2,第二個滯後性閾值。
  • 第五個引數,int型別的apertureSize,表示應用Sobel運算元的孔徑大小,其有預設值3。
  • 第六個引數,bool型別的L2gradient,一個計算影象梯度幅值的標識,有預設值false。

需要注意的是,這個函式閾值1和閾值2兩者的小者用於邊緣連線,而大者用來控制強邊緣的初始段,推薦的高低閾值比在2:1到3:1之間。

呼叫示例:

//載入原始圖        Mat src = imread("1.jpg");  //工程目錄下應該有一張名為1.jpg的素材圖       Canny(src, src, 3, 9,3 );       imshow("【效果圖】Canny邊緣檢測", src);

如上三句,就有結果出來,非常好用。

2.3 呼叫Canny函式的例項程式碼

OpenCV中呼叫Canny函式的例項程式碼如下:

//-----------------------------------【標頭檔案包含部分】---------------------------------------//            描述:包含程式所依賴的標頭檔案//----------------------------------------------------------------------------------------------#include <opencv2/opencv.hpp>#include<opencv2/highgui/highgui.hpp>#include<opencv2/imgproc/imgproc.hpp>//-----------------------------------【名稱空間宣告部分】---------------------------------------//            描述:包含程式所使用的名稱空間//-----------------------------------------------------------------------------------------------using namespace cv;//-----------------------------------【main( )函式】--------------------------------------------//            描述:控制檯應用程式的入口函式,我們的程式從這裡開始//-----------------------------------------------------------------------------------------------int main( )//載入原始圖   Mat src = imread("1.jpg");  //工程目錄下應該有一張名為1.jpg的素材圖 Mat src1=src.clone(); //顯示原始圖  imshow("【原始圖】Canny邊緣檢測", src);  //---------------------------------------------------------------------------------- // 一、最簡單的canny用法,拿到原圖後直接用。 //---------------------------------------------------------------------------------- Canny( src, src, 150, 100,3 ); imshow("【效果圖】Canny邊緣檢測", src);   //---------------------------------------------------------------------------------- // 二、高階的canny用法,轉成灰度圖,降噪,用canny,最後將得到的邊緣作為掩碼,拷貝原圖到效果圖上,得到彩色的邊緣圖 //---------------------------------------------------------------------------------- Mat dst,edge,gray; // 【1】建立與src同類型和大小的矩陣(dst) dst.create( src1.size(), src1.type() ); // 【2】將原影象轉換為灰度影象 cvtColor( src1, gray, CV_BGR2GRAY ); // 【3】先用使用 3x3核心來降噪 blur( gray, edge, Size(3,3) ); // 【4】執行Canny運算元 Canny( edge, edge, 3, 9,3 ); //【5】將g_dstImage內的所有元素設定為0  dst = Scalar::all(0); //【6】使用Canny運算元輸出的邊緣圖g_cannyDetectedEdges作為掩碼,來將原圖g_srcImage拷到目標圖g_dstImage中 src1.copyTo( dst, edge); //【7】顯示效果圖  imshow("【效果圖】Canny邊緣檢測2", dst);  waitKey(0);  return 0; }

執行效果圖:

 

 

三、sobel運算元篇

3.1 sobel運算元相關理論與概念講解

3.1.1 基本概念

Sobel 運算元是一個主要用作邊緣檢測的離散微分運算元 (discrete differentiation operator)。 它Sobel運算元結合了高斯平滑和微分求導,用來計算影象灰度函式的近似梯度。在影象的任何一點使用此運算元,將會產生對應的梯度向量或是其法向量。

sobel運算元的wikipedia:

sobel運算元相關概念,還可以參看這篇博文:

3.1.2 sobel運算元的計算過程

我們假設被作用影象為 I.然後進行如下的操作:

1.分別在x和y兩個方向求導。

Ⅰ.水平變化: 將 I 與一個奇數大小的核心進行卷積。比如,當核心大小為3時, 的計算結果為:

 

Ⅱ.垂直變化: 將: I 與一個奇數大小的核心進行卷積。比如,當核心大小為3時, 的計算結果為:

 

2.在影象的每一點,結合以上兩個結果求出近似梯度:

另外有時,也可用下面更簡單公式代替:

 

3.2 OpenCV中Sobel函式詳解

Sobel函式使用擴充套件的 Sobel 運算元,來計算一階、二階、三階或混合影象差分。

C++: void Sobel (InputArray src,//輸入圖 OutputArray dst,//輸出圖 int ddepth,//輸出影象的深度 int dx, int dy, int ksize=3double scale=1double delta=0int borderType=BORDER_DEFAULT );
  • 第一個引數,InputArray 型別的src,為輸入影象,填Mat型別即可。
  • 第二個引數,OutputArray型別的dst,即目標影象,函式的輸出引數,需要和源圖片有一樣的尺寸和型別。
  • 第三個引數,int型別的ddepth,輸出影象的深度,支援如下src.depth()和ddepth的組合:
    • 若src.depth() = CV_8U, 取ddepth =-1/CV_16S/CV_32F/CV_64F
    • 若src.depth() = CV_16U/CV_16S, 取ddepth =-1/CV_32F/CV_64F
    • 若src.depth() = CV_32F, 取ddepth =-1/CV_32F/CV_64F
    • 若src.depth() = CV_64F, 取ddepth = -1/CV_64F
  • 第四個引數,int型別dx,x 方向上的差分階數。
  • 第五個引數,int型別dy,y方向上的差分階數。
  • 第六個引數,int型別ksize,有預設值3,表示Sobel核的大小;必須取1,3,5或7。
  • 第七個引數,double型別的scale,計算導數值時可選的縮放因子,預設值是1,表示預設情況下是沒有應用縮放的。我們可以在文件中查閱getDerivKernels的相關介紹,來得到這個引數的更多資訊。
  • 第八個引數,double型別的delta,表示在結果存入目標圖(第二個引數dst)之前可選的delta值,有預設值0。
  • 第九個引數, int型別的borderType,我們的老朋友了(萬年是最後一個引數),邊界模式,預設值為BORDER_DEFAULT。這個引數可以在官方文件中borderInterpolate處得到更詳細的資訊。

一般情況下,都是用ksize x ksize核心來計算導數的。然而,有一種特殊情況——當ksize為1時,往往會使用3 x 1或者1 x 3的核心。且這種情況下,並沒有進行高斯平滑操作。

一些補充說明:

1.當核心大小為 3 時, 我們的Sobel核心可能產生比較明顯的誤差(畢竟,Sobel運算元只是求取了導數的近似值而已)。 為解決這一問題,OpenCV提供了Scharr 函式,但該函式僅作用於大小為3的核心。該函式的運算與Sobel函式一樣快,但結果卻更加精確,其核心是這樣的:

 

2.因為Sobel運算元結合了高斯平滑和分化(differentiation),因此結果會具有更多的抗噪性。大多數情況下,我們使用sobel函式時,取【xorder = 1,yorder = 0,ksize = 3】來計算影象X方向的導數,【xorder = 0,yorder = 1,ksize = 3】來計算影象y方向的導數。

計算影象X方向的導數,取【xorder= 1,yorder = 0,ksize = 3】情況對應的核心:

而計算影象Y方向的導數,取【xorder= 0,yorder = 1,ksize = 3】對應的核心:

 

3.3 呼叫Sobel函式的例項程式碼

呼叫Sobel函式的例項程式碼如下。這裡只是教大家如何使用Sobel函式,就沒有先用一句cvtColor將原圖;轉化為灰度圖,而是直接用彩色圖操作。

//-----------------------------------【標頭檔案包含部分】---------------------------------------//            描述:包含程式所依賴的標頭檔案//----------------------------------------------------------------------------------------------#include <opencv2/opencv.hpp>#include<opencv2/highgui/highgui.hpp>#include<opencv2/imgproc/imgproc.hpp>//-----------------------------------【名稱空間宣告部分】---------------------------------------//            描述:包含程式所使用的名稱空間//-----------------------------------------------------------------------------------------------using namespace cv;//-----------------------------------【main( )函式】--------------------------------------------//            描述:控制檯應用程式的入口函式,我們的程式從這裡開始//-----------------------------------------------------------------------------------------------int main( )//【0】建立 grad_x 和 grad_y 矩陣 Mat grad_x, grad_y; Mat abs_grad_x, abs_grad_y,dst; //【1】載入原始圖   Mat src = imread("1.jpg");  //工程目錄下應該有一張名為1.jpg的素材圖 //【2】顯示原始圖  imshow("【原始圖】sobel邊緣檢測", src);  //【3】求 X方向梯度 Sobel( src, grad_x, CV_16S, 1, 0, 3, 1, 1, BORDER_DEFAULT ); convertScaleAbs( grad_x, abs_grad_x ); imshow("【效果圖】 X方向Sobel", abs_grad_x);  //【4】求這裡寫圖片描述