跳到主要内容

传感器标定与 sim2real:坐标系、时间戳与外参误差

强化学习策略、VLA 模型、MPC 控制器和 SLAM 系统都有一个共同前提:输入数据表达的是同一个时刻、同一个物理世界里的状态。如果相机外参错了 2 度,IMU 时间戳慢了 30 ms,轮速方向配置反了,算法看到的世界就会和真实机器人错开。

这就是传感器标定在具身智能里的位置:它不是某个独立工具,而是 sim2real 的地基。


1. 先把系统看成一棵坐标树

一个机器人系统里常见的坐标系包括:

  • world / map:任务或地图坐标系。
  • odom:局部连续里程计坐标系。
  • base_link / body:机器人本体坐标系。
  • camera_*:相机光心坐标系。
  • imu:惯性测量单元坐标系。
  • lidar:激光雷达坐标系。
  • end_effector / foot_*:末端执行器或足端坐标系。

这棵树里每条边都是一个变换:

T_parent_child = [R | t]

其中 R 是旋转,t 是平移。ROS2 的 tf2 会把这些边组织起来,自动计算任意两个坐标系之间的变换。

关键点:标定不是只求一个数,而是定义“哪个坐标系相对于哪个坐标系”的关系。 工程里最常见的错误不是求解器不够高级,而是方向写反了:你需要的是 T_base_camera,代码里却用了 T_camera_base


2. 数据进入算法前要过三道门

传感器数据通常不是直接进模型,而是经过时间同步、坐标变换和质量检查。

这三道门分别回答:

问题典型配置错了会怎样
这帧数据是什么时刻的?时间戳、时钟源、延迟补偿运动物体拖影,IMU 和图像对不上,控制滞后
这帧数据属于哪个坐标系?frame_id、tf tree、外参目标位置偏移,点云和图像错位,足端接触点跑偏
这帧数据可信吗?范围、跳变、频率、状态码融合发散,控制抖动,在线标定误触发

3. 外参小误差会被距离放大

假设相机相对于车体或机器人本体有一个 yaw 方向误差 delta_theta。一个真实在正前方 d 米处的目标,会在横向产生近似误差:

lateral_error ~= d * tan(delta_theta)
距离 dyaw 误差 0.5 度yaw 误差 1 度yaw 误差 2 度
2 m1.7 cm3.5 cm7.0 cm
5 m4.4 cm8.7 cm17.5 cm
10 m8.7 cm17.5 cm34.9 cm

这张表说明了一个很实用的判断:近距离抓取任务可能对平移误差更敏感,远距离导航和定位任务会更快暴露角度误差。

这也是为什么真机调试时不能只看“检测框准不准”。检测框准,只代表图像坐标里准;外参错时,投到 base_linkmap 后仍然会错。


4. 时间同步错误等价于空间误差

如果机器人以速度 v 运动,传感器时间戳有延迟 delta_t,那么最基本的位置误差近似是:

position_error ~= v * delta_t
速度 v延迟 10 ms延迟 30 ms延迟 100 ms
0.5 m/s0.5 cm1.5 cm5 cm
1.0 m/s1 cm3 cm10 cm
3.0 m/s3 cm9 cm30 cm

对机械臂末端相机来说,v 可以是末端速度;对四足机器人来说,v 可以是机身速度;对轮足或移动机器人来说,v 可以是底盘速度。只要系统在动,时间误差就会变成空间误差。

工程上可以用三个信号快速定位时间问题:

信号看什么
频率相机、IMU、轮速、关节状态是否稳定发布
单调性时间戳是否回退、重复、跳变
相位差加速度、角速度、轮速变化是否明显错位

5. 标定对象不只有相机

具身机器人常见标定对象可以按“测量什么”分组。

对象标定内容常见观测指标
相机内参焦距、主点、畸变重投影误差、边缘直线弯曲
相机外参camerabase/imu/lidar图像-点云错位、目标落点偏移
IMUbias、scale、噪声、安装方向静止漂移、重力方向、 Allan variance
轮速比例系数、左右轮差、方向直线跑偏、转弯半径不一致
关节零位、方向、传动比、限位FK 末端误差、左右腿不对称
足端/触觉接触阈值、安装位置、延迟支撑相误判、落足点跳变

这些量会互相影响。例如 IMU 安装方向错,会让姿态估计错;姿态估计错,会让相机点投到地面时错;地面目标位置错,规划和控制就会跟着错。


6. 具身机器人标定和车端标定有什么不一样

车端标定经验很有用,但不能原封不动搬到具身机器人。共同点是坐标系、时间戳、外参、残差和回归验收;差异主要来自机器人身体形态和任务闭环。

