Canny算子边缘检测原理及实现

article/2025/9/23 18:10:42

写在前面

Canny边缘检是在在1986年提出来的,到今天已经30多年过去了,但Canny算法仍然是图像边缘检测算法中最经典、先进的算法之一。

相比Sobel、Prewitt等算子,Canny算法更为优异。Sobel、Prewitt等算子有如下缺点:

  • 没有充分利用边缘的梯度方向。
  • 最后得到的二值图,只是简单地利用单阈值进行处理。

而Canny算法基于这两点做了改进,提出了:

  • 基于边缘梯度方向的非极大值抑制。
  • 双阈值的滞后阈值处理。

原理

从表面效果上来讲,Canny算法是对Sobel、Prewitt等算子效果的进一步细化和更加准确的定位。

Canny算法基于三个基本目标:

  • 低错误率。所有边缘都应被找到,且没有伪响应。
  • 边缘点应该被很好地定位。已定位的边缘必须尽可能接近真实边缘。
  • 单一的边缘点响应。这意味在仅存一个单一边缘点的位置,检测器不应指出多个像素边缘。

进而,Canny的工作本质是,从数学上表达前面的三个准则。因此Canny的步骤如下:

  1. 对输入图像进行高斯平滑,降低错误率。
  2. 计算梯度幅度和方向来估计每一点处的边缘强度与方向。
  3. 根据梯度方向,对梯度幅值进行非极大值抑制。本质上是对Sobel、Prewitt等算子结果的进一步细化。
  4. 用双阈值处理和连接边缘。

详细步骤

1、高斯平滑(略)

2、计算梯度幅度和方向

可选用的模板:soble算子、Prewitt算子、Roberts模板等等;

一般采用soble算子,OpenCV也是如此,利用soble水平和垂直算子与输入图像卷积计算dx、dy:

                                                                 Sobel_X =\begin{bmatrix}1 \\ 0 \\ -1 \end{bmatrix}*\begin{bmatrix} 1 & 2 &1 \end{bmatrix}=\begin{bmatrix} 1 & 2 &1 \\ 0& 0 &0 \\ -1& -2 &-1 \end{bmatrix}                                                                                                                            Sobel_Y =\begin{bmatrix}1 \\ 2 \\ 1 \end{bmatrix} *\begin{bmatrix} 1 & 0 &-1 \end{bmatrix}=\begin{bmatrix} 1 & 0 &-1 \\ 2&0 &-2 \\ 1 &0 &-1 \end{bmatrix}

 

                                                       d_{x}=f(x, y)^{*} Sobel_{x}(x, y)         d_{y}=f(x, y)^{*} Sobel_{y}(x, y)                                 

 
进一步可以得到图像梯度的幅值:

                                                                          M(x, y)=\sqrt{d_{x}^{2}(x, y)+d_{y}^{2}(x, y)}

为了简化计算,幅值也可以作如下近似:

                                                                         M(x, y)=|d_{x}(x, y)|+|d_{y}(x, y)|

角度为:

                                                                                   \theta_{M}=\arctan \left(d_{y} / d_{x}\right)

如下图表示了中心点的梯度向量、方位角以及边缘方向(任一点的边缘与梯度向量正交) :

                                                          

3、根据角度对幅值进行非极大值抑制

划重点:是沿着梯度方向对幅值进行非极大值抑制,而非边缘方向,这里初学者容易弄混。

例如:3*3区域内,边缘可以划分为垂直、水平、45°、135°4个方向,同样,梯度反向也为四个方向(与边缘方向正交)。因此为了进行非极大值,将所有可能的方向量化为4个方向,如下图:

                                                      

