Canny边缘检测算法的实现

article/2025/9/23 19:38:56

图像边缘信息主要集中在高频段,通常说图像锐化或检测边缘,实质就是高频滤波。我们知道微分运算是求信号的变化率,具有加强高频分量的作用。在空域运算中来说,对图像的锐化就是计算微分。由于数字图像的离散信号,微分运算就变成计算差分或梯度。图像处理中有多种边缘检测(梯度)算子,常用的包括普通一阶差分,Robert算子(交叉差分),Sobel算子等等,是基于寻找梯度强度。拉普拉斯算子(二阶差分)是基于过零点检测。通过计算梯度,设置阀值,得到边缘图像。

Canny边缘检测算子是一种多级检测算法。1986年由John F. Canny提出,同时提出了边缘检测的三大准则:

  1. 低错误率的边缘检测:检测算法应该精确地找到图像中的尽可能多的边缘,尽可能的减少漏检和误检。
  2. 最优定位:检测的边缘点应该精确地定位于边缘的中心。
  3. 图像中的任意边缘应该只被标记一次,同时图像噪声不应产生伪边缘。

Canny算法出现以后一直是作为一种标准的边缘检测算法,此后也出现了各种基于Canny算法的改进算法。时至今日,Canny算法及其各种变种依旧是一种优秀的边缘检测算法。而且除非前提条件很适合,你很难找到一种边缘检测算子能显著地比Canny算子做的更好。

关于各种差分算子,还有Canny算子的简单介绍,这里就不罗嗦了,网上都可以找得到。直接进入Canny算法的实现。Canny算法分为几步。

1. 高斯模糊。

这一步很简单,类似于LoG算子(Laplacian of Gaussian)作高斯模糊一样,主要作用就是去除噪声。因为噪声也集中于高频信号,很容易被识别为伪边缘。应用高斯模糊去除噪声,降低伪边缘的识别。但是由于图像边缘信息也是高频信号,高斯模糊的半径选择很重要,过大的半径很容易让一些弱边缘检测不到。

  

             Lena原图                                       Lena高斯模糊,半径2

2. 计算梯度幅值和方向。

图像的边缘可以指向不同方向,因此经典Canny算法用了四个梯度算子来分别计算水平,垂直和对角线方向的梯度。但是通常都不用四个梯度算子来分别计算四个方向。常用的边缘差分算子(如Rober,Prewitt,Sobel)计算水平和垂直方向的差分Gx和Gy。这样就可以如下计算梯度模和方向:

梯度角度θ范围从弧度-π到π,然后把它近似到四个方向,分别代表水平,垂直和两个对角线方向(0°,45°,90°,135°)。可以以±iπ/8(i=1,3,5,7)分割,落在每个区域的梯度角给一个特定值,代表四个方向之一。

这里我选择Sobel算子计算梯度。Sobel算法很简单,到处都可以找到,就不列出来了。相对于其他边缘算子,Sobel算子得出来的边缘粗大明亮。

 

 

 

下图是对上面半径2的高斯模糊图像L通道(HSL)应用Sobel算子的梯度模图,没有施加任何阀值。

           Sobel算子,无阀值

3. 非最大值抑制。

非最大值抑制是一种边缘细化方法。通常得出来的梯度边缘不止一个像素宽,而是多个像素宽。就像我们所说Sobel算子得出来的边缘粗大而明亮,从上面Lena图的Sobel结果可以看得出来。因此这样的梯度图还是很“模糊”。而准则3要求,边缘只有一个精确的点宽度。非最大值抑制能帮助保留局部最大梯度而抑制所有其他梯度值。这意味着只保留了梯度变化中最锐利的位置。算法如下:

  1. 比较当前点的梯度强度和正负梯度方向点的梯度强度。
  2. 如果当前点的梯度强度和同方向的其他点的梯度强度相比较是最大,保留其值。否则抑制,即设为0。比如当前点的方向指向正上方90°方向,那它需要和垂直方向,它的正上方和正下方的像素比较。

