☰
基于CNN的人体姿态与动作识别:从骨架关键点到模型部署
2026/10/2 18:16:54 网站建设 项目流程

简介:面向人工智能课程大作业与人体姿态识别入门实践,这套 Python 项目基于 CNN 实现了姿态估计与动作分类,涵盖数据获取、姿态检测、模型训练、测试等完整流程。源码按模块拆分,配合 Markdown 文档说明,便于理解网络结构与调用逻辑,适合高校学生参考、拓展或直接作为大作业交付内容。压缩包共 6 个文件,其中 5 个 Python 脚本负责数据预处理、姿态关键点检测、模型训练与推理主流程,1 份说明文档辅助梳理实现思路,整体仅 9KB,轻量易用且本地编译已通过,评审分达 95 分以上。目前已有 316 人浏览学习,热度不错。读者拿到后可直接运行验证,也可重点研究 CNN 的输入预处理、姿态关键点提取与动作分类思路,并依据 README 快速定位各模块职责,节省课程设计阶段的排错与整理时间。

1. 开箱先别急着跑:这套“人体姿态+动作识别”作业到底在解什么题

每到人工智能课程末期,总能看到一批二十几兆的压缩包在同学之间流传,名字通常就叫“人工智能大作业——python实现的基于CNN的人体姿态和动作识别+源代码+文档说明.zip”。解压之后里面往往是一份项目文档、几个Python脚本和一部分处理好的数据,看起来什么都有,但真正能把它跑通、把原理讲清楚、最后顺利交上去的人并不多。这类作业的本质是两件事:先用卷积神经网络从图像或骨架序列里提取人体姿态特征,再把这些特征映射成动作类别,比如走路、坐下、挥手、下蹲。

这份笔记写给正在写或准备写这类作业的人。不管压缩包里的代码是PyTorch还是TensorFlow,不管文档写得多简略,只要你把“视频进,动作标签出”这条主链路拆清楚,剩下的就是改参数和补实验的事。我会从数据流讲起,再给最小可运行代码,最后交代最容易被扣分的几个坑——按这个顺序走,你至少能交出一份老师挑不出硬伤的作品。

2. 把CNN动作识别拆成可交付作业:数据流、标签空间与输入张量

2.1 姿态估计和动作分类是两个任务,很多作业写混了

拿到“人体姿态和动作识别”这个题目,第一件事不是打开代码,而是先分清两个概念:人体姿态估计(human pose estimation)和人体动作识别(action recognition)。姿态估计要回答“人的肩膀、手肘、膝盖在画面什么位置”,输出通常是18个或33个关键点的像素坐标;动作识别要回答“这个人现在在做什么动作”,输出是一个类别标签,比如“站立”“深蹲”“挥手”。

很多学生作业把这两个词混在一起,代码里却只做了其中一个。常见做法是:先用一个现成姿态估计模型拿到每帧关键点坐标,再用CNN对这些坐标序列做分类,从而得到动作类别。这套方案避开“裸视频直接分类”的难题——直接拿原始视频帧训练CNN,通常需要很大的数据集和多阶段处理,不适合课程作业。而基于骨架关键点的方案轻量、可解释性强、训练快,属“大作业最常见的可靠做法”。

CNN在这个方案里的角色值得单独说。卷积神经网络在这类任务里是核心技术,但它并不总是直接吃原始图像。许多作业里的CNN输入是“时间窗口内关键点坐标构成的张量”,卷积核沿时间维度滑动,提取出动作的动态特征;也有作业把每帧关键点画成热图,再让CNN做空间特征提取。这两种路线都能交付,区别只在于输入张量长得不一样。你要在文档里先写清楚自己选的是哪一条,否则答辩时老师问“输入到底是什么形状”,你很难自圆其说。

2.2 从视频到动作标签的流水线:骨架序列与滑动窗口

