首页 / 资讯中心 / 文章详情

机械臂手眼标定实战:从相机坐标到基座坐标的完整流程与避坑指南

机械臂手眼标定实战:从相机坐标到基座坐标的完整流程与避坑指南 ★ FEATURED ARTICLE
简介面向机器人视觉与自动化控制开发者的Python-OpenCV手眼标定代码包采用九点定位法实现相机像素坐标到机械臂坐标的仿射变换可用于自动化生产线、质量检测、三维重建等视觉引导场景帮助工程师快速打通视觉系统与机械臂控制之间的坐标转换链路。压缩包共5个文件涵盖Python核心脚本、文本说明、HTML文档、可视化图片及辅助配置整体仅12KB结构紧凑、内容精炼便于直接阅读和复用。已有158人学习该资源适合具备一定OpenCV基础并希望上手实测的视觉工程师或机器人方向学生。示例代码完整演示了九个圆心像素坐标与机械臂坐标集的采集方式以及基于estimateAffine2D计算仿射矩阵的关键步骤随附文档还交代了运行环境配置、依赖安装顺序与常见问题排查思路可有效降低手眼标定的调试门槛使读者能将这套方法快速迁移到自己的项目中。1. 手眼标定解决什么问题机械臂抓不准先别急着调 PID你兴致勃勃地装好相机让机械臂去抓一个固定位置的螺丝结果每次抓取都偏出去三毫米。手动补偿一两次后换个角度又偏了这时候问题基本不在机械臂重复精度而在于“相机看到的坐标”和“机械臂运动的坐标”根本不在同一个坐标系里。手眼标定做的就是这件事算出相机坐标系和机械臂基座坐标系之间的旋转和平移关系把相机里的像素位置换算成机械臂能听懂的三维坐标。这篇文章用 Python OpenCV 完整走一遍手眼标定流程从相机内参标定、标定板位姿求解到 calibrateHandEye 的调用和踩坑排查适合正在做视觉抓取、机器人引导或双目定位的从业者。2. 手眼标定的数学本质坐标系变换与 AXXB 问题的来龙去脉2.1 两种安装方式决定标定公式Eye-in-Hand 与 Eye-to-Hand 怎么选机械臂视觉系统里相机和机械臂的相对关系只有两种。第一种是相机固定在机械臂末端跟着机械臂一起动叫 Eye-in-Hand也就是“眼在手上”。第二种是相机固定在工作空间外部机械臂在相机视野里运动叫 Eye-to-Hand也就是“眼在手外”。这两种安装方式下要标定的变换矩阵不一样Eye-in-Hand 求的是相机坐标系到机械臂末端坐标系的变换记为 end_T_camEye-to-Hand 求的是相机坐标系到机械臂基座坐标系的变换记为 base_T_cam。选哪种安装方式取决于实际场景。Eye-in-Hand 的好处是相机离目标近能看细节而且机械臂换个站位就能避开遮挡但它每次移动后相机视野会变需要实时知道相机在基座里的位置。Eye-to-Hand 的好处是相机固定不动视野稳定适合大范围定位但容易因为机械臂自身遮挡目标。对于初学或小批量验证Eye-in-Hand 更常见因为数据采集时标定板可以固定在桌上不动只要机械臂带着相机走不同位姿就行。从公式角度看两种安装方式的标准方程都是 AXXB只是 A、B、X 的具体含义不同。理解这一点很重要因为网上很多代码把两种方式混在一起写直接套用经常会算出看起来合理但实际错误的结果。2.2 用齐次变换写出约束关系为什么 AXXB 而不是直接测距离先引入齐次变换矩阵的概念。一个 4×4 的齐次矩阵 T 包含了旋转矩阵 R3×3和平移向量 t3×1表达的是“把一个坐标系里的点变换到另一个坐标系里”。比如 base_T_end 表示把末端坐标系里的点变换到基座坐标系也就是机械臂控制器通常直接给的那组位姿数据。在 Eye-in-Hand 场景下标定板固定在外部相机固定在末端。对任意一个拍照位姿 i从机器人控制器读到 base_T_end_i从图像计算得到 cam_T_target_i这两个变换之间夹着我们要求的 end_T_cam而且三者乘起来等于一个常量——标定板在基座坐标系下的位姿 base_T_targetbase_T_target base_T_end_i · end_T_cam · cam_T_target_i对两个不同的位姿 i 和 j这个常量相等于是消去 base_T_target 后得到base_T_end_j⁻¹ · base_T_end_i · end_T_cam end_T_cam · cam_T_target_j · cam_T_target_i⁻¹这就是 AXXB 的标准形式。左边 A 由机器人位姿相对运动构成右边 B 由相机观测到的标定板相对运动构成X 就是要求的 end_T_cam。OpenCV 的 cv2.calibrateHandEye 内部就是按这个思路通过多组数据联合估计 X。所以手眼标定不能靠拿尺子量相机装在哪因为相机光学中心和机械臂法兰盘中心都不是可以直接量到的物理点。AXXB 的好处是只需要相对运动关系不需要知道任何绝对尺寸这也就解释了为什么同一个公式既能用于 Eye-in-Hand也能通过角色互换用于 Eye-to-Hand。2.3 标定需要哪些已知量机器人位姿、标定板位姿和相机内参的分工一次完整的手眼标定需要三类数据配合。第一类是相机内参包括焦距、主点坐标和畸变系数内参不准直接影响标定板位姿的精度。第二类是标定板相对于相机的位姿靠相机拍标定板图像、检测角点后用 solvePnP 求解这一步依赖内参。第三类是机械臂末端相对于基座的位姿直接从机器人控制器读取。这三类数据缺一不可而且必须保证它们描述的是同一时刻的状态。手眼标定本质上是把机器人运动学和视觉几何联立起来解方程任何一方的数据有系统性偏差都会直接折进 X 里。很多人标定结果不稳定回过头排查往往就是这三个数据源里某一个出了问题而不是算法本身的问题。另外要注意单位统一。机械臂控制器输出的平移量通常是毫米而棋盘格角点的三维坐标通常用米或者毫米定义solvePnP 的输入和输出单位必须一致否则 t 向量会差一千倍手眼矩阵算出来自然是错的。3. 相机内参标定与标定板位姿求解手眼标定的两个数据源头3.1 先标相机内参畸变不修手眼矩阵就是玄学手眼标定的第二步依赖相机拍摄标定板并求解位姿如果相机内参不对solvePnP 求出来的旋转和平移会带着系统性误差后面 AXXB 解出来的 X 也会跟着错。所以我的习惯是先单独完成相机内参标定把重投影误差压到 0.1 像素以下再开始采集手眼数据。内参标定用棋盘格是最稳的。打印一张 9×6 内角点的棋盘格贴在平整硬板上用相机从不同角度拍 15~20 张照片。OpenCV 里的 findChessboardCorners 加 calibrateCamera 一套走完核心代码如下import cv2 import numpy as np import glob CHECKERBOARD (9, 6) # 内角点数9列 x 6行 SQUARE_SIZE 0.025 # 每个格子的物理边长单位米 criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-6) # 构造棋盘格的物理三维点标定板坐标系下z0 objp np.zeros((CHECKERBOARD[0] * CHECKERBOARD[1], 3), np.float32) objp[:, :2] np.mgrid[0:CHECKERBOARD[0], 0:CHECKERBOARD[1]].T.reshape(-1, 2) objp * SQUARE_SIZE obj_points [] # 所有图像的三维点 img_points [] # 所有图像的二维角点 # 遍历标定图片 for fname in sorted(glob.glob(calib_images/*.jpg)): img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 检测棋盘格内角点 ret, corners cv2.findChessboardCorners(gray, CHECKERBOARD, None) if not ret: print(f{fname} 检测失败跳过) continue # 亚像素精化把角点定位精度提到 0.1 像素级别 corners2 cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) obj_points.append(objp) img_points.append(corners2) # 相机标定 ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None ) # 保存内参和畸变系数后续手眼标定直接加载 np.save(camera_mtx.npy, mtx) np.save(camera_dist.npy, dist) print(重投影误差:, ret)代码里最关键的是 object points 的构造。mgrid 生成的坐标从 (0,0,0) 到 (8,5,0)乘上格子边长后单位是米。角点检测失败时直接跳过而不是中断因为总会有几张光线不好或者棋盘出界的图。cornerSubPix 的窗口大小选 11×11搜索半径(-1,-1)表示自动按窗口大小估计迭代准则 30 次和 1e-6 精度足够常用场景。标定完成后把 mtx 和 dist 存成 npy 文件手眼标定前先检查重投影误差 ret。如果 ret 大于 0.5说明图像质量或棋盘格平整度有问题建议重拍而不是直接往下走。3.2 棋盘格角点检测与 solvePnP从一张图得到标定板到相机的变换有了内参之后每一张手眼数据图需要计算一个 cam_T_target也就是标定板坐标系到相机坐标系的变换。这个变换用 solvePnP 来做已知棋盘格角点的三维坐标和对应的二维像素坐标求解相机外参。下面的函数封装了从图像到位姿的完整过程import cv2 import numpy as np def detect_target_pose(image, mtx, dist, checkerboard(9, 6), square_size0.025): 检测棋盘格并计算标定板坐标系到相机坐标系的变换。 返回 R_target2cam (3,3), t_target2cam (3,1)检测失败返回 None。 gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) ret, corners cv2.findChessboardCorners(gray, checkerboard, None) if not ret: return None criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-6) corners2 cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) # 构造标定板坐标系下三维点 objp np.zeros((checkerboard[0] * checkerboard[1], 3), np.float32) objp[:, :2] np.mgrid[0:checkerboard[0], 0:checkerboard[1]].T.reshape(-1, 2) objp * square_size # solvePnP 求解外参把标定板坐标系变换到相机坐标系 retval, rvec, tvec cv2.solvePnP( objp, corners2, mtx, dist, flagscv2.SOLVEPNP_ITERATIVE ) if not retval: return None R_target2cam, _ cv2.Rodrigues(rvec) return R_target2cam, tvec.reshape(3, 1)solvePnP 输入的三维点顺序必须和 findChessboardCorners 输出的角点顺序一一对应。棋盘格的 mgrid 顺序是从左上角开始按行排列的findChessboardCorners 默认也是这个顺序只要不手动打乱就没有问题。SOLVEPNP_ITERATIVE 适合已知内参的场景对畸变不大的镜头稳定性很好如果棋盘格在图像里占比较小可以换 SOLVEPNP_AP3P 或 SOLVEPNP_IPPE但实际中 ITERATIVE 足够。这个函数返回的 R_target2cam 和 t_target2cam 就是手眼标定公式里的 B。注意 tvec 的单位跟 square_size 一致square_size 用米tvec 就是米。3.3 数据采集方法17 组位姿怎么摆图像和机器人位姿怎么同步手眼标定数据不是随便拍几张就行的。我一般控制在 12~20 组之间太少解不稳定太多数据冗余对精度提升有限反而增加采集工作量。关键不是数量而是位姿的多样性。实际操作时机械臂带着相机走到一个位姿先停下来等运动完全稳定后同时记录两样东西一是机器人控制器输出的末端位姿二是相机拍下的棋盘格图像。我用的常见做法是写一个采集脚本机械臂每到位一个点先延时 0.5 秒等待震动消失再触发相机拍照同时从控制器读取当前位姿。这样能保证图像和位姿对应的是同一个时刻。位姿的摆法要注意几个原则。标定板在图像里的占比不要太小最好超过画面四分之一否则角点亚像素定位精度不够。机械臂的姿态要覆盖不同朝向不能所有点位都只是平移要在 X、Y、Z 三个旋转方向都有变化比如让末端绕工具坐标系的 X 轴转 20 度、绕 Y 轴转 30 度这样交叉变化。标定板尽量放在视野的不同区域不要一直摆在画面正中央否则外参求解的病态程度会增加。数据采集完把机器人位姿保存成文本文件每行格式统一为x, y, z, rx, ry, rz其中平移单位是毫米旋转是旋转向量。图像文件按采集顺序编号和位姿文件的行一一对应。4. 用 calibrateHandEye 跑通手眼标定完整代码与参数选择4.1 Eye-in-Hand 标定完整代码从文件到变换矩阵这一步把前面准备的数据汇合到一起调用 OpenCV 的 calibrateHandEye 求出 end_T_cam。完整代码可以分成三个模块读取机器人位姿文件、遍历图像计算标定板位姿、调用求解函数输出结果。import cv2 import numpy as np import glob def load_robot_poses(pose_file): 读取机械臂末端位姿文件。 每行格式: x_mm, y_mm, z_mm, rx, ry, rz 平移单位毫米旋转为旋转向量。 返回 R_gripper2base (N,3,3) 和 t_gripper2base (N,3)单位转换为米。 R_list, t_list [], [] with open(pose_file, r) as f: for line in f: vals [float(v) for v in line.strip().split(,)] x, y, z vals[0] / 1000.0, vals[1] / 1000.0, vals[2] / 1000.0 rvec np.array(vals[3:], dtypenp.float64).reshape(3, 1) R, _ cv2.Rodrigues(rvec) t np.array([[x], [y], [z]], dtypenp.float64) R_list.append(R) t_list.append(t) return np.array(R_list), np.array(t_list).reshape(-1, 3) # 加载相机内参 mtx np.load(camera_mtx.npy) dist np.load(camera_dist.npy) # 读取机械臂位姿和图像列表 R_gripper2base, t_gripper2base load_robot_poses(robot_poses.txt) image_files sorted(glob.glob(handeye_images/*.jpg)) assert len(image_files) len(R_gripper2base), 图像数量和位姿数量必须一致 # 遍历图像计算标定板到相机的位姿 R_target2cam_list, t_target2cam_list [], [] valid_idx [] for i, fname in enumerate(image_files): img cv2.imread(fname) result detect_target_pose(img, mtx, dist, checkerboard(9, 6), square_size0.025) if result is None: print(f{fname} 棋盘格检测失败跳过) continue R_target2cam, t_target2cam result R_target2cam_list.append(R_target2cam) t_target2cam_list.append(t_target2cam) valid_idx.append(i) # 同步剔除检测失败对应的机器人位姿 R_gripper2base R_gripper2base[valid_idx] t_gripper2base t_gripper2base[valid_idx] R_target2cam np.array(R_target2cam_list) t_target2cam np.array(t_target2cam_list) # 手眼标定 R_cam2gripper, t_cam2gripper cv2.calibrateHandEye( R_gripper2base, t_gripper2base, R_target2cam, t_target2cam, methodcv2.CALIB_HAND_EYE_TSAI ) # 输出结果 print(end_T_cam 旋转矩阵:) print(R_cam2gripper) print(end_T_cam 平移向量 (米):) print(t_cam2gripper) # 保存结果 np.save(handeye_R_cam2gripper.npy, R_cam2gripper) np.save(handeye_t_cam2gripper.npy, t_cam2gripper)这段代码里最容易出错的地方是单位换算。机器人位姿文件里的 x、y、z 是毫米除以 1000 转成米与棋盘格 square_size 的米制单位保持一致。旋转向量用 cv2.Rodrigues 转成旋转矩阵注意 rvec 的 shape 必须是 (3,1) 或 (3,)否则某些 OpenCV 版本会报错或得到错误结果。valid_idx 的作用是同步过滤。如果第 5 张图棋盘格检测失败那么第 5 行机器人位姿必须一并剔除否则数据对不齐标定结果的平移部分会直接偏掉。calibrateHandEye 要求输入数组长度一致而且内部是按数组下标配对的顺序错位是手眼标定最容易导致“结果看起来合理但实际完全不能用”的原因之一。4.2 calibrateHandEye 的五种方法TSAI/PARK/HORAUD/ANDREFF/DANIILIDIS 怎么选OpenCV 的 calibrateHandEye 支持五种算法通过 method 参数切换。我实际用下来最常用的就是 TSAI它实现简单、速度快、对数据质量要求适中大多数工业场景都能满足。PARK 和 HORAUD 在理论上是等效的但对噪声更敏感适合数据质量比较高的情况。ANDREFF 是基于正交约束的线性解不需要迭代但精度一般比不过 Tsai 和 Park。DANIILIDIS 是较新的方法对数据退化情况的鲁棒性更好如果你的采集位姿多样性不够它能比 TSAI 少出一些极端错误。下面这个表格可以帮你快速选method 参数特点适用场景CALIB_HAND_EYE_TSAI经典两步法稳定对标定板噪声不敏感默认首选大多数场景够用CALIB_HAND_EYE_PARK李群上求平均精度高但需要数据干净标定板检测质量好、位姿覆盖充分CALIB_HAND_EYE_HORAUD与 Park 类似旋转和平移分离求解同上看个人偏好CALIB_HAND_EYE_ANDREFF线性方法速度最快数据量大、实时性要求高的场景CALIB_HAND_EYE_DANIILIDIS处理退化情况更鲁棒位姿多样性不足或带噪声数据我的建议是先全部跑一遍对比五种方法输出的 X 矩阵的差异。如果差异很小说明数据质量好选哪个都行如果 TSAI 和 DANIILIDIS 的结果差了十万八千里说明你的数据里存在病态位姿或同步问题这时候先别纠结算法回去检查数据才是正路。4.3 Eye-to-Hand 的差异把相对变换取逆再传入Eye-to-Hand 场景下相机固定、标定板固定在机械臂末端。此时读到的机器人位姿还是 base_T_end但标定板不再是固定的它跟着末端一起动。OpenCV 的 calibrateHandEye 依然能用但传入参数的角色要做调整。我在项目里常用的做法是把每组 cam_T_target 取逆得到 target_T_cam把每组 base_T_end 求逆得到 end_T_base然后将前者作为 gripper2base 传入后者作为 target2cam 传入求解出来的 X 就是 base_T_cam。听起来绕但代码很简单def calibrate_eye_to_hand(R_base2end, t_base2end, R_cam2target, t_cam2target): R_gripper2base_list, t_gripper2base_list [], [] R_target2cam_list, t_target2cam_list [], [] for i in range(len(R_base2end)): # 构造 4x4 齐次矩阵 T_be np.eye(4) T_be[:3, :3] R_base2end[i] T_be[:3, 3] t_base2end[i].flatten() T_ct np.eye(4) T_ct[:3, :3] R_cam2target[i] T_ct[:3, 3] t_cam2target[i].flatten() # 取逆 T_eb np.linalg.inv(T_be) # end_T_base T_tc np.linalg.inv(T_ct) # target_T_cam R_gripper2base_list.append(T_tc[:3, :3]) t_gripper2base_list.append(T_tc[:3, 3]) R_target2cam_list.append(T_eb[:3, :3]) t_target2cam_list.append(T_eb[:3, 3]) R_base2cam, t_base2cam cv2.calibrateHandEye( np.array(R_gripper2base_list), np.array(t_gripper2base_list), np.array(R_target2cam_list), np.array(t_target2cam_list), methodcv2.CALIB_HAND_EYE_TSAI ) return R_base2cam, t_base2cam # 最终得到基座到相机的变换这段代码没有做单位转换调用前务必保证所有 t 都已经是米。Eye-to-Hand 的输出习惯上直接给 base_T_cam也就是把相机坐标系里的点变换到基座坐标系。如果你拿到的是 cam_T_base记得取逆再用。4.4 保存标定结果YAML 和 numpy 两种持久化方式标定完的结果要存下来给抓取程序用。最常见的两种方式一种是用 numpy 的 save 直接存二进制 npy适合自己写的小程序快速加载另一种是存成 YAML方便和其他模块或同事对接。OpenCV 自带 FileStorage 可以写 YAML但格式如果只是自己内部用npy 更省事。import cv2 import numpy as np # 方式一npy 直接保存 np.save(handeye_R_cam2gripper.npy, R_cam2gripper) np.save(handeye_t_cam2gripper.npy, t_cam2gripper) # 方式二YAML 保存附带内参方便后续独立加载 fs cv2.FileStorage(handeye_result.yaml, cv2.FILE_STORAGE_WRITE) fs.write(R_cam2gripper, R_cam2gripper) fs.write(t_cam2gripper, t_cam2gripper) fs.write(camera_mtx, mtx) fs.write(camera_dist, dist) fs.release() # 读取 YAML fs cv2.FileStorage(handeye_result.yaml, cv2.FILE_STORAGE_READ) R_load fs.getNode(R_cam2gripper).mat() t_load fs.getNode(t_cam2gripper).mat() fs.release()YAML 方式的好处是同时保存了内参和畸变系数抓取模块只需要读这一个文件就能完整恢复手眼链路。注意 OpenCV 的 FileStorage 写矩阵时会自动处理数据类型读取时用 mat() 转回 numpy 数组平移向量的 shape 默认是 (3,1)使用前可能需要 reshape 成 (3,)。5. 手眼标定避坑记五个让你翻车的细节5.1 标定板在画面里太小solvePnP 精度崩了现象标定板放在视野远处只占画面十分之一左右手眼标定结果换一组数据重新标X 矩阵变化很大。原因棋盘格角点数量不变但每个角点在图像里的亚像素定位误差是固定的尺度小了之后同样的像素误差映射到三维空间的角度误差就变大solvePnP 的旋转分量不稳定直接传导给 AXXB 求解。解决重新采集数据保证标定板最长边在图像里占比超过四分之一最好接近一半。我自己的经验是棋盘格外围至少留一圈空白边否则边缘角点受镜头畸变影响更明显。5.2 图像采集和机器人位姿不同步标定结果像抽风现象标定结果每次跑都有小幅度跳动或者干脆出现明显不可能的平移量比如 1 米以上。原因机械臂还没完全停稳或者控制器位姿缓存还没更新相机就先拍照了图像对应的实际末端位姿和读到的数值不一致。高速运动下哪怕差 50 毫秒末端位置就能差好几毫米。解决在采集脚本里加延时到位后至少等 0.3~0.5 秒再拍照和读数。如果控制器支持外部触发用同一个硬件信号同时触发相机和位姿记录是最稳的。另一个办法是先记录位姿再触发拍照因为位姿读取是瞬时的而拍照有曝光和传输延迟。5.3 机器人位姿转换成旋转矩阵时欧拉角顺序搞反了现象标定结果重投影验证时旋转方向完全错误比如沿着某个轴转了 90 度。原因很多机器人控制器返回的位姿是欧拉角而不是旋转向量。欧拉角的顺序在不同厂商之间可能不一样常见的 ZYX 和 XYZ 转换出来的旋转矩阵天差地别。我用 cv2.Rodrigues 处理的是旋转向量直接拿欧拉角喂进去必错。解决先确认控制器输出的旋转表示形式。如果是欧拉角要用对应的转换公式先转成旋转矩阵再喂给标定程序。一个快速自检方法是让机械臂只绕单一轴转 90 度看转换后的矩阵是否符合预期。这个坑我踩过一次之后所有位姿读取函数都会先做单轴验证再交给标定流程。5.4 采集数据全在一个平面AXXB 近似退化现象标定结果在数学上能收敛但换一组数据重新标定结果差异明显尤其是平移分量。原因AXXB 需要多组非共面的相对旋转来约束解空间。如果机械臂只是把相机平移到不同位置姿态完全不变那么所有位姿之间的相对旋转都是单位矩阵方程退化X 在某个方向上没有被约束住。解决采集时让机械臂在每个位置都做姿态变化至少绕两个轴各转 20~30 度。我一般会在 5 个不同高度、每个高度变换 3 种姿态凑 15 组确保相对运动在三维空间里铺开。5.5 相机内参和手眼标定用了不同的分辨率现象手眼标定结果单独看似乎合理一旦用真实抓取测试就偏而且偏的方向没有规律。原因相机标定内参时用的图像分辨率是 1280×720后来采集手眼数据时相机被设置成了 1920×1080或者反过来。分辨率变化后图像坐标缩放内参却不匹配solvePnP 自然出错。解决全流程固定相机分辨率内参标定和手眼数据采集用同一个分辨率、同一个镜头焦距。如果中途动过镜头或换过分辨率重新做一遍内参标定不要沿用旧的内参文件。6. 用抓取测试验证标定精度一个单点测试流程跑完才算数标定结果不能只看矩阵打印得漂亮最终要能用。我的验证流程是做一个单点抓取测试在机械臂工作空间里放一个已知半径的小圆柱或小球用相机识别它的中心点换算成基座坐标后让机械臂去抓或去指量一下末端实际到达位置和目标位置的偏差。def pixel_to_base(u, v, depth, mtx, R_cam2gripper, t_cam2gripper, R_gripper2base, t_gripper2base): 将像素坐标转换为基座坐标。 depth 为相机坐标系下的深度值单位米。 # 像素坐标转相机坐标归一化平面 fx mtx[0, 0] fy mtx[1, 1] cx mtx[0, 2] cy mtx[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]], dtypenp.float64) # 相机坐标 - 末端坐标 p_gripper R_cam2gripper p_cam t_cam2gripper.reshape(3, 1) # 末端坐标 - 基座坐标 p_base R_gripper2base p_gripper t_gripper2base.reshape(3, 1) return p_base.flatten()测试时注意目标点的深度值如果是用单目估算的精度会很差我通常配合已知目标高度或者用双目/结构光来获取可靠的 depth。如果标定结果正确机械臂执行点到目标位置的偏差应该在 5 毫米以内具体取决于机械臂本身的绝对定位精度。最后说一个我自己的教训。有一回标定结果怎么看怎么合理重投影误差也不大但抓取总是偏一个固定角度排查了三天最后发现是机器人控制器的底座安装没有校平基座坐标系的 Z 轴和手眼标定假设的完全垂直差了 1.5 度。这种问题手眼标定本身无法修正因为标定假设基座坐标系是精确已知的。遇到这种情况先把机械臂自身的坐标精度校准好再回头做手眼标定。希望这篇文章能帮你少走几步弯路标定一次就能稳。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站