← 返回报告索引
2026-09-04 · 远程执行结果

远程四件事的结果:A 没过、B 把 B0 的速度理由毙了、C 有个更硬的答案、D 做完了

对应 HANDOFF-REMOTE-2026-09-04.md 的 A/B/C/D。全程没碰硬件。 每条结论都标了证据等级,联系表原图在下面,可以自己复看。

⚠ 2026-09-04 二次更正。操作者看图时指出四处:左上角是 VR 手柄、 桌上白洞洞也被框了、我叫「白玻璃杯」的是蓝色塑料杯、我叫「天花板通风口」的就是那个桌面白洞洞。 把 43 个框逐个裁开放大核对,四条全对。 后果:本页 A 那一节的数字和结论已就地改正,真实命中率比第一版低得多。 过程见执行记录第 24 步。

结论先行

  1. A 没过验收,不进入阶段 B。逐框人工核对后,最好的一个是 「框到目标杯」5/36 = 13.9%(出框率 44.4% 是上界,不是命中率)。 失败原因有两个,同时存在:精度不行(框到 VR 手柄、桌面圆孔) + 消歧没做(场上还有第二只真杯子)。
  2. B:B0 的速度理由不成立,当场毙掉。顺带查出两个被传错的数 ——146 ms 是 ACT 的,不是 SmolVLA 的(SmolVLA 实测 1329 ms,10.3 倍)。
  3. C2 不用权衡:OpenCV 5 删掉了 cv2.CascadeClassifier,Haar 那套现在根本跑不起来。 留 YuNet。C3 注释已改、行为没动。C4 文献首次查了,三篇带链接。
  4. D 做完了,--dry 已验(10 项自检全过),并端到端过了读取端。
需要操作者拍板的两件事(都没动,等你说): ① policy_safety 要不要把 HEAD_DIMS 的绝对钳位加回来 —— 那是行为变更; ② 演示里到底要不要真的把杯子交到人手上 —— 递交要碰人,而安全层对「人在哪」一无所知。

A · 离线跑开放词表检测

跑法:head 相机,跨 3 个数据集各抽 12 帧共 36 帧(不同批次 = 不同杯子,天然覆盖 「换一种杯子」这条验收),逐帧出联系表人工看过图。 帧固化在 05-training/detect/frames/,保证几个 python 环境比的是同一批画面。

出框率不是命中率。把每个被判成 cup 的框裁出来放大逐个看:

模型出框是杯子是目标杯非杯子的是什么
OWL-ViT16/3610/36 = 27.8%5/36 = 13.9% VR 手柄 ×5、白色夹爪 ×1
GDINO tiny27/361/36 = 2.8%1/36 = 2.8% 桌面穿线圆孔 ×10、夹爪/机械臂 ×8、线缆或模糊 ×7、鼠标 ×1

OWL-ViT 那 10 个真杯子里,只有 5 个是目标杯,另外 5 个是画面左上角另一只真杯子 (淡蓝绿色不透明塑料杯,离相机近)。

OWL-ViT 的 16 个 cup 框逐个放大
OWL-ViT 判成 cup 的 16 个框全在这里。前 5 个是蓝绿塑料杯, 第 6–10 个是白色 VR 手柄,第 11–15 个是米色目标杯,最后 1 个是白色夹爪。
GDINO 的 27 个 cup 框逐个放大
GDINO 那 27 个框,26 个不是杯子。满屏那个白色桌面上带一圈小孔的圆洞是桌面穿线孔 —— 第一版我称它「天花板通风口」是错的(头相机俯视,画面上方是桌子远端)。

原始出框率与耗时

模型出框率耗时/帧备注
OWL-ViT base-patch3216/36 = 44.4%225 ms (GPU) 其中 6 个不是杯子、5 个是另一只塑料杯
Grounding DINO tiny (thr 0.15)27/36 = 75.0%2657 ms (GPU) 假的,26/27 不是杯子
GDINO 摘掉那条毒 query2/36 = 5.6%2660 ms 摘掉之后 a cup 基本不出框
EfficientDet-Lite0(COCO cup/wine glass)0/36 = 0% 66 ms (CPU)阈值 0.3 一个都没出
EfficientDet-Lite2 (thr 0.1)7/36 = 19.4%411 ms (CPU) 召回太低(这一行未逐框核对)
没有一个接近 90%。最好的一个真实命中率 13.9%。阶段 A 不通过,不进入阶段 B。

看图:三张联系表