注意,方向的正负是不起作用的,比如东南方向和西南方向是一样的,都认为是对角线的一个方向。前面我们把梯度方向近似到水平,垂直和两个对角线四个方向,所以每个像素根据自身方向在这四个方向之一进行比较,决定是否保留。这一部分的代码也很简单,列出如下。pModule,pDirection分别记录了上一步梯度模值和梯度方向。

pmoddrow = pModule + Width + 1; 
pdirdrow = pDirection + Width + 1;
pstrongdrow = pStrong + Width + 1;
for (i = 1; i < Hend - 1; i++)
{
pstrongd = pstrongdrow;
pmodd = pmoddrow;
pdird = pdirdrow;
for (j = 1; j < Wend - 1; j++)
{
switch (*pdird)
{
case 0:        // x direction
case 4:
if (*pmodd > *(pmodd - 1) && *pmodd > *(pmodd + 1))
*pstrongd = 255;
break;
case 1:        // northeast-southwest direction. Notice the data order on y direction of bmp data
case 5:
if (*pmodd > *(pmodd + Width + 1) && *pmodd > *(pmodd - Width - 1))
*pstrongd = 255;
break;
case 2:        // y direction
case 6:
if (*pmodd > *(pmodd - Width) && *pmodd > *(pmodd + Width))
*pstrongd = 255;
break;
case 3:        // northwest-southeast direction. Notice the data order on y direction of bmp data
case 7:
if (*pmodd > *(pmodd + Width - 1) && *pmodd > *(pmodd - Width + 1))
*pstrongd = 255;
break;
default:
ASSERT(0);
break;
}
pstrongd++;
pmodd++;
pdird++;
}
pstrongdrow += Width;
pmoddrow += Width;
pdirdrow += Width;
}
View Code

下图是非最大值抑制的结果。可见边缘宽度已经大大减小。但是这个图像中因为没有应用任何阀值,还含有大量小梯度模值的点,也就是图中很暗的地方。下面,阀值要上场了。

             非最大值抑制结果

4. 双阀值。

一般的边缘检测算法用一个阀值来滤除噪声或颜色变化引起的小的梯度值,而保留大的梯度值。Canny算法应用双阀值,即一个高阀值和一个低阀值来区分边缘像素。如果边缘像素点梯度值大于高阀值,则被认为是强边缘点。如果边缘梯度值小于高阀值,大于低阀值,则标记为弱边缘点。小于低阀值的点则被抑制掉。这一步算法很简单。

5. 滞后边界跟踪。

至此,强边缘点可以认为是真的边缘。弱边缘点则可能是真的边缘,也可能是噪声或颜色变化引起的。为得到精确的结果,后者引起的弱边缘点应该去掉。通常认为真实边缘引起的弱边缘点和强边缘点是连通的,而又噪声引起的弱边缘点则不会。所谓的滞后边界跟踪算法检查一个弱边缘点的8连通领域像素,只要有强边缘点存在,那么这个弱边缘点被认为是真是边缘保留下来。

这个算法搜索所有连通的弱边缘,如果一条连通的弱边缘的任何一个点和强边缘点连通,则保留这条弱边缘,否则抑制这条弱边缘。搜索时可以用广度优先或者深度优先算法,我在这里实现了应该是最容易的深度优先算法。一次连通一条边缘的深度优先算法如下:

  1. 准备一个栈s,一个队列q,设联通指示变量connected为假。从图像的第一个点开始,进入2。
  2. 如果这个点是弱边界点并且没有被标记,把它标记,并把它作为第一个元素放入栈s中,同时把它放入记录连通曲线的队列q,进入3。如果这个点不是弱边界或者已经被标记过,到图像的下一个点,重复2。
  3. 从栈s中取出一个元素,查找它的8像素领域。如果一个领域像素是弱边界并且没有被标记过,把这个领域像素标记,并加入栈s中,同时加入队列q。同时查找领域对应的强边界图,如果有一个像素是强边界,表示这条弱边界曲线和强边界联通,设置connected为真。重复3直到栈中没有元素了。如果connected为假,则依次从队列q中取出每个元素,清空标记。如果connected为真,保留标记。
  4. 清空队列q,设置connected为假,移动到图像的下一个点,到2。