一套能跑通的最小流水线一般长这样:视频按固定帧率采样,每帧经过姿态估计模型得到关键点坐标,坐标做归一化后按时间顺序拼接成序列,再用一个滑动窗口把长序列切成若干样本,每个样本对应一个动作类别。窗口长度通常取16帧或32帧,帧率的选取要保证窗口覆盖一个完整动作周期的长度。

下面是这份作业里最核心的数据构造步骤:把姿态估计输出的坐标整理成可训练的样本。

import numpy as np # 假设 pose_sequence 是从视频中按帧提取的关键点坐标 # 形状: (总帧数, 关键点数 * 2),例如 MediaPipe 33个关键点就是 (总帧数, 66) pose_sequence = np.load("pose_sequence.npy") window_size = 32 samples = [] labels = [] # 用一个滑动窗口切成样本,窗口内就是一次动作的完整过程 for i in range(0, len(pose_sequence) - window_size, window_size // 2): window = pose_sequence[i:i + window_size] samples.append(window) samples = np.array(samples) print("样本形状:", samples.shape)

这段代码里有两个关键点。窗口大小设置为32帧,滑动步长为窗口的一半,能让相邻样本之间保留50%的重叠,数据量翻倍,训练更稳定;如果步长等于窗口大小,每个视频只能切出极少数样本,模型容易欠拟合。实际作业中你可以把步长调成16甚至8,但要注意重叠太多会让训练集和验证集之间的样本高度相似,指标虚高,答辩时不抗问。

2.3 标签空间和类别均衡:先数清楚你有几类动作、每类多少样本

很多作业翻车不在模型,而在标签。下载的源代码里常见的目录结构是:每个动作一个文件夹,文件夹里放视频或关键点文件。你的标签往往就是文件夹名,这一步看似简单,实际上最容易出错。

我一般会先写一个统计脚本,把每一类动作的样本数量、每个视频的帧数、序列总长度一次看清楚。类别样本数如果悬殊很大,比如“站立”有500个样本,“跌倒”只有30个,CNN会学成“永远输出站立”,损失值照降、准确率近90%,答辩现场一演示就露馅。遇到这种情况有两个处理手段:对少数类做重复采样,或者为损失函数加类别权重。后者在PyTorch里就是CrossEntropyLoss(weight=class_weights)一行的事,但要在文档里写明你用了什么策略,不要藏着。

另外要注意,动作类别不是越多越好。作业题常给出5到10类动作就足够,类别越多,相邻动作越难分,比如“坐下”和“蹲下”在骨架序列上非常接近。选类别时尽量挑空间特征差异明显的动作,减少后期混淆矩阵里的大面积错分。

3. 用最小代码把流程跑通:Python环境、MediaPipe抽骨架与CNN前向推理

3.1 环境配置与项目结构检查:先看文档再装依赖

下载的压缩包里通常有requirements.txt或文档说明,第一步先别急着安装,打开它扫一眼依赖版本。这类大作业最常写的依赖是opencv-python、numpy、tensorflow或torch,偶尔带mediapipe和scikit-learn。给新手的建议是:单独建一个Python虚拟环境,不要直接装进全局环境。Python版本别追最新,3.8到3.10在这个技术栈里最稳,太新的版本有时会遇到预编译包还没跟上,排起错来相当折磨。

# 在项目根目录做三件事:查文件结构、建虚拟环境、装依赖 ls -R # 先看有没有 data/ models/ 目录,分清哪是训练代码哪是推理代码 python -m venv venv source venv/bin/activate # Windows 上为 venv\\Scripts\\activate pip install -r requirements.txt

这里要特别提醒一句:如果requirements.txt里写的是tensorflow且版本号是1.x,那你很可能遇到老项目跑不动的情况,不要硬跑,可以直接把模型文件检测出来重写训练脚本。做这行最常见的可靠做法是把“环境能复现”当作作业的一部分,文档里写清楚Python版本和核心库版本。老师现场检查时最讨厌的事,就是你项目能在自己电脑上跑、换台电脑就崩。

