4、边缘检测原理:图像梯度、Sobel算子、Canny算子、OpenMV中的边缘检测函数与参数
边缘检测是机器视觉测量中最基础也最关键的一步。在OpenMV的几何测量任务中,无论是测量工件边长、圆孔直径,还是定位零件角度,第一步都必须从图像中提取出目标的边缘。边缘的本质是图像中像素灰度发生剧烈变化的位置,而边缘检测的核心任务就是找到这些变化的位置并量化其方向和强度。本章将从图像梯度的数学定义出发,逐步讲解Sobel算子与Canny算子的工作原理,最后落到OpenMV中具体的边缘检测函数与参数配置上,帮助你在实际项目中正确选择检测策略。
4.1 图像梯度的数学本质与物理意义
图像在计算机中是一个二维离散矩阵,每个像素点的灰度值记为 f(x, y)。边缘检测的核心是寻找灰度发生剧烈变化的位置,而“变化率”在数学上对应导数。对于离散图像,我们使用差分代替微分:水平方向梯度 Gx = f(x+1, y) - f(x-1, y),垂直方向梯度 Gy = f(x, y+1) - f(x, y-1)。梯度的幅值 |G| = √(Gx² + Gy²) 表示该点灰度变化的剧烈程度,方向 θ = arctan(Gy / Gx) 表示边缘的法线方向。
在实际图像中,噪声会导致相邻像素的灰度随机波动,直接使用上述差分会放大噪声。因此,工程上通常先对图像进行平滑滤波,再计算梯度。OpenMV中常用的做法是使用 image.gaussian(3) 或 image.median(3) 进行预处理,将噪声抑制后再进入边缘检测流程。梯度幅值越大的像素点,越有可能是边缘点,但并非所有高梯度点都是真实边缘——纹理、反光、阴影也会产生高梯度响应,这就是后续需要非极大值抑制和阈值筛选的原因。
img.find_edges 或 img.find_lines 之前,务必先观察图像的灰度直方图。如果目标与背景的灰度差小于30(0-255范围),边缘检测会非常困难,此时应考虑增加辅助光源或调整相机曝光。
4.2 Sobel算子:一阶导数的工程实现
Sobel算子是一种离散的一阶差分算子,它使用两个3×3卷积核分别计算水平方向和垂直方向的梯度。水平核 Gx 在中心行两侧赋予更大权重,垂直核 Gy 在中心列两侧赋予更大权重,这种加权方式既实现了差分,又包含了一定的平滑效果,对噪声的敏感度低于简单差分。
| 方向 | 卷积核 | 检测目标 | 典型应用 |
|---|---|---|---|
| 水平 Gx | [-1 0 +1; -2 0 +2; -1 0 +1] | 垂直边缘(左右灰度突变) | 测量工件左右边界 |
| 垂直 Gy | [-1 -2 -1; 0 0 0; +1 +2 +1] | 水平边缘(上下灰度突变) | 测量工件上下边界 |
| 幅值 |G| | √(Gx² + Gy²) | 任意方向边缘 | 通用边缘检测 |
在OpenMV中,Sobel算子通常不直接暴露为独立函数,而是通过 img.find_lines 或 img.find_circles 内部的梯度计算间接使用。如果你需要查看梯度幅值图,可以手动实现:先复制图像,然后对每个像素计算Sobel响应,最后将幅值映射到0-255范围显示。但考虑到OpenMV的算力有限(STM32F765 Cortex-M7,主频216MHz),逐像素计算Sobel会消耗大量CPU时间,实际项目中更推荐直接使用内置的 find_lines 函数。
4.3 Canny算子:多阶段最优边缘检测
Canny算子是当前工业视觉中最常用的边缘检测算法,它通过五个阶段获得单像素宽、连续且准确的边缘:
- 高斯滤波:使用σ=1.0~1.5的高斯核平滑图像,抑制噪声。
- 梯度计算:使用Sobel算子计算Gx和Gy,得到幅值和方向。
- 非极大值抑制:沿梯度方向检查当前像素是否为局部最大值,若不是则置零,使边缘细化为单像素宽。
- 双阈值筛选:设定高阈值Th和低阈值Tl。幅值高于Th的像素为强边缘,低于Tl的像素被丢弃,介于两者之间的为弱边缘。
- 滞后连接:弱边缘像素如果与强边缘像素相连,则保留;否则丢弃。这保证了边缘的连续性。
OpenMV的 img.find_lines 函数内部实现了类似Canny的流程,但为了在嵌入式平台上实时运行,做了一些简化:它使用固定大小的Sobel核,并通过 threshold 参数控制梯度幅值的截断。需要注意的是,OpenMV的 threshold 参数与Canny的双阈值不同,它只有一个阈值,高于该值的像素才被考虑为边缘候选点。
find_lines 返回的是线段对象(包含起点、终点、长度、角度),而不是边缘像素图。这意味着它已经完成了从边缘像素到几何线段的拟合。如果你需要原始边缘像素,应使用 img.find_edges 函数,它返回一个二值图像,其中边缘像素为白色(255),非边缘为黑色(0)。
4.4 OpenMV边缘检测函数详解与参数配置
OpenMV提供了三个与边缘检测直接相关的函数,它们的适用场景和参数配置各不相同。
| 函数 | 返回值 | 核心参数 | 适用场景 |
|---|---|---|---|
img.find_edges | 二值图像(边缘为白) | threshold(梯度阈值) | 需要原始边缘像素,自定义后续处理 |
img.find_lines | 线段对象列表 | threshold, theta_margin, rho_margin, merge | 直线边缘检测,如工件轮廓、标尺刻度 |
img.find_circles | 圆形对象列表 | threshold, x_margin, y_margin, r_margin, r_min, r_max | 圆形边缘检测,如轴承孔、瓶盖 |
以 find_lines 为例,其参数含义如下:
- threshold:梯度幅值阈值,范围0-255。值越小,检测到的边缘越多,但噪声也越多。建议从100开始调试,根据实际图像调整。
- theta_margin:角度容差(单位:度),用于合并角度相近的线段。默认15度。
- rho_margin:距离容差(单位:像素),用于合并距离相近的线段。默认15像素。
- merge:布尔值,是否合并满足容差条件的线段。设为True可减少重复边缘,但可能合并本应分离的线段。
# OpenMV边缘检测示例代码
import sensor, image, time
sensor.reset()
sensor.set_pixformat(sensor.GRAYSCALE) # 灰度图处理更快
sensor.set_framesize(sensor.QVGA) # 320×240
sensor.skip_frames(time=2000)
sensor.set_auto_gain(False) # 关闭自动增益,保证稳定性
sensor.set_auto_whitebal(False)
clock = time.clock()
while True:
clock.tick()
img = sensor.snapshot()
img.gaussian(3) # 3×3高斯模糊降噪
# 检测直线边缘
lines = img.find_lines(threshold=120, theta_margin=10, rho_margin=10, merge=True)
for line in lines:
# 过滤掉太短的线段(噪声)
if line.length() > 30:
img.draw_line(line.line(), color=255, thickness=2)
print("Line: x1=%d y1=%d x2=%d y2=%d len=%d angle=%d" %
(line.x1(), line.y1(), line.x2(), line.y2(),
line.length(), line.theta()))
print("FPS: %.2f" % clock.fps())
4.5 实战案例:矩形工件边缘检测与尺寸测量
假设我们需要测量一个黑色矩形工件的宽度和高度,工件放置在白色传送带上,相机垂直拍摄。初始现象是:图像中工件与背景灰度差明显(黑色约30,白色约220),但工件表面有轻微划痕,且边缘存在反光。目标是通过边缘检测提取工件的四条边,计算宽度和高度。
第一步:参数初调。设置 threshold=100,运行后发现检测到大量短线段,包括划痕产生的伪边缘。此时将 threshold 提高到150,伪边缘明显减少,但工件边缘也出现断裂。
第二步:预处理优化。在 find_lines 前增加 img.gaussian(3),断裂现象改善。继续将 merge=True 并设置 theta_margin=5、rho_margin=5,边缘线段被正确合并为四条长直线。
第三步:几何计算。获取四条线段后,通过计算两条垂直边之间的平均距离得到宽度,两条水平边之间的平均距离得到高度。具体实现:将线段按角度分类(接近0°为水平,接近90°为垂直),然后计算对应边之间的距离。
结果对比:手动测量工件实际宽度为85.0mm,OpenMV测量结果为84.7mm,误差0.3mm,满足±0.5mm的精度要求。但发现当工件旋转一定角度时,测量误差增大到0.8mm。原因是线段拟合未考虑透视畸变,后续可通过相机标定(第8章内容)进行修正。
find_lines 在图像分辨率超过QVGA(320×240)时,帧率会显著下降,实测VGA(640×480)下帧率仅为QVGA的1/4,实时性要求高的项目应控制分辨率。
4.6 常见误区与调优复盘
误区一:threshold越低越好。很多初学者认为降低阈值可以检测到更多边缘,但实际结果是噪声边缘大量增加,后续几何拟合的稳定性反而下降。正确做法是:先使用较高阈值(如150)确认主要边缘,再逐步降低,直到边缘连续且噪声可接受。
误区二:忽略预处理。直接对原始图像调用 find_lines 是常见错误。OpenMV的传感器在低光照下会产生较多噪点,这些噪点会形成短线段干扰检测。建议始终先执行 img.gaussian(3) 或 img.median(3)。
误区三:merge参数盲目设为True。merge=True虽然能合并相近线段,但也可能将本应分离的物体边缘合并成一条线。例如两个相邻工件的相邻边,如果距离小于rho_margin,会被错误合并。此时应设置 merge=False,并在后续代码中根据线段长度和角度进行筛选。
误区四:忽略角度信息。line.theta() 返回线段的角度(0-180度),这个信息在测量工件旋转角度时非常有用。但需要注意,OpenMV的角度定义与数学坐标系不同,0度表示水平向右,90度表示垂直向下,角度按逆时针增加。在计算实际角度时需根据安装方向进行转换。
复盘总结:边缘检测的调优顺序应为:先保证图像质量(光照、对焦、曝光),再进行预处理(降噪),然后调整边缘检测参数(threshold、merge),最后进行几何拟合。每一步都需要用实际图像验证,而不是凭经验猜测。建议在开发阶段将 img.find_edges 的结果显示在IDE的帧缓冲区中,直观观察边缘提取效果,再切换到 find_lines 进行几何测量。