OWL-ViT 在 0826 数据集上的联系表
OWL-ViT / 0826,出框 10/12 —— 但全是错的。 绿框每一帧都钉在左上角背景里那只白玻璃杯上, 而桌面中央那只米色目标杯一次都没被框到。 这是第四次「数字好看、锁错目标」,也是为什么 GOAL 把「人工看图」写成不许跳过的一步。
Grounding DINO 把天花板通风口判成玻璃杯
Grounding DINO 的 75% 是这么来的。 绿框几乎每帧都在画面上方那个圆形天花板通风口上,标签 a drinking glass(0.17–0.22)。 目标杯在画面里清清楚楚,被完全忽略。75% 这个数字如果不看图,会被当成好消息。
OWL-ViT 在 sep3 数据集上有正确的框
它不是不会 —— 同一个 OWL-ViT 在 sep3 这批上出的 5 个框里有 4 个是紧紧框住目标杯的 (第 1、3、7、10 格),只有第 11 格框到了黑色夹爪。 所以问题不是「不认识杯子」,是不稳定 + 选错。

★ 这一轮真正的发现:两个问题同时存在 ★

操作者在中途指出:「一开始我以为他无法识别什么是玻璃杯,但现在看来它可以识别, 那么其实是如何选择杯子的问题……但底层的杯子识别没有错。」
前半对,后半不对 —— 目标杯确实每帧都在候选里(下面那段代码), 但逐框放大之后发现 cup 类里还混着 VR 手柄、桌面圆孔、夹爪。识别也不行。

把 OWL-ViT 的 cup 类候选框全部打印出来核对,台面上的目标杯每一帧都被检出了, 只是排在第 2–4 名:

--- frame 0 ---                            --- frame 8242 ---
  #0 0.138 a mug   (7,52,60,103)  背景玻璃杯     #0 0.090 a mug  (303,47,381,110)  ← 目标杯
  #3 0.044 a cup   (195,65,235,122) ← 目标杯      #1 0.032 a cup  (308,48,366,86)   ← 目标杯

所以这是两个问题被混成了一个:

问题现在的状态
①识别:这团东西是不是杯子 不行 —— OWL-ViT 6/16 不是杯子,GDINO 26/27 不是
②消歧:场上几只杯子,哪只是目标 完全没做 —— 10 个真杯子里 5 个是另一只

仍然成立的那半:把候选全部列出来时,目标杯在检查过的帧里都在候选中(排第 2–4 名)。 但「候选里有」推不出「识别没坏」。

★ 由此带出一个几乎零成本的动作 ★ 假阳性高度集中在两样东西上:VR 手柄(OWL-ViT 5/16)和桌面穿线圆孔(GDINO 10/27)。 手柄是录制时随手放桌边的,收走就没了;圆孔是桌子的固定特征,盖一下就没了。 在换模型、上深度之前先清场重录一小批,是整件事里性价比最高的一步 —— 这不是写外观规则,是改场景不是改代码。(现场做,我远程做不了。)

三条实测边界条件(下一轮直接当输入用)

  1. 2D 框面积不能当「最近的杯子」用。试过按框面积挑(--pick largest),没救回来: 背景那只玻璃杯离相机更近、框反而更大。 真正的「最近」得用深度旁路(640×480 16-bit PNG、毫米,要按 100–2000 mm 裁离群值)。
  2. query 集合本身是个变量,而且各模型口味相反。 a drinking glass 对 GDINO 是毒 query(钓通风口),对 OWL-ViT 反而是主力 ——摘掉后 OWL-ViT 从 16/36 掉到 12/36。 换一个模型,query 集合必须重新过一遍人工看图,不能沿用。
  3. 「不许写外观规则」这条的边界(澄清,不是放宽):禁的是我们手写的外观规则。 给开放词表模型一句语言 prompt,或给干扰物一个自己的标签 (加 "a ceiling vent" 实测能把 GDINO 从钓通风口拉回来),不在禁令内 —— 那是运行时可换的输入,不是编译进代码的假设。 但它不解决「换个杯子要改 prompt」,只能当权宜。
顺带纠正自己一个 bug。第一版腕相机跑出 37.5%,是因为我自己那条 「框面积不得超过全帧 25%」的规则把正确检测滤掉了 —— 腕相机里杯子占半个画面。 放宽到 85% 后是 38.9%。这个数从来不是模型的问题,是我的过滤器的问题,记在这里免得下次又当成模型证据。
本来差点白干的一件事:下模型之前先查仓库。 03-software/conversational_ai/vision/objects.py 2026-09-03 就写好了, 已经在用 COCO 检测器,80 类里本来就有 cup 和 wine glass,66 ms/帧 CPU。 交接文档没提它。这正是 memory 里「自研前先查上游」那条。

