3、眼在手外(Eye-to-Hand)标定:原理、数学模型、标定流程、数据采集方法
眼在手外,说白了就是相机固定不动,机器人带着标定板满世界跑。这种构型在工业现场非常常见——比如流水线上方挂一个相机,机器人去抓取传送带上的工件。我个人觉得,Eye-to-Hand 比 Eye-in-Hand 更容易理解,但坑也不少。
3.1 基本原理
先想清楚一个问题:相机不动,机器人动,我们要找的是谁和谁的关系?
答案是:相机坐标系 {C} 到 机器人基坐标系 {B} 的变换矩阵 ^B_C T。
为什么?因为相机看到了标定板(固定在机器人末端),但机器人只知道自己的末端在哪儿。我们需要把相机看到的物体位置,换算到机器人基坐标系下,才能指导机器人去抓。
核心思想: 相机坐标系 → 机器人基坐标系,就是 Eye-to-Hand 标定要解的方程。
3.2 数学模型
我们一步步推导。先定义几个坐标系:
- {B}:机器人基坐标系
- {E}:机器人末端法兰坐标系
- {C}:相机坐标系
- {P}:标定板坐标系
标定板固定在末端上,所以 ^E_P T 是已知的(或者说,我们假设它不变)。相机拍到标定板,得到 ^C_P T。机器人控制器告诉我们 ^B_E T。
那么,从标定板到机器人基坐标系的变换,可以走两条路:
- 直接走:
^B_P T = ^B_C T · ^C_P T - 绕一圈:
^B_P T = ^B_E T · ^E_P T
两条路结果相等,所以:
^B_C T · ^C_P T = ^B_E T · ^E_P T
整理一下,把未知量 ^B_C T 放到左边:
^B_C T = ^B_E T · ^E_P T · (^C_P T)^{-1}
嗯,这里要注意:^C_P T 是相机测出来的,有噪声。直接求逆会放大误差。所以实际中我们不会只拍一张,而是拍多张,然后优化求解。
我的经验: 至少拍 10-15 个不同位姿。我曾经只拍了 6 张,结果标定出来的旋转矩阵误差大得离谱,后来加了 10 张才稳定下来。
3.3 标定流程
整个流程分五步,我习惯按这个顺序走:
| 步骤 | 操作 | 注意事项 |
|---|---|---|
| 1 | 固定相机,安装标定板 | 标定板要牢固,不能松动 |
| 2 | 机器人走多个位姿 | 覆盖视野各个区域,角度要多样 |
| 3 | 记录每组数据 | 同时保存机器人位姿和相机图像 |
| 4 | 提取角点,计算 ^C_P T |
用 OpenCV 的 solvePnP |
| 5 | 求解 ^B_C T |
用最小二乘或非线性优化 |
你想想看,这五步里最容易出问题的是哪一步?我个人觉得是第二步——机器人位姿的选择。如果所有位姿都集中在视野中央,标定结果会非常脆弱。
避坑指南: 我曾经让机器人只在小范围内移动,结果标定出来的平移向量误差有 5mm。后来把位姿分布到视野的四个角落和中心,误差降到了 0.5mm 以内。
3.4 数据采集方法
数据采集听起来简单,但做起来有很多细节。我总结了一个 checklist:
3.4.1 机器人位姿规划
- 数量: 12-20 组,太少容易欠约束
- 分布: 在相机视野内均匀分布,包括 X、Y、Z 三个方向
- 姿态: 让标定板相对于相机有不同角度,避免所有位姿都平行于像平面
- 距离: 改变标定板到相机的距离,覆盖工作深度范围
3.4.2 图像采集要求
- 清晰度: 标定板角点必须清晰可见,不能模糊
- 光照: 避免反光和阴影,我遇到过标定板一半亮一半暗,角点提取失败
- 遮挡: 标定板不能部分超出视野,也不能被机器人本体遮挡
3.4.3 数据同步
这是个大坑。机器人位姿和相机图像必须严格同步。我见过有人用手动记录,结果时间戳对不上,标定结果完全不对。
我的做法: 写一个脚本,让机器人每到一个位姿就自动触发相机拍照,同时记录当前机器人位姿。这样能保证毫秒级同步。
3.5 知识结构图
下面这张图概括了 Eye-to-Hand 标定的核心逻辑,从输入到输出一目了然:
3.6 代码示例:数据采集与标定
下面是一个简化版的 Python 代码,展示核心流程。实际项目中我会加上异常处理和日志记录。
import numpy as np
import cv2
def collect_data(robot, camera, num_poses=15):
"""
采集 Eye-to-Hand 标定数据
robot: 机器人控制接口
camera: 相机接口
"""
robot_poses = [] # 存储 ^B_E T
board_poses = [] # 存储 ^C_P T
for i in range(num_poses):
# 1. 移动机器人到随机位姿
pose = generate_random_pose()
robot.move_to(pose)
# 2. 记录机器人位姿
robot_poses.append(robot.get_current_pose())
# 3. 拍照并提取角点
img = camera.capture()
ret, corners = cv2.findChessboardCorners(img, (9, 6), None)
if not ret:
print(f"位姿 {i}: 角点提取失败,跳过")
continue
# 4. 计算 ^C_P T
_, rvec, tvec = cv2.solvePnP(obj_points, corners, camera_matrix, dist_coeffs)
board_poses.append(compose_transform(rvec, tvec))
print(f"位姿 {i}: 采集成功")
return robot_poses, board_poses
def calibrate_eye_to_hand(robot_poses, board_poses):
"""
求解 ^B_C T
使用最小二乘法
"""
# 构建方程 A * X = B
# 其中 X = ^B_C T
A_list = []
B_list = []
for i in range(len(robot_poses)):
# ^B_E T * ^E_P T = ^B_C T * ^C_P T
A = board_poses[i] # ^C_P T
B = robot_poses[i] # ^B_E T (假设 ^E_P T 为单位矩阵)
A_list.append(A)
B_list.append(B)
# 使用 SVD 求解
X = solve_ax_xb(A_list, B_list)
return X
小技巧: 代码里的 solve_ax_xb 函数,我推荐用 Tsai 方法或者 Park 方法。OpenCV 的 calibrateHandEye 也支持,但要注意参数顺序——Eye-to-Hand 和 Eye-in-Hand 的输入顺序是反的。
3.7 常见问题与避坑
最后,我把自己踩过的坑整理一下:
- 标定板松动: 有一次标定板没拧紧,结果每次拍照位置都不一样,标定结果自然一塌糊涂。后来我用螺纹胶固定了。
- 机器人精度不够: 如果机器人本身重复定位精度差(比如 ±0.1mm),标定结果很难优于这个量级。别指望标定能弥补机械误差。
- 相机畸变未校正: 我见过有人直接用未校正的图像做标定,结果角点位置偏差很大。先做相机内参标定,这是前提。
- 位姿退化: 如果所有位姿的标定板都平行于像平面,旋转矩阵的某些分量会不可观。一定要让标定板有俯仰和偏转。
重要提醒: 标定完成后,一定要做验证。拿一个已知位置的物体,让机器人去抓,看看误差在不在容忍范围内。我一般要求重投影误差小于 0.5 像素,平移误差小于 1mm。
好了,Eye-to-Hand 标定的核心内容就这些。说白了就是解一个矩阵方程,但工程实现上细节很多。记住:数据质量决定标定质量,别在采集阶段偷懒。