资讯详情

基于CNN的人体姿态估计与动作识别:关键点检测与分类实战

📅 2026/9/12 22:45:34 | 华诺云谱 👁 阅读
基于CNN的人体姿态估计与动作识别:关键点检测与分类实战
简介基于CNN深度学习的人体姿态与动作识别系统是一份可直接运行学习的Python源码项目主要面向正在准备毕业设计、课程设计或期末大作业的计算机相关专业学生也适合对计算机视觉与深度学习感兴趣的开发者作为实战练习。资源内共6个文件包含5个Python脚本和1个Markdown说明文档整体仅7KB代码结构紧凑。脚本覆盖姿态检测、模型训练、动作数据采集、模型测试等核心环节并配有项目说明便于快速理解工程脉络与各模块职责。该方案经导师指导并评审通过代码已测试可稳定运行。目前已有205人学习下载适合在现有基础上修改拓展以适配不同的动作识别场景也可作为CNN网络在人体姿态分析中应用的入门参考。项目中融合了深度学习与图像处理的关键思路能帮助读者快速掌握从数据准备到模型部署的基本流程兼具学习与二次开发价值。1. 人体姿态和动作识别CNN能解决什么解决不了什么一个健身应用要自动数出用户做了多少次深蹲一个康复系统要判断患者是否完成了指定的抬手动作路径大概都是先把人从画面中“拆”成关键点再对关键点序列做分类。前者叫姿态估计后者叫动作识别。基于CNN深度学习识别人体姿态和动作系统核心就是这条流水线用深度卷积神经网络检测人体关键点再让另一个CNN从关键点序列里分辨动作类别。标题里那份源码包的常见结构也逃不出这两个模块。CNN在这条链路里的角色非常明确在姿态估计阶段它从原始像素回归出肩、肘、腕、膝等关节的坐标或热图在动作识别阶段它接收的是关键点组成的结构特征而不是原始视频帧。这个拆分直接决定了代码怎么写、模型怎么训练、推理怎么加速。一个常被忽略的事实是姿态估计的精度直接决定动作分类的上限而动作分类的数据组织方式又决定了CNN是看单帧还是看时间窗口。下文按“理论选型—最小实现—参数调优—部署验证”这四步展开每一步都给出可复制的Python代码和命令。2. 从关键点到动作分类CNN视角下的两个子问题2.1 姿态估计的输出之争坐标回归还是热图回归CNN做姿态估计输出层的设计有两种主流选择。第一种是直接回归关键点的像素坐标比如输出(17, 2)的张量代表17个关节的x、y坐标。这种方案网络简单但坐标回归的损失函数对尺度敏感且难从全局特征中稳定收敛。第二种是生成热图heatmap对每个关键点输出一张和输入尺寸成比例的概率图每个像素值表示该位置是关键点的置信度。热图天然保留空间结构例如肩、肘、腕三者的相对位置关系会被卷积层隐式建模。训练时真实关键点坐标会被转换成一个二维高斯分布绘制到热图上比如使用σ 2像素的高斯核。推理时再在热图上取最大值所在的位置解码成坐标。热图方案虽然多了一次解码操作但小模型也能达到可用的精度这也解释了为什么OpenPose、HRNet这些经典框架都采用热图回归。你的源码包里如果姿态部分用的是回归坐标那通常是为了极快的推理代价是精度受边界框裁剪影响较大。2.2 动作识别的输入单帧、多帧序列还是关键点序列动作识别有三种数据组织方式。第一种是单帧图像直接让CNN分类当前画面里的动作适合“站立”“坐着”这种静态姿势。第二种是连续视频帧使用3D CNN或结合循环网络优点是利用时间上下文但计算量成倍增加对训练数据量要求高。第三种是先把每帧的关键点提取出来组织成时间序列再用1D CNN或LSTM分类。这种方案计算量最小且因为输入已经去掉了背景和人物纹理模型泛化能力更依赖动作本身的结构信息。我在实际项目中优先选择第三种方式。原因在于视频帧级的CNN分类需要大量标注好的视频片段而关键点序列只需要你现有的姿态标注就能生成。UCF101这类数据集虽然有视频动作标签但直接训练3D CNN对个人项目而言硬件门槛太高。反之把MediaPipe或HRNet输出的关键点坐标缓存成CSV文件再用一个参数不超过几万的小CNN就能在CPU上跑出可接受的结果。这份源码包的核心价值也正是在这里教你如何绕过姿态估计的预训练模型专注于动作分类的部分。2.3 网络结构选型ResNet、MBConv与注意力机制姿态估计阶段的骨干网络经典选择是ResNet50或MobileNetV3。ResNet50的残差结构能避免深层网络退化适合在GPU上训练MobileNetV3使用深度可分离卷积和压缩激励模块SE在CPU上推理延迟更低。动作分类阶段由于输入是关键点序列形状通常是(序列长度, 关键点个数 * 坐标维度)不需要堆叠太深的卷积层。常见做法是使用两层1D卷积加全局平均池化再接全连接层。以ResNet50为例它由4个Stage组成每个Stage包含多个残差块输出尺寸逐步减半通道数逐步增加。在姿态估计任务中通常会在最后一个Stage后增加一个转置卷积层把特征图分辨率恢复到适合预测热图的尺度。HRNet则采用了多分支并行、反复交换信息的结构能同时保持高分辨率和丰富语义但参数量、显存占用也水涨船高。选择哪一款核心取决于你的运行环境如果目标是实时摄像头推理MobileNetV3热图回归是平衡点如果追求精度处理离线视频HRNet更适合。3. 最小可运行系统Python实现关键点提取与CNN动作分类3.1 环境准备假设你使用Python 3.8及以上环境需要安装PyTorch、OpenCV、MediaPipe和NumPy。建议使用conda创建独立环境conda create -n pose_action python3.8 conda activate pose_action pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu pip install opencv-python mediapipe numpy scikit-learn如果你有NVIDIA显卡把上面安装PyTorch的指令换成pip install torch torchvision默认会安装带CUDA的版本。MediaPipe负责姿态关键点提取它是一个基于轻量级CNN模型的推理框架内部结构你不需要改动直接调用接口即可。注意MediaPipe的Python包对版本较敏感建议固定使用mediapipe0.10.7或你本地测试通过的版本。3.2 用MediaPipe提取姿态关键点下面这段代码从一张图片或一帧视频中提取33个关键点坐标并保存为数组import cv2 import mediapipe as mp mp_pose mp.solutions.pose pose mp_pose.Pose( static_image_modeFalse, model_complexity1, smooth_landmarksTrue, min_detection_confidence0.5, min_tracking_confidence0.5 ) def extract_landmarks(frame): rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) result pose.process(rgb) if not result.pose_landmarks: return None # 33个关键点的x,y坐标归一化到0~1忽略z轴和可见度 lm result.pose_landmarks.landmark coords [(p.x, p.y) for p in lm] return coords cap cv2.VideoCapture(0) # 或改为视频文件路径 sequence [] while cap.isOpened(): ret, frame cap.read() if not ret: break coords extract_landmarks(frame) if coords: sequence.append(coords) # 按帧累积 cap.release() np.save(sequence.npy, sequence)参数说明model_complexity1表示使用中等复杂度的姿态模型0最快但精度低2最慢但精度最高。min_detection_confidence是检测阶段的最小置信度如果人物有遮挡可降到0.4min_tracking_confidence是跟踪阶段的最小置信度当帧率低或运动快时降低它可减少关键点的跳变。smooth_landmarks开启时间平滑能抑制抖动但会引入约100毫秒的延迟实时性要求高时建议关闭。提取后的sequence是一个列表每个元素是(33, 2)的二维数组。注意如果你的动作分类模型需要固定的输入长度例如10帧一个动作需要做重采样或滑窗。3.3 构建轻量级1D CNN动作分类器现在定义动作分类模型。输入形状为(batch, sequence_len, 66)其中66表示33个坐标的x和y拼接。模型使用两层1D卷积import torch import torch.nn as nn class PoseActionCNN(nn.Module): def __init__(self, num_classes4, sequence_len10): super().__init__() self.conv1 nn.Conv1d(in_channels66, out_channels128, kernel_size3, padding1) self.conv2 nn.Conv1d(in_channels128, out_channels256, kernel_size3, padding1) self.pool nn.AdaptiveAvgPool1d(1) self.fc nn.Linear(256, num_classes) def forward(self, x): # x: (batch, sequence_len, 66) - 转成 (batch, 66, sequence_len) x x.permute(0, 2, 1) x torch.relu(self.conv1(x)) x torch.relu(self.conv2(x)) x self.pool(x).squeeze(-1) return self.fc(x)这里Conv1d的in_channels66表示把每个关键点的坐标维度当作通道卷积核在时间序列维度上滑动这样模型能捕捉到关键点坐标在一段时间内的连续变化模式例如手臂摆动时肘关节x坐标的周期性上升和下降。AdaptiveAvgPool1d(1)把卷积后的时间特征压缩成一个固定长度的向量保证全连接层输入尺寸不随序列长度改变。训练时加载你之前保存的sequence.npy并准备对应的动作标签。以下是一个简化的训练循环from sklearn.model_selection import train_test_split import numpy as np X np.load(sequence.npy) # 假设已经聚合了多个样本 y np.array([0, 1, 2, 3] * (len(X)//4)) # 替换为你的真实标签 X_train, X_val, y_train, y_val train_test_split(X, y, test_size0.2, stratifyy) train_data torch.tensor(X_train, dtypetorch.float32) train_labels torch.tensor(y_train, dtypetorch.long) val_data torch.tensor(X_val, dtypetorch.float32) val_labels torch.tensor(y_val, dtypetorch.long) model PoseActionCNN(num_classes4) optimizer torch.optim.Adam(model.parameters(), lr1e-3) loss_fn nn.CrossEntropyLoss() for epoch in range(50): model.train() optimizer.zero_grad() output model(train_data) loss loss_fn(output, train_labels) loss.backward() optimizer.step() if epoch % 10 0: model.eval() with torch.no_grad(): val_output model(val_data) val_acc (val_output.argmax(1) val_labels).float().mean() print(fEpoch {epoch}, loss: {loss.item():.4f}, val_acc: {val_acc:.4f})训练时注意一个关键细节train_data的X_train必须确保每个样本的序列长度一致。如果从视频中提取的关键点数量不同需要截取或填充到固定长度。这里默认sequence_len为10如果实际的X形状不是10帧请在送入模型前用np.concatenate或插值处理。3.4 推理与动作可视化推理时对每个滑窗里的关键点序列进行分类并标注动作名称def predict_action(sequence, model): # sequence: (seq_len, 33, 2) - (seq_len, 66) seq sequence.reshape(sequence.shape[0], -1) if len(seq) 10: # 简单填充: 重复最后一帧 pad np.repeat(seq[-1:], 10 - len(seq), axis0) seq np.concatenate([seq, pad], axis0) elif len(seq) 10: seq seq[:10] seq_tensor torch.tensor(seq, dtypetorch.float32).unsqueeze(0) model.eval() with torch.no_grad(): logits model(seq_tensor) action_id logits.argmax(1).item() return action_id这里采用了最简单的滑窗策略每10帧一个窗口不足10帧时重复最后一帧补足。在实际项目中你还需要判断当前窗口是否包含有效动作比如通过计算关键点坐标的方差如果方差过小说明人体基本静止不触发分类。model是前面训练好的模型调用前确保已加载权重model.load_state_dict(torch.load(pose_action.pth))。4. 关键参数、训练技巧与高频踩坑4.1 数据归一化不要直接把像素坐标喂给CNN关键点坐标通常归一化到[0, 1]区间但人物的身高和位置会直接影响坐标分布。站在远处的人和站在近处的人即使做同一个动作关键点坐标差异也很大。常见做法是消除位置和尺度信息以臀部中点为原点用肩宽或躯干长度缩放坐标。这样CNN的输入就不再是绝对坐标而是相对位置关系模型的泛化能力会明显提升。比如对每个时间步先计算左肩和右肩的中点作为参考点然后把所有关键点减去参考点再除以肩宽即左右肩的欧氏距离。这个预处理在训练和推理时都必须使用完全相同的参数。你可以在训练前写一个函数应用到所有样本上并在推理代码中同样调用。4.2 序列长度、帧率与动作分段动作识别的核心参数是序列长度。太短的序列无法覆盖一个完整动作比如“坐下”从开始到下蹲到起立可能占用2秒太长的序列又会引入大量无关帧导致分类器混淆。如果视频帧率是30fps我一般用30帧覆盖1秒的动作。但这里要区分动作的粒度完整动作一次深蹲用30帧连续动作行走、跑步则建议用10帧因为周期性动作可以靠短窗口识别过长反而会让卷积特征包含两个周期。另一个容易忽视的点是动作分段。在一个真实视频里你可能连续做多个动作中间有停顿。纯滑窗方式会在动作边界出现错误分类。较实用的方案是引入一个“动作检测”前置当连续若干帧中人体中心点位移超过一个阈值时认为动作开始并记录关键点序列当位移低于阈值持续若干帧认为动作结束。之后只把这段序列送进分类器。4.3 小样本与类别不平衡的处理个人项目的动作分类常常每个类别只有几十个样本。此时直接用全连接层容易过拟合调低学习率也只是减缓症状。有效手段包括数据增强对关键点坐标做随机噪声、缩放、旋转保持关节相对位置不变比如对坐标乘以1 np.random.uniform(-0.05, 0.05)。冻结骨干如果模型不止一个模块可以先用大量无标注数据自监督预训练姿态模型然后只训练分类头。使用类别权重在CrossEntropyLoss中传入weight参数让少数类别的梯度占比更高。此外验证时不要只用准确率至少看混淆矩阵。如果“站立”和“下蹲”经常混淆优先去检查这两类样本关键点的差异是否被归一化步骤抹掉了。比如下蹲时臀部中心明显下移如果你用了以臀部为原点的归一化臀部本身的绝对位置信息就丢失反而导致这两个动作变得相似。因此归一化参考点要慎重我通常选择双肩中点而不是臀部中点。4.4 常见运行时报错与处理报错现象可能原因解决办法MediaPipe报TypeError: __init__() got an unexpected keyword argument版本过旧或过新固定安装mediapipe0.10.7关键点坐标全为nan输入图像全黑或人体完全被遮挡检查pose.process调用前是否将BGR转RGB训练时loss不下降数据未归一化或标签与样本错位检查train_test_split的random_state并固定打印几个样本的坐标范围模型推理时CPU占用过高model.complexity2且同时跑多个摄像头在推理时关闭smooth_landmarks并降低输入分辨率上述排错顺序是先看数据再看模型最后看环境。很多新手一上来就调网络结构却忽略了数据对齐和归一化的一致性。5. 用混淆矩阵验证姿态动作识别系统的真实性能最后提一个被多数教程跳过但极其重要的验证技巧用混淆矩阵而不是一行准确率数字来评估你的模型。准确率在类别不平衡时极具欺骗性比如“站立”样本占80%模型全猜站立也有80%的准确率。正确做法是在训练结束后把验证集每个样本的预测结果和真实标签保存下来绘制混淆矩阵。import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay # 假设 model, val_data, val_labels 已经存在 model.eval() with torch.no_grad(): preds model(val_data).argmax(1).numpy() cm confusion_matrix(val_labels.numpy(), preds) disp ConfusionMatrixDisplay(cm, display_labels[stand, sit, squat, walk]) disp.plot(cmapBlues) plt.savefig(confusion_matrix.png, dpi150)从混淆矩阵里能直接看出哪些类别对容易混淆。如果“站立”和“行走”经常互判大概率是行走的样本里包含了较多近似静止的瞬间你的滑窗策略没有排除掉这些帧。这时候调分类器没有用要回到数据预处理中把行走动作定义成连续位移超过阈值的关键点序列或者把当前帧与前一帧的关键点位移量作为额外通道加入输入。更进一步的验证方法是做留一交叉验证Leave-One-Subject-Out即同一个人的全部样本只能出现在训练集或测试集不能同时存在。很多项目的动作分类准确率虚高是因为同一个人既出现在训练集又出现在测试集模型记住了人而不是记住了动作。用测试群体外的新人数据做最终评估才能反映真实落地效果。你的源码包里如果有评估脚本优先看它是否按人分组切分而不是随机切分帧序列。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。