news 2026/10/7 15:05:35

UR3与Realsense L515手眼标定实战:从AX=XB到抓取精度校准

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
UR3与Realsense L515手眼标定实战:从AX=XB到抓取精度校准

干这行的人应该都有过类似的经历:UR3装好了、Realsense L515固定好了,抓取程序也能跑通,但机器人抓个杯子就是差那么几厘米,不是偏左就是怼到物体上。我一开始还以为是视觉识别误差,反复调YOLO阈值、换颜色过滤,折腾了两天没进展。后来才反应过来,问题根本不在识别,而是相机和机械臂之间压根不在同一个坐标系里。这就引出了本文要讲的核心内容:UR3和Realsense L515之间的手眼标定。

这篇文章会把我实际操作中踩过的坑、修正过的参数、验证过的流程全部整理出来,重点回答三个问题:手眼标定到底在解什么(或者说AX=XB里那个X为什么那么关键)、UR3加L515这个组合有什么特殊性、以及从采数据到求解再到验证,完整闭环应该怎么做。如果你正在做机械臂抓取、视觉引导定位、或者任何涉及"相机看到的"要和"机械臂摸到的"对齐的工作,这篇可以直接当操作手册来用。

1. 手眼标定的核心逻辑:先搞明白AX=XB和坐标系关系

很多教程上来就让你装easy_handeye、跑一个demo,然后说"你看标定完成了"。但如果你不理解背后的坐标系变换,一旦结果不对,你根本不知道是该重新采数据、换算法,还是改代码里哪个inv()。所以这一节我先把数学模型讲清楚。

1.1 三种坐标系与手眼矩阵的物理含义

手眼标定涉及的坐标系,说穿了就三个:

  • 机器人基座坐标系(base):UR3的所有运动都是相对这个坐标系规划的。
  • 末端/工具坐标系(tool,对应UR的flange或自定义TCP):UR3能实时告诉你工具末端在base下的位姿,也就是T_base_tool。
  • 相机坐标系(camera):Realsense L515观测到的所有三维点都在这个坐标系下。
  • 标定板坐标系(board):贴在标定板上的原点,用于表达"相机看到了什么"。

相机固定在机械臂末端上这种方式,叫eye-in-hand(眼在手上)。相机在机器人末端移动时,标定板放在工作台面上不动。我们要标定的,就是相机坐标系相对于末端工具坐标系的变换矩阵X = T_tool_camera。为什么这个矩阵重要?因为只要知道了X,机械臂就能把相机看到的任何一个点的三维坐标,换算到自己的base坐标系里,从而完成抓取。

eye-in-hand只是其中一种。如果你的相机固定在天花板或者支架上,机械臂在它视野里运动,那叫eye-to-hand(眼在手外)。这种方式要标的是相机相对base的变换。UR3上最常见的是eye-in-hand,但无论哪种,核心逻辑都是同一个方程,只是X的位置和含义不同。

1.2 AX=XB这个方程是怎么推出来的

这个方程的推导是整个标定的灵魂,理解了它,后续所有的数据采集策略你就能自己想明白。

假设有一个固定不动的标定板,相机固定在机械臂末端。对于任意一个机械臂位姿,下面这个变换链都是成立的:

T_base_tool * T_tool_camera * T_camera_board = T_base_board

机械臂末端位姿(T_base_tool)是UR3实时读出来的,相机到标定板的变换(T_camera_board)是相机通过拍摄标定板算出来的,手眼矩阵T_tool_camera就是未知数X。而标定板在base坐标系中的位姿T_base_board是固定不变的。

现在让机械臂动到两个不同的位姿,分别记为位姿1和位姿2,两个式子都成立。由于T_base_board两边相等,把两个式子联立:

T_base_tool1 * X * T_camera_board1 = T_base_tool2 * X * T_camera_board2

两边同时左乘inv(T_base_tool2),右乘inv(T_camera_board1),整理一下就得到了经典的AX=XB形式:

A * X = X * B

其中:

  • A = inv(T_base_tool2) * T_base_tool1,表示机械臂末端从位姿1到位姿2的相对运动;
  • B = T_camera_board2 * inv(T_camera_board1),表示标定板在相机视野里从观察1到观察2的相对运动。

一句话总结:机械臂末端走了多远,相机就应该看到标定板反向走了多远(因为坐标变换的方向相反)。X就是把这两段"相对运动"对齐起来的那个固定变换。不是凑出来的拟合,而是严格数学推导下的最优解。