量化化情况可总结为:

  •  水平边缘--梯度方向为垂直: \theta_{M}\in [0,22.5)\cup (-22.5,0]\cup (157.5,180]\cup (-180,157.5]
  •  135°边缘--梯度方向为45°:\theta_{M}\in [22.5,67.5)\cup [-157.5,-112.5) 
  •  垂直边缘--梯度方向为水平: \theta_{M}\in [67.5,112.5]\cup [-112.5,-67.5]
  • 45°边缘--梯度方向为135°: \theta_{M}\in (112.5,157.5]\cup [-67.5,-22.5]

非极大值抑制即为沿着上述4种类型的梯度方向,比较3*3邻域内对应邻域值的大小:

                                                  

在每一点上,领域中心 x 与沿着其对应的梯度方向的两个像素相比,若中心像素为最大值,则保留,否则中心置0,这样可以抑制非极大值,保留局部梯度最大的点,以得到细化的边缘。

 

4、用双阈值算法检测和连接边缘 

  • 选取系数TH和TL,比率为2:1或3:1。(一般取TH=0.3或0.2,TL=0.1);
  • 将小于低阈值的点抛弃,赋0;将大于高阈值的点立即标记(这些点为确定边缘点),赋1或255;
  • 将小于高阈值,大于低阈值的点使用8连通区域确定(即:只有与TH像素连接时才会被接受,成为边缘点,赋 1或255)

 

代码实现

