python Canny边缘检测算法的实现
作者:kuweicai 发布时间:2023-04-23 20:41:55
图像边缘信息主要集中在高频段,通常说图像锐化或检测边缘,实质就是高频滤波。我们知道微分运算是求信号的变化率,具有加强高频分量的作用。在空域运算中来说,对图像的锐化就是计算微分。对于数字图像的离散信号,微分运算就变成计算差分或梯度。图像处理中有多种边缘检测(梯度)算子,常用的包括普通一阶差分,Robert算子(交叉差分),Sobel算子等等,是基于寻找梯度强度。拉普拉斯算子(二阶差分)是基于过零点检测。通过计算梯度,设置阀值,得到边缘图像。
Canny边缘检测算子是一种多级检测算法。1986年由John F. Canny提出,同时提出了边缘检测的三大准则:
低错误率的边缘检测:检测算法应该精确地找到图像中的尽可能多的边缘,尽可能的减少漏检和误检。
最优定位:检测的边缘点应该精确地定位于边缘的中心。
图像中的任意边缘应该只被标记一次,同时图像噪声不应产生伪边缘。
Canny算法出现以后一直是作为一种标准的边缘检测算法,此后也出现了各种基于Canny算法的改进算法。时至今日,Canny算法及其各种变种依旧是一种优秀的边缘检测算法。而且除非前提条件很适合,你很难找到一种边缘检测算子能显著地比Canny算子做的更好。
关于各种差分算子,还有Canny算子的简单介绍,这里就不罗嗦了,网上都可以找得到。直接进入Canny算法的实现。Canny算法分为以下几步。
1. 高斯模糊。
这一步很简单,类似于LoG算子(Laplacian of Gaussian)作高斯模糊一样,主要作用就是去除噪声。因为噪声也集中于高频信号,很容易被识别为伪边缘。应用高斯模糊去除噪声,降低伪边缘的识别。但是由于图像边缘信息也是高频信号,高斯模糊的半径选择很重要,过大的半径很容易让一些弱边缘检测不到。
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 + 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;
}
下图是非最大值抑制的结果。可见边缘宽度已经大大减小。但是这个图像中因为没有应用任何阀值,还含有大量小梯度模值的点,也就是图中很暗的地方。下面,阀值要上场了。
非最大值抑制结果
4. 双阀值。
一般的边缘检测算法用一个阀值来滤除噪声或颜色变化引起的小的梯度值,而保留大的梯度值。Canny算法应用双阀值,即一个高阀值和一个低阀值来区分边缘像素。如果边缘像素点梯度值大于高阀值,则被认为是强边缘点。如果边缘梯度值小于高阀值,大于低阀值,则标记为弱边缘点。小于低阀值的点则被抑制掉。这一步算法很简单。
5. 滞后边界跟踪。
至此,强边缘点可以认为是真的边缘。弱边缘点则可能是真的边缘,也可能是噪声或颜色变化引起的。为得到精确的结果,后者引起的弱边缘点应该去掉。通常认为真实边缘引起的弱边缘点和强边缘点是连通的,而由噪声引起的弱边缘点则不会。所谓的滞后边界跟踪算法检查一个弱边缘点的8连通领域像素,只要有强边缘点存在,那么这个弱边缘点被认为是真的边缘保留下来。
这个算法搜索所有连通的弱边缘,如果一条连通的弱边缘的任何一个点和强边缘点连通,则保留这条弱边缘,否则抑制这条弱边缘。搜索时可以用广度优先或者深度优先算法,我在这里实现了应该是最容易的深度优先算法。一次连通一条边缘的深度优先算法如下:
准备一个栈s,一个队列q,设连通指示变量connected为假。从图像的第一个点开始,进入2。
如果这个点是弱边界点并且没有被标记,把它标记,并把它作为第一个元素放入栈s中,同时把它放入记录连通曲线的队列q,进入3。如果这个点不是弱边界或者已经被标记过,到图像的下一个点,重复2。
从栈s中取出一个元素,查找它的8像素领域。如果一个领域像素是弱边界并且没有被标记过,把这个领域像素标记,并加入栈s中,同时加入队列q。同时查找领域对应的强边界图,如果有一个像素是强边界,表示这条弱边界曲线和强边界联通,设置connected为真。重复3直到栈中没有元素了。如果connected为假,则依次从队列q中取出每个元素,清空标记。如果connected为真,保留标记。
清空队列q,设置connected为假,移动到图像的下一个点,回到2。
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;
}
下面是对Lena图计算Canny边缘检测的梯度模图和二值化图,高斯半径2,高阀值100,低阀值50。
Canny检测梯度模图 Canny检测梯度二值图
作为对比,下面是用一阶差分和Sobel算子对原图计算的结果,阀值100。由于一阶差分的梯度值相对较小,我对一阶差分的梯度值放大了一定倍数,使得它和Sobel的梯度值保持同样的水平。
一阶差分梯度模图 一阶差分梯度二值图
Sobel梯度模图 Sobel梯度二值图
很明显,Canny边缘检测的效果是很显著的。相比普通的梯度算法大大抑制了噪声引起的伪边缘,而且是细化过的边缘,易于后续处理。对于对比度较低的图像,通过调节参数,Canny算法也能有很好的效果。
来源:https://blog.csdn.net/kuweicai/article/details/79463878