2. 为什么这套组合容易被坑:UR3与L515的选型逻辑和限制边界

在做标定之前,我建议你先搞清楚两个硬件的脾气。UR3和L515单拎出来都是好东西,但组合在一起有几个隐形的"坑"跟选型强相关。

2.1 L515的特殊之处:短距离高精度,但也有先天限制

Realsense L515和常见的D435i不一样。D435i是主动红外立体视觉,L515用的是固态激光雷达式ToF方案(MEMS微振镜扫描),它的优势在高精度上:官方标称在0.5米距离处深度精度可以达到0.6mm左右,这比同价位的立体视觉方案要好不少。UR3本身负载只有3kg,抓一些细小零件时,1mm的误差都可能致命,所以L515非常适合这种场景。

但代价也明显:

  • 工作距离短:L515的有效深度范围大约是0.25~1.5米,超过1米以后噪声明显增大。标定时相机到标定板的距离如果超过0.8米,深度信息的可靠性会显著下降,而标定又不是只用深度图,彩色图同样受影响。
  • 振镜扫描机制导致运动伪影:L515的深度是激光逐行扫描出来的,物体一移动,深度图就会产生扭曲变形,术语叫"motion blur"。所以采集数据时机械臂哪怕只是轻微抖动,都可能导致整张深度图不可用。
  • L515已经被Intel停产:现在想买全新的是买不到了,只能买二手或者用备件。这个事实导致它在一些机器人框架里的驱动适配停留在旧版本,比如和较新的realsense2-ros版本存在兼容性问题。如果你用的是ROS2,可能需要手动从源码编译。

2.2 UR3的精度和接口参数

UR3的重复定位精度官方写的是±0.1mm级别(UR3e标称±0.03mm)。在实际手眼标定中,这个精度足够当"真值"来用了。UR3提供多种方式读取当前TCP位姿,我推荐直接用RTDE接口或者Python的urx库,因为它能直接给出(x, y, z, rx, ry, rz)格式的TCP位姿,换算成变换矩阵非常方便。

需要注意,UR3控制柜里可以设置多个TCP,如果你在示教器上定义了一个自定义TCP(比如装了一个气爪),那么get_pose()返回的就是这个自定义TCP在base下的位姿。如果你把相机和法兰固连,建议直接用法兰坐标系做手眼矩阵的参照,也就是让TCP等于法兰中心。否则你标出来的X就是相机到气爪TCP的变换,后续换算多一步没关系,但容易绕晕。我的建议是:标定期间统一使用法兰坐标系,不要选自定义TCP。

2.3 为什么"这个组合"经典

你搜索的时候会发现,UR系列+L515是很多视觉抓取demo的标准组合。原因有两个:一是UR3标称精度高、重复性好,适合做精确的视觉伺服验证;二是L515本身带RGB和深度,且颜色对齐效果不错,做二维标定和三维抓取都能兼顾。另一个潜在原因是两者都是"典型的机器人学教材级设备",在UR官方的UR+生态和ROS的robotics社区里有大量现成软件包,问题排查资料多。

但"经典组合"不代表"顺手就能用"。因为L515的短距离限制和扫描伪影问题,如果你用常规的机械臂录像走一圈的方式采集数据,大概率要翻车。下面从准备阶段开始认真讲。

3. 从零准备:标定板、驱动与API的数据基础

标定前的准备,重点在标定板材质尺寸、驱动版本和数据接口三件事。任何一件没做到位,后面全白搭。

3.1 标定板怎么选:我为什么用Charuco而不是普通棋盘格

最常见的候选是普通棋盘格和ArUco码,但我强烈推荐Charuco板。原因很实际:

  • 棋盘格必须完整出现在图像里才能检测出全部角点,L515视野有限,相机稍微倾斜,棋盘格边缘一截出画面就检测失败。
  • Charuco板由ArUco码拼成,允许部分遮挡,只要看到足够多的码就能恢复出角点网格,对视角要求宽松很多。
  • Charuco板每个格子是全黑或全白,角点可以通过亚像素细化,精度比单纯用ArUco码的中心点高不少。