维度车端常见情况具身机器人常见情况
运动模型平面运动为主,车体刚性较强机械臂、轮式、四足、人形都有不同运动学
坐标树传感器多固定在车体上传感器可能装在末端、头部、躯干或关节链上
标定对象camera-body、camera-lidar、IMU、轮速hand-eye、关节零位、足端接触、触觉、末端相机
误差表现车道线、目标、点云、轨迹横向误差抓取偏差、落足偏差、末端位姿误差、接触误判
激励条件转弯、加减速、不同距离目标关节运动、末端姿态变化、支撑相切换、接触事件
在线风险误更新会影响定位和规划误更新可能直接影响抓取、碰撞、平衡和接触控制

因此,具身智能里的标定要多问一个问题:这个参数误差最后会让机器人任务失败在哪里?

例如:

  • 机械臂末端相机 yaw 错:目标在图像里看起来对,但抓取点投到 base_link 后偏了几厘米。
  • 四足机身相机外参错:目标方向估计偏,策略一直朝错误方向修正,步态看起来像在“追偏”。
  • 触觉或足端接触阈值错:状态估计以为已经接触地面,实际还在空中,控制器会提前切换支撑相。
  • 关节零位错:仿真里的腿长和真实 FK 不一致,sim2real 后落足点系统性偏移。

7. 在线标定要有触发和回退

离线标定解决“出厂或装配后参数是什么”,在线标定解决“运行中参数有没有变”。在线标定不能只看优化是否收敛,还要看它是不是在正确场景下被触发。

一个可落地的在线标定监控至少要记录:

  • 触发原因:残差升高、温度变化、碰撞/震动、维修换件、长时间运行漂移。
  • 数据质量:运动激励是否足够,传感器频率是否稳定,是否有时间戳异常。
  • 参数变化:roll / pitch / yaw / x / y / z 相对上一次可信参数变化多少。
  • 收敛状态:优化是否收敛,收敛时间是否异常。
  • 回退条件:新参数是否让重投影、点云对齐、轨迹误差或控制稳定性变差。

8. 调试清单:先查低级错误

真实项目里,很多“算法效果不好”最后会落到很基础的问题。

优先级检查项快速判断
1frame_id 是否正确RViz/tf tree 是否能连通,父子方向是否符合约定
2单位是否一致角度是 rad 还是 deg,长度是 m 还是 mm
3轴方向是否一致x 前 y 左 z 上,还是相机光学坐标系
4时间戳是否可信是否回退、重复、跳变,延迟是否补偿
5静态/动态变换归属固定外参不要被多个节点重复发布
6参数是否匹配车型/机器人相机编号、IMU 位置、轮距、关节零位是否串配置
7数据是否覆盖标定所需激励只直行很难估 yaw,静止数据不能估尺度

实践中建议把每次问题定位写成四列:

现象 -> 观测信号 -> 根因假设 -> 验证动作

例如:

点云投影到图像整体偏右
-> 近处偏差小,远处偏差大
-> camera-lidar yaw 外参偏差
-> 人工调 yaw +/- 1 度观察投影残差曲线

9. 在线标定小游戏:把外参误差变成机器人任务误差

完全小白最容易被“公式”和“坐标系”劝退,所以可以先玩一个很小的在线标定游戏。它不只估计 yaw,还会把残差映射成机器人任务误差。

网页端入口:

/perception/sensor-calibration-playground

如果本地服务运行在 http://localhost:3000/dive-into-embodied-ai/,完整地址是:

http://localhost:3000/dive-into-embodied-ai/perception/sensor-calibration-playground
  1. 程序藏了一个真实相机 yaw 外参误差,例如 1.8 deg
  2. 机器人每一帧看到一个前方目标,目标越远,yaw 误差造成的横向残差越大。
  3. 在线标定器不能直接看到真实误差,只能根据残差一点点更新估计值。
  4. 残差会被换算成任务误差:机械臂抓取偏差、移动机器人到点偏差或四足目标跟踪偏差。
  5. 如果残差下降、估计值收敛,并且任务误差小于阈值,就算通关。

运行:

python3 codes/foundations/perception/sensor-calibration/online_calib_game.py \
--level normal \
--embodiment arm \
--initial-guess-deg 0.0 \
--update-gain 0.35 \
--csv /tmp/online_calib_game.csv

输出类似:

Online calibration game: estimate the hidden camera yaw offset
level=normal seed=7 true_yaw=1.80 deg
robot_task=eye-in-hand grasp metric=grasp_miss_m threshold=0.035 m

frame dist residual estimate abs_err task_err event
0 1.50m | 0.000 1.800 2.31cm watch
3 4.25m |>> 0.700 1.100 5.57cm UPDATE
6 6.50m |> 1.525 0.275 4.52cm UPDATE
9 2.00m | 1.751 0.049 0.37cm watch

