Python实现Canny边缘检测算法详解:从原理到代码
本文详细解析Canny边缘检测算法的五大步骤,包括高斯模糊去噪、Sobel梯度计算、非最大值抑制细化边缘、双阈值筛选以及滞后边界跟踪。通过Python代码实现,对比一阶差分与普通梯度算子,展示Canny算法在抑制噪声和细化边缘方面的显著优势,适用于低对比度图像的精确边缘提取。
图像边缘信息主要集中于高频段,因此图像锐化或边缘检测在本质上属于高频滤波操作。微分运算用于求信号变化率,能够增强高频分量;在空域运算中,图像锐化即表现为计算微分。针对数字图像的离散信号,微分运算转化为计算差分或梯度。常见的边缘检测(梯度)算子包括普通一阶差分、Robert算子(交叉差分)和Sobel算子等,这些方法基于寻找梯度强度;而拉普拉斯算子(二阶差分)则基于过零点检测。通过计算梯度并设置阈值,即可得到边缘图像。
Canny边缘检测算子是一种多级检测算法,由John F. Canny于1986年提出,并确立了边缘检测的三大准则:
- 低错误率的边缘检测:算法应尽可能精确地找出图像中所有边缘,同时最大限度地减少漏检和误检。
- 最优定位:检测出的边缘点应精确地位于边缘的中心位置。
- 单一响应:图像中的任何边缘只应被标记一次,且图像噪声不应产生伪边缘。
自Canny算法问世以来,它一直作为标准的边缘检测算法存在,此后也涌现出多种基于Canny的改进算法。时至今日,Canny算法及其变种依然是优秀的边缘检测方案。除非前提条件极为理想,否则很难找到一种边缘检测算子能显著优于Canny算子。
关于各种差分算子及Canny算子的简要介绍,网上资料丰富,此处不再赘述。接下来直接进入Canny算法的具体实现。Canny算法主要包含以下步骤:
1. 高斯模糊。
这一步骤相对简单,类似于LoG算子(高斯-拉普拉斯)中的高斯模糊处理,其主要作用是去除噪声。由于噪声也集中在高频信号中,容易被误识别为伪边缘,应用高斯模糊可有效降低伪边缘的识别率。然而,图像边缘信息同样属于高频信号,因此高斯模糊半径的选择至关重要:半径过大可能导致弱边缘无法被检测到。

2. 计算梯度幅值和方向。
图像边缘可能指向不同方向,因此经典Canny算法曾使用四个梯度算子分别计算水平、垂直和对角线方向的梯度。但通常无需使用四个算子分别计算四个方向。常用的边缘差分算子(如Robert、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算法也能取得良好的效果。

到此,这篇关于Python实现Canny边缘检测算法的文章介绍完毕。更多相关Canny边缘检测算法内容,请搜索本站以前的文章或继续浏览下面的相关文章。希望大家以后多多支持本站!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













