Canny边缘检测算法及实现
微信公众号:幼儿园的学霸
目录
前言
提取图片的边缘信息是底层数字图像处理的基本任务之一.边缘信息对进一步提取高层语义信息有很大的影响.
由于导数对噪声比较敏感,因此提取边缘之前最好先对图像进行平滑处理,以去除噪声(噪声是高频信号,通过低通滤波去除).
原理
Canny 边缘检测算法是John F.Canny于1986年开发出来的一个多级边缘检测算法,也被很多人认为是边缘检测的最优算法,最优边缘检测的三个主要评价标准是:
- 低错误率:标识出尽可能多的实际边缘,同时尽可能的减少噪声产生的误报.
- 高定位性:标识出的边缘要与图像中的实际边缘尽可能接近.
- 最小响应:图像中的边缘只能标识一次.
步骤
某点的梯度 G G G和方向 θ \theta θ如下示意图所示:

注意上图中梯度方向为-45°,这是由于y坐标轴方向向下的原因.
3.非极大值抑制. 非极大值抑制是进行边缘检测的一个重要步骤,通俗意义上是指寻找像素点局部最大值.在每一点上,领域中心x与沿着其对应的梯度方向的两个像素相比,若中心像素为最大值,则保留,否则中心置0,这样可以抑制非极大值,保留局部梯度最大的点,以细化边缘.
沿着梯度方向进行比较,而非在邻域内进行比较.

如上图所示,需要将中心像素与其梯度方向的两个像素进行比较.但是,由于图像中的像素点是离散的二维矩阵,其梯度方向两侧并没有真实存在的像素,或者说是一个亚像素(sub pixel)点,对于这个不存在的点的位置与梯度就必须通过对其两侧的点进行插值来得到.针对利用插值计算梯度进行非极大值抑制的处理手段相关资料比较多,不赘述.

