简介基于时空图卷积的骨骼动作识别毕业设计项目面向需要完成毕业设计、课程设计或期末大作业的学生。项目提供完整可运行的源代码和配套文档模型核心采用时空图卷积网络ST-GCN及双流结构从人体骨骼关键点数据出发完成动作类别识别。代码按数据处理、模型构建、训练评测、离线演示与实时演示等模块组织并附有详细注释即使基础薄弱的新手也能较快理解图卷积网络在动作识别中的实际应用流程。压缩包共90个文件包含29个Python脚本、13个YAML配置文件、多个模型权重文件、演示GIF与视频、说明文档等整体大小约52.61MB。压缩包内目录结构清晰主要模块包括数据读取、网络结构、训练器和演示工具各类型文件分工明确脚本负责数据加载、训练与推理配置用于调整网络结构和超参数权重文件可直接加载使用GIF和MP4便于直观查看识别效果。已有354人学习下载适合作为毕业设计模板快速部署后可在其基础上进行改进扩展。1. ST-GCN骨骼动作识别为什么值得作为Python毕业设计当摄像头捕获到一个人的骨架坐标序列如何判断他是在走路、挥手还是摔倒这个看似简单的问题传统做法是把关节坐标拼成向量丢给LSTM或全连接网络但空间结构信息被扁平化后丢失严重。基于时空图卷积ST-GCN的骨骼动作识别把人体的25个关节建模成一张图用图卷积提取空间特征、用时间卷积捕捉动作时序在NTU RGBD这类标准数据集上准确率能稳定超过LSTM方案且PyTorch实现源码不过几百行。这个方向特别适合做Python毕业设计数据集公开、模型结构清晰、可视化效果好、调参空间大从数据处理到训练评估再到论文产出是一条完整的闭环路径。下文从原理、数据、源码、训练到避坑按实际落地顺序展开。2. 骨骼动作识别的建模方式从坐标序列到时空图卷积2.1 骨骼数据与图像数据的本质差异为什么CNN不直接适用骨骼动作识别的输入是一组关节坐标通常每帧包含25个关节点头部、肩膀、肘部、手腕、髋部、膝盖、脚踝等每个点有x、y、z三个坐标值有时还带置信度。一段动作视频就是T帧的坐标序列形状为[T, 25, 3]。拿这个张量直接塞进二维卷积网络问题在于卷积核天然假设数据排列在规则网格上而人体关节是图结构——手腕和肩膀之间没有固定的“像素距离”髋部与脚踝的语义关联比髋部与手指更紧密这种非欧几里得结构是CNN的卷积核无法建模的。把骨骼数据拍扁成向量再送入全连接网络也不理想。25个关节的坐标排列顺序一旦发生变化模型预测结果就完全改变而同一动作在屏幕上左右翻转后关节排列随之变化模型应当识别为同一动作。图结构能天然解决这个问题节点是关节边是骨骼连接图的拓扑结构不随视角变化。ST-GCN就是在这类需求下被提出的它的核心图卷积层只做两件事按邻接矩阵聚合邻居节点的特征再对聚合结果做线性变换。2.2 图的构建方式节点、边、邻接矩阵与自连接用人体的自然骨骼连接定义图结构25个关节就是25个节点骨骼是边。ST-GCN的论文中定义了三种子图直接连接、一阶邻居连接和二阶邻居连接。实际工程里多数复现只用一阶邻居加自连接。邻接矩阵A是一个25×25的方阵A[i][j]1表示节点i与节点j直接相连对角线上的A[i][i]也设为1把每个关节自身的特征保留下来。构建A的代码通常是从数据集自带的骨骼连接定义文件读取比如NTU RGBD的25点骨架每一条边是一对关节编号遍历边列表填入矩阵即可。得到A之后需要做归一化因为不同关节的邻居数量差异很大——髋部连接躯干和下肢邻居多左右手末端关节邻居少。如果不归一化聚合后的特征尺度会被关节度数带偏。常用的是对称归一化D是度矩阵对角线上是每个节点的邻居数最终参与计算的矩阵是D^{-1/2}·A·D^{-1/2}。这个形式与图卷积文献中的标准归一化一致。在PyTorch里实现时预先把归一化后的矩阵算好存成常量张量训练过程中不再修改。2.3 ST-GCN的核心思想空间图卷积加时间卷积ST-GCN的“时空”二字拆开看就是两个模块的串联空间图卷积提取单帧内的关节关联特征时间卷积提取帧与帧之间的运动特征。空间图卷积的输入是形状为[N, C, T, V]的特征图其中N是batch大小C是通道数T是时间帧数V是关节数。图卷积在V维度上做聚合时间卷积在T维度上做普通的一维卷积两者交替堆叠最后经过全局平均池化输出动作类别。为什么时间卷积用普通的Conv1d就够了因为同一关节在不同帧之间的对应关系是明确的——第t帧的左手腕必然对应第t1帧的左手腕这种对应是规则的序列关系不需要图结构来描述。空间和时间两个维度的处理方式完全不同这也解释了为什么ST-GCN的命名里有“时空”而不是只叫GCN。理解了这两个维度各自的处理逻辑后面读源码时就能分清每个张量维度上的操作含义。我自己刚开始复现时把图卷积和时间卷积混在一起看反复看shape才理清楚建议你重点盯住特征图的四个维度batch、通道、时间、关节每次操作都问一句“这一步在哪个维度上干活”。3. 数据处理与图构建把骨骼坐标变成模型能吃的张量3.1 数据集字段解析NTU RGBD骨架文件的读入与清洗做ST-GCN毕业设计最常用的数据集是NTU RGBD也叫NTU-60包含60类动作由微软Kinect v2采集提供3D关节坐标。每个样本是一个.btv文件或转换后的txt/csv里面按帧存储25个关节的x、y、z坐标和置信度。原始文件格式有些乱不同版本的数据集字段顺序不一样我一般会先写一个解析脚本把数据统一成[N, C, T, V]的标准张量。以下代码演示如何读取一份预处理过的CSV骨架数据import numpy as np import pandas as pd def load_skeleton_csv(csv_path, num_joints25, num_channels3): 读取单段动作的骨骼数据CSV 每行是一帧前75列依次是25个关节的x,y,z最后一列是动作类别标签 返回(T, C, V) 张量和标签 df pd.read_csv(csv_path, headerNone) label int(df.iloc[0, -1]) # 本文件末尾保存动作类别 frame_data df.iloc[:, :num_joints * num_channels].values T frame_data.shape[0] skeleton np.zeros((T, num_channels, num_joints)) for v in range(num_joints): skeleton[:, 0, v] frame_data[:, v * 3] # x坐标 skeleton[:, 1, v] frame_data[:, v * 3 1] # y坐标 skeleton[:, 2, v] frame_data[:, v * 3 2] # z坐标 return skeleton.astype(np.float32), label # 使用示例读取第一个样本检查shape和坐标范围 skeleton, label load_skeleton_csv(S001C001P001R001A001.csv) print(skeleton.shape) # 期望输出如 (46, 3, 25) print(x坐标范围:, skeleton[:, 0, :].min(), ~, skeleton[:, 0, :].max())逻辑说明原始CSV每行对应一帧前75列是25个关节点各3个坐标按顺序排列。这里用循环把列索引映射到关节点编号上输出形状是(T, C, V)。参数num_joints和num_channels必须与数据集定义一致NTU用25个关节其他数据集如Kinetics骨架版可能只有18个关节务必先确认。3.2 邻接矩阵与归一化从物理连接到计算图数据读入后下一步是构建邻接矩阵。NTU数据集的骨骼连接关系是固定的官方文档里有一张关节编号图。下面这段代码根据指定的连接边列表生成归一化邻接矩阵并在模型初始化阶段加载def build_normalized_adjacency(edges, num_joints25): edges: list of (u, v) 表示关节u与关节v之间的骨骼连接 返回归一化后的邻接张量用于图卷积层的矩阵乘法 A np.zeros((num_joints, num_joints), dtypenp.float32) for u, v in edges: A[u, v] 1.0 A[v, u] 1.0 A A np.eye(num_joints) # 自连接保留节点自身特征 # 计算度矩阵并做 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.0 D_mat_inv_sqrt np.diag(D_inv_sqrt) A_norm D_mat_inv_sqrt A D_mat_inv_sqrt return A_norm.astype(np.float32)逻辑说明edges列表直接从NTU的关节定义抄过来例如(1,2)代表躯干到右肩的连接。构建A后先加单位矩阵再按公式做对称归一化防止度数高的关节点特征被稀释。参数num_joints要跟数据集对齐如果你用的是自定义数据集比如OpenPose提取的18关节骨架edges列表也要相应重写。生成好的A_norm在训练程序中只计算一次作为buffer存入模型。3.3 数据增强与预处理随机旋转、时序裁剪与坐标标准化骨骼数据增强与图像不同不能在像素上做随机裁剪但可以在三维空间做旋转和缩放。对同一个动作人体朝向不同模型应当同样识别。常见的增强手段包括围绕竖直轴随机旋转、整体坐标平移、随机缩放、时间维度随机裁剪。以下代码演示了训练阶段最常用的三种增强def augment_skeleton(skeleton, rotation_range30, scale_range0.1, time_crop_ratio0.9): skeleton: (T, C, V) 原始骨骼序列 随机水平旋转、缩放、时间裁剪保持动作语义不变 T, C, V skeleton.shape # 1. 绕z轴(竖直方向)随机旋转 angle np.random.uniform(-rotation_range, rotation_range) rad np.deg2rad(angle) rot_mat np.array([ [np.cos(rad), -np.sin(rad), 0], [np.sin(rad), np.cos(rad), 0], [0, 0, 1] ]) rotated np.tensordot(rot_mat, skeleton, axes(1, 1)) # 对每个关节坐标做旋转 # 2. 随机缩放 scale np.random.uniform(1 - scale_range, 1 scale_range) rotated rotated * scale # 3. 时间维度随机裁剪模拟动作起始位置偏移 crop_len int(T * time_crop_ratio) start np.random.randint(0, T - crop_len 1) result rotated[start:start crop_len] # 坐标标准化以髋部中心为原点控制数值范围 hip_center result[:, :, 1] # 假设第1号关节是髋部中心 result result - hip_center.mean(axis0, keepdimsTrue) return result.astype(np.float32)逻辑说明旋转矩阵作用在C维度上使用tensordot把3×3旋转矩阵乘到每个关节坐标上。时间裁剪的crop_len取原序列的90%保证动作主体不丢失。坐标标准化以髋部中心为原点消除人物在画面中的绝对位置影响。参数rotation_range、scale_range和time_crop_ratio是三个最敏感的超参数我测试时发现rotation_range设到30度以内训练稳定超过45度后准确率反而下降因为人体关节翻转后自遮挡导致数据分布偏离真实场景。4. Python源码实现从零搭建ST-GCN训练框架4.1 图卷积层的PyTorch实现两行矩阵运算完成空间特征聚合图卷积层的数学表达是对特征图在关节维度上做邻接矩阵乘法再接一个1×1卷积。用PyTorch实现时本质上就是一次矩阵乘法和一次卷积的串联import torch import torch.nn as nn class SpatialGraphConv(nn.Module): 空间图卷积在关节维度上聚合邻居特征 def __init__(self, in_channels, out_channels, adj_matrix): super().__init__() self.adj nn.Parameter(adj_matrix, requires_gradFalse) # (V, V) 归一化邻接矩阵 self.conv nn.Conv2d(in_channels, out_channels, kernel_size1) def forward(self, x): # x: (N, C, T, V) N, C, T, V x.shape x_flat x.permute(0, 2, 3, 1) # (N, T, V, C) # 邻接矩阵乘法每个节点的特征替换为邻居特征的加权和 x_agg torch.einsum(ntvc,vu-ntuc, x_flat, self.adj) x_agg x_agg.permute(0, 3, 1, 2) # 回到 (N, C, T, V) return self.conv(x_agg)逻辑说明permute把通道维度调到最后方便用einsum做矩阵乘法。adj是25×25的归一化邻接矩阵requires_gradFalse表示它在训练中不更新。einsum表达式ntvc,vu-ntuc的含义是对每个样本n、每帧t、每个源节点v把它的特征乘上邻接矩阵第v行第u列的值累加到目标节点u上。conv层的kernel_size1等价于对聚合后的特征做线性变换把通道数从in_channels映射到out_channels。这里的参数核心是adj矩阵必须是归一化形式直接使用原始0/1邻接矩阵会让靠近躯干的节点特征数值远大于末端节点训练容易震荡。4.2 时间卷积模块残差连接与多尺度卷积核空间图卷积只处理单帧内的节点关系跨帧运动信息要靠时间卷积提取。时间卷积是一个标准的一维卷积作用在特征图的T维度上。实现时一般搭配残差连接帮助梯度流动class TemporalConv(nn.Module): 时间卷积沿时间维度提取动作变化特征 def __init__(self, in_channels, out_channels, kernel_size9, stride1): super().__init__() pad kernel_size // 2 self.conv nn.Conv2d(in_channels, out_channels, kernel_size(kernel_size, 1), padding(pad, 0), stride(stride, 1)) self.bn nn.BatchNorm2d(out_channels) self.relu nn.ReLU(inplaceTrue) self.residual None if in_channels ! out_channels or stride ! 1: self.residual nn.Sequential( nn.Conv2d(in_channels, out_channels, kernel_size1), nn.BatchNorm2d(out_channels)) def forward(self, x): out self.relu(self.bn(self.conv(x))) if self.residual is not None: x self.residual(x) return out x逻辑说明kernel_size设为9一般效果较好对应约9帧的动作上下文padding4保证输出时间长度不变。残差分支在通道数改变或时间下采样时做1×1卷积对齐否则直接相加。参数上kernel_size是ST-GCN最值得调的超参数之一数据集动作越快需要的时间上下文越短NTU上9帧效果好短促动作数据集上5帧更合适。这里用Conv2d模拟Conv1d把卷积核写为(kernel_size, 1)本质是在T维度卷积、在V维度上卷积核为1不动。4.3 整体网络结构与训练脚本参数配置、学习率与可视化将空间图卷积和时间卷积交替堆叠组成完整的ST-GCN网络class STGCN(nn.Module): def __init__(self, in_channels3, num_joints25, num_classes60, adj_matrixNone): super().__init__() self.adj adj_matrix self.block1 self._build_block(in_channels, 64, adj_matrix) self.block2 self._build_block(64, 128, adj_matrix) self.block3 self._build_block(128, 256, adj_matrix) self.pool nn.AdaptiveAvgPool2d((1, 1)) self.fc nn.Linear(256, num_classes) def _build_block(self, in_ch, out_ch, adj_matrix): return nn.Sequential( SpatialGraphConv(in_ch, out_ch, adj_matrix), TemporalConv(out_ch, out_ch), SpatialGraphConv(out_ch, out_ch, adj_matrix), TemporalConv(out_ch, out_ch, stride2) # 时间维度减半扩大感受野 ) def forward(self, x): # x: (N, C, T, V) x self.block1(x) x self.block2(x) x self.block3(x) x self.pool(x) # 全局池化输出 (N, 256, 1, 1) x x.view(x.size(0), -1) return self.fc(x)网络三个block的通道数从64翻倍到256时间维度逐步降采样。AdaptiveAvgPool2d把(T, V)两个维度都压成1最终得到每个样本256维特征向量接全连接层输出类别概率。训练脚本的核心参数包括学习率、权重衰减和batch size初始化学习率0.01每40个epoch乘以0.1权重衰减1e-4防过拟合batch size在3090显卡上可以设64显存不足时降到32。优化器用SGD配合Nesterov动量动量为0.9比Adam在ST-GCN上收敛更稳。训练时打印验证集loss连续5个epoch不下降就降低学习率。4.4 文档说明的组织方式让毕业设计源码可复现毕业设计答辩时评委打开项目文件先看README。一份合格的源码文档至少包含四块环境依赖Python版本、PyTorch版本、CUDA版本、数据准备说明原始数据下载地址、预处理脚本的输入输出、训练命令一行可复现的命令、参数配置表学习率、batch size、增强参数、各层通道数。如果数据集的预处理脚本和模型训练脚本混在同一个目录还互相覆盖答辩现场很被动。我习惯把项目分成data_processing、models、utils、runs四个目录每个目录配一个极简README说明这个目录里的脚本在完整流程中处于哪个环节。另外在train.py开头放一段argparse参数列表并打印全部配置训练日志里保留每次运行的参数快照这种“代码可复现、结果可追溯”的组织方式比多写五千字原理更能打动评委。5. 常见问题与踩坑记录训练不收敛、显存溢出与数据泄漏5.1 训练损失下降但验证集准确率不增长现象训练集准确率很快冲到95%以上验证集停留在45%上下波动损失在两者之间差距越拉越大。原因最直接的怀疑对象是过拟合。ST-GCN的参数总量不小三组block加全连接层约300万参数NTU的每类样本只有几千个模型完全能记住训练样本。另一个常见原因是数据泄漏预处理时把测试集的统计量算进了训练集的标准化里或者数据增强在验证集上也做了随机变换导致验证集分布被污染。解决先检查验证流程是否严格关闭了增强模式。再在模型里加Dropout全连接层前dropout比率设0.5。数据侧增加增强强度rotation_range从15度调大到30度time_crop_ratio从0.9降到0.8。如果还是不行降低模型容量把三个block的通道数从64/128/256改为32/64/128观察准确率是否回升。我实际调试时上面三步里先做第二步就见效了建议按顺序排查。5.2 PyTorch显存溢出batch size与序列长度的权衡现象训练到第一个batch直接报RuntimeError: CUDA out of memory或运行几个epoch后显存逐渐被占满。原因ST-GCN的中间特征图形状是(N, 256, T/4, 25)T等于原始帧数。如果原始动作序列是300帧单样本中间特征就有约500万浮点数乘以batch size 64显存占用轻松超过12GB。另一个隐蔽原因是时间卷积的kernel_size9padding后虽然输出形状不变但计算图中的中间变量比输出大得多。解决把batch size从64降到16先跑通流程确认代码没问题再逐步提升。如果必须用大batch在时间卷积里改用零填充stride减半的降采样结构。还有一个实用技巧训练时用torch.cuda.amp自动混合精度显存占用直接减半准确率通常不掉。把骨骼数据的dtype从float64改成float32也能省一半显存精度损失可以忽略。5.3 图卷积结果不如普通CNN邻接矩阵与初始化的坑现象你把邻接矩阵直接设为0/1矩阵训练后准确率还不如把骨骼坐标拉平丢给多层感知机。原因0/1邻接矩阵没有经过归一化图卷积的聚合操作相当于对邻居特征求和而不是加权平均。靠近躯干的关节髋部、胸部邻居多聚合后的特征数值大训练时梯度被这些节点主导末端关节的特征被淹没。对称归一化D^{-1/2}·A·D^{-1/2}的作用正是让每个节点的聚合结果与度数无关。解决归一化时用float32精度因为中间要计算-0.5次方float16的精度会抹掉很多小数值。另外检查邻接矩阵是否加了对角线自连接不加自连接时每层都会削弱节点自身特征深层网络里最初的信号衰减殆尽。还有一个容易忽略的坑einsum中的邻接矩阵需要转置维度顺序必须与特征图的关节维度严格对齐方向反了模型也能收敛但准确率明显偏低。5.4 跨数据集泛化差视角变化与关节噪声现象在NTU上训练好的模型用在自拍的视频骨架数据上准确率掉到20%以下。原因NTU是Kinect固定视角采集动作类型和人物朝向相对规整。自拍视频用OpenPose提取骨架时存在关节抖动、遮挡跳变、坐标漂移问题。模型的训练数据太“干净”对噪声和视角变化不具备鲁棒性。解决训练时增加数据增强力度比如把rotation_range提升到45度模拟不同视角。更有效的做法是在训练样本中加入噪声对每个关节坐标加高斯噪声标准差设为关节范围长度的2%迫使模型学习更鲁棒的动作模式。如果你准备在自定义数据上验证建议先把骨架序列做平滑滤波再送入模型一维中值滤波窗口大小5就能滤掉大部分偶发抖动又不影响大尺度动作。6. 让ST-GCN模型更具说服力的三个进阶技巧6.1 可视化骨架序列上的卷积注意力热图答辩时最好用的素材是把模型关注的关节可视化。用Grad-CAM思路取最后一个block的输出特征图对动作类别求梯度结合梯度加权得到每个关节的重要性分数画在骨架序列上红色代表高贡献蓝色代表低贡献。你立刻就能看到模型在“挥手”动作上关注手腕和肘部而不是躯干——这比任何准确率数字都有说服力。可视化还能反向帮你发现数据问题如果“跑步”动作上模型主要关注头部大概率训练数据里人物面部区域有异常噪声。6.2 消融实验证明ST-GCN各模块的贡献毕业设计论文里最核心的评估环节是消融实验。设计三组对比完整ST-GCN、去掉时间卷积只用空间图卷积、去掉图卷积把关节特征拉平送入普通卷积。对比结果能清晰证明两个模块各自的作用。推荐再加一组把邻接矩阵从骨骼自然连接改为全连接图如果后者效果下降说明人体结构先验确实有效。消融实验要控制变量除了改动目标模块外保证学习率、batch size、训练epoch完全相同否则对比不严谨。6.3 导出与部署从PyTorch模型到WebDemo如果希望演示环节更出彩用torch.onnx.export把模型导出为ONNX格式再用Flask封装一个识别接口前端通过摄像头采集人体姿态后端返回动作类别。ONNX导出的关键操作是固定输入序列长度一般取60帧不足60帧重复最后一帧填充超过60帧均匀采样截断。部署时不需要GPUCPU推理单帧耗时约30毫秒完全够用。把骨架数据传入模型前记得做与训练时完全相同的预处理坐标标准化、以髋部中心为原点、关节编号对齐。我踩过最深的坑是训练时用增强后的数据、部署时忘了关增强开关导致每次预测结果都带随机抖动排查了一整天才发现。做ST-GCN毕业设计最大的教训是不要迷信跑通官方代码而是从数据处理开始一行行自己写踩过一批坑之后你对模型的理解会完全不同。坚持把每个模块的输入输出shape写注释坚持每次实验记录超参数快照这些习惯会让你少熬夜调参希望帮到你。本文还有配套的精品资源点击获取