骨骼关键点 33 点:把人压成一根能算的火柴人
上一页把脸拆成 468 个点,这一页把整个人拆成 33 个点。它的意义不在「画得好看」,而在于:判断「他在举手」这件事,从此变成比较两个 y 坐标的大小——不需要认出是谁、不需要看清衣服、也不需要语义模型。
01目标:33 点 35 线,一根火柴人跟着人动
- 说清关键点为什么比「检测框」更有用:框回答「人在哪」,点回答「他手举到哪了」,而动作判定只需要比坐标
- 跑通单图与实时两条命令,看到左栏叠骨架、右栏黑底火柴人,状态栏打
Pose: OK - 调出骨架的两种坏相:可见度门限抬高 → 手脚断线;人站太远 → 整帧不画
vis 筛、按 35 条边连线。难点:直觉是「33 点就是模型的 33 个输出」,实际是一个 195 维向量里塞了 39 个槽位(33 姿态点 + 6 个附加点),每槽 5 个数(x, y, z, vis, presence)—— 取错了会整体错位。
一张全身照(举手姿势)。也可以换 --ros 的相机画面。
双栏图:左 = 原图叠骨架,右 = 黑底红点绿线火柴人,状态栏 Pose: OK。
02问题:判断「举手」不需要看清这个人是谁
隔空控制有一类做法很朴素:手举过肩就算「举手」。它不需要知道你是谁、穿什么、脸朝哪,只要知道手腕和肩膀哪个更高。
这件事用人脸、用检测框都做不到:框只能告诉你「人在这个矩形里」,举手与放下时框几乎一样大。**要把人变成一组可比较的数字**,才谈得上判定动作。
| 对比 | 平时你看到的(黑盒) | 机器必须做的(白盒) |
|---|---|---|
| 他手举起来没有 | 一眼就看出来 | ? —— 得先知道手腕和肩膀各在哪 |
| 挡住腿还画得出吗 | 看不出来,正常照片没有这个视角 | ? —— 每个点自带「可见度」,低的点不画 |
| 人站远了 | 你照样看得见 | ? —— 人占的像素太少,整帧可能都不给点 |
三个问号拆成三个具体问题:
① 33 个点是模型直接输出的吗?
② 拿到点之后,怎么变成「举手 / 下蹲」这样的结论?
③ 为什么有人画得全、有人只有半截骨架?
03现象:左栏叠骨架,右栏黑底火柴人
你会看到:一张双栏图。左栏把人像叠上骨架(看得清「点对不对得上人」),右栏把背景压黑只留火柴人(看得清「结构对不对」)。底部 FPS: 0 Pose: OK。画面里的人举着右手 —— 右栏那根线就跟着扬起来。
images/pose_demo.jpg
怎么跑出来:进项目根,把包目录加进 PYTHONPATH,用模块方式跑单图。
cd ~/ros2_ws/src/xflz_eai_car1/vision/10_pose_landmark export PYTHONPATH=$PWD/car1_pose_landmark python -m pose_landmark_core.demo_image --config config/pose_config.yaml --save out.jpg
🔍 看什么:三处,后面都要用。
- 点落在关节上,不在轮廓上 —— 肩、肘、腕、髋、膝、踝,这些是转动副,正是判动作需要的位置。
- 线只有 35 条 —— 33 个点两两连起来有 528 对,实际只连「骨架该有的那段」,官方拓扑就是 35 条边。
- 右栏背景是黑的 —— 只留点和线,方便看结构;这是绘制层的处理,不是模型输出的「黑背景」。
这就是本实验最终要呈现的现象。先照做;第 05 节补上实时相机与「站远 / 半身」两种边界。
04原理:四步:缩到 256、一次吐 33 点、按可见度筛、连成 35 条线
把人缩到 256 见方喂进去,模型一次吐出 33 组坐标;每个点自带一个「我可见吗」的分数,低的就不画;最后按官方拓扑的 35 条边把留下的点连起来。
打个比方:像给木偶拍照后量关节 —— 只在照片上标出关节位置,再按「哪些关节之间本来就有骨头」连线。 揭个盖:木偶缺了个零件(被挡住),你不会硬画一根骨头过去 —— 模型也一样:两端都够可信才连线,这就是骨架「断」而不是「歪」的原因。
四步回答第 02 节的三问:① 33 点怎么来 → 步骤 2;② 怎么变成动作结论 → 步骤 3 与 4;③ 为什么有人只有半截骨架 → 步骤 3 的 vis。
1为什么整帧缩到 256 就够,不用裁?
吃什么:整帧图。做什么:等比缩放并补黑边到 256×256,同时记住缩放比与补边量。吐什么:一张方形输入 + 一组反算参数。
人脸要裁,是因为脸太小;人体本来就占画面一大块,整帧缩到 256 之后人还占几十到上百像素,够模型看清关节。所以省掉了 ROI 那一步 —— 代价是「离得远的小人」会变得看不清(第 02 节第三问)。
动它会怎样:没有可调参数,但 resize_pad 记下的 scale/pad 必须一路传到步骤 3 反算,漏了坐标就整体偏。
233 个点是怎么塞进一个向量的?
这是本页最反直觉的一点,也是「为什么取错了会整体错位」的答案:
- 模型输出第一路是 195 个数,不是 33×4=132。
- 它其实是 39 个槽位 × 5 个数:
x, y, z, vis, presence交错排列。 - 这 39 个槽位里,前 33 个才是姿态点,后 6 个是模型内部的附加点(不做可视化)。
- 所以解码时要按「每 5 个取一组、只取前 33 组」来切 —— 直接
reshape(33, 4)会整体错位。
另外还有一路 117 维的三维世界坐标(39×3),本实验不用它:做机器人控制只需要「画面里的位置」,不需要真实世界尺度。
动它会怎样:没有旋钮。但换模型版本时形状会变,解码必须跟着改 —— 源码注释里就专门写了这条。
3它凭什么知道哪个点「看不见」?
每个槽位自带 vis(可见度)和 presence,都是 logit,要过 sigmoid 才是 0~1 的分数。它们管两件不同的事:
vis:这个关节看得见吗。人站在桌子后面、腿被挡住时,膝、踝的vis会明显偏低。绘制时低于门限的点就不画。presence:整帧里到底有没有人。它和检测分数一样,用min_detection_confidence(默认 0.5)卡门限;不过门限就整帧不给点。
这就是第 02 节第三问的答案:骨架「断」不是 bug,是 vis 在告诉你「这段我猜的,别用」。
动它会怎样:绘制门限抬高 → 骨架越来越残缺;降低 → 会出现「凭空的线」(把两个都不确定的点连起来),看着像乱飞。
433 个点怎么变成「举手 / 下蹲」?
这是第 02 节第二问的答案,也是本页最实用的收获:动作判定只是一次坐标比较。
- 举手:手腕的 y 比同侧肩膀的 y 小(画面坐标越小越靠上)。
- 下蹲:髋部的 y 超过画面高度的某个比例(人整体压低了)。
- 抬手多少度:肘关节用三点夹角算(肩-肘-腕)。
注意所有比较都要先归一化(除以画面宽高),否则换个大屏、人走近一点,同一条阈值就失效了 —— 这正是 EXP-11 要处理的第一件事。
动它会怎样:这一步在 EXP-11 里才会出现阈值(0.72 与 0.08);本页只到「点在哪」。
vis 没过门限,程序主动没连。先认识 5 个词
人体上可定位的关节点,每个是一组 (x, y, vis)。什么时候失效:人被大面积遮挡时,被挡的点只能靠「猜」。
这个点「有没有看到」的分数(logit 转概率)。什么时候失效:宽松衣服、深色背景会让分数整体偏低。
整帧「有没有人」的判断,与检测分数同源。什么时候失效:只露一条腿、画面里几乎没人时直接掉下门限。
官方定义的 35 条骨架边,规定哪些点之间连线。什么时候失效:换成手部/面部模型时索引完全不同,不能混用。
点位置除以画面宽高,变成 0~1 的比例。什么时候失效:画面被裁切或缩放后没同步更新比例,判定会整体偏移。
什么时候会翻车:五个真实边界选读 1 分钟
- 只露上半身 —— 下半身点位全部低可见度,骨架只剩上半截。做「下蹲」判定会永远判不出来。
- 人站太远 —— 人体占的像素太少,
presence掉下 0.5,整帧不给点。退后一步就是「没反应」。 - 宽松长裙 / 大衣 —— 髋、膝位置被布料盖住,模型按轮廓外推,蹲下时误差最大。
- 侧身对着镜头 —— 左右肩重叠,两点几乎重合,靠「左右手」区分动作的逻辑会失效。
- 两人入镜 —— 本模型只给一个人(取分最高的),第二个人会被静默忽略;要多人得跑多实例或先用人检测裁片。
| 步 | 数据变成什么 | 为什么非它不可 / 少了会怎样 | 谁在干 | 动参数会怎样 |
|---|---|---|---|---|
| 1 | 整帧 → 256×256 + scale/pad | 模型只认死尺寸;不记参数就无法反算 | resize_pad | 无参数 |
| 2 | 图 → 195 个数(39×5)+ 117 维世界坐标 | 模型本体;取错槽位就整体错位 | 模型前向 | min_detection_confidence 卡 presence |
| 3 | 195 → 33 组 (x, y, z, vis) 像素坐标 | 不按 5 个一组切,坐标全乱 | decode_pose | 无参数;vis 供绘制筛点 |
| 4 | 33 点 → 点 + 35 条线 | 不筛直接连,会画出「凭空的骨头」 | POSE_CONNECTIONS + view | 绘制门限↑ 骨架残缺;↓ 出现乱线 |
这张表只看第 2、5 两列就够:第 2 列是形态,第 5 列是旋钮。函数名在第四列。
05实操:单图 / 实时 / 站远与半身三条命令
先看「敲的命令」与「原理第几步」怎么对上号。
| 环节 | 函数(文件) | 吃哪个参数 | 对应原理第几步 |
|---|---|---|---|
| 取图 / 取流 | cv2.imread / Ros2Camera(demo_image.py / camera_demo.py) | source.mode --ros | 链外 |
| 缩放补边 | resize_pad / prepare(pose.py:43 / :87) | (无) | 步骤 1 |
| 推理 | 模型前向(engine.py) | min_detection_confidence | 步骤 2 |
| 解码 | decode_pose(pose.py:56) | (无) | 步骤 3 |
| 绘制 | view.draw + POSE_CONNECTIONS | 绘制门限 | 步骤 4 |
| 推流 | MjpegServer.set_frame | server.port stream_name | 链外 |
-
单图跑通,确认「点贴在人上」约 3 分钟
命令见第 03 节。跑完打开
out.jpg,按下面三行判断对错:左栏:骨架关节落在肩/肘/腕/髋/膝/踝上 右栏:火柴人结构与姿势一致(右手是抬起的) 底部:FPS: 0 Pose: OK这一步别急着走:右栏那根「扬起来的线」就是后面判「举手」的全部依据。
-
实时相机:看它跟不跟得住约 5 分钟
实时模式走 MJPEG,端口 8082(
pose.mjpg)。先起驱动再跑程序,两个终端ROS_DOMAIN_ID一致(N 取 0~101,各设备不同)。终端一 · 起相机驱动
source /opt/ros/humble/setup.bash && source ~/ros2_ws/install/setup.bash export ROS_DOMAIN_ID=N ros2 launch ascamera hp60c.launch.py
日志持续滚动,无 ERROR终端二 · 跑骨骼关键点
source /opt/ros/humble/setup.bash && source ~/ros2_ws/install/setup.bash export ROS_DOMAIN_ID=N cd ~/ros2_ws/src/xflz_eai_car1/vision/10_pose_landmark export PYTHONPATH=$PWD/car1_pose_landmark python -m pose_landmark_core.camera_demo --config config/pose_config.yaml --ros --port 8082
[camera_demo] MJPEG on http://<板IP>:8082/pose.mjpg ← 浏览器打开,举手/下蹲试着看骨架跟不跟 -
故意让它失败两次约 6 分钟
第一次:站远三步,让整个人只占画面一小块 —— 骨架要么整体消失(
presence掉门限),要么点位乱跳。第二次:只露出上半身(蹲到桌子后面)—— 下肢的点会掉到低可见度,骨架只剩上半截。这两次失败比成功更有价值:它们定义了这套方案的适用边界。
把
min_detection_confidence从 0.5 降到 0.3 再试一次远距离 —— 能画出点,但会抖。这就是「门限换召回,抖动换稳定」。
Pose: OK。两处对上了,说明缩放、推理、解码、绘制四段你已完整走通。你该看到的画面
33 点、35 线齐全,关节落在肩肘腕髋膝踝上。
下肢 vis 低,连线自动省略 —— 不是画错,是「不敢连」。
presence 掉下 0.5,程序认为画面里没有人。
先靠近、再看光线;或把门限降到 0.3 试一次。
踩坑速查
| 现象 | 原因 | 怎么办 |
|---|---|---|
报 ModuleNotFoundError: pose_landmark_core | 没加 PYTHONPATH | export PYTHONPATH=$PWD/car1_pose_landmark,用 -m 跑 |
| 点位整体偏到一边 | scale/pad 没传到反算,或画面被裁过 | 检查解码用的缩放参数;不要手动裁帧后再喂模型 |
| 只画出半截骨架 | 下肢被挡 / 出画,vis 低 | 退到全身入镜;或接受「只用上半身判定」 |
| 骨架抖动厉害 | 人太远、光不足,点位噪声大 | 靠近镜头;必要时对点位做几帧平滑 |
状态栏 Pose: NO | presence 低于 0.5 | 靠近、露全身;或把 min_detection_confidence 降到 0.3 |
--ros 报 cannot open ros topic | 驱动没起 / ROS_DOMAIN_ID 不一致 / 话题被 remap | 先起 ascamera;两终端 export 同一个 N;别改话题名 |
进阶 1 · 用三点夹角算「抬手多少度」约 20 分钟
取肩-肘-腕三个点,用余弦定理算夹角。手臂垂下时约 170°,举平约 90°,举过头顶会小于 60°。
比「比 y 大小」更细腻:可以做「抬到一半停住」「挥手」这类连续量判定,也是 EXP-12 手势识别的同一套数学。
进阶 2 · 把点位画成一条「运动轨迹」约 25 分钟
把最近 30 帧的右手腕坐标钉在画面上,连成一条线 —— 挥手、画圈这些动作会立刻显现出形状。
这是「时序特征」的起点:单帧只能判姿态,多帧才能判动作,EXP-11 的动作分类就建立在这个思路上。
🧪 Vibe Coding · 自己把这段代码读明白约 20 分钟
你要产出三样:模块职责表 → 控制流流程图 → 时序图。与真源码逐条对照。
提示词 1 · 模块职责分析
请阅读这个骨骼关键点项目的模块:pose_landmark_core/pose.py、engine.py、config.py、view.py、demo_image.py。 它们共同完成「读一张图 → 输出 33 个关键点 → 画骨架」。 请输出一张表,每行一个模块,四列:模块名 / 一句话职责 / 被谁调用 / 从哪个配置项取值。 关键函数标行号;不确定的地方请明说。
提示词 2 · 控制流流程图
请为 pose.py 的 decode_pose 画一张流程图。 必须说清:模型的 195 维输出如何按「每 5 个数一组」拆成 39 个槽位、只取前 33 个;归一化 x/y 如何按 scale 与 pad 反算成原图像素;vis 与 presence 各自用在哪一步。 框里写函数名或变量名,不要写「处理数据」这类空词。
提示词 3 · 时序图
请为实时模式画一张时序图,参与者用真实名字:ros2_camera 节点、Ros2Camera 回调、PoseEngine、view.draw、MjpegServer。 按顺序标出:话题帧到达、缩放补边、模型推理、解码、绘制、推流。 并说明这条链路里只有一次模型推理,没有 ROI 裁剪,所以比人脸关键点那条链更短。
自查三问:能指出 195 = 39×5 在代码里的位置?能说清 scale/pad 在解码里怎么用?时序图里说清了「只推理一次」?
三处必然的差异:
| 差异点 | AI 版常见写法 | 工程版(本仓) | 为什么工程这么写 |
|---|---|---|---|
| 输出怎么切 | reshape(33, 4) 当成 x/y/z/vis | 按 39×5 交错切、只取前 33 组(pose.py:62-76) | 模型实际是 39 个槽位、每槽 5 个数;切错则整体错位 |
| 要不要裁片 | 照人脸那套先裁出人再回归 | 整帧缩到 256 直接推理(pose.py:43) | 人体本来就大,裁片收益小、还多一次变换与误差 |
| 点位怎么画 | 所有点全连成网 | 只用官方 35 条边,且两端 vis 都要过门限(pose.py:21) | 528 对全连会糊成一团;低可信点连线会画出「凭空的骨头」 |
改一处、先预测再验证:把绘制门限从默认调到 0.9,先写下骨架会变成什么样,再跑一次对答案。
06互动:拖可见度门限,看骨架是残缺还是乱连
📌 这一节是「掌握检查」:亲手改一个参数,看现象是否朝你预期的方向变,还要能说出为什么 —— 说得出,才算掌握。
第 04 节说「骨架断不是 bug,是 vis 在过滤」。这一节把那条门限交给你:挡住几个部位是场景,门限多高是你说了算。
怎么读出规律:先不动门限、只加遮挡 —— 看骨架从「少一个点」变成「断几段」;再把门限往下压到 0.2 —— 点都回来了,但那些点是猜的。所以门限不是「越高越好」,而是「要可信还是要完整」。
15 点 14 线齐全,就是第 03 节那张图的样子。
手腕和膝的点不画,对应两条线断开 —— 骨架「断」不是错。
四肢大段丢失,只能判「人在不在」,判不了动作。
但被挡部位的点是外推出来的,位置可能离谱 —— 判定动作会被骗。
vis 在替你保守。门限抬高要完整度,降低要覆盖面 —— 这一步选错,后面的动作判定全跟着错。07验证:三道小测
回页首对照「实验目标」:① 关键点为何比框有用 → 试 Q1;② 单图与实时跑通了没有、Pose: OK 能否解释 → 回第 05 节;③ 两种坏相(断线 / 整帧空白)调出来没有 → 试 Q2 与 Q3,并回第 06 节把门限推到两端。三条全 ✓ 就完成。
reshape(33, 4) 会整体错位,画出来的骨架完全对不上人。源码注释专门写了「不是参考写的 [x,y,z,vis]×33」,就是提醒这一点。vis 低,程序主动不连;要么让全身入镜,要么改用只依赖上半身的判定pool.workers 调大workers 只影响帧率,与点质量无关。08章节总结:33 个点 + 35 条线,把「看懂人」压成「比大小」
这页讲了什么,它们怎么串起来
| 概念 | 它解决了什么 | 和别的关系 |
|---|---|---|
| resize_pad 到 256 | 把任意尺寸的画面变成模型认得的方形输入 | 记住的 scale/pad 是步骤 3 反算的唯一依据 |
| 195 维输出 | 一次前向给出全身所有关节的候选位置 | 39×5 交错,只取前 33 槽 —— 与 EXP-08 的「裁片」形成对照:人体不需要裁 |
| 33 组坐标 | 把「人」变成可计算的数字 | 下游所有动作判定都建立在这 33 组数上 |
vis 可见度 | 告诉调用方「这个点是看到的还是猜的」 | 决定点画不画、线连不连;也是「骨架会断」的原因 |
presence | 判断整帧里到底有没有人 | 与 min_detection_confidence 挂钩;不过门限整帧空白 |
POSE_CONNECTIONS | 规定 35 条骨架边,让结构可读 | 只影响显示;换模型必须换表 |
| 归一化比较 | 让「举手/下蹲」的阈值与画面尺寸无关 | 是 EXP-11 动作判定的第一步 |
② 直觉是骨架断线是模型不行,实际是
vis 在主动保守 —— 硬把线连回去反而更糟;③ 直觉是人体也要像人脸那样先裁片,实际是整帧缩到 256 就够,因为人体本来就占画面一大块。
195 = 39 × 5,不是 33 × 4;另有 117 维世界坐标,本页不用,别把两路搞混;②
min_detection_confidence 卡的是 presence(整帧要不要),画点的门限是 vis(每个点要不要)—— 两个门限管两件事;③ 端口是 8082(
pose.mjpg),与前两个实验的 8080/8081 错开;④ 只支持一个人(取分最高),两人入镜第二人会被静默忽略 —— 需要多人得先做人检测再逐个裁片。
前置依赖 · 缺一样就跑不起来
| 类型 | 具体项 | 缺了会怎样 / 怎么补 |
|---|---|---|
| 环境 | 板上 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 | 缺 rknnlite 模型加载失败;缺 mjpeg_streamer 实时启动即退出 |
| 模型 / 资源 | models/pose_landmark_full.rknn(约 7MB)+ images/pose_demo.jpg | 模型缺失没有任何输出;示例图太小(384×407)会让人误以为「分辨率低所以不准」,其实换大图点位更稳 |
| 配置 | config/pose_config.yaml:min_detection_confidence: 0.5、source.mode: image、server.port: 8082 | 缺 key 抛 ConfigError;mode: image 没写 rgb_path 直接报错 |
| 运行方式 | 项目根目录 + PYTHONPATH 含 car1_pose_landmark,用 -m 模块方式运行 | 直接跑 .py 文件会因相对导入失败 |
这套代码用在哪儿
- 体感控制:举手→前进、下蹲→后退,机器人/小车隔空操作 —— 这就是 EXP-11 要做的。
- 健身与康复:深蹲计数、手臂角度纠正,靠三点夹角与重复次数统计。
- 工业人机协作:判断人是否进入危险区、是否弯腰作业,替代穿戴式传感器。
- 不该用它的场合:只需要「有没有人」用检测就够(EXP-04);要精细手势(比耶、OK)得上手部关键点(EXP-12),33 点里手只到腕。
vis 决定哪段敢用,presence 决定这帧算不算有人。下一个实验离开人体,回到几何:画面里那个物体,离我到底多远?EXP-10 用深度图把第三维补上。