4. 2D视觉定位

各位好,欢迎来到第4章。

上一章我们聊了相机标定,把像素和物理世界的映射关系搞清楚了。但标定完,相机拿到一张图,我们怎么知道目标在哪儿?怎么把它的位置告诉机械臂?

这就是2D视觉定位要干的事。说白了,就是从图像里把目标“抠”出来,算出它的中心点坐标。

我个人习惯把这一章叫做“视觉抓取的第一公里”。因为后面所有的位姿精调、抓取规划,都依赖这一步给出的像素坐标。如果这一步偏了,后面再怎么调也是白搭。

4.1 边缘检测:找到物体的“边界线”

图像里,物体和背景之间通常有亮度突变。边缘检测,就是找到这些突变的位置。

我最早做项目时,以为边缘检测越复杂越好。后来发现,对于工业场景,简单的方法往往更可靠。你想想看,产线上光照稳定,背景单一,用经典的Canny算子就足够了。

4.1.1 Canny边缘检测流程

Canny不是一步到位的,它分几步走:

  1. 高斯滤波:先降噪,避免把噪点当成边缘。
  2. 计算梯度:用Sobel算子算出每个像素的梯度强度和方向。
  3. 非极大值抑制:只保留梯度方向上的局部最大值,让边缘变细。
  4. 双阈值检测:设定高低两个阈值,高于高阈值的保留,低于低阈值的丢弃,中间的看是否与高阈值边缘相连。

核心参数:双阈值中的高阈值(通常设为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视觉定位流程。我画了一张图,方便你理解整体脉络:

2D视觉定位完整流程 图像采集 预处理(灰度+滤波) Canny边缘检测 轮廓提取+筛选 中心点计算(矩/外接圆) 输入 相机原始图像 标定参数 输出 目标中心像素坐标 (cx, cy) 轮廓点集

4.5 实战中的避坑指南

最后,分享几个我在实际项目中遇到的坑,希望能帮你少走弯路。

坑1:光照变化导致边缘断裂

我曾经在一条产线上调试,白天和晚上的光照不一样,Canny阈值白天能用,晚上就断边了。后来我加了自适应阈值,或者用直方图均衡化做预处理,才稳定下来。

坑2:多个目标怎么选?

如果画面里有多个工件,我一般按面积排序,取最大的那个。或者根据抓取优先级,先抓离相机中心最近的。

坑3:轮廓点太多,计算慢

一个复杂轮廓可能有上千个点。用CHAIN_APPROX_SIMPLE可以压缩到几十个点,计算矩和外接圆的速度快很多。精度损失可以忽略不计。