☰
骨骼动作识别实战:ST-GCN时空图卷积从原理到部署
2026/10/14 1:22:02 网站建设 项目流程

简介:这份资源面向计算机、电子信息等专业的学生与研究者,提供基于时空图卷积(ST-GCN)的骨骼动作识别完整项目源码与说明,适合用作课程设计、期末大作业或毕业设计,也可作为深度学习与图神经网络方向的入门实践参考。压缩包共90个文件,约52.55MB,以29个Python源码为核心,配合13个yaml配置、3个pt预训练权重、3个mp4演示视频与11个gif效果图,另有prototxt网络定义、sh脚本及md说明文档,覆盖数据生成、模型定义、训练与推理全流程。项目内含NTU-RGB-D与Kinetics两套骨骼数据生成脚本、feeder数据加载模块、st_gcn与双流网络实现,以及离线与实时识别demo,目录按net、feeder、processor、resource等模块划分,结构清晰。目前已有589人学习下载,读者可据此理解时空图卷积的建模思路,掌握骨骼动作识别的数据处理、模型搭建与调试方法,并在此基础上自行扩展功能。

1. 骨骼动作识别为什么值得拆:从关节坐标到动作标签的一条完整链路

很多人第一次接触动作识别,脑子里想的是把视频丢进网络等结果。真做过就知道,RGB 视频里背景、光照、衣服颜色全是干扰项,换个拍摄角度模型就翻车。骨骼动作识别走的是另一条路:先用姿态估计把人体抽成十几到二十几个关节点的二维或三维坐标,再让模型只盯着这些坐标随时间怎么动。背景没了,视角影响小了,数据量也从几 MB 一帧降到几 KB 一帧。

时空图卷积(ST-GCN)就是这条路线里最经典的实现之一。它把每一帧的骨架当成一张图——关节点是图的节点,人体骨骼连接是图的边;再把连续帧之间同一关节连起来,构成一张时空图。图卷积负责在空间维度聚合相邻关节的信息,时间维度的卷积负责捕捉动作的演化。这套思路在 NTU-RGB+D 这类标准数据集上跑出过很有说服力的结果,也是后续很多骨骼动作识别工作的基线。

这份资源是一套完整的 Python 源码加项目说明,包含训练、推理、离线 demo、实时 demo、数据预处理脚本和预训练权重。适合两类人:一类是课程设计或毕设需要可运行基线的人,另一类是已经做过图像分类、想切到图神经网络和时序建模方向的人。下面按「这是什么 → 怎么跑起来 → 坑在哪 → 怎么改」的顺序拆一遍。

2. 环境与数据准备:把依赖、权重和 NTU 数据摆到位

2.1 依赖安装与目录结构确认

拿到压缩包先别急着python main.py。这个项目的依赖集中在requirements.txt,但里面有些包对版本敏感,尤其是 PyTorch 和 numpy。我一般会先建一个干净的虚拟环境,再按文件装。

# 建虚拟环境,Python 建议 3.8 到 3.10 python -m venv stgcn_env source stgcn_env/bin/activate # Windows 用 stgcn_env\Scripts\activate # 装依赖,先看 requirements.txt 里 torch 的版本要求 pip install -r requirements.txt # 如果 requirements 里没锁 torch,按自己 CUDA 版本装 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118

装完先确认目录里几个关键文件在不在:net/st_gcn.py是网络定义,feeder/feeder.py是 NTU 数据加载器,processor/recognition.py是推理流程,main.py是训练入口,demo_offline.py和demo_realtime.py是两个演示脚本。resource/下面放的是骨架数据和参考模型,work_dir/是训练输出目录。

提示:如果pip install卡在某个包上,先单独装 torch,再装其余依赖。torch 装不上,后面全是白搭。

2.2 预训练权重与数据格式

models/目录里有几个.pt文件,比如OriginSTGCN.pt、AddEdgeSTGCN12345.pt、kinetics-st_gcn.pt。这些是预训练权重,可以直接加载做推理,省去从头训练的时间。resource/下的kinetics_skeleton和NTU-RGB-D是骨架数据目录,kinetics-motion.txt和reference_model.txt是配套的标签或参考文件。

NTU 数据的加载逻辑在feeder/ntu_gendata.py和feeder/feeder.py里。数据不是原始视频,而是已经抽好的骨架序列,通常是.skeleton或.npy格式。每个样本的形状大致是(C, T, V, M):C 是坐标通道数(2D 是 2,3D 是 3),T 是帧数,V 是关节点数,M 是人数。这个形状很关键,后面改网络或换数据集都要围绕它调。

# feeder/feeder.py 里数据加载的核心逻辑(简化示意) class Feeder(data.Dataset): def __init__(self, data_path, label_path, ...): self.data = np.load(data_path) # 形状 (N, C, T, V, M) self.label = np.load(label_path) # 形状 (N,) # 训练时通常会做随机裁剪和抖动增强 self.random_choose = True def __getitem__(self, index): data = self.data[index] # (C, T, V, M) label = self.label[index] # 统一帧数,不足补零,超出随机截取 data = self._pad_or_crop(data, self.max_frames) return data, label

