Canny边缘检测算法的实现
图像边缘信息主要集中在高频段,通常说图像锐化或检测边缘,实质就是高频滤波。我们知道微分运算是求信号的变化率,具有加强高频分量的作用。在空域运算中来说,对图像的锐化就是计算微分。由于数字图像的离散信号,微分运算就变成计算差分或梯度。图像处理中有多种边缘检测(梯度)算子,常用的包括普通一阶差分,Robert算子(交叉差分),Sobel算子等等,是基于寻找梯度强度。拉普拉斯算子(二阶差分)是基于过零点检测。通过计算梯度,设置阀值,得到边缘图像。
Canny边缘检测算子是一种多级检测算法。1986年由John F. Canny提出,同时提出了边缘检测的三大准则:
- 低错误率的边缘检测:检测算法应该精确地找到图像中的尽可能多的边缘,尽可能的减少漏检和误检。
- 最优定位:检测的边缘点应该精确地定位于边缘的中心。
- 图像中的任意边缘应该只被标记一次,同时图像噪声不应产生伪边缘。
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要求,边缘只有一个精确的点宽度。非最大值抑制能帮助保留局部最大梯度而抑制所有其他梯度值。这意味着只保留了梯度变化中最锐利的位置。算法如下:
- 比较当前点的梯度强度和正负梯度方向点的梯度强度。
- 如果当前点的梯度强度和同方向的其他点的梯度强度相比较是最大,保留其值。否则抑制,即设为0。比如当前点的方向指向正上方90°方向,那它需要和垂直方向,它的正上方和正下方的像素比较。
注意,方向的正负是不起作用的,比如东南方向和西北方向是一样的,都认为是对角线的一个方向。前面我们把梯度方向近似到水平,垂直和两个对角线四个方向,所以每个像素根据自身方向在这四个方向之一进行比较,决定是否保留。这一部分的代码也很简单,列出如下。pModule,pDirection分别记录了上一步梯度模值和梯度方向。
pmoddrow = pModule + Width + ;
pdirdrow = pDirection + Width + ;
pstrongdrow = pStrong + Width + ;
for (i = ; i < Hend - ; i++)
{
pstrongd = pstrongdrow;
pmodd = pmoddrow;
pdird = pdirdrow;
for (j = ; j < Wend - ; j++)
{
switch (*pdird)
{
case : // x direction
case :
if (*pmodd > *(pmodd - ) && *pmodd > *(pmodd + ))
*pstrongd = ;
break;
case : // northeast-southwest direction. Notice the data order on y direction of bmp data
case :
if (*pmodd > *(pmodd + Width + ) && *pmodd > *(pmodd - Width - ))
*pstrongd = ;
break;
case : // y direction
case :
if (*pmodd > *(pmodd - Width) && *pmodd > *(pmodd + Width))
*pstrongd = ;
break;
case : // northwest-southeast direction. Notice the data order on y direction of bmp data
case :
if (*pmodd > *(pmodd + Width - ) && *pmodd > *(pmodd - Width + ))
*pstrongd = ;
break;
default:
ASSERT();
break;
}
pstrongd++;
pmodd++;
pdird++;
}
pstrongdrow += Width;
pmoddrow += Width;
pdirdrow += Width;
}
下图是非最大值抑制的结果。可见边缘宽度已经大大减小。但是这个图像中因为没有应用任何阀值,还含有大量小梯度模值的点,也就是图中很暗的地方。下面,阀值要上场了。
非最大值抑制结果
4. 双阀值。
一般的边缘检测算法用一个阀值来滤除噪声或颜色变化引起的小的梯度值,而保留大的梯度值。Canny算法应用双阀值,即一个高阀值和一个低阀值来区分边缘像素。如果边缘像素点梯度值大于高阀值,则被认为是强边缘点。如果边缘梯度值小于高阀值,大于低阀值,则标记为弱边缘点。小于低阀值的点则被抑制掉。这一步算法很简单。
5. 滞后边界跟踪。
至此,强边缘点可以认为是真的边缘。弱边缘点则可能是真的边缘,也可能是噪声或颜色变化引起的。为得到精确的结果,后者引起的弱边缘点应该去掉。通常认为真实边缘引起的弱边缘点和强边缘点是连通的,而又噪声引起的弱边缘点则不会。所谓的滞后边界跟踪算法检查一个弱边缘点的8连通领域像素,只要有强边缘点存在,那么这个弱边缘点被认为是真是边缘保留下来。
这个算法搜索所有连通的弱边缘,如果一条连通的弱边缘的任何一个点和强边缘点连通,则保留这条弱边缘,否则抑制这条弱边缘。搜索时可以用广度优先或者深度优先算法,我在这里实现了应该是最容易的深度优先算法。一次连通一条边缘的深度优先算法如下:
- 准备一个栈s,一个队列q,设联通指示变量connected为假。从图像的第一个点开始,进入2。
- 如果这个点是弱边界点并且没有被标记,把它标记,并把它作为第一个元素放入栈s中,同时把它放入记录连通曲线的队列q,进入3。如果这个点不是弱边界或者已经被标记过,到图像的下一个点,重复2。
- 从栈s中取出一个元素,查找它的8像素领域。如果一个领域像素是弱边界并且没有被标记过,把这个领域像素标记,并加入栈s中,同时加入队列q。同时查找领域对应的强边界图,如果有一个像素是强边界,表示这条弱边界曲线和强边界联通,设置connected为真。重复3直到栈中没有元素了。如果connected为假,则依次从队列q中取出每个元素,清空标记。如果connected为真,保留标记。
- 清空队列q,设置connected为假,移动到图像的下一个点,到2。
// 5. Edge tracking by hysteresis
stack<CPoint> s;
queue<CPoint> q;
BOOL connected = FALSE;
long row_idx = Width;
for (i = ; i < Height - ; i++, row_idx += Width)
{
for (j = ; j < Width - ; j++)
{
pweakd = pWeak + row_idx + j;
if (*pweakd == )
{
s.push(CPoint(j, i));
q.push(CPoint(j, i));
*pweakd = ; // 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 - ) == )
{
CPoint np = CPoint(p.x - , p.y - );
s.push(np);
q.push(np);
*(pweakd - Width - ) = ; // Label it
}
if (*(pweakd - Width) == )
{
CPoint np = CPoint(p.x, p.y - );
s.push(np);
q.push(np);
*(pweakd - Width) = ; // Label it
}
if (*(pweakd - Width + ) == )
{
CPoint np = CPoint(p.x + , p.y - );
s.push(np);
q.push(np);
*(pweakd - Width + ) = ; // Label it
}
if (*(pweakd - ) == )
{
CPoint np = CPoint(p.x - , p.y);
s.push(np);
q.push(np);
*(pweakd - ) = ; // Label it
}
if (*(pweakd + ) == )
{
CPoint np = CPoint(p.x + , p.y);
s.push(np);
q.push(np);
*(pweakd + ) = ; // Label it
}
if (*(pweakd + Width - ) == )
{
CPoint np = CPoint(p.x - , p.y + );
s.push(np);
q.push(np);
*(pweakd + Width - ) = ; // Label it
}
if (*(pweakd + Width) == )
{
CPoint np = CPoint(p.x, p.y + );
s.push(np);
q.push(np);
*(pweakd + Width) = ; // Label it
}
if (*(pweakd + Width + ) == )
{
CPoint np = CPoint(p.x + , p.y + );
s.push(np);
q.push(np);
*(pweakd + Width + ) = ; // Label it
}
// Search strong edge 8-point neighborhood
if (connected == FALSE)
{
pstrongd = pStrong + p.y*Width + p.x;
for (int m = -; m <= ; m++)
{
for (int n = -; n <= ; n++)
{
if (*(pstrongd + m*Width + n) == )
{
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] = ;
}
}
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 = ; i < len; i++)
{
if (pWeak[i] == ) pStrong[i] = ;
}
下面是对Lena图计算Canny边缘检测的梯度模图和二值化图,高斯半径2,高阀值100,低阀值50。
Canny检测梯度模图 Canny检测梯度二值图
作为对比,下面是用一阶差分和Sobel算子对原图计算的结果,阀值100。由于一阶差分的梯度值相对较小,我对一阶差分的梯度值放大了一定倍数,使得它和Sobel的梯度值保持同样的水平。
一阶差分梯度模图 一阶差分梯度二值图
Sobel梯度模图 Sobel梯度二值图
很明显,Canny边缘检测的效果是很显著的。相比普通的梯度算法大大抑制了噪声引起的伪边缘,而且是边缘细化,易于后续处理。对于对比度较低的图像,通过调节参数,Canny算法也能有很好的效果。
原图 Canny梯度模,高斯半径2,低阀值30,高阀值100 Canny梯度二值化图,高斯半径2,低阀值30,高阀值100
原图 Canny梯度模,高斯半径1,低阀值40,高阀值80 Canny梯度二值化图,高斯半径1,低阀值40,高阀值80
参考
Canny边缘检测算法的实现的更多相关文章
- 一些关于Canny边缘检测算法的改进
传统的Canny边缘检测算法是一种有效而又相对简单的算法,可以得到很好的结果(可以参考上一篇Canny边缘检测算法的实现).但是Canny算法本身也有一些缺陷,可以有改进的地方. 1. Canny边缘 ...
- 【算法随记】Canny边缘检测算法实现和优化分析。
以前的博文大部分都写的非常详细,有很多分析过程,不过写起来确实很累人,一般一篇好的文章要整理个三四天,但是,时间越来越紧张,后续的一些算法可能就以随记的方式,把实现过程的一些比较容易出错和有价值的细节 ...
- Canny边缘检测算法的一些改进
传统的Canny边缘检测算法是一种有效而又相对简单的算法,可以得到很好的结果(可以参考上一篇Canny边缘检测算法的实现).但是Canny算法本身也有一些缺陷,可以有改进的地方. 1. Canny边缘 ...
- OpenCV: Canny边缘检测算法原理及其VC实现详解(转载)
原文地址:http://blog.csdn.net/likezhaobin/article/details/6892176 原文地址:http://blog.csdn.net/likezhaobin/ ...
- Canny边缘检测算法原理及其VC实现详解(一)
转自:http://blog.csdn.net/likezhaobin/article/details/6892176 图象的边缘是指图象局部区域亮度变化显著的部分,该区域的灰度剖面一般可以看作是一个 ...
- Canny边缘检测算法原理及其VC实现详解(二)
转自:http://blog.csdn.net/likezhaobin/article/details/6892629 3. Canny算法的实现流程 由于本文主要目的在于学习和实现算法,而对于图像 ...
- [转载+原创]Emgu CV on C# (六) —— Emgu CV on Canny边缘检测
Canny边缘检测也是一种边缘检测方法,本文介绍了Canny边缘检测的函数及其使用方法,并利用emgucv方法将轮廓检测解算的结果与原文进行比较. 图像的边缘检测的原理是检测出图像中所有灰度值变化较大 ...
- 学习笔记-canny边缘检测
Canny边缘检测 声明:阅读本文需要了解线性代数里面的点乘(图像卷积的原理),高等数学里的二元函数的梯度,极大值定义,了解概率论里的二维高斯分布 1.canny边缘检测原理和简介 2.实现步骤 3. ...
- opencv-学习笔记(6)图像梯度Sobel以及canny边缘检测
opencv-学习笔记(6)图像梯度Sobel以及canny边缘检测 这章讲了 sobel算子 scharr算子 Laplacion拉普拉斯算子 图像深度问题 Canny检测 图像梯度 sobel算子 ...
随机推荐
- Delphi获取文件的大小(实际&物理)
源:获取文件的大小(实际&物理) class function TDuoFile.GetFileSize(const AFile: TFileName): Int64; var sr:TSea ...
- iOS推送的众多坑
新来的一家公司,昨天和同事解决推送问题(工程里有集成百度推送和环信即时通讯),信誓旦旦的声称:" app在前台和后台运行时,推送触发的是didReceiveRemoteNotificatio ...
- hadoop如何查看文件系统
1.查看当前的文件系统 [root@hadoopmaster bin]# . itemsdrwxr 00 00 /user 当然也可以以浏览器中这样查看localhost:50070 这就是had ...
- 为什么无线信号(RSSI)是负值(转)
源:为什么无线信号(RSSI)是负值 为什么无线信号(RSSI)是负值 答:其实归根到底为什么接收的无线信号是负值,这样子是不是容易理解多了.因为无线信号多为mW级别,所以对它进行了极化,转化为dBm ...
- 初步了解php,实现注册及登录
直接上图(数据库是用wamp实现的) 注册页 html 及 php 代码 登陆页 html 及 php 代码
- TLD视觉跟踪算法(转)
源:TLD视觉跟踪算法 TLD算法好牛逼一个,这里有个视频,是作者展示算法的效果,http://www.56.com/u83/v_NTk3Mzc1NTI.html.下面这个csdn博客里有人做的相关总 ...
- Java解析JSON文件的方法(一)
一.首先需要在Eclipse工程中导入相关的jar包,jar包参见链接:http://yunpan.alibaba-inc.com/share/link/NdA5b6IFK 二.提供一份待解析的jso ...
- 样式(Style)和主题(Theme)资源——主题资源
与样式资源非常相似,主题资源的XML文件通常也放在/res/values 目录下,主题资源的XML文档同样以<resources.../>元素作为根元素,同样使用<style.../ ...
- CSS入门介绍
一.背景 这里将陆续介绍前端CSS中相关知识,先介绍CSS2.1,后续会介绍CSS3的相关属性,通过该系列的文章,希望能给准备转战前端的人员一些帮助,同时也帮助自己梳理知识,文章中如有错误,欢迎指出. ...
- Java数据库连接--JDBC基础知识(操作数据库:增删改查)
一.JDBC简介 JDBC是连接java应用程序和数据库之间的桥梁. 什么是JDBC? Java语言访问数据库的一种规范,是一套API. JDBC (Java Database Connectivit ...