简介:基于时空图卷积(ST-GCN)的骨骼动作识别项目源码包,面向计算机视觉、人工智能及相关专业的学生与开发者,可用于毕业设计、课程设计、课程大作业或初期项目演示。资源内置完整的Python实现与项目说明,覆盖从骨骼数据预处理、图卷积网络构建、双流ST-GCN变体到离线/实时推理的完整链路;同时提供演示动图、操作录屏和预训练模型权重,既可跳过训练直接验证效果,也可深入阅读源码进行二次开发,便于对比不同网络结构以加深对方法的理解。压缩包共90个文件,以py源码、yaml配置、pyc缓存、gif演示、mp4录屏、pt模型权重、markdown说明为主,整体约52.55MB,目录模块划分清晰,检索和上手都很方便。目前已有230人浏览学习,适合需要完整实战项目参考、或想快速理解图卷积动作识别原理的初学者与进阶者使用。
1. 基于时空图卷积(ST-GCN)的骨骼动作识别:一份能直接跑起来的 Python 源码
拿到手的这份资源是「基于时空图卷积(ST-GCN)的骨骼动作识别」Python 源码包,压缩包把工程代码、配置文件、模型权重和项目说明一起打好了。做动作识别时,很多人第一反应是上 RGB 视频接 3D CNN,但骨骼序列方案在算力消耗和特征鲁棒性上明显更省——它把人体抽象成几十个关键点,动作就变成一张随时间变化的图,ST-GCN 就在这张图上做卷积。这个包把整条链路都带齐了:NTU-RGB-D 和 Kinetics 数据集的处理脚本、双流 ST-GCN 模型、三个训练好的权重文件、离线视频 demo 和实时摄像头 demo。适合拿来当毕业设计、课程设计、大作业的复现起点,也适合想搞清楚图卷积在动作识别里到底怎么落地的从业者。
2. 时空图卷积在计算什么:从骨架坐标到邻接矩阵与时间卷积
2.1 骨骼图是怎么构建的:节点、边和归一化邻接矩阵
ST-GCN 的核心假设是:人体骨架不是一堆孤立的点,而是一张结构化的图。每一帧里,关节是图的节点,骨骼是图的边。以 NTU-RGB-D 数据为例,单个人有 25 个关节点,一个样本是一段 T 帧的序列,那整个输入就可以张成一个张量[N, C, T, V, M]——N 是样本数,C 是通道数(一般 3 个通道:x、y 坐标加置信度),T 是时间帧数,V 是关节数,M 是帧内人数。项目里 feeder 层就是按这个格式往外吐数据的。
图卷积跟普通卷积最大的差别在邻接矩阵上。普通卷积的卷积核在像素网格上滑窗,而图卷积要先把图结构编码成矩阵,再跟特征做运算。我节选了项目st_gcn.py里图卷积的核心段落,结构大致如下:
# 图卷积前向:x 是输入特征,A 是归一化后的邻接矩阵 def forward(self, x, A): # 先用 1x1 卷积把通道数做变换,等价于对每个节点做特征映射 x = self.conv1(x) # 核心公式:邻接矩阵与特征相乘,让每个节点聚合邻居节点的信息 x = torch.einsum('nctvw,vw->nctv', x, A) if self.residual: x = self.resa(x) return x这段代码里最关键的逻辑是torch.einsum那一步。nctvw是输入张量的维度,vw是邻接矩阵的形状,两者做矩阵乘法,相当于把每个节点周围邻居的特征按权重累加起来,这也是"图卷积"名字的由来——它跟图像卷积一样是聚合邻域信息,只不过邻域结构由邻接矩阵定义,而不是由 3×3 的滑窗定义。
真正工程上要注意的是 A 矩阵的归一化。原始邻接矩阵直接乘会出现一个问题:度大的节点(比如躯干上的关节)聚合的特征量级明显大于四肢末端节点,训练时容易偏向某些关节。常见做法是用对称归一化公式D^(-1/2) * A * D^(-1/2),其中 D 是度矩阵。项目里这部分是在模型初始化时算好的,我建议你拿到代码后第一件事就是把 A 打印出来看一眼,确认是归一化后的矩阵,这一步对训练稳定性影响很大。
2.2 时间维度怎么处理:为什么叫"时空"图卷积
空间上的图卷积解决了一帧内关节之间的信息传递,但动作的本质是"随时间变化",所以还必须处理时间维。ST-GCN 的做法比较直接:在空间图卷积之后接一个时间维度的普通卷积,卷积核沿着帧的方向滑动。因为骨架序列每一帧的节点数是固定的,时间维度可以当图像的宽来处理,用 Conv2d 就能实现。
# 时间卷积:卷积核大小为 (1, kernel_size),只在时间维上滑动 self.tcn = nn.Sequential( nn.Conv2d( out_channels, out_channels, kernel_size=(1, kernel_size), stride=(1, stride) ), nn.BatchNorm2d(out_channels), nn.ReLU(inplace=True), )时间卷积核的kernel_size决定了每个节点能"看到"前后多少帧的信息,这是动作识别里很重要的超参数。你把它设成 1,模型就只能看到当前帧,等于退化成纯空间模型;设太大(比如超过 15),短时动作容易被平滑掉,响应变迟钝。项目默认配置差不多在 9 左右,这个值在 NTU-60 上验证过,我拿它跑实时 demo 时识别延迟也能接受。
另外注意这个包里有一个st_gcn_twostream.py,对应的是双流 ST-GCN。所谓双流,是把关节坐标流和骨骼向量流分别送进两个 ST-GCN 模型,最后融合分数。骨骼向量的几何含义是关节之间的方向矢量,它对"手举起还是放下""左腿踢还是右腿踢"这类镜像动作区分度更高,两个流互补后精度会有明显提升。这也解释了为什么压缩包里有OriginSTGCN.pt和AddEdgeSTGCN12345.pt多个权重文件——一个是原始单流/原版复现,一个是加了自定义边的改进版本,跑 demo 的时候记得看清楚加载的是哪一个。
3. 源码地图:从 main.py 到 st_gcn.py 的数据链路
3.1 入口文件是怎么调度的:main.py 与 processor 的分工
解压之后不要急着双击某个 py,先把目录结构捋一遍。main.py是总入口,它负责读取config目录下的 yaml 配置、初始化模型和处理器;processor目录里的processor.py是训练和验证的核心循环;feeder目录负责数据加载,把磁盘上的骨架数据变成模型能吃的张量。链路是:main.py→ 读配置 → 初始化processor→ 初始化feeder→ 开始训练/测试/demo。
main.py里调用处理器的代码很简短,但决定了整个任务跑什么模式:
# main.py 中根据子命令选择运行模式 if args.mode == 'train': processor = Processor(config, train=True) processor.train() elif args.mode == 'test': processor = Processor(config, train=False) processor.test() elif args.mode == 'demo': processor = Processor(config, train=False) processor.demo()这里args.mode是在命令行由--mode参数指定的,常见的坑是有人只想测试模型,却忘了带--mode test,结果默认进了训练流程。Processor类里还维护了优化器、学习率调度和日志记录,训练时每多少个 epoch 会往work_dir里写 checkpoint。
3.2 feeder 层:骨架数据从原始文件到 Tensor 的加工流程
数据侧是整个项目里最容易翻车的地方,因为 NTU-RGB-D 原始数据是.skeleton格式,Kinetics 的骨架是 json 格式,两种格式完全不一样。项目里有ntu_gendata.py和kinetics_gendata.py两个脚本,分别把两种原始数据的坐标信息抽取成 numpy 数组或 json 缓存,供 feeder 读取。
# ntu_gendata.py 里读取单帧骨架数据的示意逻辑 def get_body(fd, frame_data): # 帧数据里先读入人体数量 num_body = frame_data[0] # 每个body的25个关节坐标 + 置信度依次读取 for b in range(num_body): for v in range(25): x = frame_data[offset] y = frame_data[offset + 1] confidence = frame_data[offset + 2] # 写入 [关节数, 3, 帧数] 的结构每个关节读到的 x、y、confidence 三个值就是模型的原始输入通道。这里有一个隐藏细节:NTU 数据里有视野之外或者被遮挡的关节,confidence 就是用来标记不可见关节的。有的同学预处理时直接把低置信度的关节坐标置零,这会导致图卷积聚合到一堆假信息,反而干扰判断——合理的做法是保留坐标但让模型自己去学置信度的权重,这个包的双流模型就是这么处理的。
feeder.py里还有一个正则化步骤:把坐标从像素坐标系归一化到 [-1, 1] 左右。如果你之后想用自己的骨架数据替换训练集,一定要先做同样的归一化,否则加载预训练权重后第一轮 loss 往往是 NaN,这一条后面避坑章节会展开讲。
4. 复现路径:装依赖、下权重、跑通离线与实时 demo
4.1 环境安装与 torchlight 工具库
这个项目用的是 PyTorch,依赖清单都在requirements.txt里,基础包包括 torch、torchvision、numpy、opencv-python、pyyaml、h5py 这些常规项。安装前我建议用虚拟环境,别把系统 Python 环境搞乱了,命令如下:
# 创建虚拟环境并激活 python -m venv stgcn_env source stgcn_env/bin/activate # Windows 下执行 stgcn_env\Scripts\activate # 安装依赖 python -m pip install -r requirements.txt # 安装项目自带的 torchlight 工具库 cd torchlight python -m pip install -e . cd ..torchlight 是这个项目自己封装的辅助库,提供配置解析、日志输出、GPU 设备管理等基础功能,所以必须先装。-e表示以可编辑模式安装,之后你改 torchlight 里的代码不用重新安装即可生效,调试时方便很多。
这里有个值得注意的点:如果requirements.txt里没有锁死 PyTorch 版本,建议装 1.13 或 2.x 的稳定版本。太老的 PyTorch 在新显卡上可能因为 CUDA 版本不匹配起不来,太新的版本又可能跑老代码时遇到 API 变动。项目里get_models.sh是下载三个预训练权重的脚本,如果网络条件不支持直接跑脚本,也可以手动下载后放到models目录,文件名必须和代码里写的一致。
4.2 跑离线 demo:用现成骨架视频验证模型
装好依赖后先用离线 demo 验证整条链路通不通,不要一上来就开摄像头。离线 demo 的意思是输入一段已经抽取好骨架的素材(不是原始 RGB 视频),项目demo_asset目录里带了样例素材。
# 离线demo:读取骨架素材并输出识别结果 python demo_offline.py --config config/st_gcn.twostream/kinetics-skeleton.yaml --weights models/kinetics-st_gcn.pt参数说明:--config指定模型结构和训练超参的 yaml 文件,st_gcn.twostream表示双流配置;--weights指定预训练权重,这里我用的是 Kinetics 上训练的kinetics-st_gcn.pt,它的类别数是 400 类,覆盖日常动作面更广。如果屏幕上弹出的窗口能看到骨架叠加画面和动作类别标签,说明环境没有问题。
4.3 实时摄像头 demo:识别延迟与设备选择
离线验证通过后再上demo_realtime.py,它调用本机摄像头实时抽取骨骼并识别。这个脚本依赖 OpenPose 或类似工具做人体关键点检测,所以对 CPU 占用很高,我跑的时候 GPU 版还能维持在实时帧率,纯 CPU 环境建议把输入分辨率调低。
# 实时demo,默认调用摄像头 0 python demo_realtime.py --config config/st_gcn/kinetics-skeleton.yaml --weights models/OriginSTGCN.pt我一般习惯先用--weights models/OriginSTGCN.pt而不是双流加边模型跑通流程,因为单流模型结构更简单,出问题更容易定位。双流模型要同时加载两个流的网络结构,如果权重文件缺失其一,大概率在load_state_dict阶段直接崩溃,这个先后顺序能帮你少踩一层坑。
识别延迟上,如果感到明显的卡顿,第一件事检查摄像头输入帧率,第二件事检查模型是否在 GPU 上。项目里torchlight/gpu.py是 GPU 设备管理工具,默认会自动选择 cuda,你可以加--device cuda:0显式指定。
5. 常见问题与排查:五个实操里最容易翻车的点
5.1 模型权重加载报错:键名不匹配
现象:运行 demo 或测试时,控制台抛出Missing key(s) in state_dict或unexpected key的报错,后面跟一长串 conv 层的名字。 原因:权重文件里的模型结构和当前代码定义的模型不一致。最常见的是你用了双流配置却加载了单流权重,或者新旧代码里层的命名从st_gcn.0.conv改成了st_gcn_0.conv。 解决:先把报错里缺的键名打印出来,用torch.load加载权重后看state_dict的 key 列表,对照st_gcn_twostream.py的模型定义逐层核实。如果只是命名空间差异,写个循环把权重字典里的 key 做字符串替换即可,不要直接忽略错误继续跑。
5.2 实时 demo 黑屏或报摄像头被占用
现象:demo_realtime.py能运行,但窗口黑屏,或者直接报Cannot open camera。 原因:摄像头索引不对。笔记本自带摄像头一般是 0,外接 USB 摄像头可能是 1 或 2。另外很多软件(比如微信视频通话、OBS)占着摄像头时,OpenCV 拿不到设备。 解决:把代码里cv2.VideoCapture(0)的索引依次改成 1、2 逐个尝试;关掉其他占用摄像头的程序再试。这个坑在课堂演示时最容易遇到,建议提前把摄像头单独测一遍。
5.3 训练 loss 不降或直接发散
现象:训练第一个 epoch loss 就是 NaN,或者 loss 持续在 2.0 以上不下降。 原因:最常见的是学习率设置不合理。这个项目原始配置用的是 SGD,初始学习率 0.1,如果你换了 Adam 优化器还沿用 0.1,必定发散。再者,输入数据没做归一化,坐标值直接落在像素量级(几百到上千),梯度很容易爆炸。 解决:换成 Adam 时把学习率降到 0.001~0.0001 起步;检查 feeder 输出的张量最大值,确认坐标已经缩放到 [-1,1]。还有一个小玄学是 batch size 太小(比如 1)时 BatchNorm 的统计量不稳定,也容易出 NaN,至少要 16。
5.4 骨架点数量对不上模型输入
现象:用自己的视频跑 demo,识别完全不对,或者模型直接报维度错误的异常。 原因:Kinetics 预训练模型用的是 OpenPose 输出的 18 个(或对齐后的 25 个)关节点,NTU 用的是 25 个关节点。你采集的骨骼数据关节点定义和顺序跟预训练模型不一致时,输入张量的 V 维就对不上。 解决:先确认你用的骨架抽取工具输出哪些关节点,然后写一个映射表,把自定义骨架的关节点顺序对齐到模型要求的顺序。关节顺序错了模型不会报错,但识别结果会像随机乱猜,这是最隐蔽的问题。
5.5 视频 demo 无法播放或保存失败
现象:离线 demo 能正常推理,但输出视频不生成,或者生成的是空文件。 原因:OpenCV 的VideoWriter编码器依赖系统编解码库,Linux 服务器上尤其容易缺mp4v编码支持。 解决:在代码里把VideoWriter_fourcc从mp4v换成XVID,容器从 mp4 改成 avi,成功率会高很多。这招在服务器上救了我好几次。
6. 进阶:给邻接矩阵加一条自定义边,改出自己的动作识别模型
跑通 demo 只是第一步,这个压缩包里真正有意思的是AddEdgeWeight_2.txt和AddEdgeSTGCN12345.pt——它暗示了这个项目做过"加边"实验。所谓加边,就是在原始骨骼图基础上人为增加一些跨越式连接,比如把左手腕和右脚踝连一条边,让模型捕捉长距离的肢体协同关系。
# 加载一个训练好的模型,观察加边前后的权重差异 import torch from net.st_gcn import Model model_origin = Model(in_channels=3, num_class=400) model_origin.load_state_dict(torch.load('models/OriginSTGCN.pt')) model_added = Model(in_channels=3, num_class=400) model_added.load_state_dict(torch.load('models/AddEdgeSTGCN12345.pt')) # 对比第一条图卷积层的邻接矩阵差异 A_origin = model_origin.st_gcn[0].gcn.A A_added = model_added.st_gcn[0].gcn.A diff = (A_added - A_origin).abs().sum(dim=0) print(diff)这段代码加载了两个权重,对比它们在第一个图卷积层上的邻接矩阵差异。如果AddEdgeSTGCN12345.pt确实是在原模型基础上加边重训的,diff 矩阵里会有一批非零位置,那些位置就是新增边或者权重调整过的边。项目里那个DrawLine.py就是干这个可视化的——把差异矩阵的边结构画成一张图,比直接看数字直观得多。
如果你想把加边实验做到自己项目里,步骤一般是这样:先确定加边的成对关节(比如躯干中心到四肢末端),构建一个V×V的 0/1 掩码矩阵,加在原邻接矩阵上,然后对 A 做重新归一化。注意加了新边之后,节点的度和归一化系数都会变,必须重算 D 矩阵。重训时建议先在原模型权重上 fine-tune 而不是从零训练,收敛速度和精度都会好很多。
从那以后我每次拿到公开模型,第一件事就是打印state_dict的键名和输入维度,确认骨架定义跟我的输入对得上,再开始跑 demo。这个习惯帮我躲过了好几次"模型默默输出乱猜结果"的翻车事故,希望帮到你。
本文还有配套的精品资源,点击获取