从OpenPose到动作识别:关键点提取、规则判定与时序分类实践

从OpenPose到动作识别:关键点提取、规则判定与时序分类实践 简介本资源是一套基于OpenPose的实时姿态估计与动作识别实战项目面向计算机视觉方向的初学者与开发者旨在解决从视频流中提取人体关键点并对常见动作进行分类的问题。项目涵盖完整的工作流视频采集、人体姿态估计、关键点预处理、动作分类输出并附带可运行的Python源码与预训练模型便于快速复现和二次开发。压缩包共33个文件以Python脚本为主包含模型推理与网络定义、动作分类、工具函数等模块另含pb格式的预训练模型、gif格式的演示效果、配置文件及说明文档整体大小33.66MB。已有360人学习下载适合希望通过实战案例理解OpenPose原理并实现动作识别功能的读者。通过研读源码和运行示例可以掌握姿态估计模型的使用方法、动作特征的组织方式以及完整项目的工程结构为后续开发智能监控、人机交互等应用打下基础。1. 姿态估计不只是“画骨架”它是动作识别的第一层抽象当我们需要计算机判断“这个人是在走路还是跑步”“这个工人在操作台上是否完成了规定动作”时最直接的做法是看视频、做分类。但视频是一堆像素光照变了、衣服换了、摄像机角度偏了同样的动作在像素层面可能完全不同。于是行业里沉淀出一条更稳的路径先把人还原成一组关键点坐标再基于这些坐标做动作判断。OpenPose 就是这条路径上最经典的开源实现。它能在单张图片或连续视频帧里同时检测多人的身体、手、脚共 135 个关键点输出每个点的坐标和置信度。这一层输出天然具备尺度不变性和一定的外观无关性——骨架拉远了还是一副骨架换件衣服骨架也不变。把关键点序列喂给分类模型或规则引擎就构成了一个完整的实时动作识别系统。这篇文章面向的是已经写过一些 Python、用过 OpenCV、但还没完整搭过姿态估计落地方案的开发者。我会从 OpenPose 1.7.0 的模型结构与配置说起给出可复现的推理代码再讲从关键点序列到动作分类的两种主流做法规则法和时序分类法。最后讨论性能瓶颈怎么破以及一个很实用的技巧用关键点差分替代光流来检测动作边界。文中全部代码基于官方 C/Python 接口的常见用法编写你拿到任何一份 OpenPose 项目源码都能对照本文把“从视频到动作标签”这条链路跑通。2. OpenPose 实时姿态估计的模型构成与复现要点OpenPose 的核心创新是 Part Affinity Fields (PAF)即部件亲和场。早期姿态估计方法如 CPM只对关键点做响应图预测然后依赖推理算法来配对左右肩、左右肘当多人出现在画面里时这种全局配对很容易错乱。PAF 的思路是对每一对相邻关键点之间的“连线方向”也做逐像素预测这样每个关键点归属到哪个人体就变成了一个有方向的匹配问题而不是盲目的组合问题。2.1 PAF 与置信图的双分支结构网络主干VGG19 的前 10 层提取特征后分成两个分支重复迭代 T 个 stage一个分支输出关键点置信图另一个分支输出 PAF 亲和场。每个 stage 都会把两个分支的结果和原始特征拼接再送入下一个 stage。这种做法让浅层的高分辨率细节和深层的语义信息不断融合对遮挡和小目标更友好。推理时OpenPose 的 pipeline 分为四步输入图片归一化到固定尺寸通常是 368x368 或 656x368保持人物完整入框。网络同时输出热力图和 PAF两者都是多通道的张量。从热力图中提取每个关键点的局部峰值坐标。利用 PAF 的向量场做匈牙利匹配把属于同一个人的关键点连接成骨架。这套设计使得同一张图里的人再多计算量也几乎不增长因为卷积计算是共享的增长的只是一些匹配耗时。这也是 OpenPose 在多人场景下仍然能保持实时性的原因。2.2 不同版本的模型文件选型OpenPose 官方发布的预训练模型主要有三个版本分别针对不同推理环境做了取舍模型参数量通道数适用设备关键点数FPS 参考GTX 1080pose/coco/pose_iter_440000.caffemodel约 46M3 分支CPU/GPU 均可COCO 18 点约 9pose/coco/pose_iter_440000.caffemodel 的 TensorRT 版本压缩后约 22M3 分支Jetson 等嵌入式COCO 18 点约 22pose/mpi/pose_iter_160000.caffemodel约 66M3 分支更关注四肢精度MPI 15 点约 7这里出现的3是 OpenPose 的一个常见配置项number_people_max影响的是匹配阶段的最大人数假设而不是网络本身。实际上网络输出通道数由body_part数量决定COCO 18 点意味着热力图有 18 个通道PAF 有 38 个通道19 对连线 x 2 个方向分量。提示如果是在 GPU 上做实时识别推荐直接用 COCO 18 点模型它比 MPI 15 点多出了脚踝关键点对“走/跑/跳”这类动作的区分度明显更好。2.3 最小复现命令一行代码跑通单人姿态估计拿到项目源码后最常见的入口是build/examples/openpose/openpose.bin。先做一次最基本的推理验证环境是否通build/examples/openpose/openpose.bin \ --model_pose COCO \ --image_dir examples/media/ \ --write_json output/ \ --write_images output_vis/ \ --display 0 \ --render_pose 1 \ --net_resolution 656x368逐项解释各个参数--model_pose COCO指定使用 COCO 关键点定义对应 18 个身体关键点。--image_dir指向输入图片目录支持批量处理。--write_json把关键点坐标和置信度以 JSON 格式写入指定目录后续动作识别直接消费这些 JSON。--write_images输出渲染了骨架的可视化图片方便快速检查关键点是否对齐。--display 0关闭 GUI 窗口在无显示器的服务器上必须加。--render_pose 1表示在输出图中画骨架设为 0 则只输出数据不渲染能省一点耗时。--net_resolution 656x368是网络的输入分辨率w x h必须是 16 的倍数。跑完这一步你会看到output/下每个图片对应一个 JSON里面有people数组每个人体包含pose_keypoints_2d数组按x, y, confidence三元组排列。2.4 用 Python 接口嵌入到自有服务如果你不想依赖openpose.bin这个独立进程而是把姿态估计嵌入到 Python 后端里官方提供了 C 封装后的 Python 模块。一种常见的稳定做法是使用 OpenCV 读取帧序列逐帧喂给openpose模块并收集 JSON 数据结构。示例代码如下import cv2 import openpose import json params dict() params[model_folder] ./models/ params[net_resolution] 656x368 params[number_people_max] 1 op openpose.OpenPose(params) cap cv2.VideoCapture(demo.mp4) while cap.isOpened(): ret, frame cap.read() if not ret: break keypoints, output_image op.forward(frame, renderTrue) # keypoints: shape (num_people, 18, 3) 的 numpy 数组 if keypoints.shape[0] 0: person keypoints[0] nose person[0] print(fnose: ({nose[0]:.1f}, {nose[1]:.1f}) conf{nose[2]:.2f})这段代码的逻辑是打开视频文件逐帧调用op.forward()拿到所有人体的关键点数组。keypoints的维度是(人数, 关键点类型数, 3)最后一维的前两个分量是归一化到输入分辨率下的整数坐标第三个分量是模型置信度。renderTrue会让返回值第二项带上骨架渲染图方便保存或推流。需要特别注意params里的number_people_max设成 1 可以迫使 OpenPose 只返回最自信的那个人体在单人动作识别场景里不仅能省掉匹配开销还能减少误检抖动。3. 从关键点到动作规则式动作识别的工程实现有了每帧的 18 个关键点接下来要回答的核心问题是怎么把这些坐标翻译成“举手”“弯腰”“下蹲”这类语义标签。先说不依赖任何训练数据的规则式方案它特别适合动作类别少、约束场景明确的生产项目。3.1 为什么先做规则式而不是直接训分类网络很多人拿到关键点序列后第一反应是“堆一个 LSTM”。但冷静下来想一想动作识别的落地场景往往是受限的监控摄像头固定机位、工人站在工位前、患者在做康复训练。这些场景里动作本身的几何特征非常强。以“检测一个人是否举起右手”为例规则只需要判断右手腕关键点的 y 坐标是否显著小于右肩的 y 坐标同时两者间距是否超过某个阈值。这类几何规则的计算延迟是纳秒级的且不需要标注数据可以在采集数据的同时并行开发让项目先跑起来。规则式的另一个优势是可解释。医疗康复或安全生产场景里监管方会问“为什么判断为异常动作”规则引擎可以直接给出“右肘角度 162 度超出 150 度阈值”这样的量化依据而神经网络只能给出一堆特征权重。当然规则式方案的代价是泛化能力弱不同人的身高臂长差异需要做归一化。下面给出一种标准解法。3.2 关键点归一化让规则对身高和画面距离不敏感OpenPose 输出的坐标是像素值同样一个“弯腰”动作身高 160cm 和 190cm 的人在手肘弯曲角度上差异不大但手腕关键点的绝对像素位移差异很大。因此规则引擎的输入不应是原始坐标而是归一化后的相对特征。最常用的归一化基准是脊柱长度——即左肩和右髋之间的距离。这个距离定义了“这个人的骨架尺度”所有其他坐标都除以它就能得到一个尺度无关的骨架描述。import numpy as np def normalize_skeleton(pose): 输入 pose: (18, 3) 数组COCO关键点顺序 返回 scale_skeleton: (18, 2) 归一化后的 x,y 坐标 # COCO 索引: 2右耳, 5左肩, 8右髋 left_shoulder pose[5][:2] right_hip pose[8][:2] spine_len np.linalg.norm(left_shoulder - right_hip) 1e-6 scale_skeleton pose[:, :2] / spine_len return scale_skeleton3.3 如何设计动作规则的阈值体系基于归一化骨架就可以定义关键动作的判定规则。以“检测下蹲”为例需要同时看两个关节角度这种判定需要两个条件同时满足才判定为下蹲。条件一是髋关节角度条件二是膝关节角度。def angle_between(p1, p2, p3): 计算 p2 处的夹角度p1、p3 是相邻点 a np.array(p1) - np.array(p2) b np.array(p3) - np.array(p2) cos_a np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b) 1e-6) return np.degrees(np.arccos(np.clip(cos_a, -1.0, 1.0))) def is_squat(pose, hip_angle_max100, knee_angle_max110): # COCO: 8右髋, 9右膝, 10右踝 hip angle_between(pose[5][:2], pose[8][:2], pose[9][:2]) knee angle_between(pose[8][:2], pose[9][:2], pose[10][:2]) return hip hip_angle_max and knee knee_angle_max参数hip_angle_max100的意思是当髋关节弯曲到 100 度以内时认为躯干已有明显前倾knee_angle_max110同理膝关节弯曲超过 110 度意味着大腿和小腿折叠明显。两个阈值都可以根据实际观测的机位和人的体型微调但一般不要设成更宽松的值否则“半蹲”和“弯腰拾物”容易混淆。注意实际项目中姿态估计的关键点带有抖动直接对单帧做阈值判定会产生大量毛刺。常见的做法是加一个时序平滑层最轻量的方法是滑动窗口投票——取最近 5 帧里is_squat的众数作为当前判定结果。3.4 规则式动作识别为何在真实场景中失败最常见的失败模式不是规则写得不对而是输入的关键点本身质量差。OpenPose 处理侧身或背对镜头的人时自遮挡会导致肩、髋关键点置信度跌破 0.3而我们的归一化代码里用了这些低置信度的点做分母和角度基准噪声会被放大。一个强制的保护措施是置信度过滤conf_threshold 0.3 valid_indices pose[:, 2] conf_threshold if not valid_indices[[5, 8, 9, 10]].all(): return None # 关键点不足跳过该帧另外画面里出现多个人时OpenPose 返回的 person 顺序不保证稳定同一人可能在不同帧中被分配到不同索引。如果你的场景是固定单人用number_people_max1即可规避如果是多人就要引入追踪算法如按关键点中心点做最近邻匹配来保持人物 ID。4. 用关键点序列训练动作分类模型LSTM 与时空图网络规则式方案做到一定程度就会遇到天花板动作边界模糊走和跑、动作组合复杂太极的云手、个体差异极大儿童和成人做同一动作时手工调的规则很难继续提升。这时候就需要切换范式让模型自己从关键点时序里学习动作的时空特征。4.1 为什么选择关键点序列作为模型输入而不是原始视频一个常被忽略的事实姿态估计已经把原始视频压缩了大约两个数量级。以 640x360 分辨率 30fps 的视频为例单帧数据量约 691KB而 18 个关键点x, y, confidence的单帧数据量是 54 个 float约 216B。这种压缩不仅让模型规模可以很小也把训练从“图像分类”问题降维成了“序列拟合”问题数据需求大幅减小。另一个优势是隐私保护。很多工厂和医院场景不允许把原始视频传到云端训练但关键点坐标是脱敏数据可以直接上传到云端做模型迭代合规成本低很多。4.2 构造训练数据从 OpenPose JSON 到样本序列训练样本的构造逻辑是把一段连续视频帧的骨架堆叠成张量。通常取 30 帧1 秒为窗口窗口重叠 15 帧做滑窗采样得到大量样本。import json import numpy as np import os def load_json_to_sequence(json_dir, window_size30, stride15): files sorted(os.listdir(json_dir)) all_frames [] for f in files: with open(os.path.join(json_dir, f)) as fp: data json.load(fp) if len(data[people]) 0: all_frames.append(np.zeros((18, 3), dtypenp.float32)) else: kp np.array(data[people][0][pose_keypoints_2d]).reshape(-1, 3) all_frames.append(kp) sequences [] for start in range(0, len(all_frames) - window_size 1, stride): seq np.stack(all_frames[start:startwindow_size], axis0) # (30, 18, 3) sequences.append(seq) return np.array(sequences)window_size30表示输入 30 帧大约 1 秒动作stride15让相邻样本有 50% 重复能平滑过拟合。pose_keypoints_2d被 reshape 成(18, 3)第三个通道是置信度。关键点缺失OpenPose 没检测到时用全零占位模型需要学会忽略这些帧。4.3 一个极简的 LSTM 分类模型对于 30 帧的骨架序列一个单层 LSTM 往往就够用了因为序列很短且每帧特征维度也只有 3618 点 x 2 坐标或再加置信度。import torch import torch.nn as nn class SkeletonLSTM(nn.Module): def __init__(self, input_dim36, hidden_dim64, num_classes5): super().__init__() self.lstm nn.LSTM(input_dim, hidden_dim, batch_firstTrue, num_layers1) self.classifier nn.Linear(hidden_dim, num_classes) def forward(self, x): # x: (batch, seq_len30, input_dim36) out, _ self.lstm(x) out out[:, -1, :] # 取最后时间步 return self.classifier(out)这里的input_dim36是把 x 和 y 坐标拼接丢弃置信度用置信度做 mask 需要额外处理初级项目直接拼接坐标最简单。hidden_dim64的 LSTM 对 30 步序列有足够容量。取最后时间步的输出作为整段序列的表示再加一个线性层做分类。4.4 训练策略与特征顺序的影响训练这个模型时要注意骨架数据做归一化是必须的否则不同身高的人在坐标空间里差异过大LSTM 很难收敛。推荐的预处理顺序是先用 3.2 节的方法对每一帧做尺度归一化再对每个关键点做 z-score 标准化按训练集的均值方差。另一个容易踩的坑是不要把面朝左和面朝右的动作合并训练。骨架坐标是绝对的一个人面向左“挥手”和面向右“挥手”x 坐标序列基本互为镜像如果标签只有“挥手”一个类模型会学到“x 坐标减小等于挥手”导致泛化崩溃。常见做法是在训练时做水平翻转数据增强把 x 坐标映射为 width - x标签不变。这样训练出来的模型在测试集上的准确率通常能到 90% 以上但实际部署到新环境时可能会有明显掉点主要原因是 OpenPose 在训练集和使用环境里的检测误差分布不同。5. 用关键点差分做动作边界检测一个高性价比技巧训练完分类模型后有一个问题很少有人提前想清楚一段连续的视频里动作什么时候开始、什么时候结束如果模型每 30 帧滑动一次分类你会在两个动作的过渡期得到一堆乱序标签。5.1 关键点位移差分的定量表达一个有效的边界检测信号是“关键点位移能量”。对连续两帧的同一个关键点求欧氏距离把所有关键点的距离累加得到一个标量值描述这一帧的瞬时运动强度。动作开始前人体是静止的能量趋于零动作进行中能量显著上升动作结束回到静止能量回落。def motion_energy(pose_t1, pose_t2): 输入两帧关键点 (18, 2)输出一个标量表示运动强度 sq_diff (pose_t1 - pose_t2) ** 2 dist np.sqrt(np.sum(sq_diff, axis1)) # 过滤掉置信度低的点这里假定输入已做过置信度过滤 return np.mean(dist)np.mean(dist)而非np.sum(dist)是为了让人数不同时的能量值可比动作识别场景中单人检测两者差别不大。也可以做加权平均给手腕和脚踝更大的权重因为这些远端点的运动幅度最能反映意图。5.2 双阈值判定与静默期合并单阈值判定边界会产生抖动在零点和峰值之间反复横跳。更好的方案是双阈值状态机。def segment_by_energy(energy_list, hi0.8, lo0.3, min_duration5): state idle segments [] start 0 for i, e in enumerate(energy_list): if state idle and e hi: state active start i elif state active and e lo: state idle if i - start min_duration: segments.append((start, i)) return segmentshi0.8是进入动作态的触发阈值必须选得足够高确保不是噪声引起的误触发lo0.3是退出动作态的释放阈值小于进入阈值以形成滞后避免在阈值附近震荡。min_duration5过滤掉少于 5 帧的瞬时闪烁这类闪烁通常是姿态估计抖动造成的。动作低谷时的短暂停顿如举重时的保持可能会被误切为两个动作需要根据具体业务设置最大暂停长度来合并。5.3 把边界分割接入完整推理链路联调时的流程是读视频帧 - OpenPose 推理获取关键点 - 计算相邻帧运动能量 - 用双阈值切出动作片段 - 对片段内的每 30 帧窗口用 LSTM 分类 - 对片段内多个窗口的分类做投票得到该动作片段的最终标签。这套流程的关键收益在于分类器不再需要每帧都做只在边界检测判定为“动作进行中”时才调用 LSTMCPU 占用大幅下降。在树莓派或边缘盒子上这种优化往往比换轻量级姿态模型更有效。5.4 一个工程上的补充做法用置信度辅助边界判断运动能量在跟丢目标时会产生较大的失真。如果 OpenPose 出现多帧关键点置信度全部低于阈值但检测程序仍输出垃圾坐标能量信号就会异常放大出现假边界。所以实践里我会把上一节的能量公式升级为带置信度门控的版本def motion_energy_conf(pose_t1, pose_t2, conf_t2, conf_th0.3): mask conf_t2 conf_th if mask.sum() 6: return 0.0 # 有效关键点太少视为静止 dist np.sqrt(((pose_t1 - pose_t2) ** 2).sum(axis1)) return dist[mask].mean()这个改动在工程上很关键。它不会让你拿 6 个点以下的帧直接报错而是视作无效帧返回零能量让后续状态机停留在当前状态。从视频流稳定性角度看这样就避免了丢失一帧导致整个动作被切断的尴尬情况。动作识别系统的最后一公里往往不在模型结构而在这种异常信号的兜底设计上。本文还有配套的精品资源点击获取