Regression checks
- final estimate: 1.751 deg
- final abs error: 0.049 deg
- residual improvement: 65.7%
- convergence frame: 10
- final task error: 0.85 cm
- final task success rate: 100.0%
- result: PASS

可以切换不同机器人任务:

# 机械臂:末端相机抓取,任务指标是 grasp_miss_m
python3 codes/foundations/perception/sensor-calibration/online_calib_game.py --embodiment arm

# 移动机器人:朝目标点移动,任务指标是 nav_lateral_error_m
python3 codes/foundations/perception/sensor-calibration/online_calib_game.py --embodiment mobile

# 四足机器人:机身相机目标跟踪/落足参考,任务指标是 tracking_or_foothold_error_m
python3 codes/foundations/perception/sensor-calibration/online_calib_game.py --embodiment quadruped

这段小游戏背后对应真实工程里的几个概念:

游戏变量工程对应物
true_yaw真实外参误差,真机上通常不可直接知道
lateral_residual重投影误差、点云投影误差、轨迹横向误差等残差
robot_task_error_m抓取偏差、导航横向误差、目标跟踪或落足误差
triggered在线标定触发条件,例如残差持续升高且运动激励足够
estimate在线估计出的新外参
accepted参数验收,只有残差、参数变化和任务误差都可信才写回

代码不依赖任何私有数据,也不需要 ROS、MCAP 或云平台;它的价值是让初学者先看到“在线标定不是玄学,而是一条残差下降曲线,更是一条任务误差下降曲线”。


10. 工程化迭代验收:四级门禁

真实项目里,标定算法不是“我看了一段日志觉得不错”就能合入。每次修改都应该从便宜到昂贵经过四级门禁:

门禁跑什么目的失败时怎么处理
Level 0 本地小实验online_calib_game.py 这种合成数据先证明更新逻辑没有明显写反、震荡或不收敛修公式、符号、阈值,不进入真实数据
Level 1 离线回归脱敏 CSV/JSON/MCAP 小集合比较精度、收敛帧数、误触发次数定位具体 case,补规则或修算法
Level 2 本地回灌x86/J6/仿真环境回放固定数据验证运行时依赖、参数加载、topic 和日志产出修环境、配置、参数包和解析脚本
Level 3 云端回灌云平台批量跑数据集验证规模化稳定性和版本对比输出报告,阻塞合入或灰度

这四级门禁的核心思想是:越靠前越便宜,越靠后越接近真实系统。 不要把符号写反、参数包加载错误这种问题留到云端批量任务里才发现。

小游戏可以直接作为 Level 0 smoke test:

python3 codes/foundations/perception/sensor-calibration/online_calib_game.py \
--level normal \
--embodiment arm \
--fail-on-regression

如果回归条件不满足,脚本会返回非零退出码,CI 可以直接拦住这次修改。


11. 从小游戏升级成真实回归

  1. 精度有没有变好?
  2. 收敛有没有变慢?
  3. 有没有在异常数据上误触发?

因此一个最小回归集可以按数据类型分层:

数据类型目的验收指标
正常收敛 case验证算法能收敛最终误差、收敛帧数、残差下降比例
弱激励 case验证不会乱更新触发次数、参数变化上限
时间戳异常 case验证健康检查是否拒绝回退/重复/跳变时间戳
传感器跳变 case验证鲁棒性spike 是否被过滤,是否误写参数
机器人任务 case验证是否真的帮助任务抓取成功率、到点误差、落足误差、接触误判率
基线对比 case验证版本迭代新旧版本参数差、耗时差、任务成功率差

在线标定回归不应该只看“最终是否成功”,还要保留过程曲线。推荐每次输出一份结构化结果,作为后续报告和 PR 讨论的依据:

{
"case_name": "camera_yaw_normal_converge",
"input_bag": "desensitized_case_001.mcap",
"baseline_version": "v1",
"candidate_version": "v2",
"final_abs_error_deg": 0.12,
"robot_task_error_m": 0.008,
"task_success_rate": 0.98,
"convergence_frame": 9,
"residual_improvement": 0.66,
"trigger_count": 12,
"accepted": true
}

这和上面的小游戏是一回事,只是真实系统把 lateral_residual 换成了真实日志中的重投影、点云对齐、轨迹或接触残差。


12. 云平台回灌:把一次调试变成可重复实验

当数据变多以后,本地手动跑脚本会很快失控。工业项目通常会把回放和评测放到云平台上:输入一批脱敏数据和一个算法版本,平台自动拉起环境、回放数据、收集结果、生成报告。这类流程可以理解成“标定算法的 CI”。