#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
sobel算子/
//阶乘
int factorial(int n){int fac = 1;//0的阶乘if (n == 0)return fac;for (int i = 1; i <= n; ++i){fac *= i;}return fac;
}//获得Sobel平滑算子
cv::Mat getSobelSmoooth(int wsize){int n = wsize - 1;cv::Mat SobelSmooothoper = cv::Mat::zeros(cv::Size(wsize, 1), CV_32FC1);for (int k = 0; k <= n; k++){float *pt = SobelSmooothoper.ptr<float>(0);pt[k] = factorial(n) / (factorial(k)*factorial(n - k));}return SobelSmooothoper;
}//获得Sobel差分算子
cv::Mat getSobeldiff(int wsize){cv::Mat Sobeldiffoper = cv::Mat::zeros(cv::Size(wsize, 1), CV_32FC1);cv::Mat SobelSmoooth = getSobelSmoooth(wsize - 1);for (int k = 0; k < wsize; k++){if (k == 0)Sobeldiffoper.at<float>(0, k) = 1;else if (k == wsize - 1)Sobeldiffoper.at<float>(0, k) = -1;elseSobeldiffoper.at<float>(0, k) = SobelSmoooth.at<float>(0, k) - SobelSmoooth.at<float>(0, k - 1);}return Sobeldiffoper;
}//卷积实现
void conv2D(cv::Mat& src, cv::Mat& dst, cv::Mat kernel, int ddepth, cv::Point anchor = cv::Point(-1, -1), int delta = 0, int borderType = cv::BORDER_DEFAULT){cv::Mat  kernelFlip;cv::flip(kernel, kernelFlip, -1);cv::filter2D(src, dst, ddepth, kernelFlip, anchor, delta, borderType);
}//可分离卷积———先垂直方向卷积,后水平方向卷积
void sepConv2D_Y_X(cv::Mat& src, cv::Mat& dst, cv::Mat kernel_Y, cv::Mat kernel_X, int ddepth, cv::Point anchor = cv::Point(-1, -1), int delta = 0, int borderType = cv::BORDER_DEFAULT){cv::Mat dst_kernel_Y;conv2D(src, dst_kernel_Y, kernel_Y, ddepth, anchor, delta, borderType); //垂直方向卷积conv2D(dst_kernel_Y, dst, kernel_X, ddepth, anchor, delta, borderType); //水平方向卷积
}//可分离卷积———先水平方向卷积,后垂直方向卷积
void sepConv2D_X_Y(cv::Mat& src, cv::Mat& dst, cv::Mat kernel_X, cv::Mat kernel_Y, int ddepth, cv::Point anchor = cv::Point(-1, -1), int delta = 0, int borderType = cv::BORDER_DEFAULT){cv::Mat dst_kernel_X;conv2D(src, dst_kernel_X, kernel_X, ddepth, anchor, delta, borderType); //水平方向卷积conv2D(dst_kernel_X, dst, kernel_Y, ddepth, anchor, delta, borderType); //垂直方向卷积
}//Sobel算子边缘检测
//dst_X 垂直方向
//dst_Y 水平方向
void Sobel(cv::Mat& src, cv::Mat& dst_X, cv::Mat& dst_Y, cv::Mat& dst, int wsize, int ddepth, cv::Point anchor = cv::Point(-1, -1), int delta = 0, int borderType = cv::BORDER_DEFAULT){cv::Mat SobelSmooothoper = getSobelSmoooth(wsize); //平滑系数cv::Mat Sobeldiffoper = getSobeldiff(wsize); //差分系数//可分离卷积———先垂直方向平滑,后水平方向差分——得到垂直边缘sepConv2D_Y_X(src, dst_X, SobelSmooothoper.t(), Sobeldiffoper, ddepth);//可分离卷积———先水平方向平滑,后垂直方向差分——得到水平边缘sepConv2D_X_Y(src, dst_Y, SobelSmooothoper, Sobeldiffoper.t(), ddepth);//边缘强度(近似)dst = abs(dst_X) + abs(dst_Y);cv::convertScaleAbs(dst, dst); //求绝对值并转为无符号8位图
}//确定一个点的坐标是否在图像内
bool checkInRang(int r,int c, int rows, int cols){if (r >= 0 && r < rows && c >= 0 && c < cols)return true;elsereturn false;
}//从确定边缘点出发,延长边缘
void trace(cv::Mat &edgeMag_noMaxsup, cv::Mat &edge, float TL,int r,int c,int rows,int cols){if (edge.at<uchar>(r, c) == 0){edge.at<uchar>(r, c) = 255;for (int i = -1; i <= 1; ++i){for (int j = -1; j <= 1; ++j){float mag = edgeMag_noMaxsup.at<float>(r + i, c + j);if (checkInRang(r + i, c + j, rows, cols) && mag >= TL)trace(edgeMag_noMaxsup, edge, TL, r + i, c + j, rows, cols);}}}
}//Canny边缘检测
void Edge_Canny(cv::Mat &src, cv::Mat &edge, float TL, float TH, int wsize=3, bool L2graydient = false){int rows = src.rows;int cols = src.cols;//高斯滤波cv::GaussianBlur(src,src,cv::Size(5,5),0.8);//sobel算子cv::Mat dx, dy, sobel_dst;Sobel(src, dx, dy, sobel_dst, wsize, CV_32FC1);//计算梯度幅值cv::Mat edgeMag;if (L2graydient)   cv::magnitude(dx, dy, edgeMag); //开平方else  edgeMag = abs(dx) + abs(dy); //绝对值之和近似//计算梯度方向 以及 非极大值抑制cv::Mat edgeMag_noMaxsup = cv::Mat::zeros(rows, cols, CV_32FC1);for (int r = 1; r < rows - 1; ++r){for (int c = 1; c < cols - 1; ++c){float x = dx.at<float>(r, c);float y = dy.at<float>(r, c);float angle = std::atan2f(y, x) / CV_PI * 180; //当前位置梯度方向float mag = edgeMag.at<float>(r, c);  //当前位置梯度幅值//非极大值抑制//垂直边缘--梯度方向为水平方向-3*3邻域内左右方向比较if (abs(angle)<22.5 || abs(angle)>157.5){float left = edgeMag.at<float>(r, c - 1);float right = edgeMag.at<float>(r, c + 1);if (mag >= left && mag >= right)edgeMag_noMaxsup.at<float>(r, c) = mag;}//水平边缘--梯度方向为垂直方向-3*3邻域内上下方向比较if ((angle>=67.5 && angle<=112.5 ) || (angle>=-112.5 && angle<=-67.5)){float top = edgeMag.at<float>(r-1, c);float down = edgeMag.at<float>(r+1, c);if (mag >= top && mag >= down)edgeMag_noMaxsup.at<float>(r, c) = mag;}//+45°边缘--梯度方向为其正交方向-3*3邻域内右上左下方向比较if ((angle>112.5 && angle<=157.5) || (angle>-67.5 && angle<=-22.5)){float right_top = edgeMag.at<float>(r - 1, c+1);float left_down = edgeMag.at<float>(r + 1, c-1);if (mag >= right_top && mag >= left_down)edgeMag_noMaxsup.at<float>(r, c) = mag;}//+135°边缘--梯度方向为其正交方向-3*3邻域内右下左上方向比较if ((angle >=22.5 && angle < 67.5) || (angle >= -157.5 && angle < -112.5)){float left_top = edgeMag.at<float>(r - 1, c - 1);float right_down = edgeMag.at<float>(r + 1, c + 1);if (mag >= left_top && mag >= right_down)edgeMag_noMaxsup.at<float>(r, c) = mag;}}}//双阈值处理及边缘连接edge = cv::Mat::zeros(rows, cols, CV_8UC1);for (int r = 1; r < rows - 1; ++r){for (int c = 1; c < cols - 1; ++c){float mag = edgeMag_noMaxsup.at<float>(r, c);//大于高阈值,为确定边缘点if (mag >= TH)trace(edgeMag_noMaxsup, edge, TL, r, c, rows, cols);else if (mag < TL)edge.at<uchar>(r, c) = 0;}}
}int main(){cv::Mat src = cv::imread("I:\\Learning-and-Practice\\2019Change\\Image process algorithm\\Img\\lena.jpg");if (src.empty()){return -1;}if (src.channels() > 1) cv::cvtColor(src, src, CV_RGB2GRAY);cv::Mat edge,dst;//CannyEdge_Canny(src, edge, 20,60);//opencv自带Cannycv::Canny(src, dst, 20, 80);cv::namedWindow("src", CV_WINDOW_NORMAL);imshow("src", src);cv::namedWindow("My_canny", CV_WINDOW_NORMAL);imshow("My_canny", edge);cv::namedWindow("Opencv_canny", CV_WINDOW_NORMAL);imshow("Opencv_canny", dst);cv::waitKey(0);return 0;
}