尺寸方面,我的经验是:标定板物理边长(每个格子)8~15mm,整板长宽控制在15~25cm。不要弄太大,UR3的工作半径才500mm,标定板太大很难在多个位姿下都能完整进入视野。打印时务必用哑光纸,不要覆亮膜。L515的激光扫到反光面上,深度图会出现大量空洞,彩色图也会出现高光导致角点检测偏移。

有条件的话把打印好的Charuco板贴在铝塑板或亚克力硬板上,保证板面平整。柔性的纸板弯折几毫米,在求解时等效于标定板位姿变化,直接污染数据。

3.2 安装驱动的版本避坑

我用的是Ubuntu系统,Python环境。以下是我测试通过的版本组合,可以直接抄作业:

# Python环境推荐3.8~3.10 pip install pyrealsense2==2.54.2 pip install opencv-python==4.8.1.78 pip install opencv-contrib-python==4.8.1.78 pip install numpy pip install urx # 读取UR3位姿用的

一个亿级重要的坑:opencv-python和opencv-contrib-python必须版本完全一致,否则cv2.aruco.CharucoBoard_create等aruco模块会报错或者行为诡异。另一个坑是pyrealsense2不要装最新版,最新版配合旧L515固件会出现深度流无法启动或者彩色流和深度流时间戳对不齐的问题。装完以后,用这个命令验证一下设备是否正常枚举:

rs-enumerate-devices | grep -A 5 "L515"

如果设备固件版本太旧,可能需要用Realsense Viewer升级固件到5.13.0.50以上。

3.3 需要提前写好的两个数据接口

正式采集前,我把会用到的关键代码框架列一下,确保后面采集时不会手忙脚乱。

接口一:从UR3读取当前TCP位姿

用urx读取非常直接:

import urx robot = urx.Robot("192.168.1.2") # UR3的IP pose = robot.get_pose() # 返回一个4x4齐次变换矩阵,即 T_base_tool # 需要把 pose 保存成 4x4 numpy 数组

如果你不习惯用urx,也可以用RTDE:

# 使用 rtde_control 和 rtde_receive 包 from rtde_receive import RTDEReceive as RTDEReceive rtde_r = RTDEReceive("192.168.1.2") pose_rtde = rtde_r.getActualTCPPose() # 返回 [x, y, z, rx, ry, rz],需要先用ur_rtde库或自己转成旋转矩阵

我实际用下来urx更省事,因为直接返回矩阵,省去了从旋转向量转矩阵这一步。但如果你打算用UR的实时数据流做其他事(比如在线视觉伺服),RTDE更好,位姿刷新频率更高。

接口二:从L515同步获取彩色图和深度图并做相机位姿估计

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) config.enable_stream(rs.stream.depth, 1280, 720, rs.format.z16, 30) profile = pipeline.start(config) # 彩色图和深度图对齐 align = rs.align(rs.stream.color) frames = pipeline.wait_for_frames() aligned_frames = align.process(frames) color_frame = aligned_frames.get_color_frame() depth_frame = aligned_frames.get_depth_frame() color_image = np.asanyarray(color_frame.get_data()) depth_image = np.asanyarray(depth_frame.get_data()) # 获取彩色相机内参 color_intrinsics = color_frame.profile.as_video_stream_profile().get_intrinsics()

然后检测Charuco板并计算T_camera_board,这是流程里最绕的一步,放在下面讲。

4. 采数据不是随便拍:姿势选取和样本多样性的讲究

这一步是手眼标定成败的分水岭。我看到很多人标定结果飘忽不定,重投影误差就是降不下来,根源几乎都出在数据采集的姿势太单一上。

4.1 为什么"姿势多样性"比"数据量多"更重要

从数学上看,AX=XB的求解本质是通过多组机械臂运动约束来估计X。如果机械臂的运动模式很单一(比如每次都只绕一个轴旋转,或者只做平移),那么约束方程会变成病态的,算出来的X在某个方向上误差非常大。

我自己的教训是:第一轮采集了30组数据,全部是让机械臂从正上方平移去拍标定板,姿势只有俯视角度。结果标定出来的矩阵手眼矩阵带入验证时,水平方向误差只有3mm,但前后方向(z向)误差接近25mm。原因就是纯平移运动几乎不提供旋转约束,而手眼标定恰恰需要旋转信息来约束旋转解耦。

