
简介这是一份基于LSTM与MediaPipe实现的人体动作姿态识别Python源码适合作为计算机、人工智能、数据科学等专业的毕业设计、课程设计或大作业。系统能捕捉视频流中的人体关键姿态并实时分类可应用于教育、运动分析、康复训练等场景代码完整且已验证可运行。资源共280个文件主要包括240个npy数据文件、16个py核心脚本、16个mp4演示视频、2个h5模型权重及2个md说明文档压缩包整体约22.09MB。npy用于姿态特征数据py为训练与识别流程代码mp4提供效果演示h5为已训练好的模型结构清晰便于直接学习与二次开发。目前已有186人浏览学习适合从入门到进阶逐步理解姿态识别项目完整链路若基础较好也可在此基础拓展更多动作类别实现个性化功能。1. 人体动作姿态识别自带训练好的 action.h5这份毕设源码能直接复现吗如果你正在做人机交互、健身动作计数或者康复训练相关的课程设计第一反应往往是“姿态识别太贵了”OpenPose 要单独配 CUDA 环境直接训练视频分类又绕不开上万张标注帧。这份工程走的是另一条路——MediaPipe 先把人体关键点实时提出来LSTM 再把关键点序列分类成动作模型已经训练好并导出成 action.h5拿到的第一分钟不用从零训练就能看到识别效果。它解决的核心问题是“用普通摄像头实时区分几类动作”不是科研级的姿态估计而是能交作业、能演示、能二次开发的落地组合。适合正在做毕设、课程设计或者想快速验证动作识别思路的 Python 开发者这台组合的性价比在同类方案里确实少见。2. MediaPipe 关键点提取从摄像头画面到 132 维特征向量2.1 为什么是 MediaPipe33 个关键点和 OpenPose 的差别MediaPipe Pose 输出的是一组人体姿态关键点默认是 33 个 landmark覆盖鼻子、眼睛、耳朵、肩肘腕、髋膝踝等位置和 OpenPose 常用的 COCO 18 点模型相比多出了面部轮廓和更完整的躯干点。动作识别真正关心的是四肢角度和躯干姿态33 点的冗余反而带来一个好处就算某个点被遮挡模型还能靠周围点兜底。选 MediaPipe 还有一个很实际的理由它在 CPU 上就能跑实时。用 OpenPose 做视频流处理普通笔记本掉帧到 10 帧以内而 MediaPipe 在 model_complexity1 时基本能保住 30 帧。毕设演示现场一般没有 GPU 机器这个差距就是能不能演示的关键。这段选型经验是我在改自己项目时反复对比过的。动作识别不一定要把 33 个点全用上如果只做上半身动作可以只保留肩、肘、腕等 11 个点噪声更少、训练更快。但工程里通常直接用全量点因为裁剪点集意味着要二次验证哪些点对动作有区分度这个时间成本比训练本身还高。拿到资源后建议先保持原工程的特征维度跑通了再考虑裁剪。2.2 环境准备python 版本选择与 mediapipe 安装踩点解压后第一件事不是看代码是先看介绍.md里面一般写了运行版本。推荐用 conda 建一个独立环境避免把系统 Python 搞乱。我的固定组合是 Python 3.8 OpenCV-Python MediaPipe 0.10.x这套搭配在 Windows 和 Ubuntu 上都验证过。conda create -n pose python3.8 -y conda activate pose pip install opencv-python mediapipe0.10.14 tensorflow2.10.0如果下载速度慢加-i https://pypi.tuna.tsinghua.edu.cn/simple换国内镜像。这里最怕的是 mediapipe 和 protobuf 版本打架mediapipe 0.10.x 对 protobuf 有严格约束直接装最新版 pip 可能会把 protobuf 升到 4.x然后 import mediapipe 就报错。用上面这条命令组合装出来的环境我跑过三台机器没有一次翻车。依赖推荐版本说明Python3.8 ~ 3.103.11 以下兼容性最稳mediapipe0.10.14关键点索引稳定opencv-python4.x 最新即可读视频和摄像头tensorflow2.10.0对应 Keras 的 h5 加载2.3 关键点提取代码把 l0.mp4 变成 npy 特征项目里的 l0.mp4、r0.mp4、r1.mp4 就是现成的测试样本先拿它们跑关键点提取比一上来接摄像头省事得多。下面是把视频逐帧转成关键点数组的完整流程。import cv2 import numpy as np import mediapipe as mp mp_pose mp.solutions.pose pose mp_pose.Pose( static_image_modeFalse, model_complexity1, min_detection_confidence0.5, min_tracking_confidence0.5 ) cap cv2.VideoCapture(r0.mp4) frames_points [] 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 is not None: row [] for lm in result.pose_landmarks.landmark: row.extend([lm.x, lm.y, lm.z, lm.visibility]) frames_points.append(row) pose.close() cap.release() features np.array(frames_points) print(features.shape) # 输出类似 (帧数, 132) np.save(r0_keypoints.npy, features)代码逻辑分四层先用Pose()初始化模型参数里的model_complexity1表示中等模型0 更快但关键点抖动明显2 最准但 CPU 压力大然后逐帧读取视频并把 BGR 转成 RGB这是 MediaPipe 的硬性要求不转的话头部关键点会识别错位接着把每个 landmark 的 x、y、z、visibility 拼成一维向量33 个点乘 4 个通道正好是 132 维最后把整个视频的帧拼成二维数组保存。min_detection_confidence和min_tracking_confidence这两个阈值值得单独说。前者控制首次检测的置信度下限后者控制跟踪帧的置信度下限。毕设演示场景光线不稳定我习惯把两个都设在 0.5~0.6 之间太低会混入大量误检帧太高会出现关键点断档。2.4 特征归一化用相对坐标降低身材和距离的干扰这是最容易忽略的细节。直接用原始 x、y、z 训练模型会把“人离摄像头远近”当成动作特征同一个动作人走近一点坐标整体变大LSTM 就可能判断成别的动作。正确做法是转成相对坐标以左肩为原点再除以肩宽做尺度归一化。base_idx 11 # MediaPipe 中左肩的索引 norm features.reshape(-1, 33, 4).copy() # 以左肩为参考点做平移 base_xyz norm[:, base_idx, :3] for i in range(33): norm[:, i, 0] - base_xyz[:, 0] norm[:, i, 1] - base_xyz[:, 1] norm[:, i, 2] - base_xyz[:, 2] # 用左肩到右肩的距离做缩放 left_shoulder norm[:, 11, :3] right_shoulder norm[:, 12, :3] scale np.linalg.norm(right_shoulder - left_shoulder, axis1, keepdimsTrue) norm[:, :, :3] / (scale[:, :, None] 1e-6)平移后用肩宽归一不同体型、不同拍摄距离的同类动作在特征空间里就对齐了。1e-6是防止肩宽为 0 时除零报错这个细节在个别帧关键点丢失时救过我一次。visibility 通道不要去归一化它本身就是一个 0~1 的置信度值保持原样即可。训练时用了这套归一化推理时必须用完全相同的处理否则特征空间错位模型效果直接崩掉。3. 时序数据集构建为什么 LSTM 需要 30 帧的滑窗样本3.1 为什么单帧不够动作是时序状态转移“坐下”和“站起”这两个动作在某一帧上可能姿态极其接近——都是躯干微倾、膝盖微屈。真正的区别在时间轴坐下是髋关节往下走站起是髋关节往上走。单帧 CNN 根本分不出来这就是 LSTM 在这个项目里不可替代的原因。LSTM 的每个时间步会维护一个记忆单元把前面若干帧的状态“记住”再和当前帧一起决定输出。用视频整体做分类也是一种思路但它在推理阶段没法实时——你必须等整个动作结束才能出结果。毕设演示时评委要看到“人一挥手屏幕立刻显示挥手”的效果所以工程上用的是滑窗固定取最近 30 帧组成一个窗口窗口滑动一步就预测一次。30 帧在 30 帧率的视频里刚好是 1 秒能覆盖大部分日常动作的持续时间。3.2 滑窗采样sequence_length 与 step 怎么定代码里常见的做法是对每个视频的关键点序列做滑窗切片然后把切片和标签配对。下面这段是数据构造的核心逻辑。sequence_length 30 step 5 X_seq, y_seq [], [] for video_feat, label in zip(all_video_features, all_labels): frame_count len(video_feat) if frame_count sequence_length: continue for start in range(0, frame_count - sequence_length 1, step): window video_feat[start:start sequence_length] X_seq.append(window) y_seq.append(label) X_seq np.array(X_seq) # shape: (样本数, 30, 132) y_seq np.array(y_seq)step5意味着相邻两个窗口有 25 帧重叠数据量被放大了 6 倍。重叠的好处是样本平滑、模型不容易过拟合单帧噪点坏处是冗余样本太多、训练时间变长。我一般先设step10快速验证效果再调小到 5 看精度有没有提升没有提升就保持 10。sequence_length不要盲目加大到 60动作做完之后手臂停留的静止帧会被硬塞进窗口反而稀释动作特征。如果某个动作的视频特别长比如录了 30 秒滑窗后会得到上千个样本而其他动作只有几百个就出现了类别不平衡。常见处理是给样本少的动作做重复采样或者随机丢弃样本多的动作的一部分窗口。不要用那种过采样到完全 1:1 的方式动作姿态之间本身有相似性硬凑完全平衡反而放大噪声。3.3 标签编码与数据集划分按视频切别按帧随机切标签处理在毕设级代码里通常有两种写法一是直接把动作名换成数字二是用 one-hot 编码。LSTM 加 softmax 输出的组合配合交叉熵损失用 one-hot 是标准配置。from tensorflow.keras.utils import to_categorical num_classes 5 # 具体以介绍.md 里的动作清单为准 y_cat to_categorical(y_seq, num_classesnum_classes)真正坑的是数据划分。很多人直接train_test_split(X_seq, y_cat, test_size0.2)这里的随机抽样是逐样本的而样本之间高度重叠——来自同一个视频的相邻窗口几乎一模一样。结果就是验证集里混进了训练样本的“近亲”val_loss 虚低换到真实摄像头上一测就露馅。正确做法是按视频划分先算每个视频的滑窗索引范围再按视频维度切分。例如 3 个视频 5 个动作就选其中 2 个视频做训练、1 个做验证。这样验证集里不会出现和训练集来自同一段视频的窗口评估结果才可信。我在做自己的数据集时还会再加一层同一个动作的 3 段视频至少保证 1 段完全留给验证这是毕设答辩时经得起问的数据集设计。4. LSTM 模型与 action.h5网络结构、训练参数和实时预测这条链路4.1 LSTM 网络结构从 input_shape(30, 132) 到 softmax拿到工程后先别急着跑 predict先看模型结构。Keras 里复现这个项目最常见的结构是双层 LSTM 加 Dropout再加一个全连接层做分类。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout model Sequential([ LSTM(64, return_sequencesTrue, input_shape(30, 132)), Dropout(0.3), LSTM(64), Dropout(0.3), Dense(32, activationrelu), Dense(num_classes, activationsoftmax) ]) model.compile( optimizeradam, losscategorical_crossentropy, metrics[accuracy] )input_shape(30, 132)里的 30 对应序列长度、132 对应每帧 33 个关键点乘 4 个通道。第一个 LSTM 层return_sequencesTrue输出的是一个完整的时间步序列交给第二层继续处理第二层不保留序列只输出最后一个时间步的隐状态再接全连接层。Dropout 加在 LSTM 输出之后防止模型记住训练集里的噪声。参数建议值调整方向LSTM units64 ~ 128动作类少时 32 也够Dropout0.3 ~ 0.5过拟合加重时往上调Dense 中间层32类别多可加到 64optimizeradam换 rmsprop 效果差不多这个结构对 5 类左右的简单动作已经足够。如果训练数据只有几千个样本双层 LSTM 可能过拟合我的经验是先从单层 64 units 起步验证集精度上不去了再加深。这属于那种“加层容易、降层难”的模型设计毕设项目用双层 LSTM 是安全牌但别一上来就堆三层。4.2 训练参数与回调让 val_loss 早停而不是硬跑 100 轮训练 LSTM 最忌讳的就是不看验证集损失硬跑几百轮。一是训练慢二是后期 val_loss 早就开始反弹了。工程里拿出一份能用的 action.h5训练过程中一般都会配 EarlyStopping 和 ModelCheckpoint。from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau, ModelCheckpoint callbacks [ EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue), ReduceLROnPlateau(monitorval_loss, factor0.5, patience5, min_lr1e-5), ModelCheckpoint(action.h5, monitorval_loss, save_best_onlyTrue) ] history model.fit( X_train, y_train, validation_data(X_val, y_val), epochs50, batch_size32, callbackscallbacks )EarlyStopping的 patience 设为 10意思是验证集损失连续 10 轮不下降就停restore_best_weightsTrue会把权重回滚到最优那一轮这个参数一定要开否则停的时候拿的是最后几轮已经变差的权重。ReduceLROnPlateau在损失平缓时把学习率减半是让模型跳出局部最优的常见手段。ModelCheckpoint只在验证集损失变好时覆盖保存 h5保证 action.h5 始终是最优版本。训练完成后看两件事训练曲线里 train_loss 和 val_loss 的间距间距越来越大就是过拟合以及 val_accuracy 是否达到项目介绍里声明的水平。如果精度差得远先回头查数据划分这个环节出问题的概率比网络结构高得多。4.3 实时推理链路滑窗缓冲、预测频率与平滑输出训练完模型只是第一步毕设演示核心在实时推理。推理链路用一个队列缓冲最近 30 帧的关键点每来一帧就更新队列攒够 30 帧就喂给 LSTM 预测一次。from collections import deque import numpy as np buffer deque(maxlensequence_length) current_action unknown cap cv2.VideoCapture(0) while cap.isOpened(): ret, frame cap.read() if not ret: break # 复用第 2.3 节的关键点提取逻辑返回 132 维向量或 None lm extract_landmarks(frame) if lm is not None: buffer.append(lm) if len(buffer) sequence_length: seq np.array(buffer).reshape(1, sequence_length, 132) prob model.predict(seq, verbose0)[0] label_id int(np.argmax(prob)) confidence float(np.max(prob)) if confidence 0.6: current_action action_names[label_id] cv2.putText(frame, fAction: {current_action}, (20, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) cv2.imshow(Pose Recognition, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这里有一个性能细节model.predict不是每帧都要跑。视频流是 30 帧每秒LSTM 推理一次大约 5~10 毫秒MediaPipe 关键点提取才是耗时大头。我通常每 3 帧才做一次预测然后把结果缓存起来画面显示用的是最近一次预测结果帧率能提升明显。4.4 加载 action.h5先 model.summary() 再 predict拿到资源后最稳妥的第一步是加载 h5 并打印结构确认输入输出维度和你预期一致。from tensorflow.keras.models import load_model model load_model(action.h5) model.summary()model.summary()会显示输入层的 shape 和输出层的类别数。如果输出层是 5说明模型识别 5 类动作项目里有三个演示视频不代表只有 3 类有的视频是同一动作的不同视角具体动作清单以压缩包里的介绍.md 为准。我在接手别人训练好的 h5 时吃过一次亏没看结构直接 predict传进去的是 (30, 132)模型要的是 (1, 30, 132)维度错了 Keras 报错还不太直观后来养成了先看 summary 的习惯再也没有因为 shape 浪费过时间。5. 避坑记录中文路径、mediapipe 安装和动作识别抖动的五个解法5.1 解压中文路径导致视频读不出来现象cv2.VideoCapture 读 mp4 返回 False或者 model.load_model 报路径错误但代码看起来完全没问题。原因MediaPipe 和部分 OpenCV 版本对中文路径支持不完善解压到“毕业设计_最终版”这种目录时底层 API 无法正确解析路径。解决解压后立即重命名为英文路径比如pose_recognition且路径里不要有空格和特殊符号。这是项目说明里反复强调的事我在 Windows 上亲自遇到过改成英文路径后视频读取立刻恢复。5.2 mediapipe 安装失败protobuf 报错现象pip install mediapipe 成功但 import mediapipe 时抛TypeError: Descriptors cannot not be created directly或者提示 protobuf 版本冲突。原因mediapipe 内部依赖 protobuf 3.20.x而自带的 protobuf 4.x 破坏了它依赖的 descriptor API。解决把 protobuf 锁到 3.20.x或者直接用 2.2 节的环境组合命令重装。不要盲目装最新版pip install protobuf3.20.3是目前最稳的后悔药。5.3 model.predict 报维度错误现象加载 action.h5 后调用 predict提示Input 0 of layer lstm is incompatible。原因输入数据是 (30, 132) 或 (None, 132)而模型期望的是 (batch, 时间步, 特征数)也就是 (1, 30, 132)。解决先跑model.summary()看输入层 shape再对数据做 reshape。常见修复是seq np.array(buffer).reshape(1, 30, 132)。这个错误还有一个变体训练时用了归一化推理时没做特征分布错位导致准确率断崖式下跌这种不会报错但比报错更难排查。5.4 识别结果跳变动作标签一秒换三次现象演示视频里同一个动作保持不动画面上标签却在两个动作之间反复横跳。原因单帧预测直接用 argmax 取最大概率而两个动作在边界附近时 softmax 输出很接近轻微的关键点抖动就会反转结果。解决给置信度加阈值概率低于 0.6 就保持原标签再配合连续 N 帧确认即连续 5 帧预测出相同动作才切换标签。这个方案在 6.2 节有代码。5.5 训练集精度 99%验证集和摄像头上一塌糊涂现象模型在训练集上准确率接近满值但验证集上只有 60%换到摄像头实时预测更差。原因多半是数据划分时把同一视频的滑窗随机打散验证集里的样本和训练集样本高度重叠模型其实是在“背答案”还有一种可能是视频中有大量静止帧模型学到的是“不动”这个状态。解决按视频维度做数据集划分保证验证集视频和训练集视频完全隔离。如果动作之间有静止段采集数据时标注动作起点和终点只切有效动作段。6. 验证与扩展离线测试脚本、置信度阈值和新动作的二次开发拿到一份训练好的 h5先别急着接摄像头先做离线验证。把项目自带视频逐段喂给模型输出每一帧的预测标签和置信度这样能快速判断模型状态。下面是我常用的离线验证脚本核心段。model load_model(action.h5) cap cv2.VideoCapture(r0.mp4) buffer deque(maxlensequence_length) results [] while cap.isOpened(): ret, frame cap.read() if not ret: break lm extract_landmarks(frame) if lm is not None: buffer.append(lm) if len(buffer) sequence_length: seq np.array(buffer).reshape(1, sequence_length, 132) prob model.predict(seq, verbose0)[0] label_id int(np.argmax(prob)) results.append((label_id, float(np.max(prob)))) cap.release()跑完后统计每个动作的标签占比如果某个动作的置信度均值低于 0.5说明模型对这个动作尚未学会需要补充训练数据。进阶技巧里最实用的是置信度阈值加连续帧确认能把实时演示的稳定性提升一个档次。threshold 0.6 confirm_count 5 current_action unknown streak 0 # 在每一帧预测后执行以下逻辑 if confidence threshold: if label_id current_label: streak 1 else: current_label label_id streak 1 if streak confirm_count: current_action action_names[label_id]这个状态机解决了 5.4 节的跳变问题单次预测结果必须先稳定累计 5 帧才允许切换动作标签。演示现场的人动作再随意也不会在 5 帧内完成一次假动作。如果你想在毕设上做二次开发新增一个动作不需要重新训练整个模型。LSTM 层提取的是通用的时序运动特征你只需录几段新动作的视频按第 3 章的流程生成样本然后把最后一层 Dense 的类别数改成新类别冻结前面所有层只训练最后一层几十轮就能收敛。这种微调方式在数据量少时比全量重训稳定得多。我在这类项目上最有感触的一课是拿到任何别人训练好的 h5先看介绍.md再model.summary()核对结构然后跑一段离线视频验证最后才接摄像头。这样走完四步之后剩下的大部分问题都在数据而不是模型。那次跳过前两步、直接接摄像头的翻车经历让我学会了对每个下载下来的工程先建立环境快照再动代码。从那以后我每次解压这类毕设工程都强制走一遍“看说明、建环境、核对模型、离线验证”的流程再也没因为低级问题卡壳过。希望这份复盘和数据流程对你也有用。本文还有配套的精品资源点击获取