3.2 抽骨架:MediaPipe的33个关键点怎么喂进CNN

姿态估计部分现在90%以上的大作业用MediaPipe,因为它CPU就能跑、不依赖GPU、一段代码就拿到33个关键点。下面这段是把视频拆成关键点序列的最少代码:

import cv2 import mediapipe as mp import numpy as np mp_pose = mp.solutions.pose pose = mp_pose.Pose( static_image_mode=False, model_complexity=1, min_detection_confidence=0.5, min_tracking_confidence=0.5, ) cap = cv2.VideoCapture("action_video.mp4") keypoints_list = [] while cap.isOpened(): ok, frame = cap.read() if not ok: break rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results = pose.process(rgb) if results.pose_landmarks is None: # 检测失败时补一行零向量,保持帧号对齐,后续可在数据清洗时剔除 keypoints_list.append(np.zeros(33 * 2, dtype=np.float32)) continue pts = [] for lm in results.pose_landmarks.landmark: pts.extend([lm.x, lm.y]) keypoints_list.append(np.array(pts, dtype=np.float32)) cap.release() pose.close() keypoints_all = np.vstack(keypoints_list) np.save("pose_sequence.npy", keypoints_all)

这段代码要解释三个参数。model_complexity取1是速度与精度平衡,取2精度更高但CPU会明显变慢;min_detection_confidence控制“画面里有没有人”的判定,默认0.5够用;关键点坐标取的是归一化后的lm.x和lm.y,范围0到1,天然适合直接喂CNN,不需要再做min-max归一化。画面里有人但姿态没检测出来时,上面代码会用全零帧占位,你后面要专门处理这些空帧,否则CNN会学到“全零=某个动作”这种假规律。

3.3 把关键点序列拼成CNN输入:窗口、归一化与标签编码

抽完骨架后处理的是原始关键点,但想要让CNN输入稳定,需要再做一套序列张量。把每帧的66维坐标(33个关键点×2)按照时间顺序堆叠,单条样本维度就是(32, 66),再把它转换成(1, 32, 66)或(1, 66, 32)喂给卷积层。多数作业代码里会用Conv1d来处理,内核沿时间方向扫过,相当于用一组可学习滤波器去识别“肘部逐渐上抬”这类动态模式。

下面给一个用PyTorch写的最小CNN骨架结构,专门处理坐标序列输入:

import torch.nn as nn class PoseActionCNN(nn.Module): def __init__(self, num_keypoints=33, hidden_dim=64, num_classes=5): super().__init__() # 输入形状: (batch, window_size, num_keypoints*2) # 将最后一维当作通道维,Conv1d在该维上做卷积 self.cnn = nn.Sequential( nn.Conv1d(num_keypoints * 2, hidden_dim, kernel_size=3, padding=1), nn.ReLU(), nn.Conv1d(hidden_dim, hidden_dim, kernel_size=3, padding=1), nn.ReLU(), nn.AdaptiveAvgPool1d(1), ) self.classifier = nn.Linear(hidden_dim, num_classes) def forward(self, x): # x: (batch, window, 66) x = x.permute(0, 2, 1) # 变成 (batch, 66, window) feat = self.cnn(x).squeeze(-1) return self.classifier(feat)

这个结构本质上是一个二维堆叠的微型CNN,两个卷积层加上一个全局池化,参数量很小,跑一二十个epoch不会过拟合得太狠。kernel_size=3表示卷积核覆盖3帧,padding=1让时间长度不变。真正调参时你要掌握一个原则:窗口拉长,卷积核感受野也要跟着拉长,否则尾部的动作特征很难被前面的卷积层看到。

3.4 三个必调的参数:窗口、置信度与类别权重

