
简介机器人视觉系统中手眼标定是将相机像素坐标转换到机械臂坐标的关键环节。这份基于 Python-opencv 的手眼标定代码包面向机器人开发者、自动化工程师以及视觉引导项目的初学者采用九点定位法完成标定准备九个标定点记录像素坐标与机械臂坐标再利用 opencv 的 estimateAffine2D 函数估计 2D 仿射变换矩阵从而快速建立两套坐标系间的映射关系免去手动推导底层算法的繁琐。包体共 5 个文件涵盖核心 .py 脚本、运行说明 txt、可视化结果 png、网页说明 html 以及在线运行配置 inscode压缩包总大小仅 12KB轻量完整。目前已有 155 人学习代码示例详细展示了坐标集获取、仿射矩阵求解以及运行中的常见问题处理按步骤运行即可复现标定流程并可迁移至视觉抓取、自动化装配等实际任务中为机器人视觉系统开发提供直接参考。1. 手眼标定到底在解决什么问题做机器人和机器视觉结合的项目迟早都会撞上“手眼标定”这道坎。我自己第一次接触这个需求是在做一个机械臂抓取的项目相机已经固定在机械臂末端了通过相机识别到目标物体的坐标结果把坐标发给机械臂位置怎么都不对。后来才明白根本原因是相机坐标系和机械臂坐标系之间没有建立起准确的转换关系。这个转换关系就是手眼标定要算出来的东西。用一句话概括手眼标定就是求相机和机械臂之间的固定位姿变换关系。只要有了这组变换矩阵相机看到的任何点都能换算成机械臂能理解的坐标抓取、避障、轨迹规划这些下游动作才有意义。从安装方式上手眼标定分两种典型场景眼在手上eye-in-hand相机固定在机械臂末端法兰上跟着机械臂一起运动。需要求的是相机坐标系到机械臂末端坐标系的变换关系也就是cam2gripper。眼在手外eye-to-hand相机固定在工作空间外部不随机械臂运动。需要求的是相机坐标系到机械臂基座的变换关系也就是cam2base。我在实际项目里两种都做过。眼在手上更常见于抓取、装配这类需要“跟着手走”的场景眼在手外则更适合固定工位的质量检测、定位引导。两者的标定流程和代码逻辑高度相似核心数学问题都是求解一个AX XB形式的矩阵方程。AX XB这个等式看起来简单其实是整个标定过程的地基。A 是机械臂末端或基座在两个不同位姿之间的相对变换B 是标定板在相机坐标系下的相对变换X 就是我们要找的那个固定的手眼变换矩阵。求解这个方程的方法有很多OpenCV 直接封装好了主流的几种我们不需要从零推导矩阵推导过程但要理解每种方法的行为差异不然标出来的结果可能很离谱。2. 数学原理AXXB 到底在解什么2.1 坐标系变换的基本逻辑要真正理解标定得先把坐标系变换这件事捋清楚。整个系统里有四个关键坐标系机械臂基座坐标系、机械臂末端坐标系、相机坐标系、标定板坐标系。机械臂在运动到不同位姿时我们能从两个渠道拿到信息从机械臂的控制器或 SDK 里读出末端相对于基座的位姿这个就是gripper2base也就是公式里的 A 矩阵的来源。从相机拍摄标定板的图像中通过角点检测和 PnP 求解得到标定板相对于相机的位姿这个就是target2cam也就是公式里 B 矩阵的来源。当机械臂运动到两个不同位姿 i 和 j 时手眼矩阵 X 是不变的标定板相对于机械臂基座eye-in-hand 场景也是不变的。于是就有gripper2base_i * cam2gripper * target2cam_i gripper2base_j * cam2gripper * target2cam_j把等式右边的项挪到左边整理一下就能得到A * X X * B其中A inv(gripper2base_j) * gripper2base_i B target2cam_j * inv(target2cam_i)这里 A 是机械臂末端在两个位姿之间的相对运动B 是标定板在相机坐标系下的相对运动X 就是固定的手眼变换矩阵。2.2 OpenCV 的 calibrateHandEye 函数OpenCV 从 4.x 版本开始提供了cv2.calibrateHandEye函数直接封装了多种求解算法我们只需要把一组对应位姿传进去就能得到手眼矩阵。函数签名核心参数如下R, t cv2.calibrateHandEye( R_gripper2base, t_gripper2base, R_target2cam, t_target2cam, methodcv2.CALIB_HAND_EYE_TSAI )method 参数有几种可选每种方法的数学假设和数值稳定性不太一样方法特点适用场景TSAI两步法先求旋转再求平移经典稳定大多数 eye-in-hand 场景默认首选PARK旋转用四元数表示精度较高数据噪声较大时表现更好HORAUD也是四元数方法与 PARK 类似类似 PARK看个人偏好ANDREFF同时估计旋转和平移的全局优化方法数据质量高时精度更优DANIILIDIS也是全局优化方法需要高精度的场合但对初值敏感我自己实测下来大部分场景下 TSAI 和 PARK 的差异不大但 TSAI 收敛更稳定。如果标定结果出现明显的旋转矩阵不正交或平移偏差过大可以换 PARK 或 HORAUD 试试不需要改代码逻辑改个参数就行。这样就能快速判断是算法问题还是数据问题。2.3 为什么要强调旋转矩阵的约束标定出来的手眼矩阵旋转部分必须是正交矩阵行列式为 1这才是合法的旋转矩阵。但数值计算中往往会有微小偏差所以拿到结果后最好做一个正交化修正。3. 标定前的准备工作3.1 相机内参标定手眼标定绕不开相机内参因为求解标定板位姿target2cam时需要用到内参矩阵和畸变系数。内参不准后续所有结果都会偏。内参标定我用的是 OpenCV 的cv2.calibrateCamera标定板用棋盘格checkerboard打印出来贴在平整的硬板上。需要注意几个关键点棋盘格要尽量平整不要用软纸直接贴建议贴在亚克力板或玻璃板上。拍摄时从多个角度、多个距离采集 15~20 张图像覆盖视野的各个区域。角点检测时cv2.findChessboardCorners找的是内角点比如 9x6 的棋盘格内角点是 8x5这个很容易搞混。import cv2 import numpy as np # 棋盘格内角点数 pattern_size (8, 5) # 棋盘格每个格子的实际尺寸单位毫米 square_size 30.0 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 obj_points [] img_points [] for fname in image_files: img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners cv2.findChessboardCorners(gray, pattern_size, None) if ret: obj_points.append(objp) corners2 cv2.cornerSubPix( gray, corners, (11, 11), (-1, -1), criteria(cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) ) img_points.append(corners2) ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None )代码里的square_size必须填真实物理尺寸因为它直接影响后续位姿的尺度。如果这一步尺寸填错了标定出来的平移量会整体缩放机械臂抓取时偏差会非常明显。3.2 机械臂位姿的获取方式机械臂末端位姿gripper2base的获取方式取决于你用的是什么机械臂。市面上主流机械臂不管是 UR、AUBO、遨博还是自研控制器基本都会提供 SDK 或 ROS 接口。从 SDK 拿到的位姿通常有两种表示形式平移向量 旋转矩阵直接拆成R_gripper2base和t_gripper2base用。平移向量 欧拉角/四元数需要先转成旋转矩阵。这里有一个非常容易踩的坑不同机械臂 SDK 返回的位姿含义不一样。有的返回的是末端法兰中心相对于基座的位姿有的返回的是工具坐标系相对于基座的位姿还有的返回的是基座相对于末端的位姿。在做手眼标定时必须统一用末端法兰坐标系的数据并且确认是从基座到末端的变换不要搞反了方向。我之前用某款国产机械臂时SDK 文档写的位姿是“末端相对基座”但实际内部实现是“基座相对末端”导致标定结果旋转矩阵怎么验都不对。排查了很久最后把矩阵转置了一下问题才解决。所以拿到 SDK 数据后建议先用机械臂自带的上位机软件手动动一下关节同步打印输出数据确认坐标变化方向是不是和预期一致。3.3 标定板的选择与固定标定板可以用 OpenCV 的cv2.drawChessboardCorners生成棋盘格图案也可以用 ChArUco 板。棋盘格胜在简单、检测稳定ChArUco 的优点是允许标定板部分出视野对位姿估计更鲁棒。如果项目要求高精度建议购买玻璃基板的陶瓷棋盘格热膨胀系数小平整度好。自制的纸质板在环境温度变化大的车间里很容易因为纸张受潮或轻微变形导致角点位置偏移直接影响标定精度。标定板的固定位置也有讲究。眼在手上时标定板应当固定在工作空间内的某个稳定位置在整个标定过程中绝对不能移动。眼在手外时标定板固定在机械臂末端同样不能有丝毫松动。4. 手眼标定核心代码实现4.1 完整标定代码框架下面是完整的手眼标定代码这个框架我用了很多次兼容性好直接照搬基本没问题。import cv2 import numpy as np import glob import json class HandEyeCalibrator: def __init__(self, pattern_size(8, 5), square_size30.0): self.pattern_size pattern_size self.square_size square_size self.camera_matrix None self.dist_coeffs None self.gripper2base_list [] self.target2cam_list [] def load_camera_params(self, calib_file): 加载相机内参标定结果 data np.load(calib_file) self.camera_matrix data[mtx] self.dist_coeffs data[dist] print(相机内参加载完成) print(内参矩阵:\n, self.camera_matrix) print(畸变系数:\n, self.dist_coeffs) def solve_target2cam(self, image_path): 从单张标定板图像求解标定板在相机坐标系下的位姿 img cv2.imread(image_path) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners cv2.findChessboardCorners(gray, self.pattern_size, None) if not ret: print(f角点检测失败: {image_path}) return None corners cv2.cornerSubPix( gray, corners, (11, 11), (-1, -1), criteria(cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) ) objp np.zeros((self.pattern_size[0] * self.pattern_size[1], 3), np.float32) objp[:, :2] np.mgrid[0:self.pattern_size[0], 0:self.pattern_size[1]].T.reshape(-1, 2) objp * self.square_size success, rvec, tvec cv2.solvePnP(objp, corners, self.camera_matrix, self.dist_coeffs) if not success: print(fsolvePnP 求解失败: {image_path}) return None R, _ cv2.Rodrigues(rvec) t tvec.reshape(3, 1) return R, t def add_pose_pair(self, gripper_pose, image_path): 添加一组标定数据 gripper_pose: 机械臂末端位姿格式为 [rx, ry, rz, tx, ty, tz] rx/ry/rz 是旋转向量tx/ty/tz 是平移向量 rvec np.array(gripper_pose[:3], dtypenp.float64).reshape(3, 1) tvec np.array(gripper_pose[3:], dtypenp.float64).reshape(3, 1) R_gripper2base, _ cv2.Rodrigues(rvec) result self.solve_target2cam(image_path) if result is None: return False R_target2cam, t_target2cam result self.gripper2base_list.append((R_gripper2base, tvec)) self.target2cam_list.append((R_target2cam, t_target2cam)) return True def calibrate(self, methodcv2.CALIB_HAND_EYE_TSAI): 执行手眼标定 R_g2b [item[0] for item in self.gripper2base_list] t_g2b [item[1] for item in self.gripper2base_list] R_t2c [item[0] for item in self.target2cam_list] t_t2c [item[1] for item in self.target2cam_list] R_cam2gripper, t_cam2gripper cv2.calibrateHandEye( R_g2b, t_g2b, R_t2c, t_t2c, methodmethod ) # 将旋转矩阵和平移向量组合成齐次变换矩阵 T_cam2gripper np.eye(4) T_cam2gripper[:3, :3] R_cam2gripper T_cam2gripper[:3, 3] t_cam2gripper.flatten() return T_cam2gripper def save_result(self, T_cam2gripper, save_path): 保存标定结果 result { cam2gripper: T_cam2gripper.tolist(), camera_matrix: self.camera_matrix.tolist(), dist_coeffs: self.dist_coeffs.tolist() } with open(save_path, w) as f: json.dump(result, f, indent2) print(f标定结果已保存到 {save_path})4.2 怎么把机械臂位姿数据喂进来上面的代码中add_pose_pair接收的是旋转向量和平移向量的组合。实际项目中机械臂 SDK 返回的位姿格式五花八门最常见的是四元数[x, y, z, w]加平移量[tx, ty, tz]。需要先做一个转换。from scipy.spatial.transform import Rotation def quat_to_rotvec(x, y, z, w): 四元数转旋转向量 r Rotation.from_quat([x, y, z, w]) return r.as_rotvec() # 示例机械臂返回的位姿 quat [0.0, 0.0, 0.0, 1.0] # (x, y, z, w) pos [0.5, 0.2, 0.3] # 单位米 rotvec quat_to_rotvec(*quat) pose np.concatenate([rotvec, pos])注意scipy的Rotation.from_quat接受的参数顺序是[x, y, z, w]而很多机械臂 SDK 返回的是[w, x, y, z]一定要确认清楚。我因为这个顺序问题浪费过一整个下午标定结果怎么算怎么不对。4.3 坐标单位统一的问题这里再强调一个细节机械臂位姿的平移量单位是米还是毫米不影响手眼标定的结果因为标定求解的是相对变换尺度会被抵消。但是标定板的square_size如果不准会直接影响target2cam的尺度进而影响最终手眼矩阵的平移部分。所以标定板尺寸一定要用卡尺量不要凭打印设置里的数值想当然。4.4 标定结果验证函数拿到手眼矩阵之后不能直接投入使用必须先做验证。最直观的验证方法是用标定数据本身做回代def verify_handeye(T_cam2gripper, gripper2base_list, target2cam_list): 通过回代验证手眼矩阵的精度 对每一组数据计算标定板在机械臂基座坐标系下的位置理论上应该是一致的 errors [] for (R_g2b, t_g2b), (R_t2c, t_t2c) in zip(gripper2base_list, target2cam_list): T_g2b np.eye(4) T_g2b[:3, :3] R_g2b T_g2b[:3, 3] t_g2b.flatten() T_t2c np.eye(4) T_t2c[:3, :3] R_t2c T_t2c[:3, 3] t_t2c.flatten() # 标定板在基座坐标系下的位姿 T_t2b T_g2b T_cam2gripper T_t2c errors.append(T_t2b[:3, 3]) errors np.array(errors) # 计算所有位姿之间的最大偏差 max_diff 0.0 mean_point errors.mean(axis0) for e in errors: diff np.linalg.norm(e - mean_point) max_diff max(max_diff, diff) print(f验证结果平移偏差最大值 {max_diff * 1000:.2f} mm) # 同时计算旋转部分的偏差 return max_diff验证的原理是标定板固定不动通过不同机械臂位姿计算出来的标定板在基座坐标系下的位置应该重合。如果最大偏差在 3~5 毫米以内说明标定结果基本可靠。这里排除机械臂自身绝对定位精度误差的影响标定补偿的只是相机与末端的相对关系。5. 数据采集标定成败的关键环节5.1 采集多少组数据合适理论上求解AX XB最少需要 2 组数据就够了但实际采集时数据量太少噪声影响会被放大。以我的经验采集 15~20 组位姿比较合适。超过 20 组收益递减而且增加了采集时间。核心不在于数量多而在于位姿之间差异足够大、足够分散。5.2 机械臂姿态要怎么摆这一步是整个标定过程中最容易出问题、也最容易被忽视的地方。很多新手采集数据时机械臂只在很小的范围内微调位置标定出来的结果直接不能用。正确的采集策略是让机械臂末端在可达空间内尽量分散覆盖多个位置不要集中在某一个角落。姿态要多样化绕不同轴转动比如绕 X 轴旋转、绕 Y 轴旋转、绕 Z 轴旋转的位姿都要有。相邻位姿之间的姿态变化要大于 20~30 度不要让机械臂只是平移。避免所有位姿都让标定板正对相机尽量让标定板在相机视野中呈现不同的角度。我在实际项目中踩过这个坑用某机械臂标定图省事让机械臂在一个很小的区域里拍了 12 组图标出来的旋转矩阵看起来很正常但验证时回代误差高达 30 毫米。后来重新规划位姿让机械臂大幅运动覆盖整个工作空间回代误差立刻降到 1 毫米以内。这个对比非常明显。5.3 标定时的环境控制标定过程对环境光照有一定要求尤其是使用棋盘格时光照不均匀会导致角点检测不稳定。建议使用漫射光源避免强光直射和反光。相机曝光尽量手动固定不要让自动曝光在每张图之间变化。机械臂运动到位后等它完全停止、没有震动后再拍照。固定相机的支架要稳不能有松动。5.4 自动采集标定数据的脚本思路手动一条条记录位姿和图像太痛苦了实际项目中最好写一个自动化采集脚本。大致的逻辑是def auto_collect(robot, camera, num_poses15): 自动规划机械臂位姿并采集图像 robot: 机械臂控制对象 camera: 相机控制对象 # 预定义一组经过验证的、分散的关节角度或位姿 pose_list [ # [rx, ry, rz, tx, ty, tz] 或关节角 ] collected_data [] for i, pose in enumerate(pose_list[:num_poses]): robot.move_to(pose) time.sleep(0.5) # 等待机械臂稳定 # 获取当前机械臂末端位姿 current_pose robot.get_end_pose() # 拍照 img camera.capture() img_path fcalib_data/pose_{i:02d}.png cv2.imwrite(img_path, img) collected_data.append({ pose: current_pose, image: img_path }) print(f已采集第 {i1}/{num_poses} 组数据位姿: {current_pose}) return collected_data实际执行前建议先手动运行一遍确认机械臂走这些位姿不会发生碰撞也不会走到奇异点附近。机械臂的奇异点会导致实际位姿与预期偏差很大标定时要想办法避开。6. 常见问题与排查技巧实录6.1 标定结果验证误差大从哪里入手排查这是被问得最多的一个问题。误差偏大时的排查顺序我建议按下面的思路来问题现象可能原因排查方法回代误差 5mm机械臂位姿读取与真实不一致手动动一下机械臂对比 SDK 输出与上位机显示回代误差 5mm标定板发生位移或变形检查标定板是否固定牢固是否平整旋转矩阵不正交数据噪声大或位姿多样性不足增加数据组数增大姿态变化范围不同算法结果差异大位姿数据存在系统性问题打印位姿数据检查是否有异常跳变角点检测失败率高光照不均或标定板反光调整光源角度修改相机曝光标定板角点检测不准图像分辨率不足检查相机工作距离考虑提高分辨率或更换镜头6.2 标定板角点检测失败怎么办如果findChessboardCorners一直返回 False可以按顺序尝试先检查棋盘格是否完整出现在画面中特别是边缘没有被截断。把图像转成灰度图后先做直方图均衡化再检测。适当模糊图像cv2.GaussianBlur去噪。调整findChessboardCorners的 flags 参数比如加cv2.CALIB_CB_ADAPTIVE_THRESH cv2.CALIB_CB_NORMALIZE_IMAGE。换 ChArUco 板鲁棒性会好很多。6.3 标定结果能用但抓取偏斜怎么回事旋转矩阵验出来是准的但实际相机引导机械臂抓取时位置偏差不大角度偏差明显。这种情况多半是标定板物理放置的平面和相机成像平面不平行造成的。比如标定板稍微有点倾斜target2cam求解出的旋转部分会有微小的系统性偏差。解决思路是标定时尽量让标定板平面和机械臂末端平面大致平行同时采集一些标定板有明显倾角的位姿做中和而不是全部正对。6.4 手眼标定完还有位置误差正常吗要明确一个观念手眼标定只是补偿相机和机械臂末端之间的相对关系它不能消除机械臂本身的绝对定位误差。如果你的机械臂重复定位精度是 ±0.5mm但绝对定位精度只有 ±5mm那手眼标定做得再好最终引导精度也会受机械臂绝对定位精度的限制。如果项目对精度要求高还需要配合额外的误差补偿手段比如加装激光跟踪仪、使用视觉伺服闭环或者在关键工序末端用工装做二次定位。这些已经超出单纯手眼标定的范畴了但实际项目中经常要一起考虑。6.5 换镜头后必须重新标定最后提醒一个容易忽略的点相机换了镜头、调整了光圈焦距、或者内参标定结果有变化手眼标定结果全部作废必须从头来一遍。手眼标定和相机内参是强耦合的任何一端变了另一端就失去了意义。7. 对标定结果的一点实操心得做手眼标定做到后面你会慢慢发现代码本身不复杂真正复杂的是数据。一次标定能不能成从数据采集阶段就已经决定了。我个人习惯是在标定之前先花十几分钟把机械臂的位姿列表在仿真环境或者手动模式下预演一遍确认不会碰到任何东西再开始自动采集。标定完成后除了用回代误差验证还建议做一个实际的“指哪打哪”测试在标定板附近放一个已知位置的目标物通过相机识别后引导机械臂去触碰实际测量偏差。这个验证比任何数值指标都直观、都可信。另外标定结果文件一定要保存好最好是 JSON 带内参一起存别只存一个手眼矩阵。项目上线后一旦发现精度变差可以快速回溯是内参漂了还是手眼关系变了。希望这份经验能帮你少走几步弯路。手眼标定不是什么高深的技术但它连接了视觉系统和运动系统可以说是整个机器人感知-执行链路里最基础也最关键的一环。把这步做扎实了后面做视觉引导、抓取、焊接这些功能才有底气。本文还有配套的精品资源点击获取