Part11. Canny 算子產(chǎn)生的背景
一階導(dǎo)數(shù)、二階導(dǎo)數(shù)的邊緣算子雖然簡單易用,但存在一些缺點(diǎn)。例如容易受噪聲影響,容易產(chǎn)生虛假邊緣。
John F. Canny 在 1986 年提出了 Canny 邊緣檢測算法。它是結(jié)合了梯度計算方法和非極大值抑制技術(shù)的一種邊緣檢測算法。該算法克服了之前的邊緣檢測算法在抑制噪聲和保持邊緣信息方面的缺陷,具有較好的性能。
Canny 邊緣檢測算法的優(yōu)點(diǎn):
能夠有效地抑制噪聲,同時保持邊緣信息。
能夠檢測到細(xì)小的邊緣。
具有較高的魯棒性,能夠處理各種噪聲類型的圖像
Canny 邊緣檢測算法是最經(jīng)典的邊緣檢測算法之一,它在圖像分割、目標(biāo)檢測、圖像識別等領(lǐng)域有著廣泛的應(yīng)用。
Part22. ?Canny 算子
12.1 Canny 邊緣檢測的步驟
2.1.1 使用高斯濾波器平滑圖像去除噪聲
高斯濾波器是一種常用的平滑濾波器,它可以用來去除圖像中的噪聲。
使用高斯濾波器與原圖進(jìn)行卷積,該步驟將平滑圖像,以便在進(jìn)行梯度計算時可以更好地抑制噪聲。
2.1.2 計算圖像的梯度幅度和方向角
在該系列的第八篇文章中,我們曾介紹過圖像的梯度,它是圖像灰度變化的速率,可以用來表示圖像的邊緣信息。
梯度的大小表示圖像灰度變化的大小,梯度的方向表示圖像灰度變化的方向。梯度幅度表示梯度的大小,用來表示圖像灰度變化的劇烈程度。
Canny 邊緣檢測算法通常使用 Sobel 算子來計算圖像的梯度幅度和方向角。
其中,用 L1 范數(shù)來近似梯度幅度:
梯度的方向角:
//?計算梯度、梯度幅度和方向
Mat?gradXY,?theta;
theta?=?Mat::zeros(src.size(),?CV_8U);
Mat?grad_x,?grad_y;
Sobel(gauss,?grad_x,?CV_32F,?1,?0,?3);
Sobel(gauss,?grad_y,?CV_32F,?0,?1,?3);
Mat?gradX,?gradY;
convertScaleAbs(grad_x,?gradX);
convertScaleAbs(grad_y,?gradY);
gradXY?=?gradX?+?gradY;
2.1.3 使用非極大值抑制消除邊緣檢測的虛假響應(yīng)
非極大值抑制(Non-Maximum Suppression,NMS) 其思想是搜素局部最大值,抑制非極大值。
在 Canny 邊緣檢測算法中,非極大值抑制是指在一個鄰域內(nèi),對于一個像素點(diǎn)如果其梯度幅度小于其鄰域內(nèi)同方向梯度幅度的最大值,則該像素點(diǎn)不是邊緣點(diǎn)。在 8 鄰域內(nèi),非極大值抑制就只是在 0 度、90 度、45 度、135 度四個梯度方向上進(jìn)行的,每個像素點(diǎn)梯度方向按照相近程度用這四個方向來代替。