效果

与OpenCV的Canny API做了对比。

opencv的canny API: 

void Canny(InputArray image, OutputArray edges, double threshold1, 
double threshold2, int apertureSize=3, bool L2gradient=false )

 

 

一些小注意点:

atan2返回给定的 X 及 Y 坐标值的反正切值。反正切的角度值等于 X 轴与通过原点和给定坐标点 (Y坐标, X坐标) 的直线之间的夹角。结果以弧度表示并介于 -pi 到 pi 之间(不包括 -pi)。 atan2(a, b) 与 atan(a/b)稍有不同,atan2(a,b)的取值范围介于 -pi 到 pi 之间(不包括 -pi), 而atan(a/b)的取值范围介于-pi/2到pi/2之间(不包括±pi/2)。
 

参考:

https://blog.csdn.net/weixin_40647819/article/list/2?

https://docs.opencv.org/3.0-last-rst/doc/tutorials/imgproc/imgtrans/canny_detector/canny_detector.html?highlight=canny

https://blog.csdn.net/liuzhuomei0911/article/details/51345591

https://blog.csdn.net/just_sort/article/details/85053157 


http://chatgpt.dhexx.cn/article/i0Jqd0xx.shtml

相关文章

Opencv(C++)学习系列---Canny边缘检测算法

目录 【1】边缘检测算法流程 【2】Canny算子介绍 【3】完整代码 【1】边缘检测算法流程 使用高斯滤波器来平滑图像&#xff0c;达到滤除噪声的效果。(降噪)计算图像中每个像素点的梯度大小和方向&#xff08;求梯度,与sobel算法求梯度一致&#xff09;使用非极大值抑制&…