B · 判 B0 生死

结论:B0 的速度理由不成立,当场毙掉。 要留 B0,只能靠「结构先验帮泛化」这一条,那就得和 B1/B2 重新比, 不能再拿「顺带把推理变快」当加分项。

更正一:146 ms 不是 SmolVLA 的数,是 ACT 的

出处是 run_policy_trials.py:265 的注释,原文写的是 「2026-08-21 实测(ACT 51.6M,三路相机,本机 Orin)cuda 146 ms/帧」。 传到交接文档和 GOAL-DEPTH-INTEGRATION.md 时模型名被换成了 SmolVLA。 同机同法重测:

模型一次推理(3 路相机)chunk
ACT129.5 ms100 步
SmolVLA1329.2 ms50 步

SmolVLA 是 ACT 的 10.3 倍。

更正二:「6.9 Hz vs 训练 30 Hz」这个对比对分块策略本身就不成立

SmolVLA 一次推理规划 n_action_steps = 50 步,在 30 Hz 回路上等于 1667 ms 的动作。 该问的不是「推理频率够不够 30 Hz」,而是一次推理能不能在 1667 ms 内算完:

实测 1329 ms,预算 1667 ms,余量 +337 ms(用掉 80%)。 摊到每个控制步 26.6 ms。紧,但没有超预算 —— 和「6.9 Hz 远远追不上 30 Hz」是完全不同的处境。

Y 的上界:视觉 token 不是大头

做法:在预处理之后从 batch 里删 image 键,而不是把画面涂黑 ——涂黑的图照样过 vision encoder、照样产生 token,量到的会是 0,那是个假答案。 本 checkpoint empty_cameras = 0,所以删掉的键不会被补成黑图。

相机数中位耗时
1 路1210.7 ms
2 路1278.4 ms
3 路1329.2 ms

每多一路 +59.2 ms,三路的视觉部分合计 177.7 ms = 总耗时的 13%。 而这 177.7 ms 是 Y 的上界 —— B0 是把每路 token 从 256 压到 16,不是压到 0,真实 Y 更小。

所以 X(分割模型每帧开销)必须显著小于 177.7 ms,B0 才可能净正。 FT-Dinosaur 是个 ViT,在这块板子上不可能比 177.7 ms 小多少。不用再去下它了。

证据等级:实测。Jetson Orin Nano Super,train310 + CUDA,warmup 3 次、计时 12 次取中位。 数据 05-training/detect/bench_cameras.json,脚本 bench_smolvla_cameras.py。

C · 盘点 HRI + 修安全层那处数字错误

C2 · 留哪套人脸检测 —— 这个选择已经被环境替我们做了

OpenCV 5.0.0 删掉了 cv2.CascadeClassifier。
环境cv2CascadeClassifier(Haar)FaceDetectorYN(YuNet)
train310(跑策略/脚本的那个)5.0.0没有有
conversational_ai/.venv5.0.0没有有
lerobot4.13.0有有

实测把 face_perception.HaarFaceDetector() 在 train310 里构造一下,直接抛 AttributeError: module 'cv2' has no attribute 'CascadeClassifier'。 face_follow.py:262 调的是同一个 API,在同一环境里会死在同一个地方 (未单独跑 —— 跑它要开相机、要动舵机,属于现场的事)。

Haar 那套不是「不如 YuNet」,是「现在根本跑不起来」, 而唯一还能跑它的 lerobot 环境恰恰是 torch 没有 CUDA、不能跑策略的那个。

决定:留 YuNet。落地方式是在 face_perception.py 里加一个 YuNetFaceDetector 后端 —— 那文件第 133 行本来就定义了 FaceDetectorBackend 协议, 这是它设计好的扩展点,不是第三套实现。 然后让 face_follow.py 改成用 face_perception,而不是像现在这样把 Haar 代码又内联抄了一遍。

YuNet 实测开销(train310,8 帧热身后取中位):640×480 18.9 ms/帧,320×240 10.0 ms/帧。 模型 227 KB,已在仓库里。

只量了耗时,没量准确率 —— 手头数据集画面里没有人脸。 「YuNet 比 Haar 检得准」这条我们没有自己的证据,现在只靠上游说法。要验得用带人脸的录像,那是现场的事。

