
简介本资源面向计算机视觉初学者与进阶开发者提供基于OpenPose的实时姿态估计与动作识别完整项目实战。包内共33个文件以18个Python脚本为核心涵盖姿态估计网络、关键点预处理、动作分类与跟踪等模块另含3个pb模型文件、1个mat数据及gif演示动图压缩包约33.66MB便于快速部署与二次开发。项目从视频流采集、人体关键点提取到动作识别输出形成完整链路并附README说明与配置脚本帮助读者理解算法原理、调试模型并迁移至人机交互、智能监控或体育分析等场景。目前已有360人学习下载适合希望以源码驱动方式掌握实时动作识别技术、积累项目经验的开发者参考。1. 实时姿态估计加动作识别从 OpenPose 骨架到可落地分类器的完整链路很多人第一次接触「动作识别」这个词脑子里浮现的是监控画面里框出一个人然后标注「跌倒」「打架」。但真正动手做过就知道直接拿视频帧喂给分类网络效果往往玄学得很——换个背景、换件衣服、换个人准确率就崩了。我踩过这个坑之后才转向另一条路先用 OpenPose 把人体骨架关键点提取出来再基于骨架序列做动作分类。这样做的好处是骨架数据天然屏蔽了背景、光照、衣着这些干扰因素模型只需要关心关节之间的相对运动关系。这套方案适合谁适合有一定 Python 基础、想做实时人体动作识别但不想从零训一个视频分类大模型的工程师。整条链路的核心环节包括OpenPose 环境搭建与关键点提取、骨架数据预处理与归一化、动作分类模型选型与训练、实时推理管线的工程化。下面按实际落地顺序拆开讲。2. OpenPose 关键点提取环境搭建与最小可跑通流程2.1 为什么选 OpenPose 而不是其他姿态估计方案做实时姿态估计市面上可选的路子不少。MediaPipe 轻量、部署方便但关键点精度在遮挡场景下会明显下降HRNet 精度高但推理速度对实时场景不够友好。OpenPose 的优势在于多人场景下的鲁棒性和关键点定义的成熟度——它输出的是 COCO 格式的 18 个关键点包括鼻子、脖子、肩膀、手肘、手腕、髋关节、膝盖、脚踝等这套定义已经被大量下游任务验证过。我一般会这样选如果只是单人、简单场景MediaPipe 够用如果需要多人、有遮挡、要求实时OpenPose 是更稳的基线。OpenPose 的推理管线分两段先通过 VGG 或 MobileNet 骨干网络提取特征图再分别用两个分支预测关键点热力图confidence map和部位亲和场PAF, Part Affinity Fields最后通过贪心匹配把关键点组装成每个人的骨架。2.2 环境搭建从源码编译到 Python API 调用OpenPose 的安装是第一个大坑。官方推荐从源码编译依赖 CMake、CUDA、cuDNN、OpenCV 和 Boost。以下是在 Ubuntu 20.04 CUDA 11.x 环境下的最小编译流程# 安装系统依赖 sudo apt-get update sudo apt-get install -y cmake libopencv-dev libboost-all-dev \ libprotobuf-dev protobuf-compiler libgoogle-glog-dev \ libgflags-dev libhdf5-dev # 克隆 OpenPose 源码 git clone https://github.com/CMU-Perceptual-Computing-Lab/openpose.git cd openpose mkdir build cd build # CMake 配置开启 Python API关闭不用的模块 cmake .. -DBUILD_PYTHONON \ -DPYTHON_EXECUTABLE$(which python3) \ -DBUILD_EXAMPLESOFF \ -DUSE_CUDNNON \ -DCUDA_ARCH_BIN7.5 # 根据你的显卡算力调整 # 编译-j 后面的数字按 CPU 核数调整 make -j$(nproc) sudo make install编译完成后需要把生成的 Python 模块路径加入环境变量export PYTHONPATH$PYTHONPATH:/usr/local/python参数说明CUDA_ARCH_BIN必须和你的 GPU 算力匹配写错了编译能过但运行时会报 no kernel image。BUILD_PYTHON打开后才能用import pyopenpose。如果编译过程中 protobuf 报版本冲突常见做法是卸载系统 protobuf 后用 OpenPose 自带的 3rdparty 版本。2.3 用 Python API 提取骨架并保存为 JSON环境跑通后提取骨架的代码其实很短。下面这段是我常用的最小提取脚本import cv2 import json import numpy as np from sys import platform import pyopenpose as op # OpenPose 参数配置 params { model_folder: /path/to/openpose/models/, # 模型权重目录 net_resolution: 368x368, # 输入网络分辨率 number_people_max: 1, # 最多检测人数单人动作设为1 render_pose: 0, # 不渲染可视化加速 } opWrapper op.WrapperPython() opWrapper.configure(params) opWrapper.start() # 读取视频并逐帧提取 cap cv2.VideoCapture(test_action.mp4) fps cap.get(cv2.CAP_PROP_FPS) skeleton_sequence [] while cap.isOpened(): ret, frame cap.read() if not ret: break datum op.Datum() datum.cvInputData frame opWrapper.emplaceAndPop(op.VectorDatum([datum])) if datum.poseKeypoints is not None: # poseKeypoints 形状: (人数, 关键点数, 3) 最后一维是 x,y,confidence keypoints datum.poseKeypoints[0] # 取第一个人 skeleton_sequence.append(keypoints.tolist()) else: skeleton_sequence.append(np.zeros((18, 3)).tolist()) cap.release() # 保存为 JSON后续训练直接读 with open(skeleton.json, w) as f: json.dump({fps: fps, skeleton: skeleton_sequence}, f)逻辑说明net_resolution控制精度和速度的平衡368x368 在 1080Ti 上大约能跑到 20-25 FPS如果显卡较弱可以降到 256x256。number_people_max1在单人动作场景下能减少后处理时间。每帧输出的是一个 (18, 3) 的数组18 个关键点按 COCO 顺序排列第三列是置信度。如果某帧没检测到人用全零填充保持序列长度一致。注意model_folder必须指向包含pose/coco/子目录的路径模型权重需要单独下载不在源码仓库里。3. 从骨架序列到动作标签数据预处理与分类模型训练3.1 骨架归一化让模型只看动作不看位置原始骨架坐标是像素值同一个人站在画面左边和右边坐标差异巨大但动作是一样的。如果不做归一化模型会学到「位置」而不是「动作」换个拍摄角度就翻车。常见的归一化策略是以髋关节中心为原点用肩宽或躯干长度做尺度归一化。import numpy as np def normalize_skeleton(sequence): sequence: (T, 18, 3) 的骨架序列 返回归一化后的 (T, 18, 2) 坐标 normalized [] for frame in sequence: kp frame[:, :2].copy() # 只取 x,y conf frame[:, 2] # 用置信度大于阈值的点计算中心 valid conf 0.1 if valid.sum() 4: normalized.append(np.zeros((18, 2))) continue # 髋关节中心COCO 索引 8 和 11 分别是左右髋 hip_center (kp[8] kp[11]) / 2.0 # 肩宽做尺度参考索引 2 和 5 是左右肩 shoulder_width np.linalg.norm(kp[2] - kp[5]) if shoulder_width 1e-6: shoulder_width 1.0 kp_norm (kp - hip_center) / shoulder_width normalized.append(kp_norm) return np.array(normalized) # (T, 18, 2)参数说明置信度阈值 0.1 是经验值设太高会导致遮挡帧被丢弃设太低会引入噪声。肩宽作为尺度参考比身高更稳定因为下半身关键点经常被遮挡。归一化后每个关节坐标大致在 [-2, 2] 范围内。3.2 动作分类模型选型ST-GCN 还是 LSTM骨架动作识别有两个主流方向基于图卷积的 ST-GCN 系列和基于循环网络的 LSTM/GRU 系列。ST-GCN 把人体骨架建模为图结构关节是节点骨骼是边通过图卷积捕捉空间关系通过时间卷积捕捉时序关系。LSTM 则把骨架序列当作时间序列直接建模。我的经验是数据量小于 5000 条时LSTM 或 GRU 更容易训起来收敛快调参少数据量上万后ST-GCN 的上限更高。对于「优质项目实战」这种场景我一般先用 LSTM 跑一个基线确认数据管线没问题再考虑换 ST-GCN 提精度。下面是一个基于 LSTM 的动作分类模型import torch import torch.nn as nn class SkeletonLSTM(nn.Module): def __init__(self, num_joints18, in_channels2, hidden_dim128, num_layers2, num_classes10, dropout0.3): super().__init__() # 输入: (batch, T, 18*2) - 展平关节坐标 self.input_dim num_joints * in_channels self.lstm nn.LSTM( input_sizeself.input_dim, hidden_sizehidden_dim, num_layersnum_layers, batch_firstTrue, dropoutdropout, bidirectionalTrue ) self.classifier nn.Sequential( nn.Linear(hidden_dim * 2, 64), # 双向所以乘2 nn.ReLU(), nn.Dropout(dropout), nn.Linear(64, num_classes) ) def forward(self, x): # x: (batch, T, 18, 2) - (batch, T, 36) batch, T, J, C x.shape x x.reshape(batch, T, J * C) lstm_out, _ self.lstm(x) # 取最后一个时间步的输出 last_out lstm_out[:, -1, :] return self.classifier(last_out)逻辑说明双向 LSTM 能同时利用过去和未来的帧信息对离线训练友好。如果要做在线实时推理需要改成单向。hidden_dim128和num_layers2是我在中小数据集上比较稳的配置再大容易过拟合。分类头用两层全连接加 Dropout输出类别数按你的动作类别调整。3.3 训练流程与关键超参数训练骨架分类模型和训练普通分类网络流程类似但有几个骨架特有的注意点。下面是一个完整的训练循环骨架from torch.utils.data import Dataset, DataLoader class SkeletonDataset(Dataset): def __init__(self, skeleton_list, labels, seq_len30): self.data skeleton_list self.labels labels self.seq_len seq_len def __len__(self): return len(self.data) def __getitem__(self, idx): seq self.data[idx] # (T, 18, 2) # 统一序列长度短补零长均匀采样 if len(seq) self.seq_len: pad np.zeros((self.seq_len - len(seq), 18, 2)) seq np.concatenate([seq, pad], axis0) elif len(seq) self.seq_len: indices np.linspace(0, len(seq)-1, self.seq_len).astype(int) seq seq[indices] return torch.FloatTensor(seq), self.labels[idx] # 训练配置 model SkeletonLSTM(num_classes10).cuda() optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max50) criterion nn.CrossEntropyLoss() for epoch in range(50): model.train() for batch_x, batch_y in train_loader: batch_x, batch_y batch_x.cuda(), batch_y.cuda() optimizer.zero_grad() logits model(batch_x) loss criterion(logits, batch_y) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() scheduler.step()参数说明seq_len30对应约 1 秒的动作片段30 FPS 下太短捕捉不到完整动作太长会引入无关帧。weight_decay1e-4是 LSTM 的常用正则化强度。梯度裁剪max_norm1.0对 LSTM 很重要防止梯度爆炸。学习率用余弦退火比固定学习率收敛更稳。提示如果类别不均衡比如「跌倒」样本远少于「走路」在 loss 里加weight参数按类别频率的倒数设置权重。4. 实时推理管线从摄像头到动作标签的工程化落地4.1 滑动窗口推理让实时识别不再等整段视频训练好的模型输入是固定长度序列但实时场景下视频流是无限的。常见做法是维护一个滑动窗口缓冲区每来一帧就推入缓冲区取最近 N 帧做一次推理。窗口大小和推理间隔需要权衡窗口太小动作信息不足窗口太大延迟高。from collections import deque class RealtimeActionRecognizer: def __init__(self, model, seq_len30, infer_interval5, num_classes10): self.model model self.model.eval() self.buffer deque(maxlenseq_len) self.seq_len seq_len self.infer_interval infer_interval self.frame_count 0 self.classes [faction_{i} for i in range(num_classes)] def update(self, keypoints): keypoints: (18, 3) 单帧骨架 self.buffer.append(keypoints) self.frame_count 1 # 缓冲区未满或未到推理间隔直接返回 if len(self.buffer) self.seq_len: return None if self.frame_count % self.infer_interval ! 0: return None # 构造输入 seq np.array(self.buffer) # (T, 18, 3) seq_norm normalize_skeleton(seq) # (T, 18, 2) tensor torch.FloatTensor(seq_norm).unsqueeze(0).cuda() with torch.no_grad(): logits self.model(tensor) probs torch.softmax(logits, dim1) pred torch.argmax(probs, dim1).item() conf probs[0, pred].item() return {action: self.classes[pred], confidence: conf}逻辑说明infer_interval5表示每 5 帧做一次推理中间帧复用上次结果这样能把推理开销降到 1/5。deque(maxlenseq_len)自动丢弃旧帧不用手动管理。置信度低于阈值时可以输出「不确定」而不是强行分类。4.2 端到端延迟优化哪些环节最耗时实时管线的延迟主要来自三块OpenPose 推理、骨架归一化、分类模型推理。实测下来 OpenPose 占 70% 以上。优化手段包括降低net_resolution、用 MobileNet 骨干替代 VGG、把 OpenPose 和分类模型放在不同 GPU 上流水线并行。环节典型耗时 (1080Ti)优化手段OpenPose 推理35-45ms降分辨率、换 MobileNet骨架归一化1ms无LSTM 分类2-3ms无视频解码5-10ms用 GPU 解码如果单卡跑不动可以把 OpenPose 部署在一台机器上通过 socket 把骨架数据发给分类服务这样两个环节互不阻塞。4.3 多动作切换的平滑处理实时识别有个体验问题动作切换的瞬间分类结果会在两个类别之间抖动。解决办法是对最近几次推理结果做投票或加权平均。我一般用简单多数投票窗口大小取 3-5 次推理结果能明显减少闪烁。from collections import Counter class SmoothPredictor: def __init__(self, window5): self.history deque(maxlenwindow) def smooth(self, action): self.history.append(action) if len(self.history) 3: return action # 多数投票 counter Counter(self.history) return counter.most_common(1)[0][0]这个逻辑很简单但效果显著。注意窗口不要设太大否则动作切换会有明显延迟。5. 避坑与排查骨架动作识别里最容易翻车的 5 个地方5.1 关键点抖动导致分类结果跳变现象同一个人做同一个动作分类结果在相邻帧之间反复跳变。原因OpenPose 逐帧独立推理关键点坐标存在高频抖动LSTM 对这种噪声敏感。解决对骨架序列做时序平滑常用的是指数移动平均或 Savitzky-Golay 滤波。我一般用 EMAalpha 取 0.3-0.5实现简单且延迟低。5.2 训练集和推理时的归一化不一致现象训练时准确率 95%部署后准确率掉到 60%。原因训练时用了某套归一化参数推理时忘了同步或者归一化逻辑写了两份不一致的代码。解决把归一化封装成一个函数或类训练和推理共用同一份代码。这是血泪经验我见过不止一个项目栽在这。5.3 序列长度对齐方式选错现象模型对快动作识别好对慢动作识别差。原因训练时用均匀采样把不同长度序列对齐到固定长度慢动作被压缩后丢失了关键帧。解决慢动作用更大的seq_len或者改用基于 DTW 的对齐方式。简单做法是按动作时长分桶不同桶用不同seq_len。5.4 置信度低的关节没做处理现象遮挡场景下分类完全失效。原因OpenPose 对遮挡关节输出的坐标是估计值置信度很低但坐标看起来「正常」模型无法区分。解决把置信度作为额外通道输入模型或者在归一化时对低置信度关节做特殊标记比如置零。我一般把置信度拼到坐标后面变成 (T, 18, 3) 输入。5.5 实时推理时 GPU 显存泄漏现象跑了几分钟后程序崩溃报 CUDA out of memory。原因每帧都创建新的 CUDA tensor 但没有释放或者 OpenPose 的 datum 对象没复用。解决用torch.no_grad()包裹推理复用 datum 对象定期调用torch.cuda.empty_cache()。如果还不行检查是不是在循环里反复创建了 OpenPose wrapper。6. 进阶技巧用迁移学习把小样本动作识别做到可用实际项目里标注数据往往只有几百条从头训 LSTM 很容易过拟合。我的做法是先在公开骨架数据集比如 NTU RGBD 的子集上预训练再在自己的数据上微调。微调时冻结 LSTM 层只训分类头学习率降到 1e-4通常 10 个 epoch 就能收敛。另一个技巧是数据增强。骨架序列的增强手段比图像少但有几个很有效时间轴上的随机缩放模拟快慢动作、关节坐标加高斯噪声模拟关键点抖动、随机旋转模拟拍摄角度变化。下面是一个简单的增强函数def augment_skeleton(seq, noise_std0.02, scale_range(0.9, 1.1)): seq: (T, 18, 2) # 时间缩放 scale np.random.uniform(*scale_range) T seq.shape[0] new_T int(T * scale) indices np.linspace(0, T-1, new_T).astype(int) seq seq[indices] # 加噪声 noise np.random.normal(0, noise_std, seq.shape) seq seq noise return seq验证方法上我习惯用留一人交叉验证leave-one-subject-out而不是随机划分。因为随机划分会让同一个人的不同片段同时出现在训练集和测试集准确率虚高。留一人验证更接近真实部署场景虽然数字会低一些但更可信。最后说一个我自己的习惯每次训完模型我都会用混淆矩阵看哪些类别容易混。通常「坐下」和「蹲下」、「挥手」和「举手」是最容易混的这时候要么加更多区分性特征比如关节角度要么合并这两个类别。不要硬调参去刷那几个点的准确率先把类别定义理清楚。希望帮到你。本文还有配套的精品资源点击获取