1. 手眼标定基础概念与准备工作
手眼标定是机器人视觉领域的关键技术,特别是在"眼在手上"(Eye-in-Hand)配置中。简单来说,就是确定相机安装在机械臂末端时,相机坐标系与机械臂末端坐标系之间的精确变换关系。想象一下,这就像给你的机械臂装上了一双"眼睛",但需要明确这双眼睛看到的世界和机械臂"身体"之间的空间对应关系。
在实际操作前,你需要准备以下硬件和软件环境:
硬件部分:
- 六轴工业机械臂(如UR5、KUKA等)
- 相机(建议使用工业级USB或GigE相机)
- 棋盘格标定板(建议使用6x4或9x6的黑白棋盘)
- 稳定的工作台和照明环境
软件环境:
- Python 3.6+
- OpenCV 4.0+(建议4.5.5版本)
- NumPy和transforms3d库
- 机械臂控制SDK(根据品牌选择)
我推荐使用Anaconda创建虚拟环境:
conda create -n handeye python=3.8 conda activate handeye pip install opencv-python==4.5.5.64 numpy transforms3d2. 数据采集:机械臂位姿与图像同步获取
数据采集是手眼标定的第一步,也是最容易出错的环节。这里有个经验法则:至少需要15组不同位姿的数据,且机械臂末端姿态变化要足够大(建议旋转角度>30度)。
具体操作流程如下:
- 固定标定板:将棋盘格标定板固定在机械臂工作范围内,确保它在所有采集位姿下都能被相机完整看到
- 编写机械臂控制脚本:控制机械臂移动到不同位姿,每个位姿停留2-3秒
- 同步记录数据:
- 通过机械臂API获取末端位姿(X/Y/Z位置和Rx/Ry/Rz欧拉角)
- 同时触发相机拍摄棋盘格图像
# 示例:机械臂位姿数据结构 pose_vectors = [ # X(mm), Y(mm), Z(mm), Rx(deg), Ry(deg), Rz(deg) [222.8, 14.3, 373.4, -151.08, 23.19, -90.93], [310.0, -36.1, 222.3, -146.52, 16.32, -91.03], # 更多位姿数据... ]常见坑点:
- 照明不足导致棋盘格检测失败 → 解决方案:增加环形光源
- 机械臂振动导致图像模糊 → 解决方案:移动后等待500ms再拍照
- 欧拉角奇异性问题 → 解决方案:避免俯仰角接近±90度
3. 相机标定与棋盘格检测
在正式进行手眼标定前,我们需要先获取相机的内参和畸变系数。OpenCV提供了完整的相机标定流程:
def calibrate_camera(images, pattern_size=(6,4), square_size=20.0): obj_points = [] img_points = [] # 准备3D世界坐标点 objp = np.zeros((np.prod(pattern_size), 3), dtype=np.float32) objp[:,:2] = np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1,2) * square_size for img_path in images: img = cv2.imread(img_path) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners = cv2.findChessboardCorners(gray, pattern_size) if ret: # 亚像素级精确化 criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners_refined = cv2.cornerSubPix(gray, corners, (11,11), (-1,-1), criteria) obj_points.append(objp) img_points.append(corners_refined) # 实际标定 ret, K, dist, rvecs, tvecs = cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None) return K, dist这个函数会返回相机内参矩阵K和畸变系数dist。在我的项目中,典型值如下:
K = np.array([ [2374.37, 0, 1303.06], [0, 2383.08, 688.61], [0, 0, 1] ]) dist = np.array([0.0928, -0.4332, 0, 0])4. 核心算法:手眼标定实现
有了相机内参和机械臂位姿数据,我们就可以进行核心的手眼标定计算了。OpenCV提供了多种算法实现,实测下来Tsai方法效果最稳定:
def hand_eye_calibration(pose_vectors, image_paths, K, dist, pattern_size=(6,4)): # 步骤1:处理机械臂位姿数据 R_end2bases, t_end2bases = [], [] for pose in pose_vectors: R = euler_to_rotation_matrix(pose[3], pose[4], pose[5]) t = np.array(pose[:3]) R_end2bases.append(R) t_end2bases.append(t) # 步骤2:检测棋盘格并求解标定板到相机的变换 R_board2cameras, t_board2cameras = [], [] objp = np.zeros((np.prod(pattern_size), 3), dtype=np.float32) objp[:,:2] = np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1,2) * 20.0 for img_path in image_paths: img = cv2.imread(img_path) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners = cv2.findChessboardCorners(gray, pattern_size) if ret: ret, rvec, tvec = cv2.solvePnP(objp, corners, K, dist) R, _ = cv2.Rodrigues(rvec) R_board2cameras.append(R) t_board2cameras.append(tvec) # 步骤3:执行手眼标定 R_cam2end, t_cam2end = cv2.calibrateHandEye( R_gripper2base=R_end2bases, t_gripper2base=t_end2bases, R_target2cam=R_board2cameras, t_target2cam=t_board2cameras, method=cv2.CALIB_HAND_EYE_TSAI ) # 组合成齐次变换矩阵 T_cam2end = np.eye(4) T_cam2end[:3,:3] = R_cam2end T_cam2end[:3,3] = t_cam2end.reshape(3) return T_cam2end关键点解析:
euler_to_rotation_matrix将机械臂返回的欧拉角转换为旋转矩阵solvePnP求解每个位姿下标定板到相机的变换calibrateHandEye是核心函数,计算相机到末端的变换
5. 结果验证与精度提升
得到标定结果后,必须进行验证。我常用的验证方法有两种:
方法一:重投影误差检查
def check_reprojection(T_cam2end, pose_vectors, image_paths, K, dist): total_error = 0 for i, (pose, img_path) in enumerate(zip(pose_vectors, image_paths)): # 计算当前位姿下标定板的理论像素位置 R_end2base = euler_to_rotation_matrix(pose[3], pose[4], pose[5]) t_end2base = np.array(pose[:3]) # 标定板到末端坐标系的变换(假设标定板固定) T_board2end = np.eye(4) # 需要根据实际安装调整 # 计算标定板到相机的变换 T_board2cam = T_cam2end @ T_board2end @ np.linalg.inv( np.vstack([np.hstack([R_end2base, t_end2base.reshape(3,1)]), [0,0,0,1]])) # 重投影 objp = ... # 同上 img_points, _ = cv2.projectPoints( objp, T_board2cam[:3,:3], T_board2cam[:3,3], K, dist) # 与实际检测点比较 img = cv2.imread(img_path) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) _, corners = cv2.findChessboardCorners(gray, (6,4)) error = cv2.norm(corners, img_points, cv2.NORM_L2) / len(img_points) total_error += error return total_error / len(pose_vectors)方法二:实物距离测量
- 控制机械臂移动到特定位置
- 使用相机测量已知物体的距离
- 与实际测量值比较
如果误差较大(通常>2mm需要考虑重新标定),可以尝试:
- 增加数据量(建议30组以上)
- 优化位姿分布(覆盖工作空间各个区域)
- 检查机械臂位姿读数精度
- 使用更高精度的标定板
6. 实际应用中的优化技巧
经过多个项目的实践,我总结出以下优化经验:
- 温度补偿:工业环境下温度变化会影响机械臂精度,建议在恒温环境标定或增加温度补偿系数
- 动态标定:对于高速应用,可以开发动态标定方法补偿运动模糊
- 多位置标定:在工作空间的不同区域分别标定,使用时根据位置加权平均
- 自动重标定:开发定期自动标定程序,保持长期精度
# 示例:加权平均多个标定结果 def weighted_calibration(calibration_results, weights): R_list = [r[:3,:3] for r in calibration_results] t_list = [r[:3,3] for r in calibration_results] # 对旋转矩阵求平均 R_mean = average_rotations(R_list, weights) # 对平移向量求加权平均 t_mean = np.average(t_list, axis=0, weights=weights) T_mean = np.eye(4) T_mean[:3,:3] = R_mean T_mean[:3,3] = t_mean return T_mean7. 常见问题排查指南
在实际部署中,你可能会遇到以下问题:
问题一:标定结果不稳定
- 可能原因:机械臂重复定位精度差
- 解决方案:检查机械臂的零点校准,更换高精度编码器
问题二:棋盘格检测失败率高
- 可能原因:图像模糊或光照不均
- 解决方案:增加曝光时间,使用抗反射标定板
问题三:重投影误差大但实物测量准
- 可能原因:机械臂DH参数不准确
- 解决方案:先进行机械臂运动学标定
问题四:标定结果随位置变化大
- 可能原因:机械臂刚度不足导致变形
- 解决方案:降低运动速度,增加机械臂负载能力
8. 进阶应用:与ROS集成
对于使用ROS的开发者,可以将标定结果集成到ROS中:
import tf from geometry_msgs.msg import TransformStamped def to_ros_transform(T_cam2end, frame_id="camera", child_frame_id="flange"): msg = TransformStamped() msg.header.stamp = rospy.Time.now() msg.header.frame_id = frame_id msg.child_frame_id = child_frame_id msg.transform.translation.x = T_cam2end[0,3] msg.transform.translation.y = T_cam2end[1,3] msg.transform.translation.z = T_cam2end[2,3] q = tf.transformations.quaternion_from_matrix(T_cam2end) msg.transform.rotation.x = q[0] msg.transform.rotation.y = q[1] msg.transform.rotation.z = q[2] msg.transform.rotation.w = q[3] return msg这个Transform可以发布到ROS的tf树,方便后续的视觉伺服等应用开发。
手眼标定是机器人感知的基础,精度直接影响后续所有视觉任务的性能。我在实际项目中发现,花时间做好标定可以节省后期大量的调试时间。建议建立标准化的标定流程文档,并保存每次标定的原始数据以便追溯。当机械臂或相机发生物理碰撞后,一定要重新进行标定验证。