
1. 手眼标定前期拆解别急着敲代码先搞清楚你要解什么方程做机器人抓取、视觉定位这类项目迟早会撞上“手眼标定”这堵墙。你拿着一台RealSense D435深度相机想让它告诉睿尔曼机械臂“工件在哪儿”但相机看到的坐标和机械臂自己认知的坐标完全是两套语言。手眼标定干的事情就是建立这两套坐标系的换算关系。我刚接触这个项目时也犯过“拿代码硬怼”的毛病装了一堆库跑了一个所谓的标定程序结果误差大得离谱。后来才明白手眼标定的核心不是Python代码本身而是你是否理解被标定的那个矩阵变换到底在解什么。所以这一节我们先花五分钟把原理捋顺再谈实操。1.1 “眼在手上”和“眼在手外”两种模型先分清睿尔曼机械臂是六轴协作臂D435可以装在机械臂末端眼在手上eye-in-hand也可以固定在旁边看全局眼在手外eye-to-hand。两种装法标定的数学关系完全相反代码虽然都能跑但目标矩阵的代求参数不一样。眼在手上相机跟着机械臂动标定的结果是相机坐标系相对于机械臂末端坐标系的固定变换T_cam2gripper。我们通过移动机械臂到不同姿态拍摄同一个标定板利用多组“机械臂末端位姿”和“标定板在相机下的位姿”求解一个形如AX XB的方程。眼在手外相机固定在场景中不动机械臂带着标定板或者末端带着已知几何关系的工具运动标定结果是相机坐标系相对于机械臂基座坐标系的变换T_cam2base。同样利用多组数据构成AX XB只是A和B的取值来源不同。睿尔曼的官方SDK里提供了末端位姿读取接口D435通过pyrealsense2取RGB图OpenCV负责识别标定板角点整个管线这样串起来是通用的。你不需要买昂贵的标定套件一块普通的棋盘格打印纸就能干活精度完全够。注意很多初学者把AXXB当成一个神秘黑盒其实它只是描述“相机在机械臂坐标系下的位姿固定不变”这个事实。眼在手上时每次移动后相机与末端的相对关系恒定眼在手外时相机与基座的相对关系恒定。这个“恒定”就是方程里的X。1.2 为什么用棋盘格而不是二维码或者ArUcoD435的RGB分辨率是1920x1080做手眼标定我强烈建议用经典棋盘格而不是ArUco或者二维码。原因有三点第一棋盘格的角点检测亚像素精度很高OpenCV的findChessboardCorners配合cornerSubPix可以稳定做到0.1像素以内这对标定结果的稳定性非常关键。第二棋盘格不需要事先知道每个格子的“编码信息”只要知道格子物理尺寸就能解算位姿。第三D435的RGB在近距和中距下畸变控制尚可但广角边缘畸变不小棋盘格覆盖整个视野时角点分布均匀有利于求解单应性矩阵和相机外参。实操中打印棋盘格时我建议用A3纸格子边长30mm内角点数别太少我常用的是9x6或者7x5。格子太密反光会造成误检测格子太少位姿解算容易退化。1.3 手眼标定所需的关键数据形式不管哪种模型最终喂给算法的都是两类数据的配对机械臂末端位姿眼在手上或基座位姿眼在手外一般取四元数平移向量也可以转成4x4齐次矩阵。标定板坐标系相对于相机的位姿从solvePnP得到旋转向量和平移向量转成4x4齐次矩阵。每采集一帧图像记录一对数据。跑标定程序时OpenCV的cv2.calibrateHandEye接收两组4x4矩阵列表输入格式是(4, 4, N)的数组N表示样本数。样本数不是越多越好但至少15-20组且姿态变化要“花”一点要包含旋转、俯仰、偏航的不同组合否则方程会退化。2. 环境准备与工具链选型从Python到D435驱动一次装齐这个项目对硬件和软件环境都比较敏感尤其是RealSense的Python库和OpenCV的版本兼容性问题很容易把人卡在第一步。我基于Windows 11 Python 3.9的环境跑通全流程LinuxUbuntu 20.04/22.04下步骤类似只是驱动安装方式略有区别。2.1 Python版本与虚拟环境建议Python版本建议直接上3.9或3.10不要用最新的3.12、3.13。原因是pyrealsense2的预编译wheel包对新版本Python的支持往往滞后老版本Python配合成熟依赖能避免大量“编译源码失败”的坑。我所有的实验都跑在虚拟环境里强烈建议你也不要裸装在系统Python里否则后面装OpenCV、NumPy的时候容易把系统环境搞乱。创建虚拟环境很简单三步走python -m venv handeye_env handeye_env\Scripts\activate # Windows # source handeye_env/bin/activate # Linux/Mac pip install --upgrade pip后面所有依赖都在这套环境里安装项目删了环境一扔干干净净。2.2 安装RealSense SDK与Python库D435用起来比较省心的地方是官方对Python的支持很成熟不需要自己编译librealsense。安装步骤就两大块# 安装Intel RealSense SDK运行库 # Windows下访问Intel官网下载Intel.RealSense.SDK.exe安装即可 # Linux下建议直接用apt源 # sudo apt-get install librealsense2-dev librealsense2-dkms # Python绑定库 pip install pyrealsense2安装完可以跑一个极简脚本确认相机能被识别import pyrealsense2 as rs ctx rs.context() if len(ctx.devices) 0: print(没有发现RealSense设备) else: for dev in ctx.devices: print(f发现设备: {dev.get_info(rs.camera_info.name)})这里有个容易踩的坑USB接口一定要插在USB 3.0或以上接口D435的RGB和深度数据流带宽很大插在USB 2.0上会出现画面卡顿、设备断连、甚至完全无法打开。我第一次插在机箱前置USB 2.0口上折腾了一个小时以为设备坏了。2.3 OpenCV与NumPy的安装OpenCV负责棋盘格角点检测和solvePnP运算NumPy做矩阵运算。版本我建议用OpenCV 4.8.x或4.9.x不要用最新的4.10以上版本因为部分calibrateHandEye的默认参数在老版本上验证更多新版本行为没有变化但依赖的NumPy版本有时会冲突。pip install opencv-python4.8.1.78 pip install opencv-contrib-python4.8.1.78 pip install numpy1.24.3注意opencv-python和opencv-contrib-python不要同时装二选一即可我装的是带contrib的版本因为后续如果需要aruco模块就用得上。安装完成后最简单的验证是import cv2 import numpy as np print(cv2.__version__) print(np.__version__)如果打印正常环境就通了一半。2.4 睿尔曼机械臂的Python通信睿尔曼机械臂以RM65系列为例官方提供了rm_ctrl或者基于TCP/串口的SDKPython接口封装了机械臂的移动、状态查询、位姿读取。我用的方式是走TCP网络接口机械臂控制器默认监听端口通过JSON指令交互。以读取机械臂末端位姿为例核心就一个函数调用from rm_ctrl import RoboticArm arm RoboticArm(ip192.168.1.18, port8080) pose arm.get_end_pose() # 返回通常包含x, y, z, rx, ry, rz欧拉角形式或四元数 print(pose)需要注意每种型号的坐标定义可能不同。睿尔曼的坐标系一般是基座坐标系原点在底座安装面中心Z轴向上X轴向前。拿到末端位姿后建议直接转成齐次矩阵不要用欧拉角直接做运算欧拉角有万向锁问题中间转换很容易出幺蛾子。2.5 依赖安装的顺序雷区如果按“先装OpenCV再装pyrealsense2”的顺序失败报DLL load failed之类的错误大概率是NumPy版本被OpenCV或pyrealsense2的依赖覆盖了。我的经验是先装NumPy再装pyrealsense2最后装OpenCV。这样可以避免pyrealsense2把NumPy降级到老版本导致OpenCV的API不兼容。还有一点Windows下如果提示缺少vcruntime140.dll或者msvcp140.dll需要去微软官网装“Microsoft Visual C Redistributable”这个问题很常见因为librealsense和OpenCV的预编译库都依赖这个运行库。3. 标定数据采集让机械臂摆出“花式”姿态拍出高质量样本数据采集是整个手眼标定流程里最考验耐心的环节。很多人标定结果差问题几乎都出在数据采集环节——姿态不够丰富、标定板成像太小、图像模糊、标定板部分出画。这一节重点讲怎么采出高质量数据。3.1 场景布置与固定方式眼在手上时标定板固定不动放在机械臂工作空间内一个比较居中的位置相机随机末端移动拍摄。关键要求是标定板要在相机视野内尽量大我一般让标定板在画面中占比超过三分之一。太小了角点检测精度暴跌solvePnP的位姿解算噪声就会很大。眼在手外时标定板固定在机械臂末端或者抓在夹爪上相机固定在三脚架或者铝型材支架上机械臂带着板子在相机前方移动。同样要求板子在画面中占据较大面积。我自己用的标定板是A3纸打印的9x6棋盘格内角点数格子边长30mm贴在硬纸板上。贴的时候一定要保证平整有气泡或者褶皱都会导致局部角点位置偏差。如果条件允许用玻璃夹板或者铝板背胶更好。3.2 机械臂姿态规划让旋转矩阵“多样化”这一步是数据质量的核心。很多人图省事让机械臂在几个固定位置微调一下就算采集了结果标定出来的X矩阵误差巨大。原因在于AXXB这个方程需要多组“旋转轴不同”的姿态组合才能约束住解。实操中我的策略是在机械臂可达空间内画一个虚拟的“球壳”让末端在球壳上的多个点以不同的俯仰、偏航角对准标定板。具体来说控制末端在X、Y、Z方向上平移范围覆盖机械臂工作空间的中间区域避免到奇异点附近。每次移动后让末端绕自身坐标系的X轴、Y轴、Z轴分别做一次明显旋转15度到30度组合出不同的姿态。总共采集20-30组数据每组之间姿态差异越大越好。一个简单的控制代码示意基于睿尔曼SDKimport time import numpy as np from rm_ctrl import RoboticArm arm RoboticArm(ip192.168.1.18, port8080) # 预设一组末端位姿x, y, z, rx, ry, rz单位毫米和度 poses [ [300, 0, 300, 180, 0, 0], [300, -100, 250, 170, 10, 15], [350, 0, 260, 190, -15, 10], [280, 80, 320, 160, 5, -20], # 继续添加更多组合 ] for i, pose in enumerate(poses): arm.move_l(pose) time.sleep(2) # 等机械臂完全停止 print(f已移动到第{i1}个姿态)注意每个点位之间留足机械臂的运动时间不要连续快速发指令否则控制器缓冲区的指令堆积会导致运动未完成就开始采图位姿数据与图像不匹配。3.3 D435图像采集RGB对齐与相机内参D435的RGB和深度图来自不同传感器两个传感器的视野和分辨率不一致。手眼标定只需要RGB图不需要深度图但需要在pyrealsense2里做align操作吗答案是如果只做手眼标定不需要深度对齐直接取RGB流即可。不过如果你后面要做抓取需要把深度图对齐到RGB坐标系那才需要align。标定阶段我们保留两份数据原始RGB图和对应的机械臂位姿。启动相机的代码import pyrealsense2 as rs import cv2 import numpy as np pipeline rs.pipeline() config rs.config() config.enable_stream(rs.stream.color, 1920, 1080, rs.format.bgr8, 30) profile pipeline.start(config) # 获取内参 color_profile profile.get_stream(rs.stream.color) intrinsics color_profile.as_video_stream_profile().get_intrinsics() print(ffx{intrinsics.fx}, fy{intrinsics.fy}, cx{intrinsics.cx}, cy{intrinsics.cy}) print(f畸变系数: {intrinsics.coeffs})这里打印出来的内参建议记下来。后面calibrateHandEye和solvePnP用到的相机矩阵和畸变系数可以直接用D435出厂内参也可以自己在标定一次相机内参。我实测D435出厂内参精度已经够手眼标定用省一步是一步。但要注意一个细节分辨率变了内参就要重新取值。如果你用config.enable_stream设置了1280x720分辨率那么get_intrinsics拿到的就是720p的内参不要拿1920x1080的内参硬套720p图像那会导致标定结果完全错乱。3.4 采集时图像质量的几个检查点采图前一定先看一眼实时画面确认以下几个条件棋盘格在画面中完整可见四个角都在画面内部。只要有一个角出画这张图就不能用。光照均匀不要有强反光。D435对高光比较敏感棋盘格白格子过曝时角点检测会失败。机械臂和相机没有相对运动。等机械臂完全停稳后至少再等0.5秒再采图。标定板平面与相机光轴夹角不要太大不要超过45度。夹角太大会导致角点透视变形后检测不稳定虽然OpenCV能检测到但亚像素精度会下降。我建议写一个实时的“采集-预览-保存”脚本先检测角点检测成功且满足条件再自动保存图像和位姿数据。这样比盲采后处理高效得多。4. 核心算法实现与代码解析从solvePnP到calibrateHandEye的完整链路环境好了、数据采完了接下来进入最核心的算法部分。这一节我把完整代码拆开讲每一段都说明为什么这么写参数为什么这么设。4.1 标定板角点检测与位姿解算对每一帧图像先检测棋盘格角点再用solvePnP计算标定板相对于相机的位姿。import cv2 import numpy as np chessboard_size (9, 6) # 内角点数 square_size 0.030 # 格子边长单位米 # 生成棋盘格三维点以标定板中心为原点Z0平面 objp np.zeros((chessboard_size[0] * chessboard_size[1], 3), np.float32) objp[:, :2] np.mgrid[0:chessboard_size[0], 0:chessboard_size[1]].T.reshape(-1, 2) objp * square_size def detect_board_pose(image, camera_matrix, dist_coeffs): gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) ret, corners cv2.findChessboardCorners(gray, chessboard_size, None) if not ret: return None, None # 亚像素精化 criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners_refined cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) # 解算位姿 ret_pnp, rvec, tvec cv2.solvePnP(objp, corners_refined, camera_matrix, dist_coeffs) if not ret_pnp: return None, None # 转4x4齐次矩阵 R, _ cv2.Rodrigues(rvec) T np.eye(4) T[:3, :3] R T[:3, 3] tvec.flatten() return T, corners_refined这里objp的三维点坐标是固定的标定板平面Z坐标设为0角点按行列均匀分布单位是米。square_size必须和你实际打印的棋盘格尺寸完全一致如果打印时缩放过大或过小最后标定出的平移量会有系统性偏差。4.2 机械臂末端位姿转齐次矩阵睿尔曼SDK返回的位姿通常是(x, y, z, rx, ry, rz)rx/ry/rz是欧拉角角度制。转齐次矩阵有两种做法一种用SDK自带接口转一种自己转。建议自己在代码里统一转方便检查。def euler_to_matrix(x, y, z, rx, ry, rz, degreesTrue): if degrees: rx, ry, rz np.deg2rad([rx, ry, rz]) # 按ZYX顺序旋转不同机械臂的欧拉角约定不同以SDK文档为准 Rx np.array([[1, 0, 0], [0, np.cos(rx), -np.sin(rx)], [0, np.sin(rx), np.cos(rx)]]) Ry np.array([[np.cos(ry), 0, np.sin(ry)], [0, 1, 0], [-np.sin(ry), 0, np.cos(ry)]]) Rz np.array([[np.cos(rz), -np.sin(rz), 0], [np.sin(rz), np.cos(rz), 0], [0, 0, 1]]) R Rz Ry Rx T np.eye(4) T[:3, :3] R T[:3, 3] [x, y, z] return T这里有一个坑不同厂家的欧拉角约定不同。睿尔曼可能返回的是固定轴XYZ或者ZYX顺序如果你的代码与SDK定义不一致标定出的旋转矩阵会完全错误而且错误是“整体性”的——你会在后面的精度验证中发现机械臂指点位置差得离谱。最稳妥的办法是先在机械臂零点位姿打印一次SDK返回的欧拉角和自己预期的旋转矩阵对一下。4.3 调用OpenCV手眼标定算法有了两组4x4齐次矩阵列表后一行代码就能完成标定核心计算def calibrate(T_gripper2base_list, T_target2cam_list, methodcv2.CALIB_HAND_EYE_TSAI): # 转为OpenCV需要的格式: (4, 4, N) R_gripper2base np.array([T[:3, :3] for T in T_gripper2base_list]) t_gripper2base np.array([T[:3, 3] for T in T_gripper2base_list]) R_target2cam np.array([T[:3, :3] for T in T_target2cam_list]) t_target2cam np.array([T[:3, 3] for T in T_target2cam_list]) R_cam2gripper, t_cam2gripper cv2.calibrateHandEye( R_gripper2base, t_gripper2base, R_target2cam, t_target2cam, methodmethod ) T_cam2gripper np.eye(4) T_cam2gripper[:3, :3] R_cam2gripper T_cam2gripper[:3, 3] t_cam2gripper.flatten() return T_cam2gripperOpenCV提供了多种求解器CALIB_HAND_EYE_TSAI、CALIB_HAND_EYE_PARK、CALIB_HAND_EYE_HORAUD、CALIB_HAND_EYE_ANDREFF、CALIB_HAND_EYE_DANIILIDIS。我最常用TSAI和DANIILIDIS实测在样本质量好的情况下两者结果非常接近。如果两种方法结果差异巨大说明数据有严重问题需要回到采集环节检查。4.4 标定结果验证用重投影误差说话标定完不能直接去干活必须先验证精度。验证方法很简单固定相机和标定板的位置机械臂移动到几个“测试点”记录机械臂位姿和图像中标定板位姿用标定出的T_cam2gripper把相机坐标系的点变换到机械臂基座坐标看它与机械臂实际位姿的差异。这段验证代码是整个流程的灵魂我每次标定完都跑一遍def verify_calibration(T_cam2gripper, T_gripper2base_test, T_target2cam_test): errors [] for T_g2b, T_t2c in zip(T_gripper2base_test, T_target2cam_test): # 目标点标定板原点在相机坐标系下的三维坐标 p_cam T_t2c[:3, 3] # 变换到机械臂末端坐标系 p_gripper T_cam2gripper[:3, :3] p_cam T_cam2gripper[:3, 3] # 变换到机械臂基座坐标系 p_base T_g2b[:3, :3] p_gripper T_g2b[:3, 3] # 理论上标定板原点在基座坐标系下应该固定眼在手上标定板不动 errors.append(p_base) errors np.array(errors) print(标定板原点在基座坐标系下的波动范围毫米:) print(f X: {errors[:, 0].max() - errors[:, 0].min():.2f}) print(f Y: {errors[:, 1].max() - errors[:, 1].min():.2f}) print(f Z: {errors[:, 2].max() - errors[:, 2].min():.2f})如果波动范围在2-3毫米以内说明标定质量很好5毫米以内可用但建议优化超过10毫米就需要检查数据。5. 手眼标定精度上不去按这个排查思路走一遍这节直接把我踩过的坑和常见的排查方法整理成速查表按照从“数据采集问题”到“算法选择问题”的顺序排查。5.1 数据质量排查优先级症状优先级检查内容重投影波动 10mm高机械臂位姿是否与图像同步是否有运动未停止就采图标定结果矩阵明显异常旋转部分非正交高欧拉角顺序是否正确机械臂位姿转换是否有误部分样本角点检测失败中光照不均匀、反光、板子倾斜过大TSai和Daniilidis结果差异大中姿态多样性不足姿态变化范围不够平移量误差大、旋转误差小低标定板物理尺寸与实际不符5.2 最容易犯的三个错误第一个错误是标定板尺寸填错。square_size单位是米一张A3纸打印30mm格子实际打印出来可能因为打印机缩放导致是29.8mm。毫米级误差听着不大但在500mm工作距离下会对平移量造成等比例的误差。第二个错误是数据配对错位。如果你在采集循环里先移动机械臂再等2秒采图但机械臂运动指令实际没有执行完或者网络延迟导致位姿读取的是旧值那么每帧图像对应的机械臂位姿就是错的。这种错误不是随机噪声而是系统性偏差标定结果必然不可用。第三个错误是标定板不平整。软纸贴在表面不平的桌上或者板子中部弯曲角点检测时会引入局部偏差。这个偏差会直接传导到solvePnP的位姿解算里。5.3 我保留的一个“杀手锏”用多组姿态做交叉验证标定完成后不要急着删除采集数据。我习惯把20-30组数据分成两部分前20组做标定后5-10组做验证或者反过来。每次标定完用验证集算一次重投影误差。更进阶的做法是不加验证集而是直接用全部数据标定然后从原始数据里随机剔除一组观察标定结果的变化幅度。如果剔除一组数据后标定结果变化很大说明数据里可能存在离群点要把那组数据找出来删掉。这一步虽然麻烦但对精度要求高的场景比如精密装配、喷涂轨迹引导非常有用。我以前做一个视觉引导的项目标定结果在验证集上误差4mm看起来还行但利用剔除测试发现一组异常数据删掉后误差降到1.5mm这个发现直接让项目少走了三天弯路。5.4 如果标定板角点经常检测失败D435在逆光、暗光环境下棋盘格的对比度会降低findChessboardCorners会经常返回False。一个简单的预处理技巧是把彩色图转灰度后做一次自适应直方图均衡化CLAHE可以明显提升角点检测的成功率。clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) gray_eq clahe.apply(gray) ret, corners cv2.findChessboardCorners(gray_eq, chessboard_size, None)另外如果标定板与背景颜色相近可以放一张白色A4纸垫在下面人为增加对比度。实测这个土办法非常有效。6. 标定结果如何落地到实际抓取标定完成后T_cam2gripper这个矩阵要能真正用起来才有价值。这一节讲两个最常见的应用场景视觉引导抓取和坐标变换链路。6.1 从相机坐标到机械臂基座坐标的完整变换链眼在手上的场景下拿一个目标物体举例它的坐标变换链路是物体在相机坐标系下的坐标 - 相机到末端 - 末端到基座 - 基座到世界坐标系可选假设我们通过D435的RGB图像检测到目标物体中心点的像素坐标用rs2_deproject_pixel_to_point或者深度图获取物体中心在相机坐标系下的三维坐标P_cam那么物体在机械臂基座坐标系下的坐标是P_gripper T_cam2gripper[:3, :3] P_cam T_cam2gripper[:3, 3] P_base T_gripper2base[:3, :3] P_gripper T_gripper2base[:3, 3]这里的T_gripper2base是机械臂当前末端位姿矩阵每个时刻都在变化。这就是每次抓取前都要重新计算的原因——相机在动末端在动但相机和末端的变换关系是固定的。6.2 一个简化的抓取定位Demo思路我之前用这套标定流程做过一个简单的“视觉引导抓取”demo逻辑不复杂D435识别一个物体的中心点输出像素坐标。利用深度图获取该点的三维坐标P_cam。读取机械臂当前位姿通过标定得到的T_cam2gripper把P_cam转换到基座坐标。机械臂move_l移动到目标点上方下降抓取。这里有个方向问题要特别注意相机坐标系到末端坐标系的旋转关系决定了你从图像上看到的“左右”和机械臂运动的“左右”方向是否一致。标定完成后我先做一个“方向验证”在相机画面里放一个物体计算它在机械臂基座系下的坐标对比它的真实位置。如果左右相反或者前后相反通常是在T_gripper2base的取法上出了问题而不是标定本身错。6.3 深度图的坐标转换与RGB对齐问题如果你要用的不是目标中心点而是深度图直接给出的三维坐标要注意D435深度图和RGB图是不同传感器坐标系原点不同。我一般建议在pyrealsense2里做一次align操作把深度图对齐到RGB坐标系下这样像素点一一对应三维坐标取值才准确。align_to rs.stream.color align rs.align(align_to) frames pipeline.wait_for_frames() aligned_frames align.process(frames) color_frame aligned_frames.get_color_frame() depth_frame aligned_frames.get_depth_frame()对齐后depth_frame.get_distance(x, y)得到的就是RGB像素坐标(x, y)处对应的深度值单位是米。利用rs2_deproject_pixel_to_point可以把像素坐标和深度值转成相机坐标系下的三维点。depth_intrinsics depth_frame.profile.as_video_stream_profile().get_intrinsics() point_3d rs.rs2_deproject_pixel_to_point(depth_intrinsics, [x, y], depth)这个方法返回的point_3d就是P_cam了。7. 最后分享几个实操中的小经验标定这件事听起来是固定的流程实际操作中有很多细节决定成败。最后把这几次项目中沉淀下来的经验写在这里算是给后来人的礼物。第一采集数据的脚本一定要做成“边采边校验”的模式。不要等采完20组数据再去处理而是每采一组立刻检测角点、计算位姿、保存数据并在界面上显示当前累计的姿态覆盖情况。如果发现连续几组姿态太接近及时手动调整机械臂的移动范围。我就是一个偷懒吃了大亏开始采了25组处理时发现其中18组姿态几乎一样等于用8组有效数据标定精度自然拉胯。第二机械臂的运动要“慢”但姿态要“花”。慢是确保每个采集点相机和机械臂都完全静止花是让旋转矩阵的轴线充分分散避免方程退化。实际操作中我每采一个点都会人工判断一下机械臂末端的姿态是否和之前的某几个点过于接近如果接近就微调一下角度重新采。第三标定板不要离相机太远。有人为了拍到更大的场景把标定板放很远虽然角点能检测到但位姿解算的误差会随距离放大。我的经验是标定板在相机画面中占1/2到2/3画幅时标定质量最好。如果实在受限于机械臂工作空间可以分近、中、远三组距离采集然后分别标定选验证误差最小的一组。第四不要把验证实验省掉。标定矩阵看着像模像样不经过验证就上线生产环境里很容易出事故。再强调一遍重投影误差是你唯一应该相信的数字。如果条件允许用激光跟踪仪或者高精度千分表打几个点验证是最硬核的办法。第五标定的本质是“状态估计”不是“精确测量”。即使标定结果很好相机本身的深度误差、机械臂重复定位精度、标定板打印误差加在一起整个视觉引导系统最终的绝对精度通常在3-8毫米左右。如果你的应用要求的精度在1毫米以内手眼标定只是基础后续还要做TCP补偿、相机温漂补偿、机械臂运动学标定等一系列工作。这不是泼冷水而是让你对系统的真实能力有合理预期。做完一整轮手眼标定我最大的感受是这套流程的瓶颈永远不在算法代码上而在数据质量上。把数据采好标定结果自然就好数据一塌糊涂再花哨的算法也救不回来。希望这篇文章能让你少走几个弯把宝贵的时间花在真正有意思的机器人应用上。