体感控制:把 33 个关节翻译成两条速度指令
上一页拿到了 33 个关键点,可机器人听不懂坐标 —— 它只认 cmd_vel 里的两个数:前进速度和转向角速度。这一页做翻译:手举过肩 → 左转,双手举高 → 前进,蹲下 → 后退,都不是 → 停。有意思的是,判定成立时车默认不会动,这是刻意的。
01目标:抬左手就左转,松手就停
- 说清五段链路:像素 → 归一化 → 可见度校验 → 三档比较 → 速度指令,每段各由一个函数负责
- 跑通单图模式,看到状态栏
Action: Turn Left与终端action: turn_left (Turn Left),并解释为什么车没动 - 调出五档动作:改
SQUAT_HIP_Y与HAND_UP_DELTA,让同一姿势判出不同结果,并说清优先级
publish_cmd_vel: false,判定成立也不会真的发速度。这是刻意的安全约定:先用海龟仿真验证方向,确认规则对了再谈接底盘。课程不鼓励把它改成 true 在实车上试。
难点:直觉是「左手举高就是左转」,实际是先判优先级 —— 蹲下时无论手怎么举都算后退;而「双手举高」还要求两只手都比对应肩膀高出 0.08,只举一只手不算前进。
来自 EXP-09 的骨架;单图用 images/pose_demo.jpg,实时走相机话题。
一个动作名 + 一组目标速度(默认不发)。五个动作:Stop / Forward / Backward / Turn Left / Turn Right。
02问题:从「你抬左手」到「车左转」,中间有五段
隔空开车听着简单:抬手、车转。可从「画面里一只手举起来了」到「底盘收到角速度」,中间要过五道手:
| 对比 | 你看到的(黑盒) | 机器必须做的(白盒) |
|---|---|---|
| 抬手为什么车会转 | 手一抬,车就动 | ? —— 得先把「举手」这件事变成一次坐标比较 |
| 为什么蹲下时不判手 | 你根本没想过这个情况 | ? —— 三条规则会同时成立,必须定优先级 |
| 判出来了为什么不动 | 以为程序坏了 | ? —— 有一个默认关闭的开关,叫 publish_cmd_vel |
三个问号拆成三个具体问题:
① 33 个点,凭什么变成「举手」这一个结论?
② 蹲下时也举着手,算前进还是后退?
③ 明明判出 Turn Left,为什么车一动不动?
03现象:同一张图跑出 Action: Turn Left,可车一动不动
你会看到:一张双栏图,右栏火柴人举着右手,底部状态栏写 FPS: 0 Action: Turn Left;终端同时打印 [pose_control] action: turn_left (Turn Left)。而底盘没有任何动静 —— 因为发布开关默认关着。
images/pose_demo.jpg:照片里举起的是他的右手怎么跑出来:进项目根,加 PYTHONPATH,用模块方式跑单图。
cd ~/ros2_ws/src/xflz_eai_car1/vision/11_pose_control export PYTHONPATH=$PWD/car1_pose_control python -m pose_control_core.demo_image --config config/pose_control_config.yaml --save out.jpg
🔍 看什么:三件事,缺一不可。
- 动作名是英文的 —— 状态栏写
Turn Left而不是「左转」。因为cv2.putText不带中文字形,写中文会变成一串问号。这是工程上真实存在的小坑。 - 「他的右手」被判成
Turn Left—— 站在人的视角,右手举起是「向右」;但机器看的是画面坐标:这只手在画面左半边,所以算左。左右的定义要先说清,否则整条规则都反。 - 没有速度输出 —— 状态栏只有动作名,没有任何 m/s。这就是第 ⑤ 段被关掉的样子。
这就是本实验最终要呈现的现象。第 05 节会让你亲手改阈值,把同一个姿势判成不同结果 —— 那才算真懂规则。
04原理:三关:归一化、比大小、定优先级
把像素坐标除以画面宽高变成 0~1 的比例,然后只做三件比较:髋够不够低、腕够不够高、两只手是不是都高 —— 最后按优先级挑出唯一一个动作。
打个比方:像给动作定规矩的裁判 —— 先看「蹲下了吗」(最优先),再看「双手都举起来了吗」,最后才看「举的是哪只手」。 揭个盖:现实里这些状态会同时成立(蹲下时手也可能举着),所以规则必须写成一条有序的判断链,而不是三条并列的 if。顺序一换,结果就变。
三步回答第 02 节的三问:① 点怎么变结论 → 步骤 2;② 蹲下又举手算谁 → 步骤 3 的优先级;③ 为什么车不动 → 步骤 4。
vis 不够就直接 Stop —— 髋部看不见时,整套姿态都不可信。1为什么第一件事是除以 640 和 480?
吃什么:33×4 的像素关键点 + 帧尺寸。做什么:把 x 除以宽、y 除以高,得到 0~1 的归一化坐标。吐什么:同一套关键点,但「大小无关」了。
因为阈值必须和分辨率解耦。hip.y > 0.72 这句话的意思是「髋部到了画面下方 28% 的位置」;如果直接写像素,换 1280×720 的相机,同一个 345 就完全不是那个意思了。
动它会怎样:这一步没有旋钮,但漏了它,后面所有阈值都只对当前分辨率有效 —— 这是「本地能跑、换设备就废」的典型成因。
2三条比较,各自在比什么?
全部用归一化后的 y(越小越靠上),只比大小,不做任何形状判断:
- 下蹲:两个髋关节里较低的那个,y 是否超过 0.72。用
max是因为侧身时两点高度不同,取更低的那个更保守。 - 举手:手腕 y 是否比同侧肩膀的 y 小 0.08 以上。减 0.08 是为了避开「手自然下垂时略微抬高」的抖动区,不然肩膀一动就误判举手。
- 眼睛不看:注意这套规则完全没用到脸或手型 —— 33 点里它只用 6 个(髋 2、肩 2、腕 2)。
动它会怎样:SQUAT_HIP_Y 调小(0.6)→ 稍微屈膝就判后退;HAND_UP_DELTA 调大(0.2)→ 手要举得更高才算数。两个阈值决定「这套动作好不好做」。
3同时满足好几条,听谁的?
这是第 02 节第二问的答案,也是本页最容易写错的地方:顺序就是语义。
if 髋不够高 believe → return BACKWARD if 左手 且 右手 都高 → return FORWARD if 只有左手高 → return TURN_LEFT if 只有右手高 → return TURN_RIGHT return STOP
源码里的写法与这五步完全一致(判断链 + 提前 return)。所以:
- 蹲下 + 双手举高 →
Backward(第一条就命中了)。 - 双手都高 →
Forward;只举一只手 → 转向,不是前进。 - 髋部 vis < 0.5 → 直接
Stop,后面几条都不看。
动它会怎样:把优先级换个顺序(比如先判单手),蹲下时就会变成转圈 —— 因为蹲下的人往往手也抬着。规则顺序是设计出来的,不是随手写的。
4判出来了,为什么车不动?
因为「判定」和「执行」是两个开关。配置里 publish_cmd_vel: false 是默认值,action 会照常输出到状态栏与终端,但不会发布速度话题。
这是有意的安全设计:调规则时人常常做出各种奇怪姿势,如果判定错就真发速度,车会突然冲出去。先在仿真里验方向、再谈接底盘,是这门课反复强调的顺序。
动它会怎样:改成 true 并接上底盘,一次误判就是一次真实位移 —— 这也是为什么页面不给你「一键开启」的按钮。
Turn Left,因为机器看的是画面哪半边;而车不动,是因为第 ⑤ 段被关着 —— 两件事都不是错误。先认识 5 个词
像素除以宽高得到的 0~1 比例。什么时候失效:画面被裁切/缩放后没同步更新比例,阈值就错位。
多条规则有序判断、先命中先返回。什么时候失效:顺序写错,蹲下+举手会判成转圈。
HAND_UP_DELTA手腕要比肩膀高出多少才算举手,默认 0.08。什么时候失效:设太小,肩膀抖动就误判;设太大,手要举很高才认。
SQUAT_HIP_Y髋部低于画面哪个位置算下蹲,默认 0.72。什么时候失效:相机机位太低或太高时,站着的髋部就已经接近 0.72。
cmd_velROS 的速度话题,通常两个数:线速度与角速度。什么时候失效:发出去前不做限幅,一次误判就是一次猛冲。
什么时候会翻车:五个真实边界选读 1 分钟
- 相机机位不对 —— 相机放得低,站着的髋部 y 就已经接近 0.72,一直判后退。机位高度是这条规则的隐含前提。
- 左右手搞反 —— 镜像画面(自拍视角)会把左右翻过来,转向全反。先确认画面是否被镜像。
- 只露上半身 —— 髋部不可见 →
vis不够 → 永远Stop。这是「怎么都不动」的最常见原因。 - 手举得不够高 —— 离 0.08 只差一点点,判定在举手与不举手之间抖动,动作来回跳。
- 两人入镜 —— 姿态只给一个人(分最高),旁边的助手会被静默忽略,也可能抢走判定。
| 步 | 数据变成什么 | 为什么非它不可 / 少了会怎样 | 谁在干 | 动参数会怎样 |
|---|---|---|---|---|
| 1 | 像素 → 归一化 (33,4) | 不归一化,阈值绑死分辨率 | _norm | 无参数 |
| 2 | 点 → 三条布尔判断 + vis 校验 | 不比大小就无法把姿态变成结论 | classify_action | 0.72 / 0.08 / 0.5 三个阈值 |
| 3 | 布尔 → 唯一动作名 | 不定优先级,冲突时结果随机 | 判断链 | 换个顺序,蹲下就变转圈 |
| 4 | 动作名 → 速度(默认不发) | 判定与执行分离是安全底线 | publish_cmd_vel | 改 true 就真的会动 |
只看第 2、5 两列:第 2 列是数据形态,第 5 列是你能拧的旋钮。第 4 行请谨慎对待 —— 那是唯一会动到物理世界的旋钮。
05实操:单图看判定 / 海龟验方向 / 改规则
命令与原理的对应关系。
| 环节 | 函数(文件) | 吃哪个参数 | 对应原理第几步 |
|---|---|---|---|
| 关键点 | 姿态引擎(engine.py,复用 EXP-09 模型) | min_detection_confidence | 链上游 |
| 归一化 | _norm(actions.py:43) | 帧尺寸 | 步骤 1 |
| 三档比较 | classify_action(actions.py:51) | SQUAT_HIP_Y HAND_UP_DELTA MIN_VIS | 步骤 2、3 |
| 状态栏 | view.put_status(view.py:29) | (无) | 步骤 4 的可视化 |
| 速度发布 | 节点内 publish_cmd_vel 分支 | publish_cmd_vel | 步骤 4 |
-
单图看判定,先信「判定」这一层约 3 分钟
命令见第 03 节。判据两条:状态栏的动作名,和终端那一行。
[pose_control] action: turn_left (Turn Left) 状态栏:FPS: 0 Action: Turn Left再换一张举手方向相反的照片(或拍一张自己),确认判出来是
Turn Right—— 左右搞反是这条链最常见的坑。 -
用海龟验方向,别用车约 6 分钟
打开
publish_cmd_vel之前,先用 turtlesim 确认「左转」在速度里到底是哪个符号:source /opt/ros/humble/setup.bash ros2 run turtlesim turtlesim_node
弹出海龟窗口(板上桌面或 noVNC)ros2 topic pub --once /turtle1/cmd_vel geometry_msgs/msg/Twist "{linear: {x: 0.0}, angular: {z: 0.6}}"
海龟原地逆时针转 —— 正 z 是左转这一步把「动作名 → 速度符号」的映射钉死。接底盘前必须先过这一关。
-
改一个阈值,看判定怎么切约 8 分钟
在
actions.py里把SQUAT_HIP_Y从 0.72 改成 0.95 后重跑单图:站姿的髋部永远到不了 0.95,于是「下蹲」这一档被彻底关掉。再把
HAND_UP_DELTA从 0.08 改成 0.30 —— 同一张照片里的手举得不够高,动作会从Turn Left掉回Stop。这两次改动让你确认:阈值就是这套系统的「性格」。
Action: Turn Left + 终端 action: turn_left + 底盘不动。而且你现在能说出「为什么是他的右手判成左转」「为什么不发速度」。两处对上了,才叫走通。你该看到的画面
单手举 → 左/右转;双手举 → 前进;蹲下 → 后退;站直 → 停。
只露上半身时 vis 不够,第一条校验就返回 Stop。
手举得刚好在 0.08 边界,判定抖动。可加几帧平滑。
这是默认值,不是故障。安全设计,别急着改。
踩坑速查
| 现象 | 原因 | 怎么办 |
|---|---|---|
| 一直判 Stop | 髋部不可见 / 只露上半身 / 机位太低 | 让全身入镜;确认相机高度 |
| 左右转向反了 | 画面被镜像,或按「人的左右」理解成「画面左右」 | 确认无镜像;规则用的是画面坐标 |
| 蹲下时判成转圈 | 判断链顺序被改过 | 恢复顺序:下蹲 → 双手 → 单手 |
| 动作名显示成问号 | 用中文写进了 label | 保持 ASCII(cv2.putText 无中文字形) |
| 判定对但底盘不动 | publish_cmd_vel 默认 false | 先用 turtlesim 验方向,再谨慎考虑开启 |
报 ModuleNotFoundError: pose_control_core | 没加 PYTHONPATH | export PYTHONPATH=$PWD/car1_pose_control |
| 换分辨率后判定全乱 | 阈值绑了像素而不是归一化值 | 确认 _norm 生效,别绕过它 |
进阶 1 · 加一个「双手放下 → 停车」的急停姿势约 20 分钟
现有五档里 Stop 是「都不是」的兜底,不是主动姿势。可以把「双手举起交叉」定义成显式急停,判定优先级排第一 —— 任何时候做出这个姿势都立刻停。
这也是安全设计的一般做法:兜底不等于急停,急停要有独立的、优先级最高的入口。
进阶 2 · 给动作加几帧确认约 25 分钟
现在每帧独立判定,手在阈值附近会来回跳。加一个长度为 5 的滑动窗口,同一动作连续出现 3 帧才切换,抖动立刻消失。
代价是响应变慢几帧(约 0.2 秒)。这就是 EXP-07 里 n_init 的同一个思想:确认次数换稳定性。
🧪 Vibe Coding · 自己把这段代码读明白约 20 分钟
你要产出三样:模块职责表 → 控制流流程图 → 时序图。
提示词 1 · 模块职责分析
请阅读这个体感控制项目的模块:pose_control_core/actions.py、engine.py、pose.py、view.py、demo_image.py、pose_control_node.py。 它们共同完成「姿态关键点 → 动作名 → 速度指令」。 请输出一张表,每行一个模块,四列:模块名 / 一句话职责 / 被谁调用 / 从哪个配置项取值。 关键函数标行号;不确定处请明说。
提示词 2 · 控制流流程图
请为 actions.classify_action 画一张流程图。 必须体现:像素如何归一化;髋部可见度不足时如何提前返回 Stop;三条判断的先后顺序(下蹲 / 双手 / 单手)以及为什么这个顺序重要。 每个判断框写出完整的比较表达式。
提示词 3 · 时序图
请为节点模式画一张时序图,参与者:相机话题、姿态引擎、classify_action、状态栏绘制、cmd_vel 发布(受 publish_cmd_vel 控制)、turtlesim 或底盘。 标出判定结果的发布频率,以及 publish_cmd_vel=false 时链路上哪一段被断开。
自查三问:能指出三个阈值在代码里的位置?流程图的判断顺序与源码一致?时序图里画出了那个「开关」的位置?
三处必然的差异:
| 差异点 | AI 版常见写法 | 工程版(本仓) | 为什么工程这么写 |
|---|---|---|---|
| 多个条件同时成立 | 三个独立 if,最后覆盖或并列输出 | 有序判断链 + 提前 return(actions.py:51-79) | 蹲下时手往往也举着;不排序就会来回跳或有歧义 |
| 可见度处理 | 只判坐标,不判 vis | 髋部 vis < 0.5 直接 Stop(actions.py:58-60) | 点位是猜的还发速度,等于拿噪声开车 |
| 显示文字 | 直接写中文动作名 | ASCII label(Turn Left) | cv2.putText 无 CJK 字形,中文会变问号 |
改一处、先预测再验证:把 HAND_UP_DELTA 改成 0.25,先写下手要举多高才判转,再跑一次对答案。
06互动:拖三个高度,看五档动作怎么切
📌 这一节是「掌握检查」:亲手改一个参数,看现象是否朝你预期的方向变,还要能说出为什么 —— 说得出,才算掌握。
规则只有三条、优先级四层,但「谁先命中」不看一遍永远记不住。这里把三个高度交给你:左腕、右腕、髋部。判定完全照 classify_action 的写法重算。
HAND_UP_DELTA = 0.08(手腕要比肩小 0.08 才算举)、SQUAT_HIP_Y = 0.72。判定顺序照源码:下蹲 → 双手 → 单手 → 停,先命中先返回。怎么读出规律:先把髋拉到 0.9(蹲下)—— 无论手怎么放都判 Backward,这就是优先级第一的威力;再把髋拉回 0.6、两只手都拉到 0.3 —— 变 Forward;只抬起一只 —— 变成对应方向的转向。
蹲下最优先,手举得再高也走不出去。
两只腕都低于肩-0.08,判双手举高。
第 03 节那张图就是这一档(画面左侧的手)。
低于肩线,一档都不命中,走兜底。
07验证:三道小测
回页首对照「实验目标」:① 五段链路能否背出来 → 试 Q1;② 单图跑通、动作名与终端对上、能否解释「车不动」→ 试 Q3;③ 两个阈值改过没有 → 试 Q2,并回第 06 节把髋拉到 0.9 看优先级。三条全 ✓ 就完成。
squat > both_up > left/right。写成三个并列的 if 就会出现「后写的覆盖先写的」这种玄学行为。顺序是设计出来的,改顺序等于改语义。Action: Turn Left,底盘却不动。最可能的原因是?publish_cmd_vel 默认是 false —— 判定照常输出,但不会真的发布速度08章节总结:把 33 个关节翻成两条速度数
这页讲了什么,它们怎么串起来
| 概念 | 它解决了什么 | 和别的关系 |
|---|---|---|
| 归一化 | 让阈值与分辨率解耦 | 上游来自 33 点像素坐标,下游所有比较都用它 |
| 三条比较 | 把姿态变成三个布尔值 | 只用 6 个关节点;两个阈值(0.72 / 0.08)决定灵敏度 |
| 可见度校验 | 点位不可信时不动作 | 髋 vis < 0.5 直接 Stop,是链条最前面的闸 |
| 优先级链 | 多条规则同时成立时给出唯一结果 | 顺序即语义:蹲下 > 双手 > 单手 > 停 |
动作名 label | 给状态栏一个可显示的名字 | 必须 ASCII,因为 cv2.putText 无中文字形 |
cmd_vel | 把动作变成线速度与角速度 | 默认不发(publish_cmd_vel: false),安全底线 |
| turtlesim | 在没有真车的情况下验证方向符号 | 接底盘前的必经一步 |
② 直觉是三条规则并列、谁先写谁生效,实际是有序判断链,蹲下会压过一切;
③ 直觉是判出动作就该动,实际是后面还有一个默认关闭的发布开关 —— 判定与执行必须解耦。
HAND_UP_DELTA = 0.08 不是「举手高度」,而是手腕比同侧肩膀高出多少;肩膀位置由姿态本身提供,所以人离镜头远近不影响这个判断;②
SQUAT_HIP_Y = 0.72 隐含了相机高度:机位太低时站着的髋部就接近 0.72,会一直判后退;③ 状态栏里是
Turn Left 不是「左转」:cv2.putText 没有中文字形,写中文会变问号;④
publish_cmd_vel: false 是默认值,不鼓励改;一定要开,先在 turtlesim 里把方向验完。
前置依赖 · 缺一样就跑不起来
| 类型 | 具体项 | 缺了会怎样 / 怎么补 |
|---|---|---|
| 环境 | 板上 Python 在 ~/ros2_venv;实时/海龟验证需 ROS 2 Humble 两级 source | 未激活 venv → python: command not found;未 source ROS → ImportError: librcl_action.so |
| Python 库 | numpy、opencv-python、pyyaml、rknnlite、mjpeg_streamer、turtlesim(验方向用) | 缺 turtlesim 只是没法验方向,单图判定不受影响 |
| 模型 / 资源 | models/pose_landmark_full.rknn(与 EXP-09 同一个)+ images/pose_demo.jpg | 缺模型没有任何输出;示例图太小会让点位抖动,判定跟着跳 |
| 配置 | config/pose_control_config.yaml:source.mode: image、server.port: 8083、publish_cmd_vel: false | mode: image 未填 rgb_path 会报错;误改 publish_cmd_vel 需自负后果 |
| 运行方式 | 项目根 + PYTHONPATH 含 car1_pose_control,用 -m 运行 | 直接跑 .py 会因相对导入失败 |
这套代码用在哪儿
- 隔空操控:展厅里的机械臂、AGV 的粗略遥控,人不必接触设备。
- 安全动作门:车间里「蹲下」= 进入检修姿态,自动降速或停机 —— 这类规则简单可靠,比语义模型更适合安全回路。
- 康复训练打分:把「蹲到位没有」变成一条百分比曲线,做重复次数与幅度统计。
- 不该用它的场合:需要精细指令(选菜单、调速度档位)时,五个离散动作远远不够 —— 那是 EXP-12 手势识别的地盘。
最后一个实验把镜头收回到手上:33 点判大动作够用,但「比个耶」「竖大拇指」这种精细语言,需要 21 个手部点 + 一套四模型级联。