资讯详情

基于LSTM与MediaPipe的人体动作姿态识别实战:从骨架提取到模型训练

📅 2026/10/13 20:05:18 | 华诺云谱 👁 阅读
基于LSTM与MediaPipe的人体动作姿态识别实战:从骨架提取到模型训练
简介本资源是一套基于LSTM与MediaPipe实现人体动作姿态识别的Python项目源码面向计算机、人工智能、通信、物联网等专业的在校学生与教师可用于毕业设计、课程设计、大作业或初期项目立项演示。项目通过MediaPipe提取视频流中人体关键姿态信息再交由LSTM网络完成动作分类识别可应用于教育、运动分析、康复训练等场景。压缩包共280个文件约22.09MB包含16个py源码文件、240个npy姿态数据、16个mp4演示视频、2个h5训练模型及说明文档等代码完整且功能验证通过解压后建议重命名为英文路径再运行。目前已有186人学习关注。读者可借此掌握姿态关键点提取、时序特征建模、模型训练与推理的完整流程并在此基础上二次开发DIY其他动作识别功能。1. 姿态识别这条路的真实门槛在哪里很多人第一次接触人体动作姿态识别脑子里想的是摄像头一开模型就知道我在干嘛。真动手才发现卡住你的从来不是模型本身而是前面那一堆坐标。MediaPipe 把人体骨架点吐出来之后怎么把这些散点变成 LSTM 能吃的序列才是这个方向真正的分水岭。基于 LSTM 和 MediaPipe 实现人体动作姿态识别本质上是两段式流水线前段用 MediaPipe 做轻量级关键点提取后段用 LSTM 对关键点时间序列做分类。它适合做课程设计、毕业设计这类需要完整闭环、又不想在算力上烧钱的场景也适合已经会调 Python 但没做过时序分类的开发者拿来练手。这篇笔记按骨架怎么取、序列怎么造、模型怎么训、坑怎么躲的顺序讲透你照着能跑出一个可演示的动作识别 Demo。2. 用 MediaPipe 把视频拆成骨架坐标序列2.1 为什么选 MediaPipe 而不是自己训检测器做姿态识别第一步永远是人现在在哪、关节在哪。传统路线是先用目标检测框出人再跑姿态估计网络两步串起来延迟高、依赖多、模型文件大。MediaPipe 的 Pose 方案把检测和关键点回归压进一条流水线CPU 上就能跑到实时单帧输出 33 个关键点每个点带 x、y、z 和可见度。对毕业设计这种要在普通笔记本上演示的场景这个取舍非常划算。常见做法是直接用mediapipe.solutions.pose它内部已经处理了多人场景下的主体选择。我一般会先把static_image_mode设成 False让它走视频流模式这样帧间会做跟踪抖动明显小。另一个关键参数是model_complexity0 是轻量、1 是均衡、2 是精度优先。做动作识别0 和 1 通常够用2 在低端机器上会拖慢帧率反而让序列采样不均匀。2.2 提取关键点的最小可跑脚本下面这段代码把一段视频逐帧读进来提取 33 个关键点的归一化坐标存成一个二维数组。注意我做了两件事一是只保留 x、y、z 三个通道丢掉可见度二是把每一帧的坐标拉平成一维向量方便后面拼序列。import cv2 import mediapipe as mp import numpy as np mp_pose mp.solutions.pose def extract_keypoints(video_path, model_complexity1): cap cv2.VideoCapture(video_path) # 视频流模式帧间跟踪减少抖动 pose mp_pose.Pose( static_image_modeFalse, model_complexitymodel_complexity, smooth_landmarksTrue, min_detection_confidence0.5, min_tracking_confidence0.5 ) frames [] while cap.isOpened(): ret, frame cap.read() if not ret: break # MediaPipe 要 RGBOpenCV 默认 BGR rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) result pose.process(rgb) if result.pose_landmarks: # 33 个点 × 3 通道 99 维 kp np.array([[lm.x, lm.y, lm.z] for lm in result.pose_landmarks.landmark]) frames.append(kp.flatten()) else: # 没检测到人时补零保持时间轴对齐 frames.append(np.zeros(99)) cap.release() pose.close() return np.array(frames) # shape: (T, 99)逻辑说明smooth_landmarksTrue会让相邻帧的关键点做平滑代价是快速动作时会有轻微拖影做挥手跳跃这类动作时如果发现识别迟钝可以关掉它对比。min_detection_confidence和min_tracking_confidence都设 0.5 是保守值光线差的环境可以降到 0.3但会引入更多误检。补零那一步很关键如果直接跳过没检测到的帧序列长度会变后面切窗口时时间轴就乱了。参数说明model_complexity建议先用 1 跑通确认流程没问题再考虑升到 2。static_image_modeFalse是视频场景的默认选择如果你要做的是单张图片分类才设 True。2.3 坐标归一化与坐标系选择MediaPipe 输出的 x、y 是相对图像宽高的归一化值范围 0 到 1z 是相对髋部深度的近似值量纲和 x、y 不一致。直接把这 99 维丢给 LSTMz 通道的尺度问题会让模型偏向平面动作。我一般会做两步处理先把 x、y 按图像宽高比还原成近似等比例再对整段序列做一次以髋部中心为原点的平移让动作特征和人在画面里的位置解耦。def normalize_sequence(seq, img_w640, img_h480): seq seq.reshape(-1, 33, 3) # x、y 还原到像素尺度z 保持相对值 seq[:, :, 0] * img_w seq[:, :, 1] * img_h # 以左右髋中点(23,24)为原点平移 hip (seq[:, 23, :] seq[:, 24, :]) / 2 seq seq - hip[:, None, :] # 按肩宽缩放消除远近差异 shoulder np.linalg.norm(seq[:, 11, :2] - seq[:, 12, :2], axis1) scale np.where(shoulder 1e-3, shoulder, 1.0) seq seq / scale[:, None, None] return seq.reshape(-1, 99)这段处理做完同一个人在不同位置、不同距离做同一个动作序列形态会接近很多。肩宽缩放那一步是血泪经验不做的话人离摄像头近时关键点间距大模型会误以为动作幅度大分类边界直接糊掉。3. 把骨架序列喂给 LSTM窗口切分与模型搭建3.1 滑窗切分序列长度和步长怎么定LSTM 吃的是定长序列但一段视频的帧数是不定的。标准做法是滑窗切分把长序列切成固定长度的窗口每个窗口带一个动作标签。窗口长度直接决定模型能看到多长的动作上下文。太短像下蹲这种有起承转合的动作会被切碎太长一个窗口里混进两个动作标签就不纯了。我一般按帧率反推假设视频 30fps一个完整动作持续 1.5 到 2 秒那窗口长度取 45 到 60 帧比较稳。步长取窗口的一半做数据增强同时避免相邻窗口太相似导致过拟合。def make_windows(seq, label, win48, step24): X, y [], [] for start in range(0, len(seq) - win 1, step): X.append(seq[start:start win]) y.append(label) return np.array(X), np.array(y)参数说明win48对应 30fps 下约 1.6 秒step24是 50% 重叠。如果你的动作普遍偏快比如拳击出拳win 可以降到 30偏慢的太极类动作win 提到 90 也不过分。判断标准是随机抽几个窗口可视化一下看窗口内是否包含一个语义完整的动作。3.2 LSTM 模型结构层数、隐藏单元和正则骨架序列维度不高99 维动作类别通常也就 5 到 10 类模型不需要堆很深。两层 LSTM 加一层全连接就够隐藏单元 64 到 128 之间。层数再多在几千条样本的量级上几乎必然过拟合。import torch import torch.nn as nn class PoseLSTM(nn.Module): def __init__(self, input_dim99, hidden128, num_layers2, num_classes6): super().__init__() self.lstm nn.LSTM( input_dim, hidden, num_layers, batch_firstTrue, dropout0.3, # 层间 dropoutnum_layers1 才生效 bidirectionalFalse ) self.fc nn.Sequential( nn.Linear(hidden, 64), nn.ReLU(), nn.Dropout(0.3), nn.Linear(64, num_classes) ) def forward(self, x): # x: (B, T, 99) out, (h, c) self.lstm(x) # 取最后一个时间步的隐藏状态 return self.fc(out[:, -1, :])逻辑说明batch_firstTrue让输入维度是 (batch, time, feature)符合直觉。取out[:, -1, :]是单向 LSTM 的标准做法用最后一步的隐状态代表整个窗口。如果你发现动作的判别信息集中在窗口中间可以改成对时间维做平均池化两种都试试。参数说明dropout0.3是 LSTM 层间的num_layers1时这个参数无效PyTorch 会警告。全连接前的 dropout 是独立的一层别混淆。bidirectional在离线分类里可以开能涨一两个点但推理时要等整个窗口读完实时场景慎用。3.3 训练循环里必须盯的三个量训练脚本本身不复杂但有几个量不看会翻车。第一是训练损失和验证损失的分离点一旦验证损失开始上升而训练损失还在降就是过拟合信号该早停或加正则了。第二是每个 epoch 的学习率用ReduceLROnPlateau在验证损失停滞时降学习率比固定学习率稳得多。第三是混淆矩阵准确率看着高但某两类互相混说明特征维度不够区分这两个动作得回去看关键点选得对不对。from torch.optim import Adam from torch.optim.lr_scheduler import ReduceLROnPlateau model PoseLSTM() opt Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler ReduceLROnPlateau(opt, modemin, factor0.5, patience5) criterion nn.CrossEntropyLoss() for epoch in range(80): model.train() for xb, yb in train_loader: opt.zero_grad() loss criterion(model(xb), yb) loss.backward() # 梯度裁剪LSTM 容易梯度爆炸 nn.utils.clip_grad_norm_(model.parameters(), 1.0) opt.step() # 验证阶段省略拿到 val_loss 后 scheduler.step(val_loss)weight_decay1e-4是轻量 L2 正则配合 dropout 一起用。clip_grad_norm_那行别省LSTM 在序列稍长时梯度爆炸是常事不裁剪的话 loss 会突然变 NaN排查起来很费时间。4. 数据采集与标注决定上限的一步4.1 每个动作要录多少条、怎么录模型结构再漂亮数据不够也是白搭。我的经验是每个动作至少 30 到 50 条独立样本每条持续 2 到 3 秒。这里的独立很重要同一个人连续做 50 次同一个动作样本间高度相关模型学到的是这个人的体型和习惯换个人就崩。正确做法是找 3 到 5 个人每人每个动作录 10 条左右覆盖不同身高、不同朝向、不同距离。录制时还要刻意制造变化正面、侧面各录一部分光线有明有暗背景有干净有杂乱。这些变化在训练时是噪声在测试时就是鲁棒性的来源。我见过太多 Demo 在实验室灯光下准确率 95%搬到走廊就掉到 60%问题全出在数据多样性上。4.2 标注格式与标签一致性标注本身简单一个视频文件对应一个动作标签就行。坑在于标签一致性同一个人录的挥手如果前 10 条是左右挥后 10 条变成上下挥模型会困惑。建议先定一份动作定义文档写清楚每个动作的起止姿态和运动轨迹录之前所有人对着文档做一遍校准。存储上我一般把提取好的关键点序列存成.npy文件名带标签和序号比如wave_p1_01.npy。这样数据集构建脚本可以按文件名解析标签不用额外维护标注文件。视频原文件保留一份方便出问题时回溯。import os import numpy as np def build_dataset(root): X, y [], [] label_map {wave: 0, squat: 1, jump: 2, turn: 3, clap: 4, idle: 5} for fname in os.listdir(root): if not fname.endswith(.npy): continue label_name fname.split(_)[0] seq np.load(os.path.join(root, fname)) seq normalize_sequence(seq) xw, yw make_windows(seq, label_map[label_name]) X.append(xw) y.append(yw) return np.concatenate(X), np.concatenate(y)逻辑说明标签从文件名前缀解析label_map集中管理类别顺序避免训练和推理时类别索引对不上。normalize_sequence在切窗之前调用保证归一化用的是整段序列的统计量而不是单个窗口的这样窗口之间尺度一致。5. 避坑与排查那些让准确率莫名掉点的细节5.1 现象训练准确率很高实时演示时动作识别延迟明显原因滑窗推理时每个窗口都要等 48 帧攒满才出一次结果30fps 下就是 1.6 秒延迟。加上 LSTM 逐帧计算实际响应更慢。解决实时场景改用滑动缓冲队列每来一帧就推进一次用最近 win 帧做推理但推理频率降到每 5 到 10 帧一次。或者把窗口长度砍到 24 帧牺牲一点上下文换响应速度。演示时提前告诉观众动作做完稍等一秒体验会好很多。5.2 现象换个人测试准确率断崖式下跌原因训练集里只有一两个人的数据模型记住了特定体型的关键点分布没有学到动作本身的运动模式。解决采集阶段强制多人参与训练时按人划分训练集和验证集而不是随机划分窗口。随机划分会让同一个人的窗口同时出现在训练和验证里验证准确率虚高掩盖了泛化问题。5.3 现象某些动作类别总是互相混淆原因两个动作在关键点空间里轨迹接近比如挥手和举手在 33 点骨架里差异很小LSTM 抓不到区分特征。解决先做特征可视化把两个动作的序列降维画出来看是否重叠。如果重叠考虑加特征关键点速度、关节角度、肢体相对位置。单纯堆 LSTM 层数解决不了特征本身不可分的问题。5.4 现象loss 训练几个 epoch 后变成 NaN原因LSTM 梯度爆炸常见于序列较长、学习率偏大、没有梯度裁剪的情况。解决加clip_grad_norm_学习率从 1e-3 降到 5e-4检查输入里有没有异常大的值。归一化没做好时某些帧的关键点坐标可能超出正常范围喂进去就是灾难。5.5 现象MediaPipe 在某些帧检测不到人序列出现空洞原因遮挡、快速运动模糊、光线突变都会让检测失败。解决短时空洞用前后帧线性插值补上长时空洞超过 10 帧直接丢弃该段。补零是下策零向量在特征空间里是个特殊点模型会把它当成一种动作来学反而干扰分类。6. 从 Demo 到可用提升泛化的一点实战技巧模型跑通之后真正决定这个方案值不值得继续投入的是它在训练分布之外的表现。我踩过最深的坑是实验室里六类动作准确率 94%拿到走廊里录了一段直接掉到 58%。回去查混淆矩阵发现转身和挥手混得一塌糊涂因为走廊里人离得远肩宽缩放后关键点噪声被放大了。后来我固定了一套验证习惯每次改完模型或数据都跑一遍三个测试集同人同场景、同人换场景、换人换场景。三个数字的差距比单一准确率有信息量得多。如果同人同场景 95%、换人换场景 60%说明模型学的是人不是动作得回去补数据多样性如果三个都低才是模型结构或特征的问题。另一个实用技巧是测试时增强。推理时对同一个窗口做几次轻微扰动——时间轴随机裁掉头尾各两帧、关键点加小高斯噪声——把几次预测概率平均。这个操作不训练、不加参数通常能涨 2 到 4 个点代价是推理耗时翻几倍。演示场景下完全值得。还有一个容易被忽略的点类别不平衡。如果静止类样本远多于其他动作模型会倾向于全预测静止准确率看着不低但没用。我一般会在损失里加类别权重或者对少数类做窗口重叠采样。CrossEntropyLoss(weight...)传一个按类别频率倒数算出来的权重向量就行。最后说个工程习惯每次训练完把模型结构、超参数、数据集版本、三个测试集的准确率记到一个文本文件里和模型权重放一起。做到第五版的时候你会感谢自己当初记了这些。姿态识别这个方向调参的随机性不小没有记录就等于每次都在重新开始。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