这段逻辑说明两件事:一是数据已经预处理成 numpy 数组,不需要自己跑姿态估计;二是帧数不固定,加载器会做补齐或裁剪。参数max_frames一般设 300,对应 NTU 里大多数动作的长度。改这个值会直接影响显存占用和时序建模范围。

2.3 训练与推理的启动方式

训练入口是main.py,配置文件在config/下,有st_gcn和st_gcn.twostream两套。单流模型直接跑:

python main.py --config config/st_gcn/ntu-xsub/train.yaml

推理用processor/recognition.py,或者直接跑demo_offline.py看单样本结果。demo_realtime.py是接摄像头的实时版本,但需要本地有可用的姿态估计前端,否则它拿不到骨架输入。这一点很多人第一次跑会懵:实时 demo 不是端到端的,它假设你已经有关节点流。

注意:demo_realtime.py依赖外部姿态估计输出,不是开箱即用的摄像头动作识别。没有骨架输入源,它只会报错或空转。

3. ST-GCN 网络结构拆解:图卷积、邻接矩阵与双流设计

3.1 空间图卷积的邻接矩阵怎么来的

ST-GCN 的核心在net/st_gcn.py。它先根据人体骨骼连接定义一个邻接矩阵 A,形状是(V, V),V 是关节点数。NTU 用的是 25 个关节点的布局,所以 A 是 25×25。这个矩阵不是学出来的,是人工按人体结构写死的:手连肘、肘连肩、肩连脊柱,等等。

但原始邻接矩阵只表达了「谁和谁直接相连」,ST-GCN 做了个重要改进:把邻接矩阵按「向心、离心、静止」三种关系拆成三个子矩阵。向心是远离骨架中心的边,离心是靠近中心的边,静止是自连接。这样每个关节点在聚合邻居信息时,能区分不同方向的邻居,表达能力比单一邻接矩阵强不少。

# net/st_gcn.py 里邻接矩阵拆分的简化逻辑 def get_adjacency(self, strategy): # 根据骨架连接生成基础邻接矩阵 A = self._build_adjacency() if strategy == 'spatial': # 拆成三个子图:向心、离心、自连接 A_out = normalize(A * self.edge_direction_out) A_in = normalize(A * self.edge_direction_in) A_self = normalize(np.eye(self.num_nodes)) return np.stack([A_self, A_in, A_out], axis=0) # (3, V, V)

参数strategy决定用哪种拆分方式。常见的有uniform(不拆)、distance(按距离拆)、spatial(按空间关系拆)。spatial效果通常最好,但计算量也最大。改这个参数会影响模型对动作方向的敏感度。

3.2 时间卷积与双流融合

空间维度聚合完,时间维度用标准的一维卷积沿 T 轴滑动。st_gcn.py里每个 ST-GCN block 的结构是:图卷积 → 时间卷积 → 残差连接。时间卷积的核大小一般是 9,也就是一次看前后各 4 帧。这个值在config里可以调,调大能捕捉更长时序,但显存涨得快。

双流版本在net/st_gcn_twostream.py。它同时跑两路:一路输入关节坐标,另一路输入骨骼向量(相邻关节的坐标差)。两路各出一个分类分数,最后融合。骨骼向量对动作方向更敏感,关节坐标对位置更敏感,两者互补。AddEdgeSTGCN12345.pt这个权重就是双流或加边版本的产物。

# net/st_gcn_twostream.py 双流融合示意 class TwoStreamSTGCN(nn.Module): def __init__(self, ...): self.joint_stream = STGCN(...) # 输入关节坐标 self.bone_stream = STGCN(...) # 输入骨骼向量 def forward(self, x_joint, x_bone): score_joint = self.joint_stream(x_joint) score_bone = self.bone_stream(x_bone) # 融合方式:加权求和或取平均 return score_joint + score_bone

融合方式在代码里可能是直接相加,也可能是加权。改权重会影响两路的重要性。如果动作以方向变化为主,骨骼流权重大一点;如果以位置变化为主,关节流权重大一点。这个没有固定答案,得看数据集。

3.3 训练参数与显存控制

训练时几个关键参数:batch_size、learning_rate、num_epochs、max_frames。NTU 数据量不小,batch_size一般设 16 到 64,取决于显存。learning_rate常用 0.1 配余弦退火,或者 0.01 配阶梯下降。max_frames设 300 时,单样本显存占用大概几百 MB,双流翻倍。

# 训练命令示例,覆盖配置文件里的参数 python main.py --config config/st_gcn/ntu-xsub/train.yaml \ --batch_size 32 \ --learning_rate 0.01 \ --num_epochs 80 \ --max_frames 300

如果显存不够,优先降batch_size,再降max_frames。降max_frames会损失长动作的时序信息,但比直接跑不起来强。训练日志在work_dir/下,看 loss 曲线和验证准确率判断是否收敛。