OpenCV-Canny边缘检测

作者&#xff1a;翟天保Steven 版权声明&#xff1a;著作权归作者所有&#xff0c;商业转载请联系作者获得授权&#xff0c;非商业转载请注明出处 函数原型 void Canny( InputArray image, OutputArray edges,double threshold1, double threshold2,int apertureSize 3, boo…

Python cv.Canny()方法参数与用法详解

函数原型与参数详解 OpenCV提供了cv.Canny()方法&#xff0c;该方法将输入的原始图像转换为边缘图像。该方法的原型为&#xff1a; cv.Canny(image, threshold1, threshold2[, edges[, apertureSize[, L2gradient]]]) -> edges cv.Canny(dx, dy, threshold1, threshold2[…

OpenCV—python 边缘检测(Canny)

边缘检测 一、边缘定义及类型二、边缘检测算子类别三、OpenCV-Python 中 Canny() 参数 一、边缘定义及类型 边缘类型&#xff1a;简单分为4中类型&#xff0c;阶跃型、屋脊型、斜坡型、脉冲型&#xff0c;其中阶跃型和斜坡型是类似的&#xff0c;只是变化的快慢不同。 二、边…

Canny算子Matlab实现

1、Canny边缘提取原理 Canny边缘检测方法利用了梯度方向信息&#xff0c;采用“非极大抑制”以及双阈值技术&#xff0c;获得了单像素连续边缘&#xff0c;是目前所认为的检测效果较好的一种边缘检测方法。 先利用高斯函数对图像进行低通滤波&#xff1b;然后对图像中的每个像…

canny算子的python实现以及pytorch实现

canny算子的python实现以及pytorch实现 canny的python实现canny的pytorch实现 canny的python实现 参考Canny边缘检测算法(python 实现&#xff09; import numpy as np import cv2 as cv from matplotlib import pyplot as pltdef smooth(image, sigma 1.4, length 5) :&qu…

Python 使用cv2.canny 进行图像边缘检测

CV2提供了提取图像边缘的函数canny。其算法思想如下&#xff1a; 1. 使用高斯模糊&#xff0c;去除噪音点&#xff08;cv2.GaussianBlur&#xff09;2. 灰度转换&#xff08;cv2.cvtColor&#xff09;3. 使用sobel算子&#xff0c;计算出每个点的梯度大小和梯度方向4. 使用非极…

计算机视觉中Canny算子详解

文章目录 前言一、Canny的实现步骤二、具体实现1.高斯平滑滤波2.计算梯度大小和方向3.非极大抑制4.双阈值(Double Thresholding)和滞后边界跟踪 总结 前言 Canny边缘检测是一种非常流行的边缘检测算法&#xff0c;是John Canny在1986年提出的。它是一个多阶段的算法&#xff0c…

Canny 边缘检测算法

目录 一、边缘检测的步骤 二、最优边缘定义 三、Canny边缘检测算法步骤 1.对图像进行灰度化&#xff1a; 2.对图像进行高斯滤波&#xff1a; 3. 计算梯度幅值和方向 4.非极大值&#xff08;Non-Maximum Suppression&#xff09;抑制 5.用双阈值算法检测和连接边缘 代…

Canny算法

Canny Canny分为5个步骤 1)、使用高斯滤波器&#xff0c;以平滑图像&#xff0c;滤除噪声。 高斯滤波器是一种平滑空间滤波器&#xff0c;用于模糊处理和降低噪声。我们的高斯滤波器通过以下公式得到。 我们运用该公式计算出高斯卷积核&#xff0c;如k越大&#xff0c;检测…

OpenCv之Canny

