ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

ST-GCN骨骼动作识别:时空图卷积网络原理与工程实践

2026/9/13 11:21:53 拓冰建站 浏览量
ST-GCN骨骼动作识别:时空图卷积网络原理与工程实践 简介这是一份基于时空图卷积网络ST-GCN的骨骼动作识别毕业设计项目面向计算机视觉、深度学习方向的本科或研究生适合需要完成人体动作识别课题并希望快速跑通完整流程的开发者。项目以Python与PyTorch为核心将人体骨骼结构建模为时空图可识别行走、挥手、举重等动作应用场景覆盖体育分析、医疗康复与智能交互。压缩包共91个文件约52.56MB主体为29个Python源码、13个YAML配置、12个Pyc编译文件另有预训练模型、演示GIF与MP4视频、说明文档等便于对照代码理解数据预处理、模型训练与可视化流程。目前已有159人学习下载。资源包含主程序、测试脚本、骨骼关键点绘制工具、OpenPose相关工具、NTU与Kinetics数据集处理脚本以及多个预训练权重文件并配有演示动图与视频可直接体验识别效果也为二次开发或论文复现提供了较为完整的工程骨架。1. ST-GCN骨骼动作识别这套工程直接拿来当毕设骨架骨骼动作识别在体育分析、医疗康复、人机交互里都是热门方向而ST-GCN时空图卷积网络是绕不开的基线模型。这个压缩包不是单文件论文代码而是一套完整工程NTU-RGB-D和Kinetics-skeleton的数据解析脚本、双流ST-GCN模型定义、训练入口、离线与实时demo、日志和可视化工具都在里面。对要做毕业设计的学生改配置就能跑通训练和识别对想快速验证图卷积想法的工程师models目录下三份权重和AddEdge实验记录也有参考价值。下面从模型原理开始逐层拆开每个环节的代码和参数。2. 图卷积不是噱头ST-GCN的建模逻辑与网络实现2.1 为什么骨架动作要建模成图动作识别常见的做法是把人体关键点坐标拼成一维向量丢给LSTM或1D CNN。这样做的缺点是关节之间的结构关系被抹平了比如左手腕和左肘的距离与左手腕和右膝的距离在特征里变得等价模型必须靠数据硬学。ST-GCN把人体骨架定义成一张图关节点是节点骨骼是边每一帧是图的一个快照整个动作序列就是图在时间轴上的演化。空间上做图卷积聚合邻居信息时间上做标准卷积两个维度分开建模再堆叠既保留拓扑又有序贯性。这个设计在NTU-RGB-D和Kinetics-skeleton上都验证过效果作为毕业设计选题论文好写、复现路径清晰。2.2 邻接矩阵构造与图划分策略图卷积的第一步是把骨骼图转成邻接矩阵。项目里net/graph.py干的就是这件事。以OpenPose的18个关键点为例先定义哪些关节点物理相连再生成带自环的邻接矩阵。常见做法是不直接用原始A矩阵而是用归一化后的矩阵# net/graph.py 中构造归一化邻接矩阵的核心思路 import numpy as np def normalize_adjacency(A): 对邻接矩阵做对称归一化D^{-1/2} A D^{-1/2} A A np.eye(A.shape[0]) # 加自环让节点保留自身特征 D np.diag(np.sum(A, axis1)) # 度矩阵 D_inv_sqrt np.linalg.inv(np.sqrt(D)) # D^{-1/2} return D_inv_sqrt A D_inv_sqrt # 返回 (num_node, num_node) 的归一化邻接矩阵 A normalize_adjacency(raw_adjacency)这里加自环的意义是让每个关节在卷积时能聚合到自己的特征否则节点更新后只包含邻居信息自身信息会丢失。对称归一化D^{-1/2} A D^{-1/2}是为了消除关节度数差异带来的尺度偏差躯干中间的关节邻居多、末端关节邻居少不归一化的话高度数节点特征数值会偏大。实际在OpenPose布局中num_node18而NTU-RGB-D数据集是25个关键点换数据集时只需要改graph的布局定义和连接关系表。ST-GCN原文里提出了几种图划分策略unified、distance、spatial项目默认用的是spatial策略把每个关节的邻居分成三个子集——根节点本身、向心邻居离骨架重心更近、离心邻居离重心更远。每个子集给一个独立的卷积核图卷积核张量形状是(K, C_out, C_in)其中K3。代码上的差异就在get_adjacency_matrix返回的是带K维的矩阵而不是普通的一层邻接矩阵。2.3 时空卷积模块与双流融合空间图卷积处理好每一帧但动作本身是时序的所以还需要时间维建模。net/st_gcn.py里的基本模块是“空间图卷积 时间卷积”串联# net/st_gcn.py 中 ST-GCN 基础模块的结构 class STGCNBlock(nn.Module): def __init__(self, in_channels, out_channels, A, stride1): super().__init__() self.gcn GraphConv(in_channels, out_channels, A) # 空间3个子集图卷积 self.tcn nn.Sequential( nn.Conv2d(out_channels, out_channels, kernel_size(9, 1), padding(4, 0)), # 时间9帧窗口 nn.BatchNorm2d(out_channels), nn.ReLU(inplaceTrue), ) def forward(self, x): # x 形状: (N, C, T, V)N批量、C通道、T帧数、V节点数 x self.gcn(x) x self.tcn(x) return x这里有个容易忽略的细节图卷积的输入输出在空间维V上长度不变所以时间卷积的kernel只需要写成(9, 1)9是时间窗口1是在空间维上不卷积。之所以不用LSTM而用一维卷积建模时间是因为卷积可以并行计算、训练速度更快而且9帧的感受野在堆叠多层后已经能覆盖完整的动作周期。项目里默认kernel_size9, padding4保证时间维不缩水如果处理高速动作比如resource里的clean_and_jerk举重视频可以把kernel加到11或13代价是参数增加。双流模型st_gcn_twostream.py是另一个关键设计。原始ST-GCN只输入关节坐标joint双流在此基础上增加骨骼向量分支把相邻关节坐标相减得到骨骼向量同样构造一个图输入第二个ST-GCN分支。两个分支的输出在softmax之前做加权融合权重作为可学习参数。这个改造对识别准确率的提升非常明显在NTU-XSub上通常能比单流提高2到3个百分点也是很多毕业设计把它作为基线的原因。配置文件中model_type: st_gcn_twostream就是切换到这个双流结构。3. 从原始骨架文件到训练样本数据链路怎么打通3.1 NTU-RGB-D 骨架文件解析NTU-RGB-D原始数据是.skeleton后缀的文本每个文件对应一段动作视频。tools/ntu_read_skeleton.py负责把这种文本解析成Python对象。解析的关键在于理解它的层级结构文件第一行是总帧数之后每帧先有body个数每个body又带25个关节每个关节一行。常见做法是维护一个游标按行读取# tools/ntu_read_skeleton.py 解析单个 skeleton 文件的简化逻辑 def read_skeleton(file_path): with open(file_path, r) as f: lines f.readlines() cur 0 num_frames int(lines[cur].strip()); cur 1 frames [] for _ in range(num_frames): num_bodies int(lines[cur].strip()); cur 1 bodies [] for _ in range(num_bodies): # 每个 body 的信息头关节数等字段 num_joints int(lines[cur].strip()); cur 1 joints [] for _ in range(num_joints): parts lines[cur].strip().split() joints.append([float(parts[0]), float(parts[1]), float(parts[2])]) cur 1 bodies.append(joints) frames.append(bodies) return frames这段代码保留了最核心的游标推进逻辑。实际NTU文件里body信息头不止关节数一个字段还可能包含置信度和裁剪信息解析时按固定索引取值即可具体字段顺序以数据集说明为准。很多人在毕设答辩时被问到数据格式答不出body header的结构会明显减分。ntu_read_skeleton.py处理的就是这段逻辑替换数据集时只需要改关节数和字段索引。3.2 从原始帧到图卷积输入解析出的原始数据是变长的帧数不同、人数不同不能直接进batch。ntu_gendata.py的作用就是把原始骨架序列整理成固定长度通常采样到300帧的数组没到300帧的补零超过的均匀抽样然后按train/val划分保存。生成的pkl文件里每条样本是dict包含keypoint数组和label。在feeder.py中训练时还会做数据增强# feeder/feeder.py 中随机旋转增强的典型实现 def random_rot(self, data): 绕Z轴随机旋转幅度由magnitude参数控制只作用于前3个坐标通道 angle np.random.uniform(-self.magnitude, self.magnitude) c, s np.cos(angle), np.sin(angle) R np.array([[c, -s, 0], [s, c, 0], [0, 0, 1]], dtypenp.float32) return np.einsum(ab,nctb-ncta, R, data)einsum里的nctb对应批量N、通道C、时间T、空间节点数V旋转矩阵在最后一维上做线性变换。注意增强只对坐标通道生效如果输入还带了置信度或速度通道需要先分离再旋转否则会把非坐标信息也转进去。Kinetics数据集的路径类似tools/kinetics_gendata.py接收的是已经用OpenPose从视频抽出的json骨架文件处理逻辑和NTU版本基本一致区别主要在关节数18 vs 25和类别数400 vs 60。Kinetics-skeleton的类别不是连续编号label_name.txt就是做类别名和索引映射的训练完拿它来翻译预测结果。3.3 配置文件与超参数速查torchlight是这个项目自己封装的配置与训练框架config目录下按数据集和模型组织。以config/st_gcn.twostream/kinetics-skeleton下的train.yaml为例参数典型值含义与调整建议in_channels3关节坐标维度若加置信度改为2或3加速度改为6num_class400训练集动作类别数换成自己的数据集必改batch_size64显存不足时先减半再考虑换GPUbase_lr0.1SGD初始学习率微调时降到0.01step[30, 40]在第30、40个epoch学习率乘0.1edge_importance_weightingTrue是否学习边权重矩阵一般保持True提示base_lr设0.1对从头训练合理但使用kinetics预训练权重微调时务必降到0.01或更低否则loss很容易发散。换自己的数据集时第一优先改num_class第二把step改成自己总epoch的一半和四分之三位置。训练指令不复杂# 以 NTU-XSub 双流配置启动训练 python main.py --config config/st_gcn.twostream/ntu-xsub/train.yaml训练过程中所有中间结果由torchlight输出到work_dir目录每个epoch会打印loss、top1和top5准确率。如果不想从头训练models目录下的预训练权重可以直接用来评估或做迁移学习。4. 训练、评估与两类Demo的正确打开方式4.1 main.py 与 processor 的工作流主入口main.py做的事情很纯粹读配置、初始化模型和数据加载器、构建处理器processor/processor.py、开始训练或评估。识别场景对应recognition.py它继承processor.py的基类重写了train和test两个方法。对于只想跑通流程的人不需要关心内部细节只要知道训练状态会被序列化保存断点续训时 torchlight 会在work_dir下找最新的checkpoint。如果中途断了直接再执行一次相同命令它会自动加载最近权重而不是从零开始。评估单个权重文件可以用命令行直接指定模型参数# 用训练好的双流权重做 top-1 / top-5 评估 python main.py --config config/st_gcn.twostream/ntu-xview/test.yaml \ --weights models/OriginSTGCN.pt注意评估时配置文件里的模型结构必须和权重文件对应双流权重配单流配置会直接报key不匹配报错信息里会列出缺失和多余的层名对着层名检查是哪个环节配错了。4.2 models 目录下三份权重怎么选项目压缩包里带了三个.pt文件三者的定位差别很大选错会导致测试结果完全对不上权重文件训练数据适用场景OriginSTGCN.ptNTU-RGB-D 单流原始模型复现原论文基线AddEdgeSTGCN12345.ptNTU-RGB-D 加入边权重改进对比改进效果、毕设创新点演示kinetics-st_gcn.ptKinetics-skeleton 预训练迁移到自己的动作数据集使用时要看模型类型和后缀是否匹配单流权重不能直接加载进双流模型会报key不一致。Kinetics预训练权重加载到双流模型时最后一层全连接维度不同需要冻结前面的层、只替换最后的分类头。JustTest.py里就有快速验证代码可以先把模型初始化成对应配置再load_state_dict时设strictFalse这样只要关键层对得上分类头维度不匹配也不会中断加载。4.3 离线视频识别与实时摄像头演示演示是毕业设计评分里权重很高的一环。项目给出两条演示路径demo_offline.py处理录制好的视频demo_realtime.py走摄像头实时识别demo_old.py是早期版本的参考实现不建议新代码基于它改。离线版本会先用OpenPose检测每帧的关键点再把关键点序列整理成模型输入最后把识别结果叠加在画面上。运行方式一般是# 识别 resource 目录下的太极拳视频 python processor/demo_offline.py --video resource/ta_chi.mp4 \ --weights models/kinetics-st_gcn.pt # 启动摄像头实时识别按 q 退出 python processor/demo_realtime.py --weights models/kinetics-st_gcn.pt \ --cam_id 0两个demo里最容易出问题的环节不是模型而是OpenPose的依赖。它会拉取caffe和pyopenpose相关组件环境装不对就会在导入阶段抛错报错通常显示找不到openpose模块。我一般建议把关键点检测的耗时单独打点如果一帧超过50ms就把输入分辨率降一档实时演示时识别结果有1到2帧延迟是正常的不用刻意追求零延迟。resource里的gif和demo_video.gif是之前跑通的样例输出可以参照对比自己的画面效果。5. 日志挖潜与AddEdge改进让毕设多两个亮点5.1 从 logData 和 log.txt 里找训练异常work_dir/recognition/kinetics_skeleton/tmp/log.txt 记录了每次运行的完整日志logData 里还保存了AddEdgeWeight_2.txt这样针对边权重实验的对比输出。检查训练是否正常先看loss是否在step节点比如第30个epoch出现明显下降如果下降发生在step之前说明学习率偏小可以把base_lr加大一档看看。top1准确率在第一个epoch后如果接近随机水平要怀疑类别标签对齐出了问题优先检查label_name.txt的映射表。NTU里还有部分样本骨架缺失samples_with_missing_skeletons.txt列出了这些文件评估时必须先过滤否则会出现某一类准确率异常偏低或者loss跳成NaN。5.2 DrawLine.py 做骨架时序可视化DrawLine.py是个容易被低估的工具它能把骨骼序列画成连线图并逐帧输出适合生成论文插图。常见用法是把NTU的skeleton文件或pkl中的keypoint数组画成图像序列python DrawLine.py --input S001C001P001R001A051_w.gif \ --output skeleton_sequence.mp4画图时注意关节连线关系要和graph.py里的neighbor_link保持一致否则会出现手肘连线接到肩膀上的错误。对论文来说可视化关键点叠加在原始视频上的效果resource里的attentionprediction.png比单独画骨架更有说服力因为评委能直观看到OpenPose的检测结果和模型关注区域注意力特征图可以用最后几层GCN输出的梯度做CAM类激活图叠加上去。5.3 在原始ST-GCN上加边权重的落地方案logData里的AddEdgeWeight_2.txt记录了给ST-GCN增加边权重的实验思路原始模型已经有edge_importance_weighting但它学习的是每个节点的权重对角矩阵而AddEdge改成了对每条边也学一个标量权重等价于在邻接矩阵上叠加一个可学习的边权矩阵。实现时只需要在GraphConv里增加一个与A同形状的nn.Parameterforward时用A * W替代A代码改动不超过十行。这个实验和OriginSTGCN.pt与AddEdgeSTGCN12345.pt两份权重正好形成对照组差分对比就是毕业论文的完整“改进-验证”闭环。记录文件名里的_2说明这是第二轮实验第一轮的参数或结果记录可以在logData里顺藤摸瓜找到演变轨迹答辩时这一过程讲清楚很加分。验证时记得用同一份test配置、同一个随机种子跑两个模型不要只报最优数字把两个权重在各类别上的准确率差值列成表格能更清楚看到加边权重到底改善了哪几类动作。本文还有配套的精品资源点击获取