这套方案从头到尾最关键且最常被忽略的参数有三个。第一个是窗口长度:16帧适合快动作如“挥手”“拍手”,32帧适合“坐下”“站起”,48帧适合“跌倒”“弯腰捡东西”。窗口小于一个完整动作周期,模型只能看到动作的一部分;窗口太长,动作做完后多余的静止帧会稀释特征,精度不升反降。第二个是姿态检测的置信度阈值:室内固定机位、背景干净时调到0.3,背景杂乱或动作幅度大时调到0.7,但调高后检测不到的帧会变多,样本缺失率也随之上升。第三个是类别权重,写成torch.nn.CrossEntropyLoss(weight=torch.tensor([1.0, 2.5, 3.0, ...])),给少数类的损失放大,防止模型被多数类带偏。

参数默认值推荐调整范围调整依据
窗口长度32帧16—48帧覆盖一个完整动作周期
min_detection_confidence0.50.3—0.7背景复杂度与检测失败率
类别权重不设置按类别样本数反比训练集类别是否均衡
滑动步长窗口一半窗口的1/4到1/2需要多少训练样本

我见过最多的失败案例,是窗口、置信度、权重全用默认值一个没调,最后取了个83%的准确率就交差。其实这三项每调一次都能看到可度量的变化,是文档里最好写的“实验对比”,也是老师最愿意看到的调参过程,不要浪费。

4. 把训练结果变成答辩硬素材:损失曲线、混淆矩阵与实验记录

4.1 数据划分:按视频分组,别按帧随机切

训练之前,必须先过数据划分这一关。很多下载的源代码里直接写train_test_split(X, y, test_size=0.2),这对视频序列数据来说是个隐患:同一个视频的相邻帧高度相关,随机划分会把同一段动作的片段同时放进训练集和验证集,验证精度虚高。老师抽查代码后只要指出这一点,你的实验可信度会大打折扣。

正确做法是按视频或按人分组切分。如果数据集中每个动作视频来自不同录制者,分组粒度就按视频文件名来;如果同一人录了多段视频,就按人来分,确保验证集里的动作模式不是训练时见过的同一人。

from sklearn.model_selection import GroupKFold # sequences: 样本特征,labels: 动作标签,video_ids: 每条样本来自哪个视频 gkf = GroupKFold(n_splits=5) for train_idx, val_idx in gkf.split(sequences, labels, groups=video_ids): X_train, X_val = sequences[train_idx], sequences[val_idx] y_train, y_val = labels[train_idx], labels[val_idx]

GroupKFold的groups参数接收每条样本所属的视频ID,它能保证同一视频的样本只出现在同一折里。交叉验证做5折,每次用4折训练1折验证,最后报告5折平均准确率,这一条写进课程文档比任何图表都有说服力。没做分组划分的,答辩被问到“数据泄露”就很容易卡住。

4.2 混淆矩阵加分类报告:找出最容易混淆的两个动作

模型训练完,别只看准确率一个数,要生成混淆矩阵和分类报告。混淆矩阵能告诉你“坐下”有多少次被错分成“蹲下”,“站立”是不是被模型当成了“走路”。这些错误往往集中在动作边界相似、运动幅度较小的类别里,分析清楚后你可以在文档里写“错分原因”而不只是贴图。

import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix, classification_report import numpy as np cm = confusion_matrix(y_val, y_pred) plt.figure(figsize=(6, 5)) plt.imshow(cm, cmap="Blues") plt.colorbar() plt.xlabel("预测类别") plt.ylabel("真实类别") for i in range(cm.shape[0]): for j in range(cm.shape[1]): plt.text(j, i, str(cm[i, j]), ha="center", va="center") plt.xticks(range(len(class_names)), class_names, rotation=45) plt.yticks(range(len(class_names)), class_names) plt.tight_layout() plt.savefig("confusion_matrix.png", dpi=150) print(classification_report(y_val, y_pred, target_names=class_names))

