简介:这套基于CNN的人体姿态与动作识别Python源码包,面向计算机视觉初学者及需要快速验证姿态识别算法的开发者。项目以卷积神经网络为核心,借助mediapipe自动提取人体骨骼点,构成从图像采集、姿态标注到模型训练和测试的完整链路。资源包内含6个文件,其中5个Python脚本分别承担图像保存、姿态检测、CNN训练与模型测试等功能,另有1个Markdown项目说明文档梳理整体架构与调用关系,整体压缩包大小仅7KB,小巧而清晰。通过这套代码,可以学习如何结合OpenCV和mediapipe进行人体关键点检测,并理解基于CNN的多分类识别流程;项目说明文档还能帮助快速定位各模块职责,便于在此基础上扩展动作类别或更换网络结构。目前已有471人学习,适合用于课程设计、毕设预研或人体动作识别方向的入门实践。
1. 基于CNN的人体姿态与动作识别:这套源码包到底能解决什么
「基于CNN的人体姿态和动作识别」这个标题拆开看其实是两个任务:先让CNN找到画面里人的关节位置,再根据关节组成的骨架序列判断人正在做什么动作。市面上这类源码包通常把两个任务串成一条流水线——先输出关键点坐标,再对坐标序列做动作分类,最后在视频流里实时给出动作标签。它能解决的实际问题很具体:安防场景的跌倒检测、健身App的动作计数、摄像头下的异常行为提醒,都属于这个技术方向。适合的人群也清晰:做毕业设计的本科生、想把姿态识别接入摄像头Demo的开发者,以及想搞懂关键点检测和动作分类怎么衔接的工程师。这套方案不用从零设计网络,重点是把两段模型拼对、参数调好、坑踩完。下文按「原理→跑通→调参→避坑→验证」的顺序把这套流程讲透,新手能跟着把项目跑起来,熟手也能在参数和边界上有点收获。
2. 从姿态估计到动作识别:CNN 在这里到底做两件事
2.1 姿态估计和动作识别是两个任务,别混在一起做
很多第一次接触这个标题的人会以为「人体姿态识别」是一个网络一次搞定的事:输入一张图,输出一个动作标签。实际项目里几乎没人这么干,因为单帧的 CNN 分类器在静态画面上根本分不清「站立」和「行走」——这两类在单帧图像上往往只差零点几秒的位移,光靠空间特征必然翻车。正经做法是把任务拆成两段:姿态估计负责空间理解,动作识别负责时序理解,两者各自训练、串联推理。
姿态估计要解决的是「人的关节在哪里」。输入一张含人的图像,输出通常是 17 个或 21 个关键点的坐标和置信度,包括左右肩、左右肘、左右腕、髋、膝、踝等。CNN 在这里干的事是提取空间特征,常见实现是输出 K 张热图(heatmap),每一张对应一个关节,热图上的峰值位置就是该关节的预测位置。这个设计的好处是热图保留了空间分辨率,比直接回归坐标稳定得多——直接回归坐标相当于让网络做一次全局压缩,结果容易飘。
动作识别要解决的是「这个人正在做什么动作」。输入不再是单张图,而是一段连续帧里的关键点序列,输出是动作类别,比如站立、行走、坐下、跌倒、挥手。这个任务的核心是捕获时序变化,所以网络结构上通常要有时序建模能力,至少要能把连续 8~16 帧的关键点串起来。两段任务混在一起做的最常见翻车姿势,是拿单帧 CNN 直接输出动作类别。这类模型在静态数据上可能刷到 90% 以上的准确率,一上视频就暴露问题:只要是相似的站姿,走路和站立都会判成同一个类。把任务拆开之后,每个子网络的职责边界清楚,排错时也容易定位——检测不准去看姿态模型的输出,分不清动作去看时序模型的输入特征,两不干扰。
2.2 时序信息怎么进 CNN:单帧基线、3D 卷积与两阶段方案
动作识别绕不开一个核心问题:时序信息以什么形态输入模型。常见做法有三种,选型直接决定这个源码包的数据准备和训练成本。
第一种是单帧基线,即把每一帧单独分类,再用投票机制决定整段视频的标签。这个方案最省事,但正如前面说的,对站立和行走这类细粒度动作几乎无效,只适合动作差异特别大的场景,比如摔倒检测——摔倒过程里单帧图像就有显著的空间特征变化,单帧模型也能做。第二种是 3D 卷积网络,直接喂连续帧的图像块,让卷积核同时在空间和时间维度滑动。效果上限高,但训练数据需求大、显存占用高,在一个毕设规模的数据集上很容易过拟合,而且调参过程像黑匣子,出了问题不好解释。第三种是两阶段方案:先用姿态估计模型把每帧的关键点抽出来,再把关键点序列喂给一个时序分类器。这个方案在小数据集上最稳,因为关键点把人的外观信息剥离了,分类器只需要学「骨架怎么动」,而不是「像素怎么变」,训练量小、收敛快、可解释性强。
大部分面向工程落地的源码包都会选第三种方案,也就是标题里「姿态 + 动作识别」的自然衔接方式。我一般建议第一次做这个方向的人也走这条路:姿态网络用现成预训练权重,时序分类器从头训练,整个流程的调试难度比端到端 3D 卷积低一个量级。唯一要注意的是两阶段方案存在误差传递——姿态模型某个点检测偏了,后面分类器就会跟着错,所以后面第 5 章会专门讲关键点质量的控制方法。
2.3 选模型骨架:从轻量到标准,替换时只改一处
姿态估计的 CNN 骨架决定精度和速度的平衡点。常见的做法是拿分类网络做骨干,比如 ResNet 系列或 MobileNet 系列,然后在骨干后面接反卷积头,把特征图分辨率升回去,再输出关键点热图。选哪个骨架,主要看跑在什么设备上。CPU 或嵌入式设备上,MobileNet 这类轻量骨干更现实;有 GPU 的实验室环境,ResNet-50 或 ResNet-101 是标准选择,精度明显更好但显存占用和延迟也上去了。
替换骨架时要注意的核心参数只有一个:骨干输出通道数要和关键点解码头的输入对齐。下面是这个源码包里最常见的模型头定义方式,和具体骨干无关,理解这一段就知道换网络时改哪里。
import torch.nn as nn class PoseHead(nn.Module): def __init__(self, backbone_out_channels, num_keypoints=17): super().__init__() # 两次上采样,把骨干输出的低分辨率特征升回高分辨率 self.deconv = nn.Sequential( nn.ConvTranspose2d(backbone_out_channels, 256, kernel_size=4, stride=2, padding=1), nn.ReLU(inplace=True), nn.ConvTranspose2d(256, 256, kernel_size=4, stride=2, padding=1), nn.ReLU(inplace=True), ) # 每个关节对应一张热图,所以输出通道数 = 关键点数量 self.final = nn.Conv2d(256, num_keypoints, kernel_size=1) def forward(self, x): return self.final(self.deconv(x))这段代码逻辑很直白:骨干网络输出一个低分辨率的特征图,反卷积层把它逐级放大,最后用 1×1 卷积把通道数压到关键点数量。两个反卷积层各做 2 倍上采样,所以热图分辨率是骨干输出的 4 倍。如果你的骨干输出是 24×18,热图就是 96×72。换骨架时只需要把backbone_out_channels改成新骨干实际的输出通道数,其他部分不用动。
这里有个很多人踩过的坑:反卷积头不是层数越多越好。对 192×192 的输入,两层反卷积通常够了,加到三层会引入棋盘格伪影,热图出现周期性条纹,关键点精度反而下降。如果发现热图质量差,先检查是不是上采样倍数和骨干输出的空间尺寸不匹配,再考虑是不是反卷积层数过多。调参优先动骨干,而不是无脑加解码层深度。
3. 把源码包跑起来:环境、数据与推理流程
3.1 解压、依赖与最小环境:跑通这套源码的完整命令
拿到「python源码+项目说明文档.zip」之后,第一步不是看代码,而是先把环境搭好。这类项目几乎都基于 PyTorch 或 TensorFlow 二选一,依赖项翻来覆去就是那几个:torch、torchvision、opencv-python、numpy、matplotlib、tqdm。我习惯用 Miniconda 建独立环境,避免把系统 Python 环境搞乱。下面是推荐的最小环境搭建流程。
# 创建独立环境,避免污染系统 Python conda create -n pose_action python=3.8 -y conda activate pose_action # 安装基础依赖,torch 版本按本机 CUDA 版本选择,CPU 机器直接装 CPU 版 pip install torch torchvision pip install opencv-python numpy matplotlib tqdm pip install -r requirements.txt # 如果压缩包里带了 requirements.txt # 验证安装是否完整 python -c "import torch, cv2, numpy; print('deps ok')"解释一下每步的作用:conda create建了一个干净的 Python 3.8 环境,避免把项目依赖装到系统环境里导致版本冲突;torch 和 torchvision 是模型训练和推理的核心框架,GPU 机器装对应 CUDA 版本,CPU 机器直接装 CPU 版也能跑通流程,只是慢;opencv-python 负责视频读取、图像缩放和画骨架可视化,是整个流水线里离不开的图像处理库。如果压缩包里带了requirements.txt,直接pip install -r省事,但要注意项目说明文档里标注的依赖版本,不需要完全一致,大版本对上就行。
跑代码前还有一步容易被忽略:确认项目说明文档里写的模型权重路径是相对路径还是绝对路径。很多源码包发布时会把权重文件排除在压缩包外,只留一个weights/空目录,里面放一个README告诉你下载地址。拿到包后先检查weights/目录下有没有.pth或.h5文件,没有就去把权重补齐,否则后面一跑就报「文件不存在」。这一步花两分钟,能省掉后面排查路径问题的半小时。
3.2 数据准备:视频抽帧、关键点格式与训练集目录组织
跑通推理之后,如果要自己训练模型,最先要解决的是数据从哪来、格式怎么组织。这个源码包的数据管线一般围绕「视频序列」组织:每个动作类别一个文件夹,里面放若干段视频,标注文件记录每一帧里人的 bounding box 和 17 个关键点坐标。实际落地时最常用的是把视频抽帧成图片序列,再做标注。
import cv2 import os def extract_frames(video_path, out_dir, every_n=2): """把视频按固定间隔抽帧,输出为 jpg 图片序列""" os.makedirs(out_dir, exist_ok=True) cap = cv2.VideoCapture(video_path) idx = 0 saved = 0 while True: ret, frame = cap.read() if not ret: break if idx % every_n == 0: # 每 every_n 帧保存一帧 cv2.imwrite(os.path.join(out_dir, f"{saved:06d}.jpg"), frame) saved += 1 idx += 1 cap.release() print(f"saved {saved} frames from {video_path}")every_n参数控制抽帧密度。对走路、挥手这类动作,2 帧抽 1 帧基本够用;对跌倒这类快速动作,建议每帧都抽,否则跌倒过程可能被跳过关键帧。抽完帧之后,关键点标注一般存成 JSON 或文本文件,每行一帧:左边是图片文件名,中间是 17 个关键点的 (x, y, confidence) 三元组,右边是动作类别标签。拿到标注之后先写个可视化脚本,把关键点画回原图上检查一遍——这一步能发现一半以上的标注错误,比如左右手标反、遮挡关节被标到背景上。
3.3 完整推理流水线:检测器框人、姿态网络出点、时序分类器出动作
现在进入最关键的一步:把三段模型串成一条完整的推理流水线。这条流水线是这套源码包的核心骨架,代码逻辑基本固定——目标检测器先从画面里把人框出来,姿态网络对每个框输出关键点坐标,坐标序列攒够一个窗口长度后交给动作分类器。
def infer_video(video_path, detector, pose_model, action_model, window_size=16): cap = cv2.VideoCapture(video_path) keypoint_buffer = [] # 攒够 window_size 帧就做一次动作分类 while True: ret, frame = cap.read() if not ret: break boxes = detector(frame) # 返回每个人体的边界框 [x1, y1, x2, y2, score] for box in boxes: # 姿态网络只处理单人框,输出 17x3(坐标 + 置信度) kpts = pose_model(frame, box) # 关键点要归一化,否则离镜头远近会直接影响分类结果 kpts_norm = normalize_keypoints(kpts, box) keypoint_buffer.append(kpts_norm) if len(keypoint_buffer) == window_size: action_label = action_model(keypoint_buffer[-window_size:]) draw_result(frame, box, kpts, action_label) keypoint_buffer.pop(0) # 滑窗:移出最老的一帧 # 写入输出视频或直接显示这段主循环有 3 个必须注意的设计点。第一,关键点一定要归一化,这是动作分类能否泛化的分水岭。我见过太多人直接把像素坐标喂给分类器,结果同一个「挥手」动作,人离镜头近时判定为挥手,离镜头远时判定为站立——因为近处的关键点位移幅度大,远处的位移幅度小。常见做法是把坐标除以检测框的高度,让特征和人的尺度解耦。第二,滑窗重叠是必要的,window_size设 16、步长 1,相邻窗口重叠 15 帧,保证动作标签切换平滑,不会跳变。如果步长设 16,视频会像抽风一样一卡一卡地刷标签。第三,一个人时这么跑没问题,多人场景要按检测框逐个处理,动作分类器天然支持多实例,因为每个实例的输入是独立的关键点序列。
4. 训练与调参:结构、超参与后处理的参数细节
4.1 模型结构参数:输入分辨率、热图尺寸和关键点数对齐
训练姿态模型时,最先要确定的是输入分辨率和热图输出尺寸。这两个参数决定精度上限,也决定显存占用和推理延迟。标准配置是输入 256×192,骨干输出经过反卷积上采样后的热图是 64×48。输入分辨率越低,模型越快,但小目标的关键点可能直接糊掉;分辨率越高,精度越好,但训练速度和显存压力成倍增加。
| 输入分辨率 | 热图尺寸(两层反卷积后) | 适合场景 |
|---|---|---|
| 192×192 | 48×48 | CPU 环境、快速原型验证 |
| 256×192 | 64×48 | 标准配置,精度/速度均衡 |
| 384×288 | 96×72 | 小目标多、遮挡严重的场景 |
热图尺寸和输入尺寸的比值就是关键点解码时的 stride,后面从热图还原坐标时必须除以这个比例。改输入分辨率时要注意三处联动:数据预处理的缩放尺寸、热图标签生成时高斯核的尺寸、以及解码时回映射坐标的系数。这三个地方只要有一处没改,训练出来的模型必然偏差。很多源码包只改了一个地方就跑训练,结果 loss 能降但验证时关键点全部偏移,这就是三处不一致造成的。
4.2 训练超参:学习率、batch size、epoch 与收敛节奏
姿态估计的训练超参和普通图像分类差别不小。最常用的策略是 SGD + Momentum,学习率从 1e-3 起,配合 cosine 退火,跑 100 到 140 个 epoch。AdamW 在这个任务上收敛确实快,但实测经常停在次优解上,热图峰值不够尖锐,最后精度不如 SGD。batch size 在单卡上设 32 到 64 都合理,显存不够就减半,同时把学习率也跟着减半,因为大 batch 需要更大的学习率才能保证同等收敛速度。
# 姿态模型训练的基本配置,参考常见做法按需调整 optimizer = torch.optim.SGD( model.parameters(), lr=1e-3, # 初始学习率,batch size 减半时建议同步减半 momentum=0.9, # 动量项,稳定收敛方向 weight_decay=1e-4, # 权重衰减,防止过拟合 ) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR( optimizer, T_max=140, # 总 epoch 数,退火周期和训练长度对齐 eta_min=1e-5, # 最低学习率,防止后期震荡 )这里有两条实战经验值得记下来。第一,带 warmup 会让训练更稳,前 5 个 epoch 把学习率从 1e-4 线性升到 1e-3,能避免一开始 loss 爆掉。第二,验证集上的准确率比训练集 loss 更重要——姿态模型天然容易过拟合训练集,训练 loss 降得很漂亮、验证集关键点准确率不涨是常态,需要在训练过程中持续保存验证集最优的权重,而不是最后一轮的权重。这个源码包的说明文档里如果写了评估指标,优先按它来卡训练节奏。
4.3 后处理参数:热图解码、高斯 sigma 和测试时增强
姿态模型输出的是热图,要把热图变成坐标还需要一步解码。常见做法是取热图最大值的位置,再乘回 stride。这个环节看起来简单,实际决定关键点抖不抖。一个经常被忽略的细节是:直接从热图取整数坐标,关键点在连续帧之间会有像素级跳动,对后续动作分类产生噪声。改进做法是在峰值附近做亚像素插值,或者用热图质心替代最大值。
import numpy as np def heatmap_to_keypoint(heatmap, stride=4): # heatmap: [K, H, W],输出 K 个关键点的 (x, y, score) K, H, W = heatmap.shape keypoints = np.zeros((K, 3)) for k in range(K): h = heatmap[k] idx = np.argmax(h) y, x = np.unravel_index(idx, (H, W)) score = h[y, x] # 亚像素细化:取峰值附近 3x3 邻域的加权质心 if 0 < x < W - 1 and 0 < y < H - 1: patch = h[y-1:y+2, x-1:x+2] patch_sum = patch.sum() + 1e-8 x = (patch * np.array([[-1, 0, 1]] * 3).T).sum() / patch_sum y = (patch * np.array([[-1, -1, -1], [0, 0, 0], [1, 1, 1]])).sum() / patch_sum keypoints[k] = [x * stride, y * stride, score] return keypointsstride的计算方式是输入分辨率除以热图分辨率,比如输入 256 宽、热图 64 宽,stride 就是 4。亚像素质心的改进码会让时间序列平滑不少,动作分类的准确率往往能提升 2~3 个百分点,属于性价比极高的改动。测试时的另一项标准操作是翻转测试:把输入图水平翻转推理一次,再把热图翻回来,和原始热图取平均,能抑制左右对称关节的偏差。这个操作在验证集上稳定涨点,代价是推理时间翻倍,实际部署时按需取舍。高斯核的 sigma 值一般设 2,如果热图峰值太散,可以先从 sigma 调起,而不是动网络结构。
5. 避坑清单:人体关键点检测与动作分类的 5 个高频雷区
5.1 关键点检测阶段:偏移、丢目标与 loss 横盘
坑一:关键点整体偏移但 loss 正常下降。现象是训练时 loss 一路走低,但可视化时所有关键点都偏离真实关节位置几个像素。原因是数据预处理时直接把检测框拉伸到模型输入尺寸,没有保持宽高比。人体框一般是长方形的,强制缩放到正方形等于把图像横向压扁,关节坐标跟着一起变形。解决方法是统一走「等比例缩放 + 灰边填充」的流程:把检测框内图像按长边缩放到输入尺寸,短边用 128 灰度值填充,记录缩放系数和填充偏移量,推理完成后反推回原图坐标。这一条是血泪经验,基本每个从零开始做姿态识别的人都会踩一次。
坑二:多人场景下检测框数量不稳定,关键点频繁丢失。现象是同一段视频里,有时检测出 3 个人,有时只检测出 1 个,动作分类结果随之乱跳。原因通常是目标检测器的 NMS 阈值设置不当,两个重叠的人被合并成了一个框。解决方法是先把 NMS 阈值调到 0.4~0.5 区间,同时检查检测器的置信度阈值——设太高会把低置信度的真实人体框滤掉,设太低会把人以外的物体框进来。另外要注意姿态模型一次只处理一个框,如果源码包把多人图像整图塞给姿态网络,输出会出现多组关键点混叠,这种项目结构本身就有问题,建议改成按检测框逐个裁剪。
坑三:loss 一直横盘不降,训练像玄学。现象是训练跑了十几个 epoch,loss 在初始值附近不动。排查顺序应该是:先可视化训练标签的热图,看看高斯峰值是否落在正确关节位置。最常见的原因是标注坐标和图像尺寸不匹配——标注是在原图上做的,预处理时缩放到了 256×192,但标签坐标没有对应缩放,高斯核画到了错误位置。其次是输出热图的分辨率和标签热图分辨率不一致,网络输出 64×48,标签却是 128×96,loss 永远对不齐。这种问题靠瞪眼很难发现,写一个把标签热图叠加到输入图上的可视化函数,一眼就能看清问题在哪。
5.2 动作分类阶段:静态动作混淆与训练推理输入不一致
坑四:「站立」和「行走」分不清,准确率卡在 70% 上不去。现象是验证集里这两个类互相混淆严重,其他动作分类都正常。原因大概率是分类器吃的是单帧关键点,模型压根看不到位移信息。解决方法是把输入改成连续帧的关键点序列,让分类器看到关键点坐标随时间的位移变化。一个简单的改法是把输入从 (17×3) 变成 (16 帧 × 17×3),分类头换成 1D 卷积或 LSTM。如果源码包已经用了序列输入但还是分不清,再检查是不是归一化方式把位移信息抹掉了——比如按整个人体框的高度归一化,人走近走远时躯干长度变化会被消除,但行走的摆动幅度特征还在,正常来说不会抹掉。
坑五:训练时准确率很高,一到真实摄像头画面全乱。现象是离线测试集上准确率 90% 以上,接上摄像头实时推理时动作标签疯狂跳变。原因通常是训练和推理的数据预处理不一致——最常见的是训练用绝对像素坐标,推理时忘了归一化;或者训练时做了随机缩放增强,但推理时没有统一到同一个尺度空间。排查方法是把训练输入特征和推理输入特征的数值分布各打印一组出来对比,看看范围是否一致。另一个高频原因是从姿态网络得到的关键点置信度没有过滤,低置信度点在时间序列上形成尖峰噪声。解决方案很简单:置信度低于 0.3 的关键点直接置为 0,或者在序列上做一个 3 帧滑动平均,噪声立刻被压下去。这两个动作对准确率的提升立竿见影。
6. 验证与进阶:让模型不仅「跑得通」,还「靠得住」
6.1 可视化验证:把关键点画回原图,检查每一帧的对齐质量
模型训完后第一件事不是看准确率,而是做可视化验证。把推理得到的关键点画回原图,连着播放几十帧,重点看三处:关节是否贴合骨骼、左右手有没有抖动、遮挡时关键点是否跳到背景上。可视化能发现维度指标完全看不出的问题——比如准确率 95% 的模型,在左臂被身体遮挡的帧里把肘关节标到了腰上,这类错误在混淆矩阵里根本不会体现。画骨架时顺便把置信度分数打出来,低置信度帧一眼就能定位。
6.2 进阶技巧:用混淆矩阵定位短板,再决定要不要换时序头
准确率只能告诉你模型行不行,混淆矩阵才能告诉你模型差在哪。把验证集预测结果做成混淆矩阵,重点看对角线之外哪些类互相混淆。如果「坐下」和「跌倒」分不清,说明这两类的骨架特征太接近,需要补样本;如果「挥手」和「站立」混淆,说明时序信息不够,需要考虑把分类器的全连接头换成 1D 卷积或 LSTM,加大时序感受野。这一步做完你才知道瓶颈在数据还是在结构,而不是盲目加训练轮数。
6.3 最后的习惯:先小后大,先单后多
我自己第一次做这个方向时踩得最疼的一脚,是直接在多人视频上调参,结果检测、姿态、分类三个环节的误差叠在一起,怎么调都找不到根因。后面我养成了个习惯:先用单人单动作的干净视频验证每一段单独的效果,确认姿态模型不飘、分类模型能分清,再逐步加遮挡、加多人、加速率变化。每一轮只改一个变量,跑出来的结果才能给你确定的反馈。希望这个习惯能帮你在做这套源码包的时候少走几段弯路,也帮你把每个环节的改动都变成可解释、可回退的决策,而不是靠运气跑出一个好看的数字就收工。
本文还有配套的精品资源,点击获取