边缘检测
一、核心原理:变化的度量
边缘的本质是图像函数(灰度值、颜色值)的突然变化或不连续性 。在数学上,这种"变化"可以通过导数 或梯度来度量。
- 一维信号类比 :想象一个一维的灰度信号(一条扫描线)。在平坦区域,灰度值恒定,导数为 0 。在斜坡(灰度渐变)区域,导数为一个非零常数 。在阶跃(灰度突变,即边缘)处,导数会达到一个极值(峰值)。
- 扩展到二维图像 :对于二维图像函数
I(x, y),我们使用梯度(Gradient) 来描述其变化。梯度是一个向量,指向函数值增长最快的方向。- 梯度大小(Magnitude) :表示变化的强度。边缘点处的梯度幅值很大。
- 梯度方向(Direction):垂直于边缘方向,即指向变化最快的方向。
因此,边缘检测的基本任务就是:计算图像中每个像素点的梯度幅值和方向,然后通过阈值等方法找出那些幅值大(变化剧烈)的点,即边缘点。
二、核心步骤(传统方法)
一个完整的传统边缘检测流程通常包括以下几步:
-
滤波(平滑):
- 目的:去除图像中的噪声。因为噪声也是灰度值的剧烈变化,容易被误检为边缘。
- 方法:通常使用高斯滤波等平滑滤波器进行卷积操作。这是一个权衡:滤波太强会模糊边缘,太弱则去噪不彻底。
-
增强:
- 目的:突出像素值变化的区域,为检测边缘做准备。
- 方法 :计算图像的梯度幅值。通过卷积算子(如Sobel、Prewitt)与图像进行卷积,分别得到X方向(水平)和Y方向(垂直)的梯度近似值
Gx和Gy。 - 梯度幅值计算 :
Magnitude = sqrt(Gx^2 + Gy^2)(更精确)- 或为了加快速度使用:
Magnitude ≈ |Gx| + |Gy|
- 或为了加快速度使用:
- 梯度方向计算 :
Theta = arctan(Gy / Gx)
-
检测:
- 目的:找出真正的边缘点。仅仅幅值大还不够,需要确定这个"大"是局部的峰值。
- 关键问题:上一步得到的梯度幅值图,在真正的边缘处会形成一条"山脊",而不是一条单像素宽的细线。
- 方法 :非极大值抑制(Non-Maximum Suppression, NMS) 。这是关键一步,它沿着梯度方向,比较每个像素与其前后两个像素的梯度幅值。只有当该像素的幅值是局部最大值时,才将其保留为候选边缘点,否则将其抑制(设为0)。这样就能得到细化的、单像素宽的边缘线。
-
定位(阈值化与连接):
- 目的:对NMS后的结果进行二值化,区分出强边缘和弱边缘。
- 方法 :双阈值检测 (如Canny算子使用)。
- 设定一个高阈值
T_high和一个低阈值T_low。 - 梯度幅值 >
T_high的点,确定为强边缘点。 - 梯度幅值 <
T_low的点,直接丢弃。 - 梯度幅值在两者之间的点,标记为弱边缘点。
- 边缘连接 :检查弱边缘点,如果它们与任何强边缘点相连(在8邻域内),则认为它们是真正的边缘的一部分,并将其保留。否则丢弃。这一步能有效连接断裂的边缘,同时抑制孤立的噪声点。
- 设定一个高阈值
三、经典边缘检测算子
这些算子本质上是不同的卷积核(模板),用于近似计算图像的梯度。
- Sobel算子 :
- 结合了高斯平滑和微分求导,对噪声有一定的抑制作用。
- 常用3x3的卷积核,分别检测水平和垂直边缘。
- Prewitt算子 :
- 与Sobel类似,但平滑部分的权值不同(都是1),对噪声更敏感一些。
- Roberts算子 :
- 使用2x2的卷积核,通过交叉差分计算梯度。计算简单,但对噪声敏感,且检测的边缘较粗。
- Laplacian of Gaussian (LoG) :
- 先使用高斯滤波器平滑图像,再应用拉普拉斯算子(二阶导数)寻找过零点。
- 对边缘定位更准确,但对噪声也比较敏感。
- Canny算子(公认的最佳传统算子) :
- 不是一个简单的卷积核,而是一个完整的算法流程,严格遵循上述四个步骤。
- 特点:低错误率、高定位精度、单一边缘响应(边缘很细)。它是实际应用中最广泛、最稳定的传统边缘检测方法。
下面我们具体来实现这些边缘检测算子。
sobel算子
python
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 输入图像
img = cv2.imread('lena.jpeg')
# X轴方向算子
kx = np.array([
[1,0,-1],
[2,0,-2],
[1,0,-1]
])
# Y轴方向Sobel算子
ky = np.array([
[1, 2, 1],
[0, 0, 0],
[-1,-2,-1]
])
# 展示输入图像
plt.imshow(img[:, :, ::-1])
plt.axis('off')
输出