這樣簡化了計算,因?yàn)楝F(xiàn)實(shí)中圖像中的邊緣梯度方向不一定是沿著這四個方向的,就需要進(jìn)行線性插值,會略顯繁瑣。
//?非最大值抑制
void?nonMaximumSuppression?(Mat?srcGx,?Mat?srcGy,?Mat?&gradXY,?Mat?&theta,?Mat?&dst)?{
????dst?=?gradXY.clone();
????for?(int?j?=?1;?j?<?gradXY.rows-1;?j++)?{
????????for?(int?i?=?1;?i?<?gradXY.cols-1;?i++)?{
????????????double?gradX?=?srcGx.ptr<uchar>(j)[i];
????????????double?gradY?=?srcGy.ptr<uchar>(j)[i];
????????????theta.ptr<uchar>(j)[i]?=?atan(gradY/gradX);?//計算梯度方向
????????????double?t?=?double(theta.ptr<uchar>(j)[i]);
????????????double?g?=?double(dst.ptr<uchar>(j)[i]);
????????????if?(g?==?0.0)?{
????????????????continue;
????????????}
????????????double?g0,?g1;
????????????if?((t?>=?-(3*M_PI/8))?&&?(t?<?-(M_PI/8)))?{
????????????????g0?=?double(dst.ptr<uchar>(j-1)[i-1]);
????????????????g1?=?double(dst.ptr<uchar>(j+1)[i+1]);
????????????}
????????????else?if?((t?>=?-(M_PI/8))?&&?(t?<?M_PI/8))?{
????????????????g0?=?double(dst.ptr<uchar>(j)[i-1]);
????????????????g1?=?double(dst.ptr<uchar>(j)[i+1]);
????????????}
????????????else?if?((t?>=?M_PI/8)?&&?(t?<?3*M_PI/8))?{
????????????????g0?=?double(dst.ptr<uchar>(j-1)[i+1]);
????????????????g1?=?double(dst.ptr<uchar>(j+1)[i-1]);
????????????}
????????????else?{
????????????????g0?=?double(dst.ptr<uchar>(j-1)[i]);
????????????????g1?=?double(dst.ptr<uchar>(j+1)[i]);
????????????}
????????????if?(g?<=?g0?||?g?<=?g1)?{
????????????????dst.ptr<uchar>(j)[i]?=?0.0;
????????????}
????????}
????}
}
2.1.4 使用雙閾值處理確定潛在邊緣
使用兩個閾值來確定邊緣。
強(qiáng)邊緣點(diǎn)是梯度值大于高閾值的像素點(diǎn),將像素值置為255。
弱邊緣點(diǎn)是梯度值大于低閾值但小于高閾值的像素點(diǎn),保留像素值不變。
如果梯度值小于低閾值,則會被抑制,將像素值置為0。
//?雙閾值算法
void?doubleThreshold?(Mat?&src,?double?low,?double?high,?Mat?&dst)?{
????dst?=?src.clone();
????//?區(qū)分出強(qiáng)邊緣點(diǎn)和弱邊緣點(diǎn)
????for?(int?j?=?0;?j?<?src.rows?-?1;?j++)?{
????????for?(int?i?=?0;?i?<?src.cols?-?1;?i++)?{
????????????double?x?=?double(dst.ptr<uchar>(j)[i]);
????????????//?像素點(diǎn)為強(qiáng)邊緣點(diǎn),置255
????????????if?(x?>?high)?{
????????????????dst.ptr<uchar>(j)[i]?=?255;
????????????}
????????????//?像素點(diǎn)置0,被抑制掉
????????????else?if?(x?<?low)?{
????????????????dst.ptr<uchar>(j)[i]?=?0;
????????????}
????????}
????}
}
2.1.5 滯后連接
強(qiáng)邊緣點(diǎn)可以認(rèn)為是真的邊緣,弱邊緣點(diǎn)則可能是真的邊緣也可能是噪聲或顏色變化引起的。
通常會認(rèn)為,真實(shí)邊緣引起的弱邊緣點(diǎn)和強(qiáng)邊緣點(diǎn)是連通的,而又噪聲引起的弱邊緣點(diǎn)則不會。
Canny 算子中的滯后連接,是指將弱邊緣點(diǎn)連接到強(qiáng)邊緣點(diǎn),從而減少邊緣斷裂。也就是在一個弱邊緣點(diǎn)的 8 鄰域像素內(nèi),只要有強(qiáng)邊緣點(diǎn)存在,那么這個弱邊緣點(diǎn)的像素點(diǎn)置為 255 被保留下來;如果它的 8 鄰域內(nèi)不存在強(qiáng)邊緣點(diǎn),則這是一個孤立的弱邊緣點(diǎn),像素點(diǎn)置為 0。
//?通過滯后連接斷開的邊緣
void?hysteresis?(Mat?&src)?{
????//?循環(huán)找到強(qiáng)邊緣點(diǎn),把其領(lǐng)域內(nèi)的弱邊緣點(diǎn)變?yōu)閺?qiáng)邊緣點(diǎn)
????for?(int?j?=?1;?j?<?src.rows?-?2;?j++)?{
????????for?(int?i?=?1;?i?<?src.cols?-?2;?i++)?{
????????????//?如果該點(diǎn)是強(qiáng)邊緣點(diǎn)
????????????if?(src.ptr<uchar>(j)[i]?==?255)?{
????????????????//?遍歷該強(qiáng)邊緣點(diǎn)領(lǐng)域
????????????????for?(int?m?=?-1;?m?<?1;?m++)?{
????????????????????for?(int?n?=?-1;?n?<?1;?n++)?{
????????????????????????//?該點(diǎn)為弱邊緣點(diǎn)(不是強(qiáng)邊緣點(diǎn),也不是被抑制的0點(diǎn))
????????????????????????if?(src.ptr<uchar>(j?+?m)[i?+?n]?!=?0?&&?src.ptr<uchar>(j?+?m)[i?+?n]?!=?255)?{
????????????????????????????src.ptr<uchar>(j?+?m)[i?+?n]?=?255;?//該弱邊緣點(diǎn)補(bǔ)充為強(qiáng)邊緣點(diǎn)
????????????????????????}
????????????????????}
????????????????}
????????????}
????????}
????}
????for?(int?j?=?0;?j?<?src.rows?-?1;?j++)?{
????????for?(int?i?=?0;?i?<?src.cols?-?1;?i++)?{
????????????//?如果該點(diǎn)依舊是弱邊緣點(diǎn),及此點(diǎn)是孤立邊緣點(diǎn)
????????????if?(src.ptr<uchar>(j)[i]?!=?255?&&?src.ptr<uchar>(j)[i]?!=?255)?{
????????????????src.ptr<uchar>(j)[i]?=?0;?//該孤立弱邊緣點(diǎn)抑制
????????????}
????????}
????}
}
22.2 Canny 算子的實(shí)現(xiàn)
下面,將上述五個步驟結(jié)合起來,一步步實(shí)現(xiàn) Canny 邊緣檢測算法。
#include?<opencv2/opencv.hpp>
#include?<math.h>
using?namespace?std;
using?namespace?cv;
//?非最大值抑制
void?nonMaximumSuppression?(Mat?srcGx,?Mat?srcGy,?Mat?&gradXY,?Mat?&theta,?Mat?&dst)?{
????dst?=?gradXY.clone();
????for?(int?j?=?1;?j?<?gradXY.rows-1;?j++)?{
????????for?(int?i?=?1;?i?<?gradXY.cols-1;?i++)?{
????????????double?gradX?=?srcGx.ptr<uchar>(j)[i];
????????????double?gradY?=?srcGy.ptr<uchar>(j)[i];
????????????theta.ptr<uchar>(j)[i]?=?atan(gradY/gradX);?//計算梯度方向
????????????double?t?=?double(theta.ptr<uchar>(j)[i]);
????????????double?g?=?double(dst.ptr<uchar>(j)[i]);
????????????if?(g?==?0.0)?{
????????????????continue;
????????????}
????????????double?g0,?g1;
????????????if?((t?>=?-(3*M_PI/8))?&&?(t?<?-(M_PI/8)))?{
????????????????g0?=?double(dst.ptr<uchar>(j-1)[i-1]);
????????????????g1?=?double(dst.ptr<uchar>(j+1)[i+1]);
????????????}
????????????else?if?((t?>=?-(M_PI/8))?&&?(t?<?M_PI/8))?{
????????????????g0?=?double(dst.ptr<uchar>(j)[i-1]);
????????????????g1?=?double(dst.ptr<uchar>(j)[i+1]);
????????????}
????????????else?if?((t?>=?M_PI/8)?&&?(t?<?3*M_PI/8))?{
????????????????g0?=?double(dst.ptr<uchar>(j-1)[i+1]);
????????????????g1?=?double(dst.ptr<uchar>(j+1)[i-1]);
????????????}
????????????else?{
????????????????g0?=?double(dst.ptr<uchar>(j-1)[i]);
????????????????g1?=?double(dst.ptr<uchar>(j+1)[i]);
????????????}
????????????if?(g?<=?g0?||?g?<=?g1)?{
????????????????dst.ptr<uchar>(j)[i]?=?0.0;
????????????}
????????}
????}
}
//?用雙閾值算法檢測
void?doubleThreshold?(Mat?&src,?double?low,?double?high,?Mat?&dst)?{
????dst?=?src.clone();
????//?區(qū)分出強(qiáng)邊緣點(diǎn)和弱邊緣點(diǎn)
????for?(int?j?=?0;?j?<?src.rows?-?1;?j++)?{
????????for?(int?i?=?0;?i?<?src.cols?-?1;?i++)?{
????????????double?x?=?double(dst.ptr<uchar>(j)[i]);
????????????//?像素點(diǎn)為強(qiáng)邊緣點(diǎn),置255
????????????if?(x?>?high)?{
????????????????dst.ptr<uchar>(j)[i]?=?255;
????????????}
????????????//?像素點(diǎn)置0,被抑制掉
????????????else?if?(x?<?low)?{
????????????????dst.ptr<uchar>(j)[i]?=?0;
????????????}
????????}
????}
}
//?通過滯后連接斷開的邊緣
void?hysteresis?(Mat?&src)?{
????//?循環(huán)找到強(qiáng)邊緣點(diǎn),把其領(lǐng)域內(nèi)的弱邊緣點(diǎn)變?yōu)閺?qiáng)邊緣點(diǎn)
????for?(int?j?=?1;?j?<?src.rows?-?2;?j++)?{
????????for?(int?i?=?1;?i?<?src.cols?-?2;?i++)?{
????????????//?如果該點(diǎn)是強(qiáng)邊緣點(diǎn)
????????????if?(src.ptr<uchar>(j)[i]?==?255)?{
????????????????//?遍歷該強(qiáng)邊緣點(diǎn)領(lǐng)域
????????????????for?(int?m?=?-1;?m?<?1;?m++)?{
????????????????????for?(int?n?=?-1;?n?<?1;?n++)?{
????????????????????????//?該點(diǎn)為弱邊緣點(diǎn)(不是強(qiáng)邊緣點(diǎn),也不是被抑制的0點(diǎn))
????????????????????????if?(src.ptr<uchar>(j?+?m)[i?+?n]?!=?0?&&?src.ptr<uchar>(j?+?m)[i?+?n]?!=?255)?{
????????????????????????????src.ptr<uchar>(j?+?m)[i?+?n]?=?255;?//該弱邊緣點(diǎn)補(bǔ)充為強(qiáng)邊緣點(diǎn)
????????????????????????}
????????????????????}
????????????????}
????????????}
????????}
????}
????for?(int?j?=?0;?j?<?src.rows?-?1;?j++)?{
????????for?(int?i?=?0;?i?<?src.cols?-?1;?i++)?{
????????????//?如果該點(diǎn)依舊是弱邊緣點(diǎn),及此點(diǎn)是孤立邊緣點(diǎn)
????????????if?(src.ptr<uchar>(j)[i]?!=?255?&&?src.ptr<uchar>(j)[i]?!=?255)?{
????????????????src.ptr<uchar>(j)[i]?=?0;?//該孤立弱邊緣點(diǎn)抑制
????????????}
????????}
????}
}
int?main?()?{
????Mat?src?=?imread(".../street.jpg");
????imshow("src",src);
????Mat?gray;
????cvtColor(src,?gray,?cv::COLOR_BGR2GRAY);?//?灰度化
????Mat?gauss;
????GaussianBlur(gray,?gauss,?Size(5,?5),0);
????//?計算梯度、梯度幅度和方向
????Mat?gradXY,?theta;
????theta?=?Mat::zeros(src.size(),?CV_8U);
????Mat?grad_x,?grad_y;
????Sobel(gauss,?grad_x,?CV_32F,?1,?0,?3);
????Sobel(gauss,?grad_y,?CV_32F,?0,?1,?3);
????Mat?gradX,?gradY;
????convertScaleAbs(grad_x,?gradX);
????convertScaleAbs(grad_y,?gradY);
????gradXY?=?gradX?+?gradY;
????//?局部非極大值抑制
????Mat?nms;
????nonMaximumSuppression(grad_x,grad_y,gradXY,?theta,?nms);
????imshow("nms",nms);
????//?用雙閾值算法檢測
????Mat?dst;
????doubleThreshold(nms,?50,?100,??dst);
????imshow("thresh",dst);
????//?滯后連接
????hysteresis(dst);
????imshow("edge",dst);
????//?OpenCV?的?Canny?函數(shù)
????Mat?result;
????Canny(gauss,?result,?50,?100);
????imshow("canny",?result);
????waitKey(0);
????return?0;
}