// 5. Edge tracking by hysteresis
stack<CPoint> s;
queue<CPoint> q;
BOOL connected = FALSE;
long row_idx = Width;
for (i = 1; i < Height - 1; i++, row_idx += Width)
{
for (j = 1; j < Width - 1; j++)
{
pweakd = pWeak + row_idx + j;
if (*pweakd == 255)
{
s.push(CPoint(j, i));
q.push(CPoint(j, i));
*pweakd = 1;        // Label it
while (!s.empty())
{
CPoint p = s.top();
s.pop();
// Search weak edge 8-point neighborhood
pweakd = pWeak + p.y*Width + p.x;
if (*(pweakd - Width - 1) == 255)
{
CPoint np = CPoint(p.x - 1, p.y - 1);
s.push(np);
q.push(np);
*(pweakd - Width - 1) = 1;        // Label it
                    }
if (*(pweakd - Width) == 255)
{
CPoint np = CPoint(p.x, p.y - 1);
s.push(np);
q.push(np);
*(pweakd - Width) = 1;        // Label it
                    }
if (*(pweakd - Width + 1) == 255)
{
CPoint np = CPoint(p.x + 1, p.y - 1);
s.push(np);
q.push(np);
*(pweakd - Width + 1) = 1;        // Label it
                    }
if (*(pweakd - 1) == 255)
{
CPoint np = CPoint(p.x - 1, p.y);
s.push(np);
q.push(np);
*(pweakd - 1) = 1;        // Label it
                    }
if (*(pweakd + 1) == 255)
{
CPoint np = CPoint(p.x + 1, p.y);
s.push(np);
q.push(np);
*(pweakd + 1) = 1;        // Label it
                    }
if (*(pweakd + Width - 1) == 255)
{
CPoint np = CPoint(p.x - 1, p.y + 1);
s.push(np);
q.push(np);
*(pweakd + Width - 1) = 1;        // Label it
                    }
if (*(pweakd + Width) == 255)
{
CPoint np = CPoint(p.x, p.y + 1);
s.push(np);
q.push(np);
*(pweakd + Width) = 1;        // Label it
                    }
if (*(pweakd + Width + 1) == 255)
{
CPoint np = CPoint(p.x + 1, p.y + 1);
s.push(np);
q.push(np);
*(pweakd + Width + 1) = 1;        // Label it
                    }
// Search strong edge 8-point neighborhood
if (connected == FALSE)
{
pstrongd = pStrong + p.y*Width + p.x;
for (int m = -1; m <= 1; m++)
{
for (int n = -1; n <= 1; n++)
{
if (*(pstrongd + m*Width + n) == 255)
{
connected = TRUE;
goto next;
}
}
}
}
next:
continue;
}
// No more element in the stack
if (connected == FALSE)
{
// The weak edge is not connected to any strong edge. Suppress it.
while (!q.empty())
{
CPoint p = q.front();
q.pop();
pWeak[p.y*Width + p.x] = 0;
}
}
else
{
// Clean the queue
while (!q.empty()) q.pop();
connected = FALSE;
}
}
}
}
// Add the connected weak edges (labeled) into strong edge image.
// All strong edge pixels are labeled 255, otherwise 0.
for (i = 0; i < len; i++)
{
if (pWeak[i] == 1) pStrong[i] = 255;
}
View Code

下面是对Lena图计算Canny边缘检测的梯度模图和二值化图,高斯半径2,高阀值100,低阀值50。

  

             Canny检测梯度模图                        Canny检测梯度二值图

