Canny边缘检测算法及实现

Canny边缘检测算法及实现提取图片的边缘信息是底层数字图像处理的基本任务之一

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

(0)
上一篇 2026年3月18日 下午12:21
下一篇 2026年3月18日 下午12:21


相关推荐

  • java jps_java11教程–jps命令

    java jps_java11教程–jps命令您可以使用该jps命令列出目标系统上已检测的JVM。概要注意:此命令是实验性的,不受支持。jps[-q][-mlvV][hostid]**jps[-help]-q抑制类名,JAR文件名和传递给该main方法的参数的输出,从而仅生成本地JVM标识符的列表。-mlvV-m显示传递给main方法的参数。输出可能是null针对嵌入式JVM的。-l显示应用程序main类的完整软件包名称或应用程序JA…

    2026年1月30日
    6
  • (二) redfish简单信息获取

    (二) redfish简单信息获取redfish 有个开源调试工具 redfishtool 不过它就如同 ipmitool 工具一样为命令行操作方式 似乎并不友好 除这个工具之外 还有两款强大的图形化调试工具 postman 和 apipost 简要介绍一下这两款工具 Postman 是 Google 开发的一款接口测试工具 提供功能强大的 WebAPI amp HTTP 请求调试 它能够发送任何类型的 HTTP 请求 GET HEAD POST PUT 附带任何数量的参数 headers 支持不同的认

    2026年3月17日
    1
  • sourceinsight激活码3.5_pdf注册码及序列号

    sourceinsight激活码3.5_pdf注册码及序列号SI3US-032434-64929

    2026年4月16日
    8
  • 黑盒测试基础[通俗易懂]

    黑盒测试基础[通俗易懂]黑盒测试方法:黑盒测试也称为功能测试和数据驱动测试。它将被测软件视为一个无法打开的黑盒,主要根据功能需求设计测试用例和测试。把产品软件想象成一个只有出口和入口的黑盒。在测试过程中,你只需要知道向黑盒输入什么,知道黑盒会产生什么结果。黑盒测试方法主要有等价类划分、边界值分析、因果图、错误推测等,主要用于软件验证测试。“黑盒”法侧重于程序的外部结构,不考虑内部逻辑结构,针对测试软件界面和软件功能。“黑盒”方法是详尽的输入测试,只有当所有可能的输入都用作测试条件时,才能以这种方式检测程序中的所有错误。

    2022年10月20日
    6
  • 信息系统项目管理师必背核心考点(二十四)WBS分解的原则

    信息系统项目管理师必背核心考点(二十四)WBS分解的原则科科过为您带来软考信息系统项目管理师核心重点考点 二十四 WBS 分解的原则 内含思维导图 真题

    2026年3月16日
    3
  • SpringBoot启动流程–总结

    SpringBoot启动流程–总结说明:我这里只说结果,和简单的代码,面试应该是够了,毕竟源码内容不是所有人都能记住的,如果要学习源码请看其他大佬的文章,写的比较详细,而且差不多都一样。背景:面试经常会问道springboot启动流程或者原理,看了多数博友的文章,都是大同小异,但是面试的时候不可能那么多,所以我将启动流程总结一下。启动流程:1.启动springboot这需要执行SpringApplication执行类即可2.执行的时候执行两个重要的代码,@springBootAppli…

    2025年9月1日
    4

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号