调酒演示里有一个动作:旋转杯身让观众看清酒的状态,然后朝观众推出。 这一条看起来只是「再加一个动作」,实际上它引入了第二个主体——人——并因此要求两件重新设计: 头部控制权归谁,以及安全层怎么理解「人」。这份 GOAL 说明为什么它不能塞进前两份里顺手做。
face_follow.py(2026-08-14)的设计目标逐条对着这个需求写的。
坏消息是它和策略试跑抢同一条串口总线。操作者提出的需求(2026-09-03 夜):
前两份 GOAL 处理的都是机器人和物体的关系。这一条引入了第二个主体:人。
origin/main 和 china-console 都有,本地也有。
作者 ginagina19992023,2026-08-14。
| 文件 | 行数 | 做什么 |
|---|---|---|
face_perception.py | 265 | 感知适配器 → 统一的 FaceObservation |
face_follow.py | 330 | 头部跟随一张主脸(pan/tilt 直驱舵机) |
face_display.py | 168 | ESP32-S3 + OLED 表情脸,硬件无关 |
test_face_emotion.py | 113 | 相机 → 人脸/表情 冒烟测试 |
DEMONSTRATION-INTERFACES.md | 325 | 示范接口决策文档(见 §1.3) |
face_follow.py 的设计目标,是照着「面向观众」写的(文件头原文):
vr_teleop.py or another process.「只跟一张主脸不来回跳」「死区 + 每帧硬上限」「跟丢了缓慢回中」—— 这正是面向观众时头部控制该有的性质,不是随手写的。实测到的关键常量:
| 项 | 值 | 出处 |
|---|---|---|
| 头部舵机 ID | pan=7, tilt=8,在左臂总线上 | face_follow.py:49-50 |
| pan / tilt 行程 | 342–3754 / 1479–2617 | :58-59 |
| 中位 / 力矩上限 | 2048 / 200 | :60-61 |
| 死区 | X 0.10,Y 0.12(归一化画面坐标) | :72-73 |
| 主脸选择 | choose_primary(faces, previous),粘性 | :205 |
| 跟丢后 | recenter_tick() 缓慢回中;退出 release() 松力矩 | :179, :196 |
检测器用 OpenCV 自带 Haar 级联 —— 不用下载模型。文件头明说后面可以换成 MediaPipe / YOLO-face 而不改头部控制器。这个分层是对的,新工作应该沿用,不要另起。
face_perception.py 做的是检测 + 表情分类(可选 HSEmotionONNX),
不做身份识别 —— 「认出是哪位客人」目前没有。conversational_ai/vision/face.py 里那个 YuNet
是另一套(2026-09-03 提交,服务对话系统)。仓库里现在有两套并行的人脸检测,
一套 Haar(scripts/)、一套 YuNet(conversational_ai/)。
做这份 GOAL 时要先决定留哪套,别再加第三套。DEMONSTRATION-INTERFACES.md(2026-08-16)里已经有这一段:
GRASP / PLACE / POUR → Leader-heavy demonstrationsSHAKE / PRESENT → VR-heavy demonstrations也就是说「呈递展示」这个技能,三周前就被判给了 VR 示范。 这份 GOAL 不需要重新论证示范接口的选择,但要注意同一份文档 §4 的成本警告: A/B/C 消融是好论文,演示才是截止日期。
现在三个地方各自直接控制头部:
| 谁 | 怎么控 | 出处 |
|---|---|---|
face_follow.py | 自己开左臂总线,直写舵机 7/8 | face_follow.py:49 |
vr_extras.py | 同样直写舵机 7/8 | vr_extras.py:42 |
| 策略试跑 | 每帧把头覆盖成启动时的位置 | policy_safety.py:367 |
第三条是关键:policy_safety.py 的第 3 步是
mask head → head_motor_1/2 := locked pose, always, silently。
所以策略运行期间,机器人物理上不可能转头看观众。
bus_guard.py 存在的原因就是这个 —— 它记录的事故是 2026-08-17 23:16:
一个手敲的 trace_teleop.py 在录制进行时启动,录制以
[TxRxResult] Port is in use! 死掉,
掉了一集,并且双臂砸在桌上。操作者 2026-09-04 的判断:
这不是为了省事,是被两个硬约束逼出来的唯一答案。
量了 xlerobot-team/xlerobot-cup-grasp-20260820-0230 全部
50 集 / 19453 帧:
12 head_1 σ=0.0000 范围 [-4.308, -4.308]
13 head_2 σ=0.0000 范围 [49.846, 49.846]
两个自由度都是常数。策略从来没见过「头在动」的画面。 抓取/倾倒阶段让头跟着人转,头部相机输入就跑出训练分布了。
相机装在会动的头上,外参是「头在某个姿态时」的值 —— 头一动,标定全废。 两个约束指向同一个结论:操作阶段头必须不动。
| 阶段 | 头部目标来源 | 相当于 |
|---|---|---|
| 接近 / 抓取 / 倾倒 / 搅拌 | 锁定常数(启动时读到的位置) | 就是现在的行为,不变 |
| 展示 / 推杯 | 人脸驱动 | 新增 |
代价:要改 policy_safety.py 第 3 步的头部掩码 ——
从「always, silently 锁死」改成「按阶段决定锁死还是跟随」。那是安全层,见 §2.2。
policy_safety.py:239 的 HEAD_LOCK_NOTE 写着:
xlerobot-cup-grasp-20260820-0230 carries
head_motor_2 = 99.649 … 99.649 * 4095/360 + (1479+2617)/2 = 3181 counts …
against a calibrated range_max of 2617 — 564 counts, about 50 degrees, past the end
of the calibrated span.实测(2026-09-04):19453 帧全部是 head_motor_2 = 49.846,不是 99.649。
用它自己的公式重算:
| head_2 | counts | 对 range_max = 2617 |
|---|---|---|
| 99.649(注释写的) | 3181.5 | 超出 +564.5 |
| 49.846(实测) | 2615.0 | 在范围内,差 2 counts |
HEAD_DIMS(原文:step 4 skips HEAD_DIMS)。
前提不成立,这个跳过就失去了依据。face_follow.py 自己的
HEAD_PAN_LIMITS / HEAD_TILT_LIMITS —— 那是另一条代码路径,
按 §2.1 合并进策略进程之后,那层保护就不在路径上了。2615 counts 距 range_max 2617 只差 2 counts。
也就是说 50 集录制期间,头的 tilt 一直贴在标定行程的边缘上。
grep -in "human|person|旁人|观众" policy_safety.py 只有 3 处命中,
没有一处是「感知到人」:
:49 步长上限来自「人类曾经指令过的最大单帧变化」:89 SAFETY.md 里「有人守着急停按钮」:281 2026-08-16 打到旁人那次的事后注释所以当前的人身安全 = 固定几何包络 + 有人按急停。
而「朝观众推出」是故意朝人伸手 —— 它要求手臂穿过现在这套包络想拦住的方向。
z_floor(pitch) / y_floor(pitch) 那套「离机身多远就拒帧」的思路,
在「把杯子送到人手边」这个目标下不是参数不对,是前提不对。
需要的是另一类东西:知道人在哪、离多近、速度上限随距离收紧。
z_floor(pitch) / y_floor(pitch) 是 2026-09-03 刚接好的,
回归 40 集拒帧率 96.6% → 0.49%。深度整合 GOAL 里那三条路, 原来是按当前代价排的。观众互动加进来之后,应该多一个评分项:能不能带着走。
| 迁移到「多物体 + 有人在场」 | 为什么 | |
|---|---|---|
| B0(物体 token 当先验) | 最好 | 不编码任何任务专属量,只按物体重组视觉输入。场景变成「瓶→杯→人」三实体加关系时,正是它的主场 |
| B2(深度注入) | 中 | 几何对「杯口在哪、离人多远」有用,但玻璃高脚杯 + 液体是深度相机最差场景 |
| B1(算 3D 坐标喂流程) | 最差 | 要手写的东西从「杯子在哪」膨胀成「杯子在哪 + 人在哪 + 杯口朝向 + 推到哪停」,每加一个动作再加一批 |
"a cup" 和 "a person",
而手写几何规则根本做不出「人」 —— 你没法用「减桌面找凸起」找出观众。
这不是效果差异,是能力有无。这一步的产物是一份清单,不是代码。 三周前已经有一套 HRI 分层了, 再写一套是重复劳动 —— 只不过这次的「上游」是自己三周前的提交。
| 内容 | |
|---|---|
| 0.1 | 通读四个文件 + DEMONSTRATION-INTERFACES.md,出「已有 / 缺 / 冲突」清单 |
| 0.2 | 决定留哪套人脸检测:Haar 还是 YuNet。两套并存迟早出事 |
| 0.3 | |
| 0.4 | 查文献:人机递交(human-robot handover)是有成熟研究的方向,我们没查过。按规矩先查再定方案 |
| 0.5 | 现场一分钟:确认 head tilt 的 range_max 方向是抬头还是低头,从录制姿态还剩多少余量(见 §2.3) |
验收:0.3 的安全层问题有明确处置;0.4 至少三篇带链接的出处;0.5 有现场确认的答复。
独立于现有安全层设计,不改它。
| 内容 | |
|---|---|
| 1.1 | 定义「人在场」的状态量:方位、距离、置信度、丢失多久算没人 |
| 1.2 | 定义速度/距离约束:离人越近上限越低。要给出拒绝条件,不只是钳位 |
| 1.3 | 拿已录的 30 集 + 人脸检测离线回放,看这套约束会拒掉多少帧 |
| 1.4 | ★ 人工看图核对 ★ —— 和深度那份同一条纪律,数字好看不算数 |
验收(事先写死):在有人入画的回放片段上,人的方位判对 ≥90%(人工核对 ≥30 帧); 约束在无人时的拒帧率 <1%(不能因为加了这层就把正常抓取搞挂)。
前置:阶段 0.3 和阶段 1 通过。
MEASURED_STEP_CAP_DEG 是从抓取数据量出来的
(policy_safety.py:147 注释写明来源是 xlerobot-cup-grasp-20260820-0230),
wrist_roll 上限 10.4°/帧,而且是静默钳位不是拒绝
(policy_safety.py:55:a too-large step just gets slowed)。| 有 | 没有 |
|---|---|
| 三路 RGB:头 240×424 + 左右腕各 480×640 | 力/力矩传感器 |
| 头部深度(Orbbec) | 身份识别 |
17 维关节位置(observation.state,纯位置) | 电流/负载反馈进入观测 |
observation.state 是 [17] 纯关节位置 —— 实测自
xlerobot-team/xlerobot-cup-grasp-20260820-0230 的 info.json。
递交动作(把杯子交到人手上、感知对方接手了没有)在没有力觉的情况下只能靠视觉判断,
这是个硬约束,方案设计时不能假装它不存在。
face_follow.py 的设计目标逐条对着这个需求),
坏消息是它和策略试跑抢同一条串口总线,现在不可能同时跑。