资讯动态

ST-GCN骨骼动作识别:源码解析、环境搭建与模型训练实战

发布时间:2026/9/28 22:31:00 来源:尧图企业网站定制
简介这是一份基于时空图卷积ST-GCN的骨骼动作识别完整项目面向计算机专业毕业生、课程设计与期末大作业需求者也适合想深入图神经网络动作识别实战的学习者。项目经导师指导并获得98分高分源码结构清晰包含模型定义、数据预处理、训练识别、离线与实时演示等模块可直接运行或二次开发。压缩包共88个文件以Python源码为主辅以yaml配置、pt预训练模型、gif动图、mp4演示视频、txt说明文档等整体约52.56MB目录划分清晰便于按需查阅。目前已有424人学习下载。资源内还附带NTU-RGB-D与Kinetics数据集处理脚本、双流网络实现以及模型对比文件可帮助快速复现实验、理解ST-GCN核心原理同时提供预训练权重与多种演示入口方便从数据到结果快速跑通是完成毕设或项目实战的高质量参考。1. 基于ST-GCN的骨骼动作识别这份毕设源码包打开就能跑通吗做动作识别方向毕业设计的人大概率会在骨骼动作识别的选题上卡三步骨架数据怎么预处理、时空图卷积网络ST-GCN的核心结构怎么实现、预训练权重在哪里能稳定跑起来。这份基于ST-GCN的骨骼动作识别python源码项目恰好把这三步全打包了——从 NTU-RGB-D 原始骨架数据转 numpy 格式的 ntu_gendata.py到实现图卷积与时间卷积组合的 net/st_gcn.py再到离线/实时都能跑的识别 demo外加 OriginSTGCN.pt 和 AddEdgeSTGCN12345.pt 两份预训练权重。评审分 98 的项目不是拿来看的是拿来跑拿来改的。适合正在做毕设的本科生、想复现 ST-GCN 的初学者以及需要一套可改的骨骼识别基线做课程设计的同学。2. 时空图卷积原理两道卷积怎么在骨骼图上跑2.1 骨骼序列怎么变成图节点、边和自环骨骼动作识别的输入不是图像而是每一帧的关节点坐标。NTU-RGB-D 数据集每帧给出 25 个关节点Kinetics-skeleton 是 18 个点每个点包含 x、y、z 三个坐标。单独看这些坐标只是一个 N×3 的矩阵丢进全连接网络也能做分类但动作识别的关键在“关节点之间的关联”上——挥拳和招手手腕、手肘、肩膀之间的相对运动关系才是区分信号。图卷积的思路就是把这 N 个关节点当成图上的 N 个节点把骨骼连接当成边。st_gcn.py 里的图构建逻辑非常直接先根据人体骨骼连接关系生成邻接矩阵 AA[i][j]1 表示第 i 个关节和第 j 个关节直接相连然后加一个自环 I 让节点在聚合邻域时保留自身特征。import torch def build_adjacency(num_nodes, edges): # num_nodes: 关节点数量NTU 是 25Kinetics 是 18 # edges: 骨骼连接对例如 (0,1) 表示两个关节点相连 A torch.zeros(num_nodes, num_nodes) for i, j in edges: A[i, j] 1 A[j, i] 1 # 骨骼连接是无向边对称写入 A torch.eye(num_nodes) # 自环保留节点自身特征 return A # 以 NTU-RGB-D 前几个关节为例完整 25 关节连接表在 ntu_gendata.py 里 ntu_edges [(0, 1), (1, 20), (20, 2), (2, 3), (20, 8), (8, 9), (9, 10)] A build_adjacency(25, ntu_edges)这里的 A 就是后续所有图卷积层的“图结构”输入。edges 列表在项目里写死在 ntu_gendata.py 和 kinetics_gendata.py 中不同数据集的关节点编号规则不同edge 顺序不能混用——这点在第五章还会专门说。光有 A 还不够。直接把 A 和特征 X 相乘度大的节点连接多的关节点会被放大好几倍训练时数值不稳定。所以 ST-GCN 做了一个常见的对称归一化先用度数矩阵 D 计算 D 的 -0.5 次方然后得到 A_norm D^-0.5 × A × D^-0.5。这一步做完每个节点聚合邻居时都按度的平方根做了缩放特征尺度被拉回同一起跑线。def normalize_adjacency(A): # A: 加了自环的邻接矩阵 D A.sum(dim1) # 度矩阵对角线元素 D_hat D.pow(-0.5).diag() # D^-0.5 return D_hat A D_hat # 对称归一化 A_norm normalize_adjacency(A_built)参数上唯一要留意的是 A 的数据类型必须是 float不能用 int 矩阵直接做矩阵乘法否则会在图卷积层里报矩阵维度或类型错误。这个归一化矩阵在整个前向过程中只算一次可以提前放到模型外算好再传进去项目里 edge_importance_weighting 开启时还会在此基础上乘一层可学习的边权重矩阵这是后面模型对比实验的改动点之一。2.2 图卷积加时间卷积ST-GCN 单流模块怎么堆有了图结构空间维度上的卷积就可以定义成一次消息传递每个节点的新特征等于自身特征和所有邻居特征的加权和。公式层面图卷积一次传播就是 H A_norm × X × W其中 X 是 (V, C_in) 的特征矩阵W 是 (C_in, C_out) 的可学习权重。但骨骼序列还有第三个维度——时间。一段动作视频有几十上百帧每一帧都有 25 个关节点坐标所以输入张量实际是 (N, C, T, V)N 是批量大小C 是通道数坐标 x、y、z 就是 3 个通道T 是帧数V 是关节点数。空间图卷积处理 V 和 C 维度时间维度则需要一维卷积在帧序列上滑窗。项目里每个 ST-GCN 块由两部分拼成先是空间图卷积层然后是一个时间卷积层。时间卷积用的是 Conv2d卷积核大小是 (temporal_kernel, 1)在 T 维度上滑动每个关节点独立滑窗这样既能捕捉局部帧之间的变化又不会在空间维度上把不同关节的特征混掉。import torch.nn as nn class STGCNBlock(nn.Module): def __init__(self, in_ch, out_ch, temporal_kernel9, stride1): super().__init__() self.gcn GraphConv(in_ch, out_ch) # 空间图卷积内部做 A_norm X W self.tcn nn.Conv2d( out_ch, out_ch, kernel_size(temporal_kernel, 1), stride(stride, 1), padding(temporal_kernel // 2, 0) ) self.bn nn.BatchNorm2d(out_ch) self.relu nn.ReLU() # 残差连接通道数或时间步长变化时用 1x1 卷积对齐 if stride ! 1 or in_ch ! out_ch: self.residual nn.Conv2d(in_ch, out_ch, 1, stride(stride, 1)) else: self.residual nn.Identity() def forward(self, x): # x 形状: (N, C, T, V) res self.residual(x) x self.gcn(x) # 空间维度聚合 x self.tcn(x) # 时间维度滑窗 x self.bn(x) return self.relu(x res)这个块是整个网络的基本单元配置文件里通常堆 9 层左右通道数从 3 逐层增加到 64、128、256最后做全局池化接全连接分类。temporal_kernel 是每个块里最难调的参数之一NTU 和 Kinetics 上常见数值是 9帧率更高或动作更短的场景可以降到 5。stride 在部分层设成 2作用是时间维度降采样把 300 帧逐步压到几十帧减少计算量。2.3 双流模型为什么是主流st_gcn_twostream 的合并逻辑单流 ST-GCN 用的是关节点坐标但坐标本身包含大量静态位置信息。两个人做同一个动作站的位置、身高臂长不同坐标值差异巨大这会让模型把“站位”也学进特征里。双流模型的做法是第二条流输入不是坐标而是关节点的运动信息——最常见的是相邻帧的坐标差也就是速度流。项目里 net 目录下有两个模型文件st_gcn.py 是单流版本st_gcn_twostream.py 是双流版本。双流内部其实是两个独立的 ST-GCN 网络一条流接收原始坐标另一条流接收速度特征两条流的输出在分类层之前做求和或拼接融合。训练时两份数据由 feeder 同时产出batch 里的每个样本都有两个张量。class STGCNTwostream(nn.Module): def __init__(self, in_channels, num_class, graph_args): super().__init__() # 流1关节坐标流2速度/边特征 self.stream_joint STGCN(in_channels, num_class, graph_args) self.stream_motion STGCN(in_channels, num_class, graph_args) def forward(self, joint_data, motion_data): out_joint self.stream_joint(joint_data) out_motion self.stream_motion(motion_data) # 两个流的 logits 直接相加 return out_joint out_motion参数上要注意两条流的 in_channels 必须一致project 里通常都是 3区别只在输入数据的第二个维度——一条是 xyz另一条是 dx、dy、dz。双流模型参数量基本翻倍显存占用也翻倍但换来的是在 NTU-RGB-D 上准确率提升 3 到 5 个点。如果你的毕设需要“改进点”双流是最容易写进论文里的改动方向之一。3. 环境搭建与数据准备从源码到第一帧识别3.1 环境配置torchlight 和 requirements 先装对源码依赖项不多requirements.txt 里列的是 PyTorch、numpy、opencv-python、tqdm 这些常规包。关键在 torchlight——这个项目自己封装的轻量训练框架负责命令行参数解析、日志输出、模型保存加载。它不是 PyPI 上的包需要从源码安装项目目录下就有 torchlight 文件夹里面有 setup.py。# 建议用 conda 创建独立环境Python 3.7 到 3.9 实测都能跑 conda create -n stgcn python3.8 conda activate stgcn # 先装 PyTorch版本 1.8 到 1.13 都兼容注意 CUDA 版本和 GPU 驱动对应 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 再装项目依赖和 torchlight pip install -r requirements.txt cd torchlight python setup.py install cd ..安装顺序上我建议先把 torchlight 装上再跑任何脚本因为 processor 和 feeder 模块里都有 from torchlight import 的语句缺了直接 import 报错。setup.py install 之后的包会进到当前 Python 环境的 site-packages 里如果后面改了 torchlight 源码需要重新 install 一次或者用 python setup.py develop 做成链接模式方便调试。GPU 不是必需但 ST-GCN 在 CPU 上跑 NTU 训练基本等不起推理单视频的话 CPU 还能接受。3.2 数据准备ntu_gendata.py 把原始骨架变成 numpyNTU-RGB-D 原始数据是一堆 .skeleton 文件每个文件里逐帧记录人体关节点的 3D 坐标、置信度和帧信息。项目里 tools/ntu_gendata.py 负责把这些文本格式的骨架序列解析成训练能用的 numpy 数组和标签文件。这个脚本做的事分三步读取每个 .skeleton 的帧序列按规定的关节顺序提取 xyz 坐标然后做填充和归一化。# 参数含义 # --data_path NTU 原始 .skeleton 文件所在目录 # --out_folder 输出 npy 和 pkl 的目录 # --mode train / val决定走训练集还是验证集划分 python tools/ntu_gendata.py \ --data_path /path/to/nturgbd_raw \ --out_folder ./data/NTU-RGB-D/xsub \ --mode train脚本跑完输出目录里会有 train_data.npy、train_label.pkl、val_data.npy 这些文件。npy 的四维结构是 (样本数, 通道数, 帧数, 关节点数)通道是 xyz帧数会统一填充到固定长度NTU 上常见值是 150 帧——太短的序列补零太长的截断。这个固定帧长是后面训练配置里 window_size 参数和模型里的时间卷积核大小是两回事别搞混。Kinetics 数据集的处理逻辑相同入口是 tools/kinetics_gendata.py只是它从 JSON 格式的骨骼标注读数据。如果你手头只有摄像头实时数据没有现成骨架那就得先跑一遍姿态估计拿到关节点坐标再喂给模型process 目录里的 demo_realtime.py 走了这条路。3.3 跑通离线识别 demo一条命令看到识别结果数据没准备好不影响先跑 demo因为项目里放着预训练模型。离线 demo 的入口是 processor/demo_offline.py它读入一个视频文件对每帧做姿态估计这里依赖 openpose 或 mediapipe 产出的关节点数据把连续帧的关节点组合成样本再送进 ST-GCN 模型输出动作类别和置信度最后把类别名画在视频上。# 识别 resource/demo_asset 下的示例视频 # --weights 指定加载哪个预训练模型 python processor/demo_offline.py \ --weights ./models/kinetics-st_gcn.pt \ --video_path ./resource/demo_asset/sample_video.mp4用 kinetics-st_gcn.pt 做识别输出的是 Kinetics 400 类里定义的 400 个动作标签比如“饮酒”“鼓掌”“挥手”这类日常动作。如果你的 demo 视频是 NTU 数据的动作类别就得换 OriginSTGCN.pt 或者 AddEdgeSTGCN12345.pt同时处理器里的标签映射文件也要对应用 NTU 的 60 类。第一次跑建议先用自带 demo 视频验证环境模型加载成功会打印网络结构和参数量随后视频窗口弹出并逐帧标注动作名。没有视频窗口的时候检查是不是 headless 服务器改用 --save_result 把输出写进文件。processor 目录下还有一个 demo_realtime.py是摄像头实时版本原理和离线版完全一样只是把视频文件源换成了摄像头帧流。实时版对帧率敏感如果 GPU 不强建议把输入分辨率调低不然姿态估计那一步就会卡到个位数帧率。4. 源码结构拆解改配置训练自己的动作类别4.1 四个核心目录各自管什么项目源码的组织方式非常清晰拿到压缩包先别急着跑训练花十分钟把目录结构过一遍后面改代码效率高很多。net 目录放网络定义st_gcn.py 是单流模型st_gcn_twostream.py 是双流模型utils 里是图工具函数。feeder 目录放数据加载器feeder.py 定义了一个 Feeder 类负责从 npy 和 pkl 文件里按索引读取样本、处理成模型输入张量。processor 目录是训练和推理入口main.py 是总入口processor.py 里有训练循环和验证循环demo_offline.py 和 demo_realtime.py 是两个 demo。torchlight 是训练框架基础模块。用一个表格把关键文件和职责列清楚排错时能快速定位文件/目录职责对应操作入口net/st_gcn.py单流 ST-GCN 网络定义被 processor 调用net/st_gcn_twostream.py双流 ST-GCN 网络定义被 processor 调用feeder/feeder.py数据加载产出 (N,C,T,V) 张量训练/推理时实例化tools/ntu_gendata.pyNTU 原始 .skeleton 转 numpy命令行独立运行processor/processor.py训练、验证、模型保存逻辑main.py 的 backendprocessor/demo_offline.py单视频识别入口命令行独立运行config/st_gcn/单流模型的 yaml 配置训练参数来源Feeder 类是这里的核心衔接点。它对外暴露标准的getitem接口训练逻辑完全不知道数据是 NTU 还是 Kinetics只要 Feeder 能返回 (joint_data, motion_data, label) 三元组就行。这也是为什么换数据集时基本不用改网络代码只改数据预处理脚本和 config 里的 data_path。4.2 训练自己的动作类别config 参数怎么改项目自带 config 目录里面按模型类型分子目录st_gcn 下面是单流模型的 yaml 配置文件st_gcn.twostream 下面是双流模型的配置。config 里包含数据路径、训练超参、模型参数三大块。改动作类别时最核心的是 num_class 和 data_path。# config/st_gcn/st_gcn_ntu.yaml 简化后的核心字段 model: st_gcn num_class: 60 # 改成你自己的动作类别数 in_channels: 3 # x, y, z 三个坐标通道 edge_importance_weighting: True data_path: ./data/NTU-RGB-D/xsub/train_data.npy label_path: ./data/NTU-RGB-D/xsub/train_label.pkl val_data_path: ./data/NTU-RGB-D/xsub/val_data.npy val_label_path: ./data/NTU-RGB-D/xsub/val_label.pkl batch_size: 64 # OOM 时降一半 base_lr: 0.1 step: [30, 40] # 第 30 和 40 epoch 时学习率除以 10 num_epoch: 50 device: [0] # GPU 编号CPU 跑改成 [-1]data_path 指向 3.2 节 ntu_gendata.py 生成的 npy 和 pkl 文件换成你自己采集的动作数据时要保证 npy 的四维形状和 label 的格式完全一致。训练启动命令是# main.py 通过 --config 指定配置文件--work-dir 指定输出目录 python processor/main.py \ --config ./config/st_gcn/st_gcn_ntu.yaml \ --work-dir ./work_dir/recognition训练过程中 work_dir 里会定期保存 checkpoint名字类似 epoch-30-xxxx.pt。这个文件可以直接作为后续 demo 的 --weights 参数。注意 config 里 base_lr 写 0.1 时配合的是 SGD 优化器如果你换成 Adam学习率建议降到 0.001 量级直接沿用 0.1 会一路 loss 飘红。4.3 边权重实验从哪里入手AddEdgeWeight 的改动逻辑压缩包里不仅有原始 ST-GCN还有一份带“加边权重”字样的模型权重 AddEdgeSTGCN12345.pt 和一份 AddEdgeWeight_2.txt 文本记录。熟悉 ST-GCN 源码的人知道原版模型有个 edge_importance_weighting 参数开启后会在每个图卷积层给邻接矩阵乘上一个可学习的边权重矩阵——本质是让模型自己决定哪些骨骼连接更值得聚合。AddEdgeWeight 系列可能是在这个机制上做了新改动把权重矩阵的初始化方式或训练策略改了然后单独保存了权重文件。这类改动对应的代码位置在 st_gcn.py 的图卷积层内部。常规实现里 A_norm 是固定的edge_importance 会引入一个与 A_norm 同形状的 mask 张量参与矩阵逐元素相乘。对比实验只需要开一个开关# st_gcn.py 里图卷积层的典型写法示意 class GraphConv(nn.Module): def __init__(self, in_ch, out_ch, edge_importanceTrue): super().__init__() self.edge_importance edge_importance if edge_importance: # 可学习的边权重初始化为 1随训练更新 self.edge_weight nn.Parameter(torch.ones_like(A_norm)) else: self.register_buffer(edge_weight, A_norm) def forward(self, x): # x: (N, C, T, V) 空间聚合逻辑 adj A_norm * self.edge_weight if self.edge_importance else A_norm # 用 adj 对关节点特征做聚合 return aggregate(x, adj)如果你毕设想写“基于注意力机制的边权重改进”就是把 edge_weight 从固定常数改成可学习的、甚至按帧动态生成的。AddEdgeWeight_2.txt 里记录的就是某个中间轮次的权重分布情况可以直接当对比数据写进论文。模型文件里 OriginSTGCN.pt 是对照组AddEdgeSTGCN12345.pt 是实验组两者加载方式完全一致切换时改一处 --weights 路径即可。5. 避坑笔记显存、模型加载与数据形状的五个坑5.1 现象训练一开始就 CUDA out of memory训练 NTU 数据batch_size 设 64输入张量是 (64, 3, 150, 25)单显存 8G 的显卡直接 OOM。原因很直接150 帧的序列长度是填充后的固定值哪怕实际视频只有 40 帧张量仍然占到 150 帧的显存空间。解决方法是把 batch_size 降到 32 或 16同时检查 config 里是否有 num_worker 参数把数据加载线程数降一点也能缓解显存峰值。如果训练的是双流模型显存占用几乎翻倍batch_size 建议直接减半。5.2 现象加载 AddEdgeSTGCN12345.pt 报 missing keys 错误torch.load 之后调用 model.load_state_dict(ckpt) 报 missing keys 或 unexpected keys。原因大多数时候是模型类不匹配——单流模型结构用 st_gcn.py 定义双流模型用 st_gcn_twostream.py 定义如果配置文件里 model 字段是 twostream 却拿单流权重去加载键名对不上。解决方法是先打印 checkpoint 里的键名import torch ckpt torch.load(./models/AddEdgeSTGCN12345.pt, map_locationcpu) print(list(ckpt.keys())[:20])看到明显的 stream_joint. 前缀就是双流权重需要在 config 里把 model 改成 st_gcn_twostream或者加载时手动剥离前缀。另外我习惯在 load_state_dict 里加 strictFalse先看能对上多少键再决定是改模型还是改权重。5.3 现象 demo 推理结果全部输出同一个类别视频能跑但不管做什么动作输出的标签永远一样且置信度很高。最常见的原因是模型参数和输入数据的关节点顺序不对应——NTU 训练出来的权重用了 NTU 的 25 点关节排布而 demo 视频的姿态估计输出的是 MediaPipe 的 33 点或 OpenPose 的 18 点坐标顺序。解决方法是确认姿态估计器和训练数据集的关节点定义一致如果中间层做了坐标重排需要把重排逻辑写死在预处理管线里而不是每次 demo 时手动对。Kinetics 预训练模型用的是 18 点骨架拿它配 MediaPipe 的 33 点输出除非有映射表否则结果必然全是噪声。5.4 现象训练 loss 下降缓慢且验证集精度始终很低网络结构没错、数据也没乱但精度就是上不去。排查方向先看数据标准化npy 里的坐标是不是像素绝对值还是归一化后的值不问清楚模型很容易被坐标量纲带着跑偏。NTU 原始数据单位是米坐标范围大致在 -3 到 3 之间如果换用 Kinetics 的 2D 骨架坐标范围是 0 到 500 像素级直接混训会出现量纲灾难。解决方法是统一做一个减均值除方差的预处理在 ntu_gendata.py 里把坐标先中心化再缩放一遍。5.5 现象CPU 上跑 demo 极慢GPU 却报版本冲突环境里有 GPU 但代码跑在 CPU 上或者 CUDA 版本编译和 torch 版本不匹配。解决方法是训练和推理统一设置 deviceconfig 里 device 字段明确写 GPU 编号模型加载后用 .cuda() 和 .to(device) 保证输入输出在同一个设备。如果 torch 和显卡驱动版本对不上优先重装匹配的 torch 版本不要动系统驱动。没有 GPU 的机器注意 model.cuda() 会直接抛 AssertionError需要把 demo 脚本里的设备选择逻辑改成显式判断 torch.cuda.is_available()。6. 进阶用离线 demo 回放做模型可信度验证模型训练完、demo 也能跑但怎么跟导师证明识别结果是可靠的我的习惯是拿一份没参与训练的视频做“回放验证”不只看最终标签而是把每一帧的输出置信度记录下来和视频动作对齐观察模型是在动作刚开始就给对了结果还是等动作快结束了才反悔。这个步骤能暴露很多测试集精度看不出来的问题比如模型其实在依赖帧序号的先验分布而不是动作本身。操作上分两步。第一步在 demo_offline.py 基础上改一个小脚本把每帧的 softmax 输出保存下来同时记录动作切换的帧位置import torch import torch.nn.functional as F # 推理时把每帧 logits 转成 top-2 置信度保存 def dump_confidence(model, sample, label_map, save_path): model.eval() with torch.no_grad(): logits model(sample.unsqueeze(0)) # sample 是 (C,T,V) 单样本 prob F.softmax(logits, dim1) top2 torch.topk(prob, 2, dim1) with open(save_path, w) as f: for i in range(top2.indices.shape[1]): cls label_map[top2.indices[0, i].item()] conf top2.values[0, i].item() f.write(f{i},{cls},{conf:.4f}\n)第二步是找到视频里动作切换的关键帧对比 top-2 置信度的变化曲线。如果换动作的那几帧模型置信度突然掉到 0.3 以下说明时序上下文还没建立起来如果 top-1 从头到尾没换过但置信度慢慢爬升说明模型吃到了完整的动作轨迹。两种表现对应不同的调优方向前者加大时间卷积核尺寸或增加输入帧数后者降学习率再微调几轮就会稳定。这套验证方法帮助我抓住过两个问题一次是模型把“坐下”和“弯腰”混淆原因是测试视频里两个动作的结束姿势几乎一样模型在最后几帧才勉强区分——于是我把时间卷积核从 9 改成 5强制模型更多依赖动作前半段的状态另一次是边权重实验里 AddEdge 模型在低频动作上置信度偏低回放对比 Origin 模型后确认是新增的可学习权重在训练后期过拟合了训练集动作分布把 edge_weight 的 weight_decay 加上去就缓解了。从那以后我每次拿到新的动作识别权重无论是自己训的还是别人给的都会强制走一遍离线回放验证流程确认置信度和帧画面能对上。希望这篇拆解能帮你把 ST-GCN 的毕设项目跑顺也提醒一句先把 demo 和模型验证跑通再动网络结构是最省时间的路径。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