需要注意的是,如何标志方向并不重要,重要的是梯度方向的计算要和梯度算子计算的方向保持一致.
- a.如果某一像素位置的幅值超过高阈值,该像素被保留为边缘像素.
- b.如果某一像素位置的幅值小于低阈值,该像素被排除.
- c.如果某一像素位置的幅值在两个阈值之间,则根据连通性来分类为边缘或者非边缘:该像素与确定为边缘的像素点邻接,则判定为边缘;否则为非边缘.
上述判断中,只要该像素与确定为边缘的像素点邻接,则判定该点为边缘点,而非 该像素与大于高阈值的像素点邻接来判定该点是否为边缘点. 注意这两者的区别.
Canny 推荐的 高:低 阈值比在 2:1 到3:1之间.
一点设想:双阈值的选择可以和之前的一篇Multi Leve OTSU实现的博文进行结合,利用最大类间方差法的思想,求解出2个阈值,以实现高低阈值的自动选择.该部分应该值得探究.
实现
实现代码如下.
// // Created by liheng on 12/21/21. // #include
//#include
/*! * 边缘连接:从确定边缘点出发,延长边缘 * @param x * @param y 当前像素坐标 * @param magnitude 梯度幅值.CV_32FC1 * @param tUpper * @param tLower 双阈值 * @param edges 边缘图 CV_8UC1 */ void followEdges(int x, int y, const cv::Mat &magnitude, int tUpper, int tLower, cv::Mat &edges); /*! * 边缘检测.通过滞后阈值,进行伪边缘去除和边缘连接 * @param magnitude 梯度幅值 CV_32FC1 * @param tUpper * @param tLower 双阈值 * @param edges 边缘图 CV_8UC1 */ void edgeDetect(const cv::Mat& magnitude, int tUpper, int tLower, cv::Mat& edges); /*! *非极大值抑制 * @param magnitudeImage CV_32FC1 各点的梯度幅值 * @param directionImage CV_32FC1 存储各点的梯度方向0-360° */ void nonMaximumSuppression(cv::Mat &magnitudeImage,const cv::Mat &directionImage); /*! * 自定义Canny算法实现 * @param src * @param edges * @param upperThresh * @param lowerThresh */ void myCanny(const cv::Mat& src, cv::Mat& edges, int upperThresh, int lowerThresh) { //Step1. 高斯滤波 Remove noise (apply gaussian) cv::Mat image; cv::GaussianBlur(src, image, cv::Size(3, 3), 1.5); //Step2. 使用sobel计算相应的梯度幅值及方向. Calculate gradient (apply sobel operator) cv::Mat magX,magY;//X,Y方向的梯度 cv::Sobel(image, magX, CV_32FC1, 1, 0, 3); cv::Sobel(image, magY, CV_32FC1, 0, 1, 3); cv::Mat Mag,Ori;//梯度幅值,幅角 cv::cartToPolar(magX,magY,Mag,Ori,true);//幅角0~360 //Step3.Non-maximum supression 非极大值抑制 // For each pixel find two neighbors (in the positive and negative gradient directions, // supposing that each neighbor occupies the angle of π/4 , and 0i s the direction straight to the right). // If the magnitude of the current pixel is greater than the magnitudes of the neighbors, nothing changes, // otherwise, the magnitude of the current pixel is set to zero. nonMaximumSuppression(Mag, Ori); //Step4. 双阈值检测和边缘连接 Double thresholding edgeDetect(Mag, upperThresh, lowerThresh, edges); } void followEdges(int x, int y, const cv::Mat &magnitude, int tUpper, int tLower, cv::Mat &edges) { edges.at
(y, x) = 255;//该点与强边缘点邻接,故确定其为边缘点 for (int i = -1; i < 2; i++)//8邻域: (i,j) ∈ [-1 0 1].一共8个点,因此要去掉自身 { for (int j = -1; j < 2; j++) { if(i==0 && j==0 )//去除自身点 continue; // 边界限制 if ( (x + i >= 0) && (y + j >= 0) && (x + i < magnitude.cols) && (y + j < magnitude.rows)) { // 梯度幅值边缘判断及连接 if ((magnitude.at
(y + j, x + i) > tLower) && (edges.at
(y + j, x + i) != 255))//大于低阈值,且该点尚未被确定为边缘点 { followEdges(x + i, y + j, magnitude, tUpper, tLower, edges); } } } } } void edgeDetect(const cv::Mat& magnitude, int tUpper, int tLower, cv::Mat& edges) { int rows = magnitude.rows; int cols = magnitude.cols; edges = cv::Mat(magnitude.size(), CV_8UC1, cv::Scalar(0)); for (int y = 0; y < rows; y++) { for (int x = 0; x < cols; x++) { // 梯度幅值判断.//大于高阈值,为确定边缘点 if (magnitude.at
(y, x) >= tUpper) { followEdges(x, y, magnitude, tUpper, tLower, edges); } } } } void nonMaximumSuppression(cv::Mat &magnitudeImage,const cv::Mat &directionImage) { cv::Mat edgeMag_noMaxsup = cv::Mat::zeros(magnitudeImage.size(), CV_32FC1); //根据输入的角度,判断该点梯度方向位于位于那个区间 //[0,45,90,135] auto _judgeDir = [](float angle)->int { if( (0<=angle&&angle<22.5) || (157.5<=angle&&angle<202.5) ||(337.5<=angle&&angle<360) )//梯度方向为水平方向 return 0; else if( (22.5<=angle&&angle<67.5) || (202.5<=angle&&angle<247.5) )//45°方向 return 45; else if( (67.5<=angle&&angle<112.5) || ((247.5<=angle&&angle<292.5)) ) return 90; else /*if( (112.5<=angle&&angle<157.5) || ((292.5<=angle&&angle<337.5)) )*/ return 135; }; for (int r = 1; r < magnitudeImage.rows - 1; ++r) { for (int c = 1; c < magnitudeImage.cols - 1; ++c) { const float mag = magnitudeImage.at
(r, c);//当前位置梯度幅值 //将其量化到4个方向中进行计算 const float angle = directionImage.at
(r,c); const int nDir = _judgeDir(angle); //非极大值抑制,8邻域的点进行比较,但只比较梯度方向 //或者采用线性插值的方式,在亚像素层面进行比较 //由于图像的y轴向下,x轴向右,因此注意这里的45°和135° switch(nDir) { case 0://梯度方向为水平方向-邻域内左右比较 { float left = magnitudeImage.at
(r, c - 1); float right = magnitudeImage.at
(r, c + 1); if (mag > left && mag >= right) edgeMag_noMaxsup.at
(r, c) = mag; break; } case 135://即我们平常认为的45°.邻域内右上 左下比较. { float right_top = magnitudeImage.at
(r - 1, c+1); float left_down = magnitudeImage.at
(r + 1, c-1); if (mag > right_top && mag >= left_down) edgeMag_noMaxsup.at
(r, c) = mag; break; } case 90://梯度方向为垂直方向-邻域内上下比较 { float top = magnitudeImage.at
(r-1, c); float down = magnitudeImage.at
(r+1, c); if (mag > top && mag >= down) edgeMag_noMaxsup.at
(r, c) = mag; break; } case 45://邻域内右下 左上比较 { float left_top = magnitudeImage.at
(r - 1, c - 1); float right_down = magnitudeImage.at
(r + 1, c + 1); if (mag > left_top && mag >= right_down) edgeMag_noMaxsup.at
(r, c) = mag; break; } default: break; }//switch }//for col }//for row edgeMag_noMaxsup.copyTo(magnitudeImage); } int main() { cv::Mat srcImage = cv::imread("1.bmp", cv::IMREAD_GRAYSCALE); int highValue = 100; int lowValue = 50; cv::Mat cannyEdges; cv::Mat cvcannyEdges; myCanny(srcImage, cannyEdges, highValue, lowValue); cv::GaussianBlur(srcImage, srcImage, cv::Size(3, 3), 1.5); cv::Canny(srcImage,cvcannyEdges,highValue,lowValue,3,true); cv::Mat merged; cv::Mat t = cv::Mat::zeros(cannyEdges.size(),CV_8UC1); std::vector
channels; channels.push_back(t); channels.push_back(cannyEdges); channels.push_back(cvcannyEdges); cv::merge(channels,t); cv::imshow("original", srcImage); cv::imshow("edges", cannyEdges); cv::imshow("cvedges", cvcannyEdges); cv::imshow("mergeed canny",t); cv::waitKey(0); return 0; }
参考资料
发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/216185.html原文链接:https://javaforall.net
