
简介面向计算机相关专业毕业设计的人体姿态与动作识别项目提供基于CNN深度学习的完整Python实现与项目说明文档。这份代码源自大四高分毕业设计评审均分达到96.5分所有程序均经运行验证非常适合正在完成毕业设计、课程设计或期末大作业的学生也适合对深度学习实战感兴趣的初学者进阶学习。压缩包共6个文件以5个Python脚本为主另附1份Markdown项目说明整体大小仅7KB结构清晰且便于快速定位关键模块。源码模块覆盖动作数据采集、模型训练、模型测试与实时姿态检测等完整流程项目说明对运行方式与处理逻辑进行梳理可帮助读者理解CNN模型的构建思路并能在此基础上修改扩展实现更多自定义功能。目前已有206人学习下载如遇运行问题可私聊获取远程教学支持可有效降低上手难度辅助顺利完成项目部署与答辩展示。1. 基于CNN深度学习识别人体姿态和动作系统先弄清楚它到底解决了什么问题很多人第一次拿到“基于CNN深度学习识别人体姿态和动作系统python源码项目说明.zip”这种压缩包第一反应是赶紧解压装环境结果往往折腾一整天也没跑出像样的效果。这套系统真正要解决的不是“识别出一个人”而是两件事一是把人体的关键点肩膀、肘腕、膝盖从图像里定位出来二是根据这些关键点的空间分布判断当前动作比如走路、站立、弯腰。它服务于动作分析、运动姿态评估、看护监测这类场景。适合手里有图像/视频标注数据、想在本地训练和部署的工程师也适合正在入门姿态估计的开发者。下面从原理、源码结构、最小推理、训练到避坑一次性讲透。2. CNN在姿态和动作识别里的分工原理、任务拆解与源码常见组织任何一个能跑的“姿态动作”系统背后基本都是两条流水线一条从图像得到关键点一条从关键点得到动作。CNN在这条链路里不是干一件事而是同时扮演定位器和分类器。你在源码包里看到两个模型文件不要觉得冗余把它们拆清楚后面调参才有方向。2.1 姿态估计和动作识别不是一回事任务边界决定模型边界姿态估计的产物是“位置”动作识别的产物是“类别”。位置是连续坐标类别是离散标签。前者用回归损失监督后者用交叉熵监督。如果硬把一个CNN骨干接到动作分类输出上网络会倾向于记住“像站姿的纹理”而不是记住“肩膀和膝盖的相对关系”。一旦换场景、换衣服泛化会断崖式下跌。这是很多入门项目看起来准确率高、实测不敢用的主要原因。所以常见的源码包会采用两段式结构先训关键点网络再训动作网络。拿到包后先确认models目录下有没有两个类如果没有说明作者大多是在关键点基础上用规则判断动作这时候动作部分基本不用训练。这个判断直接影响你后面要不要补数据、要不要改最后那个全连接层。我一般会先画一张数据流图图像输入到姿态网络得到17个关键点关键点归一化后进动作网络得到N个动作类别的概率。这张图画完整个项目的执行顺序就清楚了。边界也决定了损失函数和优化策略。姿态网络通常用关键点热力图上的均方误差动作网络用交叉熵。训练时先固定姿态权重调动作网络姿态网络收敛稳定后再考虑联合微调否则动作分支的梯度会把姿态分支的共享骨干拉偏。很多源码包直接把两个网络分开训练原因就是联合训练时超参太难调容易两头都顾不好。你在项目说明里看到类似“先跑pose_train.py再跑action_train.py”的顺序时按这个顺序走就对了。2.2 从输入图像到关键点热力图CNN回归的常见结构关键点回归最稳妥的形式不是直接回归坐标而是预测热力图。网络为每个关键点输出一个二维概率图峰值位置就是关节位置。这样做的好处是空间信息被保留训练也更稳。以常见分辨率192x256输入为例经过下采样后骨干网络特征图的尺寸大概是48x64最后接一个上采样头输出K个通道的热力图。我一般会先确认这个头的输出通道数是否等于数据集关键点数比如COCO标准的关键点数是17。如果源码给的最后一个卷积是17而你自己的标注只有14个点最后一层必须改。import torch import torch.nn as nn class PoseHead(nn.Module): def __init__(self, in_channels, num_keypoints, heatmap_size(48, 64)): super().__init__() self.heatmap_size heatmap_size self.decoder nn.Sequential( nn.Conv2d(in_channels, 256, 3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(256, num_keypoints, 1) ) def forward(self, feature_map): heatmaps self.decoder(feature_map) return heatmaps这里的in_channels是骨干网络输出的特征通道数取决于你用的backbonenum_keypoints对应关键点数量COCO格式是17自建数据集按实际标注数改。heatmap_size决定后续argmax后的坐标分辨率不能随意设。如果设成96x128关键点定位理论上更细但显存占用会明显涨设成32x44速度上去但关节位置容易偏肉眼可见地点错。我一般的做法是先用48x64跑通再根据显存余量和PCK指标决定要不要调。热力图训练时会把每个关键点坐标生成一个高斯峰值推理时取每个通道的最大值位置作为关节坐标。这里有个很容易忽略的细节如果一个人太靠近画面边缘某些关键点的高斯峰会被截断导致argmax结果偏移。很多源码包允许关键点带visible标志当目标是不可见时把对应热力图全部置零并且不参与loss计算。如果你训练的图片里常有遮挡一定要保留这个标志否则模型会强行给遮住的关节生成一个假峰值。2.3 从关键点序列到动作标签单帧CNN方案怎么把动作做出来动作分类可以基于RGB视频学习但这要求3D卷积和大规模视频数据。常见源码包为了降低门槛会把动作识别放到关键点层面用1D CNN吃时间窗口。这里的操作是标准卷积只不过卷积核沿时间维度滑动。选用1D CNN而不是循环网络是考虑到源码包的定位训练快、依赖少、部署容易。窗口通常取8到16帧太短会抖太长会拖慢响应。具体来说窗口内每一帧的关键点被归一化成x、y、置信度三个值拼成一个行向量连续T帧叠起来就得到一个[T, K*3]的输入序列。1D CNN在这个序列上做卷积输出动作类别概率。如果只是对单帧图做分类也可以直接把一帧的关键点向量当作T1的输入但稳定性差很多。下面是这类动作网络最常见的实现骨架class ActionNet(nn.Module): def __init__(self, num_keypoints17, num_classes8, window_size8): super().__init__() in_channels num_keypoints * 3 self.conv1 nn.Conv1d(in_channels, 128, kernel_size3, padding1) self.conv2 nn.Conv1d(128, 64, kernel_size3, padding1) self.pool nn.AdaptiveAvgPool1d(1) self.fc nn.Linear(64, num_classes) def forward(self, keypoints): # keypoints: [B, T, num_keypoints*3] 归一化坐标 x keypoints.transpose(1, 2) # [B, in_channels, T] x torch.relu(self.conv1(x)) x torch.relu(self.conv2(x)) x self.pool(x).squeeze(-1) return self.fc(x)这里第一个Conv1d的kernel_size3表示只看相邻三帧的上下文padding1保证序列长度不变。如果帧率是30fps窗口8帧约270ms适合站立、走路、坐下这类过渡不快的动作。做摔倒这类需要更长上下文的动作时窗口要调到16以上否则“倒地”过程还没形成完整姿态模型已经给出了错误结论。num_classes要和你的动作类别映射表严格一致多一个或少一个都会让推理结果全部错位。2.4 拿到源码包第一步先分清五类目录别急着装环境常见人体姿态动作源码包目录结构大同小异。解压后我先不看代码逻辑先扫一遍文件布局pose_action_system/ ├── README.md ├── requirements.txt ├── config/ │ └── pose_action.yaml ├── data/ │ ├── dataset.py │ └── transforms.py ├── models/ │ ├── pose_net.py │ └── action_net.py ├── tools/ │ ├── train.py │ ├── evaluate.py │ └── inference.py └── checkpoints/一般来说config放所有超参数models放网络结构data放数据集读取逻辑tools放训练评估推理入口checkpoints放权重。真正决定系统行为的是config文件而不是训练脚本。先读它能看出输入分辨率、热力图尺寸、关键点数量和动作类别数。接着打开requirements看依赖的大版本。如果这些和你本机环境差得远再决定要不要新建虚拟环境。不要看到requirements就无脑pip install更不要在全局base环境里装一旦版本冲突后面会非常被动。项目说明文档里如果有“先下载权重再运行”的字样应该把权重下载和放置路径当作前置条件圈出来。没有权重文件时最小推理也可能跑不起来这属于正常现象不是代码坏了。3. 在本地跑通最小推理解压、装环境、用预训练参数出结果跑通最小推理是整个项目的及格线。这一章我按“解压→环境→推理→没有权重怎么办”的顺序写。先不要看训练先让一个输入图像能流到输出结果。3.1 解压和核对项目说明先把依赖清单读对拿到zip先解压到一个纯英文路径不要放在桌面那种含中文的短路径里。解压后第一件事不是装环境而是看文件树和项目说明。用下边命令找文件unzip pose_action_system.zip -d pose_action_system cd pose_action_system find . -maxdepth 2 -type f | sort如果项目说明写得规范会有环境要求、权重下载位置和一行推理命令。先把这一行推理命令抄出来后面90%的问题都从它展开。requirements.txt通常是关键里面一般有numpy、opencv-python、torch、torchvision这类基础依赖。注意看有没有一个单独的requirements-gpu.txt有的话用它而不是默认那个因为默认可能只适合CPU。不要小看这一步很多人翻车在第一次pip install就版本冲突最后不得不回头清理环境。3.2 用虚拟环境隔离依赖别把基础环境弄脏我一般会为这类源码单独建一个虚拟环境哪怕事情看起来很小。原因是公开源码包的依赖版本常常和你的日常项目冲突比如一个项目需要torch1.8这套代码需要torch2.x在base环境里一定会打架。用conda创建环境的命令如下conda create -n pose_action python3.9 -y conda activate pose_action pip install -r requirements.txt这里python版本选3.9是为了兼容大多数这种源码包的torch安装轮子如果你机器上的CUDA版本比较高可以选3.10但如果项目说明明确写了python下限以说明为准。等pip装完先跑一句python -c import torch; print(torch.__version__)确认torch可用。如果import cv2报错通常是opencv-python依赖的libgl1缺失在Linux下用系统包管理器安装libgl1即可。这一步的具体坑在第5章还会展开。3.3 跑一次推理Demo从图像到关键点和动作标签的最小命令大多数这类工程都会有一个推理入口通常叫inference.py或demo.py。使用预训练权重时最小命令一般长这样python tools/inference.py --image data/sample.jpg \ --config config/pose_action.yaml \ --checkpoint checkpoints/pose_action.pth \ --output output/result.jpg--image传测试图像--config传配置文件--checkpoint传权重--output写结果图。如果你拿到的是源码加项目说明而没带权重先看说明里的权重获取方式别直接怪代码。下面这段代码展示了一个简化版推理脚本内部最常见的几行方便你理解参数是怎么被消耗的import cv2 import torch from models.pose_net import PoseNet from utils.decode import decode_heatmaps def main(): cfg load_yaml(config/pose_action.yaml) num_k cfg[num_keypoints] model PoseNet(num_keypointsnum_k) ckpt torch.load(cfg[checkpoint], map_locationcpu) model.load_state_dict(ckpt[model_state_dict]) model.eval() img cv2.imread(data/sample.jpg) orig_h, orig_w img.shape[:2] input_w, input_h cfg[input_size] resized cv2.resize(img, (input_w, input_h)) # 转张量、归一化 tensor torch.from_numpy(resized.transpose(2, 0, 1)).float() / 255.0 tensor tensor.unsqueeze(0) with torch.no_grad(): heatmaps model(tensor) # [1, num_k, H, W] keypoints decode_heatmaps(heatmaps[0], scale_xorig_w / input_w, scale_yorig_h / input_h) print(keypoints.shape)这段逻辑说明模型输入是resize后的图片输出是归一化坐标decode_heatmaps里的scale_x和scale_y负责把网络坐标还原到原图尺寸。很多开源实现习惯直接把argmax除以热力图尺寸再乘输入尺寸如果你看到的代码没有这组scale说明它假设输入图片就是网络分辨率推理真实视频时会点偏。参数上PoseNet的num_keypoints必须以config为准如果权重是17点的改成14会直接加载报错如果代码里没有decode_heatmaps函数就在utils目录下找类似heatmap2coord的函数。3.4 没有预训练权重时先用随机权重验证通路这是很多新手最容易卡住的地方权重文件不存在模型没法载入。我会建议不要先纠结精度先让一条随机权重的空模型跑通输入输出。也就是说把推理脚本的load_state_dict临时去掉或者生成一个随机权重文件看heatmap和关键点坐标能不能正常打印。只要能打印说明数据流、张量shape、后处理都没问题。这一步通过后再考虑训练或找可迁移的公开权重。不要迷信别人说的“随便一个预训练权重就能用”关键点网络的第一阶段必须用目标数据集或邻近数据集训练否则关键点偏了后面的动作分类再准也没有意义。这就是为什么很多老工程师拿到新源码包会先跑随机权重而不是先花几天调训练参数。先验证通路再追求精度能把心态稳下来。4. 训练自己的数据数据集格式、训练参数与评估方法推理跑通只是开始。要在自己的场景里稳定必须用自己的数据训练。很多源码包自带数据读取类但格式不一定合你手头数据。下面按数据结构、预处理、训练参数、评估四条线展开。4.1 数据集有三种常见样式关键点热力图、关键点坐标、动作分类标签在动手准备数据前先确认这套源码需要什么格式。我看过的同类项目里姿态与动作系统一般会吃三种数据之一格式核心字段适用场景常见读取方式COCO风格JSONimages, annotations, keypoints姿态估计预训练或迁移自定义Dataset按ann_id索引关键点坐标CSVframe_id, x1,y1,x2,y2,...,label动作分类、两级式系统pandas读取转浮点Tensor热力图H5images, target_heatmaps离线生成热力图训练IO更快h5py读取COCO风格JSON适合姿态网络CSV适合动作网络H5适合你已经把热力图生成完毕的情况。常见操作是先用COCO风格JSON训姿态再从姿态预测结果导出CSV训动作。不要试图让一个数据集同时满足所有格式能跑通源码才是第一位。下面是一段CSV样例frame_id, x_shoulder, y_shoulder, ..., x_ankle, y_ankle, label 0, 0.34, 0.21, ..., 0.52, 0.88, walk 1, 0.33, 0.20, ..., 0.53, 0.89, walk如果源码的数据加载需要的是每行一个动作样本你要把同一动作的连续帧窗口切出来这部分通常由动作网络的数据集类完成不要自己先把窗口硬编码到CSV里。4.2 从原始采集到可训练样本标注和预处理脚本标注关键点常见做法是用开源标注工具导出每个bbox下的关键点坐标。拿到标注后我一般先写一段脚本做两件事把关键点从原图坐标转成相对bbox的归一化坐标同时检查不可见点标记。所谓不可见点指被遮挡时标注为0的坐标不能当成真实位置参与训练。下面这段代码示意了归一化逻辑适用姿态网络输入前处理import numpy as np def normalize_keypoints(kpts, bbox): x, y kpts[0], kpts[1] x1, y1, x2, y2 bbox w, h x2 - x1, y2 - y1 cx, cy x1 w / 2, y1 h / 2 nx (x - cx) / max(w, 1) ny (y - cy) / max(h, 1) return np.array([nx, ny])参数说明bbox是单人检测框归一化后姿态动作与人物在画面里的绝对像素不再相关对动作分类尤其重要。如果你发现训练时站着和坐着容易混淆先看是不是忘了做这一步。还有一个常见坑是翻转增强时关键点的左右关系要交换例如左肩和右肩互换。代码里必须维护一个关键点交换列表否则增强后的人体结构是错位的。这个交换表一般放在config或data/transforms里要找出来确认。4.3 训练脚本里的六个关键参数batch_size、学习率、热力图尺寸、增强开关、类别平衡、epoch训练脚本能改的地方很多但下面这份yaml几乎每次都要调data: input_size: [192, 256] heatmap_size: [48, 64] window_size: 8 train: batch_size: 32 epochs: 80 lr: 0.001 lr_decay: step pose_aug: true action_class_balance: trueinput_size和heatmap_size决定显存与定位精度。如果你的显存只有8GBbatch_size直接降到8或16input_size保持192x256不要一上来就用384x384。heatmap_size通常等于input_size的四分之一所以192x256对应48x64。window_size只在训练动作网络时使用建议8起步帧率30FPS下约270ms的上下文。lr0.001是姿态网络从头训练的常见默认值如果加载了骨干网络预训练权重我把learning rate降到0.0001否则前几个epoch容易冲崩。pose_aug开启随机旋转/缩放/翻转augment能大幅降低过拟合但对关键点坐标的同步变换要求很严格这点第5章会细说。action_class_balance在动作类别样本差异大时开启例如“蹲下”样本只有“站立”的十分之一不做平衡模型会偏向预测多数类训练时准确率虚高。4.4 训练完怎么评估PCK、OKS、分类准确率三个指标分别看什么姿态网络常用PCK和OKS动作网络看分类准确率。PCK的含义是预测关键点与标签之间的距离小于某个阈值通常按头部框尺寸的0.1到0.2倍算合格。下面这段是PCK的朴素实现用来快速看单批结果def pck(pred, gt, head_size, threshold0.2): # pred/gt: [num_kpts, 2], 已经映射到原图坐标系 dist np.linalg.norm(pred - gt, axis-1) return (dist head_size * threshold).mean()head_size是头部框的对角线长度不同动作尺度下能自适应所以比绝对像素阈值合理。OKS比PCK更细它会区分不同关键点的难易程度比如肩膀比脚踝容错大COCO风格的json里可以直接用现成实现不用自己折腾。动作网络就别只看整体准确率要打印混淆矩阵。摔倒被识别成躺下、站立被识别成蹲下这种混淆是部署时最要命的。逐类查漏比整体准确率有意义得多。训练完成不要急着存最后一个epoch我会保存验证集指标最好的checkpoint再配合训练日志确认loss没有异常回升。5. 避坑记录CNN姿态与动作系统训练推理的五个翻车点下面这五个坑都是我在类似源码包里实际翻过的有的来自环境有的来自数据有的来自后处理。每一条按“现象-原因-解决”写照着排查能省几天时间。5.1 路径与系统编码解压到中文目录必坑现象系统是Windows把zip解压到“项目2025”这种中文目录运行时DataLoader报FileNotFoundError而且报错路径里显示乱码。原因zip内文件名是UTF-8编码Windows自带解压工具会用本地编码解出乱码代码里再用默认编码去读路径就对不上。另外中文路径本身在opencv里支持不稳容易踩到。解决解压时直接统一到纯英文路径目录名用pose_action这种不要在中间层夹中文。如果已经解压坏了删掉重解别尝试去改源码里的编码代价大。代码入口统一用pathlib.Path来处理路径避免手写字符串拼路径。这个习惯能避开很多诡异问题。5.2 关键点坐标没跟着数据增强走训练曲线像心电图现象开启旋转、翻转增强后姿态网络的loss先降后升或者反复震荡val的PCK还不如不增强。原因图像被随机旋转和翻转了但关键点坐标没有做同样的变换模型看到的是“肩膀上长了手”的错位样本。最常见是忘了翻转时交换左右关键点比如左肩右肩不换损失永远压不下去。解决用图像增强库的keypoints参数来绑定坐标不要自己手写旋转矩阵。使用albumentations时写法如下import albumentations as A transform A.Compose([ A.Rotate(limit30, p0.5), A.HorizontalFlip(p0.5), ], keypoint_paramsA.KeypointParams(formatxy))这个写法下图像和keypoints会使用同一组随机参数翻转时albumentations会自动交换左右索引。要注意的是如果你的训练代码里还有额外的RandomCrop必须把crop后的bbox也同步到坐标上否则关键点坐标依旧错位。5.3 类别编号错位Loss正常但推理结果全乱现象动作网络训练loss很漂亮验证准确率95%但一到真实视频里把站立识别成走路走路识别成坐下看起来完全随机。原因这种翻车经常不是模型问题是类别编号错位。你训练时用的CSV里类别0对应走路但推理时模型输出0却按站立去画标签或者裁剪类别列表时少了一个类别所有编号整体偏移一位。解决把类别映射抽成单独的class_list.yaml训练和推理都从同一个文件读不硬编码在脚本里。一般在config目录下会有类似的类别文件。如果源码里没有就自己建一个并在训练入口和推理入口都加一段断言验证类别数量一致。这样以后新增动作只需改一个文件。5.4 显存不足先看热力图尺寸和batch别急着换显卡现象一训练就OOM报CUDA out of memory把batch_size从32调到8还是不够。原因显存占用不只是batch_size输入分辨率、热力图分辨率、骨干网络通道数共同决定active cache。很多人只调batch_size实际上热力图尺寸占大头。有些项目的config默认heatmap_size96x128这比batch翻倍还吃显存。解决先把input_size降到192x256heatmap_size降到48x64再把batch_size调到8看显存余量。同时打开混合精度训练用GradScaler减少中间张量的精度缓存scaler torch.cuda.amp.GradScaler() with torch.cuda.amp.autocast(): heatmaps model(img) loss criterion(heatmaps, target) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()如果你用的是老显卡不支持autocast再考虑把backbone换轻量版本。不要一开始就换大卡多花冤枉钱。5.5 玄学指标Loss很低但PCK难看先查坐标还原现象模型训练完loss低得开心结果画出来的关键点整体偏左上角PCK惨不忍睹。原因这不是模型玄学是坐标还原时漏了尺度映射。推理脚本把热力图argmax后直接除以热力图尺寸再乘热力图尺寸看起来没毛病但热力图尺寸、网络输入尺寸、原图尺寸三者不是一回事少一步就偏。常见是热力图是48x64网络输入是192x256原图是1080x1920如果只在热力图尺度里自洽没有乘原图到输入图的scale输出当然偏。解决记录原图宽高在网络输出坐标后做完整的反向映射。简化转换如下scale_x orig_w / input_w scale_y orig_h / input_h pred_x heat_x * (input_w / heatmap_w) * scale_x pred_y heat_y * (input_h / heatmap_h) * scale_y这里heat_x是每个通道argmax得到的整数索引heatmap_w和heatmap_h是热力图宽高。很多源码的decode_heatmaps里只处理到input_w没有最后一个scale_x如果你发现推理图比训练图大就一定要补上这段。这个坑我栽过一次后现在都会先写一个单人单图的绘图脚本把坐标直接画在原图上人工确认。6. 把动作识别做稳的进阶技巧滑动窗口、多数投票和置信度阈值单帧动作分类最典型的问题是抖相邻两帧可能从“走路”跳到“站立”再跳回“走路”。这不是模型完全错了而是某些中间姿态在两个类别边界上。我常用的一个做法是维护一个长度为T的帧队列模型对队列做整体判断然后对最近N次输出做多数投票概率低于阈值的帧记为unknown不参与最终决策。下面的代码示意了这种逻辑from collections import deque import numpy as np import torch CLASSES [stand, walk, sit, bend] def detect_action(queue, action_net, history, threshold0.7): keypoints np.array(queue, dtypenp.float32) keypoints keypoints.reshape(1, len(queue), -1) tensor torch.from_numpy(keypoints) with torch.no_grad(): logits action_net(tensor) prob torch.softmax(logits, dim-1).squeeze(0).numpy() vote int(prob.argmax()) if prob.max() threshold else -1 history.append(vote) if len(history) 5: history.popleft() counts [history.count(c) for c in range(-1, len(CLASSES))] best int(np.argmax(counts)) - 1 return best, prob这里的action_net接收的是形状[1, T, K*3]的关键点序列K是姿态关键点数量3对应x、y、置信度。threshold取0.7会让unknown增多但误判减少如果场景要求实时响应我一般取0.6并且把history长度从5降到3。窗口长度T8时大约270ms的上下文处理站立、走路这类过渡动作足够。如果是摔倒检测T要提高到16到21否则“倒地”过程还没形成完整姿态模型就过早判成“躺下”。投票窗口里我需要保证unknown不会长时间占据多数否则动作开始和结束会有明显延迟。一个更细的做法是对每个类别统计平滑概率而不是粗暴计数但多数投票实现最直观且瓶颈通常不在这个逻辑而在关键点提取。先做投票再决定要不要做更复杂的平滑。这套思路我在模拟项目X上验证过误检率比单帧直接输出降低了一半以上。我现在的习惯是先跑通打通链路再调阈值和窗口最后才优化模型本身。希望帮到你。本文还有配套的精品资源点击获取