对应 HANDOFF-REMOTE-2026-09-04.md 的 A/B/C/D,全程没碰硬件。
这一份按执行顺序写,包括走错的和自己改回来的 —— 那几步恰恰是这次信息量最大的地方。
只想看结论的看另一份按结论组织的。
末尾是需要你考虑的 7 个问题,每个都附了我的建议。
05-training/detect/manual_review.json。交接 §0 说「量任何模型耗时都必须用 train310,用 lerobot 会得到一个假的跑不动」。先验:
| 项 | 实测 |
|---|---|
| 磁盘 | 34 G / 116 G 可用(跑完剩 32 G) |
train310 | torch 2.8.0,cuda.is_available() = True,transformers 5.15.1 |
| 数据集 | 本地 14 批,全在 xlerobot-team/ 下 |
✅ 交接说的都对。往下走。
第一版的选框规则是取全局最高分的那个框,并且用「干扰项 query」(robotic arm / table / human hand …) 让机械臂有自己的标签可归 —— 想法是:只要最高分那个框的标签不是 cup 类,这一帧就记未检出。 这道闸正是前三次缺的。
结果 0/6。
打印原始分数才看明白:
frame 0 a table 0.235 (422x227 整幅) ← 全局最高分
a robot gripper 0.219
a mug 0.138 (53x52) ← 真的杯子,排第 3
frame 5495 a table 0.348 (421x230 整幅) ← 又是它
a cup 0.133 (50x50) ← 真的杯子,排第 8
是我的选框规则错了,不是模型不行。桌子确实是桌子,它有权拿到最高分; 错在我拿「全局第一名」去回答「杯子在哪」。改成两条规则: ①只在标签属于 cup 类的框里挑;②丢掉面积超过全帧 25% 的框。
如果这一步不看图,我会写「OWL-ViT 83%,接近可用」交给你。GOAL 把人工看图写成不许跳过的一步,是对的。
要分清是「模型看不见目标杯」还是「我挑错了」,只能把候选全列出来:
--- 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) ← 目标杯
--- frame 12363 --- #2 0.032 a cup (298,49,360,109) ← 目标杯
#2 0.040 a cup (216,79,258,135) ← 目标杯
「一开始我以为他无法识别什么是玻璃杯,但现在看来它可以识别, 那么其实是如何选择杯子的问题……但底层的杯子识别没有错。」这把「杯子定位」从一个问题拆成了两个:
3 个数据集各抽 12 帧。不同批次 = 不同杯子,天然覆盖 GOAL 那条「至少换一种杯子」的验收。
| 数据集 | 出框 | 看图 |
|---|---|---|
| right-pick-cup-20260826(当前主数据集) | 10/12 | 5 个是另一只蓝绿塑料杯,5 个是 VR 手柄,目标杯 0 个 |
| cup-grasp-20260820(现役模型用的) | 1/12 | 那 1 个框得很准 |
| left-pick-cup-sep3-20260903 | 5/12 | 4 个准,1 个框到黑色夹爪 |
a drinking glass(0.17–0.22)。
第一版我把它写成「天花板通风口」,错了 —— 头相机是俯视,画面上方是桌子的远端,
那是桌面上的穿线圆孔(白色桌面上一个带小孔环的圆洞)。操作者指出后裁图放大证实,见第 24 步。而且它 2657 ms/帧 —— 比 OWL-ViT 慢 12 倍。
a drinking glass → 2/36,一条反证既然通风口是被 a drinking glass 钓来的,摘掉它应该变好。结果 GDINO 掉到 2/36 ——
说明它那 75% 几乎全是通风口,真杯子在 0.15 阈值下本来就不出框。
更意外的是:同一条 query 对 OWL-ViT 是主力。摘掉后 OWL-ViT 从 16/36 掉到 12/36。
盘 C 的时候顺手翻到 03-software/conversational_ai/vision/objects.py ——
MediaPipe EfficientDet-Lite,COCO 80 类里本来就有 cup 和 wine glass,
文件头写着 65 ms/帧 CPU。交接文档没提它,我差一点又去下一遍别的模型。
它跑在 conversational_ai/.venv(只有那个环境装了 mediapipe),
所以先写了 eval_frames.py 把 36 帧固化成 PNG,保证几个环境比的是同一批画面,
不然分不清差异来自模型还是来自抽帧。
| 配置 | 出框 | 耗时 |
|---|---|---|
| Lite0-int8,阈值 0.3 | 0/36 | 66 ms CPU |
| Lite2,阈值 0.1 | 7/36 = 19.4% | 411 ms CPU |
不行 —— head 相机只有 424×240,模型内部还会缩到 320,杯子只剩约 38 px,
正好卡在 objects.py 自己写的「40 px 以下看不见」那条线下面。
但这条仍然值钱:下模型之前先查仓库(memory 里「自研前先查上游」那条)。
既然问题是消歧,最省事的消歧信号是「哪个杯子离机器人近」。俯视视角下,近的框应该大。
加了 --pick largest 重跑 —— 还是那只背景玻璃杯。
no cup (a cup) —— 最高分那个框的标签本来就是 "a cup",
却被判成未检出。因为我在第 2 步加的「框面积不得超过全帧 25%」规则,
在腕相机里把占了半个画面的杯子给滤掉了。第 2 排第 1 格那个 a cup 0.47 才是它本来的水平。放宽到 85% 重跑:38.9%(sep3 是左臂批次,右腕相机看不到杯子,本来就该低)。
(下表的「是杯子 / 是目标杯」两列是第 24 步逐框放大核对后填的, 第一版只有「出框率」那一列,把这一节读得比实际乐观。)
| 模型 | 出框率 | 是杯子 | 是目标杯 | 耗时/帧 |
|---|---|---|---|---|
| OWL-ViT base-patch32 | 16/36 = 44.4% | 10/36 = 27.8% | 5/36 = 13.9% | 225 ms GPU |
| Grounding DINO tiny | 27/36 = 75% | 1/36 = 2.8% | 1/36 = 2.8% | 2657 ms GPU |
真正的命中率是 13.9%,不是 44.4%。
| 模型 | 出框率 | 耗时/帧 | 备注 |
|---|---|---|---|
| GDINO 摘毒 query | 2/36 | 2660 ms | 基本不出框 |
| EfficientDet-Lite0 | 0/36 | 66 ms CPU | — |
| EfficientDet-Lite2 | 7/36 = 19.4% | 411 ms CPU | 召回太低(这一行未逐框核对) |
没有一个接近 90%,最好的一个真实命中率只有 13.9%。
要量「每多一路相机值多少毫秒」,最容易犯的错是把画面涂黑 —— 涂黑的图照样过 vision encoder、照样产生 token,量出来会是 0,那是个假答案。
去读了上游代码:modeling_smolvla.py:341
present_img_keys = [k for k in config.image_features if k in batch]
—— 从 batch 里删键是合法的;再确认本 checkpoint empty_cameras = 0,
所以删掉的键不会被补成黑图。这才是真的少了 token。
ValueError: (b,c,h,w) expected, but got torch.Size([240, 424, 3])
我自己拼的 observation 少了一步。去看上游线上推理怎么走的
(control_utils.predict_action:82),发现要先过
prepare_observation_for_inference()(HWC→CHW、uint8→float[0,1]、加 batch 维、塞 task)。
照抄这条链,量到的才是线上真实推理的耗时,不是我另拼的一个东西。
差 9 倍不能糊过去。全仓 grep 「146 ms」,找到源头
run_policy_trials.py:265,原文写的是:
2026-08-21 实测(ACT 51.6M,三路相机,本机 Orin,12 次取热身后均值)
cpu 2397 ms/帧 → 0.4 Hz cuda 146 ms/帧 → 6.9 Hz
GOAL-DEPTH-INTEGRATION.md(两处)时,模型名被换成了 SmolVLA。同机、同批图、同样的计时方法量 ACT:
| 模型 | 一次推理(3 路相机) | chunk |
|---|---|---|
| ACT | 129.5 ms | 100 步 |
| SmolVLA | 1329.2 ms | 50 步 |
129.5 和文档的 146 同一量级 —— 归属确认无误。SmolVLA 是它的 10.3 倍。
n_action_steps = 50 步,在 30 Hz 回路上等于 1667 ms 的动作。
该问的不是「推理频率够不够 30 Hz」,而是一次推理能不能在 1667 ms 内算完:| 相机数 | 中位耗时 |
|---|---|
| 1 路 | 1210.7 ms |
| 2 路 | 1278.4 ms |
| 3 路 | 1329.2 ms |
每多一路 +59.2 ms,三路视觉部分合计 177.7 ms = 总耗时的 13%。 而这是 Y 的上界 —— B0 是把每路 token 从 256 压到 16,不是压到 0,真实 Y 更小。
conversational_ai/models/ 里躺着一个 blaze_face_short_range.task
(MediaPipe BlazeFace,229 KB)。全仓 grep 过,没有任何代码引用它 ——
是个没接线的资产,不是第三套实现。记一笔免得有人以为「已经有三套了」,也别顺手接上去。
另外发现一处真实重复:face_follow.py:262 把 Haar 的代码又内联抄了一遍,
而 face_perception.py:159 已经有一个 HaarFaceDetector 类。同一段代码两份拷贝。
cv2.CascadeClassifier| 环境 | cv2 | CascadeClassifier(Haar) | FaceDetectorYN(YuNet) |
|---|---|---|---|
train310(跑策略/脚本的) | 5.0.0 | 没有 | 有 |
conversational_ai/.venv | 5.0.0 | 没有 | 有 |
lerobot | 4.13.0 | 有 | 有 |
实测在 train310 里构造 face_perception.HaarFaceDetector(),直接抛
AttributeError: module 'cv2' has no attribute 'CascadeClassifier'。
face_follow.py:262 调同一个 API,同环境会死在同一个地方
(未单独跑 —— 跑它要开相机、动舵机,属于现场)。
lerobot 环境恰恰是 torch 没 CUDA、不能跑策略的那个。交接说「实测 19453 帧全部是 49.846」。我自己读了一遍
observation.state[13](DIMS 下标 13 = head_motor_2.pos):
frames: 19453
head_motor_2: min=49.8462 max=49.8462 唯一值数=1
按注释自己那条公式重算(deg*4095/360 + (range_min+range_max)/2,range 1479..2617):
| 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。
用仓库自己的 joint_degree_limits() 换算成度,这就是我们手上可用的头部边界:
head_motor_1.pos ±149.98 度 (录制时实测 -4.31 度)
head_motor_2.pos ± 50.02 度 (录制时实测 49.85 度,差 0.18 度到头)
改了什么:policy_safety.py 的 HEAD_LOCK_NOTE 改成「撤回 + 复算 + 说明这撤回了什么」。
钳位行为一个字没动 —— 那是行为变更,按规矩要你拍板。详见下面问题 1。
| 文献 | 对我们的直接含义 |
|---|---|
| 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 | 松手时机是独立的控制问题,不是「到位了就张爪」。而我们的杯子会从空到满变重量。 |
GOAL 边界条件 2 要求「改完用 --dry 跑一次验证」。但
--dry 在进入试验循环之前就 return 了 —— 光跑它碰不到关键帧那段代码。
所以先把验证做成 trial_keyframes.selftest():合成一段「爪子先开后闭再开」的观测,
把帧号编进像素值,核对挑中的确实是最闭合那一帧 —— 不是只看有没有生成文件。
四个设计选择,都是绕开已知的坑:
| 坑 | 做法 |
|---|---|
| 30 Hz 回路里同步写盘会掉帧 | 回路里只做数组 copy(约 0.1 ms),三张 JPEG 全在循环结束后的 finish() 里写 |
面板模式不能插 input() | 本模块完全不与人交互 |
| 「闭爪瞬间」拍脑袋定阈值,换夹爪就废 | 定义成整场里爪子开度最小的那一帧 —— 无阈值、无标定 |
| 写死 "right" 会在左臂数据上错 | 两只爪子都跟,取行程更大的那只(sep2/sep3 是左臂批次) |
run_policy_trials.py,--dry 验改之前先 pgrep 确认没人在跑(它会驱动真手臂)。改完 --dry:
推理成功,动作 (17,) 范围 [-13.29, 49.84]
安全层判定: OK
== 关键帧采集自检
ok 作业臂判成 right ok 闭爪在第 5 个 tick
ok init 是第 1 帧 ok closure 是第 5 帧 ok end 是最后一帧
关键帧自检 PASS (10 项全过)
再端到端过读取端 trials_io.normalize_trial,字段原样读出。写入端和读取端都通了。
| 项 | 结果 |
|---|---|
selfcheck.py(HTML 标签配平 + 7 个 py 的 py_compile) | 全过 |
trial_keyframes.py 自检 10 项 | 全过 |
run_policy_trials.py --dry | 通过 |
trials_io 端到端读新字段 | 通过 |
| 36 帧联系表人工看图 | 全部看过 |
| 安全层行为改动 | 零 —— 只改注释 |
| 硬件 / git 写命令 | 全程没碰 / 一条没跑 |
三份 GOAL 已按「累积原则」回填,包括把 GOAL-DEPTH-INTEGRATION.md 里那两处 146 ms 更正掉。
操作者看第 4 步那张候选图时说:
「实际上它把左上角的 VR 手柄、桌子上的白洞洞都进行了识别, 虽然说右下角的橙色杯子是相对稳定的,左边那个也并非玻璃杯子而是蓝色塑料杯。」
随后又补一句:「其实你说的天花板通风口就是桌上的白洞洞。」
这四条如果成立,我第 4 步那个「识别没坏、只是消歧」的结论就说轻了。
所以写了 crop_review_boxes.py:按 records.json 的框把原帧裁出来、放大到 300 px、拼成表,
逐个看。四条全对。
cup-grasp f4863 plastic cup 0.16),而且正好是目标杯。
其余是圆孔 ×10、夹爪/机械臂 ×8、线缆或运动模糊 ×7、鼠标 ×1。| 模型 | 出框 | 是杯子 | 是目标杯 | cup 类精度 | 非杯子的是什么 |
|---|---|---|---|---|---|
| OWL-ViT | 16/36 | 10/36 = 27.8% | 5/36 = 13.9% | 62.5% | VR 手柄 ×5、夹爪 ×1 |
| GDINO tiny | 27/36 | 1/36 = 2.8% | 1/36 = 2.8% | 3.7% | 桌面圆孔 ×10、夹爪/臂 ×8、线缆/模糊 ×7、鼠标 ×1 |
| 问题 | 证据 | |
|---|---|---|
| ① | 精度不行:cup 类会框到非杯子 | OWL-ViT 6/16 不是杯子;GDINO 26/27 不是 |
| ② | 消歧没做:场上第二只真杯子会赢 | OWL-ViT 那 10 个真杯子里,5 个是另一只塑料杯 |
脚本 03-software/scripts/crop_review_boxes.py,
逐框记录 05-training/detect/manual_review.json,
裁图 05-training/detect/runlog/crops_*.jpg。
每个都附了我的建议。只有问题 3 和 4 会挡住下一步,其余可以延后。
安全层第 3 步把头的两个值整个覆盖成启动位置,第 4 步的绝对钳位因此continue跳过了头
(代码注释:already overwritten above)。今天头不动,所以无害。
但观众互动方案要让头跟随人脸。那时第 3 步不能再锁死,头的目标值会真发出去,
而第 4 步仍然跳过 —— 头的指令一路上没有任何绝对上限检查。
原来那条被撤回的注释(「标定不可信」)会让人以为没法加钳位;现在知道能加,数字也有了:
head_motor_1 ±149.98°、head_motor_2 ±50.02°。
HEAD_DIMS
从第 4 步的 continue 里拿出来,用上面两个数。
这件事和数字都已经写进注释和 GOAL,不会丢。你需要做的只是认可这个安排。这个决定在写任何递交代码之前。递交要碰人,而安全层现在对「人在哪」一无所知 ——
z_floor(pitch) / y_floor(pitch) 管的是桌面,没有任何关于人的约束。
Intent-Handover 那篇还指出:夹爪离接收方的手太近,被感知到的安全性会下降。
识别是好的,缺的是消歧。四条路:
| 路线 | 代价 | 风险 | |
|---|---|---|---|
| a | 深度消歧:检测出所有 cup 框 → 查深度 → 取离机械臂最近的 | 中(深度已有,要对齐 RGB) | 深度-RGB 时间对齐没做过 |
| b | 提高 head 相机分辨率(现在 424×240) | 低,但要重录数据 | 策略见过的就是 424×240,换了要重训 |
| c | 语言 prompt 指定("the beige cup") | 最低 | 换个杯子要改 prompt,不是根本解 |
| d | 二维码兜底(memory 里已列为备选) | 低 | 演示时贴码,观感差 |
速度理由已经没了。剩下的唯一理由是「物体 token 当结构先验,帮泛化」。 而 Oat-VLA 的真机成功率用我们自己的 Clopper-Pearson 算是不显著的(29/49 vs 20/49,p = 0.106)。
C 是「盘点不是开发」,所以我只出了决定,没动代码。
真正落地要两步:①在 face_perception.py 加 YuNetFaceDetector 后端
(那文件第 133 行本来就定义了 FaceDetectorBackend 协议,是设计好的扩展点);
②让 face_follow.py 改用 face_perception,删掉那份内联的 Haar 拷贝。
face_follow.py 会撞一个莫名其妙的 AttributeError。
但准确率仍然验不了(没有带人脸的录像),所以我只能保证「能跑起来、耗时已知」,
不能保证「检得比 Haar 准」。要我做的话说一声。试过 OWL-ViT base(225 ms)和 Grounding DINO tiny(2657 ms)。 更上一档是 OWLv2 / GDINO base / SAM,体积和耗时都要再上一个台阶。
你已经说了 detect 那 11 MB 可以提交。清单:
| 类型 | 文件 |
|---|---|
| 新增脚本 | detect_cup_openvocab.py · detect_cup_coco.py · eval_frames.py · bench_smolvla_cameras.py · trial_keyframes.py |
| 改动 | run_policy_trials.py(接关键帧)· policy_safety.py(只改注释) |
| 文档 | 三份 GOAL 回填 · 本页 + 结论页 + index 卡片 |
| 数据 11 MB | 05-training/detect/(36 帧 PNG + 8 组联系表 + records.json + bench_cameras.json) |
upload_server.py、GOAL-TRIALS-VIEW-2026-09-04.md 等文件,
robot_server_dashboard.html 也有改动)。那些不是我的,提交时别一起裹进去。
git 写命令我一条没跑。HANDOFF-REMOTE-2026-09-04.md。
按结论组织的版本在这里。
原始数据 05-training/detect/*/records.json,
联系表 05-training/detect/*/sheet_*.jpg,计时 05-training/detect/bench_cameras.json。