简介:这份资源是面向计算机相关专业学生与深度学习入门者的CNN人体姿态与动作识别实战项目,源自大四毕业设计,经导师指导并通过答辩评审,平均分达96.5分,适合作为毕设、课程设计、期末大作业或项目实战练习的参考方案。压缩包共6个文件,以5个Python源码文件与1份Markdown项目说明为主,整体约7KB,代码结构围绕数据采集、模型训练、模型测试与姿态检测等环节展开,便于读者快速理解CNN在人体姿态与动作识别任务中的完整实现流程。项目代码均经过运行测试,功能正常后才上传,下载后可直接对照说明文档梳理模块职责与调用关系,也可在此基础上修改以扩展其他功能。目前已有205人学习关注,适合希望掌握深度学习项目落地思路、积累实战经验的学习者参考使用。
1. 从一段视频到骨架坐标:CNN 动作识别系统到底在做什么
你手里有一段手机拍的深蹲视频,想自动判断动作是否标准;或者你有一批监控片段,想识别里面的人是在走路、跌倒还是挥手。这类需求落到工程上,核心就两件事:先把人体姿态从画面里抠出来变成一组坐标点,再用这些坐标点去判断动作类别。这个标题里的「基于 CNN 深度学习识别人体姿态和动作系统」,讲的正是这条链路——用卷积神经网络做特征提取,一端接姿态估计,一端接动作分类,中间用 Python 串起来。
它适合谁?适合已经会一点 Python、装过 PyTorch 或 TensorFlow、想找一个能跑通的完整项目练手的人。不适合完全没写过代码、指望双击运行就出结果的人。整套系统的价值不在于模型多先进,而在于它把「数据怎么组织、模型怎么搭、推理怎么落地」这条线走通了,你能照着改、照着换数据集。下面我按实际做项目的顺序,把这条链路拆开讲清楚。
2. 姿态估计与动作识别为什么都绕不开 CNN
2.1 姿态估计的本质是热力图回归
人体姿态估计要输出的是关键点坐标,比如鼻子、肩膀、手肘、膝盖这些位置。主流做法不是直接回归坐标数值,而是让网络输出一张张热力图,每个关键点对应一张,图上亮度最高的位置就是该关键点的坐标。CNN 在这里的作用是逐层卷积,把原始像素逐步抽象成「这里像不像手肘」这种高层特征。
为什么用热力图而不是直接回归坐标?因为直接回归对空间信息的约束太弱,网络容易学偏。热力图保留了空间结构,卷积核在局部感受野上滑动,天然适合处理这种「位置敏感」的任务。常见做法是先用一个骨干网络(比如 ResNet 或 MobileNet)提取特征,再用几层反卷积把分辨率还原回去,最后输出 K 张热力图,K 就是关键点数量。
2.2 动作识别吃的是时序信息
光有单帧姿态还不够,动作是随时间变化的。挥手和举手在某一帧可能长得一样,区别在于连续几帧的轨迹。所以动作识别这一端,输入通常是两种形式:一种是骨架序列,把每帧的关键点坐标按时间排成序列,用 CNN 或 RNN 处理;另一种是直接对视频帧做 3D 卷积,同时捕捉空间和时间特征。
标题里强调 CNN,说明这套系统大概率走的是「骨架序列 + 1D/2D 卷积」或者「视频帧 + 3D 卷积」的路线。前者计算量小、对背景干扰不敏感,适合关键点已经能稳定提取的场景;后者信息更全,但吃数据和算力。选哪条路,取决于你手头的数据和硬件。
2.3 两段式还是端到端
工程上常见两种架构。两段式:先跑姿态估计模型拿到关键点,再把关键点序列喂给动作分类模型。好处是模块解耦,姿态模型可以用现成的预训练权重,动作模型可以单独换、单独调。端到端:一个网络直接从视频输出动作类别,中间不显式输出关键点。好处是省事,坏处是训练难、可解释性差、数据需求大。
我一般会推荐两段式,尤其是刚上手的时候。因为你能清楚看到每一步的输出,姿态提取错了还是分类错了,一眼就能定位。端到端一旦效果不好,排查起来就是黑匣子,血泪经验。
3. 用 Python 把姿态提取跑通:从环境到第一帧骨架
3.1 环境配置与依赖安装
先把环境弄干净。建议用 conda 建独立环境,避免和系统 Python 打架。Python 版本选 3.8 到 3.10 之间,太新了有些库还没跟上。
conda create -n pose_action python=3.9 conda activate pose_action pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu pip install opencv-python numpy matplotlib这里 torch 装的是 CPU 版,方便你先跑通逻辑。有显卡的话把 index-url 换成对应的 CUDA 版本。opencv 负责读写视频和画骨架,numpy 做数组运算,matplotlib 用来可视化热力图或轨迹。
3.2 加载姿态模型并推理单帧
下面这段代码演示怎么用现成的姿态估计模型对一帧图像做推理,拿到关键点坐标。这里用 torchvision 里自带的关键点模型做示例,实际项目里换成你选的模型即可。
import torch import cv2 import numpy as np from torchvision.models.detection import keypointrcnn_resnet50_fpn from torchvision.transforms import functional as F # 加载预训练关键点模型,切换到推理模式 model = keypointrcnn_resnet50_fpn(pretrained=True) model.eval() # 读一帧图像,转成模型需要的张量格式 img = cv2.imread("frame.jpg") img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) tensor = F.to_tensor(img_rgb).unsqueeze(0) with torch.no_grad(): outputs = model(tensor) # outputs[0]["keypoints"] 形状为 [人数, 17, 3],最后一维是 x, y, 置信度 keypoints = outputs[0]["keypoints"].numpy() print("检测到人数:", keypoints.shape[0]) print("第一人关键点:", keypoints[0][:3])逻辑说明:模型输出的是一个列表,每个元素对应一张图。keypoints的第三维里,前两个值是坐标,第三个是置信度,低于阈值的点要丢掉。参数上,pretrained=True会下载官方权重,第一次运行需要联网。如果你要换自己的模型,只要保证输出格式一致,后面的动作分类代码不用改。
3.3 把关键点画回画面验证
拿到坐标后一定要可视化,不然你不知道模型到底提没提对。
# 在图像上画关键点和连线,用于人工检查 skeleton = [(0,1),(1,3),(3,5),(0,2),(2,4),(4,6), (5,7),(7,9),(6,8),(8,10),(5,6),(5,11), (6,12),(11,12),(11,13),(13,15),(12,14),(14,16)] for person in keypoints: for x, y, conf in person: if conf > 0.5: cv2.circle(img, (int(x), int(y)), 3, (0, 255, 0), -1) for a, b in skeleton: if person[a][2] > 0.5 and person[b][2] > 0.5: cv2.line(img, (int(person[a][0]), int(person[a][1])), (int(person[b][0]), int(person[b][1])), (255, 0, 0), 2) cv2.imwrite("skeleton.jpg", img)置信度阈值 0.5 是个经验值,遮挡严重时可以降到 0.3,但会引入更多误检。连线表是 COCO 数据集的 17 点定义,换模型时这张表要跟着换。跑完打开图片看一眼,骨架贴不贴人,一眼就知道模型行不行。
4. 动作分类模型怎么搭:骨架序列的 CNN 方案
4.1 数据组织成「时间 × 关键点 × 坐标」
动作分类的输入不是单帧,而是一段连续帧。假设你每个动作截取 30 帧,每帧 17 个关键点,每个点有 x、y 两个坐标,那一条样本就是一个 30×17×2 的张量。标签就是动作类别编号。
这里有个关键预处理:坐标归一化。不同视频里人的位置和大小不一样,直接喂原始像素坐标,模型会去学「人在画面哪个位置」这种无关信息。常见做法是以髋部中心为原点,用肩宽或躯干长度做尺度归一化,把所有坐标缩放到相对值。
def normalize_sequence(seq): # seq 形状 [T, K, 2],以髋中心为原点,肩宽为尺度 hip = (seq[:, 11, :] + seq[:, 12, :]) / 2.0 seq = seq - hip[:, None, :] shoulder = np.linalg.norm(seq[:, 5, :] - seq[:, 6, :], axis=1) scale = shoulder.mean() + 1e-6 return seq / scale这段逻辑是动作识别能不能泛化的分水岭。不做归一化,换个拍摄距离模型就废了。1e-6是防止除零,别省。
4.2 一维卷积网络结构
骨架序列是典型的时间序列,用 1D 卷积在时间维度上滑动,能捕捉「先抬手再挥手」这种局部时序模式。
import torch.nn as nn class ActionCNN(nn.Module): def __init__(self, num_classes=6, num_keypoints=17): super().__init__() in_ch = num_keypoints * 2 self.net = nn.Sequential( nn.Conv1d(in_ch, 64, kernel_size=3, padding=1), nn.BatchNorm1d(64), nn.ReLU(), nn.MaxPool1d(2), nn.Conv1d(64, 128, kernel_size=3, padding=1), nn.BatchNorm1d(128), nn.ReLU(), nn.AdaptiveAvgPool1d(1) ) self.fc = nn.Linear(128, num_classes) def forward(self, x): # x 形状 [B, T, K*2],转成 [B, K*2, T] 给 Conv1d x = x.permute(0, 2, 1) x = self.net(x).squeeze(-1) return self.fc(x)结构说明:输入通道数是关键点数乘 2(x 和 y)。两层卷积加池化,最后用自适应平均池化把时间维压成 1,接全连接分类。num_classes按你的动作类别数改,num_keypoints按你用的姿态模型改。这个网络很浅,适合数据量几千条以内的场景,数据多了可以加深或换 ResNet 结构。
4.3 训练循环与关键参数
from torch.utils.data import DataLoader, TensorDataset # 假设 X 形状 [N, 30, 34],y 形状 [N] dataset = TensorDataset(torch.tensor(X, dtype=torch.float32), torch.tensor(y, dtype=torch.long)) loader = DataLoader(dataset, batch_size=32, shuffle=True) model = ActionCNN(num_classes=6) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) criterion = nn.CrossEntropyLoss() for epoch in range(50): model.train() total_loss = 0 for xb, yb in loader: optimizer.zero_grad() loss = criterion(model(xb), yb) loss.backward() optimizer.step() total_loss += loss.item() print(f"epoch {epoch}, loss {total_loss/len(loader):.4f}")学习率 1e-3 是 Adam 的常用起点,loss 不降就降到 1e-4。batch_size 32 在几千条数据上比较稳。训练轮数看 loss 曲线,一般 30 到 50 轮就收敛了。注意这里没有划分验证集,实际项目一定要切出 20% 做验证,否则过拟合了你都不知道。
5. 避坑与排查:姿态动作系统最容易翻车的五个地方
5.1 关键点抖动导致分类不稳
现象:同一段动作,相邻帧的骨架坐标跳来跳去,分类结果在几个类别之间反复横跳。
原因:单帧姿态估计没有时序约束,遮挡或模糊时模型会猜,坐标自然抖。
解决:对关键点序列做平滑,最简单的是滑动平均或卡尔曼滤波。滑动窗口取 5 帧,对每个关键点的 x、y 分别平均,能压掉大部分高频抖动。代价是动作起始和结束会有轻微延迟,实时场景要权衡窗口大小。
5.2 归一化用错参考点
现象:训练集准确率很高,换一段视频测试就崩。
原因:归一化时用了画面绝对坐标或者整帧的边界框,模型学到了拍摄距离和位置。
解决:统一用人体自身的解剖结构做参考,髋中心加肩宽是经过验证的组合。如果姿态模型的关键点定义不同,先确认索引对应关系,别照抄 COCO 的 11、12 号点。
5.3 类别不平衡让模型偏向多数类
现象:某个动作识别率接近 100%,另一个动作几乎全错。
原因:训练数据里各类别样本数差距大,模型学会了「全猜多数类」也能拿高准确率。
解决:要么补数据,要么在 loss 里加类别权重,CrossEntropyLoss(weight=...)传入按类别频率倒数算出的权重。还可以用重采样,让每个 batch 里各类别比例接近。
5.4 帧采样策略影响时序判断
现象:快速动作识别不准,慢速动作还行。
原因:固定间隔抽帧,快速动作的关键帧被跳过了。
解决:按动作持续时间动态采样,或者用光流辅助判断运动剧烈程度。简单做法是保证每个动作片段至少覆盖完整周期,抽帧数固定但时间跨度按动作长度自适应。
5.5 推理时 batch 维度和训练不一致
现象:训练好好的模型,推理时报维度错误或者结果全乱。
原因:训练时输入是[B, T, K*2],推理时忘了加 batch 维,变成[T, K*2]。
解决:推理前统一unsqueeze(0),输出后再squeeze(0)。养成习惯,模型输入输出维度在训练和推理两侧写注释标清楚,能省很多调试时间。
6. 把系统跑得更稳:验证方法与一个提点技巧
模型训完不是终点,你得知道它到底靠不靠谱。最直接的办法是留出测试集算混淆矩阵,看哪些类别容易混。混淆矩阵比单一准确率有用得多,能告诉你「挥手」是不是老被认成「举手」。如果两个类别互相混,先回去看骨架可视化,大概率是这两个动作在关键点层面就区分度不够,这时候加关键点或者换更细的姿态模型比调分类网络更有效。
另一个我常用的技巧是测试时增强。对同一段骨架序列,做轻微的时间缩放、左右翻转、加小噪声,各推理一次,把 softmax 输出平均。这个操作几乎不增加训练成本,但能明显降低单次推理的随机波动。翻转时注意左右关键点索引要对应交换,不然骨架就反了。
def tta_predict(model, seq, n=5): # seq 形状 [T, K*2],做 n 次轻微扰动后平均输出 model.eval() preds = [] for i in range(n): noise = np.random.normal(0, 0.01, seq.shape) scale = 1.0 + np.random.uniform(-0.05, 0.05) aug = (seq + noise) * scale with torch.no_grad(): out = model(torch.tensor(aug, dtype=torch.float32).unsqueeze(0)) preds.append(torch.softmax(out, dim=1).numpy()) return np.mean(preds, axis=0)噪声标准差 0.01 和缩放范围 ±5% 是经验值,太大反而会把正确样本推错。这套 TTA 在骨架序列上收益稳定,尤其是数据量不大的时候。
最后说个我自己的习惯:每换一次姿态模型或者改一次归一化方式,一定重新跑一遍可视化,把骨架画到原视频上逐帧看。别只看 loss 和准确率,那些数字会骗人,骨架贴不贴人才是真相。这个系统值不值得做,取决于你能不能把姿态这一环做扎实,后面分类网络再花哨,前端坐标是歪的,全是白搭。希望帮到你。
本文还有配套的精品资源,点击获取