邊緣檢測
- 一、邊緣檢測
- 二、邊緣檢測算子
- 2.1、Sobel算子
- 2.2、Scharr算子
- 2.3、Laplacian算子
- 三、Canny邊緣檢測
- 3.1、Canny邊緣檢測的步驟
- 3.2、Canny算法的實現
一、邊緣檢測
邊緣是指圖像中像素的灰度值發生劇烈變化的區域:
圖像中的邊緣主要有以下幾種成因:
- 表面不連續:兩個面的交界處會自然形成邊緣
- 深度不連續:主要是視覺因素
- 顏色不連續:兩種不同顏色的交匯處會形成邊緣
- 照明不連續:受光線影響形成的陰影會產生邊緣
邊緣檢測方法主要有以下兩大類:
- 通過灰度值曲線一階導數的最大值來尋找邊緣,如Sobel算子、Scharr算子、Prewitt算子、roberts算子等
- 通過灰度值曲線二階導數過零點來尋找邊緣,如Laplacian算子、Canny邊緣檢測等
二、邊緣檢測算子
2.1、Sobel算子
Sobel算子是通過一階導數的最大值進行邊緣檢測的,用Sobel算子進行邊緣檢測的步驟如下:
1. 將圖像與x方向的Sobel算子進行卷積。x方向的Sobel算子(尺寸3*3)如下:
-1 -2 1
-2 0 2
-1 0 1
2. 將圖像與y方向的Sobel算子進行卷積。y方向的Sobel算子(尺寸3*3)如下:
-1 -2 -10 0 01 2 1
3. 對圖像中的像素計算近似梯度幅度:
4. 統計極大值所在位置,獲得圖像的邊緣:
Sobel算子有著不同的尺寸和階次。如果想自己生成Sobel算子,則可以用getDerivKernels()函數實現:
//生成邊緣檢測用的濾波器。ksize=CV_SCHARR時生成的是Scharr濾波器,其余情況下生成的是Sobel濾波器
void Imgproc.getDerivKernels(Mat kx, Mat ky, int dx, int dy, int ksize, boolean normalize, int ktype)
- kx:行濾波器的輸出矩陣,類型為ktype
- ky:列濾波器的輸出矩陣,類型為ktype
- dx:x方向上導數的階次
- dy:y方向上導數的階次
- ksize:生成濾波器的尺寸,可選參數有CV_SCHARR或者1、3、5、7
- normalize:是否對濾波器系數進行歸一化。如果要濾波器的圖像的數據類型是浮點型,則一般需要進行歸一化;如果處理的是8位圖像,結果存儲在16位圖像中并希望保留所有的小數部分,則需要將normalize設為false
- ktype:濾波器系數的類型,可以是CV_32F或CV_64F
該函數智能生成Sobel或Scharr算子。事實上,Sobel()函數和Scharr()函數內部調用的就是這個函數。
//用Sobel算子進行邊緣檢測
void Imgproc.Sobel(Mat src, Mat dst, int ddepth, int dx, int dy, int ksize)
- src:輸入圖像
- dst:輸出圖像,和src具有相同的尺寸和通道數
- ddepth:輸出圖像的深度
- dx:x方向求導的階數,通常只能是0或1。如果dx為0,則表示x方向上沒有求導
- dy:y方向求導的階數,通常只能是0或1。如果dy為0,則表示y方向上沒有求導
- ksize:Sobel算子的尺寸,只能是1、3、5或7
由于圖像的邊緣可能從高灰度值變為低灰度值,也可能從低灰度值變為高灰度值,所以用Sobel()函數計算的結果可能為正也可能為負。為了正確地顯示圖像,還需要用convertScaleAbs()函數將計算結果轉為絕對值:
//計算矩陣中數值的絕對值,并轉換為8位數據類型,可在此過程中進行縮放。對矩陣中每個數據和函數依次執行三項操作:縮放、求絕對值、轉換為CV_8U類型。如為多通道矩陣,函數則需對每個通道獨立進行處理
void Core.convertScaleAbs(Mat src, Mat dst, double alpha)
- src:輸入矩陣
- dst:輸出矩陣
- alpha:縮放因子,可選
public class Sobel {static {OpenCV.loadLocally(); // 自動下載并加載本地庫}public static void main(String[] args) {//讀取圖像灰度圖并顯示Mat src = Imgcodecs.imread("F:/IDEAworkspace/opencv/src/main/java/demo/butterfly.png", Imgcodecs.IMREAD_GRAYSCALE);HighGui.imshow("src", src);HighGui.waitKey(0);Mat grad = new Mat();Mat gx = new Mat();Mat gy = new Mat();Mat abs_gx = new Mat();Mat abs_gy = new Mat();//提取x方向邊緣Imgproc.Sobel(src, gx, -1, 1, 0);Core.convertScaleAbs(gx, abs_gx);//提取y方向邊緣Imgproc.Sobel(src, gy, -1, 0, 1);Core.convertScaleAbs(gy, abs_gy);//顯示x和y方向邊緣HighGui.imshow("Sobel-X", gx);HighGui.waitKey(0);HighGui.imshow("Sobel-Y", gy);HighGui.waitKey(0);//計算整副圖像的邊緣并顯示Core.addWeighted(abs_gx, 0.5, abs_gy, 0.5, 0, grad);HighGui.imshow("Sobel", grad);HighGui.waitKey(0);//直接計算整幅圖像邊緣Mat all = new Mat();Imgproc.Sobel(src, all, -1, 1, 1);HighGui.imshow("all", all);HighGui.waitKey(0);System.exit(0);}
}
原圖灰度圖:
X方向邊緣:
Y方向邊緣:
整圖邊緣:
一次計算整幅圖邊緣:
2.2、Scharr算子
用Sobel算計進行邊緣檢測的效率較高,但它有一個缺點:當Sobel算子尺寸較小時精度比較低。如果Sobel濾波器的尺寸為33且梯度方向接近水平或垂直方向,則問題會變得愈發明顯。為了解決這個問題,OpenCV引進了Scharr算子。Scharr算子其實是一個特殊尺寸33的濾波器,在getDerivKernels()函數中將ksize設為CV_SCHARR時就是Scharr算子。當濾波器尺寸為3*3時,使用Scharr算子的速度與Sobel算子的速度一樣,但是準確度更高。
//x方向的Scharr算子
-3 0 3
-10 0 10
-1 0 3//y方向Scharr算子
-3 -10 -30 0 03 10 3
Scharr算子的濾波器尺寸只能是3*3,因為它的產生就是為了解決Sobel算子在該尺寸的問題
//用Scharr算子進行邊緣檢測
void Imgproc.Scharr(Mat src, Mat dst, int ddepth, int dx, int dy)
- src:輸入圖像
- dst:輸出圖像,和src具有相同的尺寸和通道數
- ddepth:輸出圖像的深度
- dx:x方向求導的階數,通常只能是0或1。如果dx為0,則表示x方向上沒有求導
- dy:y方向求導的階數,通常只能是0或1。如果dy為0,則表示y方向上沒有求導
public class Scharr {static {OpenCV.loadLocally(); // 自動下載并加載本地庫}public static void main(String[] args) {//讀取圖像灰度圖并顯示Mat src = Imgcodecs.imread("/Users/acton_zhang/J2EE/MavenWorkSpace/opencv_demo/src/main/java/demo1/butterfly.png", Imgcodecs.IMREAD_GRAYSCALE);HighGui.imshow("src", src);HighGui.waitKey(0);Mat grad = new Mat();Mat gx = new Mat();Mat gy = new Mat();Mat abs_gx = new Mat();Mat abs_gy = new Mat();//提取x方向邊緣Imgproc.Scharr(src, gx, -1, 1, 0);Core.convertScaleAbs(gx, abs_gx);//提取y方向邊緣Imgproc.Scharr(src, gy, -1, 0, 1);Core.convertScaleAbs(gy, abs_gy);//在屏幕上顯示X與Y方向邊緣HighGui.imshow("Scharr-X", gx);HighGui.waitKey(0);HighGui.imshow("Scharr-Y", gy);HighGui.waitKey(0);//計算整幅圖的邊緣并顯示Core.addWeighted(abs_gx, 0.5, abs_gy, 0.5, 0, grad);HighGui.imshow("Scharr", grad);HighGui.waitKey(0);System.exit(0);}
}
原圖:
X方向邊緣:
Y方向邊緣;
整圖邊緣:
2.3、Laplacian算子
Sobel算子和Scharr算子進行邊緣檢測的效率較高,但是它們具有方向性,需要先分別在x方向和y方向求導,然后根據兩個結果經計算后才可以得到圖像的邊緣。Laplacian算子則沒有方向性,不需要分方向計算。Laplacian算子和Sobel算子、Scharr算子的另一個區別是:Laplacian算子是一個基于二階導數的邊緣檢測算子。ksize=1時的Laplacian算子如下:
0 1 0
1 -4 1
0 1 1
//用Laplacian算子進行邊緣檢測
void Imgproc.Laplacian(Mat src, Mat dst, int ddepth, int ksize)
- src:輸入圖像
- dst:輸出圖像,和src具有相同的尺寸和通道數
- ddepth:輸出圖像的深度
- ksize:濾波器尺寸,必須為正奇數
public class Laplacian {static {OpenCV.loadLocally(); // 自動下載并加載本地庫}public static void main(String[] args) {//讀取圖像灰度圖并顯示Mat src = Imgcodecs.imread("/Users/acton_zhang/J2EE/MavenWorkSpace/opencv_demo/src/main/java/demo1/butterfly.png", Imgcodecs.IMREAD_GRAYSCALE);HighGui.imshow("src", src);HighGui.waitKey(0);//高斯濾波后用Laplacian算子提取邊緣Mat dst = new Mat();Imgproc.GaussianBlur(src, dst, new Size(3, 3), 5);Imgproc.Laplacian(src, dst, 0, 3);Core.convertScaleAbs(dst, dst);//顯示HighGui.imshow("Laplacian", dst);HighGui.waitKey(0);System.exit(0);}
}
原圖:
Laplacian算子邊緣:
三、Canny邊緣檢測
Canny邊緣檢測算法源自John F.Canny于1986年發表的論文,論文中提出了以下3個評價最優邊緣檢測的標準:
- 準確檢測:算法能盡可能多地標識出圖像的實際邊緣,而遺漏或錯標的邊緣點應盡可能少
- 精確定位:檢測出的邊緣點的位置應與實際邊緣中心盡可能接近
- 單次響應:每個邊緣位置只能標識一次
3.1、Canny邊緣檢測的步驟
1. 平滑降噪:
在Canny邊緣檢測中,一般使用高斯平滑濾波器進行平滑降噪。高斯濾波器考慮了像素離濾波器中心的距離因素,距離越近權重越大,距離越遠權重越小。以下是一個5*5的高斯濾波器:
2 4 5 4 2
4 9 12 9 4
5 12 15 12 5 * 1/139
4 9 12 9 4
2 4 5 4 2
2. 梯度計算:
計算圖像中每像素的梯度幅值和方向,主要分以下兩步:
- 用Sobel算子分別檢測x方向和y方向的邊緣
- 計算梯度的幅值和方向。為了簡化起見,梯度方向取0°、45°、90°和135°這四個值
3. 非極大值抑制:
上一步得到的梯度圖像存在邊緣較粗及噪聲干擾等問題,此時可以用非極大值抑制來影除非邊緣的像素。Canny 中的非極大值抑制是沿著梯度方向對幅值進行比較,如圖所示。圖中A點位于邊緣附近,箭頭方向為梯度方向。選擇梯度方向上A點附近的像素B和C來檢驗A點的梯度值是否為極大值,若為極大值,則A保留為(候選)邊緣點,否則A點被抑制。由此可見,所謂非極大值抑制就是將不是極大值的 候選點予以剔除的過程。
4. 雙閾值處理:
經過以上三步之后得到的邊緣質量已經很高了,但還是存在一些偽邊緣,因此Canny算法用雙閾值法對邊緣進行篩選。雙閿值法設置 minVal 和 maxVal 兩個閾值,當候選的邊緣點的梯度幅值高于 maxVal 時被認為是真正的邊界,當低于 minVal 時則被拋棄:如果介于兩者之間,則要看這個點是否與某個被確定為真正的邊界的像素相連,如果是,則認定為邊界點,否則該點被拋棄。
如圖所示,由于 A 點高于 maxVal,所以是真正的邊界點;由C點雖然低于 maxVal 但高于minVal 并且與 A 點相連,所以也是真正的邊界點,而 B 點介于 minVal 和 maxVal 之間,但沒有與真正的邊界點相連,因而被拋棄。為了達到較好的效果,選擇合適的 maxVal 和 minVal 值非常重要。
3.2、Canny算法的實現
//用Canny算法進行邊緣檢測
void Imgproc.Canny(Mat image, Mat edges, double threshold1, double threshold2, int apertureSize)
- image:8位輸入圖像
- edges:輸出的邊緣圖像,必須是8位單通道圖像,尺寸與輸入圖像相同
- threshold1:閾值1
- threshold2:閾值2。threshold1和threshold2誰大誰小沒有規定,系統會自動選擇較大值為maxVal,較小值為minVal
- apertureSize:Sobel算子的尺寸
Canny邊緣檢測的過程雖然較為復雜,但是經過OpenCV封裝后的Canny()函數卻非常簡單:
public class Canny {static {OpenCV.loadLocally(); // 自動下載并加載本地庫}public static void main(String[] args) {//讀取圖像灰度圖并顯示Mat src = Imgcodecs.imread("/Users/acton_zhang/J2EE/MavenWorkSpace/opencv_demo/src/main/java/demo1/butterfly.png", Imgcodecs.IMREAD_GRAYSCALE);HighGui.imshow("src", src);HighGui.waitKey(0);//進行Canny邊緣檢測并顯示Mat dst = new Mat();Imgproc.GaussianBlur(src, src, new Size(3, 3), 5);Imgproc.Canny(src, dst, 60, 200);HighGui.imshow("Canny", dst);HighGui.waitKey(0);System.exit(0);}
}
原圖:
Canny算法檢測:
可以看出Canny邊緣檢測的效果非常好。無論是Sobel算子、Scharr算子還是Laplacian算子檢測的邊緣都比較模糊,而Canny算法得出的邊緣非常請清晰。當然,為了得到較好的邊緣,Canny算法耗費的時間也比較長。