
1. 项目概述为什么UR3和L515一定要做手眼标定先聊一个我踩过的大坑。刚接触UR3机械臂那会儿我以为只要把Realsense L515往机械臂旁边一放相机能看见目标机械臂能抓过去这事儿就完了。结果真跑起来才发现相机告诉我的坐标和机械臂实际抓取的位置能差出十几毫米抓个螺丝都费劲。后来才明白这里面缺的不是精度而是两个坐标系之间的“翻译官”——手眼标定。说白了手眼标定要解决的就是一个非常朴素的问题相机看到的物体它的坐标是在相机坐标系下的而机械臂要抓它需要的是在机械臂基坐标系下的坐标。这两个坐标系之间是什么关系必须通过标定来算出来。对于UR3这种六轴协作机器人配L515这种深度相机不做这一步视觉引导抓取、无序分拣这些功能全都是空中楼阁。这个项目适合谁参考如果你正在做UR系列机械臂的视觉抓取或者你手里有一台Realsense系列相机想接到机械臂上做定位又或者你只是被“手眼标定”这四个字吓住、想搞明白它到底是什么原理那么这篇文章应该能帮到你。我会把从理论到实操从数据采集到结果验证的完整过程都捋一遍包含我实际踩过的坑和最终验证有效的参数方案。2. 手眼标定核心原理一个方程里的两个坐标系2.1 从AXXB说起手眼标定的数学本质就是求解一个矩阵方程AXXB。这个方程看起来简单但它背后藏着整个标定过程的全部逻辑。先别被这个式子吓跑我用一个生活化的方式来解释。把机械臂的末端法兰盘比作你的手掌把相机比作你的眼睛。现在你有两种“看”的方式眼睛长在手上的眼在手上eye-in-hand以及眼睛长在外面一直盯着手看的眼在手外eye-to-hand。UR3配合L515最常见的安装方式是eye-in-hand也就是把L515固定在UR3的末端法兰盘上相机跟随机械臂一起运动。不管哪种安装方式我们都在找同一个东西相机坐标系和机械臂末端坐标系之间的变换关系。方程里A是机械臂末端在两个不同位姿之间的变换矩阵B是相机在这两个位姿之间观察到的标定板位姿变换矩阵而X就是我们要求的相机到末端的固定变换。为什么AXXB能成立因为机械臂带着相机从位姿1移动到位姿2机械臂自己知道自己末端动了多少A相机也知道自己相对标定板动了多少B这两个“知道”之所以能对得上就是靠中间这个固定的X。解出X就相当于知道了眼睛和手掌之间的相对位置关系。2.2 eye-in-hand和eye-to-hand的判断逻辑很多初学者上来就乱套公式结果标出来的结果一塌糊涂。我建议你在动手之前先花五分钟搞清楚自己的安装方式。如果你把L515固定在UR3末端相机跟着机械臂跑这叫eye-in-hand。这种情况下标定板固定在机械臂工作空间内的一个位置不动机械臂带着相机从不同角度去拍它。求解目标是相机到末端的变换通常记作T_cam2tool。如果你的相机固定在天花板或者支架上机械臂在相机视野里活动这叫eye-to-hand。这时候标定板要固定在机械臂末端让机械臂带着它走进相机视野。求解目标是相机到机械臂基座的变换通常记作T_cam2base。我这个项目的硬件配置是UR3协作机器人末端安装L515标定板固定在桌面上。这是典型的eye-in-hand结构。下面所有操作和代码都是围绕这个结构展开的eye-to-hand的思路可以类比但公式方向不同别混用。2.3 为什么要强调数据质量和姿态多样性方程AXXB不是随便采几组数据就能解的。它的求解精度重度依赖输入数据的质量。A矩阵来自机械臂的位姿读数B矩阵来自相机对同一块标定板的位姿估计这两个输入只要有一个不准确X的结果就偏移。更关键的一点是机械臂采集数据时的姿态必须足够多样化。如果只是让机械臂平移着拍或者只在很小的角度范围内变化那么方程会接近病态解出来的X误差极其夸张。我实测下来同样的算法如果姿态范围太小重投影误差能到10个像素以上如果把姿态铺开误差能压到1个像素以内。后面我会给出具体的姿态规划方案。3. 工具选型与准备从硬件到软件的一次配齐3.1 L515和标定板的选择Realsense L515是英特尔的一款基于激光扫描技术的深度相机它有RGB图像和深度图像两种数据流。做手眼标定主要用的是RGB图像来检测标定板的角点深度图用于后续的抓取定位。L515的分辨率和帧率足够视野也比较适中配合UR3的臂展来说挺合适。标定板这块我推荐使用ChArUco板而不是传统的棋盘格。原因有两个一是ChArUco板即使在部分遮挡的情况下也能检测出足够多的有效角点这对机械臂运动过程中可能出现的边缘遮挡非常友好二是OpenCV对ChArUco板有开箱即用的检测函数不需要自己写复杂的角点提取逻辑。我用的是一个A4大小的ChArUco板具体参数是方块大小25mm标记大小18mm行列各8个格子。打印出来之后贴在硬纸板上尽量保证板面平整。这里有一个很容易被忽略的点标定板一旦打印出来它的物理尺寸就是固定的了代码里的squareLength和markerLength必须和实物严格一致差1mm都不行否则标出来的外参直接报废。3.2 软件栈OpenCV、realsense SDK和UR3的通信软件环境我强烈建议直接用Python配合pyrealsense2、OpenCV和numpy再配合UR3的socket通信来读取机械臂位姿。这些库安装非常简单网上教程也多遇到问题好查。版本上我用的Python 3.8OpenCV 4.5.xpyrealsense2跟随Realsense SDK的版本走。UR3那边获取位姿的方式主要有两种一是通过URScript的get_actual_tcp_pose()函数来读这需要你写一个小的socket服务端跑在UR3的控制器上二是如果你用ROS可以用MoveIt的接口直接拿。我的方案是用socket方式直接在UR3控制屏上写一段脚本把实时位姿发出来Python这边接收解析成4x4齐次变换矩阵。提示UR3的TCP位姿输出格式是[x, y, z, rx, ry, rz]前三项是位置后三项是旋转向量。需要先用cv2.Rodrigues()把旋转向量转成旋转矩阵才能组装成4x4的变换矩阵。这个细节坑过很多人。3.3 手眼标定算法库的选择OpenCV从4.x开始在calib3d模块里带了cv2.calibrateHandEye()函数这就是标准的AXXB求解器支持Tsai、Park等多种解法。这个函数直接输入两个列表——机械臂末端位姿的列表和相机检测到的标定板位姿的列表输出就是相机到末端的变换矩阵。这个接口对Python用户极其友好而且结果稳定完全没必要自己去实现SVD分解之类的东西。不过要提醒一句OpenCV的calibrateHandEye要求输入的位姿列表顺序一一对应也就是机械臂第i个位姿对应相机第i帧检测到的标定板位姿。这个对应关系绝对不能错否则方程就乱了结果肯定不对。我一开始就是没注意帧同步结果外参乱得像一锅粥。4. 手眼标定实操全流程一步步把数据喂给方程4.1 第一步安装L515并确认图像流正常先把L515用USB3.0线接到电脑上装上Intel RealSense SDK。然后用realsense-viewer软件确认RGB流和深度流都能正常输出。这里有一个实操要点L515对USB接口带宽要求比较高一定要插在USB 3.0以上的接口上否则图像会掉帧或者黑屏。我遇到过几次在USB 2.0口上完全无法启动的情况别在硬件上省事。确认图像流正常之后用pyrealsense2做一次简单的读取测试import pyrealsense2 as rs import cv2 import numpy as np pipeline rs.pipeline() config rs.config() config.enable_stream(rs.stream.color, 1280, 720, rs.format.bgr8, 30) profile pipeline.start(config) try: for _ in range(10): frames pipeline.wait_for_frames() color_frame frames.get_color_frame() if not color_frame: continue color_image np.asanyarray(color_frame.get_data()) cv2.imshow(color, color_image) cv2.waitKey(1) finally: cv2.destroyAllWindows() pipeline.stop()这段代码是在验证L515的RGB流是否能正常读取。注意这里没有读取深度流因为标定只需要RGB深度流后面做抓取定位的时候才用。L515有一个特性它的RGB摄像头和激光深度模块在硬件位置上是有偏移的所以在使用深度图做定位时需要用realsense SDK提供的内参和外参去做对齐这是另一个话题这里先不展开。4.2 第二步准备UR3的位姿读取通道我选择在UR3控制屏上写一个URScript脚本把实时TCP位姿通过socket发送到电脑上。UR3默认的socket端口是30001这是一个实时反馈端口以10Hz的频率推送机械臂状态。直接在这个端口上读取数据解析出位姿信息是最省事的方案。下面是我在UR3控制屏上使用的URScript脚本很简单def stream_pose(): while True: pose get_actual_tcp_pose() socket_send_string(str(pose[0]) , str(pose[1]) , str(pose[2]) , str(pose[3]) , str(pose[4]) , str(pose[5])) sync() sleep(0.1) end这个脚本把TCP位姿以“x,y,z,rx,ry,rz”的格式发送到端口30001。电脑端用socket接收并解析。UR3的get_actual_tcp_pose()返回的是末端工具中心点的位姿也就是法兰盘坐标系的位姿。如果你的末端还装了额外的治具需要注意区分TCP和法兰盘中心——手眼标定用的位姿必须是相机安装面的位姿通常就是法兰盘坐标系除非你单独配置了工具坐标系。电脑端接收并转换的代码大概长这样import socket import struct HOST 192.168.1.100 # UR3控制器的IP PORT 30001 sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((HOST, PORT)) def read_tcp_pose(): # 在30001端口上接收到的数据包含文本信息直接按行读取 data sock.recv(4096).decode(utf-8) # 这里只截取最后一行带位姿的数据做解析 lines data.strip().split(\n) for line in reversed(lines): parts line.split(,) if len(parts) 6: try: x, y, z, rx, ry, rz [float(v) for v in parts] return np.array([x, y, z, rx, ry, rz]) except ValueError: continue return None严格来说UR3的30001端口发的是二进制数据加上一些文本直接裸解析可能会混乱。更稳妥的方式是用UR3的Dashboard端口或者用专门的UR通信库比如ur_rtde。我实测下来用ur_rtde来读实际TCP位姿最省心它已经封装好了RTDE协议稳定性和实时性都很好。import ur_rtde rtde ur_rtde.RTDE(192.168.1.100) rtde.start() pose rtde.getActualTCPPose() rtde.stop()无论你选哪种方式核心要求只有一个读到的TCP位姿和L515拍到的图像必须是同一时刻的。如果时间对不上比如机械臂已经在运动了但你拍到的是运动模糊的旧帧或者位姿已经更新了但图像还是上一帧的那么A和B就不匹配标定结果必然是错的。4.3 第三步规划标定采集的姿态序列这一步是整个标定过程中最影响成败的环节。我见过很多人在这一步偷懒随便让机械臂动几个位置就开拍结果标定结果惨不忍睹。手眼标定对姿态的要求可以总结为三点第一姿态变化要足够大。绕各个轴的旋转角最好覆盖±30度以上。如果机械臂只是在某个位姿附近微调旋转角度只有几度那么法方程会退化求出来的旋转矩阵误差极大。第二平移和旋转要同时变化。不要只平移不旋转也不要只旋转不平移。理想情况是机械臂带着相机在空间中画出各种角度和大小的轨迹每一帧都有明显的平移变化和旋转变化。第三标定板要在图像中占据足够大的面积。太小了角点检测不稳定太大了容易超出视野。我记得经验值是标定板在画面中的宽度占图像宽度的三分之一到二分之一之间比较理想并且要尽量让标定板成像清晰不要有运动模糊。我用的是UR3的示教器手动模式预先示教了12个不同的拍照位姿让机械臂自动循环移动并拍照。这些位姿不均匀分布在机械臂的workspace里有些偏左有些偏右有些仰视有些俯视有些让标定板处在图像的左上角有些在右下角。这样做的目的是让机械臂末端的空间位置和姿态都充分“张”开让AXXB方程能在一个尽量大的空间范围里找到最优解。如果你用ROS驱动或者UR3的Python SDK也可以直接写脚本让机械臂自动到一组规划好的关节角位置。我建议至少采集15到20组数据少于10组的话标定结果会很不稳定尤其是旋转分量的可重复性非常差。4.4 第四步检测标定板并计算相机位姿拿到图像和对应的机械臂位姿之后下一步就是用OpenCV检测ChArUco板并计算相机相对于标定板的位姿。这一步实际上是利用PnP求解标定板的每个角点在板坐标系下的坐标是已知的在图像中的像素坐标已经被检测到那么相机相对于板的旋转和平移就能求出来。这里贴一个核心的检测和位姿求解代码import cv2 import cv2.aruco as aruco import numpy as np dictionary aruco.getPredefinedDictionary(aruco.DICT_4X4_50) board aruco.CharucoBoard_create(8, 6, 0.025, 0.018, dictionary) # 相机内参和畸变系数先用相机标定得到 camera_matrix np.array([[640.0, 0, 640.0], [0, 640.0, 360.0], [0, 0, 1.0]]) dist_coeffs np.zeros((1, 5)) def detect_board_pose(image): gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) corners, ids, rejected aruco.detectMarkers(gray, dictionary) if ids is None: return None retval, charuco_corners, charuco_ids aruco.interpolateCornersCharuco(corners, ids, gray, board) if charuco_corners is None or len(charuco_corners) 4: return None valid, rvec, tvec aruco.estimatePoseCharucoBoard(charuco_corners, charuco_ids, board, camera_matrix, dist_coeffs) if valid: return rvec, tvec return None注意这里用到了相机的内参和畸变系数。这个内参必须先单独对L515做一次相机标定拿到不要偷懒跳过。相机的内参不准后面标定外参全都是空中楼阁。L515的RBG摄像头建议用OpenCV的calibrateCamera来标定可以打印一个棋盘格从各个角度拍二三十张然后跑一遍标定程序。如果不想自己打板也可以直接用Realsense SDK提供的相机内参但要确认散粒畸变模型参数是否被正确读取到了。4.5 第五步组装数据求解AXXB检测到每一帧的rvec和tvec之后把它们转换成4x4的齐次变换矩阵这代表的是标定板在相机坐标系下的位姿通常记作T_board2cam。我们要的是相机的位姿也就是它的逆矩阵T_cam2board。然后结合UR3读到的机械臂末端位姿T_tool2base就能组装出calibrateHandEye需要的两个输入。具体来说我们需要两个列表R_gripper2base, t_gripper2base机械臂末端在基坐标系下的旋转矩阵和平移向量来自UR3的TCP位姿。R_target2cam, t_target2cam标定板在相机坐标系下的旋转矩阵和平移向量来自PnP求解注意这里要用相机相对于板的位姿不要用反了。然后直接调用OpenCV函数R_cam2tool, t_cam2tool cv2.calibrateHandEye( R_gripper2base, t_gripper2base, R_target2cam, t_target2cam, methodcv2.CALIB_HAND_EYE_TSAI ) T_cam2tool np.eye(4) T_cam2tool[:3, :3] R_cam2tool T_cam2tool[:3, 3] t_cam2tool.reshape(3)标定完成后得到的T_cam2tool就是从L515相机坐标系到UR3末端坐标系的变换矩阵。这就是我们一开始要找的“翻译官”。有了它相机里看到的任何点都能变换到机械臂基坐标系下让UR3去抓取。我建议在采集完数据之后先对数据做一个简单的可视化检查把每一帧里检测到的角点数、重投影误差打出来把明显不好的帧挑出来删掉。脏数据对AXXB的影响很大一两帧坏点就足以把结果带偏。这个筛选过程不要舍不得删数据宁缺毋滥。4.6 第六步标定结果验证——不是算出来就完了算出一个T_cam2tool矩阵不等于标定成功。我见过太多人跑到这一步就收工结果第二天换了一个位置放目标物体抓取又偏了。原因很简单你没有验证标定精度。最直接有效的验证方法是“三个点重投影法”把相机手抓住的物体比如一个带尖端的笔放到某个位置用相机识别出这个点在相机坐标系下的坐标然后用T_cam2tool把它变换到末端坐标系再结合UR3当前的TCP位姿算出它在基坐标系下的坐标让UR3移动到那个坐标看看末端是否精确到达了物体尖端。具体计算流程是已知点在相机坐标系下的坐标P_cam经过T_cam2tool变换到末端坐标系P_tool再经过ur3当前的末端位姿T_tool2base变换到基坐标系P_base。P_base就是UR3需要过去抓的位置。我实测下来质量好的标定这个验证的误差应该在5mm以内。L515本身的深度精度是毫米级加上机械臂的重复定位精度、相机标定误差和手眼标定误差5mm是一个比较合理的目标。如果验证误差在10mm以上说明标定肯定有问题需要回头检查数据质量。另一个验证方式是用重投影误差把标定出的T_cam2tool代回方程用机械臂的位姿去预测标定板的角点位置和实际检测到的位置对比。如果平均重投影误差在1个像素以内说明标定质量不错如果超过2-3个像素说明数据或者模型有问题。5. 采集过程中的常见问题与排查技巧5.1 角点检测失败或者检测出的角点数太少这是我最常遇到的问题。ChArUco板在光照不足或者反光强烈的情况下检测成功率会大幅下降。L515的RGB摄像头动态范围一般在窗户旁边拍容易过曝。解决思路有三个一是调整标定板的角度尽量避免正对强光源二是固定曝光时间不要用自动曝光因为自动曝光变化会导致图像亮度波动影响角点检测稳定性三是在代码里对图像做一下直方图均衡化增强对比度再送检测。另外标定板和机械臂末端之间的距离不能太远。L515的RGB分辨率虽然还行但远了之后角点之间的像素距离太小检测精度就会下降。我建议标定板距离相机控制在0.4到1.0米之间太远过近都不合适。5.2 机械臂位姿和相机图像不同步这个问题在低速运动时不容易察觉但只要机械臂速度快一点误差就会明显变大。UR3的30001端口数据频率是10HzL515的帧率是30Hz两个数据流之间的时间戳如果不做对齐就会导致A和B错位。我一开始用了一个“笨办法”让机械臂停在每个标定位姿上静止2秒等相机拍到稳定的图像后再同时记录机械臂位姿和图像帧。这样虽然慢但能确保每一组数据都是同一个静止状态下采集的时间和空间完全一致。对于一次标定来说多花两分钟完全值得。5.3 标定结果莫名其妙跳变如果你反复标定发现结果每次都不一样尤其旋转分量的差值很大那么多半是姿态多样性不够。我排查过几次最后发现都是因为标定板在画面中的位置太靠中心机械臂的姿态变化范围太小引起的。解决办法是重新规划采集路径让机械臂从高到低、从左到右、远近交替地拍每一帧都要让标定板的位置和角度有明显变化。再一个就是增加数据量从12组加到20组甚至更多几次标定之间的重复性会明显好起来。5.4 相机内参不准确带来的连锁反应有一次标定完成后验证时发现标定板正前方的点误差很小但偏离视野中心的点误差越来越大。排查了半天最后定位到问题出在L515的内参标定上。L515的RGB镜头畸变比较明显如果直接用出厂内参而不做畸变矫正图像边缘的角点位置误差就会被放大。如果你遇到类似的情况我建议不要偷懒直接做一次完整的相机内参标定把新的camera_matrix和dist_coeffs喂给PnP求解。这一步能省掉后面无数麻烦。5.5 常见问题速查表现象可能原因排查方向重投影误差大于3像素姿态多样性不够或相机内参不准增加采集姿态重新标定相机内参验证抓取误差大于10mm数据时间不同步或脏数据未剔除检查时间戳对齐筛选有效数据帧标定结果重复性差数据量太少或标定板检测不稳定增加到20组以上数据调整光照和标定板角度角点检测经常失败光照过强或过弱标定板不平整固定曝光换平整标定板增强图像对比度L515图像黑屏或掉帧USB带宽不足线材质量差换USB3.0口换高质量USB线降低分辨率或帧率6. 标定效果实测记录一组可复现的数据参考最终我的标定结果是T_cam2tool的旋转分量大概是绕x轴1.532弧度绕y轴-1.104弧度绕z轴-1.527弧度平移分量大约为x方向30.2mmy方向-12.8mmz方向26.5mm。这组数字是跟我的具体安装方式强相关的比如L515在法兰盘下的安装支架高度和角度、相机方向朝向都会直接影响结果所以不同人做出来完全不一样不需要跟我的对比。更有参考价值的是精度数据。我用20组数据进行标定平均重投影误差在0.8像素左右验证时抓取笔尖的重复误差小于3mm。这个精度能满足大部分轻量级抓取任务比如抓取螺丝、小零件、或者做简单的无序分拣。另外我实际验证中发现深度图的准确度对最终抓取精度影响也很大。L515在0.3米到1.5米的范围内深度精度很好超出这个范围误差会快速增大。如果你的目标物体距离相机超过2米建议重新考虑相机的高度和角度或者换用测距范围更大的相机型号。7. 一些实际经验总结做手眼标定这件事说难其实不难核心就是一台相机、一块标定板、一个机械臂位姿读取通道。但说简单也不简单因为每一步都有细节决定成败。根据我实测的经验有几个点想特别跟大家强调首先标定板的质量绝对不能将就。打印出来发现纸面褶皱或者反光厉害直接换一块硬质板材重打不值得在上面省时间。标定板尺寸越大检测越稳定但也不要大到你机械臂活动空间里放不下的程度。其次数据采集阶段宁可多采不可少采。我一般每次标定都采25组左右的数据然后筛选出其中重投影误差最小的15到20组喂给求解器。多采集的最大好处是你可以放心大胆地把明显异常的数据扔掉而不会担心剩余数据不够用。第三保持机械臂运动平稳。虽然我在数据采集时让机械臂在每个点位停顿但从一个点位移动到另一个点位的过程不要让机械臂剧烈抖动。UR3的控制器在运动启动和刹车时有轻微震荡如果不等它稳定下来就拍照图像会有一点点模糊角点检测的位置可能偏差0.2个像素左右。这0.2个像素累积到外参里可能就是1-2毫米的末端误差。最后标定结束后的验证千万不要省。花五分钟做一个“相机眼里看到的点机械臂能不能精确指过去”的实测比任何数学指标都更能说明问题。我一般会用棋盘格里某个特定角点做验证目标让机械臂末端固定一根尖笔反复测量几次确认重复性都在5mm以内才算这个标定真正完成了。