4、2D视觉定位:边缘检测、轮廓提取与中心点计算
各位好,欢迎来到第4章。
上一章我们聊了相机标定,把像素和物理世界的映射关系搞清楚了。但标定完,相机拿到一张图,我们怎么知道目标在哪儿?怎么把它的位置告诉机械臂?
这就是2D视觉定位要干的事。说白了,就是从图像里把目标“抠”出来,算出它的中心点坐标。
我个人习惯把这一章叫做“视觉抓取的第一公里”。因为后面所有的位姿精调、抓取规划,都依赖这一步给出的像素坐标。如果这一步偏了,后面再怎么调也是白搭。
4.1 边缘检测:找到物体的“边界线”
图像里,物体和背景之间通常有亮度突变。边缘检测,就是找到这些突变的位置。
我最早做项目时,以为边缘检测越复杂越好。后来发现,对于工业场景,简单的方法往往更可靠。你想想看,产线上光照稳定,背景单一,用经典的Canny算子就足够了。
4.1.1 Canny边缘检测流程
Canny不是一步到位的,它分几步走:
- 高斯滤波:先降噪,避免把噪点当成边缘。
- 计算梯度:用Sobel算子算出每个像素的梯度强度和方向。
- 非极大值抑制:只保留梯度方向上的局部最大值,让边缘变细。
- 双阈值检测:设定高低两个阈值,高于高阈值的保留,低于低阈值的丢弃,中间的看是否与高阈值边缘相连。
核心参数:双阈值中的高阈值(通常设为150-250),低阈值(通常为高阈值的一半或三分之一)。
嗯,这里要注意。双阈值设得太高,边缘会断掉;设得太低,会引入大量噪声。我在项目中遇到过,一个工件表面有轻微划痕,低阈值设到50以下,划痕全被当成边缘了,轮廓提取直接失败。
4.1.2 OpenCV实现
代码其实很简单,几行就搞定:
import cv2
# 读取图像,转为灰度
img = cv2.imread('target.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 高斯滤波,核大小5x5
blurred = cv2.GaussianBlur(gray, (5, 5), 1.5)
# Canny边缘检测
edges = cv2.Canny(blurred, 50, 150)
# 显示结果
cv2.imshow('Edges', edges)
cv2.waitKey(0)
个人建议:先用滑动条调参,找到合适的阈值范围,再写死到代码里。别一上来就硬编码。
4.2 轮廓提取:从边缘到“形状”
边缘检测得到的是二值图,一堆白色像素点。但机械臂需要知道“这是一个圆”或“这是一个矩形”。这就需要轮廓提取了。
轮廓提取,就是把相邻的边缘像素点连起来,组成一个封闭的轮廓。
4.2.1 findContours函数
OpenCV里用findContours来做:
# 从边缘图提取轮廓
contours, hierarchy = cv2.findContours(
edges,
cv2.RETR_EXTERNAL, # 只提取最外层轮廓
cv2.CHAIN_APPROX_SIMPLE # 压缩轮廓点,节省内存
)
这里有两个关键参数:
| 参数 | 选项 | 说明 |
|---|---|---|
| 检索模式 | RETR_EXTERNAL | 只取最外层轮廓,适合抓取场景 |
| 检索模式 | RETR_TREE | 提取所有层级轮廓,适合有孔洞的工件 |
| 近似方法 | CHAIN_APPROX_SIMPLE | 只保留端点,减少数据量 |
| 近似方法 | CHAIN_APPROX_NONE | 保留所有点,精度高但数据量大 |
我曾经踩过的坑:OpenCV版本不同,findContours返回值不一样。旧版本返回2个值(contours, hierarchy),新版本返回3个(image, contours, hierarchy)。建议用contours, _ = cv2.findContours(...)兼容写法。
4.2.2 轮廓筛选
提取出来的轮廓可能有很多,不是每个都要处理。我一般按以下条件过滤:
- 面积:太小的轮廓是噪声,直接丢弃。
- 周长:结合面积可以判断形状的紧凑程度。
- 外接矩形宽高比:排除明显不符合目标形状的轮廓。
# 筛选轮廓
valid_contours = []
for cnt in contours:
area = cv2.contourArea(cnt)
if area < 500: # 面积太小,跳过
continue
x, y, w, h = cv2.boundingRect(cnt)
aspect_ratio = w / h
if 0.5 < aspect_ratio < 2.0: # 宽高比在合理范围
valid_contours.append(cnt)
4.3 中心点计算:给机械臂一个“抓取点”
轮廓有了,接下来要算中心点。机械臂抓取时,通常抓物体的几何中心。
4.3.1 图像矩法
图像矩(Image Moments)是计算轮廓中心最常用的方法。它把轮廓当成一个“质量分布”,算出质心。
# 计算轮廓矩
M = cv2.moments(cnt)
# 防止除零
if M['m00'] != 0:
cx = int(M['m10'] / M['m00'])
cy = int(M['m01'] / M['m00'])
else:
cx, cy = 0, 0
为什么用矩?因为它对轮廓的轻微变形不敏感。工件边缘有一点毛刺,算出来的中心点变化很小。
4.3.2 最小外接圆法
对于圆形或近似圆形的工件,用最小外接圆的圆心更直观:
# 最小外接圆
(x, y), radius = cv2.minEnclosingCircle(cnt)
center = (int(x), int(y))
radius = int(radius)
对比总结:
- 图像矩法:通用性强,适合任意形状。
- 最小外接圆法:适合圆形工件,抗边缘噪声更好。
- 外接矩形中心:计算最快,但精度最低,适合粗定位。
4.4 完整流程与SVG知识图谱
把上面几步串起来,就是完整的2D视觉定位流程。我画了一张图,方便你理解整体脉络:
4.5 实战中的避坑指南
最后,分享几个我在实际项目中遇到的坑,希望能帮你少走弯路。
坑1:光照变化导致边缘断裂
我曾经在一条产线上调试,白天和晚上的光照不一样,Canny阈值白天能用,晚上就断边了。后来我加了自适应阈值,或者用直方图均衡化做预处理,才稳定下来。
坑2:多个目标怎么选?
如果画面里有多个工件,我一般按面积排序,取最大的那个。或者根据抓取优先级,先抓离相机中心最近的。
坑3:轮廓点太多,计算慢
一个复杂轮廓可能有上千个点。用CHAIN_APPROX_SIMPLE可以压缩到几十个点,计算矩和外接圆的速度快很多。精度损失可以忽略不计。