在上述代碼中 Canny() 函數(shù)是 OpenCV 自帶的函數(shù),放在這里主要是做一個對比。
Canny(InputArray?image,?OutputArray?edges,?double?threshold1,?double?threshold2,?int?apertureSize?=?3,?bool?L2gradient?=?false);
其各個參數(shù)的含義:
第一個參數(shù) image:輸入的源圖像。
第二個參數(shù) edges:輸出的邊緣圖像。
第三個參數(shù) threshold1:低閾值。
第四個參數(shù) threshold2:高閾值。
第五個參數(shù) apertureSize:Sobel 算子的核大小。
第六個參數(shù) L2gradient:是否使用 L2 范數(shù)來計算梯度。默認(rèn)值為 false,使用 L1 范數(shù)來計算梯度。
Part33. ?邊緣檢測的三大準(zhǔn)則
John F. 在提出 Canny 算子的同時,提出了邊緣檢測的三大準(zhǔn)則:
低錯誤率的邊緣檢測:檢測算法應(yīng)該精確地找到圖像中的盡可能多的邊緣,盡可能的減少漏檢和誤檢。
最優(yōu)定位:檢測的邊緣點(diǎn)應(yīng)該精確地定位于邊緣的中心。
圖像中的任意邊緣應(yīng)該只被標(biāo)記一次,同時圖像噪聲不應(yīng)產(chǎn)生偽邊緣。
Canny 邊緣檢測算法正是遵循了這些準(zhǔn)則。
Part44. 各個算子的關(guān)系
將之前介紹的幾種算子,整理一下他們的關(guān)系:
Roberts 算子和 Prewitt 算子是基礎(chǔ)算子,Sobel 算子是它們的擴(kuò)展。
Laplace 算子是二階導(dǎo)數(shù)算子,可以計算圖像的梯度二階導(dǎo)數(shù)。
LoG 算子和 DoG 算子是帶高斯核的微分算子,可以抑制噪聲。
Canny 邊緣檢測算法是綜合使用了高斯濾波、 Sobel 算子、非極大值抑制和雙閾值檢測的邊緣檢測算法。
Part55. ?總結(jié)
本文介紹了 Canny 算子的特點(diǎn)以及如何一步步實(shí)現(xiàn)一個 Canny 的函數(shù)。并且,在最后總結(jié)了之前介紹的幾種算子的關(guān)系。
【Java與Android技術(shù)?!抗娞?/strong>
關(guān)注?Java/Kotlin 服務(wù)端、桌面端 、Android 、機(jī)器學(xué)習(xí)、端側(cè)智能文章來源:http://www.zghlxwxcb.cn/news/detail-770245.html
更多精彩內(nèi)容請關(guān)注:文章來源地址http://www.zghlxwxcb.cn/news/detail-770245.html
到了這里,關(guān)于OpenCV 筆記(12):常用的邊緣檢測算子—— Canny的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!