准备用自采数据微调 LingBot-VLA 2.0 时,最容易出问题的不是模型配置,而是数据本身:多路相机的 security 外参各自落在不同坐标系里,动作向量的维度顺序和单位在不同批次之间不一致。这两件事在训练前不处理,后面调参基本是在噪声上做实验。可行的路径是先把相机外参统一到一个世界坐标系,再把动作维度对齐成固定顺序和单位,然后才谈样本量、采样率和微调超参。
判断方向:微调前的数据准备应当先做两件可验证的事——把全部相机外参换算到同一世界坐标系,把动作向量对齐成统一维度顺序和单位。适用场景是多相机、多批次自采数据;操作动作是读标定与元数据、写重排脚本、单条轨迹回放;验证方式是重排前后数值抽查加三维轨迹不跳变;风险边界是标定本身的精度误差和不同采集批次的手眼关系变化,脚本只能对齐格式,不能修正错误的标定。
列出所有相机及其外参,确认是否在同一世界坐标系下
先把数据集里出现的相机全部列成一张表:相机名、分辨率、帧率、对应的外参文件或元数据字段。外参通常以 4×4 齐次矩阵或「旋转四元数 + 平移向量」两种形式存储,键名一般是 extrinsic、T_cam2world、cam2base 之类,需要结合环境确认。重点看两点:一是命名里的方向约定,T_world_cam 与 T_cam_world 互为逆矩阵,混用会让轨迹整体镜像或反向;二是每个相机的父坐标系是谁,有的直接给 world,有的只给到 base 或末端。
统一到同一坐标系的一般步骤:先确定一个参考系,建议用机器人基座 base;然后对每个相机求 T_base_cam = T_base_world @ T_world_cam;如果只拿到反方向矩阵,先求逆再乘;最后把所有相机的 T_base_cam 写回同一份配置文件,并在文件头记录参考系名称和手眼类型(eye-in-hand 还是 eye-to-hand)。eye-in-hand 相机的外参会随末端位姿变化,这类相机不能只存一个常量矩阵,需要额外记录采集时的末端位姿。
- 检查表项:相机名 / 父坐标系 / 矩阵方向 / 手眼类型 / 标定日期 / 是否随批次变化
- 常见坑:旋转用四元数但顺序是 xyzw 还是 wxyz 未标注;平移单位是米还是毫米;矩阵存在转置
- 验证方式:把 base 坐标系原点和坐标轴分别投影到每个相机图像平面,观察投影位置是否符合安装位置
检查动作向量的维度顺序和单位
动作维度的定义通常能从三处读到:数据集元数据(如 meta/info.json、dataset_config.yaml 这类文件中的 feature 名称列表)、采集脚本里的写入顺序、以及数据文件本身的列名或数组宽度。三处不一致时以数据文件的实际列宽为准,其余作为注释记录。先确认动作是关节空间还是笛卡尔空间,再确认夹爪是单独一维还是并进位姿。
常见维度顺序对照:
- 7 维关节 + 1 维夹爪:
[j1..j7, gripper],单位弧度,夹爪常归一化到 0~1 - 6 维末端位姿 + 1 维夹爪:
[x, y, z, rx, ry, rz, gripper],位置单位米,姿态欧拉角单位弧度 - 位置 + 四元数 + 夹爪:
[x, y, z, qx, qy, qz, qw, gripper],共 8 维,四元数顺序必须标注 - 双臂拼接:
[左臂关节..., 左夹爪, 右臂关节..., 右夹爪],需要确认是臂优先还是维度优先交错
单位不一致比顺序错更容易被忽略:角度写成度、位置写成毫米,数值上看不出错,训练时表现为动作幅度异常。建议在配置文件里为每一维显式写 unit 字段,而不是靠约定俗成。
写一个脚本把多条轨迹的动作维度对齐到同一顺序
重排脚本只做三件事:读入源动作数组、按索引置换列、按目标 schema 写出。把置换定义为显式映射而不是硬编码切片,便于审阅。下面是与具体 SDK 无关的骨架,字段名按实际数据文件替换。
import numpy as np, json
TARGET = ["j1","j2","j3","j4","j5","j6","gripper"]
SOURCE = ["j1","j2","j3","j4","j5","j6","gripper"] # 从元数据读出
def build_perm(src, tgt):
missing = [k for k in tgt if k not in src]
if missing:
raise ValueError(f"目标维度在源中缺失: {missing}")
return [src.index(k) for k in tgt]
perm = build_perm(SOURCE, TARGET)
def convert(action, unit_map):
# unit_map: {"j1": "rad", ...},仅做单位换算,不改顺序
a = np.asarray(action, dtype=np.float32)
a = a[:, perm]
return a
# 逐条轨迹:读 -> 重排 -> 保存,并写回 schema 描述
# out = {"action": convert(arr, unit_map), "schema": TARGET}
验证重排正确性不要只看形状。可以用两个办法:一是构造一个每列值互不相同的测试数组(如列为 1、2、3…),跑一遍置换,检查输出列序与 TARGET 是否一致;二是从真实轨迹随机抽若干帧,人工比对源文件与输出文件在关节 1、夹爪等关键列上的数值。若某列出现整列错位但形状正确,通常是置换索引写反了。
用一条轨迹做可视化回放,确认坐标系和动作对齐
格式对齐不等于几何对齐。挑一条动作幅度明显、含夹爪开合的轨迹,把末端位置按时间画成三维折线,同时画关节角随时间的曲线。坐标系变换正确时,三维轨迹应当是连续的、末端在抓取点附近停留;如果出现瞬间跳到另一侧、镜像翻转或整体偏移一个固定量,优先怀疑外参方向取反或平移单位是毫米。
import matplotlib.pyplot as plt
# pos: (T, 3) 已换算到 base 系
fig = plt.figure()
ax = fig.add_subplot(111, projection="3d")
ax.plot(pos[:, 0], pos[:, 1], pos[:, 2])
ax.set_xlabel("x"); ax.set_ylabel("y"); ax.set_zlabel("z")
plt.show()
# 关节曲线看单位问题:度与弧度差约 57 倍
# plt.plot(joint[:, 0]); plt.plot(joint[:, 6])
还可以把相机图像里检测到的目标点按外参投影回 base 系,与末端轨迹叠加,两者在同一位置附近才说明相机坐标系和动作空间是对上的。这一步通常不需要精确标定,只需要能看出量级和方向是否合理。
记录数据准备的配置版本,便于复现
数据准备好之后,把决定数据形态的关键参数写进一份随数据集存放的配置文件,并和数据一起打版本号。后续微调结果有差异时,先比对这份文件,比翻采集脚本更快。
dataset_version: v0
reference_frame: base
action_schema: [j1, j2, j3, j4, j5, j6, gripper]
action_unit: {joint: rad, gripper: normalized_0_1}
pose_convention: position_m + quaternion_xyzw
extrinsics:
cam_front: {parent: base, matrix_file: calib/front.json, hand_eye: to_hand}
cam_wrist: {parent: ee, matrix_file: calib/wrist.json, hand_eye: in_hand}
permutation_applied: true
notes: 第二批数据夹爪为 0-100,已归一化
配置里应包含相机外参来源与方向、动作维度顺序、各维单位、是否做过重排和归一化。缺少其中任何一项,之后的复现都只能靠猜。需要结合环境确认的一点是:不同批次采集可能重新标定过,外参按批次分开记录比覆盖更新更安全。