【进阶】Canny边缘检测
[ˈkæni] Canny边缘检测使用一种多级边缘检测算法检测边缘的方法。1986年,John F. Canny发表了著名的论文A Computational Approach to Edge Detection,在该论文中详述了如何进行边缘检测。
1. Canny边缘检测的步骤
Step1:去噪(平滑)。因为噪声会影响边缘检测的准确性,所以首先要将噪声过滤(平滑)掉。
Step2:计算梯度的幅度与方向。
Step3:非极大值抑制,适当地让边缘“变瘦”,达到更好的边缘检测效果。
Step4:使用双阈值算法确定最终的边缘信息。
1.高斯滤波降噪
由于图像边缘非常容易受到噪声的干扰,因此为了避免检测到错误的边缘信息,通常需要对图像进行滤波以去除噪声。滤波的目的是平滑一些纹理较弱的非边缘区域,以便得到更准确的边缘。在实际处理过程中,通常采用高斯滤波去除图像中的噪声。
具体请参考上文所述《第二章:阈值与平滑处理》。
2.计算梯度
上文说过:梯度方向始终是垂直于边缘方向,梯度的模值大小提供了边缘的强度信息。即如下图所示

梯度的方向总是与边缘垂直的,通常就近取值为**水平(左、右)、垂直(上、下)、对角线(右上、左上、左下、右下)**八个不同的方向。因此,**在计算梯度时,我们会得到梯度的幅度和角度(代表梯度的方向)**两个值。
下图展示了梯度的表示方法。其中,每一个梯度包含幅度和角度两个不同的值。为了方便观察,这里使用了可视化表示方法。例如,左上角顶点的值**“2↑”实际上表示的是一个二元数对“(2, 90)”,表示梯度的幅度为2,角度为90°**。

3. 非极大值抑制
在获得了梯度的幅度和方向后,遍历图像中的像素点,去除所有非边缘的点。
在具体实现时:
逐一遍历像素点,
判断当前像素点是否是周围像素点中具有相同梯度方向的最大值,
根据判断结果决定是否抑制该点。
可以推知,该步骤是一个边缘细化的过程。针对每一个像素点:
如果该点是正/负梯度方向上的局部最大值,则保留该点;
if not, 抑制该点(像素值置为0,黑色);
举例:

如上图所示,黑色背景的点都是向上方向梯度(水平边缘)的局部最大值。因此,这些点会被保留;其余点被抑制(处理为0)。也就是说,这些黑色背景的点最终会被处理为边缘点,而其他点都被处理为非边缘点。
总的看来,经过上述处理后,对于同一个方向的若干个边缘点,基本上仅保留了一个,因此实现了边缘细化的目的。
4.双阈值确定边缘
上述步骤完成后,图像内的强边缘已经在当前获取的边缘图像内。但是,一些虚边缘可能也在边缘图像内。这些虚边缘可能是真实图像产生的,也可能是由于噪声所产生的。对于后者,必须将其剔除。所以提出了双阈值这一关键步骤。
设置两个阈值,其中一个为高阈值maxVal,另一个为低阈值minVal。根据当前边缘像素的梯度值(梯度幅度)与这两个阈值之间的关系,判断边缘的属性。
具体步骤为:
如果当前边缘像素的梯度值大于或等于maxVal,则将当前边缘像素标记为强边缘。
如果当前边缘像素的梯度值介于maxVal与minVal之间,则将当前边缘像素标记为虚边缘(需要保留)。
如果当前边缘像素的梯度值小于或等于minVal,则抑制当前边缘像素。
经过以上处理得到虚边缘还不够,还要进行进一步的处理。一般通过判断虚边缘与强边缘是否连接来确定虚边缘到底属于哪种情况,如果一个虚边缘:
● 与强边缘连接,则将该边缘处理为边缘。
● 与强边缘无连接,则该边缘为弱边缘,将其抑制。

上图中显示的是三个边缘信息,右图是对边缘信息进行分类的示意图,具体划分如下:
● A点的梯度值值大于maxVal,因此A是强边缘。
● B和C点的梯度值介于maxVal和minVal之间,因此B、C是虚边缘。
● D点的梯度值小于minVal,因此D被抑制(抛弃)。
对于虚边缘怎么处理呢?

● B点的梯度值介于maxVal和minVal之间,是虚边缘,但该点与强边缘不相连,故将其抛弃。
● C点的梯度值介于maxVal和minVal之间,是虚边缘,但该点与强边缘A相连,故将其保留。
2.Canny函数及使用
1. Canny函数
OpenCV提供了函数cv2.Canny()来实现Canny边缘检测,其语法形式如下:
edges = cv2.Canny(image, threshold1, threshold2, [, apertureSize, [L2gradient]])
● edges:函数返回值,即计算得到的边缘图像。
● image:输入图像。
● threshold1:第一个阈值。
● threshold2:第二个阈值。
2. 程序演示
"""
Author: Will Wang
Email: WillWang1998@163.com
"""
import cv2
import numpy as np
image = cv2.imread('lena.jpg', cv2.IMREAD_GRAYSCALE)
image = cv2.resize(image, (512, 512))
canny1 = cv2.Canny(image, 128, 200)
canny2 = cv2.Canny(image, 32, 128)
img = np.hstack((image, canny1, canny2))
cv2.imshow('image stack: image,canny1,canny2', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
运行效果如下图:

从运行结果可以看出,当函数cv2.Canny()的参数threshold1和threshold2的值较小时,能够捕获更多的边缘信息。