ARTICLE DETAIL

建站实战干货

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

ST-GCN骨骼动作识别实战:邻接矩阵、双流建模与NTU数据预处理

2026/10/5 4:21:01 拓冰建站 浏览量
ST-GCN骨骼动作识别实战:邻接矩阵、双流建模与NTU数据预处理 简介本资源是一套基于时空图卷积网络ST-GCN的骨骼动作识别完整实现方案面向计算机、人工智能、数据科学等专业学生及初入CV领域的开发者适用于毕业设计、课程大作业与项目原型开发。压缩包含90个文件涵盖29个核心Python源码如st_gcn.py、recognition.py、feeder.py、13个配置型YAML文件、11个演示GIF动图、9个说明类TXT文档、5个结果可视化PNG图及3个预训练模型.pt整体大小为52.55MB目录结构清晰包含NTU-RGB-D与Kinetics双数据集支持、离线/实时推理demo、模型训练与数据预处理全流程脚本。已有230人学习下载提供可直接运行的测试代码、详细项目说明、常见问题参考及多版本模型权重显著降低图神经网络在动作识别任务中的复现门槛。1. ST-GCN骨骼动作识别不是“把关公耍大刀”它真能跑通NTU-RGB-D数据集且源码里藏着三处关键图结构实现细节你可能试过用CNN直接喂入2D关节点坐标序列——结果准确率卡在65%上不去模型像在猜拳也可能下载过标着“ST-GCN”的GitHub项目解压后main.py一跑就报KeyError: A连数据加载都失败。这不是你代码写得差而是ST-GCN根本不是“换个网络结构就能跑”的黑匣子它的时空建模能力90%依赖于邻接矩阵A的构造逻辑、骨骼拓扑的物理合理性、以及时间维度卷积与图卷积的耦合方式。这份资源包.zip内含完整st_gcn.py、feeder.py、ntu_gendata.py及预训练权重是少数几个真正跑通NTU-RGB-Dxsub/xview双协议、支持离线/实时双模式、且所有图结构参数可追溯的Python实现。它不教你怎么调参但把ST-GCN最硬核的三块拼图——人体骨架图定义、时空图卷积核设计、NTU数据格式解析逻辑——全摊开在st_gcn.py和feeder.py里。适合计算机、人工智能、物联网专业做毕设/课程设计的同学你不需要从零推导GCN数学但必须看懂A怎么从get_adjacency_matrix()生成edge_importance为何要按通道加权以及为什么kinetics_gendata.py里对Kinetics数据集的处理比NTU少一步归一化。这项目不是玩具是能进答辩PPT、能贴在简历“项目经验”栏里的实战基座。2. ST-GCN核心原理与代码落地从骨架图构建到双流时空建模2.1 骨骼图结构不是画个连线图NTU-RGB-D的物理拓扑与邻接矩阵生成逻辑ST-GCN的“图”不是抽象图论概念而是严格对应人体生物力学结构的带权邻接矩阵。NTU-RGB-D数据集定义了25个关节点如0: nose,1: left_eye,2: right_eye...24: right_ankle其连接关系由st_gcn.py中GraphNTU类固化# st_gcn.py 中 GraphNTU 类的关键片段 class GraphNTU: def __init__(self, labeling_modespatial): self.num_node 25 self.self_link [(i, i) for i in range(self.num_node)] # 物理连接左肩→左肘→左手腕右肩→右肘→右手腕... self.inward [ (0, 1), (1, 2), (2, 3), (3, 4), # 头部 (0, 5), (0, 6), (5, 7), (7, 9), (6, 8), (8, 10), # 左右肩臂 (0, 11), (0, 12), (11, 13), (13, 15), (12, 14), (14, 16), # 左右髋腿 (11, 17), (17, 19), (19, 21), (12, 18), (18, 20), (20, 22), # 左右脚踝 (15, 17), (16, 18), # 脚踝间连接 ] self.outward [(j, i) for (i, j) in self.inward] self.neighbor self.inward self.outward self.self_link注意这里的inward不是“向内”而是指符合人体运动链方向的有向边如肩→肘→腕outward是反向边self_link是自环。ST-GCN论文要求图必须包含双向边自环否则信息无法在节点间充分传播。neighbor列表最终被用于构建稀疏邻接矩阵A其形状为(25, 25)非零值位置即neighbor中的边索引。邻接矩阵A的生成在st_gcn.py的get_adjacency_matrix()函数中完成但关键点在于它不是静态矩阵而是动态加权的。权重由edge_importance张量控制该张量在STGCNBlock中与图卷积核相乘# st_gcn.py 中 STGCNBlock 的 forward 方法节选 def forward(self, x): # x: [N, C, T, V] - Nbatch, Cchannel, Ttime, Vvertex x self.conv1(x) # 时间卷积 x self.gcn(x, A) # 图卷积x A * edge_importance x self.conv2(x) # 时间卷积 return xedge_importance是一个(3, 25, 25)张量对应3种边类型self/inward/outward每个元素代表该边在当前层的贡献权重。它在训练中更新但初始化时已按物理意义分配inward边权重最高运动链主干outward次之self_link最低。这是ST-GCN超越普通GCN的核心——边重要性学习让模型自动发现哪些骨骼连接对动作判别最关键。2.2 双流ST-GCN为什么光靠关节坐标不够运动流Motion Stream的实现细节单纯输入关节点坐标x,y,z会丢失速度、加速度等动态信息。ST-GCN原论文提出双流架构关节流Joint Stream处理原始坐标运动流Motion Stream处理相邻帧间的坐标差即位移向量。本项目在st_gcn_twostream.py中实现了该设计# st_gcn_twostream.py 中 MotionFeeder 的关键逻辑 class MotionFeeder(Feeder): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def _load_data(self, idx): # 加载原始关节数据 [C, T, V, M] - C3(xyz), T帧数, V25节点, M人数 joint_data self.data[idx].copy() # 计算运动流当前帧 - 前一帧首帧补零 motion_data np.zeros_like(joint_data) motion_data[:, 1:, :, :] joint_data[:, 1:, :, :] - joint_data[:, :-1, :, :] # 拼接 Joint Motion 作为双流输入 data np.concatenate((joint_data, motion_data), axis0) # C6 return data参数说明motion_data的计算是逐通道x/y/z独立进行的确保运动矢量的物理意义。np.concatenate将通道数从3扩展到6后续网络输入层需适配in_channels6。双流并非简单加权融合而是通过STGCNBlock分别处理后再在顶层特征图相加见recognition.py中TwoStreamSTGCN类的forward方法。这种设计使模型对快速动作如挥手、踢腿更敏感——因为运动流放大了位移突变。2.3 NTU-RGB-D数据预处理ntu_gendata.py如何把原始skeleton文件转成.npy张量NTU-RGB-D原始数据是.skeleton文本文件每行存储一帧的25个关节点坐标含置信度。ntu_gendata.py负责将其解析为标准numpy张量。关键步骤如下读取与校验跳过注释行以#开头按空格分割每行提取x,y,z,confidence四元组缺失值处理当某节点置信度0.1时用前一帧同节点坐标线性插值interpolate_missing函数归一化以第1帧的骨盆节点index11为原点所有坐标减去该点坐标再除以躯干长度neck到hip_center距离作尺度归一化格式转换重组为[C3, T, V25, M2]张量M2表示最多2人不足帧数补零超长截断。# ntu_gendata.py 中 normalize_skeleton 函数核心逻辑 def normalize_skeleton(skel_data): # skel_data: [T, V*4] - 每帧25*4100个数值 T skel_data.shape[0] skeleton np.zeros((T, 25, 3)) for t in range(T): for v in range(25): idx v * 4 x, y, z skel_data[t, idx], skel_data[t, idx1], skel_data[t, idx2] conf skel_data[t, idx3] if conf 0.1: # 插值逻辑找最近的有效帧 skeleton[t, v] interpolate_at_t(skel_data, t, v) else: skeleton[t, v] [x, y, z] # 归一化以第0帧骨盆(11)为原点 origin skeleton[0, 11] skeleton skeleton - origin # 尺度归一化除以neck(1)到hip_center(11)距离 scale np.linalg.norm(skeleton[0, 1] - skeleton[0, 11]) skeleton skeleton / (scale 1e-6) return skeleton # [T, 25, 3]血泪经验很多同学跑不通是因为跳过了ntu_gendata.py的generate_dataset()函数——它不仅生成.npy还创建train_label.npy和val_label.npy其中标签索引必须与NTU官方action_list.txt严格对齐如0: drink water,1: eat meal/snack。若手动修改过动作类别数务必同步更新config/st_gcn.yaml中的num_class参数否则CrossEntropyLoss会因维度不匹配崩溃。3. 从零运行项目环境配置、数据准备到模型推理全流程3.1 环境搭建Python 3.8 PyTorch 1.10 是唯一验证过的组合本项目在Ubuntu 20.04 Python 3.8.10 PyTorch 1.10.0 CUDA 11.3环境下全程测试。切勿使用PyTorch 2.x——torch.nn.functional.interpolate在2.x中默认align_cornersTrue而ST-GCN需要align_cornersFalse见st_gcn.py中TemporalConv的upsample操作否则时间维度插值会引入偏移导致精度下降5%以上。# 创建conda环境推荐 conda create -n stgcn python3.8 conda activate stgcn # 安装指定版本PyTorchCUDA 11.3 pip install torch1.10.0cu113 torchvision0.11.1cu113 torchaudio0.10.0cu113 -f https://download.pytorch.org/whl/torch_stable.html # 安装其他依赖 pip install -r requirements.txt # requirements.txt 关键项 # numpy1.21.6 # opencv-python4.5.5.64 # tqdm4.64.0 # tensorboard2.10.0提示requirements.txt中torchlight是作者自研的轻量级训练框架位于torchlight/目录它替代了torch.utils.data.DataLoader的复杂封装提供DataProcessor统一管理数据加载。安装时需先cd torchlight pip install -e .否则processor.py会报ModuleNotFoundError。3.2 数据准备NTU-RGB-D下载、解压与路径映射NTU-RGB-D官网https://rose1.ntu.edu.sg/dataset/action-recognition/需注册下载。下载后得到nturgbd_skeletons_s001_to_s017.zip等17个压缩包。解压后目录结构应为NTU-RGB-D/ ├── S001C001P001R001A001.skeleton # 示例文件 ├── S001C001P001R001A002.skeleton ...将此目录路径填入config/st_gcn.yaml的data_path字段# config/st_gcn.yaml data_path: /path/to/your/NTU-RGB-D # 注意路径末尾不要加斜杠 # 同时设置数据划分协议xsub: subject-based, xview: camera-based benchmark: xsub然后执行数据预处理脚本# 生成训练/验证数据集耗时约2小时CPU 16核 python ntu_gendata.py --data_path /path/to/NTU-RGB-D --out_folder ./data/ntu # 生成后目录结构 # ./data/ntu/ # ├── train_data.npy # [N, 3, 300, 25, 2] # ├── train_label.npy # [N,] # ├── val_data.npy # [N, 3, 300, 25, 2] # └── val_label.npy # [N,]参数说明--data_path指向NTU原始.skeleton文件根目录--out_folder指定输出路径。脚本会自动按xsub协议划分S001-S017中奇数编号Subject为训练偶数为验证并生成300帧固定长度的样本不足补零超长截断。若需xview协议修改ntu_gendata.py中get_valid_file_list()函数的camera_split逻辑。3.3 模型训练与推理一条命令启动但参数必须手调训练命令使用单卡GPU# 训练ST-GCNxsub协议 python main.py --config config/st_gcn.yaml --work_dir work_dir/stgcn_xsub --save_log # --work_dir: 日志和模型保存路径 # --save_log: 保存TensorBoard日志config/st_gcn.yaml中关键参数参数默认值说明base_lr0.1初始学习率NTU上建议0.01过大易震荡batch_size32显存占用RTX 3090可跑64GTX 1080Ti建议16num_epoch80xsub协议通常60轮收敛xview需80轮device[0]GPU索引多卡用[0,1]推理命令离线视频或.npy文件# 使用预训练模型识别单个样本 python demo_offline.py \ --weights ./models/OriginSTGCN.pt \ --data_path ./data/ntu/val_data.npy \ --label_path ./data/ntu/val_label.npy \ --sample_idx 0 \ --show_topk 3 # 输出Top-3预测动作及概率实时摄像头推理需USB摄像头# 启动实时识别依赖OpenPose或MediaPipe获取骨架 python demo_realtime.py --weights ./models/OriginSTGCN.pt --device 0 # 注意demo_realtime.py 默认调用OpenPose需提前编译OpenPose并设置OP_PATH避坑demo_realtime.py中pose_estimation模块未包含OpenPose二进制需自行编译。若无OpenPose可改用mediapipe修改demo_realtime.py第42行from pose.openpose import OpenPose为from pose.mediapipe_pose import MediaPipePose但需安装pip install mediapipe且mediapipe输出的25节点顺序与NTU不完全一致需在feeder/feeder.py中重映射。4. 避坑指南ST-GCN项目里五个让你重启三次的致命错误4.1 现象KeyError: A或AttributeError: NoneType object has no attribute shape原因st_gcn.py中STGCNBlock的forward方法调用self.gcn(x, A)时A未正确传入。根源在于processor.py的start方法未初始化图结构——self.graph为空导致A self.graph.A返回None。解决检查processor.py第127行self.graph import_class(self.arg.graph)(**self.arg.graph_args)是否执行。若self.arg.graph为graph.ntu_rgb_d.GraphNTU确认graph/ntu_rgb_d.py存在且GraphNTU类有__init__方法。常见错误是graph目录名拼错如ntu_rgb_d写成ntu-rgb-d。4.2 现象训练loss为nan或accuracy stuck at 0.04随机猜测水平原因ntu_gendata.py生成的.npy数据未归一化或归一化尺度错误。原始NTU坐标范围在[-1,1]若未减去骨盆原点所有关节点会集中在画面一角图卷积无法捕获空间关系。解决打开./data/ntu/train_data.npy用np.mean(data, axis(0,2,3))检查各通道均值。正常值应在[-0.1, 0.1]内。若均值0.5说明归一化失败回查ntu_gendata.py中normalize_skeleton函数的origin计算是否用了第0帧而非平均帧。4.3 现象demo_offline.py报错IndexError: index 100 is out of bounds for axis 0 with size 100原因val_data.npy形状为[N, 3, T, 25, 2]但sample_idx100时N100。NTU-xsub验证集仅1656个样本索引最大为1655。解决先运行python -c import numpy as np; dnp.load(./data/ntu/val_data.npy); print(d.shape)确认样本数再用合法索引如--sample_idx 0。4.4 现象demo_realtime.py黑屏或cv2.VideoCapture(0)返回False原因OpenCV未正确识别摄像头。Linux下需检查/dev/video0权限sudo usermod -a -G video $USERWindows下可能是摄像头被Zoom/Teams独占。解决先运行python -c import cv2; capcv2.VideoCapture(0); print(cap.isOpened())验证。若为False尝试capcv2.VideoCapture(1)或更换USB接口。4.5 现象main.py训练时显存OOMOut of Memory原因batch_size32在NTU数据上需约12GB显存GTX 1080Ti11GB实际极限为batch_size16。但更隐蔽的问题是feeder.py中__getitem__未释放中间变量。解决在feeder.py的__getitem__末尾添加del data, label并在DataLoader中设置pin_memoryFalseconfig/st_gcn.yaml中pin_memory: False。实测可降低显存占用1.2GB。5. 进阶技巧如何用这份源码做课程设计答辩演示三个必改参数与一个可视化神技5.1 课程设计答辩必备三处低风险高价值修改课程设计的核心诉求不是刷SOTA指标而是清晰展示技术理解深度。以下三处修改无需重训模型5分钟内完成却能让答辩老师眼前一亮修改动作类别名称resource/ntu_action_list.txt是NTU官方动作名列表如0: drink water。将其改为中文并精简例如0: 喝水 1: 吃饭 2: 打电话 ...然后在demo_offline.py第89行print(fPredicted: {label_name[pred[0]]})中加载该文件。答辩时展示“喝水”而非“drink water”体现工程落地意识。可视化骨架动画tools/DrawLine.py是作者预留的绘图工具。修改其draw_skeleton函数添加关键帧高亮# tools/DrawLine.py 第52行 def draw_skeleton(frame, joints, color(0,255,0)): # 绘制骨架线 for start, end in GraphNTU().inward: cv2.line(frame, tuple(joints[start]), tuple(joints[end]), color, 2) # 高亮手腕节点动作关键部位 for idx in [9, 10, 21, 22]: # left_wrist, right_wrist, left_ankle, right_ankle cv2.circle(frame, tuple(joints[idx]), 4, (0,0,255), -1) return frame在demo_offline.py中调用DrawLine.draw_skeleton生成GIF答辩PPT插入动态骨架图直观证明“模型关注的是肢体末端运动”。添加置信度阈值开关demo_offline.py默认输出Top-3。增加--threshold 0.7参数仅当最高置信度0.7才显示预测否则输出“动作未识别”。这体现对模型局限性的认知——ST-GCN在遮挡场景下确实会失效。5.2 验证模型是否真学到了“时空关系”用梯度热力图定位关键帧ST-GCN的卖点是“时空建模”但如何证明它没偷懒只看单帧用captum库做梯度类激活映射Grad-CAMpip install captum在demo_offline.py中插入热力图生成逻辑# demo_offline.py 末尾添加 from captum.attr import LayerGradCam import matplotlib.pyplot as plt # 获取模型最后一层STGCNBlock target_layer model.st_gcn_networks[-1].gcn cam LayerGradCam(model, target_layer) # 计算热力图针对时间维度T input_tensor torch.tensor(data).unsqueeze(0) # [1, 3, T, 25, 2] attributions cam.attribute(input_tensor, target0) # target预测类别 # attributions.shape: [1, 3, T, 25, 2] # 取时间维度平均得到每帧重要性 frame_importance attributions.mean(dim(1,3,4)).squeeze() # [T] plt.plot(frame_importance.numpy()) plt.title(Frame Importance Score) plt.xlabel(Frame Index) plt.ylabel(Importance) plt.savefig(frame_importance.png)运行后生成frame_importance.png若曲线呈双峰如挥手动作在抬手和落手帧峰值最高则证明模型确实在学习时间动态若单峰集中在首帧则说明它退化为CNN。这个图放进答辩PPT比讲一百遍“ST-GCN有时间卷积”都有力。5.3 从那以后我每次做课程设计都强制走一遍“三步验证法”第一步用demo_offline.py跑通一个已知动作如sample_idx0NTU中是“喝水”截图保存预测结果第二步打开./data/ntu/val_data.npy用np.savez_compressed(debug_sample.npz, datadata[0])导出该样本用npz查看器如VS Code的NetCDF插件观察坐标是否归一化第三步修改st_gcn.py中STGCNBlock的conv1卷积核大小如kernel_size(9,1)改为(3,1)重新训练10轮对比loss下降速度——如果变化不大说明原设计合理如果loss爆炸说明时间感受野确实关键。这三步花不了20分钟但它能帮你避开80%的“答辩现场蓝屏”事故。课程设计不是交差是建立技术直觉的起点。希望帮到你。本文还有配套的精品资源点击获取