猜你喜欢
- 前言本文主要给大家介绍了关于python用MethodType绑定方法到类的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细
- 今天请各位读者朋友欣赏用 Python 实现的鲜花盛宴,你准备好了吗?90 行代码即可实现一棵美丽的鲜花盛开树。小编也是鲜花爱护协会者之一,
- 1 读/写文件NumPy文件读写主要有二进制的文件读写和文件列表形式的数据读写两种形式1、二进制的文件读写save函数是以二进制的格式保存数
- 数据类型是所有开发语言的基础,JavaScript虽然是一个弱类型的脚本语言,但是在数据类型上也有很多讲究的,看了淘宝UED玉伯的一篇文章,
- 1、IIS为一个死循的执行过程设定执行时间(缺省为90秒)超时事件:<%response.buffer=true%><BO
- 最近有个功能需要java与python之间的数据交互,java需要把参数传给python,然后python计算的结果返回给java.于是就写
- 本文详细归纳整理了Python中列表、字典、元组、集合数据结构。分享给大家供大家参考。具体分析如下:列表:shoplist = ['
- 最近一直在研究 Javascript 相关的技术。在《Javascript 高级程序设计》有篇章节着重阐述了优化 Javascri
- <%Class Cls_TemplateDim RegDim PageDim CIDDim SIDDim RuleDim Conten
- Python2中编码相关的问题很是让人蛋疼,特别是中文字符。比如本文所述的中文网页GBK编码的诡异问题。现象例如:盲录職氓聭聵,其实网页里面
- 接触过 Django 的同学都应该十分熟悉它的 ORM 系统。对于 python 新手而言,这是一项几乎可以被称作“黑科技”的特性:只要你在
- 介绍NumPy是Python中用于数值计算的核心包之一,它提供了大量的高效数组操作函数和数学函数。它支持多维数组和矩阵运算,并且可以集成C/
- 本文实例讲述了Python基于回溯法子集树模板解决野人与传教士问题。分享给大家供大家参考,具体如下:问题在河的左岸有N个传教士、N个野人和一
- 今天有点囧a=['XXXX_game.sql', 'XXXX_game_sp.sql', 'XXXX
- 一、判断以下哪些不能作为标识符A、aB、¥aC、_12D、$a@12E、falseF、False答案为:(F、D、B、E)二、输入数,判断这
- 本文实例讲述了Python Tensor FLow简单使用方法。分享给大家供大家参考,具体如下:1、基础概念Tensor表示张量,是一种多维
- 前言之前用过Eel做的桌面应用觉得已经够 * 了,不过由于Eel是调用Chrome,时常出现各种小问题,比如窗口大小设置后有时候不管用,鼠标右键
- WinHttp; // Microsoft WinHTTP Services, version 5.1Alias HTTPREQUEST_P
- 注:以下文章原文来自于Dr Charles Severance 的 《Python for Informatics》11.3 组合查询和抽取
- 作为程序员,我们经常需要对时间进行处理。在 Go 中,标准库 time 提供了对应的能力。本文将介绍 time 库中一些重要的函数和方法,希