公开教程里可以把内部平台统一抽象成 cloud fillback,不暴露真实平台名、任务号、车辆号、bucket 路径和客户数据。你可以把 AIDI 这类平台理解成一个工程化执行器:它不改变标定原理,只是把“拉参数、跑回灌、收结果、做对比”自动化。

一个脱敏后的云端回灌任务至少要检查:

阶段检查项
输入准备数据集 ID、算法版本、参数包版本、传感器配置是否匹配
参数加载平台实际使用的是哪一份参数包,是否被旧数据集参数覆盖
回灌执行回放是否完整,关键 topic 是否存在,运行环境是否一致
结果产出是否生成标定结果、残差曲线、触发状态和错误码
任务评估抓取、导航、落足、接触等任务指标是否变好
对比评估candidate 相对 baseline 的精度、收敛时间、失败率和任务成功率是否变差
归档复现输入、输出、日志、报告是否能按任务 ID 重新定位

注意这里的“参数包实际使用了哪一份”非常重要。很多回归结论不可信,并不是算法变了,而是云端回灌时加载了错误车型、错误数据集或旧版本参数。

一个工程化验收报告可以长这样:

维度baselinecandidate结论
通过 case 数48 / 5049 / 50变好
平均收敛帧11896变好
最大 yaw 变化1.2 deg1.1 deg持平
弱激励误触发20变好
时间戳异常拒绝率100%100%持平
机器人任务成功率92%97%变好
平均运行耗时1.00x1.08x可接受

PR 里不要只贴“已验证通过”,而要贴这类对比表。评审者才能判断这次修改是局部修 case,还是整体更稳。


13. 迭代闭环:从一次问题到一条规则

工程化验收还有一个很重要的习惯:每发现一个线上或实车问题,都要尽量沉淀成可重复 case。

例如:

现场现象根因新增回归规则
某段数据外参突然跳变时间戳回退导致残差异常时间戳非单调时禁止更新参数
弱纹理路段误触发在线标定运动激励不足低激励 case 的参数变化必须小于阈值
新版本收敛更快但偶发越界更新步长过大参数单帧变化量和总变化量都要设上限
云端结果和本地不一致参数包加载路径不同报告必须记录实际使用的参数包摘要

这样做的价值是:同一个问题不会靠人肉经验反复排查,而是变成一条自动验收规则。


14. 脱敏原则:能讲方法,不泄露现场

把真实项目经验写进公开教程时,可以保留工程结构,但要脱敏数据和路径:

可以保留不应公开
回灌链路:数据 -> 回放 -> 解析 -> 规则 -> 报告内部 bucket、机器名、账号、token
指标定义:残差、收敛帧、参数变化、失败率真实车号、设备编号、客户项目名
合成或匿名化曲线原始图像、点云、车牌、人脸、路线
通用 MCAP topic 设计思想内部 topic 全量列表和私有消息字段
baseline/candidate 对比方法未发布算法分支、内部版本包路径

一个好的公开案例应该让读者学会方法,而不是依赖你的内部平台。最合适的写法是:

真实系统里我们会用云平台批量回灌 MCAP;
公开教程里先用 online_calib_game.py 模拟同样的闭环;
等读者理解后,再把输入替换成自己的 rosbag / mcap / csv。

15. 一个最小练习

不用真机也可以做一个标定敏感性实验:

  1. 运行 online_calib_game.py --level easy --embodiment arm,观察估计值和抓取误差如何收敛。
  2. --update-gain 改成 0.05,观察收敛变慢。
  3. --update-gain 改成 0.9,观察是否更容易震荡。
  4. --embodiment 改成 mobilequadruped,比较不同机器人任务的验收阈值。
  5. 把难度改成 --level hard,观察噪声变大后是否还能通过回归检查。
  6. 打开 CSV,画出 estimate_degmean_abs_residual_mrobot_task_error_m 三条曲线。

最小绘图代码:

import csv

with open("/tmp/online_calib_game.csv", newline="") as f:
rows = list(csv.DictReader(f))

for r in rows[::5]:
frame = int(r["frame"])
estimate = float(r["estimate_deg"])
residual_cm = float(r["mean_abs_residual_m"]) * 100
task_cm = float(r["robot_task_error_m"]) * 100
print(f"{frame:03d} estimate={estimate:5.2f} deg residual={residual_cm:5.2f} cm task={task_cm:5.2f} cm")

如果你把这个练习接到真实日志上,横向误差就可以替换成重投影误差、点云配准误差、轨迹横向误差或落足点误差。教程后续可以继续把它扩展成完整的 IMU、轮速、关节和足端接触标定实验。