一句话答案:今天它们完全没有配合 —— 深度只是被录到磁盘上,推理循环一眼都不看它。 这一页先把现在真实的推理循环画清楚(每个数字都有出处),再讲要让深度真正参与, 有哪三种接法、各自要动什么、代价多大。
depth/ 目录,既不进 LeRobot 数据集,也不进模型的观测。
训练时模型没见过深度,推理时也不会读深度。两条线从头到尾没有交汇点。
每个控制周期做这几件事(run_policy_trials.py):
┌─ 观测 build_obs() ──────────────────────────────────────┐
│ observation.state 17 维 float32 │
│ observation.images.head RGB 424×240 │
│ observation.images.left_arm_wrist RGB 640×480 │
│ observation.images.right_arm_wrist RGB 640×480 │
│ ★ 没有深度。一个字节都没有。 ★ │
└──────────────────────┬──────────────────────────────────┘
↓ + 语言指令(整段试验固定不变)
┌────────────────────┐
│ SmolVLA │ 一次推理吐 50 步动作
│ predict_action() │ (n_action_steps=50)
└────────┬───────────┘
↓ 动作块进队列,之后 49 帧从队列取
┌────────────────────────────────────────┐
│ PolicySafetyFilter 每一帧都过 7 步: │
│ 1 NaN/离谱值 → 拒帧 │
│ 2 底盘速度清零 3 头部锁定 │
│ 4 关节限位钳位 5 每帧步长上限 │
│ 6 FK 笛卡尔包络 → 拒帧 7 看门狗 │
└────────┬───────────────────────────────┘
↓ 只有活下来的帧才发出去
robot.send_action()
左臂 6 + 右臂 6 + 头 2 + 底盘速度 3 = 17(policy_safety.DIMS)。 全是本体自身的关节和速度,没有一维是关于外部世界的 —— 没有杯子在哪,没有距离。 模型对「杯子在哪」的全部认识,只能来自那三路 RGB 图。
| 项 | 实测 | 出处 |
|---|---|---|
| 推理耗时 | GPU 146 ms/帧 ≈ 6.9 Hz;CPU 2397 ms ≈ 0.4 Hz(慢 16.5 倍) | _pick_device() |
| 训练节拍 | 30 Hz | 数据集 meta/info.json |
| 动作块 | 一次推理规划 50 步 = 训练节拍下的 1.67 秒 | n_action_steps=50 |
| 这个落差的后果 | 块是按 30 Hz 规划的,实际只跑到 ~8 Hz,直接播会被拉长约 4 倍;--chunk-rate 就是为此加的(一个周期消费多步、只发最后一步,让计划按真实时间播) | _chunk_rate_note() |
train_config.json → 数据集 meta/tasks.parquet 取,这是唯一不会错的来源)。
录制时:DepthRecorder 起一个独立线程,按 30 fps 抓 16-bit 毫米图,写成
depth/<集>/frame_%06d.png + timestamps.jsonl(墙钟时间戳,供事后对齐)。
录完就躺在磁盘上,没有任何训练或推理代码读它。
详细的调用链、以及它自带的三个缺陷(孤儿线程写满磁盘、fd 泄漏、USB 假死导致 0 帧),
见 深度相机那一页。那里也记着一件事:
lerobot 从 2026-06-27 起就原生支持深度(存 16-bit TIFF、带 is_depth_map 标志),
这套自研旁路本可以不写。
| A · 深度在策略外面 | B · 深度当策略的额外输入 | C · 换成点云策略 | |
|---|---|---|---|
| 深度的角色 | 算出杯子的 3D 坐标,用来校验和兜底,不进模型 | 伪彩成 3 通道当第四路相机,或把 3D 位姿并进 state | 深度变点云,直接喂点云策略(DP3/iDP3) |
| 要重训吗 | 不要 | 要 | 要,而且换策略家族 |
| 要重采数据吗 | 不要 | 要(现有数据 102 集里只有 14 集有深度) | 要 |
| 现成基础 | cup_locate_demo.py 已走通一半 | colourise() 现成;lerobot 数据集侧现成 | 无 |
| 主要障碍 | 内参是估计值,要标定一次 | SigLIP/ResNet18 都是 RGB 预训练,伪彩深度是分布外输入;state 从 17 维改到 20 维 = 换了 embodiment | 整条链路重来 |
深度不进模型,而是在模型旁边并行跑,给三件事用:
深度相机 ──→ cup_locate_demo 那套(最近的一团 + 反投影) ──→ 杯子 3D 坐标 (x,y,z)
│
┌────────────────────────────────────────────────────────┤
│ │ │
▼ ①开跑前的门 ▼ ②跑的时候记录 ▼ ③失败诊断
杯子在不在工作区? 每次试验存下杯子实际位置 把「模型往哪抓」和
在不在 120-175mm 中段? (现在的试验记录里没有这个) 「杯子实际在哪」对齐,
不对就别让手臂动 才知道是看错了还是抓歪了
这三件事一件都不需要改模型,但每一件都直接冲着现在最大的未知去: 「模型抓不到,到底是没看见杯子,还是看见了抓歪了」——今天的试验记录回答不了这个问题。
两个策略的视觉主干都是 RGB 预训练的:SmolVLA 走 SigLIP(归一化到 [-1,1]), ACT 走 ResNet18 + ImageNet 权重。伪彩深度图对它们是分布外输入,等于白多一路要从头学的输入。 而且把 3D 位姿并进 state 会让 17 维变成 20 维 —— 那是换了 embodiment, 现有全部数据和 checkpoint 一次性作废。在「还没有一个 checkpoint 抓起过杯子」的前提下, 先加一个不确定的输入,只会让「是数据的问题还是输入的问题」更难分辨。
cup_locate_demo.py 现在用的是估计内参
(FX=FY=320, CX=320, CY=240,按 90° 水平视角假设)。
距离 Z 是深度相机直接测的,准;左右 X、上下 Y 是近似值。这是 A 唯一的硬门槛。head_lock 是启动时读机器人当时在哪就锁在哪
(run_policy_trials.py:531 — 日志里那句「头部锁定在它此刻的实际位置 (-3.78, 50.11)」),
不是一个固定值。也就是说每次试跑头可能都在不同位置。/api/head/aim 存一个视角,比如叫
trial),标定前、每次试跑前都先把头移到这个预置位。没有这一步,后面所有精度都是假的。
| 内参 (intrinsics) | 外参 / 手眼 (extrinsics) | |
|---|---|---|
| 回答什么 | 像素 (u,v) + 深度 → 相机坐标系里的 3D 点 | 相机坐标系的点 → 机械臂坐标系的点 |
| 现状 | 是猜的:FX=FY=320, CX=320, CY=240(按 90° 水平视角假设,cup_locate_demo.py:36) | 完全没有 |
| 不做的后果 | Z(距离)准,X/Y 有系统偏差 | 只能说「杯子离相机 0.4 m」,说不出「手臂该往哪伸」 |
仓库里没有任何现成代码:全仓 grep solvePnP / findChessboard / aruco / apriltag / calibrateCamera
命中 0。这是从零开始的一项工作。
思路:机械臂自己知道爪尖在哪(正运动学),相机也能看到爪尖在哪(深度反投影)。 同一个点的两套坐标凑够若干组,就能解出两个坐标系之间的刚体变换。
trial,之后整个过程不许动头。arm_kinematics.forward() 只给
矢状面内的 (y, z)(肩抬 + 肘),左右那一维要用 shoulder_pan 单独旋转出来,
再沿腕的朝向加上爪长 gripper_len_m = 0.1179。cv2.estimateAffine3D 或 scipy.spatial.transform.Rotation.align_vectors 都能做。cv2.findChessboardCorners + cv2.calibrateCamera → 得到真实的
fx, fy, cx, cy 和畸变系数,替换掉 cup_locate_demo.py 里那四个猜的数。cv2.solvePnP 得到「相机 → 棋盘格」。
再让机械臂爪尖去触碰棋盘格上几个已知角点,由 FK 得到「机械臂 → 棋盘格」。
两者一组合就是「相机 → 机械臂」。2.3.1:1.0 深度 / 2.3.1:1.4 RGB),出厂就是对齐的。
所以可以用 RGB 图找棋盘格角点、用深度图取那个像素的距离,不需要额外做深度↔RGB 的配准。
标定产物(存成 configs/hand_eye.json):
intrinsics: fx fy cx cy + 畸变系数
extrinsics: 4x4 变换矩阵 T_cam_to_arm
head_preset: "trial" ← 标定时头在哪,用的时候必须也在这
residual_mm: 6.2 ← 验收残差,写进去,以后才知道退化了没有
用的时候:
深度图(u,v,z) --内参--> 相机系(X,Y,Z) --外参--> 机械臂系(x,y,z) --> 就是「杯子在哪」
先把话说清楚:路线 C 的数据集那一半是现成的、而且质量不错;策略那一半一个现成的都没有。
| 能力 | 出处 | 说明 |
|---|---|---|
| 深度专用落盘格式 | datasets/utils.py:143 | DEPTH_FILE_PATTERN = "frame-{i:06d}.tiff" —— 16-bit TIFF,无损,不走视频编码 |
| 深度特征自动识别 | utils/feature_utils.py:98 | 单通道 (H,W,1) 的相机自动带 info["is_depth_map"]=True |
| 统计量不做 /255 | datasets/compute_stats.py:537 | 带该标志的特征跳过 RGB 的 0–255 归一化 —— 毫米值被当一等公民 |
| 深度视频编码器 | datasets/video_utils.py:1224 | DepthEncoderConfig,要压成视频也有专门通路 |
| 元数据能查 | datasets/dataset_metadata.py:399 | 有专门的「哪些 key 是深度」的查询 |
| 相机侧样板 | cameras/realsense/camera_realsense.py:140 | RealSense 的 use_depth + read_depth() 已实现,可照抄结构 |
is_depth_map:只命中 datasets/ 和 utils/,
policies/ 下 零命中。policies/ 下确实能 grep 到 "depth" 字样,但那是
depth=18 这种网络层数,不是深度相机 —— 2026-09-03 核对时差点误读。)
Camera 接口
(connect / read / async_read / disconnect),输出 (H,W,1) uint16。
难的那部分已经有了 —— v4l2_depth.DepthCamera 已经把裸 V4L2 读 Z16 做完,
这一步主要是接口适配。工作量:小lerobot-record → 数据集带
is_depth_map 特征 → lerobot-train 不报错。
这一步没有任何人验证过,必须先试通再谈其它。工作量:小,但风险未知grep 全仓 run_policy_trials.py
对 depth_recorder/DepthCamera 命中 0)。(H,W,1) 特征」明确未验证。03-software/scripts/ 为准 ·
配套页:深度相机的调用链与三个缺陷