另记:conversational_ai/models/ 里还躺着一个 blaze_face_short_range.task(MediaPipe BlazeFace,229 KB)。 全仓 grep 过,没有任何代码引用它 —— 是个没接线的资产,不是第三套实现。别把它算进来,也别顺手接上去。

C3 · 安全层注释里的事实错误 —— 已改注释,行为没动

独立复核过(不是照抄交接文档):读 xlerobot-cup-grasp-20260820-0230 的 observation.state[13],19453 帧全部是 49.8462(min == max),不是注释写的 99.649。 按注释自己那条公式重算:

head_2counts对 range_max = 2617
99.649(注释写的)3181.5超出 +564.5
49.8462(实测)2615.0在范围内,差 −2.0

旁证:configs/head_presets.json 的 cup-grasp-v0 tilt = 2619,比 range_max 高 2 counts。 两个独立来源都说标定边界和头的实际位置只差个位数 counts。

这撤销了什么:第 4 步跳过 HEAD_DIMS 绝对钳位,理由正是那条被撤回的断言。 理由没了,但跳过我原样留着 —— 那是行为变更,按规矩要操作者拍板。 今天头钉死所以无害;一旦让头跟随人脸,安全层对头部就没有绝对边界。

C4 · 人机递交文献(首次查,三篇带链接)

文献对我们的直接含义
Ortenzi 等,Object Handovers: A Review for Robotics
IEEE T-RO 37(6):1855–1873, 2021
递交切成 pre-handover 和 physical exchange 两段。 我们连第一段都没有 —— 没有任何机制去和观众就「在哪交、什么时候交」达成约定。
Intent-Handover
2025,n=30 被试内实验
摘要举的例子就是 "the cup"。三条能直接抄: ①抓的位置要给人留出握的地方(我们的抓取策略完全不考虑); ②夹爪不能贴着人的手过去(安全层现在对「人在哪」一无所知); ③用上半身关键点估计接收姿态(YuNet 的 5 个关键点是起点)。
Khanna 等,Impact of Object Weight in Handovers
2025
松手时机是个独立控制问题,不是「到位了就张爪」。 而我们的杯子会从空到满变重量。
证据等级:三篇的摘要都拉了 arXiv 原文核对(作者、年份、原话)。 搜索摘要里流传的具体数字 ——「递交平均时长约 500 ms」「拉力阈值 3 N 松手」—— 我没有核到原文,所以不写进结论。要用得去读全文。

D · 关键帧采集 —— 做完了

新增 03-software/scripts/trial_keyframes.py,接进 run_policy_trials.py。 三个设计选择都是为了绕开这一步已知的坑:

坑做法
30 Hz 回路里同步写盘会掉帧 回路里只做数组 copy(424×240×3 ≈ 305 KB,约 0.1 ms),三张 JPEG 全部在循环结束后的 finish() 里写
面板模式不能插 input()本模块完全不与人交互
「闭爪瞬间」拍脑袋定阈值,换夹爪就废 定义成整场里爪子开度最小的那一帧 —— 无阈值、无标定,换夹爪照样成立
写死 "right" 会在左臂数据上错 两只爪子都跟,取行程更大的那只当作业臂(sep2/sep3 是左臂批次)
--dry 验证通过,10 项自检全过。 --dry 原本在进入试验循环之前就 return,光跑它碰不到这段代码, 所以把验证做成 trial_keyframes.selftest():合成一段「先开后闭再开」的观测, 把帧号编进像素值来核对挑中的确实是最闭合那一帧 —— 不是只看有没有文件。 另外端到端过了读取端 trials_io.normalize_trial,字段原样读出。

自测清单

项结果
5 个新增/改动的 .py 全部 py_compile通过
trial_keyframes.py 自检(10 项)全过
run_policy_trials.py --dry通过(推理 + 安全层 + 关键帧自检)
trials_io 端到端读新字段通过
改 run_policy_trials.py 前确认没人在跑pgrep 无进程
36 帧联系表人工看图全部看过(本页三张是其中的证据)
安全层行为改动零 —— 只改了注释
硬件全程没碰
git 写命令一条没跑
2026-09-04 · 对应 HANDOFF-REMOTE-2026-09-04.md。 原始数据 05-training/detect/*/records.json,联系表 05-training/detect/*/sheet_*.jpg, 计时 05-training/detect/bench_cameras.json。 三份 GOAL 已按「累积原则」回填。