news 2026/9/12 22:45:32

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于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 python=3.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包对版本较敏感,建议固定使用mediapipe==0.10.7或你本地测试通过的版本。

3.2 用MediaPipe提取姿态关键点

下面这段代码从一张图片或一帧视频中提取33个关键点坐标,并保存为数组:

import cv2 import mediapipe as mp mp_pose = mp.solutions.pose pose = mp_pose.Pose( static_image_mode=False, model_complexity=1, smooth_landmarks=True, min_detection_confidence=0.5, min_tracking_confidence=0.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_complexity=1表示使用中等复杂度的姿态模型,0最快但精度低,2最慢但精度最高。min_detection_confidence是检测阶段的最小置信度,如果人物有遮挡,可降到0.4;min_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_classes=4, sequence_len=10): super().__init__() self.conv1 = nn.Conv1d(in_channels=66, out_channels=128, kernel_size=3, padding=1) self.conv2 = nn.Conv1d(in_channels=128, out_channels=256, kernel_size=3, padding=1) 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)

这里Conv1din_channels=66表示把每个关键点的坐标维度当作通道,卷积核在时间序列维度上滑动,这样模型能捕捉到关键点坐标在一段时间内的连续变化模式,例如手臂摆动时肘关节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_size=0.2, stratify=y) train_data = torch.tensor(X_train, dtype=torch.float32) train_labels = torch.tensor(y_train, dtype=torch.long) val_data = torch.tensor(X_val, dtype=torch.float32) val_labels = torch.tensor(y_val, dtype=torch.long) model = PoseActionCNN(num_classes=4) optimizer = torch.optim.Adam(model.parameters(), lr=1e-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(f"Epoch {epoch}, loss: {loss.item():.4f}, val_acc: {val_acc:.4f}")

训练时注意一个关键细节:train_dataX_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), axis=0) seq = np.concatenate([seq, pad], axis=0) elif len(seq) > 10: seq = seq[:10] seq_tensor = torch.tensor(seq, dtype=torch.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版本过旧或过新固定安装mediapipe==0.10.7
关键点坐标全为nan输入图像全黑或人体完全被遮挡检查pose.process调用前是否将BGRRGB
训练时loss不下降数据未归一化,或标签与样本错位检查train_test_splitrandom_state并固定,打印几个样本的坐标范围
模型推理时CPU占用过高model.complexity=2且同时跑多个摄像头在推理时关闭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(cmap="Blues") plt.savefig("confusion_matrix.png", dpi=150)

从混淆矩阵里能直接看出哪些类别对容易混淆。如果“站立”和“行走”经常互判,大概率是行走的样本里包含了较多近似静止的瞬间,你的滑窗策略没有排除掉这些帧。这时候调分类器没有用,要回到数据预处理中,把行走动作定义成连续位移超过阈值的关键点序列,或者把当前帧与前一帧的关键点位移量作为额外通道加入输入。

更进一步的验证方法是做留一交叉验证(Leave-One-Subject-Out),即同一个人的全部样本只能出现在训练集或测试集,不能同时存在。很多项目的动作分类准确率虚高,是因为同一个人既出现在训练集又出现在测试集,模型记住了人,而不是记住了动作。用测试群体外的新人数据做最终评估,才能反映真实落地效果。你的源码包里如果有评估脚本,优先看它是否按人分组切分,而不是随机切分帧序列。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 22:43:43

JavaWeb宿舍管理系统:Servlet+MySQL+Layui轻量架构实战

简介&#xff1a;这是一套面向计算机相关专业学生与初学者的毕业设计级宿舍管理系统实战项目&#xff0c;基于JavaWeb技术栈实现高校宿舍管理核心功能&#xff0c;解决人员信息维护、房间分配、报修处理等实际业务场景需求&#xff0c;适合作为课程设计、毕设选题或Java全栈入门…

作者头像 李华
网站建设 2026/9/12 22:41:12

OFDM系统中BPSK调制的SNR定义与校准方法

简介&#xff1a;本资源是一份面向通信工程专业本科生及MATLAB初学者的OFDM系统仿真学习材料&#xff0c;聚焦BPSK调制下OFDM在AWGN信道中的信噪比性能分析&#xff0c;助力理解数字通信系统建模与误码率评估核心流程。压缩包共7个MATLAB脚本文件&#xff08;.m&#xff09;&am…

作者头像 李华
网站建设 2026/9/12 22:36:50

车牌检测实战:基于YOLOv5/v7/v8的训练与TensorRT部署全解析

简介&#xff1a;面向毕业设计与智能交通应用场景&#xff0c;这套车牌检测代码基于YOLOv5、YOLOv7、YOLOv8三种主流目标检测框架实现&#xff0c;支持多达十二种不同类型的车牌识别&#xff0c;公开测试环境下准确率达到百分之九十九点五&#xff0c;可帮助开发者快速搭建车牌…

作者头像 李华
网站建设 2026/9/12 22:36:47

AI写作工具如何解决文科写作三大痛点

1. 文科写作的痛点与AI解决方案作为一名长期与文字打交道的文科研究者&#xff0c;我深刻理解学术写作中的三大困境&#xff1a;论证单薄、案例匮乏、表达平庸。传统写作模式下&#xff0c;我们需要花费大量时间在图书馆翻阅资料&#xff0c;手动整理文献卡片&#xff0c;这种低…

作者头像 李华
网站建设 2026/9/12 22:35:46

无人机图像数据集训练实战:YOLOv8小目标检测与切图调优指南

简介&#xff1a;面向无人机目标检测这一细分方向&#xff0c;这份数据集围绕四千余张业余无人机拍摄的实景照片构建&#xff0c;素材常见于大疆精灵等消费级无人机&#xff0c;并特意纳入非无人机及外观相似的干扰目标&#xff0c;适合训练能够区分真假无人机的检测模型。压缩…

作者头像 李华