这里几行代码做的事是:把预测结果与真实标签逐一比对,矩阵第i行第j列表示真实类别i被预测成类别j的次数。对角线上数字越大越好,非对角线上的聚集块就是你动作定义的边界问题。我一般会把准确率最低的两个类别单独截出来做对比,改成“这两个动作在骨架序列上的差异主要发生在哪些关节、哪些时间区间”,这比盲目调模型参数更接近问题本质。

4.3 实验日志:把每次改动记成一个CSV行

做深度学习大作业最容易犯的毛病是“改了配置忘了记”。窗口从32改到48,模型准确率升了还是降了,过了两天就说不清了。答辩时老师问“你这个0.02的准确率提升是怎么调出来的”,回答“我试了好几次”没有任何说服力。

一个能避免这种情况的朴素手段是建一个实验日志CSV,每次运行前把关键配置和结果追加为一行。这不需要任何额外工具,用Python标准库里的csv就能搞定。

import csv import datetime log_path = "experiments.csv" row = [ datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), "window=32", "step=16", "group_split=5fold", "lr=0.001", "batch=64", "val_acc=0.873", "note=加全体关键点归一化", ] with open(log_path, "a", newline="") as f: writer = csv.writer(f) writer.writerow(row)

这份日志有两个用法:一是横向比较同参数不同数据预处理带来的精度变化,二是给答辩PPT做实验记录表。记录项里包含时间、窗口、步长、切分方式、学习率、批次大小、验证精度和备注,一张表就能看出整个项目的调参过程。作业文档里附上这个表,给人的感觉不是调出来的分数,而是系统做出来的实验。

5. 大作业避坑指南:5个让作品在最后一步翻车的真问题

5.1 骨架检测在部分视频里疯狂丢帧

现象:训练时样本量足够、准确率也能看,但换到一段新视频上推理时,姿态估计频繁输出空结果,动作识别直接失灵。

原因:MediaPipe对光照、画面大小、人距镜头的远近敏感。手机横屏录的视频、逆光环境、人物过小,都可能导致单帧检测置信度低于阈值,返回pose_landmarks=None,关键点序列里出现大量全零帧。

解决:先统计一下所有训练视频的丢帧率,丢帧超过20%的视频要做预处理,最简单的做法是将画面做归一化后裁剪成人像中心区域,再送入MediaPipe。另一个有效手段是降低min_detection_confidence到0.3,同时开启model_complexity=1,牺牲一点实时性保证骨架不中断。零帧保留成占位符是最后一个选择,训练时要设置mask,避免模型学到“全零帧等于动作”这种错误规律。

5.2 固定随机种子没设,结果重跑就变

现象:同一份代码昨天训练验证精度0.85,今天重新跑一遍变成0.79,连损失曲线形状都变了。

原因:深度学习训练涉及随机初始化、数据加载顺序和Dropout,每个环节都受随机种子影响。训练集内部随机打乱只要发生过一次,模型的收敛路径就完全不同。

解决:在训练脚本最前面按固定顺序设置随机种子:Python的random.seed(42)、NumPy的np.random.seed(42)、PyTorch的torch.manual_seed(42),如果用了CUDA还要加上torch.cuda.manual_seed_all(42)。设置之后不要随便改动数据加载部分的随机逻辑,保证“一键复现”效果。给老师演示前重跑一遍全流程仍能拿到相近结果,这能避开“代码在你这能跑,在我这就崩”的最致命质疑。

5.3 同一人的动作被切进训练和验证,指标虚高

现象:训练集、验证集准确率接近一致,都高达0.95,原本以为模型很强,换到新视频上表现却明显下降。

原因:前面提到的随机划分把同一个视频的相邻窗口同时分到训练集和验证集,模型“见过”验证集样本,相当于开卷考试。

解决:改成按视频分组划分,具体做法见4.1节。严格点还可以在录制数据集时让不同动作片段之间留有间隔帧,并在切窗口时丢弃头部和尾部的过渡帧。验证时如果条件允许,用另一台设备重新录一段同场景动作做外场测试,那才是模型真实水平。

