1. 手眼标定到底在解决什么问题
机械臂抓取这件事,听起来简单——看到目标,伸过去,夹住。但真正上手做过的人都知道,从“相机看到目标”到“机械臂准确到达目标”,中间隔着一道必须跨过去的坎:坐标系转换。相机有相机的坐标系,机械臂有机械臂的坐标系,这两个坐标系之间的变换关系如果不确定,相机告诉机械臂“目标在图像中心偏左30像素”,机械臂根本不知道这30像素对应到自己的世界里是多少毫米、往哪个方向走。
手眼标定要做的,就是求出这两个坐标系之间的刚性变换矩阵。具体来说,是求相机坐标系到机械臂末端坐标系(eye-in-hand)或者相机坐标系到机械臂基座坐标系(eye-to-hand)的变换关系。这个变换矩阵一旦求出来,相机检测到的任何目标位置,都可以通过这个矩阵转换到机械臂坐标系下,进而驱动机械臂去抓取。
OpenCV从4.1版本开始,在calib3d模块中正式提供了calibrateHandEye函数,把原本需要自己手写求解的AX=XB问题封装成了一个API调用。但API调用只是最后一步,真正的工作量在于:数据采集、坐标变换链的构建、标定板位姿计算、以及标定结果的验证。这篇文章我会从原理讲到代码实现,把我在实际项目中踩过的坑和总结的经验都摊开来说。
这篇文章适合谁看?如果你正在做机械臂视觉抓取项目,或者准备入门手眼标定,又或者你已经调用了calibrateHandEye但结果总是不对,那这篇内容应该能帮你省下不少调试时间。我会用Python和OpenCV来演示,但原理部分对C++用户同样适用。
2. 手眼标定的数学原理拆解
2.1 AX=XB到底是怎么来的
手眼标定的核心方程是AX=XB,这个方程看起来简单,但理解它的推导过程对后续调试至关重要。
先定义几个坐标系:机械臂基座坐标系记为B,机械臂末端坐标系记为E,相机坐标系记为C,标定板坐标系记为T。对于eye-in-hand配置(相机固定在机械臂末端),我们要求的是相机坐标系到末端坐标系的变换矩阵X,即X = T_E←C。
假设机械臂运动了两次,第一次末端位姿为T_B←E1,第二次为T_B←E2。相机在两次运动中分别拍摄标定板,得到标定板在相机坐标系下的位姿T_C1←T和T_C2←T。
由于标定板是固定不动的,机械臂基座也是固定不动的,我们可以建立以下关系:
第一次:T_B←E1 · X · T_C1←T = T_B←T(标定板在基座坐标系下的位姿)
第二次:T_B←E2 · X · T_C2←T = T_B←T
两式相等,消去T_B←T:
T_B←E1 · X · T_C1←T = T_B←E2 · X · T_C2←T
整理后得到:
(T_B←E2)^(-1) · T_B←E1 · X = X · T_C2←T · (T_C1←T)^(-1)
令A = (T_B←E2)^(-1) · T_B←E1,B = T_C2←T · (T_C1←T)^(-1),就得到了AX=XB。
这里的A是机械臂末端两次运动的相对变换,B是相机两次观测到的标定板相对变换。X就是我们要解的相机到末端的变换矩阵。
注意:这个推导假设标定板固定不动,机械臂基座固定不动。如果标定板在标定过程中移动了,整个方程就不成立了。这是实际操中最容易犯的错误之一。
2.2 旋转和平移为什么要分开解
AX=XB这个方程不能直接线性求解,因为旋转矩阵有正交约束。OpenCV的calibrateHandEye内部采用了Tsai-Lenz方法或Park-Martin方法等经典算法,这些算法的共同思路是:先解旋转,再解平移。
为什么?因为旋转部分可以转化为一个线性问题。将AX=XB展开:
旋转部分:R_A · R_X = R_X · R_B
平移部分:R_A · t_X + t_A = R_X · t_B + t_X
旋转部分的方程只涉及旋转矩阵,可以通过罗德里格斯变换将旋转矩阵转为旋转向量,然后构建线性方程组求解。一旦R_X求出来了,平移部分的方程就变成了关于t_X的线性方程,直接最小二乘就能解。
这个“先旋转后平移”的策略有一个重要含义:旋转标定的精度直接决定平移标定的精度。如果旋转矩阵解偏了,平移向量再怎么优化也救不回来。所以在实际标定中,机械臂的旋转运动要充分激励,不能只做平移运动。
2.3 为什么至少需要两组运动
从AX=XB的推导可以看出,每两组运动可以构建一个方程。但一个方程不足以唯一确定X,因为旋转矩阵有3个自由度,平移向量有3个自由度,总共6个自由度。每组运动提供的约束数量取决于运动的形式。
实际使用中,OpenCV的calibrateHandEye要求至少提供2组运动(即3个位姿),但强烈建议采集15到20组以上的数据。原因很简单:噪声。机械臂的运动学误差、标定板的角点检测误差、相机内参的标定误差,都会传导到最终结果中。数据量越大,最小二乘的解越稳定。
我个人的经验是:少于10组数据,结果基本不可信;15到20组是比较稳妥的范围;超过30组后精度提升有限,但采集时间成本明显增加。
3. 标定前的准备工作
3.1 硬件配置与坐标系定义
在开始写代码之前,先把硬件和坐标系理清楚。以eye-in-hand为例,典型的硬件配置包括:
- 机械臂(比如UR、Piper、Franka等)
- 相机(RGB或RGB-D,固定在机械臂末端)
- 标定板(棋盘格或ArUco码)
- 计算机(运行标定程序)
坐标系定义必须明确:
| 坐标系 | 符号 | 说明 |
|---|---|---|
| 基座坐标系 | B | 机械臂底座中心 |
| 末端坐标系 | E | 机械臂法兰盘中心 |
| 相机坐标系 | C | 相机光心 |
| 标定板坐标系 | T | 标定板左上角或中心 |
提示:不同品牌的机械臂对末端坐标系的定义可能不同,有的在法兰盘中心,有的在工具尖端。标定前一定要确认机械臂SDK返回的位姿是相对于哪个坐标系的。
3.2 标定板的选择与制作
标定板的选择直接影响角点检测精度。常见的有两种:
棋盘格标定板:OpenCV的findChessboardCorners对棋盘格的支持最成熟,角点检测精度高。缺点是只能提供角点信息,不能提供方向信息,所以标定板不能旋转到太偏的角度。
ArUco标定板:OpenCV的aruco模块可以检测ArUco码,每个码有唯一的ID,能提供方向信息。适合标定板需要大角度旋转的场景。但ArUco码的角点检测精度通常略低于棋盘格。
我的建议是:如果机械臂的运动范围不大,用棋盘格就够了;如果需要大角度旋转来激励旋转轴,ArUco更合适。
标定板的制作要注意:打印精度要高,贴在硬质平板上,平板本身要平整。我见过有人直接把打印纸贴在纸板上,结果纸板受潮变形,标定结果一塌糊涂。
3.3 相机内参标定不能省
calibrateHandEye需要的是标定板在相机坐标系下的位姿,这个位姿是通过solvePnP计算出来的,而solvePnP需要相机内参和畸变系数。所以相机内参标定是手眼标定的前置步骤,不能跳过。
相机内参标定用calibrateCamera函数,采集15到20张不同角度的棋盘格图像,具体流程这里不展开,但有几个要点:
- 棋盘格要覆盖图像的不同区域,包括边缘
- 棋盘格要有不同的倾斜角度
- 畸变系数要标定准确,特别是广角镜头
注意:如果相机内参不准,
solvePnP算出的标定板位姿就会有系统偏差,这个偏差会直接传导到手眼标定结果中。我遇到过内参标定只用了5张图的情况,结果手眼标定怎么调都不对,重新标定内参后问题消失。
4. 数据采集的实操要点
4.1 机械臂位姿的采集与记录
数据采集是整个标定过程中最耗时、也最容易出错的环节。每一组数据需要记录两个东西:机械臂末端位姿和对应的标定板图像。
机械臂末端位姿通常以(x, y, z, rx, ry, rz)的形式给出,其中(x, y, z)是平移,(rx, ry, rz)是旋转向量(罗德里格斯角)或欧拉角。不同品牌的机械臂可能使用不同的旋转表示,需要统一转换成旋转矩阵。
import numpy as np import cv2 def pose_to_matrix(x, y, z, rx, ry, rz): """将机械臂位姿转换为4x4变换矩阵 输入:平移(x,y,z)和旋转向量(rx,ry,rz) 输出:4x4齐次变换矩阵 """ R, _ = cv2.Rodrigues(np.array([rx, ry, rz], dtype=np.float64)) T = np.eye(4) T[:3, :3] = R T[:3, 3] = [x, y, z] return T如果机械臂返回的是欧拉角,需要先转换成旋转矩阵。OpenCV没有直接的欧拉角转旋转矩阵函数,但可以用scipy.spatial.transform.Rotation或者手动构建。
提示:记录位姿时,建议同时记录时间戳和图像文件名,方便后续核对。我习惯用CSV文件记录,每行对应一组数据,包含位姿、图像路径和备注。
4.2 运动轨迹的设计原则
机械臂的运动轨迹不是随便走几个位置就行的。为了让AX=XB有稳定的解,运动需要满足以下条件:
旋转轴要充分激励。如果机械臂只绕一个轴旋转,旋转矩阵的约束就不足,解会不稳定。理想情况下,机械臂应该绕至少两个不同的轴旋转,而且旋转角度要足够大(建议每次旋转15度以上)。
平移运动要覆盖工作空间。平移运动帮助确定平移向量的尺度。如果机械臂只在很小的范围内移动,平移向量的解会有很大的不确定性。
标定板始终在相机视野内。这是最基本的约束,但实际操中很容易忽略。机械臂运动后,标定板可能跑出视野,或者被机械臂自身遮挡。
避免奇异位形。机械臂在奇异位形附近时,运动学解算不稳定,位姿读数可能跳变。采集数据时要避开这些区域。
我通常采用的采集策略是:以标定板为中心,机械臂从不同方向、不同角度“看”标定板,每次运动后确认标定板完整出现在图像中,且角点检测成功。
4.3 图像采集的注意事项
图像采集看似简单,但有几个细节直接影响标定精度:
光照要均匀。标定板上的光照不均匀会导致角点检测偏差。避免强光直射和阴影。
对焦要清晰。模糊的图像会导致角点检测精度下降。如果相机是自动对焦,建议锁定焦距。
曝光要合适。过曝或欠曝都会影响角点检测。建议手动设置曝光,保证标定板黑白分明。
采集时机械臂要静止。机械臂运动过程中采集的图像会有运动模糊,而且位姿读数也不准确。每次运动到位后,等待机械臂完全停止再采集。
注意:我见过有人在机械臂运动过程中连续采集图像,结果标定结果偏差很大。机械臂的位姿读数和图像采集必须严格对应同一时刻。
5. calibrateHandEye的代码实现
5.1 完整代码框架
下面是一个完整的Python实现,使用OpenCV的calibrateHandEye函数:
import numpy as np import cv2 import glob import yaml # ========== 1. 加载相机内参 ========== with open('camera_calibration.yaml', 'r') as f: calib_data = yaml.safe_load(f) camera_matrix = np.array(calib_data['camera_matrix']) dist_coeffs = np.array(calib_data['dist_coeffs']) # ========== 2. 标定板参数 ========== pattern_size = (11, 8) # 棋盘格内角点数 square_size = 0.025 # 方格边长,单位:米 # 生成标定板坐标系下的角点坐标 objp = np.zeros((pattern_size[0] * pattern_size[1], 3), np.float32) objp[:, :2] = np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2) objp *= square_size # ========== 3. 读取机械臂位姿数据 ========== # 假设CSV格式:image_path, x, y, z, rx, ry, rz data = np.loadtxt('robot_poses.csv', delimiter=',', dtype=str) image_paths = data[:, 0] poses = data[:, 1:].astype(np.float64) # ========== 4. 计算每组数据的标定板位姿 ========== R_target2cam = [] t_target2cam = [] R_gripper2base = [] t_gripper2base = [] for i, img_path in enumerate(image_paths): img = cv2.imread(img_path) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 检测棋盘格角点 ret, corners = cv2.findChessboardCorners(gray, pattern_size, None) if not ret: print(f"警告:{img_path} 角点检测失败,跳过") continue # 亚像素精化 criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners_refined = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) # solvePnP计算标定板到相机的变换 ret, rvec, tvec = cv2.solvePnP(objp, corners_refined, camera_matrix, dist_coeffs) if not ret: print(f"警告:{img_path} solvePnP失败,跳过") continue R, _ = cv2.Rodrigues(rvec) R_target2cam.append(R) t_target2cam.append(tvec.flatten()) # 机械臂位姿转变换矩阵 x, y, z, rx, ry, rz = poses[i] R_gb, _ = cv2.Rodrigues(np.array([rx, ry, rz])) R_gripper2base.append(R_gb) t_gripper2base.append(np.array([x, y, z])) # ========== 5. 手眼标定 ========== R_cam2gripper, t_cam2gripper = cv2.calibrateHandEye( R_gripper2base, t_gripper2base, R_target2cam, t_target2cam, method=cv2.CALIB_HAND_EYE_TSAI ) print("相机到末端的旋转矩阵:") print(R_cam2gripper) print("相机到末端的平移向量:") print(t_cam2gripper.flatten()) # ========== 6. 构建完整的变换矩阵 ========== T_cam2gripper = np.eye(4) T_cam2gripper[:3, :3] = R_cam2gripper T_cam2gripper[:3, 3] = t_cam2gripper.flatten() print("相机到末端的变换矩阵:") print(T_cam2gripper)5.2 参数选择与算法对比
calibrateHandEye提供了四种算法:
| 算法 | 常量 | 特点 |
|---|---|---|
| Tsai-Lenz | CALIB_HAND_EYE_TSAI | 经典方法,速度快,对噪声敏感 |
| Park-Martin | CALIB_HAND_EYE_PARK | 精度较高,推荐使用 |
| Horaud | CALIB_HAND_EYE_HORAUD | 对旋转噪声鲁棒 |
| Andreff | CALIB_HAND_EYE_ANDREFF | 线性方法,速度快 |
| Daniilidis | CALIB_HAND_EYE_DANIILIDIS | 对偶四元数方法,精度高 |
我的实测经验是:Park-Martin和Daniilidis的精度最好,Tsai-Lenz在数据质量好时也够用。如果标定结果不理想,可以换几种算法对比,取重投影误差最小的。
5.3 标定结果的验证方法
标定完成后,必须验证结果是否可靠。最直接的方法是重投影验证:
选取一组未参与标定的数据,用标定得到的T_cam2gripper,将标定板在相机坐标系下的位姿转换到机械臂基座坐标系下,然后与机械臂实际位姿对比。如果误差在合理范围内(通常小于2-3毫米),说明标定结果可用。
def validate_hand_eye(T_cam2gripper, R_gripper2base, t_gripper2base, R_target2cam, t_target2cam): """验证手眼标定结果 计算标定板在基座坐标系下的位姿,检查一致性 """ errors = [] for i in range(len(R_gripper2base)): # 标定板在基座坐标系下的位姿 T_gb = np.eye(4) T_gb[:3, :3] = R_gripper2base[i] T_gb[:3, 3] = t_gripper2base[i] T_tc = np.eye(4) T_tc[:3, :3] = R_target2cam[i] T_tc[:3, 3] = t_target2cam[i] T_bt = T_gb @ T_cam2gripper @ T_tc errors.append(T_bt[:3, 3]) errors = np.array(errors) mean_error = np.mean(errors, axis=0) std_error = np.std(errors, axis=0) print(f"标定板位置均值:{mean_error}") print(f"标定板位置标准差:{std_error}") return mean_error, std_error如果标准差很大,说明标定结果不稳定,需要检查数据质量或增加数据量。
6. 常见问题与排查技巧实录
6.1 标定结果偏差大的排查思路
标定结果偏差大是最常见的问题,排查要按以下顺序进行:
第一步:检查相机内参。用标定板重新投影,看重投影误差是否小于0.5像素。如果大于1像素,内参标定有问题。
第二步:检查角点检测。把检测到的角点画在图像上,肉眼确认是否准确。如果角点偏移,检查光照和对焦。
第三步:检查机械臂位姿。确认位姿的旋转表示是否正确(旋转向量还是欧拉角),确认位姿对应的坐标系是否正确。
第四步:检查数据量。少于10组数据基本不可靠,建议15组以上。
第五步:检查运动激励。如果机械臂只做了平移运动,旋转矩阵解不稳定。需要增加旋转运动。
6.2 角点检测失败的常见原因
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 完全检测不到角点 | 标定板不在视野内/光照太暗 | 调整机械臂位置/增加光照 |
| 角点数量不对 | 标定板被遮挡/棋盘格参数错误 | 检查遮挡/核对pattern_size |
| 角点位置偏移 | 镜头畸变/对焦不准 | 重新标定内参/手动对焦 |
| 角点顺序错乱 | 标定板旋转角度过大 | 减小旋转角度/使用ArUco板 |
6.3 机械臂位姿读取的坑
不同品牌的机械臂,位姿的表示方式不同。有的用旋转向量,有的用欧拉角,有的用四元数。而且欧拉角还有不同的旋转顺序(XYZ、ZYX等)。如果搞错了,标定结果会完全错误。
注意:UR机械臂返回的是旋转向量,但有些SDK返回的是轴角。Piper机械臂的位姿定义需要查阅具体文档。建议先用一个已知的简单运动验证位姿转换是否正确。
6.4 标定结果的精度评估
手眼标定的精度通常用重投影误差来评估。具体做法是:用标定结果将标定板在相机坐标系下的位姿转换到基座坐标系,然后与机械臂实际位姿对比。误差来源包括:
- 相机内参误差
- 角点检测误差
- 机械臂运动学误差
- 标定算法误差
实际项目中,1-2毫米的误差是可以接受的,如果要求更高精度,需要更精密的标定板和更稳定的机械臂。
7. 提高标定精度的实战经验
7.1 数据采集的优化策略
数据采集的质量直接决定标定精度。我总结了几条经验:
采集时机械臂要“慢走稳停”。快速运动会导致机械臂振动,位姿读数不稳定。每次运动后等待1-2秒再采集。
标定板要覆盖图像的不同区域。不要总是把标定板放在图像中心,要让它出现在图像的四个角和边缘。这样能更好地约束相机的畸变和标定板的位姿。
旋转角度要足够大。每次旋转至少15度,最好能达到30度以上。旋转角度太小,旋转矩阵的约束不足。
避免共面运动。如果机械臂的所有运动都在同一个平面内,标定结果会有很大的不确定性。要让机械臂在三维空间中充分运动。
7.2 标定板位姿的优化
solvePnP计算标定板位姿时,可以使用SOLVEPNP_ITERATIVE或SOLVEPNP_IPPE等不同方法。对于平面标定板,SOLVEPNP_IPPE通常更稳定。
另外,可以使用cv2.solvePnPRefineLM对标定板位姿进行非线性优化,进一步提高精度。
# 使用IPPE方法求解,然后用LM优化 ret, rvec, tvec = cv2.solvePnP(objp, corners_refined, camera_matrix, dist_coeffs, flags=cv2.SOLVEPNP_IPPE) rvec, tvec = cv2.solvePnPRefineLM(objp, corners_refined, camera_matrix, dist_coeffs, rvec, tvec)7.3 多次标定取平均
如果对精度要求很高,可以多次标定取平均。具体做法是:采集多组数据,分成几个子集,分别标定,然后对结果取平均。这样可以减小随机误差的影响。
但要注意:取平均之前要先剔除异常值。如果某次标定的结果与其他次差异很大,说明那次标定的数据有问题,应该剔除而不是平均。
7.4 标定结果的在线验证
标定完成后,不要急着上生产线。先做在线验证:让机械臂去抓取一个已知位置的目标,看实际抓取位置与预期位置的偏差。如果偏差在允许范围内,再正式使用。
我通常会在标定后做这样一个测试:在标定板上选一个角点,用标定结果计算机械臂应该到达的位置,然后让机械臂实际运动到那个位置,测量偏差。这个偏差就是标定精度的直接体现。
8. 从标定到抓取:完整链路打通
8.1 坐标系变换链的构建
手眼标定只是整个视觉抓取链路中的一环。完整的变换链是:
目标在图像中的像素坐标 → 相机坐标系下的3D坐标 → 机械臂基座坐标系下的3D坐标 → 机械臂运动指令
第一步需要相机内参和深度信息(如果是RGB-D相机)或者平面假设(如果是RGB相机)。第二步需要手眼标定矩阵。第三步需要机械臂逆运动学。
def pixel_to_robot_base(u, v, depth, camera_matrix, T_cam2gripper, T_gripper2base): """将像素坐标转换为机械臂基座坐标系下的3D坐标 u, v: 像素坐标 depth: 深度值(米) """ # 像素坐标转相机坐标系 fx = camera_matrix[0, 0] fy = camera_matrix[1, 1] cx = camera_matrix[0, 2] cy = camera_matrix[1, 2] x_cam = (u - cx) * depth / fx y_cam = (v - cy) * depth / fy z_cam = depth # 相机坐标系转基座坐标系 P_cam = np.array([x_cam, y_cam, z_cam, 1.0]) T_base2cam = T_gripper2base @ T_cam2gripper P_base = T_base2cam @ P_cam return P_base[:3]8.2 抓取位姿的计算
得到目标在基座坐标系下的位置后,还需要确定抓取姿态。对于简单的平行夹爪,抓取姿态通常由目标物体的朝向决定。如果目标物体是任意朝向的,还需要估计物体的6D位姿。
这部分内容超出了手眼标定的范围,但它是手眼标定的直接应用。手眼标定的精度直接影响抓取的成功率。
8.3 实际项目中的调试经验
在实际项目中,手眼标定完成后,通常还需要做一些微调。比如:
- 工具坐标系补偿:如果夹爪的抓取点不在法兰盘中心,需要在变换链中加入工具坐标系。
- 抓取高度补偿:对于平面上的物体,抓取时夹爪需要下降到物体表面以下一定距离。
- 视觉伺服:如果标定精度不够,可以在接近目标时使用视觉伺服进行微调。
我做过的一个项目中,手眼标定的重投影误差在1.5毫米左右,但实际抓取时偏差达到了5毫米。后来发现是工具坐标系没有正确补偿。加入工具坐标系后,抓取偏差降到了2毫米以内。
提示:手眼标定求的是相机到法兰盘的变换,但实际抓取时夹爪的抓取点可能不在法兰盘中心。这个偏差必须补偿,否则标定再准也没用。
9. 不同配置下的标定差异
9.1 eye-in-hand与eye-to-hand的区别
前面讲的都是eye-in-hand配置(相机固定在机械臂末端)。另一种常见配置是eye-to-hand(相机固定在外部,不随机械臂运动)。
eye-to-hand的标定方程略有不同。此时要求的是相机坐标系到机械臂基座坐标系的变换。方程变为:
T_B←E1 · T_E←C · T_C1←T = T_B←E2 · T_E←C · T_C2←T
其中T_E←C是末端到相机的变换(固定不变),需要求解。整理后仍然是AX=XB的形式,但A和B的定义不同。
OpenCV的calibrateHandEye函数同时支持这两种配置,只需要正确设置输入参数即可。对于eye-to-hand,R_gripper2base和t_gripper2base应该替换为R_base2gripper和t_base2gripper。
9.2 不同机械臂的适配要点
不同品牌的机械臂,位姿的表示方式和坐标系定义不同。以下是一些常见机械臂的适配要点:
| 机械臂 | 位姿表示 | 坐标系定义 | 注意事项 |
|---|---|---|---|
| UR | 旋转向量 | 基座在底部中心 | 位姿是法兰盘中心 |
| Piper | 欧拉角 | 基座在底部中心 | 需要确认旋转顺序 |
| Franka | 四元数 | 基座在底部中心 | 需要转换为旋转矩阵 |
| Kinova | 欧拉角 | 基座在底部中心 | 注意角度单位 |
注意:有些机械臂的位姿是相对于工具坐标系的,不是法兰盘。标定前一定要确认清楚。
9.3 相机类型的影响
RGB相机和RGB-D相机在手眼标定中的差异主要在于深度信息的获取。RGB相机需要通过标定板的已知尺寸来推算深度,而RGB-D相机可以直接获取深度。
但RGB-D相机的深度图通常有噪声,而且深度范围有限。如果标定板距离相机太远,深度值可能不准确。建议标定板距离相机在0.3到1.0米之间。
另外,RGB-D相机的深度图和彩色图可能没有对齐,需要先做对齐处理。OpenCV提供了cv2.rgbd.registerDepth等函数,但实际使用中通常用相机的SDK来做对齐。
10. 标定流程的自动化与工程化
10.1 自动化采集脚本的设计
手动采集数据效率低且容易出错。可以写一个自动化脚本,让机械臂自动走一系列预设位姿,在每个位姿处自动采集图像并记录位姿。
def auto_calibration(robot, camera, poses_list, save_dir): """自动化手眼标定数据采集 robot: 机械臂控制对象 camera: 相机对象 poses_list: 预设的机械臂位姿列表 save_dir: 数据保存目录 """ data_records = [] for i, pose in enumerate(poses_list): # 移动机械臂 robot.move_to(pose) time.sleep(1.0) # 等待机械臂稳定 # 读取实际位姿 actual_pose = robot.get_pose() # 采集图像 img = camera.capture() img_path = f"{save_dir}/image_{i:03d}.png" cv2.imwrite(img_path, img) # 记录数据 data_records.append([img_path] + list(actual_pose)) # 保存CSV np.savetxt(f"{save_dir}/robot_poses.csv", data_records, delimiter=',', fmt='%s') return data_records10.2 标定结果的持久化
标定结果需要保存下来,供后续抓取程序使用。推荐用YAML或JSON格式保存,方便读取和修改。
import yaml def save_hand_eye_calibration(T_cam2gripper, filepath): """保存手眼标定结果到YAML文件""" data = { 'rotation_matrix': T_cam2gripper[:3, :3].tolist(), 'translation_vector': T_cam2gripper[:3, 3].tolist(), 'transform_matrix': T_cam2gripper.tolist() } with open(filepath, 'w') as f: yaml.dump(data, f)10.3 标定质量的自动评估
在自动化流程中,可以加入自动评估环节。如果标定质量不达标,自动提示重新采集数据。
def evaluate_calibration(T_cam2gripper, R_gripper2base, t_gripper2base, R_target2cam, t_target2cam, threshold=0.003): """评估标定质量,返回是否通过""" errors = [] for i in range(len(R_gripper2base)): T_gb = np.eye(4) T_gb[:3, :3] = R_gripper2base[i] T_gb[:3, 3] = t_gripper2base[i] T_tc = np.eye(4) T_tc[:3, :3] = R_target2cam[i] T_tc[:3, 3] = t_target2cam[i] T_bt = T_gb @ T_cam2gripper @ T_tc errors.append(T_bt[:3, 3]) errors = np.array(errors) std = np.std(errors, axis=0) max_std = np.max(std) if max_std < threshold: print(f"标定质量合格,最大标准差:{max_std*1000:.2f}mm") return True else: print(f"标定质量不合格,最大标准差:{max_std*1000:.2f}mm") return False11. 我踩过的那些坑
11.1 标定板移动了
这是最隐蔽的坑。标定过程中,标定板必须固定不动。但有时候标定板会被碰到,或者固定不牢导致微小移动。这种移动在单张图像上看不出来,但会导致标定结果完全错误。
解决方法:标定板用夹具固定,标定前后拍照确认标定板位置没有变化。
11.2 机械臂位姿读数延迟
有些机械臂的位姿读数有延迟,即get_pose()返回的是上一次运动后的位姿,而不是当前位姿。如果运动后立即读取,可能读到旧数据。
解决方法:运动后等待足够长时间(1-2秒),或者用机械臂的回调机制确认运动完成。
11.3 相机内参不匹配
如果标定手眼时使用的相机内参与实际相机不匹配(比如换了镜头但没重新标定内参),标定结果会有系统偏差。
解决方法:每次更换镜头或调整焦距后,重新标定相机内参。
11.4 标定板角点检测的亚像素精度
findChessboardCorners返回的角点坐标是整数像素,需要用cornerSubPix做亚像素精化。如果跳过这一步,角点精度只有1像素左右,标定结果会有明显偏差。
解决方法:始终使用cornerSubPix,窗口大小设为(11,11)或(5,5),迭代次数30次以上。
11.5 机械臂运动学误差
工业机械臂的重复定位精度通常很高(0.1毫米级别),但绝对定位精度可能差很多(几毫米)。如果机械臂的绝对定位精度不高,手眼标定的精度也会受限。
解决方法:如果机械臂绝对定位精度不够,可以考虑用激光跟踪仪等外部测量设备辅助标定,或者使用视觉伺服在抓取时进行微调。
12. 标定之后的验证与迭代
手眼标定不是一次性的工作。随着机械臂的使用,机械结构可能会有微小变化,标定结果会逐渐漂移。建议定期(比如每周或每月)重新标定,或者在抓取精度下降时重新标定。
验证标定结果是否仍然有效,最简单的方法是:让机械臂去抓取一个固定位置的目标,看抓取成功率。如果成功率下降,说明标定需要更新。
另外,如果机械臂的工作空间发生了变化(比如更换了底座位置),也需要重新标定。
我在实际项目中的做法是:把标定程序做成一个独立的工具,每次需要标定时运行一遍,10分钟内完成数据采集和标定。这样即使需要频繁标定,也不会占用太多时间。
最后分享一个小技巧:标定完成后,把标定结果和标定时的数据一起存档。如果以后出现问题,可以回溯到标定时的状态,对比分析是标定问题还是其他问题。这个习惯帮我省了很多排查时间。