☰
ST-GCN骨骼动作识别实战:时空图卷积与Python毕设资源解析
2026/10/1 5:33:40 网站建设 项目流程

简介:这是一套基于时空图卷积网络(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_grad=False) # 归一化邻接矩阵 [K, V, V] self.conv = nn.Conv2d(in_channels, out_channels, kernel_size=1) 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负责生成这个A,net/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/npz
kinetics_gendata.py处理kinetics-skeleton数据
torchlight/训练工具包,提供日志、GPU管理
models/OriginSTGCN.pt、AddEdgeSTGCN12345.pt、kinetics-st_gcn.pt

main.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,比如attention+prediction.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 python=3.7 conda activate stgcn pip install -r requirements.txt # 如果requirements里没有torch,单独安装适配CUDA的版本 pip install torch==1.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/nturgb+d_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_path='data/NTU-RGB-D/npy/x_train.npz', \ label_path='data/NTU-RGB-D/npy/y_train.npz', \ window_size=300); \ print(f[0][0].shape)"

这里window_size=300表示每个序列采样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,显存不够就降到8
num_epoch训练轮数80~120
optimizer优化器SGD,momentum=0.9
lr初始学习率0.1
step学习率衰减轮次[70, 90]
num_class动作类别数NTU-60填60,Kinetics填400
num_worker数据加载线程数4或8
deviceGPU编号[0]

在项目根目录执行训练:

python main.py --config config/st_gcn/ntu-xview/train.yaml \ --work-dir work_dir/recognition

main.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.5,acc稳步上升,说明模型在学。如果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_channels=3, num_class=60, graph_args={'layout': 'ntu-rgb-d', 'strategy': 'spatial'}) checkpoint = torch.load('models/OriginSTGCN.pt', map_location='cpu') 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.pt

demo_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.py,Python的模块搜索路径不包含项目根目录,自然找不到。解决:先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 数据预处理与加载的坑

现象1:ntu_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-skeleton(400类)数据,但没改num_class,模型最后输出的向量长度就不对,损失函数会直接报错。解决:每换一个数据集,先确认数据集的类别数,再修改yaml里的num_class。这个错误报错很明确,就怕类别数相同但数据分布不同,模型硬train一波结果却是随机水平。

5.3 模型与权重加载的坑

现象1:调用model.load_state_dict(checkpoint)时,报RuntimeError: size mismatch for fc.weight。原因:预训练权重是在Kinetics-400上训练的,最后一层全连接是400维;你的模型配置num_class=60,所以最后输出维度对不上。解决:要么用NTU对应的权重,要么只加载部分权重。微调场景下,可以先加载除fc外的所有层,再随机初始化一个新fc层:

checkpoint = torch.load('models/kinetics-st_gcn.pt', map_location='cpu') pretrained = checkpoint['state_dict'] filtered = {k: v for k, v in pretrained.items() if 'fc' not in k} model.load_state_dict(filtered, strict=False)

这段代码是微调里常用的手法,strict=False让模型只能加载能匹配的层,不匹配的层保留随机初始值,这样就能在新数据集上重新训练最后的分类头。

现象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_mode=False, model_complexity=1) 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下降但结果完全不可解释的处境。希望帮到你。

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

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

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

立即咨询