Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

边缘检测

图像中的边缘是分隔两个对象的边界。 因此,边缘检测对于识别或测量对象,或分割图像非常有用。

使用导数的优势

边缘的特征是像素强度的快速变化。 Figure 1 表示了图像中沿水平线的亮度剖面。 可以清楚地看到,工业部件的轮廓显示出像素亮度的突然下降。

一张图像及其上的亮度剖面。

Figure 1:一张图像及其上的亮度剖面。

从这个例子可以看出,求导是突出边缘的有效工具: 可以通过分析垂直于边缘的强度剖面的一阶导数来检测边缘。 同样,也可以通过确定二阶导数的零交叉点来检测边缘。

 的剖面及其导数。

Figure 2:Figure 1 的剖面及其导数。

因为图像依赖于两个维度,所以必须根据两个轴计算导数。 例如,图像 ff 的一阶导数由两个项组成: f(x,y)x\frac{\partial f(x,y)}{\partial x}f(x,y)y\frac{\partial f(x,y)}{\partial y}。 此外,在数字图像的情况下,导数是离散差分。

因此,一阶导数,也称为“梯度”,由偏导数向量定义:

f=(gxgy)=(fxfy)\nabla f = \begin{pmatrix} g_x \\ g_y \end{pmatrix} = \begin{pmatrix} \frac{\partial f}{\partial x} \\ \frac{\partial f}{\partial y} \end{pmatrix}

使用离散前向差分,可以近似为:

f(x,y)(f(x+1,y)f(x,y)f(x,y+1)f(x,y))\nabla f(x,y) \approx \begin{pmatrix} f(x+1,y) - f(x,y) \\ f(x,y+1) - f(x,y) \end{pmatrix}

同样,二阶导数,称为“拉普拉斯算子”,由二阶偏导数之和定义:

Δf=2f=2fx2+2fy2\Delta f = \nabla^2 f = \frac{\partial^2 f}{\partial x^2} + \frac{\partial^2 f}{\partial y^2}

使用中心差分可以近似为:

Δf[f(x+1,y)2f(x,y)+f(x1,y)]+[f(x,y+1)2f(x,y)+f(x,y1)]\Delta f \approx [f(x+1,y) - 2f(x,y) + f(x-1,y)] + [f(x,y+1) - 2f(x,y) + f(x,y-1)]

梯度算子

梯度算子是用于检测边缘的非常简单的方法。 它们使用一阶导数,并且可以通过卷积来计算。 实际上,图像 ff 沿 xx 轴的一阶导数可以写成卷积积的形式:

f(x+1,y)f(x,y)=mnhx(m,n)f(xm,yn)f(x+1,y) - f(x,y) = \sum_m \sum_n h_x(m,n) f(x-m,y-n)

其中 hxh_x 是一个卷积核,使得:

  • hx(0,0)=1h_x(0,0) = -1,

  • hx(1,0)=+1h_x(-1,0) = +1,

  • 并且在其他地方 hx(m,n)=0h_x(m,n) = 0

因此,核 hxh_x 是:

hx=(+1100)h_x = \begin{pmatrix} +1 & -1 \\ 0 & 0 \\ \end{pmatrix}

同样,沿 yy 轴的一阶导数写为 ff 与核 hyh_y 的卷积:

hy=(+1010)h_y = \begin{pmatrix} +1 & 0 \\ -1 & 0 \\ \end{pmatrix}

注意,hxh_x 中的一行0和 hyh_y 中的一列0使得核具有相同的大小。 这两个核是非常基础的梯度算子,在实践中,会使用它们的变体。

罗伯茨算子

罗伯茨算子是沿着对角线计算的 [Roberts 1965]:

hx=(+1001)hy=(0+110)h_x= \begin{pmatrix} +1 & 0 \\ 0 & -1 \\ \end{pmatrix} \quad h_y= \begin{pmatrix} 0 & +1 \\ -1 & 0 \\ \end{pmatrix}

普鲁伊特算子

另一个变体是普鲁伊特算子 [Prewitt 1970],它处理的是奇数大小的核:

hx=(+101+101+101)hy=(+1+1+1000111)h_x= \begin{pmatrix} +1 & 0 & -1 \\ +1 & 0 & -1 \\ +1 & 0 & -1 \\ \end{pmatrix} \quad h_y= \begin{pmatrix} +1 & +1 & +1 \\ 0 & 0 & 0 \\ -1 & -1 & -1 \\ \end{pmatrix}

索伯算子

最后,索伯算子 [Sobel 1968] 是普鲁伊特算子的平滑版本。 实际上,这些系数再现了高斯滤波器的卷积效果, 这倾向于起到均值滤波器的作用以衰减噪声:

hx=(+101+202+101)hy=(+1+2+1000121)h_x= \begin{pmatrix} +1 & 0 & -1 \\ +2 & 0 & -2 \\ +1 & 0 & -1 \\ \end{pmatrix} \quad h_y= \begin{pmatrix} +1 & +2 & +1 \\ 0 & 0 & 0 \\ -1 & -2 & -1 \\ \end{pmatrix}

注意,与罗伯茨和普鲁伊特算子一样,元素的总和为零。

Figure 3 中,可以看到 hxh_x 检测水平边缘。 例如,时钟的顶部和底部被清晰地检测到。 顶部边缘是白色的,因为它对应于从黑到白的边缘。 相反,底部边缘是黑色的。 另一方面,hyh_y 检测垂直边缘。

水平和垂直索伯算子。

Figure 3:水平和垂直索伯算子。

幅度与角度

从上面的算子中,我们还定义了:

  • 边缘的幅度,可以解释为水平和垂直索伯算子的“融合”:

    M=(hxf)2+(hyf)2M = \sqrt{ (h_x*f)^2 + (h_y*f)^2 }

    Figure 4 中,无论其方向如何,所有边缘都以相同的颜色出现。

  • 边缘的角度:

    A=arctan(hyfhxf)A = \arctan \left( \frac{h_y*f}{h_x*f} \right)

    Figure 4 中,边缘的颜色对应于角度。 例如,角度为 0 是红色,角度为 π/2\pi/2 是蓝色。

索伯算子的幅度和角度。

Figure 4:索伯算子的幅度和角度。

阈值处理

在某些情况下,检测最重要的边缘会很有用。 为此,可以对幅度进行阈值处理,只保留梯度较大的值。 正如您在 Figure 5 中所看到的,阈值处理使秒针消失了。 实际上,秒针的幅度低于图像的其他部分,因为秒针是灰色的而不是黑色的。

对索伯算子的幅度进行阈值处理使秒针消失了!

Figure 5:对索伯算子的幅度进行阈值处理使秒针消失了!

噪声的影响

图像中的噪声主要带来像素间亮度的巨大变化。 因此,基于导数的梯度算子对噪声非常敏感, 如 Figure 6 所示。 因此,在边缘检测之前对图像进行去噪可能很有用。 在图 Figure 7 中,在索伯滤波器之前使用了一个小的均值滤波器: 结果比不使用均值滤波器时清晰得多。

噪声对亮度剖面及其导数的影响
(橙色:无噪声剖面,蓝色:有噪声剖面)。

Figure 6:噪声对亮度剖面及其导数的影响 (橙色:无噪声剖面,蓝色:有噪声剖面)。

左:有噪声的图像。中:索伯算子(幅度)。右:在 3\times3 均值滤波器的结果上应用索伯算子。

Figure 7:左:有噪声的图像。中:索伯算子(幅度)。右:在 3×33\times3 均值滤波器的结果上应用索伯算子。

高级方法

继梯度算子之后,人们提出了新的方法来改进边缘检测, 这些方法考虑了噪声和边缘的性质:

Marr-Hildreth检测器

Marr-Hildreth检测器包括:

  1. 对图像 ff 应用高斯滤波器 gg 以减少噪声(这类似于均值滤波器),

  2. 在平滑后的图像上计算拉普拉斯算子(二阶导数) \ell (这是通过卷积实现的),

  3. 确定结果的零交叉点。

由于 (gf)=(g)f\ell*(g*f) = (\ell*g)*f,前两个步骤合并为单个与 g\ell*g 的卷积。 因此,滤波器 g\ell*g 是高斯的二阶导数:

(g)(x,y)=[x2+y22σ2σ4]exp(x2+y22σ2).(\ell*g)(x,y) = - \left[\frac{x^2+y^2-2\sigma^2}{\sigma^4}\right] \exp\left(-\frac{x^2+y^2}{2\sigma^2}\right).

滤波器 g\ell*gFigure 8 中表示。 它也因其与墨西哥草帽的相似性而被称为“LoG”(高斯拉普拉斯算子)或墨西哥帽。

高斯拉普拉斯算子(左:作为图像,右:沿轴的剖面)。

Figure 8:高斯拉普拉斯算子(左:作为图像,右:沿轴的剖面)。

LoG卷积产生的图像中的零交叉点是通过搜索两个像素强度符号的变化来找到的。 Figure 9 给出了一个例子。

Marr-Hildreth检测器。
左:原始图像,
中:LoG滤波器的结果,
右:零交叉点的检测。

Figure 9:Marr-Hildreth检测器。 左:原始图像, 中:LoG滤波器的结果, 右:零交叉点的检测。