5.4 CNN输入尺寸写错了,训练时Loss不断NaN

现象:训练一开始Loss就是nan,或者到几十个epoch后突然变成nan,模型输出结果全一样。

原因:多数是输入张量带NaN或无穷值,常见来源包括骨架坐标除零、数据里有空白视频帧、学习率过大导致梯度爆炸。手势或身体关键点在画面边缘时会输出异常大坐标,归一化后仍超出合理范围。

解决:数据送入网络前先做数值体检,用np.isfinite(sequence).all()断言张量内无无穷值,查出非法帧后做差值填补或直接丢弃。再把学习率从0.001降到0.0003,在模型里加梯度裁剪,如PyTorch的torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)。这三步按顺序排查,几乎能解决九成以上的NaN问题。

5.5 关掉训练脚本后发现模型文件加载错误

现象:训练结束把模型保存成model.pth,次日写一个推理脚本重新加载,权重文件能读取但维度对不上,或类别数量对不上,前向传播直接报错。

原因:保存时只存了权重字典state_dict,但训练模型和推理模型在类别数或输入关键点数目上不一致。丢失classes映射文件也会让标签张冠李戴。

解决:保存模型时把类别映射一并保存为一个JSON文件,推理时用同一个参数重新构造模型结构再加载权重。

import json, torch # 保存时:把类别编号和名称映射一起写盘 model_info = {"num_classes": 5, "input_dim": 66, "class_names": class_names} with open("model_info.json", "w", encoding="utf-8") as f: json.dump(model_info, f, ensure_ascii=False, indent=2) torch.save(model.state_dict(), "model.pth") # 推理时:先读配置,再重建网络结构 with open("model_info.json", "r", encoding="utf-8") as f: model_info = json.load(f) model = PoseActionCNN( num_keypoints=model_info["input_dim"] // 2, num_classes=model_info["num_classes"], ) model.load_state_dict(torch.load("model.pth", map_location="cpu"))

保存model_info.json之后,模型文件从“黑匣子”变成可读配置,新机器上部署不用猜结构,这条习惯在课程作业和以后的实际项目里都能省去很多麻烦。

6. 答辩前最值得做的三件事:重叠窗口、消融对比和误判样本分析

第一件,把滑动窗口的步长从窗口一半缩短到四分之一,让训练样本翻倍。很多下载的源代码只按窗口长度切一次,一个视频只能产出几个样本,CNN根本学不出动作的中间状态。步长缩短后样本之间有大量重叠,模型能看到动作从起始到结束的连续过渡,这对“跨帧特征”的学习帮助非常明显。代价是训练时间变长,但大作业的数据规模通常只有几百到几千段样本,完全能接受。

第二件,做一组消融对比,并把结果整理成表格放进文档。对比项不用多,三组足够:不使用骨架归一化、使用但窗口长度32、使用且窗口长度48。每组固定其他超参,只把验证精度记下来。这组对比的意义不在于证明哪个最好,而在于展示你理解每个环节对结果的影响。答辩时被问到“为什么这么设置参数”,直接拿出表格指给老师看,比任何口头解释都有力。

第三件,主动保留一些误判样本,并分析错在哪个关节或哪段时间区间。比如“坐下”被错判成“下蹲”,就检查究竟是膝盖角度的轨迹像下蹲,还是头部关键点在俯身阶段波动太大。把错误样本连同骨架可视化图放进PPT,先展示原因再说明调整方案,这会让整份作业的深度立刻上一个台阶。

我自己每次交这类作业前都会做一个固定动作:把随机种子固定,把测试视频换成一段自己重新录制的素材,当着摄像头完整跑一遍推理流程,看它能不能稳定输出预期标签。这个测试通过后再打包代码和文档,就不会出现答辩现场模型抽风的情况。希望这套从上手到排坑、再到展示的路径能帮到你,顺利把这份“人工智能大作业”做成一份拿得出手的作品。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询