资讯动态

ST-GCN自适应图卷积实战:骨架动作识别源码解析与避坑指南

发布时间:2026/9/28 17:25:45 来源:尧图企业网站定制
简介本资源为基于ST-GCN自适应图卷积网络的人体动作识别系统Python源码包面向计算机、人工智能、数据科学等专业的在校学生、教师及企业开发者可用于课程设计、毕业设计、大作业或项目立项演示帮助理解时空图卷积在骨骼关节点动作识别中的应用。压缩包共476个文件约43.69MB以258个py源码为核心辅以60个yaml配置、26个txt说明、22个gif与14个mp4演示素材以及npy、pkl数据文件和cu、pyx等扩展模块覆盖模型定义、训练推理与可视化全流程。目前已有284人学习下载。项目代码完整且功能验证通过包含使用说明读者可据此掌握ST-GCN自适应图卷积的建模思路、数据组织方式与运行排错方法并在此基础上二次开发DIY其他动作识别功能。1. 从骨架序列到动作标签这套 ST-GCN 源码到底能跑出什么很多人第一次接触人体动作识别都是从一段 RGB 视频开始的但真正做过落地的都知道RGB 路线对光照、遮挡、背景噪声极其敏感换个机位模型就废一半。这套基于 ST-GCN 自适应图卷积网络的 Python 源码走的是另一条路它不直接吃像素而是吃人体骨架关节点序列把每一帧的关节坐标当成图的节点把骨骼连接当成图的边再在时间维度上堆叠形成一个时空图。模型要学的就是这张图随时间怎么变形。这套资源能解决的核心问题是给你一份可运行、可复现的骨架动作识别基线。它适合正在做课程设计、毕设、大作业的学生也适合想快速验证骨架动作识别 pipeline 的工程师。你拿到手后不需要从零推导图卷积公式而是直接跑通数据加载、模型前向、训练和推理这条链路再基于它改自己的数据集。源码包里带了main.css、gpu_nms.cu、nms_kernel.cu这些文件说明它不只是纯 Python 脚本还涉及 CUDA 侧的 NMS 加速以及前端展示相关的样式文件整体是一个能演示、能训练、能推理的完整工程而不是一个孤立的模型文件。2. 骨架图怎么建、自适应邻接矩阵怎么算先把原理和选型讲透2.1 为什么是骨架图而不是直接上 3D 卷积骨架动作识别的输入通常来自姿态估计器比如 OpenPose、HRNet 或 MediaPipe输出是每帧每个关节点的 (x, y, confidence)。把这些点按人体结构连起来就得到一张自然图肩连肘、肘连腕、髋连膝、膝连踝。ST-GCN 的做法是把这张空间图沿时间轴复制 T 帧形成时空图然后用图卷积在空间维度聚合邻居节点用时间卷积在时间维度聚合相邻帧。选骨架而不是 RGB理由很直接骨架序列维度低、冗余少、对背景和光照不敏感而且关节点数量固定图结构稳定。代价是它依赖姿态估计的质量如果上游关节点抖动严重后面再强的图卷积也救不回来。常见做法是先在姿态估计阶段做平滑滤波再送入 ST-GCN。2.2 自适应邻接矩阵固定拓扑的补丁标准 ST-GCN 用的是预定义的人体骨架邻接矩阵也就是物理连接关系。但物理连接不一定等于动作判别最有效的连接。比如“拍手”这个动作左右手之间的关联比手到肘的物理连接更关键而预定义矩阵里左右手之间没有边。自适应图卷积的思路就是除了固定的物理邻接矩阵再学一个可训练的邻接矩阵让模型自己发现哪些关节点之间应该建立联系。实现上通常有三类做法一是全局自适应学一个所有样本共享的矩阵二是样本自适应根据输入特征动态生成邻接矩阵三是把两者加权相加。这套源码里既然叫“自适应图卷积网络”大概率是在固定邻接矩阵基础上叠加了可学习分支。你拿到代码后重点看模型定义里邻接矩阵是怎么注册成Parameter的以及前向传播时是直接相加还是做了归一化。2.3 数据准备从姿态估计到训练张量在跑训练之前你需要把原始视频或骨架数据整理成模型能吃的格式。常见有两种一种是直接使用公开骨架数据集比如 NTU RGBD 或 Kinetics-Skeleton另一种是自己用姿态估计器提取。下面这段脚本演示如何把逐帧关节点 JSON 整理成(N, C, T, V, M)的张量其中 N 是样本数C 是坐标通道T 是帧数V 是关节点数M 是人数。import json import numpy as np def load_skeleton_sequence(json_path, max_frames300, num_joints18): 读取单人或多人骨架 JSON输出 (C, T, V, M) 张量。 C2 表示只取 x,y如果有关节置信度可扩到 C3。 with open(json_path, r) as f: frames json.load(f) T min(len(frames), max_frames) V num_joints M 1 # 单人场景多人需要按置信度排序后取前 M 人 data np.zeros((2, T, V, M), dtypenp.float32) for t in range(T): people frames[t][people] if len(people) 0: continue # 按置信度选主目标 person max(people, keylambda p: np.mean(p[pose_keypoints_2d][2::3])) kps np.array(person[pose_keypoints_2d]).reshape(-1, 3) data[0, t, :, 0] kps[:V, 0] data[1, t, :, 0] kps[:V, 1] # 时间维度对齐不足 max_frames 的补零超出的均匀采样 if T max_frames: pad np.zeros((2, max_frames - T, V, M), dtypenp.float32) data np.concatenate([data, pad], axis1) else: idx np.linspace(0, T - 1, max_frames).astype(int) data data[:, idx, :, :] return data这段代码的关键参数有三个max_frames决定时间长度太短会丢动作信息太长会显存爆炸常见取值 150 到 300num_joints必须和你的姿态估计器输出一致OpenPose BODY_25 是 25 点COCO 是 17 或 18 点对不上模型直接报维度错误M是人数单人动作设为 1多人交互动作需要设成 2 并做排序对齐。逻辑上先按帧读取再按置信度选主目标最后做时间维度的补齐或采样保证所有样本形状一致。2.4 训练入口与关键超参源码包里的训练脚本通常叫main.py或train.py配合config目录下的 YAML 或 argparse 参数。你第一次跑建议先用小批量确认前向不报错再开完整训练。下面是一个典型的启动命令和参数含义。python main.py \ --config config/st_gcn/ntu-xsub/train.yaml \ --device 0 \ --batch-size 32 \ --epochs 80 \ --lr 0.01 \ --num-joints 25 \ --max-frames 300--device 0指定第一块 GPU没有 GPU 就改成-1走 CPU但训练会慢到怀疑人生--batch-size受显存限制骨架数据本身不大32 或 64 都常见--lr初始学习率 0.01 配合余弦退火或步进衰减--num-joints和--max-frames必须和你的数据预处理一致这是最常见的翻车点。如果启动后报size mismatch先检查这两个参数再检查邻接矩阵的维度是否和关节点数匹配。3. 把源码跑起来环境、数据、训练、推理四步落地3.1 环境配置CUDA、PyTorch 和编译扩展这套源码里有gpu_nms.cu和nms_kernel.cu说明它包含 CUDA 扩展不是纯 Python 包。你需要先确认本机 CUDA 版本和 PyTorch 版本匹配。常见做法是先装 PyTorch再装对应版本的 CUDA Toolkit最后编译扩展。如果只是跑骨架动作识别NMS 可能只在可视化或检测后处理里用到但既然源码带了建议一并编译避免运行时找不到模块。# 创建虚拟环境Python 版本建议 3.8 到 3.10 conda create -n stgcn python3.9 -y conda activate stgcn # 安装 PyTorch具体版本按你的 CUDA 调整 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 安装其余依赖 pip install numpy opencv-python pyyaml tqdm tensorboard # 编译 CUDA 扩展如果源码根目录有 setup.py python setup.py build_ext --inplace这里最容易出问题的是 CUDA 版本和 PyTorch 编译版本不一致表现为ImportError: undefined symbol或CUDA error: no kernel image is available。解决办法是先nvcc -V看 CUDA 版本再python -c import torch; print(torch.version.cuda)看 PyTorch 编译时用的 CUDA 版本两者尽量一致。如果实在编译不过而你的任务只涉及骨架识别可以先把 NMS 相关调用注释掉确认主流程能跑。3.2 数据目录组织与配置文件修改源码通常有固定的数据读取路径你需要按它的约定放数据。常见结构是data/train/和data/val/下按类别分文件夹或者提供一个train_label.json和val_label.json。下面是一个典型的目录组织。stgcn_project/ ├── data/ │ ├── train/ │ │ ├── class_0/ │ │ │ ├── sample_001.json │ │ │ └── sample_002.json │ │ └── class_1/ │ └── val/ │ ├── class_0/ │ └── class_1/ ├── config/ │ └── st_gcn/ │ └── custom/ │ └── train.yaml ├── model/ ├── main.py └── setup.py配置文件里重点改四个地方num_class改成你的类别数num_joints和max_frames和预处理对齐data_path指向你的数据根目录work_dir指向保存权重和日志的目录。改完先跑一个 epoch看 loss 是否下降、验证集是否加载正常。如果 loss 一直是nan检查输入里有没有全零样本或者学习率是不是太大。3.3 训练过程监控与断点续训训练启动后终端会打印每个 epoch 的 loss 和 accuracy同时 TensorBoard 会记录曲线。建议同时开一个终端跑tensorboard --logdir work_dir观察训练集和验证集的 loss 是否同步下降。如果训练集 loss 降但验证集 loss 升说明过拟合可以加 dropout、减小模型宽度或做数据增强。骨架数据增强常见做法包括随机旋转、随机缩放、随机时间裁剪、关节点抖动。断点续训靠--resume或配置文件里的resume字段。如果你训练到一半中断重新启动时指定上次保存的权重路径即可。注意有些实现会同时恢复优化器状态和 epoch 计数有些只恢复模型权重后者会导致学习率调度错乱。你可以在main.py里搜load_state_dict看它恢复了哪些内容。3.4 推理与可视化从单条骨架到动作标签训练完成后推理脚本通常叫demo.py或infer.py。它加载权重读入一条骨架序列输出类别概率。下面是一个简化的推理流程。import torch import numpy as np from model.st_gcn import STGCN # 加载模型结构num_class 和 num_joints 必须和训练时一致 model STGCN(num_class10, num_joints25, in_channels2) model.load_state_dict(torch.load(work_dir/best_model.pth, map_locationcpu)) model.eval() # 构造输入 (N, C, T, V, M) dummy_input torch.randn(1, 2, 300, 25, 1) with torch.no_grad(): logits model(dummy_input) prob torch.softmax(logits, dim1) pred torch.argmax(prob, dim1) print(预测类别:, pred.item(), 置信度:, prob[0, pred].item())这段代码里in_channels2表示只用 x、y 坐标如果你加了置信度就是 3num_joints25对应 OpenPose BODY_25M1是单人。推理时最容易忽略的是输入归一化训练时如果对坐标做了减均值除方差推理也必须做同样的处理否则结果会完全乱掉。你可以在数据加载类里找normalize相关代码把它复制到推理流程里。4. 避坑与排查这几处不提前处理跑通也白跑4.1 现象启动就报ModuleNotFoundError: No module named model原因通常是工作目录不对。源码里main.py可能用相对导入比如from model.st_gcn import STGCN如果你在model/目录下执行或者把项目路径改成了中文Python 就找不到包。解决方法是回到项目根目录执行并且确保项目路径全英文、无空格。项目说明里专门提醒“解压后重命名为英文”这不是客套话是血泪经验。4.2 现象训练 loss 正常下降但验证准确率始终在随机水平原因多半是标签和样本没对齐。骨架数据读取时如果用了glob或os.listdir文件顺序和标签文件顺序可能不一致。解决方法是打印前几个 batch 的标签和文件名人工核对。另一个可能是数据增强把动作语义破坏了比如时间裁剪太狠把“挥手”裁成了“抬手”。先把增强关掉确认基线能过拟合一个小数据集再逐步加增强。4.3 现象CUDA out of memory但 batch size 已经调到 1骨架数据本身不大显存爆炸通常来自两个地方一是max_frames设得太大比如 1000 帧时间维度展开后中间特征图很大二是模型里某个全连接层维度没对齐导致广播出巨大张量。解决方法是先把max_frames降到 150再检查模型定义里nn.Linear的输入维度是否和前面卷积输出一致。如果还不行用torch.cuda.empty_cache()清理缓存并确认没有在循环里累积计算图。4.4 现象推理结果每次都不一样同一段视频标签乱跳原因通常是模型没有切到eval()模式dropout 和 batch norm 还在训练状态。另一个可能是输入没有做和训练一致的归一化。解决方法是推理前强制model.eval()并用with torch.no_grad()包住前向。如果还跳检查数据加载时有没有随机采样推理必须用确定性的中心裁剪或均匀采样。4.5 现象自适应邻接矩阵训练后几乎没变化原因可能是学习率太小或者自适应分支被固定分支压制。解决方法是单独给自适应邻接矩阵参数设置更大的学习率或者在损失里加正则项鼓励它偏离固定矩阵。你可以打印训练前后邻接矩阵的差异如果 L2 距离几乎为零说明这个分支没学动。常见做法是把自适应矩阵初始化为单位矩阵或固定矩阵的副本再让它慢慢偏移。5. 进阶玩法换数据集、改图结构、导出部署5.1 换自己的数据集从关节点定义到类别映射如果你想用自己的动作数据第一步是确定关节点定义。假设你用 MediaPipe Pose输出 33 个关节点而源码默认可能是 25 或 18你需要改三处数据预处理里的num_joints、模型定义里的邻接矩阵维度、配置文件里的num_joints。邻接矩阵不能直接复用因为关节点编号和连接关系变了。常见做法是写一个函数根据 MediaPipe 的骨骼连接生成对应的邻接矩阵。import numpy as np def build_adjacency(num_joints, edges): 根据边列表构建邻接矩阵并做对称化和归一化。 edges: [(i, j), ...] 表示关节点 i 和 j 相连。 A np.zeros((num_joints, num_joints), dtypenp.float32) for i, j in edges: A[i, j] 1 A[j, i] 1 # 加自环 A np.eye(num_joints, dtypenp.float32) # 对称归一化 D^{-1/2} A D^{-1/2} D np.sum(A, axis1) D_inv_sqrt np.power(D, -0.5) D_inv_sqrt[np.isinf(D_inv_sqrt)] 0 D_mat np.diag(D_inv_sqrt) return D_mat A D_mat这段代码的关键是edges必须和你的关节点编号严格对应错一个连接就会让模型学到错误的物理关系。归一化是为了防止度数大的节点在聚合时数值爆炸。生成后保存成.npy在模型初始化时加载。5.2 改图结构从固定拓扑到多尺度图标准 ST-GCN 只用一层物理邻接矩阵进阶做法是引入多尺度图比如把一阶邻居、二阶邻居分别建图再拼接或加权。这样模型能同时捕捉局部关节运动和远端关节协同。实现上你可以构建多个邻接矩阵在空间图卷积里做多分支每个分支用不同的邻接矩阵最后把输出相加或拼接。代价是参数量和计算量增加训练时间变长小数据集上容易过拟合。5.3 导出与部署从 PyTorch 到 ONNX如果你要把模型部署到边缘设备或 C 推理引擎常见做法是导出 ONNX。骨架模型输入形状固定导出比较顺利。注意导出时把model.eval()和torch.no_grad()加上并指定动态轴。import torch model.eval() dummy torch.randn(1, 2, 300, 25, 1) torch.onnx.export( model, dummy, stgcn.onnx, input_names[skeleton], output_names[logits], dynamic_axes{skeleton: {0: batch}, logits: {0: batch}}, opset_version11 )导出后建议用onnxruntime跑一遍对比 PyTorch 和 ONNX 的输出差异。如果差异很大检查是否有自定义算子没被支持或者归一化层在导出时被折叠。骨架模型通常没有复杂的控制流导出成功率较高但自适应邻接矩阵如果是动态生成的需要确认 ONNX 是否支持对应的矩阵乘法。5.4 一个具体技巧用置信度加权代替硬阈值姿态估计输出的关节点置信度是个被低估的信息。很多人直接丢掉置信度只用 x、y。更好的做法是把置信度作为第三通道输入或者在空间聚合时用置信度加权让低置信度的关节点贡献更小。实现上可以在图卷积的邻接矩阵上乘一个置信度对角阵这样噪声关节点的消息传递会被自动抑制。这个改动很小但在实际视频上往往能带来几个点的提升。从那以后我每次拿到新的骨架数据都会先检查关节点编号和邻接矩阵是否对齐再跑一个过拟合小样本的 sanity check确认模型有能力记住数据才开完整训练。这个习惯帮我省下了大量排查“模型不学习”的时间。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