边缘检测
-
- 一、核心原理:变化的度量
- 二、核心步骤(传统方法)
- 三、经典边缘检测算子
- sobel算子
-
- 计算X轴方向梯度
- 计算Y轴方向梯度
- 聚合
- 计算X轴方向梯度
一、核心原理:变化的度量
边缘的本质是图像函数(灰度值、颜色值)的突然变化或不连续性。在数学上,这种“变化”可以通过导数或梯度来度量。
- 一维信号类比:想象一个一维的灰度信号(一条扫描线)。在平坦区域,灰度值恒定,导数为 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算子
1import cv2 2import numpy as np 3import matplotlib.pyplot as plt 4# 输入图像 5img = cv2.imread('lena.jpeg') 6 7# X轴方向算子 8kx = np.array([ 9 [1,0,-1], 10 [2,0,-2], 11 [1,0,-1] 12]) 13 14# Y轴方向Sobel算子 15ky = np.array([ 16 [1, 2, 1], 17 [0, 0, 0], 18 [-1,-2,-1] 19]) 20 21# 展示输入图像 22plt.imshow(img[:, :, ::-1]) 23plt.axis('off') 24
输出

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

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

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

《计算机视觉入门到实战系列(六)边缘检测sobel算子》 是转载文章,点击查看原文。