简介:本资源面向计算机视觉学习者与开发者,提供基于OpenPose的实时姿态估计与动作识别完整项目源码,适合希望从关键点检测过渡到行为分析的中高级实践者。压缩包共33个文件,约33.66MB,以18个Python脚本为核心,覆盖姿态估计、关键点预处理、动作分类与跟踪等模块;另含3个pb模型文件、1个mat数据及gif演示素材,便于快速验证效果。项目结构清晰,包含utils、pose、models等目录,涉及网络构建、关键点提取与动作识别流程,并附README说明与运行脚本,方便二次开发与场景迁移。目前已有360人学习下载。通过源码可掌握从视频流采集、关键点提取到动作输出的完整链路,理解数据预处理、模型推理与评估各环节,并借助演示素材直观对比识别效果,为智能监控、人机交互等应用打下实战基础。
1. 从 OpenPose 到动作识别:一条能跑通的实时姿态估计链路长什么样
很多人第一次接触人体动作识别,都是从一段演示视频开始的:摄像头画面里,一个人抬手、下蹲、挥手,屏幕上实时画出骨架连线,旁边跳出「挥手」「深蹲」的标签。看起来像是模型直接看懂了动作,实际上背后是两段完全不同的工作:先用 OpenPose 把画面里的人体关键点抠出来,再用一个时序分类器把连续的关键点序列翻译成动作类别。标题里说的「实时姿态估计 + 动作识别」,本质就是把这两段串成一条低延迟流水线。
这条链路适合谁?做安防行为分析、健身动作计数、康复训练监测、体感交互的团队都能直接用。它最大的价值在于解耦:姿态估计负责「人在哪、关节在哪」,动作识别负责「这些关节在时间上怎么动」。换动作类别时,往往只需要重训后面那个小分类器,前面的 OpenPose 不用动。下面按「先立住原理、再动手复现、最后讲坑」的顺序,把这条链路拆开讲清楚,源码结构也会在中间章节对应说明。
2. OpenPose 实时姿态估计:从 BODY_25 关键点到可用的骨架序列
2.1 为什么选 OpenPose 而不是直接端到端做动作分类
端到端视频分类模型(比如把连续帧直接塞进 3D CNN)理论上能省掉中间步骤,但落地时有三个现实问题。第一,算力成本高,一段 1080p、30fps 的视频直接进 3D 网络,显存和延迟都很难压到实时。第二,可解释性差,模型说「这是摔倒」,你没法知道它到底看了哪个部位。第三,数据标注贵,动作分类要标整段视频,而关键点标注可以复用现成的姿态数据集。
OpenPose 的定位是「多人 2D 姿态估计」,输出的是每个人体的关键点坐标加置信度。它用 Part Affinity Fields(PAF)把检测到的关节连成骨架,能在同一帧里区分多个人。常见做法是:OpenPose 只跑一次,把每帧的关键点存成序列,后面的动作识别模型只吃这个序列。这样动作识别模型的输入维度从「H×W×3×T」降到「关键点数×2×T」,训练和推理都快一个量级。
BODY_25 是 OpenPose 里比较常用的模型,输出 25 个关键点,比 COCO 的 17 点多了脚部、颈部等位置,对下肢动作(深蹲、抬腿)更友好。选它还是选 COCO-18,取决于你的动作是否依赖脚部细节。健身类动作建议 BODY_25,纯上半身手势用 COCO-18 也够。
2.2 用 Python 调 OpenPose 拿到逐帧关键点
实际工程里很少直接改 OpenPose 的 C++ 源码,常见做法是用它编译出的 Python API 或者封装好的推理接口。下面这段是典型的逐帧提取骨架序列的写法,思路是:读视频 → 每帧推理 → 取置信度最高的人 → 归一化坐标 → 存成 (T, V, C) 的张量。
import cv2 import numpy as np # 假设 openpose_py 是编译好的 OpenPose Python 绑定 import pyopenpose as op params = { "model_folder": "models/", # 模型权重目录 "model_pose": "BODY_25", # 25 关键点模型 "net_resolution": "368x368", # 推理分辨率,越小越快 "number_people_max": 1, # 只跟踪一个人,降低后处理开销 } opWrapper = op.WrapperPython() opWrapper.configure(params) opWrapper.start() def extract_keypoints(video_path, target_fps=15): cap = cv2.VideoCapture(video_path) src_fps = cap.get(cv2.CAP_PROP_FPS) step = max(1, int(round(src_fps / target_fps))) # 抽帧,降低序列长度 frames, idx = [], 0 while True: ret, frame = cap.read() if not ret: break if idx % step == 0: datum = op.Datum() datum.cvInputData = frame opWrapper.emplaceAndPop(op.VectorDatum([datum])) if datum.poseKeypoints is not None and len(datum.poseKeypoints) > 0: # 取置信度最高的人,shape: (25, 3) -> x, y, score person = datum.poseKeypoints[0] h, w = frame.shape[:2] # 归一化到 0~1,消除分辨率影响 norm = person[:, :2] / np.array([w, h]) score = person[:, 2:3] frames.append(np.concatenate([norm, score], axis=1)) idx += 1 cap.release() return np.array(frames) # (T, 25, 3) seq = extract_keypoints("demo.mp4", target_fps=15) print(seq.shape)逻辑说明:net_resolution控制推理分辨率,368x368 是速度和精度的折中,追求实时可以降到 256x256,但小关节会飘。number_people_max=1在单人场景下能省掉大量匹配计算。抽帧这一步很关键,OpenPose 逐帧跑 30fps 视频在普通显卡上很难实时,抽到 15fps 对动作识别几乎无损,因为人体动作的频率远低于 15Hz。
参数说明:target_fps决定序列的时间粒度,太低会丢失快速动作(比如挥手),太高则序列变长、分类器负担加重。经验值是 10~15fps。归一化用画面宽高而不是人体框,是为了保留人体在画面中的绝对位置信息,这对「走进画面」「倒地」这类动作有用;如果你只关心姿态本身,可以改成按人体框归一化。
2.3 关键点序列的清洗与对齐
原始关键点序列有三个常见问题:置信度低的点会乱跳、不同视频的人体尺度不同、序列长度不一致。清洗策略是:置信度低于阈值(比如 0.3)的点标记为缺失,用前后帧线性插值补上;连续缺失超过 5 帧的片段直接丢弃。尺度对齐用人体髋部中点做参考,把坐标平移到以髋部为原点,再除以肩宽做缩放,这样深蹲和站立的人体尺度就统一了。
对齐后的序列才是动作识别模型真正吃的输入。这一步做不好,后面分类器再强也救不回来,属于典型的「垃圾进垃圾出」。
3. 动作识别模型:在骨架序列上做时序分类的三种落地路线
3.1 骨架动作识别的三条技术路线对比
拿到 (T, V, C) 的骨架序列后,分类模型的选择直接决定精度和延迟。常见三条路线:
| 路线 | 代表方法 | 输入形式 | 优点 | 缺点 |
|---|---|---|---|---|
| 时序卷积 | TCN / 1D-CNN | (T, V*C) 展平 | 训练快、易部署 | 对长时序依赖弱 |
| 循环网络 | LSTM / GRU | (T, V*C) | 天然处理变长序列 | 并行差、易梯度问题 |
| 图卷积 | ST-GCN 类 | (T, V, C) 图结构 | 利用骨架拓扑、精度高 | 实现复杂、调参多 |
如果目标是快速跑通一个能用的动作识别,我一般先上 TCN 或 LSTM 做基线,确认数据管线和标注没问题,再考虑换 ST-GCN 提精度。标题里说的「实时」,TCN 和轻量 LSTM 更容易满足,ST-GCN 在边缘设备上要额外做剪枝。
3.2 用 TCN 搭一个可训练的动作分类器
下面是一个基于一维卷积的时序分类器,输入是 (batch, T, V*C),输出动作类别。结构是三层膨胀卷积加全局池化,膨胀系数逐层翻倍,用较小参数量覆盖较长时序。
import torch import torch.nn as nn class SkeletonTCN(nn.Module): def __init__(self, in_dim, num_classes, channels=128, kernel=5): super().__init__() layers = [] for i in range(3): dilation = 2 ** i # 1, 2, 4 膨胀 padding = (kernel - 1) * dilation // 2 # 保持时序长度 layers.append(nn.Conv1d(in_dim, channels, kernel_size=kernel, padding=padding, dilation=dilation)) layers.append(nn.BatchNorm1d(channels)) layers.append(nn.ReLU()) self.tcn = nn.Sequential(*layers) self.pool = nn.AdaptiveAvgPool1d(1) # 时序维池化 self.fc = nn.Linear(channels, num_classes) def forward(self, x): # x: (B, T, V*C) -> (B, V*C, T) x = x.transpose(1, 2) x = self.tcn(x) x = self.pool(x).squeeze(-1) return self.fc(x) model = SkeletonTCN(in_dim=25 * 3, num_classes=10) dummy = torch.randn(8, 60, 75) # batch=8, T=60, V*C=75 print(model(dummy).shape) # (8, 10)逻辑说明:in_dim是每帧的特征维度,BODY_25 的 x、y、score 三个通道就是 75。channels控制模型容量,128 在多数动作集上够用,边缘设备可以降到 64。膨胀卷积的作用是在不堆层数的情况下扩大感受野,三层膨胀 1、2、4 配合 kernel=5,理论感受野能覆盖约 60 帧,正好对应 15fps 下 4 秒的动作。
参数说明:kernel建议 3 或 5,太大在小数据集上容易过拟合。num_classes按你的动作类别数设。训练时序列长度 T 要统一,短序列补零、长序列滑窗切分,滑窗步长取 T/2 能起到数据增强作用。
3.3 训练配置与实时推理的衔接
训练用交叉熵损失加 Adam,学习率 1e-3,batch 32,通常 50~100 轮收敛。数据量少的时候加 dropout 和权重衰减。类别不均衡用带权重的交叉熵,权重取类别频率的倒数。
实时推理时,维护一个长度为 T 的滑动窗口队列,每来一帧关键点就入队、队首出队,凑满 T 帧就推理一次。推理频率不用跟视频帧率一致,可以每 3~5 帧推理一次,中间复用上次结果,这样能把动作识别的开销压到很低。窗口步长和推理间隔是实时性的两个关键旋钮,后面避坑章节会细说。
4. 把姿态估计和动作识别串成实时流水线:源码结构与部署要点
4.1 项目源码的典型模块划分
标题里提到「附项目源码」,一个能跑通的实战项目通常包含这几个模块,你可以对照手里的源码看结构是否完整:
pose/:OpenPose 封装,负责视频/摄像头读取和关键点提取preprocess/:关键点清洗、归一化、滑窗切分model/:动作分类网络定义和权重加载train/:训练脚本、数据集加载、评估指标infer/:实时推理主循环,串起摄像头、姿态估计、分类器configs/:模型路径、阈值、窗口长度等参数集中管理
拿到源码先看configs/和infer/,这两个决定了怎么跑起来;再看preprocess/,这里最容易藏坑。
4.2 实时主循环的写法与延迟控制
实时流水线的核心是一个循环:抓帧 → 姿态估计 → 关键点入队 → 凑满窗口推理 → 叠加显示。下面是一个简化骨架,重点在队列和推理节流。
from collections import deque import cv2 import numpy as np import torch WINDOW = 60 # 窗口帧数,对应 15fps 下 4 秒 INFER_EVERY = 3 # 每 3 帧推理一次 queue = deque(maxlen=WINDOW) model.eval() last_label = "waiting" cap = cv2.VideoCapture(0) frame_id = 0 while True: ret, frame = cap.read() if not ret: break kp = run_openpose(frame) # 返回 (25, 3) 或 None if kp is not None: queue.append(kp.flatten()) # 展平成 75 维 if len(queue) == WINDOW and frame_id % INFER_EVERY == 0: x = torch.tensor(np.array(queue), dtype=torch.float32) x = x.unsqueeze(0) # (1, T, 75) with torch.no_grad(): logits = model(x) last_label = CLASSES[logits.argmax(1).item()] frame_id += 1 cv2.putText(frame, last_label, (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) cv2.imshow("action", frame) if cv2.waitKey(1) & 0xFF == 27: break cap.release() cv2.destroyAllWindows()逻辑说明:deque(maxlen=WINDOW)自动维护滑动窗口,不用手动出队。INFER_EVERY是节流阀,姿态估计每帧都跑,但分类器每 3 帧跑一次,中间复用last_label,这样显示不会闪。torch.no_grad()关掉梯度,推理显存和速度都会好很多。
参数说明:WINDOW要和训练时的 T 一致,否则模型输入分布对不上。INFER_EVERY越大越省算力,但动作切换的响应会变迟钝,2~5 是合理区间。如果姿态估计本身就很慢,瓶颈在 OpenPose 而不是分类器,这时候优先降net_resolution或抽帧。
4.3 部署时的硬件与依赖注意点
OpenPose 依赖 CUDA 和 cuDNN,版本要和编译时一致,这是新手最容易翻车的地方。如果目标机器没有 NVIDIA 显卡,可以考虑用轻量姿态估计模型替代 OpenPose 做推理,但关键点定义要对齐,否则动作分类器的输入维度会错位。部署到边缘设备时,把姿态估计和动作分类拆到两个进程甚至两台机器,用共享内存或本地 socket 传关键点,比塞进一个进程更好维护。
模型权重和配置文件建议打包进镜像,避免现场找不到路径。日志要记录每帧的推理耗时,方便定位是姿态估计慢还是分类慢。
5. 避坑与排查:实时动作识别最常见的 5 个翻车现场
5.1 关键点抖动导致动作标签疯狂跳变
现象:人站着不动,屏幕上的动作标签却在两个类别之间来回跳。原因:OpenPose 逐帧独立推理,关键点存在像素级抖动,分类器对噪声敏感。解决:对关键点做时序平滑,比如用指数移动平均,或者对分类器的输出做多数投票,连续 N 次推理结果一致才切换标签。平滑系数取 0.5~0.7,太大响应迟钝,太小压不住抖动。
5.2 训练精度很高,实时跑起来完全不对
现象:离线测试集准确率 95%,接上摄像头后基本全错。原因:训练数据的归一化方式和实时推理不一致,比如训练按人体框归一化,实时按画面归一化。解决:把预处理逻辑抽成一个函数,训练和推理共用同一份代码,杜绝两套实现。这是血泪经验,几乎每个团队都踩过一次。
5.3 窗口长度和动作时长不匹配
现象:快速动作识别不出来,慢动作又被截断。原因:固定窗口 T 只能覆盖固定时长,而不同动作持续时间差异大。解决:要么按动作类型设不同窗口,要么用多尺度窗口并行推理再融合。简单做法是把窗口设成能覆盖最长动作,短动作靠池化层自己压缩。
5.4 多人场景下关键点串人
现象:画面里两个人靠近时,骨架连线跳到另一个人身上,动作标签跟着乱。原因:OpenPose 的多人匹配在遮挡和交叉时容易出错。解决:加人体跟踪,用 IOU 或外观特征把关键点绑定到 track id,每个 id 维护独立的关键点队列和分类器状态。跟踪器用轻量的就行,别引入太重的 ReID 模型拖慢实时性。
5.5 显存泄漏导致跑几十分钟后卡死
现象:程序刚启动很流畅,跑半小时后越来越卡直到崩溃。原因:每帧都创建新的 CUDA tensor 没释放,或者 OpenPose 的 Datum 对象没复用。解决:推理循环里复用预分配的 tensor,用torch.cuda.empty_cache()定期清理,OpenPose 的 Datum 在循环外创建、循环内重置。监控显存占用,发现持续上涨就查对象生命周期。
6. 让动作识别更稳的两个进阶技巧:时序增强与置信度融合
跑通基线之后,想再提一档精度,我一般从两个地方下手。第一个是时序数据增强。骨架序列不像图像那么好做增强,但可以做的有:随机时间裁剪(截取窗口的 80%~100%)、随机时间翻转(把序列倒过来,对「挥手」这类对称动作有效但要慎用)、关节坐标加高斯噪声模拟检测误差、随机遮挡部分关节模拟遮挡场景。这些增强能让模型对真实场景的抖动和缺失更鲁棒,实测在小数据集上能涨 3~5 个点。
第二个是置信度融合。OpenPose 输出的每个关键点带 score,很多人预处理时直接把 score 丢掉只用 x、y,其实浪费了信息。把 score 作为额外通道输入模型,或者在时序平滑时用 score 加权,能让模型知道哪些点可信。更进一步,分类器输出 softmax 概率后,可以结合关键点平均置信度做加权:置信度低时降低该窗口预测的权重,避免因为姿态估计失败而误判动作。
下面是一个带置信度加权的推理片段,展示怎么把 score 用起来:
def predict_with_confidence(model, window, classes, score_idx=2): # window: (T, V, C),C 含 x, y, score x = torch.tensor(window.reshape(1, window.shape[0], -1), dtype=torch.float32) with torch.no_grad(): prob = torch.softmax(model(x), dim=1)[0] # 关键点平均置信度作为整体可信度 mean_score = window[:, :, score_idx].mean() # 置信度低于 0.3 时大幅衰减预测权重 weight = min(1.0, mean_score / 0.3) idx = prob.argmax().item() return classes[idx], prob[idx].item() * weight逻辑说明:mean_score反映这一窗口内姿态估计的整体质量,遮挡严重时它会明显下降。weight把分类置信度和姿态质量绑定,输出一个更保守的分数,下游做决策时可以用这个分数过滤掉不可靠的预测。参数上,0.3 这个阈值按你的场景调,室内光照好可以降到 0.2,室外复杂场景可以提到 0.4。
我自己的习惯是:任何动作识别项目,先把姿态估计的可视化调出来看,确认关键点稳了再谈分类精度。骨架都画不准,后面全是玄学。这套链路不复杂,难的是每个环节的细节对齐,希望帮到你。
本文还有配套的精品资源,点击获取