4. 避坑与排查:跑不起来时先看这几条

4.1 现象:加载权重报 key 不匹配

原因通常是网络结构和权重文件不对应。OriginSTGCN.pt对应原始单流结构,AddEdgeSTGCN12345.pt对应加边版本,kinetics-st_gcn.pt是在 Kinetics 上训的,类别数和 NTU 不一样。解决方法是先确认你要跑哪个配置,再加载对应权重。用torch.load后打印state_dict的 key,和模型state_dict对比,缺哪补哪或换权重。

4.2 现象:数据加载报形状错误

原因多半是数据路径不对或数据格式和 feeder 预期不符。feeder.py期望的输入是(N, C, T, V, M),如果你拿到的骨架数据是(N, T, V, C),就会在某个维度上对不上。解决方法是先np.load看形状,再决定要不要转置。ntu_gendata.py里有数据生成的参考逻辑,可以对照。

4.3 现象:实时 demo 没反应或报错

前面提过,demo_realtime.py依赖外部姿态估计输出。它本身不做姿态估计,只做动作分类。如果没有骨架输入源,它拿不到数据。解决方法是先用demo_offline.py验证模型能跑通,再考虑接实时骨架源。接的时候注意帧率和关节点顺序要和训练时一致,否则分类结果全是错的。

4.4 现象:训练 loss 不降或准确率上不去

常见原因有三个:学习率太大导致震荡,数据增强太强导致欠拟合,或者类别不平衡。先降学习率试,再把数据增强关掉试,最后看每个类别的样本数。NTU 里有些动作样本少,模型会偏向多数类。可以在 loss 里加类别权重,或者对少数类做过采样。

4.5 现象:显存溢出

batch_size和max_frames是主要因素,双流模型比单流多占一倍。先降batch_size到 8 或 4,再降max_frames到 150。如果还不行,检查是不是在 CPU 上跑,或者有没有其他进程占着显存。torch.cuda.empty_cache()能清缓存,但治标不治本。

5. 进阶用法:换数据集、改网络与验证技巧

5.1 换数据集时改哪几个地方

想用自己的骨架数据跑,需要改三处:一是数据加载器,把feeder.py里的路径和形状对齐;二是关节点数,net/st_gcn.py里的num_nodes要改成你的骨架关节点数;三是邻接矩阵,按你的人体连接重新定义。如果关节点数变了,预训练权重基本用不了,得从头训。

# 自定义数据集的 feeder 骨架 class MyFeeder(data.Dataset): def __init__(self, data_path, label_path, num_nodes=17): self.data = np.load(data_path) # 确保形状 (N, C, T, V, M) self.label = np.load(label_path) self.num_nodes = num_nodes # 和网络里的 num_nodes 一致 def __getitem__(self, index): data = self.data[index] # 如果关节点顺序和预训练不一致,这里要做重排 data = self._reorder_joints(data) return data, self.label[index]

关节点重排是容易被忽略的一步。不同姿态估计工具输出的关节点顺序不一样,直接喂给模型,邻接矩阵对不上,效果会崩。常见做法是画一张映射表,把源顺序映射到目标顺序。

5.2 验证模型是否真的学到了动作

光看准确率不够,有时候模型是靠数据集偏差蒙对的。我一般会做两个检查:一是打乱帧顺序,看准确率掉不掉,掉得厉害说明模型确实在用时序信息;二是只保留部分关节点,看哪些关节对分类贡献大。tools/下有些可视化脚本,可以画骨架序列和注意力权重。

检查项方法预期结果
时序依赖随机打乱帧顺序准确率明显下降
关节重要性逐关节遮挡关键关节遮挡后准确率下降
过拟合对比训练和验证曲线验证曲线不持续上升
类别偏差看混淆矩阵少数类不被全判成多数类

5.3 一个实用技巧:用预训练权重做特征提取

不想从头训,可以把预训练模型当特征提取器。去掉最后的分类层,取倒数第二层的输出作为特征,再接一个简单的分类器。这样在小数据集上也能有不错的效果,训练时间从几小时降到几分钟。

# 用预训练 ST-GCN 做特征提取 model = STGCN(...) checkpoint = torch.load('models/OriginSTGCN.pt') model.load_state_dict(checkpoint, strict=False) model.fc = nn.Identity() # 去掉分类层,输出特征 # 冻结前面的层,只训新加的分类器 for param in model.parameters(): param.requires_grad = False features = model(skeleton_input) # 形状 (N, feature_dim)

strict=False允许部分权重不匹配,适合结构微调后的加载。冻结参数能防止小数据集上过拟合。这个做法在课程设计里很实用,既能用上预训练,又不用花太多时间训练。

从那以后我每次拿到新的骨架数据集,都会先跑一遍帧打乱测试,确认模型不是靠静态姿势蒙对的,再往下调。希望帮到你。

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

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

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

立即咨询