
简介这是一套基于时空图卷积网络ST-GCN的骨骼动作识别毕业设计项目面向计算机视觉与深度学习方向的本科生或研究生。项目将人体骨骼建模为时空图同时利用图卷积捕捉关节拓扑与时间动态可识别行走、挥手、举重等动作适用体育、医疗、人机交互等场景。资源共91个文件压缩包约52.56MB。其中包含29个Python脚本覆盖数据处理、模型训练与可视化13个YAML配置用于设置模型结构和训练参数3个PT权重文件可直接加载预训练模型另有11个GIF动图、3个MP4视频展示识别效果以及TXT说明、PNG图示和依赖清单便于环境搭建。目前已有159人学习。代码结构清晰附NTU与Kinetics数据处理工具、OpenPose骨骼提取脚本、离线/实时演示程序并提供带注意力增强的ST-GCN变体。通过说明文档和主程序可复现实验理解算法细节便于扩展毕业设计。1. ST-GCN骨骼动作识别这份Python毕设资源到底能拆出什么做毕设选动作识别方向时我最初以为拿卷积神经网络跑视频帧就行一路做到实验才发现真正的瓶颈在于人体骨骼关键点的时空建模。ST-GCN把骨架当成一张图关节点是节点骨头是边再用时间维度的卷积去捕捉运动趋势这种思路刚好弥补了传统ConvNet对非欧几里得结构的不适应。这个压缩包就是一套完整的Python实现从NTU和Kinetics数据预处理到ST-GCN单流/双流模型训练再到预训练权重和实时demo演示都有。对毕设来说它最大的价值是给你一个能直接跑通的起点不用从零搭环境也有现成模型做对比。适合深度学习或计算机视觉方向的学生以及想快速复现ST-GCN做对比实验的从业者。跟着下面的步骤把demo跑出来半小时就能看到骨骼图上的预测结果。2. 核心模块拆解从文件结构读懂ST-GCN的实现路径拿到压缩包先别急着解压跑我一般会先扫一遍目录搞清楚每个文件是干嘛的。这个项目的基本盘是ST-GCN官方代码的二次开发里面既有原版模型也有加了自定义边的版本。下面按原理和模块对应关系拆开讲。2.1 时空图卷积为什么要把骨架当成一张图骨骼动作识别输入的不是普通图像而是每一帧的人体关节点坐标。以NTU-RGB-D数据集为例每个动作序列由若干帧组成每帧最多两个人体每个人体有25个关节点每个点有x、y、z和置信度。这些点之间天然存在连接关系比如手腕连肘、肘连肩。把这种拓扑结构用图来表示就是ST-GCN的核心。空间图卷积的做法是用邻接矩阵描述关节点之间的连接通过矩阵乘法聚合每个节点相邻节点的特征。时间维度上同一个关节点在连续帧之间的变化可以看成一个小序列这里用一维卷积来处理。一个标准的时空图卷积层常见实现是这样# 时空卷积核心逻辑对应项目里的net/tgcn.py class ConvTemporalGraphical(nn.Module): def __init__(self, in_channels, out_channels, A): super().__init__() self.A nn.Parameter(A, requires_gradFalse) # 归一化邻接矩阵 [K, V, V] self.conv nn.Conv2d(in_channels, out_channels, kernel_size1) def forward(self, x): # x: [N, C, T, V] 批大小、通道、帧数、关节数 N, C, T, V x.size() x x.permute(0, 2, 3, 1).contiguous() # [N, T, V, C] x x.view(N * T, V, C) x torch.einsum(nvw,bwc-bvn, self.A, x) # 空间聚合 x x.view(N, T, V, C) return self.conv(x.permute(0, 3, 1, 2)) # 映射到输出通道这里A是经过归一化处理的邻接矩阵V表示关节数T表示帧数C是输入通道数。einsum这一行做的事情是把每个关节的特征和它的邻居特征加权求和权重就来自A。项目里的net/graph.py负责生成这个Anet/st_gcn.py则是把多个这样的层堆叠成完整模型。graph.py里的strategy参数不是随便填的。常见策略有uniform、distance和spatial。spatial策略把人体关节点按物理距离分成“比中心关节更近”“就是中心关节”和“比中心关节更远”三个子集类似卷积核的3x3划分实际效果最好。如果你改成uniform等于把所有邻居一样看待模型表达能力会明显下降。如果你要做图卷积相关的毕业设计读懂这一段代码比跑通训练更重要因为后面的所有改进比如加强关节边权重、增加跨层连接都要回到这个矩阵上来。2.2 项目文件与模块对应关系把压缩包解压后看到的文件虽然多但归类后其实很清晰。下面是最常用的部分路径/文件作用main.py程序入口解析--config和--weights等参数processor/processor.py训练、验证、测试流程的封装net/st_gcn.pyST-GCN单流模型定义net/st_gcn_twostream.py双流模型同时输入关节坐标和骨骼向量net/tgcn.py时间图卷积基本模块net/graph.py根据人体布局和策略生成邻接矩阵feeder/feeder.py加载npz数据做归一化和随机裁剪ntu_gendata.py把NTU-RGB-D原始skeleton文件转成npy/npzkinetics_gendata.py处理kinetics-skeleton数据torchlight/训练工具包提供日志、GPU管理models/OriginSTGCN.pt、AddEdgeSTGCN12345.pt、kinetics-st_gcn.ptmain.py是整个项目的入口它读取yaml配置创建一个processor实例然后调用processor.train()或者processor.test()。processor里封装了训练循环、学习率调度、日志输出和模型保存。torchlight有点像PyTorch-Lightning的早期版本负责把训练样板代码统一起来。feeder.py除了读取npz还会做数据增强。训练时它会随机选择一段固定长度的帧序列而不是只取前300帧还会随机上下翻转关节坐标增强模型的泛化能力。这些细节直接决定最终准确率很多毕设复现时故意把augmentation去掉发现测试结果差好几个点就是这个原因。双流模型值得单独说一下。st_gcn_twostream.py同时训练两个支路一条输入原始关节坐标另一条输入骨骼向量相邻关节点坐标差。项目里的kinetics-st_gcn.pt就是双流模型在Kinetics-skeleton上的预训练权重。因为双流比单流通常高2到5个百分点很多毕设会直接拿双流当baseline再在这基础上做改进。另外logData目录里存放了训练日志config.yaml和JustTest.py是快速试跑用的可以先拿它们验证环境再跑正式训练。2.3 从OpenPose到动作标签一条完整的识别链路项目里的openpose.py和recognition.py是用来做真实视频推理的。整体流程是视频或摄像头画面先经过OpenPose检测出人体关键点再把关键点序列按时间顺序堆叠成模型输入最后输出每个动作类别的置信度。项目里的demo_realtime.py就是实时版demo_offline.py是离线版。resource目录下的那些gif和png比如attentionprediction.png、demo_video.gif就是推理结果的可视化。模型能给出动作类别还能把注意力权重画到人体上红色区域表示当前帧模型重点关注哪些关节。这个能力在答辩时非常加分因为它能直观解释模型“看哪里”而不是一个黑匣子。DrawLine.py则是拿OpenCV简单画骨架连线用的适合在没有OpenPose的情况下快速查看数据。3. 环境搭建与数据准备Python版本、NTU和Kinetics的坑老项目最怕的就是环境不一致。这个项目里能看到__pycache__.cpython-37.pyc说明原实验环境是Python 3.7。如果你直接拿Python 3.10或3.11去跑大概率会碰到语法兼容问题。我一般在第一步就把这个问题锁死。3.1 依赖环境与Python版本锁定很多python安装教程都在教你装最新版但这里恰恰相反。建议用conda新建一个Python 3.7环境然后按requirements.txt装依赖。requirements.txt一般不会把PyTorch版本写死因为不同机器的CUDA版本不一样需要你自己选择。conda create -n stgcn python3.7 conda activate stgcn pip install -r requirements.txt # 如果requirements里没有torch单独安装适配CUDA的版本 pip install torch1.9.0为什么不建议用Python 3.7以上因为老代码里有些语法在3.8之后变了比如collections.Iterable会被移到collections.abc另外部分依赖库的版本也卡在3.7附近。我用3.8跑过能跑但偶尔有torchlight内部导入的小问题。如果项目里有.pyc编译缓存说明原环境是3.7就优先用3.7。装完依赖后先做一次导入检查python -c import torch, torchlight; print(torch.__version__)如果显示ModuleNotFoundError: torchlight别急着慌这个模块不是pip包它就是项目根目录下的torchlight文件夹只要你在项目根目录下运行Python就能找到。如果你习惯用VSCode写Python记得在解释器里选择刚才创建的stgcn环境否则一运行main.py就会发现import的是全局环境的torch各种版本冲突。提示如果非要用Python 3.8遇到collections.Iterable报错把代码里的导入改为from collections.abc import Iterable这类兼容性问题就会少很多。3.2 NTU-RGB-D数据生成流程NTU-RGB-D是最常用的骨骼动作数据集但原始数据是一堆txt文件没法直接送进神经网络。项目里的ntu_read_skeleton.py负责解析单个文件ntu_gendata.py负责批量转换。转换后的数据通常保存为.npz里面包含关节坐标、骨骼向量、类别标签和序列长度。常见的数据准备命令是这样# 将原始NTU骨架数据转换为训练用的npy数据 # 如果ntu_gendata.py在根目录就直接用在tools目录下就换成tools/ntu_gendata.py python tools/ntu_gendata.py --data_path /data/nturgbd_skeleton/ \ --out_folder ./data/NTU-RGB-D/npy执行前要把原始数据按NTU命名规则放好比如S001C001P001R001A001.skeleton这种格式。脚本会读取每帧人数和关节数跳过空帧并且根据info目录下的samples_with_missing_skeletons.txt过滤掉有缺失的样本。这个过滤很重要如果不做后面训练时可能会因为某个样本关节数不对导致维度爆炸。ntu_gendata.py内部还会有一个人体姿态归一化步骤常见做法是把每个序列的中心关节平移到原点并除以人体尺度让模型不依赖绝对坐标。如果你自己写预处理脚本这一点别漏掉否则同一动作在不同人身上表现差异巨大模型很难收敛。Kinetics-skeleton数据集的处理方式类似只不过原始数据是JSON格式由kinetics_gendata.py读取。完整数据需要提前下载项目里的get_models.sh主要用来拉预训练权重数据生成完后再把权重放进models目录。转换完的数据最好按官方目录结构放比如data/目录下分NTU-RGB-D和kinetics-skeleton。这样config里的data_path不用改。如果你自己改了路径记得同步修改yaml里的data_path和label_path否则feeder会因为找不到文件直接报错。3.3 用一行代码检查数据是否真生成成功数据生成完不要急着跑训练先验证一下维度。下面这段代码可以用来看npz里到底存了什么import numpy as np data np.load(data/NTU-RGB-D/npy/x_train.npz) print(data.files) # 一般输出[x, y, bone, A, num_frames, label, ...] print(data[x].shape) # 例如 (51213, 3, 300, 25)第一个维度是样本数第二个维度是坐标通道数关节流是x/y/z骨骼流是x/y/z的差第三个维度是帧数通常会固定到300第四个维度是关节数NTU是25。如果T维度不是固定值说明预处理时没有做帧采样训练时feeder可能通过random_shift或者frame_drop来统一长度这也是ST-GCN官方代码的常见策略。建议在跑训练前先单独跑一次数据加载测试python -c from feeder.feeder import Feeder; \ f Feeder(data_pathdata/NTU-RGB-D/npy/x_train.npz, \ label_pathdata/NTU-RGB-D/npy/y_train.npz, \ window_size300); \ print(f[0][0].shape)这里window_size300表示每个序列采样300帧如果数据本身不足300帧feeder会随机重复采样超过300帧会随机裁剪。这个设计让训练时的batch维度固定不会因为视频长短不同而出错。4. 训练与推理main.py、config.yaml和模型权重怎么用模型结构和数据格式都清楚后真正动手就是训练和推理了。这个项目里训练入口是main.py所有参数都写在yaml配置文件里。学会改yaml比改代码更重要。4.1 训练入口与配置参数解读config目录下有多个子目录比如st_gcn/ntu-xview、st_gcn/ntu-xsub、st_gcn/kinetics-skeleton。每个目录里通常有train.yaml和test.yaml。打开一个yaml常见字段如下配置项含义建议值batch_size单batch样本数16/32显存不够就降到8num_epoch训练轮数80120optimizer优化器SGDmomentum0.9lr初始学习率0.1step学习率衰减轮次[70, 90]num_class动作类别数NTU-60填60Kinetics填400num_worker数据加载线程数4或8deviceGPU编号[0]在项目根目录执行训练python main.py --config config/st_gcn/ntu-xview/train.yaml \ --work-dir work_dir/recognitionmain.py会加载yaml实例化对应的processor然后开始训练。这句话背后的过程包括读取数据集的索引、构建模型、把模型放到GPU、加载torchlight的日志系统。训练过程中work_dir里会持续输出log.txt记录每个epoch的loss、top1和top5准确率。如果你想对比单流和双流只要把model配置从st_gcn改成st_gcn_twostream其它参数保持一样。训练时终端会滚动输出类似Epoch 1, Loss: 2.3, Acc: 21.5%的信息。如果loss从2.5降到0.5acc稳步上升说明模型在学。如果loss卡在2.3不动多半是lr设置太高或者数据没归一化回到第3章检查。另外log.txt会保存完整历史跑完可以直接画loss曲线答辩时放在ppt里很直观。4.2 用预训练权重跑通推理从头训练NTU-60大概要几十个epoch在单卡上可能要跑一晚上。但项目里已经给了models/OriginSTGCN.pt、AddEdgeSTGCN12345.pt、kinetics-st_gcn.pt这三个权重文件可以先用来做测试。OriginSTGCN.pt是原版模型权重AddEdgeSTGCN12345.pt看起来像是给图增加了额外边之后重新训练的模型kinetics-st_gcn.pt是双流模型在Kinetics上的预训练。测试一条指令python main.py --config config/st_gcn/ntu-xsub/test.yaml \ --weights models/OriginSTGCN.pt这里test.yaml里的设置要和训练时匹配尤其是num_class和in_channels。如果你直接用Kinetics权重去测NTU最后一层分类维度不同加载会报size mismatch。遇到这种错误检查两个地方配置里num_class是否等于权重的训练类别数模型输入的in_channels是否为3关节坐标x/y/z。如果在代码里加载权重更直接的写法import torch from net.st_gcn import STGCN model STGCN(in_channels3, num_class60, graph_args{layout: ntu-rgb-d, strategy: spatial}) checkpoint torch.load(models/OriginSTGCN.pt, map_locationcpu) model.load_state_dict(checkpoint[state_dict]) model.eval()这段代码里graph_args中的layout指定使用NTU的人体关节定义strategy指定邻接矩阵的划分策略比如spatial就是按人体部位分组。checkpoint[state_dict]这一层是官方代码保存模型时的约定如果你发现字典里没有这个键打印一下checkpoint.keys()再调整。4.3 离线视频demo与可视化验证项目里media目录有ta_chi.mp4、clean_and_jerk.mp4、skateboarding.mp4几个demo视频配合demo_offline.py可以直接看模型效果python demo_offline.py --video media/ta_chi.mp4 \ --model models/kinetics-st_gcn.ptdemo_offline.py内部会调用OpenPose提取每帧的骨骼关键点再把关键点序列按时间窗口切分送进ST-GCN得到预测类别最终输出一个叠加了骨架和类别文本的视频。如果环境里没有OpenPose可以改用项目里的openpose.py提供的轻量级检测接口或者直接跑demo_old.py快速验证。跑demo时注意输入的--model要改成kinetics-st_gcn.pt因为输出类别是Kinetics的400类如果拿NTU权重去跑预测结果全是噪声。5. 避坑指南环境、数据与训练时的翻车记录老项目的坑通常比功能多。我把搭这个项目时遇到的问题按层面分类每条都是现象、原因、解决三步讲。5.1 环境导入与GPU相关的坑现象1运行main.py时报import torchlight失败。原因torchlight是项目目录里的本地包不是pip安装的第三方库如果你在别的目录下执行python /some/path/main.pyPython的模块搜索路径不包含项目根目录自然找不到。解决先cd到项目根目录再执行或者用sys.path.insert(0, /your/project/root)把项目根目录加进环境。现象2训练时提示CUDA out of memory但模型本身很小。原因可能是num_worker设得太大或者输入序列的window_size太大。骨骼数据本身不占多少显存但PyTorch会为每个batch的图卷积中间结果分配显存帧数T从100改到300显存可能翻倍。解决把batch_size从32降到16再把window_size从300降到200。如果还想涨精度可以开梯度累积或者用混合精度。5.2 数据预处理与加载的坑现象1ntu_gendata.py中途报错提示某个skeleton文件读取的行数不对。原因NTU原始数据里有少量标注不完整的样本比如人体关节缺失或者某一帧没有人体。官方在info/samples_with_missing_skeletons.txt里列了这些样本名如果不排除读取时因为关节数不足数组shape会错。解决预处理脚本里要读取并筛掉这个列表。如果是自己写数据预处理务必把这一行过滤加上否则后面训练时会在某个随机batch里突然抛异常很搞心态。现象2训练时每个epoch准确率都稳定在1%像是没学习。原因常见情况是输入数据没有归一化。NTU的坐标范围可能很大不同人在画面中的位置、大小差异也大。如果直接送进ST-GCN梯度会被大数值淹没。解决在feeder里做中心化和尺度归一化。常见做法是计算所有训练样本每个关节点的均值和标准差然后做z-score或者把每个序列中心关节点平移到原点再按骨架尺寸缩放。项目里的feeder.py已经实现了这一点如果你改动数据生成流程别把这一步丢掉。现象3数据加载很快但num_class设错输出维度对不上。原因config里默认是NTU-60的60类如果你换到Kinetics-skeleton400类数据但没改num_class模型最后输出的向量长度就不对损失函数会直接报错。解决每换一个数据集先确认数据集的类别数再修改yaml里的num_class。这个错误报错很明确就怕类别数相同但数据分布不同模型硬train一波结果却是随机水平。5.3 模型与权重加载的坑现象1调用model.load_state_dict(checkpoint)时报RuntimeError: size mismatch for fc.weight。原因预训练权重是在Kinetics-400上训练的最后一层全连接是400维你的模型配置num_class60所以最后输出维度对不上。解决要么用NTU对应的权重要么只加载部分权重。微调场景下可以先加载除fc外的所有层再随机初始化一个新fc层checkpoint torch.load(models/kinetics-st_gcn.pt, map_locationcpu) pretrained checkpoint[state_dict] filtered {k: v for k, v in pretrained.items() if fc not in k} model.load_state_dict(filtered, strictFalse)这段代码是微调里常用的手法strictFalse让模型只能加载能匹配的层不匹配的层保留随机初始值这样就能在新数据集上重新训练最后的分类头。现象2明明加载了AddEdgeSTGCN12345.pt但测试结果和OriginSTGCN.pt差不多甚至更差。原因这个权重可能是基于自定义图结构的模型比如给某些关节增加了跨越连接增加了AddEdgeWeight_2.txt里的边权重。如果你的模型定义还是原始图结构加载时虽然有部分层匹配但没有额外边的参数效果自然不对。解决使用这个权重之前先看README.md或AddEdgeWeight_2.txt里定义的额外边在net/graph.py里把邻接矩阵对应位置加上这些边再重新初始化模型。这样才能复现“加边能否提升准确率”的实验也是毕设里一个不错的改进点。6. 进阶技巧把模型迁移到自己的视频数据集毕设往往不满足于跑通公开数据集最好能演示一段自己的视频。这里分享一个我当年迁移数据的验证路径。6.1 用MediaPipe提取自采视频的关键点OpenPose环境重没有GPU很难跑。MediaPipe轻量多了CPU也能做到实时。下面这段代码可以把视频每帧的人体33个关键点提取出来并保存坐标序列import cv2 import mediapipe as mp mp_pose mp.solutions.pose cap cv2.VideoCapture(my_video.mp4) joints [] with mp_pose.Pose(static_image_modeFalse, model_complexity1) as pose: while cap.isOpened(): ret, frame cap.read() if not ret: break results pose.process(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) if results.pose_landmarks: frame_joints [] for lm in results.pose_landmarks.landmark: frame_joints.append([lm.x, lm.y, lm.z]) joints.append(frame_joints) cap.release() print(f共提取 {len(joints)} 帧关键点)MediaPipe输出的是33个关键点而NTU-RGB-D用的是25个关键点两者索引并不一致。常见做法是做一个映射表只用NTU需要的那些节点比如髋中心、肩、肘、腕、膝、踝等丢掉耳朵和眼睛等面部点。如果不做这一步直接把33点输入到用25点训练的模型里关节维度对不上。6.2 冻结主干微调与结果验证把自己的数据转成和feeder兼容的npz后最稳妥的用法不是从头训练而是加载kinetics-st_gcn.pt把特征提取层冻结只训练最后的分类层。代码思路如下for name, param in model.named_parameters(): if fc not in name: param.requires_grad False这样只需要一个很小的数据集就能微调同时避免因为自定义数据量不足导致特征提取层过拟合。验证阶段除了看准确率我强烈建议生成一段可视化视频把模型输出的注意力热图叠加到原视频画面上。项目里的visualization.py就有这个能力它能画出每一帧模型最关注的关节位置。从那次毕业设计以后我每次迁移一个动作识别模型都会强制走一遍“先跑通官方demo、再换自己的数据、最后看注意力热图”的流程而不是一上来就调参否则很容易陷入loss下降但结果完全不可解释的处境。希望帮到你。本文还有配套的精品资源点击获取