☰
基于LSTM与MediaPipe的人体动作姿态识别实战:从骨架提取到模型训练
2026/10/13 20:05:08 网站建设 项目流程

简介:本资源是一套基于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_complexity,0 是轻量、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_complexity=1): cap = cv2.VideoCapture(video_path) # 视频流模式,帧间跟踪,减少抖动 pose = mp_pose.Pose( static_image_mode=False, model_complexity=model_complexity, smooth_landmarks=True, min_detection_confidence=0.5, min_tracking_confidence=0.5 ) frames = [] while cap.isOpened(): ret, frame = cap.read() if not ret: break # MediaPipe 要 RGB,OpenCV 默认 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_landmarks=True会让相邻帧的关键点做平滑,代价是快速动作时会有轻微拖影,做"挥手""跳跃"这类动作时如果发现识别迟钝,可以关掉它对比。min_detection_confidence和min_tracking_confidence都设 0.5 是保守值,光线差的环境可以降到 0.3,但会引入更多误检。补零那一步很关键,如果直接跳过没检测到的帧,序列长度会变,后面切窗口时时间轴就乱了。

参数说明:model_complexity建议先用 1 跑通,确认流程没问题再考虑升到 2。static_image_mode=False是视频场景的默认选择,如果你要做的是单张图片分类,才设 True。

2.3 坐标归一化与坐标系选择

MediaPipe 输出的 x、y 是相对图像宽高的归一化值,范围 0 到 1,z 是相对髋部深度的近似值,量纲和 x、y 不一致。直接把这 99 维丢给 LSTM,z 通道的尺度问题会让模型偏向平面动作。我一般会做两步处理:先把 x、y 按图像宽高比还原成近似等比例,再对整段序列做一次以髋部中心为原点的平移,让动作特征和人在画面里的位置解耦。

def normalize_sequence(seq, img_w=640, img_h=480): 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], axis=1) 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, win=48, step=24): 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)

参数说明:win=48对应 30fps 下约 1.6 秒,step=24是 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_dim=99, hidden=128, num_layers=2, num_classes=6): super().__init__() self.lstm = nn.LSTM( input_dim, hidden, num_layers, batch_first=True, dropout=0.3, # 层间 dropout,num_layers>1 才生效 bidirectional=False ) 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_first=True让输入维度是 (batch, time, feature),符合直觉。取out[:, -1, :]是单向 LSTM 的标准做法,用最后一步的隐状态代表整个窗口。如果你发现动作的判别信息集中在窗口中间,可以改成对时间维做平均池化,两种都试试。

参数说明:dropout=0.3是 LSTM 层间的,num_layers=1时这个参数无效,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(), lr=1e-3, weight_decay=1e-4) scheduler = ReduceLROnPlateau(opt, mode='min', factor=0.5, patience=5) 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_decay=1e-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=...)传一个按类别频率倒数算出来的权重向量就行。

最后说个工程习惯:每次训练完,把模型结构、超参数、数据集版本、三个测试集的准确率记到一个文本文件里,和模型权重放一起。做到第五版的时候,你会感谢自己当初记了这些。姿态识别这个方向,调参的随机性不小,没有记录就等于每次都在重新开始。

希望帮到你。

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

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

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

立即咨询