对应 HANDOFF-REMOTE-2026-09-04.md 的 A/B/C/D。全程没碰硬件。
每条结论都标了证据等级,联系表原图在下面,可以自己复看。
cv2.CascadeClassifier,Haar 那套现在根本跑不起来。
留 YuNet。C3 注释已改、行为没动。C4 文献首次查了,三篇带链接。--dry 已验(10 项自检全过),并端到端过了读取端。policy_safety 要不要把 HEAD_DIMS 的绝对钳位加回来 —— 那是行为变更;
② 演示里到底要不要真的把杯子交到人手上 —— 递交要碰人,而安全层对「人在哪」一无所知。跑法:head 相机,跨 3 个数据集各抽 12 帧共 36 帧(不同批次 = 不同杯子,天然覆盖
「换一种杯子」这条验收),逐帧出联系表人工看过图。
帧固化在 05-training/detect/frames/,保证几个 python 环境比的是同一批画面。
出框率不是命中率。把每个被判成 cup 的框裁出来放大逐个看:
| 模型 | 出框 | 是杯子 | 是目标杯 | 非杯子的是什么 |
|---|---|---|---|---|
| OWL-ViT | 16/36 | 10/36 = 27.8% | 5/36 = 13.9% | VR 手柄 ×5、白色夹爪 ×1 |
| GDINO tiny | 27/36 | 1/36 = 2.8% | 1/36 = 2.8% | 桌面穿线圆孔 ×10、夹爪/机械臂 ×8、线缆或模糊 ×7、鼠标 ×1 |
OWL-ViT 那 10 个真杯子里,只有 5 个是目标杯,另外 5 个是画面左上角另一只真杯子 (淡蓝绿色不透明塑料杯,离相机近)。
| 模型 | 出框率 | 耗时/帧 | 备注 |
|---|---|---|---|
| OWL-ViT base-patch32 | 16/36 = 44.4% | 225 ms (GPU) | 其中 6 个不是杯子、5 个是另一只塑料杯 |
| Grounding DINO tiny (thr 0.15) | 27/36 = 75.0% | 2657 ms (GPU) | 假的,26/27 不是杯子 |
| GDINO 摘掉那条毒 query | 2/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) | 召回太低(这一行未逐框核对) |
a drinking glass(0.17–0.22)。
目标杯在画面里清清楚楚,被完全忽略。75% 这个数字如果不看图,会被当成好消息。
把 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 名)。 但「候选里有」推不出「识别没坏」。
--pick largest),没救回来:
背景那只玻璃杯离相机更近、框反而更大。
真正的「最近」得用深度旁路(640×480 16-bit PNG、毫米,要按 100–2000 mm 裁离群值)。a drinking glass 对 GDINO 是毒 query(钓通风口),对 OWL-ViT 反而是主力
——摘掉后 OWL-ViT 从 16/36 掉到 12/36。
换一个模型,query 集合必须重新过一遍人工看图,不能沿用。"a ceiling vent" 实测能把 GDINO 从钓通风口拉回来),不在禁令内 ——
那是运行时可换的输入,不是编译进代码的假设。
但它不解决「换个杯子要改 prompt」,只能当权宜。03-software/conversational_ai/vision/objects.py 2026-09-03 就写好了,
已经在用 COCO 检测器,80 类里本来就有 cup 和 wine glass,66 ms/帧 CPU。
交接文档没提它。这正是 memory 里「自研前先查上游」那条。出处是 run_policy_trials.py:265 的注释,原文写的是
「2026-08-21 实测(ACT 51.6M,三路相机,本机 Orin)cuda 146 ms/帧」。
传到交接文档和 GOAL-DEPTH-INTEGRATION.md 时模型名被换成了 SmolVLA。
同机同法重测:
| 模型 | 一次推理(3 路相机) | chunk |
|---|---|---|
| ACT | 129.5 ms | 100 步 |
| SmolVLA | 1329.2 ms | 50 步 |
SmolVLA 是 ACT 的 10.3 倍。
SmolVLA 一次推理规划 n_action_steps = 50 步,在 30 Hz 回路上等于 1667 ms 的动作。
该问的不是「推理频率够不够 30 Hz」,而是一次推理能不能在 1667 ms 内算完:
做法:在预处理之后从 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。
cv2.CascadeClassifier。| 环境 | cv2 | CascadeClassifier(Haar) | FaceDetectorYN(YuNet) |
|---|---|---|---|
train310(跑策略/脚本的那个) | 5.0.0 | 没有 | 有 |
conversational_ai/.venv | 5.0.0 | 没有 | 有 |
lerobot | 4.13.0 | 有 | 有 |
实测把 face_perception.HaarFaceDetector() 在 train310 里构造一下,直接抛
AttributeError: module 'cv2' has no attribute 'CascadeClassifier'。
face_follow.py:262 调的是同一个 API,在同一环境里会死在同一个地方
(未单独跑 —— 跑它要开相机、要动舵机,属于现场的事)。
Haar 那套不是「不如 YuNet」,是「现在根本跑不起来」,
而唯一还能跑它的 lerobot 环境恰恰是 torch 没有 CUDA、不能跑策略的那个。
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,已在仓库里。
另记:conversational_ai/models/ 里还躺着一个 blaze_face_short_range.task(MediaPipe BlazeFace,229 KB)。
全仓 grep 过,没有任何代码引用它 —— 是个没接线的资产,不是第三套实现。别把它算进来,也别顺手接上去。
独立复核过(不是照抄交接文档):读 xlerobot-cup-grasp-20260820-0230 的
observation.state[13],19453 帧全部是 49.8462(min == max),不是注释写的 99.649。
按注释自己那条公式重算:
| head_2 | counts | 对 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。
HEAD_DIMS 绝对钳位,理由正是那条被撤回的断言。
理由没了,但跳过我原样留着 —— 那是行为变更,按规矩要操作者拍板。
今天头钉死所以无害;一旦让头跟随人脸,安全层对头部就没有绝对边界。| 文献 | 对我们的直接含义 |
|---|---|
| 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 |
松手时机是个独立控制问题,不是「到位了就张爪」。 而我们的杯子会从空到满变重量。 |
新增 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 写命令 | 一条没跑 |
HANDOFF-REMOTE-2026-09-04.md。
原始数据 05-training/detect/*/records.json,联系表 05-training/detect/*/sheet_*.jpg,
计时 05-training/detect/bench_cameras.json。
三份 GOAL 已按「累积原则」回填。