Canny检测器

根据Canny的说法,一个好的检测器应满足以下目的:

  • 应找到所有边缘,

  • 虚假响应应最少,

  • 边缘必须被正确定位(即检测到的点与边缘的真实点之间的距离必须尽可能小),

  • 检测到的边缘厚度必须为1像素(因此每个真实边缘点只能检测到一个点)。

Canny将这些目标用数学形式表达出来,并提出了验证这些目标的最优解。 Canny检测器算法遵循下面详述的四个步骤。

  1. 首先用高斯滤波器平滑图像 ff 以减少噪声。 这是通过与高斯核 gg 进行卷积来完成的,以获得图像 z=fgz = f*g

  2. 计算图像的梯度(幅度和角度):

    M=(hxz)2+(hyz)2A=arctan(hyzhxz)M = \sqrt{ (h_x*z)^2 + (h_y*z)^2 } \quad\text{和}\quad A = \arctan \left( \frac{h_y*z}{h_x*z} \right)
  3. 从幅度中移除所有非极大值。 这意味着 MM 中过大的轮廓被更细的轮廓所取代。 这个过程的示意图在 Figure 11 中给出。

    非极大值抑制示意图。

    Figure 11:非极大值抑制示意图。

    为此,我们应用以下算法:

    1. 对于 MM 中的每个像素 (x,y)(x,y)

      1. 选择最接近 A(x,y)A(x,y) 的方向(垂直、水平或两个对角线之一)

      2. 如果 M(x,y)M(x,y) 低于其在所选方向上的邻居之一,则取消梯度:M(x,y)=0M(x,y)=0

  4. 最后一步包括对坏边缘进行滞后阈值处理。 这个过程的示意图在 Figure 12 中给出。

    滞后阈值处理示意图。

    Figure 12:滞后阈值处理示意图。

    因此定义了两个阈值(Thigh>TlowT_\text{high} > T_\text{low})并应用以下算法:

    1. 对于 MM 中的每个像素 (x,y)(x,y)

      1. 如果 M(x,y)>ThighM(x,y) > T_\text{high}(x,y)(x,y) 是一个边缘

      2. 如果 Tlow<M(x,y)<ThighT_\text{low} < M(x,y) < T_\text{high}(x,y)(x,y) 是一个边缘,当且仅当它是边缘像素的邻居

      3. 如果 M(x,y)<TlowM(x,y) < T_\text{low}(x,y)(x,y) 不是一个边缘

Figure 13 给出了一个Canny边缘检测的例子。

Canny检测器。

Figure 13:Canny检测器。

比较

Figure 14 显示了在一张图像上Sobel、Marr-Hildreth和Canny检测器的结果。

Sobel、Marr-Hildreth和Canny检测器的比较。

Figure 14:Sobel、Marr-Hildreth和Canny检测器的比较。

Figure 15 中,比较了Marr-Hildreth和Canny检测器, 可以看出用Canny检测器检测到的边缘定位得更好。

Marr-Hildreth和Canny检测器之间的比较(放大)。
边缘以绿色显示。

Figure 15:Marr-Hildreth和Canny检测器之间的比较(放大)。 边缘以绿色显示。

代码示例

这个例子演示了如何使用 scikit-image 进行Canny边缘检测。我们将使用库中的一个标准样本图像。

import numpy as np
import matplotlib.pyplot as plt
from skimage import feature
from skimage import data

# 配置中文字体支持
plt.rcParams['font.sans-serif'] = [
    'Noto Sans CJK SC', 'Noto Sans CJK JP', 'SimHei',
    'Microsoft YaHei', 'WenQuanYi Micro Hei', 'DejaVu Sans'
]
plt.rcParams['axes.unicode_minus'] = False

# 加载一个样本图像
image = data.camera()

# 计算两个sigma值的Canny滤波器
edges1 = feature.canny(image)
edges2 = feature.canny(image, sigma=3)

# 显示结果
fig, (ax1, ax2, ax3) = plt.subplots(nrows=1, ncols=3, figsize=(8, 3), sharex=True, sharey=True)

ax1.imshow(image, cmap=plt.cm.jet)
ax1.axis('off')
ax1.set_title('有噪声的图像', fontsize=20)

ax2.imshow(edges1, cmap=plt.cm.gray)
ax2.axis('off')
ax2.set_title(r'Canny滤波器, $\sigma=1$', fontsize=20)

ax3.imshow(edges2, cmap=plt.cm.gray)
ax3.axis('off')
ax3.set_title(r'Canny滤波器, $\sigma=3$', fontsize=20)

fig.tight_layout()
plt.show()

<Figure size 800x300 with 3 Axes>