使用OpenCV+Python进行Canny边缘检测
重磅干货,第一时间送达
如果我们环顾房间,我们会看到大量的物体,每一个都很容易区分,并有自己独特的边缘。我们区分物体的先天能力部分来自于我们的视觉系统检测边缘的能力。检测边缘是视觉的一项基本任务,尽管没有它我们不会完全失明,但以前区分物体的简单任务将变得非常具有挑战性。电脑也是类似的,计算机要检测物体,首先需要识别边缘。
接下来让我们进入 Canny 边缘检测器。
Canny 边缘检测器是一种用于识别图像边缘的算法,。它由 John F. Canny 于 1986 年开发,此后一直被广泛使用。今天,我们将在开放的 Python 计算机视觉库(OpenCV-python)的帮助下,详细探讨 Canny 边缘检测器
让我们首先从初始设置开始。
向我们的 python 文件添加两个依赖项:
import cv2 as cv
from matplotlib import pyplot as plot
第一个导入是 OpenCV python,这是我们将用来生成 Canny 边缘和一些补充图像的库。我们还将使用 matplotlib 来显示我们的图像。
首先,这是我们将要查找的 Canny 边缘的图像:
Canny 边缘算法需要灰度图像才能正常运行,我们可以在 OpenCV 中将 RGB 图像读取为灰度,如下所示:
# helper function to easily display our images
def img_show(title, image):
plot.title(title)
plot.xticks([])
plot.yticks([])
plot.imshow(image, cmap="gray")
plot.show()
# read in our original image as grayscale
img = cv.imread("original@2x.jpg", cv.IMREAD_GRAYSCALE)
# show grayscale image using our helper function
img_show("Grayscale Image", img)
我们来创建一个非常简单的辅助函数,用于使用 matplotlib 显示我们的图像。使用这个函数,我们可以查看我们的灰度图像:
现在我们已经完成了初始设置,接下来让我们深入研究 Canny 边缘算法!
Canny边缘检测算法包括五个步骤:
高斯滤波
确定强度梯度
非极大值抑制
双阈值
滞后边缘跟踪
我将详细解释每个步骤。
我们可能听说过正态分布或高斯分布,这种分布在自然界中始终存在,常用于表示实值随机变量。
在图像处理中,可以对图像应用高斯滤波器以减少噪声,模糊的图片可以直观地观察到这个效果。
由于 Canny 边缘算法使用导数来寻找图像的强度梯度,因此非常容易受到噪声的影响。因此,我们通过对图片应用高斯滤波器来去除噪声。如果我们不去除噪声,算法可能会将图像中的噪声块误认为边缘并错误地标记它们。
OpenCV 使用 sigma = 1 的 5x5 高斯核作为降噪步骤。我已经创建了这个内核的 3D 可视化,可以在下面看到。当应用于我们的图像时,还包含了此过滤器的效果。
尽管高斯滤波图像可能与原始灰度图像相同,但仔细观察会发现轻微的模糊,尤其是在棕榈叶的边缘周围。我们不想过多地模糊图像;否则,我们可能会丢失图像的细节。
我们可以使用以下代码轻松生成此图像:
# blurring the image with a 5x5, sigma = 1 Guassian kernel
img_blur = cv.GaussianBlur(img, (5, 5), 1)
在对图像进行平滑处理后,Canny 边缘算法的第二步是找到图片的强度梯度。
尽管“强度梯度”这个名词可能听起来很复杂,但它只是指边缘的方向。一条边实际上可以指向任何方向,但该算法只查看四个方向以简化事情。方向是水平、垂直和两个对角线方向。在数学中,我们将其写为 [0 ° , 90 ° , 45 ° , 135 ° ]。
OpenCV 使用 3x3 Sobel 内核来确定水平方向的导数,然后将其转置以确定垂直方向的导数,这些导数可用于在所需的四个方向上找到我们的边缘。
与高斯核一样,我们也可以在 3D 中可视化 Sobel 核。下边还包括了 Sobel 过滤图像。
在代码中,我们可以按如下方式生成 Sobel 过滤后的图像:
# obtaining a horizontal and vertical Sobel filtering of the image
img_sobelx = cv.Sobel(img_blur, cv.CV_64F, 1, 0, ksize=3)
img_sobely = cv.Sobel(img_blur, cv.CV_64F, 0, 1, ksize=3)
# image with both horizontal and vertical Sobel kernels applied
img_sobelxy = cv.addWeighted(cv.convertScaleAbs(img_sobelx), 0.5, cv.convertScaleAbs(img_sobely), 0.5, 0)
一旦我们找到了强度梯度,我们就可以使用它们来细化我们的边缘。在这一步之后,结果是一个二值图像,这意味着图像将只包含两种颜色,黑色和白色。同样,非最大抑制这个名字听起来很复杂,实际上这是一个简单的操作。
我们通过检查每个像素在其梯度方向上的相邻像素来确定它是否具有最大强度,从而对每个像素应用非最大抑制。如果像素是最大的,那么我们将其值设置为 1。如果不是,这意味着像素的相邻像素具有更高的强度,我们将其值设置为 0(抑制它)。
有一个小问题:并非所有边缘都准确地代表了图像的真实边缘。许多假边缘是由噪声和轻微的颜色变化造成的。尽管该算法的第一步是去除噪声,但并非所有噪声都被去除,这是因为选择 5x5 高斯滤波器是一种折中处理。过滤器去除了大部分明显的噪声,但不会去除太多。这就是双重阈值发挥作用的地方。
我们首先选择两个阈值:最小值和最大值。这就是我们所谓的双阈值。接下来,我们将每个边缘的强度梯度与两个阈值进行比较,如果边缘的强度梯度大于最大阈值,则将其标记为强(或确定)边缘;相反,如果边缘的强度梯度小于最小阈值,则将其丢弃。但是,如果边缘的强度梯度介于最小和最大阈值之间,则将其标记为弱边缘。
请参考下图:
阈值区域图
绿色区域是强度梯度高于最大阈值的地方,这意味着该区域内的任何边缘都被归类为强边缘。类似地,可以在蓝色区域内找到弱边缘,因为该区域位于我们的两个阈值之间。红色区域代表梯度低于我们的最小阈值的边缘,因此,该区域内的任何边缘都将被丢弃。
到目前为止,我们已经确定了两种类型的边缘:弱边和强弱。我们知道强边缘是我们选择最大阈值的最终结果的一部分,但是,我们不太确定如何处理弱边缘。Canny 边缘算法使用假设来简化事情,它假设连接到强边的弱边本身就是强边,同样,未连接到强边缘的弱边缘是噪声或颜色变化。在阈值化的上下文中,这就是滞后的含义。
现在,让我们在第 4 步的图表中添加两条曲线,每条曲线代表一条边的梯度值。
添加了渐变曲线的阈值区域图
由线段 A 和 C 组成的顶部曲线穿过绿色和蓝色区域,我们知道绿色区域的边缘强,蓝色区域的边缘弱。因此,段A是强边缘,段C是弱边缘。通过应用滞后边缘跟踪,我们将 C 标记为强边缘,因为它连接到 A。
现在让我们来看底部曲线,该曲线由段 B 和 D 组成。由于 B 低于最小阈值,它应该已经被抑制了。然而,D 是一个弱边缘,这意味着它需要进行判断。通过使用滞后边缘跟踪,我们可以看到 D 没有连接到任何通过绿色区域的线段,这意味着它没有连接到任何强边,因此它被丢弃。
这很好,但是算法如何准确地知道弱边是否连接到强边?Canny 边缘算法通过考虑每个弱边缘像素及其周围的 8 个相邻像素来确定这一点。如果其相邻像素中的任何一个是强边缘的一部分,则认为它连接到强边缘。因此,该像素保留在我们的最终结果中。相反,如果相邻像素都不是强边缘,则假定它不是强边缘的一部分,因此被抑制。
对我们来说非常容易的是,我们实际上不需要执行任何这些步骤来生成我们的 Canny 边,OpenCV 将它们捆绑到一个名为 Canny() 的函数中。
通过编写以下代码,我们可以很容易地在图像上调用此函数:
# finally, generate canny edges
# extreme examples: high threshold [900, 1000]; low threshold [1, 10]
img_edges = cv.Canny(img, 50, 100)
经过一些实验,我们分别选择50和100作为我们的低阈值和高阈值。但是,我们还可以尝试不同的方法,也许会发现其他阈值会产生更好的结果!
我们最后的 Canny 边缘
参考文献:
https://en.wikipedia.org/wiki/Canny_edge_detector
https://docs.opencv.org/master/da/d22/tutorial_py_canny.html
https://scikit-image.org/docs/dev/auto_examples/filters/plot_hysteresis.html
https://unsplash.com/photos/Ow-MNAWRBW4
Github代码连接:
https://github.com/SeanDuttonJones/opencv-canny-edge