目录 一、自适应阈值 二、边缘检测Canny 一、自适应阈值 引入前提:在前面的部分我们使用是全局闻值&#xff0c;整幅图像采用同一个数作为闻值。当时这种方法并不适应与所有情况&#xff0c;尤其是当同一幅图像上的不同部分的具有不同亮度时。这种情况下我们需要采用自适应闻…

图像处理——Canny算子

首先感谢以下两位的渊博知识&#xff1a; &#xff08;1&#xff09;爱鱼 https://www.cnblogs.com/mightycode/p/6394810.html &#xff08;2&#xff09;mitutao https://www.cnblogs.com/love6tao/p/5152020.html 图像边缘信息主要集中在高频段&#xff0c;通常…

Canny算子与霍夫变换检测圆与直线

目录 引言 一、canny算子 二、canny算子代码 三、霍夫变换检测直线 四、霍夫变换检测直线代码 五、霍夫变换检测直线效果 六、霍夫变换检测圆 七、霍夫变换检测圆代码 八、霍夫变换检测圆效果 引言 canny算子是计算机视觉最常用的一种算子&#xff0c;是目前一种非常流行…

OpenCV——Canny边缘检测(cv2.Canny())

Canny边缘检测 Canny 边缘检测是一种使用多级边缘检测算法检测边缘的方法。1986 年&#xff0c;John F. Canny 发 表了著名的论文 A Computational Approach to Edge Detection&#xff0c;在该论文中详述了如何进行边缘 检测。 Canny()边缘检测步骤 Canny 边缘检测分为如下…

(十一)Canny 边缘检测算法

Canny边缘检测算法 一、边缘检测的步骤 1&#xff09;滤波&#xff1a; 边缘检测的算法主要是基于图像强度的一阶和二阶导数&#xff0c;但导数通常对噪声很敏感&#xff0c; 因此必须采用滤波器来改善与噪声有关的边缘检测器的性能。常见的滤波方法主要有高斯滤波、均值滤波…

Canny边缘检测算法的实现

图像边缘信息主要集中在高频段&#xff0c;通常说图像锐化或检测边缘&#xff0c;实质就是高频滤波。我们知道微分运算是求信号的变化率&#xff0c;具有加强高频分量的作用。在空域运算中来说&#xff0c;对图像的锐化就是计算微分。由于数字图像的离散信号&#xff0c;微分运…

【canny边缘检测】canny边缘检测原理及代码详解

文章目录 前言canny边缘检测算法主要流程一、高斯模糊二、图像梯度计算三、非极大值抑制四、双阈值边界跟踪 前言 本文通过介绍canny边缘检测原理与代码解析&#xff0c;希望能让大家深入理解canny边缘检测 canny边缘检测算法主要流程 canny边缘检测主要分为4个部分&#xff…

Canny边缘检测原理

一. Canny基本思想 1. 边缘检测 解析&#xff1a;边缘是对象和背景之间的边界&#xff0c;还能表示重叠对象之间的边界。边缘检测是图像分割的一部分&#xff0c;图像分割的目的是识别出图像中的区域。边缘检测是定位边缘像素的过程&#xff0c;而边缘增强是增加边缘和背景之…

Canny边缘检测

边缘检测发展 Canny 边缘检测是一种从不同视觉对象中提取有用结构信息并显着减少要处理的数据量的技术。它已广泛应用于各种计算机视觉系统。 Canny 发现&#xff0c;在不同的视觉系统上应用边缘检测的要求是比较相似的。因此&#xff0c;可以在各种情况下实施满足这些要求的边…

Canny边缘检测算法(python 实现)

文章目录 最优边缘准则算法实现步骤1. 应用高斯滤波来平滑(模糊)图像&#xff0c;目的是去除噪声2. 计算梯度强度和方向3. 应用非最大抑制技术NMS来消除边误检4. 应用双阈值的方法来决定可能的&#xff08;潜在的&#xff09;边界5. 利用滞后技术来跟踪边界 opencv实现Canny边缘…