作为对比,下面是用一阶差分和Sobel算子对原图计算的结果,阀值100。由于一阶差分的梯度值相对较小,我对一阶差分的梯度值放大了一定倍数,使得它和Sobel的梯度值保持同样的水平。

  

               一阶差分梯度模图                        一阶差分梯度二值图

  

                 Sobel梯度模图                          Sobel梯度二值图

很明显,Canny边缘检测的效果是很显著的。相比普通的梯度算法大大抑制了噪声引起的伪边缘,而且是边缘细化,易于后续处理。对于对比度较低的图像,通过调节参数,Canny算法也能有很好的效果。

原图   Canny边缘梯度模,高斯半径2,低阀值30,高阀值100    Canny边缘梯度二值化图,高斯半径2,低阀值30,高阀值100

                       原图                            Canny梯度模,高斯半径2,低阀值30,高阀值100       Canny梯度二值化图,高斯半径2,低阀值30,高阀值100

原图   Canny边缘梯度模,高斯半径1,低阀值40,高阀值80   Canny边缘梯度二值化图,高斯半径1,低阀值40,高阀值80

                      原图                         Canny梯度模,高斯半径1,低阀值40,高阀值80  Canny梯度二值化图,高斯半径1,低阀值40,高阀值80

参考

Canny edge detector

canny 算法

 


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

相关文章

【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边缘…

左外连接和右外连接是什么样的?

1左外连接 左表的记录都会出现在结果集中&#xff0c;并上右表与之对应的部分&#xff0c;如果右表没有&#xff0c;使用null填充。 举例。 select * from people p left join role r p.idr.id 左外连接结果&#xff1a; 2、右外连接 右表的记录都会出现在结果集中&#xf…

sql左外连接和右外连接的区别

一、数据集合不同 1、左外连接&#xff1a;是A和B的交集再并上A的所有数据。 2、右外连接&#xff1a;是A和B的交集再并上B的所有数据。 二、语法不同 1、左外连接&#xff1a;SELECT *FROM aLEFT OUTER JOIN bON a.ageId b.id。 2、右外连接&#xff1a;SELECT *FROM a…

MySQL自连接和内连接和外连接_左外连接+右外连接

自连接&#xff1a;将一张表看作两张表 练习&#xff1a;查询员工id&#xff0c;员工姓名及其管理者的id和姓名select emp.employee_id,emp.last_name,mgr.employee_id,mgr.last_name from employees emp,employees mgr where emp.manager_id mgr.employee_id;内连接 只是把左…

SQL--左外连接(LEFT JOIN)

LEFT JOIN 关键字从左表&#xff08;table1&#xff09;返回所有的行&#xff0c;即使右表&#xff08;table2&#xff09;中没有匹配。如果右表中没有匹配&#xff0c;则结果为 NULL。 SQL LEFT JOIN 语法: SELECT column_name(s) FROM table1 LEFT JOIN table2 ON table1.c…

内连接、左连接、左外连接、右连接的区别

一&#xff1a;测试例子 存在如下两张表&#xff1a;年级与班级&#xff0c;表内容如下 班级表 二&#xff1a;内连接与自然连接 1&#xff1a;自然连接 对于select * from gradeclass gc where gc.grade_id in (select id from grade)&#xff0c;其结果如下图 其查询是把符…

数据库之SQL(基本连接,内连接,左外连接,右外连接,全外连接,交叉连接,自连接)

之前的博客内容我们分享了数据表的查询与管理&#xff0c;但那只是针对数据库中的一个表格进行的查询管理&#xff0c;现在如果我们想要同时看到两个数据表中的数据的话&#xff0c;那要怎么办呢&#xff1f;采用多连接查询的方式。 SQL中有哪几种多连接的方式&#xff1f; 有七…

左连接、右连接、内连接、外连接简单说明

