4. 手眼标定实战
手眼标定,说白了就是让机器人知道“眼睛”长在哪。
你想想看,相机看到了一个零件,坐标是 (x, y, z)。但机器人要去抓,它得知道这个点相对于自己基座在哪。中间差的就是相机和机器人之间的那个变换关系。这个关系,就是手眼标定要解的。
我个人习惯把标定分成两步:先搞清楚用哪种构型,再选对方法解方程。别一上来就套代码,构型选错了,后面全白搭。
4.1 两种构型:Eye-in-Hand 与 Eye-to-Hand
先看一张图,把两种构型的核心逻辑理清楚。
两种构型的区别,我用一个表格给你讲清楚:
| 对比项 | Eye-in-Hand | Eye-to-Hand |
|---|---|---|
| 相机安装位置 | 固定在机器人末端(法兰盘) | 固定在工作空间外(如天花板、支架) |
| 标定板位置 | 固定在工作空间内 | 装在机器人末端 |
| 核心求解量 | 相机 → 末端(camTee) |
相机 → 基座(camTbase) |
| 视野特点 | 随末端移动,视野变化大 | 视野固定,覆盖整个工作区 |
| 典型应用 | 小零件抓取、装配 | 大范围定位、码垛 |
我的经验:做高精度抓取(比如电子元件),我倾向用 Eye-in-Hand。相机离目标近,误差小。但要注意,末端移动时标定板不能出视野,数据采集要覆盖整个工作空间。
4.2 数学本质:解 AX = XB
不管哪种构型,手眼标定的数学本质都一样——解 AX = XB。
什么意思?
- A:机器人末端两次位姿之间的变换(从机器人控制器读)
- B:相机两次观测标定板之间的变换(从图像算)
- X:我们要找的手眼关系矩阵
你想想看,机器人动一下,末端位姿变了,相机看到的标定板位姿也变了。这两个变化之间,就藏着 X。
方程很简单,但解起来有讲究。因为 A 和 B 都有噪声,直接求逆会放大误差。
4.3 Tsai 法与 Navy 法
业界最常用的两种解法:Tsai 法和 Navy 法。我当年刚入行时也纠结过选哪个,后来踩过坑才明白。
| 对比项 | Tsai 法 | Navy 法 |
|---|---|---|
| 全称 | Tsai & Lenz 两步法 | Navy 手眼标定法 |
| 核心思路 | 先解旋转,再解平移 | 旋转和平移同时优化 |
| 数据要求 | 至少 2 组位姿,建议 5+ | 至少 3 组位姿,建议 8+ |
| 鲁棒性 | 对旋转噪声敏感 | 整体更稳定 |
| 实现难度 | 简单,有现成代码 | 稍复杂,需 SVD 分解 |
| 我的推荐 | 快速验证、精度要求不高 | 正式部署、高精度场景 |
避坑指南:Tsai 法在旋转角很小时(比如末端只平移没怎么转),解出来的旋转矩阵可能不满足正交性。我曾经在一个项目里用 Tsai 法标定,结果抓取偏差 5mm,换成 Navy 法后降到 1mm 以内。
Navy 法的核心代码片段,我贴出来给你参考:
import numpy as np
def navy_calibration(A_list, B_list):
"""
Navy 法手眼标定
A_list: 机器人末端位姿变换列表 (4x4)
B_list: 相机观测变换列表 (4x4)
返回: X (4x4)
"""
n = len(A_list)
M = np.zeros((3*n, 4))
y = np.zeros((3*n, 1))
for i in range(n):
Ra = A_list[i][:3, :3]
Rb = B_list[i][:3, :3]
ta = A_list[i][:3, 3]
tb = B_list[i][:3, 3]
# 构造线性方程组
M[3*i:3*i+3, :] = np.hstack((Ra - np.eye(3), np.zeros((3,1))))
y[3*i:3*i+3] = ta.reshape(3,1)
# 最小二乘求解
x, _, _, _ = np.linalg.lstsq(M, y, rcond=None)
# 构造 X 矩阵
X = np.eye(4)
X[:3, :3] = ... # 旋转部分需正交化
X[:3, 3] = x[:3].flatten()
return X
注意:上面代码是简化版,实际使用时需要做旋转矩阵正交化(比如用 SVD 投影到 SO(3) 流形上)。完整实现建议用 OpenCV 的
calibrateHandEye() 函数,它内置了多种方法。
4.4 标定板选择
标定板选不好,数据采了也白采。我见过有人用 A4 纸打印棋盘格,结果反光严重,角点检测全飘了。
我的建议:
- 棋盘格:最常用,角点检测稳定。推荐 7×9 或 9×11 格,格子大小 20-40mm。
- 圆点阵:圆心检测精度高,适合高精度标定。但要注意光照均匀,否则边缘检测会偏。
- CharuCo 板:棋盘格 + ArUco 码的结合体。部分遮挡也能检测,适合 Eye-in-Hand 场景(标定板可能出视野)。
| 标定板类型 | 检测方式 | 精度 | 抗遮挡 | 推荐场景 |
|---|---|---|---|---|
| 棋盘格 | 角点检测 | ★★★ | 差 | 通用场景 |
| 圆点阵 | 圆心拟合 | ★★★★ | 中 | 高精度标定 |
| CharuCo | 角点 + 编码 | ★★★ | 好 | Eye-in-Hand |
小技巧:标定板要贴在刚性平板上,不要用软磁铁贴。我吃过亏——软磁铁贴在机器人末端,一加速就变形,标定结果全废。
4.5 数据采集实战流程
数据采集是手眼标定最耗时的环节。我总结了一套流程,照着做基本不会翻车。
- 固定标定板:确保标定板在工作空间内,且不会被机器人撞到。
- 规划采集位姿:让机器人末端走 10-15 个不同位姿。要覆盖:
- 不同高度(Z 轴变化)
- 不同旋转角度(绕 X、Y、Z 各转 30°-60°)
- 不同平移位置(X、Y 方向覆盖工作区)
- 同步记录:在每个位姿,同时记录:
- 机器人末端位姿(从控制器读,通常是 4×4 齐次矩阵)
- 相机图像(保存为图片或直接检测标定板角点)
- 数据检查:检查每张图像中标定板是否完整、角点检测是否准确。有问题的直接删掉。
- 运行标定:用 OpenCV 的
calibrateHandEye()求解。
重要:采集位姿时,相邻两个位姿的旋转角度差不要太小(至少 10°)。否则
A 和 B 矩阵接近奇异,解出来的 X 误差巨大。我见过有人采了 20 组数据,结果位姿几乎没变,标定结果完全不能用。
数据采集的伪代码,我写给你:
# 伪代码:Eye-in-Hand 数据采集
poses = generate_poses(n=15, range_xyz=[-100,100], range_rpy=[-30,30])
data = []
for pose in poses:
# 1. 移动机器人到目标位姿
robot.move_to(pose)
# 2. 读取当前末端位姿(从控制器)
T_ee_base = robot.get_current_pose()
# 3. 拍照并检测标定板
img = camera.capture()
ret, corners = detect_chessboard(img, pattern_size=(7,9))
if ret:
# 4. 计算标定板在相机坐标系下的位姿
T_board_cam = solve_pnp(corners, board_3d_points, camera_matrix, dist_coeffs)
# 5. 保存数据
data.append({
'T_ee_base': T_ee_base,
'T_board_cam': T_board_cam
})
# 6. 构造 A 和 B 矩阵
A_list = []
B_list = []
for i in range(len(data)-1):
A = inv(data[i]['T_ee_base']) @ data[i+1]['T_ee_base']
B = data[i]['T_board_cam'] @ inv(data[i+1]['T_board_cam'])
A_list.append(A)
B_list.append(B)
# 7. 标定
X = cv2.calibrateHandEye(A_list, B_list, method=cv2.CALIB_HAND_EYE_TSAI)
核心要点:标定结果的好坏,70% 取决于数据质量,30% 取决于算法。花时间把数据采好,比调算法参数管用得多。
最后说一句,手眼标定不是一次性的活。换镜头、换相机、机器人拆装后,都要重新标。我一般在项目里留一个标定脚本,现场半小时就能跑完。