正确的姿势策略可以概括成一句话:让机械臂末端以多种姿态指向标定板,覆盖工作空间的不同区域,并且不同位姿之间的旋转差异要足够大。具体来说,我建议这样规划:

  1. 让标定板放工作空间中央,机械臂在标定板四周画一个半球面,末端姿态从正对(相机光轴垂直标定板)到倾斜45度、60度左右。
  2. 每个采集点之间,末端绕X、Y、Z轴的旋转尽量都要有变化,不要只变俯仰或者只变偏航。
  3. 避免两个位姿的末端姿态完全平行只差了平移,这种情况下A矩阵接近单位矩阵,提供不了有效约束。
  4. 每次移动后,让机械臂静止至少0.5秒以上再拍照。L515的振镜扫描一旦运动就会产生伪影,静止的0.5秒既是为了机械臂本身残留抖动消失,也是让L515的深度和彩色能稳定采集。

4.2 一组完整的数据应该包含什么

一组合格的数据是一对东西:当时的机械臂末端位姿T_base_tool,以及相机图像中计算得到的标定板位姿T_camera_board。两者必须严格同步。

在我采数据时,用的是一个简单但有效的方法:

# 伪代码:在同一个循环里先移动再读取 for i in range(25): # 1. 手动控制或程序化移动到某个预设位姿 path = ur_script_move_to(pose_list[i]) robot.send_program(path) # 2. 等待稳定 time.sleep(0.6) # 3. 读取UR3当前T_base_tool T_base_tool = robot.get_pose() # 4. 采集一帧图像并检测Charuco frames = pipeline.wait_for_frames() color_image = get_aligned_color(frames) corners, ids = detect_charuco(color_image, charuco_board) if len(corners) < 20: print("角点太少,跳过此位姿") continue # 5. 用solvePnP求T_camera_board rvec, tvec = cv2.solvePnP(charuco_corners_3d, corners, K, dist) # rvec, tvec 是 board -> camera 的变换 # 6. 保存这一对 save(T_base_tool, rvec, tvec)

注意第5步里solvePnP输入的三维点是在标定板自己坐标系下的角点坐标,需要你生成Charuco板时自己定义。推荐把原点定在板上最左上角的格点,Z轴垂直板面。输出rvec和tvec就是T_board_camera所需的旋转向量和平移向量。

4.3 数据筛选与质量自检

数据采完后不要急着求解,先做一轮垃圾数据排查。我自己会做三件事:

  • 看每张图检测到的Charuco角点数,少于20个的果断剔除。
  • 看机械臂位姿之间是否有明显重叠或重复,如果有相似度过高的,删掉几组。
  • 看标定板在画面中的大小是否合适。经验法则是:标定板的最长边应占图像最长边的1/3以上。太小的话角点亚像素精度不够,太大则容易超出画面导致检测失败。

另外,我强烈建议先用手头的相机内参做一次标定板重投影误差计算。如果平均重投影误差超过2个像素,说明相机内参可能偏了,需要先重新标定彩色相机内参再继续手眼标定。这一步很多人会忽略,导致后面手眼矩阵不准还找不到原因。

5. 解算手眼矩阵:算法选择与calibrateHandEye往哪塞数据

数据准备齐了,终于到了求解这一步。这里头有无数人在数据的"方向"上栽跟头,我必须把约定讲透。

5.1 OpenCV提供了什么,以及它期望什么

OpenCV从4.0开始就有现成的cv2.calibrateHandEye()函数,内部实现了多种算法,你可以用method参数在CALIB_HAND_EYE_TSAI、CALIB_HAND_EYE_PARK、CALIB_HAND_EYE_HORAUD等之间切换。默认的Tsai-Lenz方法对大多数场景足够用,性能也快。

函数签名是这样的:

R_cam2gripper, t_cam2gripper = cv2.calibrateHandEye( R_gripper2base_list, # 列表:每个元素的机械臂末端相对base的旋转矩阵 t_gripper2base_list, # 列表:对应的平移向量 R_target2cam_list, # 列表:每个元素标定板相对相机的旋转矩阵 t_target2cam_list, # 列表:对应的平移向量 method=cv2.CALIB_HAND_EYE_TSAI )

这里有个特别容易出错的地方:OpenCV要求的R_gripper2base是T_base_tool的旋转部分的逆,也就是它期望的是"gripper在base中的位姿"的另一种表述方式,并不是直接填robot.get_pose()里的旋转矩阵。原因在于OpenCV内部使用的AX=XB写法,把gripper2base这个变换方向和很多教科书上的base2gripper搞反了。如果你直接把T_base_tool填进去,标定结果必然不对。

具体来说:urx的get_pose()返回的是T_base_tool(表示base到tool的位姿),其旋转部分是R_base_tool。OpenCV需要的是R_gripper2base,本质上是R_base_tool的转置:

R_gripper2base = R_base_tool.T # 因为 R_tool_base = R_base_tool.T t_gripper2base = -R_base_tool.T @ t_base_tool

这一步是整个调用里最容易"看起来没错但结果错"的地方。我见过很多人在社区里问为什么calibrateHandEye出来的X是垃圾,回复里一半是因为这里填反了。

5.2 从solvePnP到calibrateHandEye的完整数据流

solvePnP输出的是从标定板坐标系到相机坐标系的旋转向量rvec和平移tvec,也就是R_board_cam和t_board_cam。那么R_target2cam就应该填的是这个R_board_cam,对应OpenCV文档里的target2cam。

注意这里又有一个方向约定:target2cam表示"标定板(target)在相机(cam)坐标系下的位姿"。如果你使用的solvePnP输出的是T_board_cam,那是对的;但如果你用其他工具,它可能输出T_cam_board(相机在标定板坐标系下的位姿),那就需要求逆。务必在填入之前统一方向,否则哪怕机械臂数据全对,标定结果也是乱的。

我最终用来求解的代码骨架如下:

R_gripper2base_list = [] t_gripper2base_list = [] R_target2cam_list = [] t_target2cam_list = [] for T_base_tool, rvec, tvec in collected_data: R_base_tool = T_base_tool[:3, :3] t_base_tool = T_base_tool[:3, 3] # 转换到OpenCV需要的gripper2base R_gripper2base = R_base_tool.T t_gripper2base = -R_base_tool.T @ t_base_tool R_gripper2base_list.append(R_gripper2base) t_gripper2base_list.append(t_gripper2base) # target2cam直接用solvePnP结果 R_target2cam, _ = cv2.Rodrigues(rvec) R_target2cam_list.append(R_target2cam) t_target2cam_list.append(tvec.reshape(3)) R_cam2gripper, t_cam2gripper = cv2.calibrateHandEye( R_gripper2base_list, t_gripper2base_list, R_target2cam_list, t_target2cam_list, method=cv2.CALIB_HAND_EYE_PARK )

输出结果R_cam2gripper, t_cam2gripper就是相机坐标系在末端工具坐标系中的位姿,这是eye-in-hand下我们要求的X。

5.3 Tsai、Park、Horaud选哪个

OpenCV提供了几种算法,实测下来:

  • Tsai-Lenz:最常用,闭式求解,速度极快,数据质量好时精度不错。但对噪声稍敏感。
  • Park-Martin:基于李群上的几何优化,对噪声的鲁棒性比Tsai好一点,实测重投影误差更稳定。
  • Horaud:和Park类似,有时候更准,但极端位姿下可能收敛到局部最优。

我个人的建议是:先用Tsai跑一遍看重投影误差,如果误差明显偏大,换Park重新算。两个方法的结果如果相差超过1cm,那基本说明数据采集有问题(多为位姿多样性不足),不用调算法,回去补数据。

6. 验证比求解更重要:三种验证手段与精度量化

标定完不能直接信,必须验证。我强烈建议把验证分成三个级别,从图像级到物理级逐层递进。

6.1 第一级:重投影验证(最客观、最快)

从采集的数据里留出几组不参与求解的样本(比如25组里留5组),用标定出的X把标定板的角点从3D投影回2D图像:

# 对某组验证数据 T_base_tool = ... T_cam2gripper = compose(R_cam2gripper, t_cam2gripper) # 标定板角点在base坐标系下的3D坐标 # T_base_board = T_base_tool @ T_cam2gripper @ T_board_cam # 再转到相机坐标系,投影到彩色图 # 计算投影点与实际检测角点的像素距离

如果平均重投影误差小于2个像素,说明手眼矩阵基本正确。如果大于5个像素,可以直接断定标定有问题,不用继续往下做物理验证。

这一步里要注意的是:参与求解的数据和验证的数据必须分开。全用参与求解的数据验证,误差必然偏小,这叫过拟合,反映不了真实精度。

6.2 第二级:深度一致性验证(利用L515的深度)

L515的深度图本身就是相机坐标系下的距离测量。这是我个人最喜欢的一个验证手段,因为它直接利用了硬件的优势,又能发现纯RGB验证发现不了的深度问题。

做法是:让机械臂停在某个位姿,拍摄标定板所在区域。通过标定的X把标定板上某个角点的物理坐标投影到相机坐标系,得到理论深度值;再从L515的深度图里读取该像素的实际深度值。两者相减,理论上是0。

如果这个差值稳定在5mm以内,说明标定的旋转和平移都经得起考验;如果出现"图像投影看起来准,深度却差2cm"的情况,说明平移向量有问题,或者相机的深度标定本身偏移。L515在做深度对齐时,有时候会出现RGB和深度之间的FOV不匹配,需要你检查是否已经用rs.align正确对齐了流。

6.3 第三级:物理抓取验证(最终判据)

验证到这里还不够,最终必须让机械臂实际去"碰"一个目标点。我的做法是在工作台上固定一个针尖或者笔尖,让视觉识别出它的中心坐标,然后手眼矩阵把它转换到base坐标系,控制UR3移动到目标点上方,看末端实际位置和目标位置是否重合。

注意这里用针尖验证比用小方块抓取有效得多,因为抓取有包容性,几毫米误差可能照样夹起来,发现不了问题。针尖的验证精度可以达到毫米级,一旦标定结果差哪怕3mm,针尖都能明显看出来。

我在实际验证中达到的效果是:X/Y平面误差约2~3mm,Z轴误差约5mm。这个水平对于大多数装配、抓取场景够用了。如果你的需求是亚毫米级,那光靠手眼标定就不够,还需要考虑UR3本身的末端负载变形、相机内参高精度重标定、以及运动学参数校准(即robot calibration,UR官方或者第三方可以进行)等更多因素。

7. 我踩过的坑:L515和UR3组合的常见问题实录

最后把我在实际项目中踩过的坑按"出现频率"排序写出来。每个坑都是我真金白银花时间填过的,希望能帮你绕开。

7.1 坑一:L515的自动曝光把标定板拍成了白板

L515的彩色摄像头在自动曝光模式下,如果标定板表面反射较强,画面里的标定板局部会过曝,角点检测质量骤降。我在采集数据时,第一轮拍的图像肉眼看着都正常,但转到灰度图检测角点时发现部分格子边缘糊成一片。

解决方法是:在采集前先用Realsense Viewer连接相机,把曝光固定下来,关闭自动曝光和白平衡。步骤如下:

depth_sensor = profile.get_device().first_depth_sensor() depth_sensor.set_option(rs.option.enable_auto_exposure, 0) depth_sensor.set_option(rs.option.exposure, 200) # 根据环境调整

对于彩色流,也要关闭自动曝光。如果室内光照不均匀,可以把光照调到大致均匀后再固定曝光。标定板区域的曝光宁可稍微偏暗,也不要过曝。

7.2 坑二:机械臂没停稳就拍照,深度图带毁灭性伪影

L515的MEMS振镜扫描机制决定了它对运动极度敏感。我试过用UR3连续运动到每个点位后只停了100ms就拍照,结果深度图里标定板的边缘出现明显的重影和扭曲,18组数据里有至少6组的深度值完全不可用。

解决方法是把静止等待时间拉长到600ms以上。UR3的位姿伺服停稳后,一般200ms就开始稳态,但L515扫描一帧需要的时间不短,图像曝光加上传输也有延迟,600ms是实测下来比较稳的值。如果你用的是eye-in-hand且机械臂负载较重(比如末端装了气爪),停稳时间还要更长。

7.3 坑三:手里的标定板物理尺寸和代码里的数值不一致

这个坑极其低级但极其致命。如果你在生成Charuco板时用了一个棋盘格子边长,打印出来实际测量却是另一个值,那么solvePnP计算出来的平移向量就会整体缩放。手眼标定结果看起来旋转对了、重投影误差很小,但深度方向误差能偏到厘米级。

绝对要养成用卡尺量标定板格子的习惯。打印后量一下10个格子的总宽度除以10,得到实际格子边长,然后更新代码里的squareLength参数。L515的分辨率高,这个误差很难用肉眼发现。

7.4 坑四:UR3的TCP设了一个非零的Tool变换

我在某一轮测试中,示教器里设置了自定义TCP(给末端气爪用的,偏移了大约8cm),然后用urx读get_pose()去标定。视觉标定出来的X是相机到气爪TCP的变换,这在数学上没错,但后续使用很容易混淆,而且一旦你换了气爪,标定就全废了。

如果你不需要工具TCP参与视觉控制,最简单的方案是把Tool TCP设回法兰中心,让UR的输出直接是T_base_flange。标定结果就是相机到法兰的变换,这个变换只和相机的机械安装有关,换不换末端执行器都不受影响。

7.5 坑五:深度图和彩色图没对齐,导致后期利用深度信息时位置偏差

采集标定数据时,如果你用彩色图做角点检测,但后续用深度图做目标物位置提取,两者必须严格对齐。否则角点坐标和深度值来自两套不一样的相机参数,手眼矩阵就算准了,结合深度时也会偏。

L515的RGB和深度原本传感器位置不同,需要利用rs.align对齐到同一个坐标系。我遇到的情况是:对齐后深度图边缘存在一些无效值(深度值为0),这是正常的,不影响中央区域。采集数据时,尽量让标定板在图像中央占主体,边缘的无效深度不会触及标定板。

7.6 坑六:标定结果在不同空间区域精度不一样

标定完成后如果在工作空间A点测试误差2mm,移到B点误差变成2cm,十有八九是标定数据覆盖范围不够。UR3的关节角度在空间不同区域时,运动学精度本身也会略有差异,而手眼标定只能拟合出标定数据覆盖范围内的平均结果。

我重新补采数据时,会刻意在多个高度、多个深度、多个朝向分别采集,让标定板的位姿在工作空间里尽量均匀散布。这样标定出的X是对整个关注区域的联合最优,不会在某处特别准而在另一处失效。

7.7 坑七:UR3固件和urx库的TCP/IP缓冲导致位姿读取出错

这是个非常冷门的坑。当UR3的程序执行模式不是Remote Control时,urx读取位姿会有偶发性的数据缓冲问题,偶尔读出来一个明显跳变的值(比如X从0.4m突然跳到-1.8m)。这种脏数据混进标定数据里,会导致AX=XB求解直接发散。

后来我在数据采集环节加了检查:每次批量采完以后,扫描所有T_base_tool,凡是和前一组的矩阵差值大于某个阈值(比如平移跳变超过3cm,旋转角变化超过10度),就单独检查那组数据是否物理合理。如果明显不合理,删除重采。这个检查逻辑可以用scipy.spatial.transform.Rotation快速实现。

写到这里,UR3和L515手眼标定的完整流程就都覆盖到了。这套方法不挑具体硬件版本,换D435i、换其他UR型号也一样适用。如果你正准备做视觉抓取,建议把主要时间花在第4部分(数据采集策略)和第6部分(验证),这两个环节做好了,标定想不成功都难。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 15:05:22

Linux 内存回收机制:水位线、kswapd 与 Direct Reclaim

Linux 内存回收机制&#xff1a;水位线、kswapd 与 Direct Reclaim源码基线&#xff1a;Linux v6.18.9&#xff08;mm/page_alloc.c / mm/vmscan.c&#xff09;。 现象&#xff1a;内存明明还有&#xff08;free 数 GB&#xff09;&#xff0c;进程却偶发卡顿几百毫秒&#xff…

作者头像 李华
网站建设 2026/10/7 15:05:08

AI 练习项目 智能数仓问答

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 15:05:01

云数据中心特点

云数据中心特点&#xff1a;资源池化。通过虚拟化技术将计算&#xff0c;存储和网络资源统一整合&#xff0c;形成共享资源池&#xff0c;提高资源利用率。按需服务&#xff0c;根据业务需求动态分配和回收资源&#xff0c;实现资源的弹性供给。高可靠性&#xff0c;采用冗余设…

作者头像 李华
网站建设 2026/10/7 15:02:54

ponytail 插件实战:工作区代码整理与临时文件收束指南

第一次听说 ponytail 这个插件时&#xff0c;我下意识以为是个搞笑项目。一个做代码整理的编辑器插件&#xff0c;起名叫“马尾辫”&#xff0c;多少有点无厘头。但真在杂乱项目里用上之后&#xff0c;我反而觉得这名字起得相当贴切&#xff1a;散落各处的临时文件、随手写的调…

作者头像 李华
网站建设 2026/10/7 15:02:21

OpenClaw 2026.x Windows + WSL2 完整部署:从 Node.js 到 Ubuntu 环境一次跑通

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华