← 返回报告索引
2026-09-03 · 深度视觉

深度相机:现在是怎么被调用的,以及深度推理该怎么接进来

这一页回答两件事:①今天这套深度录制在代码里到底怎么串起来的、它坏在哪;②要让深度真正参与抓取, 有哪三条路可走、各自代价多大。所有结论都标了出处(文件:行号 或 实测数字), 推断和实测分开写。

写这一页的直接原因:2026-09-03 根分区被深度录制写满,连开发环境的命令都跑不动了。 排查下来发现这套旁路设计有三个未被发现的缺陷,而且它已经悄悄让 9-02 那晚 102 集数据里的 88 集丢了深度 —— 当晚没有任何提示。 同时发现一件更难堪的事 —— lerobot 早在 2026-06-27 就原生支持深度了,这套旁路是本可以完全不写的。
先认一个错,因为它决定了怎么读这一页。
这套自研旁路写于 2026-08-31。而 lerobot 的官方深度支持 (feat(depth maps): adding support for depth in LeRobot,PR #3644) 合入于 2026-06-27,本机这份 checkout 停在 2026-08-07 —— 写旁路的时候,原生支持已经躺在本地代码里三周多了。
更糟的是当时写进代码注释的理由:「深度进 LeRobot 数据集会被视频编码压毁」。 这句话本身就是错的 —— lerobot 存深度用的是 16-bit TIFF(datasets/utils.py:143),根本不走视频编码。
所以这不是「当时没有更好的选择」,是没查上游就自己造了一个,还给它编了一个听起来合理的理由。 本页第三节原本写成「新发现」,那是在给自己留面子;实际是三周前就该发现的东西。

先说结论

问题回答
深度现在能用吗半能。硬件和采集是好的(实测有效像素占比 0.88),但录制生命周期有 bug,数据是断续的。
深度现在被谁消费没有任何人。它只是写到磁盘上,训练不读它,推理不读它。
最快能产生价值的接法路线 A:程序化感知 —— 深度→杯子 3D 坐标,不训练、不改数据集。cup_locate_demo.py 已经把这条路走通了一半。
最贵且最不确定的接法路线 B:把深度当第四路相机喂给策略。SigLIP / ResNet18 都是 RGB 预训练的,伪彩深度图对它们是分布外输入。
该先做的一件事已完成(2026-09-03):fd 泄漏修了,面板现在报每集深度帧数、0 帧标红。中期仍要迁 lerobot 原生 depth feature。这两项修复之前采的深度数据都不可靠。

一、现在的调用链

三层,从下往上。

① v4l2_depth.py · DepthCamera —— 裸 V4L2 直接读 Orbbec 深度节点,不经过任何 SDK。
/dev/v4l/by-path/platform-3610000.usb-usb-0:2.3.1:1.0-video-index0 · 640×480 · Z16 · uint16 毫米 · 0 表示无读数
open() → read(timeout_s) → close();另有 colourise() 把毫米图转成能看的伪彩 BGR(v4l2_depth.py:189)。
↓
② depth_recorder.py · DepthRecorder —— 起一个后台线程按 30 fps 抓帧落盘。
产物三样:frame_%06d.png(16-bit 单通道 PNG,无损)、timestamps.jsonl(每帧一行 i / t_mono / t_wall / valid)、meta.json(收尾时写)。
attach_to_process(out_dir, proc) 是给子进程路径用的封装:起一个看门线程,proc 一退就 stop()(depth_recorder.py:251)。
↓
③ 两个调用方
VR / 面板录制 —— robot_server.py:5936,用 attach_to_process 绑在 lerobot.record 子进程上,写到 depth/<时间戳>/。
Leader-follower 录制 —— leader_follower_server.py:902,在 _cmd_start 里手动 start()、_cmd_stop 里手动 stop(),写到 depth/ep<N>-<时间戳>/。
关键设计:深度不进 LeRobot 数据集,它是「旁路」(sidecar)。 数据落在数据集目录旁边的 depth/ 子目录里,靠 timestamps.jsonl 的墙钟事后对齐。 代码注释写明了原因(robot_server.py:5931):深度是 16-bit 毫米值, 进 LeRobot 数据集会被视频编码压毁。
这个理由是错的 —— lerobot 存深度走 16-bit TIFF,不走视频编码(见第三节)。 写这条注释时没有去核对上游支持,整套旁路建立在一个没验证的前提上。

二、这套设计坏在哪(2026-09-03 实测)

缺陷 1 · Start 按两次会孤儿掉录制线程 (今天已修)

_cmd_start 原来直接 self._depth = DepthRecorder(...),覆盖前不停旧的。 按第二次 Start 时,上一个 DepthRecorder 就没人持有引用了:它的线程是 daemon, 只认自己的 _stop 事件,而 _cmd_stop 只停 self._depth 当前指向的那一个。

实测后果(xlerobot-left-pick-cup-sep2-20260902-1819 的 ep20-20260902-191409):
09-02 19:14:09 起跑了 13.7 小时到次日 08:53,写了 220,412 帧 / 27 GB,把根分区写满。 同数据集其余 47 个深度目录都是 44–155 帧 / 8–18 MB。
诊断特征:该目录没有 meta.json —— 那个文件只在 stop() 里写,缺失即证明 stop() 从未被调用。
最后一行日志是 libpng error: Write Error(08:53:13),即磁盘写满、线程自行退出的时刻。

缺陷 2 · 摄像头句柄泄漏 (今天已修)

DepthCamera.open() 先 os.open() 拿到 fd,再做一串可能抛异常的 ioctl(S_FMT / REQBUFS / STREAMON)。 设备被占时 ioctl 抛错,DepthRecorder.start() 捕获后只把 _cam 置 None —— 那个已经打开的 fd 永远不关。

# v4l2_depth.py:119
def open(self):
    self.fd = os.open(self.device, os.O_RDWR | os.O_NONBLOCK)   # ← 先占住
    ...
    fcntl.ioctl(self.fd, VIDIOC_S_FMT, fmt)                     # ← 这里抛异常,fd 就漏了

实测:leader_follower_server.py 进程上挂着 34 个 /dev/video0 句柄(ls /proc/<pid>/fd)。 depth_recorder.py:108 那条「2026-08-31 观测到:开-关-立刻重开会让流起不来,每次 read 都超时」的注释, 正是这个泄漏的表象 —— 当时把症状当成了 Orbbec 的怪癖,绕过去了,没找到根因。

已修:open() 的 body 包一层 try/except,任何一步 ioctl 失败就调用 self.close() (它按当前 fd/streaming/buffers 状态安全收尾,不要求 open() 已经走完)再把异常抛出去。
实测验证(monkeypatch 让第 2 个 ioctl 调用抛错,模拟半途失败):修复前会漏 1 个 fd; 修复后 open() 前后的 fd 集合完全一致,self.fd 正确回到 None,正常路径(真实设备上 open→read→close)不受影响。
leader-follower 服务已重启加载,重启后 /dev/video0 句柄数:0(此前累积到 34)。

缺陷 3 · 相机开着但一帧不给:USB 层面的假死 (无软件修法)

9-02 下午右臂四个数据集(1654 / 1717 / 1734 / 1746)的深度目录全部 0 帧, meta.json 一律是 frames=0, dropped=0, error=None,起止正好 10.0 秒。 这个组合只有一种解释:open() 成功、ioctl 全过,然后 5 次 warm-up 读帧每次超时 2 秒 = 10 秒, 还没进主循环 stop() 就来了 —— 相机开着,但一帧都不给。1819 的 ep0–ep6 同样。

实测结果
单独进程里冷开 / 关完立刻重开 / 隔 3 秒重开,各 5 轮每轮第 1 次读就出帧,0.1–0.2 s —— 「快速重开会起不来流」(08-31 那条注释)不成立
9-02 每集之间的间隔17–56 秒,与「太快」无关
假死从何时到何时至少 16:46 起,到 18:31(ep6)都是 0 帧;19:00(ep7)起突然正常
中间发生了什么内核日志 18:32:16:同一 USB hub(1-2.3)上的腕相机 USB2.0_CAM1 断开并重新枚举 —— 一次物理总线事件,之后深度就好了

USB 拓扑(/dev/v4l/by-path/)—— 深度和 RGB 头是同一个物理设备的两个接口,腕相机在同一个 hub 的另一个口:

usb-0:2.3.1:1.0  → video0   Orbbec Gemini 335   ← 深度 (Z16)
usb-0:2.3.1:1.4  → video6   Orbbec Gemini 335   ← RGB 头,同一颗相机
usb-0:2.3.2:1.0  → video9   (腕) ← 18:32:16 就是这个口断开重连的
usb-0:2.3.3:1.0  → video10  USB2.0_CAM1 (腕)
实测到的:0 帧的时间范围(≥16:46 到 18:31)、恢复的时间点(19:00)、 中间唯一的总线事件(18:32:16 同 hub 另一口重新枚举)、单独进程里怎么开都秒出帧。
推断(未证):「是 USB/驱动层卡死,开关设备节点清不掉,只有总线级复位能清」—— 这是从「软件侧怎么试都正常、而恢复恰好紧跟一次物理总线事件」推的, 没有做过对照实验(比如卡死时执行 unbind/bind 看是否恢复)。
没查到的:初始为什么卡死。日志里 16:46 之前没有线索。
它最要命的地方和缺陷 1 一样:静默。DepthRecorder 把「0 帧」当成一次正常收尾写了 meta.json(error=None),面板上什么都没报。

三个缺陷叠加起来的实际损失(1819 这一个数据集)

缺陷 3 让 ep0–ep6 是 0 帧;缺陷 1 让 ep20 的录制器一直占着相机,缺陷 2 让后续每次开相机都失败且漏一个句柄。结果:

集号深度原因
ep0 – ep60 帧缺陷 3:相机开着但不出帧,18:32 总线事件之后才好(7 集)
ep7 – ep20有,每集约 105 帧正常(14 集)
ep21 – ep33空被 ep20 的孤儿占着相机,每次 open() 都失败(13 集)

证据:ep21 起的每个深度目录都被创建了但一个文件都没有 —— 目录由 os.makedirs 在开相机之前建, 所以「空目录」精确地等于「尝试过、开相机失败」。ep21 的时间戳是 19:14:57,距 ep20 起跑仅 48 秒。
同一晚随后录的 ...-1928(17 集,19:28–19:43)整个数据集没有 depth 目录,同一个原因。

9-02 全晚的账:右臂四个数据集(51 集)深度全部 0 帧;左臂 1819(34 集)只有 ep7–ep20 共 14 集有深度;1928(17 集)完全没有。 一晚 102 集,真正录到深度的 14 集,而且当晚没有任何提示。 这是这套旁路设计最贵的教训:它的失败是静默的。 DepthRecorder.start() 的失败被设计成「降级为 no-op,绝不影响 RGB 录制」—— 这个取舍本身是对的,但降级之后没有任何东西把「这一集没有深度」报到面板上。

三、lerobot 原生的深度支持(本来就该用它)

当初做旁路的理由是「深度进数据集会被视频编码压毁」。这个理由从一开始就不成立。 本机 lerobot 0.6.2(~/lerobot/src/lerobot,checkout 于 2026-08-07)里 早已有完整的深度通路,全部来自 2026-06-27 的 PR #3644:

能力出处说明
深度单独的落盘格式datasets/utils.py:143
PR #3644 · 2026-06-27
DEPTH_FILE_PATTERN = "frame-{frame_index:06d}.tiff" —— 16-bit TIFF,不是视频,不存在压毁问题
深度特征标志utils/feature_utils.py:98单通道相机 (H,W,1) 自动打上 info["is_depth_map"]=True;三通道才当 RGB
统计量不做 /255datasets/compute_stats.py:537带 is_depth_map 的特征跳过 RGB 的 0–255 归一化 —— 说明毫米值是被当一等公民对待的
相机侧已有实现cameras/realsense/camera_realsense.py:140use_depth 开关 + read_depth(),RealSense 已经打通
xlerobot 配置里留了口子robots/xlerobot/config_xlerobot.py:45一行被注释掉的 use_depth=True
这意味着:深度可以作为数据集里的正式特征录进去(dtype: "image" + is_depth_map,存成 TIFF), 跟 RGB 帧天然按 frame_index 对齐,不需要墙钟事后对齐,也不需要自研旁路。
但有一个缺口,必须说清楚。 我核对过:is_depth_map 这个标志目前只在数据集侧被使用(落盘格式、统计量、可视化), 策略侧(ACT / SmolVLA)没有任何一处读它(全仓 grep is_depth_map 只命中 datasets/ 和 utils/)。 所以「录得进去」已验证,「训得起来」未验证 —— 见路线 C。

迁到 lerobot 原生之后,这三个缺陷还在不在

这是个绕不开的问题:既然旁路本可以不写,那换成原生就自动没事了吗?逐个看,答案不一样。

缺陷原生之后为什么
1 · 双击 Start 孤儿线程消失 原生的模型是 connect() 开一次相机、和 RGB 在同一个采集循环里读、disconnect() 关。 没有「每集手动 start/stop 一个独立线程」这个环节,也就没有「起了不停」的可能。 这个缺陷是自研旁路特有的。
2 · open 半途失败漏 fd取决于怎么接 它在 v4l2_depth.py:119。给 lerobot 写 Orbbec Camera 类时如果复用这个文件,泄漏一起带过去; 换 pyorbbecsdk 就没有。不管走哪条路,这三行都该先修。
3 · USB 层假死不会消失,但会变响 总线层的事,谁的代码都清不掉。区别在可见性: lerobot 的 record 循环读帧超时会直接抛错中断录制,当场就知道; 现在的旁路是静默写一个 0 帧目录再「正常」收尾,第二天才发现。
所以原生解决的是「出了事你不知道」,不是「不出事」。 而这次 102 集里丢掉 88 集,代价最大的恰恰是「不知道」—— 缺陷 1 和 3 都在静默降级下发生,当晚面板一句话都没报。
反过来说:就算暂时不迁原生,把「这一集录到几帧」报到面板上,也能挡住这次事故的大部分损失。 这是投入产出比最高的一步,见下面的动作顺序。

四、三条接入路线

 路线 A · 程序化感知路线 B · 深度当第四路相机路线 C · 原生 depth 特征
做什么深度 → 杯子 3D 坐标 → 喂给运动规划或作为策略的额外状态输入把深度伪彩成 3 通道图,当成多一个相机喂进现有策略用 lerobot 原生 depth feature 录进数据集,改策略去消费它
要不要训练不要要,重训要,重训 + 改模型
要不要重采数据不要要(现有数据只有 14/34 集有深度)要
现成基础已有一半 —— cup_locate_demo.pycolourise() 现成lerobot 数据集侧现成
主要风险X/Y 用的是估计内参,需要标定SigLIP/ResNet 是 RGB 预训练,伪彩深度是分布外输入策略侧无人消费 is_depth_map,要自己改
建议先做这个不建议先做中期做

路线 A —— 为什么它该排第一

03-software/scripts/cup_locate_demo.py 已经在做这件事:找画面里最近的一团, 用深度值 + 相机内参反投影出 3D 坐标。它不依赖任何 ML 模型,也不需要重训。

# cup_locate_demo.py 的自述,原文照录
# 积木② 深度相机每个像素给你一个距离(毫米);
# 积木③ 把"物体在画面的位置"+"那个位置多远"一拼 → 物体的真实 3D 坐标(反投影)。

它自己也诚实标了两条边界,这两条就是路线 A 的待办:

路线 A 的价值不只是省事。 深度给出的是真实世界坐标,不是「记住的位置」—— 换个桌面位置也能工作,这正是纯模仿学习最难泛化的部分。 把 3D 坐标作为额外的状态输入喂给策略,还能大幅降低模仿学习需要的数据量。

路线 B —— 为什么不建议先做

「多一路相机」听起来最省事,但两个策略的视觉主干都是 RGB 预训练的:

伪彩深度图对这两个主干都是分布外输入 —— 预训练特征在这种图上不成立,等于白白多一路要从头学的输入。 在「至今没有一个 checkpoint 抓起过杯子」的前提下,先加一路不确定的输入, 只会让「是数据的问题还是输入的问题」更难分辨。

路线 C —— 中期目标

录进去这一半是现成的;难点全在消费端。要做的事:

  1. 写一个实现 lerobot Camera 接口的 Orbbec 深度相机类(connect / read / async_read / disconnect, 见 cameras/camera.py),让它输出 (H,W,1) uint16 —— 这样 feature_utils 会自动打上 is_depth_map。 现有的 v4l2_depth.DepthCamera 已经把最难的裸 V4L2 部分做完了,这一步主要是接口适配。
  2. 确认训练管线端到端能吃 (H,W,1) 特征 —— 这一步没人验证过,必须先用几十集小数据集试通再说。
  3. 决定策略侧怎么用:单独的深度编码器,还是和 RGB 拼成 4 通道(后者要放弃预训练主干的第一层权重)。

五、建议的动作顺序

  1. 已完成 修 fd 泄漏(v4l2_depth.py:119,try/except + self.close())。实测验证见缺陷 2。
  2. 已完成 leader-follower 的实时录制页现在报每集深度帧数,0 帧标红 (leader_follower_server.py _cmd_stop:message_kind="err" 时前端渲染红色)。 VR / 面板批量录制那条路径(robot_server.py,整段会话一个 depth 文件夹,不是逐集)还没做,是接下来的一步。
  3. 做一次相机标定,把 cup_locate_demo.py 里的估计内参换成实测值。这是路线 A 唯一的门槛。
  4. 走通路线 A:深度 → 杯子 3D 坐标 → 先只做「报坐标」,人工核对准不准,再谈接进控制。
  5. 小规模验证路线 C:几十集,确认 lerobot 原生 depth feature 能端到端训起来,再决定要不要全面迁移。

六、诚实边界

七、这一页怎么在国内看

这些报告平时发到 HF Space Suyang99/xlerobot-build-reports(public、RUNNING、290 个文件, 2026-09-03 实测从机器上取回 HTTP 200 / 195 KB)。但 国内访问不了 huggingface.co —— 于是自己写的报告自己看不到。文件本来就在仓库里,所以 2026-09-03 给 robot_server 加了直接供页的路由:

http://100.107.145.111:8770/reports                       # 所有报告的索引(71 页)
http://100.107.145.111:8770/reports/space-2026-08-21/...  # 具体某一页

走 Tailscale,不经 HuggingFace,不需要 token(报告页里没有密钥;真实地址和密码在私有的 xlerobot-ops Space)。路径穿越已挡:解析后必须仍在 04-reports/ 内, 否则 403(实测 --path-as-is 三种穿越写法全部 403)。

2026-09-03 · 代码出处以本机 03-software/scripts/ 与 ~/lerobot/src/lerobot(0.6.2)为准 · 配套记忆:深度旁路孤儿线程、磁盘硬约束、数据集补传