ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

LSTM+MediaPipe姿态识别:从视频到动作标签的完整实战

LSTM+MediaPipe姿态识别:从视频到动作标签的完整实战 简介本资源为基于LSTM与MediaPipe实现人体动作姿态识别的Python项目源码面向计算机、人工智能、通信、物联网等专业的在校学生与教师可用于毕业设计、课程设计、大作业或初期项目立项演示。项目通过MediaPipe提取视频流中人体关键姿态信息再交由LSTM网络完成动作分类识别可应用于教育、运动分析、康复训练等场景。压缩包共280个文件约22.09MB包含16个py源码文件、240个npy数据文件、16个mp4示例视频、2个h5模型权重及说明文档等覆盖数据、模型与演示全流程。目前已有184人学习下载。代码完整且功能验证通过读者可据此理解姿态特征提取与序列建模的完整链路并在此基础上二次开发DIY其他动作识别功能。1. 从一段视频到动作标签LSTM MediaPipe 姿态识别到底在做什么你手里有一段手机拍的健身视频或者一段监控里工人搬货的片段想自动判断这个人在深蹲还是在弯腰搬东西。这件事的难点不在识别出人,而在识别出动作——单帧画面里人都是站着的只有把连续几十帧的骨骼点串起来看才能区分下蹲和坐下。这正是 LSTM 和 MediaPipe 组合要解决的问题MediaPipe 负责从每一帧里抠出人体关键点坐标LSTM 负责把这些坐标随时间变化的序列翻译成动作类别。这套方案适合谁做毕业设计的学生、想给健身 App 加动作计数功能的开发者、做工业安全行为检测的工程师。它不需要 GPU 集群一台普通笔记本就能跑推理训练数据自己拿手机录几十段就够起步。下面我把从环境搭建到模型训练、再到踩坑排查的完整路径讲清楚代码可以直接抄。2. 环境搭建与 MediaPipe 关键点提取把视频变成坐标序列2.1 为什么选 MediaPipe 而不是 OpenPose做姿态识别第一步是人体关键点检测。常见方案有 OpenPose、HRNet、MediaPipe。OpenPose 精度高但依赖重、推理慢一个 1080p 视频在 CPU 上跑要几分钟HRNet 精度最好但模型大部署麻烦。MediaPipe 的优势是轻量它把 BlazePose 模型做了工程优化CPU 上单帧推理能到 30fps 以上而且官方直接给了 Python 包pip install就能用。MediaPipe Pose 输出 33 个关键点每个点有 x、y、z 三个坐标加一个可见度 visibility。x、y 是归一化到 0~1 的图像坐标z 是相对深度以髋部为原点visibility 表示这个点被遮挡的概率。做动作识别时我一般只取上半身和下肢的核心点比如肩膀、肘、腕、髋、膝、踝共 12~15 个点每个点取 x、y、visibility 三个值一帧就是 36~45 维特征。这样既保留了动作信息又降低了 LSTM 的输入维度。2.2 安装依赖与验证 MediaPipe 是否跑通先建虚拟环境避免和系统里的包打架。Python 版本建议 3.8~3.10MediaPipe 对 3.11 的支持在部分版本上还不稳定。# 创建虚拟环境 python -m venv pose_env # Windows 激活 pose_env\Scripts\activate # Linux/Mac 激活 source pose_env/bin/activate # 安装核心依赖 pip install mediapipe0.10.9 opencv-python4.8.1.78 pip install numpy1.24.3 pandas2.0.3 pip install torch2.1.0 torchvision0.16.0装完后跑一段最小验证代码确认摄像头或视频文件能出关键点import cv2 import mediapipe as mp mp_pose mp.solutions.pose pose mp_pose.Pose( static_image_modeFalse, # 视频流用 False会做帧间跟踪 model_complexity1, # 0/1/2越大越准越慢1 是平衡点 smooth_landmarksTrue, # 平滑关键点减少抖动 min_detection_confidence0.5, min_tracking_confidence0.5 ) cap cv2.VideoCapture(test.mp4) while cap.isOpened(): ret, frame cap.read() if not ret: break rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) result pose.process(rgb) if result.pose_landmarks: # 取第 11 号点左肩验证 lm result.pose_landmarks.landmark[11] print(f左肩: x{lm.x:.3f}, y{lm.y:.3f}, vis{lm.visibility:.3f}) cap.release()这段代码里model_complexity是关键参数0 最快但精度低2 最准但 CPU 上可能掉到 10fps。做动作识别我一般用 1。smooth_landmarksTrue会让相邻帧的关键点做指数平滑能明显减少关键点乱跳导致的误判但代价是快速动作会有轻微延迟。如果你的动作很快比如拳击可以关掉它。2.3 批量提取视频特征并保存为训练数据单段验证通过后要把所有视频批量转成 CSV 或 npy 序列。核心思路是每段视频提取一个(帧数, 特征维度)的矩阵再按动作类别打标签。import os import numpy as np import cv2 import mediapipe as mp mp_pose mp.solutions.pose # 选取的核心关键点索引鼻、左右肩、左右肘、左右腕、左右髋、左右膝、左右踝 KEYPOINT_IDS [0, 11, 12, 13, 14, 15, 16, 23, 24, 25, 26, 27, 28] def extract_sequence(video_path, max_frames60): pose mp_pose.Pose(static_image_modeFalse, model_complexity1, smooth_landmarksTrue, min_detection_confidence0.5) cap cv2.VideoCapture(video_path) frames [] while cap.isOpened() and len(frames) max_frames: ret, frame cap.read() if not ret: break rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) res pose.process(rgb) if res.pose_landmarks: feat [] for idx in KEYPOINT_IDS: lm res.pose_landmarks.landmark[idx] feat.extend([lm.x, lm.y, lm.visibility]) frames.append(feat) cap.release() pose.close() # 帧数不足时用最后一帧补齐保证 LSTM 输入长度一致 while len(frames) max_frames: frames.append(frames[-1] if frames else [0.0] * (len(KEYPOINT_IDS) * 3)) return np.array(frames, dtypenp.float32) # 遍历数据集目录结构为 data/动作名/视频.mp4 X, y [], [] label_map {squat: 0, pushup: 1, wave: 2} for label_name, label_id in label_map.items(): folder os.path.join(data, label_name) for fname in os.listdir(folder): seq extract_sequence(os.path.join(folder, fname)) X.append(seq) y.append(label_id) X np.array(X) # shape: (样本数, 60, 39) y np.array(y) np.save(X.npy, X) np.save(y.npy, y) print(样本形状:, X.shape, 标签分布:, np.bincount(y))这里有几个参数要交代清楚。max_frames60是序列长度按 30fps 算就是 2 秒。太短抓不到完整动作太长 LSTM 训练慢且容易过拟合。我一般按动作周期定深蹲一个来回约 2 秒就取 60 帧。KEYPOINT_IDS选了 13 个点每点 3 维所以特征维度是 39。如果你只关心下肢动作可以砍掉上半身点降到 21 维训练更快。注意提取特征时一定要保证训练和推理用同一套KEYPOINT_IDS和同样的归一化方式否则模型上线后精度会断崖式下跌。这是最常见的翻车点之一。3. LSTM 模型设计与训练让坐标序列学会认动作3.1 为什么用 LSTM 而不是普通全连接或 CNN单帧关键点喂给全连接网络它只能学到手举过头顶这种静态姿态学不到手从下往上举这个过程。CNN 擅长处理空间结构但关键点序列的空间关系已经被 MediaPipe 编码成坐标了真正需要建模的是时间依赖。LSTM 的门控机制能记住几十帧前的状态正好适合这种动作由过程定义的任务。和 LSTM 常一起比较的是 GRU 和 Transformer。GRU 参数少、训练快在数据量小的时候和 LSTM 差不多Transformer 需要更多数据才能发挥优势几十段视频的规模下容易过拟合。所以毕业设计这个量级LSTM 是稳妥选择。如果你后面数据涨到几千段可以再试 Transformer。3.2 搭一个能跑通的 LSTM 分类网络import torch import torch.nn as nn from torch.utils.data import TensorDataset, DataLoader from sklearn.model_selection import train_test_split class PoseLSTM(nn.Module): def __init__(self, input_dim39, hidden_dim128, num_layers2, num_classes3): super().__init__() self.lstm nn.LSTM( input_sizeinput_dim, hidden_sizehidden_dim, num_layersnum_layers, batch_firstTrue, # 输入格式 (batch, seq, feature) dropout0.3, # 层间 dropout防过拟合 bidirectionalFalse # 动作识别单向足够双向会引入未来信息 ) self.classifier nn.Sequential( nn.Linear(hidden_dim, 64), nn.ReLU(), nn.Dropout(0.3), nn.Linear(64, num_classes) ) def forward(self, x): # x: (batch, seq_len, input_dim) out, (h_n, c_n) self.lstm(x) # 取最后一个时间步的输出做分类 last out[:, -1, :] return self.classifier(last) # 加载数据 X np.load(X.npy) y np.load(y.npy) X_train, X_val, y_train, y_val train_test_split( X, y, test_size0.2, random_state42, stratifyy) train_ds TensorDataset(torch.tensor(X_train), torch.tensor(y_train, dtypetorch.long)) val_ds TensorDataset(torch.tensor(X_val), torch.tensor(y_val, dtypetorch.long)) train_loader DataLoader(train_ds, batch_size16, shuffleTrue) val_loader DataLoader(val_ds, batch_size16) device torch.device(cuda if torch.cuda.is_available() else cpu) model PoseLSTM(input_dim39, hidden_dim128, num_layers2, num_classes3).to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) for epoch in range(50): model.train() total_loss 0 for xb, yb in train_loader: xb, yb xb.to(device), yb.to(device) optimizer.zero_grad() logits model(xb) loss criterion(logits, yb) loss.backward() optimizer.step() total_loss loss.item() # 验证 model.eval() correct, total 0, 0 with torch.no_grad(): for xb, yb in val_loader: xb, yb xb.to(device), yb.to(device) pred model(xb).argmax(dim1) correct (pred yb).sum().item() total yb.size(0) print(fEpoch {epoch1}: loss{total_loss/len(train_loader):.4f}, val_acc{correct/total:.4f}) torch.save(model.state_dict(), pose_lstm.pth)参数上hidden_dim128是起点数据少可以降到 64数据多可以升到 256。num_layers2比单层能多学一层时序抽象但超过 3 层在小数据上基本没收益还容易过拟合。dropout0.3是经验值验证集准确率震荡大就调到 0.4~0.5。学习率1e-3配 Adam 是通用起点如果 loss 不降就降到1e-4。3.3 训练不收敛时先查这三处第一查数据标签是否对齐。label_map的 key 必须和文件夹名完全一致大小写、空格都算。第二查输入维度。input_dim必须等于len(KEYPOINT_IDS) * 3改了点集忘了改这里模型会直接报维度错误或静默学废。第三查类别是否均衡。如果三个动作样本数差十倍模型会偏向多数类验证准确率虚高。用np.bincount(y)看一眼不均衡就对少数类做数据增强加噪声、时间轴缩放。4. 推理部署与实时动作识别把模型接到摄像头上4.1 滑动窗口推理实时场景的关键设计训练时每段视频是固定 60 帧但实时摄像头是无限流。常见做法是维护一个长度为 60 的滑动窗口队列每来一帧就推入队列、弹出最旧一帧然后对当前窗口做一次推理。这样每帧都能出一个预测但相邻预测会抖动需要加平滑。from collections import deque WINDOW_SIZE 60 SMOOTH_LEN 10 window deque(maxlenWINDOW_SIZE) pred_history deque(maxlenSMOOTH_LEN) model.load_state_dict(torch.load(pose_lstm.pth, map_locationdevice)) model.eval() cap cv2.VideoCapture(0) pose mp_pose.Pose(static_image_modeFalse, model_complexity1, smooth_landmarksTrue) while cap.isOpened(): ret, frame cap.read() if not ret: break rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) res pose.process(rgb) if res.pose_landmarks: feat [] for idx in KEYPOINT_IDS: lm res.pose_landmarks.landmark[idx] feat.extend([lm.x, lm.y, lm.visibility]) window.append(feat) if len(window) WINDOW_SIZE: inp torch.tensor(np.array([list(window)]), dtypetorch.float32).to(device) with torch.no_grad(): pred model(inp).argmax(dim1).item() pred_history.append(pred) # 多数投票平滑抑制单帧抖动 final_pred max(set(pred_history), keypred_history.count) cv2.putText(frame, fAction: {final_pred}, (30, 50), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 255, 0), 2) cv2.imshow(Pose Recognition, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()SMOOTH_LEN10表示用最近 10 次预测做多数投票。太小平滑不够画面上的标签会疯狂跳太大反应迟钝动作切换后要等半秒才更新。10 是 30fps 下约 0.3 秒延迟体感比较自然。4.2 模型导出与跨平台部署的取舍如果只是本地演示直接torch.save加载就行。要发给别人用或者集成到 App常见做法是导出 ONNXdummy torch.randn(1, 60, 39).to(device) torch.onnx.export(model, dummy, pose_lstm.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch}, output: {0: batch}}, opset_version11)导出后可以用 onnxruntime 在 C、Java、C# 里加载摆脱 Python 依赖。但注意ONNX 导出时如果模型里有dropout推理模式会自动关掉不用手动改。dynamic_axes让 batch 维度可变方便一次推理多段视频。5. 避坑与排查姿态识别项目里最容易翻车的五件事5.1 关键点抖动导致预测标签乱跳现象人站着不动屏幕上的动作标签每秒变好几次。原因MediaPipe 在遮挡或光照变化时关键点会跳变LSTM 对输入噪声敏感。解决开smooth_landmarksTrue再加预测端多数投票平滑如果还抖对关键点坐标做卡尔曼滤波或简单移动平均。5.2 训练准确率 99% 但实测一塌糊涂现象验证集准确率很高换个人、换个背景就全错。原因数据太少且单一模型学到了背景而不是动作。解决每个动作至少录 30 段不同人、不同角度、不同光照训练时对关键点加高斯噪声做增强把坐标归一化到以髋部为中心消除位置差异。5.3 帧率不一致导致序列长度对不上现象训练用 30fps 视频推理用 15fps 摄像头模型表现骤降。原因同样 60 帧30fps 是 2 秒动作15fps 是 4 秒时间尺度变了。解决提取特征时按时间重采样到固定帧率比如统一插值到 30fps 再取 60 帧或者训练时就混入不同帧率的数据。5.4 类别不均衡让模型只会猜多数类现象三个动作里站立样本占 80%模型把所有输入都判成站立准确率还有 80%。原因交叉熵损失被多数类主导。解决用WeightedRandomSampler过采样少数类或给CrossEntropyLoss传weight参数权重设为类别频率的倒数。5.5 实时推理延迟高、画面卡顿现象摄像头画面一卡一卡预测跟不上动作。原因每帧都跑 MediaPipe LSTMCPU 吃满。解决MediaPipe 的model_complexity降到 0LSTM 推理改成每 3 帧跑一次中间帧复用上次结果或者把关键点提取和模型推理放到两个线程用队列解耦。6. 把准确率再抬一截几个我常用的进阶技巧第一个技巧是特征工程加骨骼角度。原始坐标对尺度敏感同一个人离摄像头远近不同x、y 值差很多。我一般会额外算几个关节角度比如肘关节夹角、膝关节夹角、髋关节夹角这些角度是尺度无关的能显著提升跨场景泛化。具体做法是在extract_sequence里对每个关键点三元组算向量夹角把角度拼到特征后面输入维度从 39 变成 39645。第二个技巧是双流输入。把坐标序列和坐标的一阶差分速度拼成两个通道分别过 LSTM 再融合。速度信息对快速挥手和缓慢抬手的区分很关键单靠坐标位置区分不出来。实现上就是torch.cat([x, x.diff(dim1, prependx[:, :1])], dim-1)输入维度翻倍。第三个技巧是验证时用留一人交叉验证。不要随机划分训练验证集而是按录制对象划分A、B、C 三个人的数据拿 A、B 训练C 验证。这样得到的准确率才接近真实上线表现。随机划分会让同一个人相似的动作同时出现在训练和验证集准确率虚高 10~20 个百分点这是血泪经验。技巧输入维度变化预期收益代价加关节角度39 → 45跨场景 5~10%特征提取稍慢双流坐标速度39 → 78快慢动作区分 8%训练时间翻倍留一人交叉验证不变评估更真实训练轮次变多最后说个习惯我每次改完特征或模型结构都会先把X.npy的 shape 和input_dim对一遍再跑一个 epoch 看 loss 有没有降。这个动作花 10 秒能省掉半小时的为什么模型不学的排查。姿态识别这行数据质量比模型结构重要得多与其调 LSTM 层数不如多录几段不同角度的视频。希望帮到你。本文还有配套的精品资源点击获取
返回列表