-
数学基础:离散导数近似
在连续数学中,导数定义为:f'(x) = lim(Δx→0) [f(x+Δx) - f(x)]/Δx
在离散图像中,我们可以用差分来近似:
Gx ≈ I(x+1, y) - I(x-1, y) (中心差分)
这就是为什么算子中有正负1的原因。
- 你的代码中算子的分解分析
kx(水平方向边缘检测):
[[ 1, 0, -1],
[ 2, 0, -2],
[ 1, 0, -1]]
这实际上结合了两个操作:
- 垂直方向平滑:[1, 2, 1] 作为垂直方向的高斯平滑
- 水平方向差分:[1, 0, -1] 作为水平方向的中心差分
ky(垂直方向边缘检测):
[[ 1, 2, 1],
[ 0, 0, 0],
[-1, -2, -1]]
同样结合了:
- 水平方向平滑:[1, 2, 1] 作为水平方向的高斯平滑
- 垂直方向差分:[1, 0, -1]ᵀ 作为垂直方向的中心差分
- 为什么权重是[1, 2, 1]而不是[1, 1, 1]?
Sobel算子设计的精妙之处在于:
-
中心像素权重更大:中心行(kx)或中心列(ky)的权重是2,而不是1
- 这强调了中心像素的重要性
- 数学上更准确地近似了导数
- 提供了更好的平滑效果
-
平滑与微分的结合:
Sobel_x = 平滑_y * 差分_x Sobel_y = 平滑_x * 差分_y这种分离性使得算子既能够检测边缘,又对噪声有一定的鲁棒性。
假设有一个3×3的图像区域:
[[a, b, c],
[d, e, f],
[g, h, i]]
用你的kx计算水平梯度Gx:
Gx = 1*a + 0*b + (-1)*c +
2*d + 0*e + (-2)*f +
1*g + 0*h + (-1)*i
= (a - c) + 2*(d - f) + (g - i)
这等价于:
- 计算了三行(上、中、下)的水平差分
- 中间行的权重加倍(2倍)
- 然后将三行的结果相加
计算X轴方向梯度
python
# X轴方向Sobel算子与图像进行卷积
conv_x = cv2.filter2D(img, -1, kx)
plt.imshow(conv_x[:, :, ::-1])
plt.axis('off')
输出

可以观察到,沿 X X X轴方向的梯度 I x \boldsymbol{I}_x Ix,也就是垂直方向上的边缘信息被有效检测出,如手臂的线条、帽子等。然后,再计算图像 Y Y Y轴方向的梯度。
计算Y轴方向梯度
python
# Y轴方向Sobel算子与图像进行卷积
conv_y = cv2.filter2D(img, -1, ky)
plt.imshow(conv_y[:, :, ::-1])
plt.axis('off')
输出

可以观察到,沿 Y Y Y轴方向的梯度 I x \boldsymbol{I}_x Ix,也就是水平方向上的边缘信息被有效检测出,如眉毛、嘴巴等。
聚合
将两个方向上的图像聚合
python
E = abs(conv_x) + abs(conv_y)
plt.imshow(E[:, :, ::-1])
plt.axis('off')
