MediaPipe手语识别Python源码:LSTM/GRU静态动态手势识别与Gradio演示

发布时间:2026/10/1 5:41:45
MediaPipe手语识别Python源码:LSTM/GRU静态动态手势识别与Gradio演示 简介这份资源面向计算机相关专业的本科生与自学者提供一套可直接运行的Python手语识别毕业设计项目基于mediapipe完成手部关键点检测并区分静态与动态两类手势识别任务适合用于毕业设计、课程设计或期末大作业。压缩包共21个文件约9.39MB以py源码、png训练日志图、txt依赖清单和md说明文档为主另含多个已训练好的LSTM与GRU模型文件覆盖静态与动态两种识别流程。项目包含数据采集、模型训练与Gradio可视化交互等模块训练日志图可直观对比不同模型与参数下的收敛表现方便读者理解模型选型与调参思路。目前已有188人学习下载源码经本地编译验证可运行评审分达98分内容经助教审定难度适中下载后可直接复现实验并在此基础上做二次开发。1. 从一份能跑通的 Python 手语识别源码说起它到底解决了什么问题很多做毕业设计的同学卡在同一个地方算法思路能讲清楚但真到要交一份「能跑、有数据、有训练日志、有界面」的完整工程时就发现手里只有零散的 demo。这份基于 MediaPipe 的手语识别 Python 源码恰好补的就是这个缺口——它不是一段孤立的推理脚本而是一套从数据采集、模型训练到 Gradio 界面演示的闭环工程静态手势和动态手势两条线都覆盖了。资源里同时给了静态模型和动态模型静态用 LSTM 和 GRU 各训了一版动态同样有 LSTM 和 GRU 的多个权重文件还附带训练日志曲线图。这意味着你拿到手就能直接跑推理看效果也能顺着get_static_dataset.py和get_dynamic_dataset.py重新采数据、重训模型。适合谁做计算机毕业设计、课程设计、期末大作业的本科生以及想快速摸清 MediaPipe 时序模型落地套路的 Python 入门者。下面我按「先跑起来、再拆原理、最后避坑」的顺序把这份源码拆开讲。2. 环境搭建与依赖安装把 MediaPipe 和 Gradio 装进你的 Python2.1 为什么选 MediaPipe 做手部关键点提取手语识别的第一步永远是把手从画面里「抠」出来转成机器能算的坐标。传统做法是自己训一个目标检测网络标注成本高、训练慢对毕业设计这种周期紧的场景不划算。MediaPipe 的 Hands 方案直接给出 21 个手部关键点每只手 21 个点、每个点 (x, y, z) 三个坐标单帧就是 63 维特征开箱即用。选它的核心理由有三个一是 CPU 上就能实时跑不需要显卡也能出效果答辩演示时用笔记本就够二是关键点坐标是归一化的跟画面分辨率解耦换摄像头不用重新标定三是它输出的关键点顺序固定静态手势直接喂全连接网络动态手势按帧堆成序列喂 LSTM/GRU工程上非常顺。常见做法是把 21 个点先做一次相对手腕的平移归一化再送进模型这样手在画面里移动不影响识别结果。2.2 依赖安装与版本对齐requirements.txt里列了核心依赖但 MediaPipe 对 Python 版本和 protobuf 版本比较挑直接pip install最新版经常翻车。我一般会先建虚拟环境再按下面这套流程走# 建虚拟环境Python 建议 3.8~3.103.11 以上 MediaPipe 兼容性差 python -m venv venv # Windows 激活 venv\Scripts\activate # Linux / macOS 激活 source venv/bin/activate # 先装 mediapipe让它自己拉匹配的依赖 pip install mediapipe0.10.9 # 再装其余依赖 pip install opencv-python gradio numpy tensorflow逻辑说明先单独装 MediaPipe 是为了让它锁定自己需要的 protobuf 版本避免后面装 TensorFlow 时把 protobuf 顶到不兼容的版本。参数上mediapipe0.10.9是相对稳定的一个版本如果你本地 Python 是 3.11装的时候大概率报No matching distribution这时候要么降 Python 版本要么换更新的 MediaPipe 版本试。提示装完先跑一句python -c import mediapipe; print(mediapipe.__version__)能打印出版本号再往下走别急着跑主程序。2.3 目录结构与文件职责拿到压缩包解压后先别急着运行把每个文件干什么搞清楚后面排错能省一半时间文件 / 目录作用static_hand_detect.py静态手势识别主程序调摄像头实时推理dynamic_hand_detect.py动态手势识别主程序按帧序列推理get_static_dataset.py采集静态手势数据按键保存样本get_dynamic_dataset.py采集动态手势序列数据gradio_app.pyGradio 网页界面浏览器里演示models/训练好的 LSTM / GRU 权重文件logs/训练过程曲线图写论文时可直接引用requirements.txt依赖清单models目录里文件名带lstm_126、gru_258这种后缀数字一般对应训练轮数或样本量具体含义以你本地实际训练配置为准别照搬我的猜测。logs里的 png 是训练日志曲线答辩 PPT 里放一张 loss 下降曲线比空口说「模型收敛了」有说服力得多。3. 静态手势识别从采集数据到实时推理的完整链路3.1 静态数据采集脚本怎么用静态手势的核心是「一帧画面 一个类别」。get_static_dataset.py的逻辑通常是打开摄像头实时显示 MediaPipe 画出的手部骨架你摆出某个手势后按指定键脚本把当前帧的 21 个关键点坐标存成一条样本同时记录类别标签。import cv2 import mediapipe as mp import numpy as np import os mp_hands mp.solutions.hands hands mp_hands.Hands(max_num_hands1, min_detection_confidence0.7) # 类别名和保存目录按你的手势类别改 GESTURE_NAME ok SAVE_DIR fdataset/static/{GESTURE_NAME} os.makedirs(SAVE_DIR, exist_okTrue) cap cv2.VideoCapture(0) count 0 while True: ret, frame cap.read() if not ret: break frame cv2.flip(frame, 1) # 镜像符合直觉 rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) result hands.process(rgb) if result.multi_hand_landmarks: for hand in result.multi_hand_landmarks: # 提取 21 个点的 x,y,z展平成 63 维 coords [] for lm in hand.landmark: coords.extend([lm.x, lm.y, lm.z]) coords np.array(coords) # 按 s 键保存当前样本 if cv2.waitKey(1) 0xFF ord(s): np.save(os.path.join(SAVE_DIR, f{count}.npy), coords) count 1 print(fsaved {count}) cv2.imshow(collect, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()逻辑说明max_num_hands1表示只检测一只手手语识别里单手场景居多设成 1 能减少误检。min_detection_confidence0.7是检测置信度阈值调高更严格但可能漏检调低更灵敏但容易把背景误判成手。坐标展平成 63 维后存成.npy一个类别一个文件夹这是后面训练时按目录读标签的基础。参数怎么改类别名GESTURE_NAME每换一个手势就改一次或者写成命令行参数循环采集。保存格式用.npy比.csv读写快样本量大时优势明显。3.2 静态模型训练与 LSTM/GRU 的选择静态手势理论上用全连接网络就够但这份资源里静态也用了 LSTM/GRU原因在于它把单帧的 63 维当成「长度为 1 的序列」处理这样静态和动态两套代码能复用同一套模型结构工程上更统一。训练脚本的核心是读dataset/static下所有.npy按文件夹名映射标签然后切分训练集验证集。import numpy as np import os from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.utils import to_categorical DATA_DIR dataset/static actions sorted(os.listdir(DATA_DIR)) # 类别列表 X, y [], [] for idx, action in enumerate(actions): for f in os.listdir(os.path.join(DATA_DIR, action)): res np.load(os.path.join(DATA_DIR, action, f)) X.append(res) y.append(idx) X np.array(X).reshape(-1, 1, 63) # (样本数, 时间步1, 特征63) y to_categorical(y, num_classeslen(actions)) model Sequential([ LSTM(64, return_sequencesFalse, input_shape(1, 63)), Dropout(0.3), Dense(32, activationrelu), Dense(len(actions), activationsoftmax) ]) model.compile(optimizeradam, losscategorical_crossentropy, metrics[accuracy]) model.fit(X, y, epochs100, batch_size16, validation_split0.2) model.save(models/static_model_lstm.h5)逻辑说明reshape(-1, 1, 63)把每个样本变成时间步为 1 的序列这是静态手势能塞进 LSTM 的关键。Dropout(0.3)防过拟合样本少的时候尤其重要。epochs100配合validation_split0.2训练完看验证集准确率如果训练集 99% 验证集 60%那就是过拟合得加数据或加 Dropout。LSTM 和 GRU 怎么选GRU 参数少、训练快样本量小的时候更不容易过拟合LSTM 表达能力强动态手势这种长序列场景通常效果更好。资源里两套都给了建议先跑 GRU 看基线再换 LSTM 对比论文里正好做一组消融实验。3.3 实时推理脚本的运行与调参static_hand_detect.py是最终演示入口逻辑是摄像头取帧 → MediaPipe 提关键点 → 归一化 → 送模型 → 显示类别。跑之前确认模型路径和类别列表跟训练时一致否则会出现「预测结果全是同一个类」的玄学现象。import cv2 import mediapipe as mp import numpy as np from tensorflow.keras.models import load_model model load_model(models/static_model_lstm.h5) actions [ok, fist, palm] # 必须和训练时顺序一致 mp_hands mp.solutions.hands hands mp_hands.Hands(max_num_hands1, min_detection_confidence0.7) cap cv2.VideoCapture(0) while True: ret, frame cap.read() frame cv2.flip(frame, 1) rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) result hands.process(rgb) if result.multi_hand_landmarks: for hand in result.multi_hand_landmarks: coords [] for lm in hand.landmark: coords.extend([lm.x, lm.y, lm.z]) coords np.array(coords).reshape(1, 1, 63) pred model.predict(coords, verbose0) label actions[np.argmax(pred)] cv2.putText(frame, label, (10, 50), cv2.FONT_HERSHEY_SIMPLEX, 1.5, (0, 255, 0), 3) cv2.imshow(static, frame) if cv2.waitKey(1) 0xFF ord(q): break逻辑说明actions列表顺序必须和训练时sorted(os.listdir())的顺序完全一致这是最常见的翻车点——顺序错了模型输出索引对不上标签识别结果全乱。verbose0关掉每帧的预测日志不然控制台刷屏。参数怎么改min_detection_confidence在光线差的环境可以降到 0.5但误检会变多cv2.putText的坐标和字号按你摄像头分辨率调1080p 下 1.5 的字号偏小。4. 动态手势识别序列建模与 Gradio 界面落地4.1 动态数据采集与序列长度对齐动态手势跟静态最大的区别是「一个动作 一段帧序列」。get_dynamic_dataset.py通常按固定帧数采集比如每个动作采 30 帧每帧 63 维一个样本就是 (30, 63)。采集时一般会有一个「开始录制」的触发键录满 30 帧自动停。SEQUENCE_LENGTH 30 sequence [] recording False while True: ret, frame cap.read() frame cv2.flip(frame, 1) rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) result hands.process(rgb) if result.multi_hand_landmarks: for hand in result.multi_hand_landmarks: coords [] for lm in hand.landmark: coords.extend([lm.x, lm.y, lm.z]) sequence.append(coords) key cv2.waitKey(1) 0xFF if key ord(r): # 按 r 开始录制 recording True sequence [] if recording and len(sequence) SEQUENCE_LENGTH: np.save(fdataset/dynamic/{ACTION}/{count}.npy, np.array(sequence)) count 1 recording False print(saved, count)逻辑说明SEQUENCE_LENGTH30是序列长度采太短动作信息不全采太长模型难训且推理延迟高。30 帧在 30fps 摄像头下约 1 秒覆盖大多数手语动作。序列长度必须全数据集统一否则没法堆成 batch。参数怎么改动作快的手势可以降到 20 帧慢动作可以加到 40 帧但改完要重新采全部数据不能混用。4.2 LSTM 与 GRU 在动态手势上的训练差异动态模型输入是 (30, 63)输出是类别。LSTM 和 GRU 的结构差异直接体现在训练日志曲线上资源里logs目录那几张 png 就是证据。from tensorflow.keras.layers import LSTM, GRU, Dense, Dropout def build_model(kind, num_classes): layer LSTM(64) if kind lstm else GRU(64) model Sequential([ layer, Dropout(0.3), Dense(32, activationrelu), Dense(num_classes, activationsoftmax) ]) model.compile(optimizeradam, losscategorical_crossentropy, metrics[accuracy]) return model逻辑说明LSTM(64)和GRU(64)的 64 是隐藏单元数样本量小可以降到 32样本多可以升到 128。return_sequencesFalse表示只取最后一个时间步的输出做分类这是序列分类的标准写法。对比经验GRU 训练一轮大概比 LSTM 快 20%~30%收敛曲线更平滑LSTM 在动作区分度低的类别上准确率通常高 2~5 个百分点。资源里dynamic_train_log_gru_1662.png和dynamic_train_log_lstm_1662.png这种同后缀不同模型的图正好拿来对比写论文时放一起很有说服力。4.3 Gradio 界面把模型变成能演示的网页gradio_app.py是答辩加分项——老师不用装环境浏览器打开就能试。Gradio 的核心是把推理函数包一层输入是图像输出是标签。import gradio as gr import numpy as np import cv2 import mediapipe as mp from tensorflow.keras.models import load_model model load_model(models/dynamic_model_lstm.h5) actions [hello, thanks, yes] mp_hands mp.solutions.hands hands mp_hands.Hands(max_num_hands1, min_detection_confidence0.7) def predict(image): rgb cv2.cvtColor(image, cv2.COLOR_BGR2RGB) result hands.process(rgb) if not result.multi_hand_landmarks: return 未检测到手 coords [] for lm in result.multi_hand_landmarks[0].landmark: coords.extend([lm.x, lm.y, lm.z]) # 单帧演示实际动态需缓存多帧 seq np.array([coords] * 30).reshape(1, 30, 63) pred model.predict(seq, verbose0) return actions[np.argmax(pred)] gr.Interface(fnpredict, inputsimage, outputstext).launch()逻辑说明这里为了演示简单把单帧复制 30 次凑成序列实际动态识别应该用队列缓存最近 30 帧。launch()默认起在本地 7860 端口加shareTrue能生成临时公网链接但答辩现场网络不稳建议本地跑。参数怎么改inputsimage可以换成webcam做实时但 Gradio 的 webcam 组件延迟比 OpenCV 窗口高演示静态图更稳。5. 避坑与常见问题排查那些让我重训三次的坑5.1 关键点顺序错乱导致识别全错现象模型训练准确率 99%实时推理却永远输出同一个类别。原因训练时读数据的顺序和推理时actions列表顺序不一致sorted()在不同系统上对中文或大小写混合的排序结果可能不同。解决把类别列表写死成一个固定的labels.json训练和推理都读同一个文件别依赖os.listdir()的默认顺序。5.2 MediaPipe 检测不到手现象摄像头画面正常但result.multi_hand_landmarks一直是None。原因一是光线太暗二是手离镜头太远三是min_detection_confidence设太高。解决先把阈值降到 0.5 试再调整光照和手部距离MediaPipe 对背景杂乱比较敏感纯色背景效果最好。5.3 序列长度不统一导致训练报错现象np.array(sequences)时报setting an array element with a sequence。原因采集时有的样本 30 帧、有的 28 帧形状不一致。解决采集脚本里强制len(sequence) SEQUENCE_LENGTH才保存或者在训练前统一截断/补零到固定长度。5.4 模型文件加载报版本不兼容现象load_model抛Unknown layer或save_format相关错误。原因训练用的 TensorFlow 版本和推理环境不一致.h5格式跨版本兼容性差。解决训练和推理用同一个虚拟环境或者改用SavedModel格式保存兼容性更好。5.5 Gradio 端口被占用现象launch()报OSError: Cannot find empty port。原因7860 端口被其他程序占了。解决launch(server_port7861)换端口或者先netstat查一下谁占着。6. 进阶技巧用训练日志曲线反推模型是否值得继续训拿到这份源码后很多人跑通就停了其实logs目录那几张训练曲线图才是写论文的富矿。我的习惯是每次训练完先把 loss 和 accuracy 曲线画出来横轴 epoch、纵轴指标训练集和验证集两条线放一起看。如果训练 loss 一直降、验证 loss 先降后升那就是过拟合加 Dropout 或加数据如果两条线都平着不降那是学习率太大或模型容量不够。具体做法是在训练脚本里加一段回调把每个 epoch 的指标存下来import matplotlib.pyplot as plt history model.fit(X, y, epochs100, validation_split0.2) plt.plot(history.history[loss], labeltrain_loss) plt.plot(history.history[val_loss], labelval_loss) plt.plot(history.history[accuracy], labeltrain_acc) plt.plot(history.history[val_accuracy], labelval_acc) plt.legend() plt.savefig(logs/my_train_curve.png, dpi150)逻辑说明dpi150保证论文里插图清晰legend()必须有不然审阅老师分不清哪条线是哪个。资源里现成的 png 可以直接对比但自己重训一遍再画答辩时讲起来更有底气。还有一个技巧是拿dynamic_model_lstm_258和dynamic_model_gru_258做交叉验证同一批测试数据分别喂给两个模型统计各自混淆矩阵。如果某个类别在两个模型上都错那大概率是数据采集阶段这个手势样本太少或动作不规范回去补采比调模型有效得多。我当初就是靠这个发现「谢谢」和「再见」两个手势在动态序列里前 10 帧几乎一样后来把序列长度从 30 降到 20只保留区分度高的后半段准确率直接涨了 8 个点。从那以后我每次拿到新的时序识别项目都强制先跑一遍混淆矩阵再谈调参不然就是盲人摸象。希望这份拆解帮到你把这份源码真正跑成你自己的东西。本文还有配套的精品资源点击获取