
简介这份资源面向计算机视觉入门者与深度学习实践者提供一套基于卷积神经网络的人体姿态与动作识别Python实现帮助读者理解从骨骼点提取到模型训练、测试的完整流程。压缩包共6个文件包含5个py源码与1份md项目说明整体约7KB其中py文件分别承担姿态检测、动作数据采集、CNN模型训练与测试等职责md文档用于梳理项目结构与使用说明。项目以mediapipe定位人体骨骼点并绘制标注再借助OpenCV保存图像构建训练数据最终交由卷积网络完成动作分类模块划分清晰便于按步骤阅读与二次开发。目前已有471人学习下载适合希望快速上手姿态识别、动作分类小项目或课程设计参考的读者可从中获取可运行的代码骨架、模块协作思路与训练测试流程为后续扩展数据集或更换网络结构打下基础。1. 从一段监控视频说起CNN 人体姿态与动作识别到底在解决什么假设你手头有一段健身房深蹲的监控视频教练想知道学员膝盖有没有内扣、下蹲深度够不够。传统做法是人工逐帧看费时费力还容易漏。基于 CNN 的人体姿态和动作识别要做的就是从原始画面里自动定位关节点、还原骨架再根据骨架时序判断当前在做哪个动作。它解决的核心问题是把「人做了什么」从像素级视频变成结构化的坐标序列和动作标签。这套方案适合三类人做体育动作评分的开发者、做安防异常行为检测的工程师、以及想入门计算机视觉的学生。标题里提到的 python 源码加项目说明文档本质上是一份可运行的参考实现帮你跳过从零搭网络结构的阶段。但源码不是银弹姿态估计和动作识别是两个可以独立拆开的子任务理解它们的边界比跑通代码更重要。这一章先把概念立住后面几章再动手。2. 姿态估计与动作识别的技术选型为什么是 CNN 而不是别的2.1 两条技术路线自上而下与自下而上人体姿态估计的主流方案分两派。自上而下先用目标检测框出每个人再对每个框单独做关键点回归代表思路是 Mask R-CNN 加关键点头。优点是精度高缺点是人数越多耗时线性增长。自下而上先检测出画面里所有关键点再用聚类算法把关键点组装成不同人的骨架代表思路是 OpenPose 系列。优点是速度与人数无关缺点是拥挤场景下聚类容易出错。选哪条路取决于你的场景。单人健身动作分析自上而下足够且精度更好广场人群行为分析自下而上更稳。源码项目里如果用的是单人姿态估计大概率走的是轻量级 CNN 直接回归坐标的路线比如基于 MobileNet 或 ResNet 做骨干网络后面接几个反卷积层输出热力图。2.2 从 2D 骨架到动作分类时序建模的三种做法拿到每帧的 2D 关键点坐标后动作识别就变成了时序分类问题。常见做法有三种第一种是把骨架序列画成热力图堆叠直接喂给 3D CNN让网络自己学时空特征。这种做法数据需求量大但端到端省心。第二种是用 LSTM 或 GRU 处理坐标序列把每帧的关节点坐标当成一个特征向量按时间步输入循环网络。这种做法参数量小适合小数据集。第三种是把骨架转成图结构用图卷积网络GCN建模关节之间的连接关系ST-GCN 就是典型代表。源码项目如果标题只写了 CNN大概率用的是第一种或第二种的简化版。你拿到代码后先看模型定义文件确认输入是热力图还是坐标序列这决定了你后续换数据集时要怎么预处理。2.3 骨干网络怎么选精度与速度的权衡骨干网络参数量级适用场景注意事项MobileNetV2约 3.4M移动端、实时推理精度略低需数据增强补ResNet50约 25M服务器端、精度优先显存占用大batch 要调小VGG16约 138M教学演示推理慢不建议落地EfficientNet-B0约 5.3M平衡型输入分辨率敏感我一般会先用 ResNet50 跑一版看精度上限再换 MobileNetV2 看速度能不能接受。如果换完后精度掉超过 5 个百分点说明你的数据集对细粒度特征依赖强这时候要么加数据要么换更深的网络。2.4 用 Python 加载模型并跑通单帧推理下面这段代码演示如何用 PyTorch 加载一个预训练的姿态估计模型并对单张图片推理。注意这里用的是 torchvision 里的 keypointrcnn_resnet50_fpn它是自上而下的典型实现。import torch import torchvision from PIL import Image import torchvision.transforms as T # 加载预训练的姿态估计模型 model torchvision.models.detection.keypointrcnn_resnet50_fpn(pretrainedTrue) model.eval() # 读取图片并转成张量 img Image.open(test.jpg).convert(RGB) transform T.Compose([T.ToTensor()]) img_tensor transform(img).unsqueeze(0) # 增加 batch 维度 # 推理 with torch.no_grad(): outputs model(img_tensor) # outputs[0][keypoints] 形状为 [N, 17, 3]N 是检测到的人数 # 17 个关键点对应 COCO 数据集定义的鼻子、眼睛、肩膀等 keypoints outputs[0][keypoints] scores outputs[0][scores] print(检测到人数:, keypoints.shape[0]) print(第一人关键点坐标:, keypoints[0][:, :2])逻辑说明模型输出是一个列表每个元素对应一张图。keypoints 的第三个维度是 3分别代表 x 坐标、y 坐标和置信度。置信度低于 0.5 的点建议丢弃否则骨架会抖动。参数方面pretrainedTrue 会下载约 220MB 的权重文件第一次运行需要联网。如果显存不够把 img_tensor 转成半精度或缩小输入尺寸。2.5 动作识别的最小训练循环假设你已经把骨架序列整理成了形状为 [样本数, 帧数, 关节点数*2] 的数组下面是一个用 LSTM 做动作分类的训练骨架。import torch.nn as nn import torch.optim as optim class ActionLSTM(nn.Module): def __init__(self, input_dim, hidden_dim, num_classes): super().__init__() self.lstm nn.LSTM(input_dim, hidden_dim, batch_firstTrue) self.fc nn.Linear(hidden_dim, num_classes) def forward(self, x): # x: [batch, frames, input_dim] out, _ self.lstm(x) out out[:, -1, :] # 取最后一个时间步 return self.fc(out) model ActionLSTM(input_dim34, hidden_dim128, num_classes10) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-3) for epoch in range(50): for batch_x, batch_y in train_loader: optimizer.zero_grad() pred model(batch_x) loss criterion(pred, batch_y) loss.backward() optimizer.step()input_dim 等于关节点数乘以 2COCO 的 17 个点就是 34。hidden_dim 从 128 起步如果欠拟合加到 256过拟合就降到 64。帧数建议统一采样到 30 或 60 帧太短丢失动作信息太长 LSTM 记不住。3. 把源码跑起来环境配置与数据准备的具体步骤3.1 Python 环境与依赖安装的版本坑拿到源码压缩包后第一件事不是急着 pip install而是看项目说明文档里有没有 requirements.txt。如果没有按下面这个顺序装能避开大部分版本冲突。# 建议用 conda 建独立环境避免污染系统 Python conda create -n pose_action python3.8 -y conda activate pose_action # 先装 PyTorch注意 CUDA 版本要和显卡驱动匹配 pip install torch1.12.1 torchvision0.13.1 --extra-index-url https://download.pytorch.org/whl/cu113 # 再装 OpenCV用于视频读取和画骨架 pip install opencv-python4.6.0.66 # 最后装其他依赖 pip install numpy pandas matplotlib tqdm scikit-learn为什么锁 python 3.8因为很多姿态估计的源码依赖 pycocotools这个包在 3.9 以上编译容易报错。opencv-python 锁 4.6 是因为 4.7 之后部分 API 有变动老代码里的 cv2.findContours 返回值数量变了会直接报 ValueError。3.2 数据集目录结构怎么组织源码项目通常期望特定的目录结构。以常见的动作识别数据集为例我一般会整理成下面这样dataset/ ├── train/ │ ├── squat/ │ │ ├── 001.mp4 │ │ └── 002.mp4 │ └── pushup/ │ ├── 001.mp4 │ └── 002.mp4 ├── val/ │ ├── squat/ │ └── pushup/ └── labels.csvlabels.csv 里两列文件名和类别编号。如果你的原始数据是骨架坐标而不是视频就把 mp4 换成 npy 文件每个文件存一个 [帧数, 34] 的数组。注意帧数不一致时要么统一截断到最短要么用插值补齐否则 DataLoader 会报错。3.3 用 OpenCV 提取骨架并保存为训练数据下面这段代码把视频逐帧跑姿态估计把关键点坐标存成 npy 文件供后续动作分类使用。import cv2 import numpy as np import os def extract_keypoints(video_path, model, transform, max_frames60): cap cv2.VideoCapture(video_path) frames [] while len(frames) max_frames: ret, frame cap.read() if not ret: break img cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) img_tensor transform(img).unsqueeze(0) with torch.no_grad(): output model(img_tensor) kps output[0][keypoints] if kps.shape[0] 0: frames.append(np.zeros(34)) # 没检测到人就补零 else: # 取置信度最高的人 best kps[0][:, :2].reshape(-1).numpy() frames.append(best) cap.release() # 不足 max_frames 的用最后一帧补齐 while len(frames) max_frames: frames.append(frames[-1] if frames else np.zeros(34)) return np.array(frames) # 批量处理 for label in os.listdir(dataset/train): for video in os.listdir(fdataset/train/{label}): path fdataset/train/{label}/{video} kps extract_keypoints(path, model, transform) np.save(fdataset/train/{label}/{video}.npy, kps)逻辑说明max_frames 控制每个视频采样的帧数统一长度才能组成 batch。没检测到人的帧补零训练时模型会学到零向量对应无动作。置信度最高的人用 kps[0] 取因为模型输出按得分排序。参数方面如果视频帧率很高可以每隔 2 帧取 1 帧减少冗余。3.4 训练脚本的关键参数怎么调源码里的 train.py 通常有一堆 argparse 参数。我挑几个最影响结果的说明batch_size显存 8G 用 1612G 用 32。太小梯度震荡太大泛化差。learning_rateAdam 用 1e-3 起步SGD 用 1e-2。如果 loss 前三轮不降除以 10。epochs小数据集 50 轮够大数据集 100 轮以上。看验证集 loss 连续 10 轮不降就停。weight_decay1e-4 防过拟合数据量少于 1000 样本时加到 1e-3。跑之前先把 batch_size 设成 2 试一轮确认不报显存错误再往上加。这是血泪经验直接设 64 大概率 OOM。4. 避坑与排查姿态动作识别项目里最容易翻车的五个地方4.1 关键点抖动导致动作误判现象视频里人站着不动但骨架在帧间跳来跳去动作分类结果频繁切换。原因单帧姿态估计没有时序平滑CNN 对遮挡和光照变化敏感相邻帧的关节点坐标可能差几十像素。解决加一个滑动窗口平均滤波或者用 One Euro Filter 做自适应平滑。简单做法是对每个关节点的 x、y 坐标做窗口为 5 的移动平均。如果动作分类还是不稳把分类器的输入从单帧改成 5 帧堆叠让模型看到上下文。4.2 训练集和测试集来自同一批人导致精度虚高现象验证集准确率 98%换一批新视频测试掉到 60%。原因数据划分时按视频随机分同一个人既出现在训练集又出现在测试集模型记住了这个人的体型和穿着没学到动作本质。解决按人划分数据集同一个人只能出现在训练或测试其中一边。如果数据里没有人员 ID至少按拍摄日期或场景分。这个坑在学术数据集上不明显一到实际项目就暴露。4.3 OpenCV 读视频返回空帧现象cap.read() 一直返回 False或者读几帧后就断了。原因视频编码格式不被 OpenCV 支持或者文件路径含中文或者 ffmpeg 后端没装。解决先确认 cv2.getBuildInformation() 里 FFMPEG 是 YES。路径全改英文。如果还不行用 imageio 或 decord 替代读取。另外注意 cv2.VideoCapture 的第二个参数可以指定后端cv2.CAP_FFMPEG 通常最稳。4.4 类别不平衡让模型只预测多数类现象训练 loss 正常下降但混淆矩阵显示所有样本都被预测成同一个类别。原因某个动作的样本数远多于其他动作交叉熵损失被多数类主导。解决在 CrossEntropyLoss 里加 weight 参数按类别频率的倒数赋值。或者用 WeightedRandomSampler 在 DataLoader 层面重采样。如果某个类样本少于 50建议先补数据加权只能缓解不能根治。4.5 推理速度慢到无法实时现象单帧推理超过 200ms视频处理比实时慢好几倍。原因模型没转 eval 模式或者没加 torch.no_grad()或者输入分辨率太大。解决推理前必写 model.eval() 和 torch.no_grad()。输入从 1080p 降到 640x480速度能快 3 倍。如果还慢用 torch.jit.trace 导出 TorchScript或者转 ONNX 用 onnxruntime 跑CPU 上也能提速 2 到 3 倍。5. 进阶技巧用迁移学习和骨架归一化把精度再提一截5.1 迁移学习怎么用才不白费源码项目如果提供了预训练权重别从头训。加载权重后先冻结骨干网络只训最后的分类头 10 轮再解冻全部微调 20 轮。这样收敛快且不容易过拟合。如果源码没给权重用 torchvision 里 ImageNet 预训练的 ResNet 做骨干初始化即使任务不同浅层特征也能复用。# 冻结骨干 for param in model.backbone.parameters(): param.requires_grad False # 只训分类头 optimizer optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr1e-3) # 10 轮后解冻 for param in model.backbone.parameters(): param.requires_grad True optimizer optim.Adam(model.parameters(), lr1e-4) # 学习率降 10 倍5.2 骨架归一化让模型不看体型只看动作不同人的骨架尺度差异很大高个子和矮个子的关节点坐标绝对值差很多。把骨架归一化到统一尺度能显著提升跨人泛化。做法是选一个参考点通常是髋部中心把所有关节点坐标减去参考点再除以躯干长度。def normalize_skeleton(kps): # kps: [frames, 17, 2] hip_center (kps[:, 11, :] kps[:, 12, :]) / 2 # 左右髋中点 shoulder_center (kps[:, 5, :] kps[:, 6, :]) / 2 torso_length np.linalg.norm(shoulder_center - hip_center, axis1, keepdimsTrue) torso_length np.maximum(torso_length, 1e-6) # 防除零 normalized (kps - hip_center[:, None, :]) / torso_length[:, None, None] return normalized归一化后模型学到的就是相对姿态换个人做同样动作输入分布一致。这个技巧在跨数据集测试时效果尤其明显我试过归一化前后跨人准确率能差 15 个百分点。5.3 验证模型是否真的学到了动作别只看准确率。用混淆矩阵看哪些动作容易混比如深蹲和硬拉在骨架序列上确实相似模型分错情有可原。再做一个消融实验把骨架序列的时间顺序打乱如果准确率不掉说明模型根本没学时序只看了单帧姿态。这时候要检查 LSTM 的输入维度是不是搞错了或者帧数设得太少。我一般会保留一个「傻瓜基线」用 SVM 对每帧骨架做分类再取多数投票。如果 CNN 模型比这个基线高不到 5 个百分点说明网络结构没发挥价值不如用轻量方案。这个习惯帮我省过好几次盲目调参的时间。希望帮到你。本文还有配套的精品资源点击获取