例如有两个表: test1: test2: 内连接&#xff1a;&#xff08;inner join on&#xff09;返回两个表的交集。 例如&#xff1a; select * from test1 a inner join test2 b on a.idb.id; 结果&#xff1a; 外连接&#xff1a;返回两个表的并集。&#xff08;在此就不做截图…

MySQL之左外连接右外连接

外连接&#xff1a; 假设A和B表进行连接&#xff0c;使用外连接的话&#xff0c; AB两张表中有一张表是主表&#xff0c;一张表是副表&#xff0c;主要查询主表中的数据&#xff0c;捎带着查询副表&#xff0c; 当副表中的数据没有和主表中的数据匹配上&#xff0c;副表自动模…

秒懂sql中的左外连接

左外连接&#xff1a; 1.语法&#xff1a; SELECT 查询字段 FROM 查询表格&#xff08;左表&#xff09; LEFT JOIN 右表 ON 查询条件 2.左外连接查询的就是左表&#xff0c;那干嘛加个右表&#xff0c;岂不是没有作用&#xff1a; 区别在于左外连接是查询完左表后&…

sql的四种连接——左外连接、右外连接、内连接、全连接

一、内连接 满足条件的记录才会出现在结果集中。 二、 左外连接&#xff08;left outer join&#xff0c;outer可省略&#xff09; 左表全部出现在结果集中&#xff0c;若右表无对应记录&#xff0c;则相应字段为NULL 举例说明&#xff1a; 客户表&#xff1a; 订单表&#x…

数据库的内连接、外连接(左外连接、右外连接、全外连接)以及交叉连接

SQL数据库的连接&#xff1a;内连接、和外连接&#xff08;左外连接、右外连接、和全连接&#xff09; 本次实验在MySQL数据库中进行&#xff0c;数据库初始表如下&#xff1a; 一、内连接&#xff1a;结果仅包含符合连接条件的两表中的行。如下&#xff1a; 二、外连接&#…

oracle左外连接、右外连接、完全外连接以及(+)号用法

准备工作 oracle连接分为&#xff1a; 左外连接&#xff1a;左表不加限制&#xff0c;保留左表的数据&#xff0c;匹配右表&#xff0c;右表没有匹配到的行中的列显示为null。右外连接&#xff1a;右表不加限制&#xff0c;保留右表的数据。匹配左表&#xff0c;左表没有匹配到…

左连接,右链接

用两个表&#xff08;a_table、b_table&#xff09;&#xff0c;关联字段a_table.a_id和b_table.b_id来演示一下MySQL的内连接、外连接&#xff08; 左(外)连接、右(外)连接、全(外)连接&#xff09;。 MySQL版本&#xff1a;Server version: 5.6.31 MySQL Community Server (…

全外连接、左外连接和右外连接区别(含举例)

全外连接是左外连接和右外连接的结合。 左外连接和右外连接的区别如下&#xff1a; 1、数据集合上的区别 &#xff08;1&#xff09;左外连接&#xff1a;是A与B的交集&#xff0c;然后连接A的所有数据。 &#xff08;2&#xff09;右外连接&#xff1a;是A与B的交集&#xff…

连接查询之外连接(左外链接和右外连接)

内连接&#xff1a; 假设A表和B表进行连接查询&#xff0c;使用内连接的话&#xff0c;凡是A表和B表能够匹配上的记录被查询出来&#xff0c;这就是内连接。A、B两张表没有主副之分&#xff0c;两张表是平等的。 外连接&#xff1a; 假设A表和B表进行连接查询&#xff0c;使用…

左连接、右连接、内连接、外连接

一.外连接 外连接分三类&#xff1a;左外连接&#xff08;LEFT OUTER JOIN&#xff09;、右外连接&#xff08;RIGHT OUTER JOIN&#xff09;和全外连接&#xff08;FULL OUTER JOIN&#xff09;。 1.左外连接 左连接 left join 是left outer join的简写&#xff0c;left j…