简介:这份资源面向计算机相关专业的本科生与自学者,提供一套可直接运行的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 装进你的 Python
2.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.10,3.11 以上 MediaPipe 兼容性差 python -m venv venv # Windows 激活 venv\Scripts\activate # Linux / macOS 激活 source venv/bin/activate # 先装 mediapipe,让它自己拉匹配的依赖 pip install mediapipe==0.10.9 # 再装其余依赖 pip install opencv-python gradio numpy tensorflow逻辑说明:先单独装 MediaPipe 是为了让它锁定自己需要的 protobuf 版本,避免后面装 TensorFlow 时把 protobuf 顶到不兼容的版本。参数上,mediapipe==0.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.py | Gradio 网页界面,浏览器里演示 |
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_hands=1, min_detection_confidence=0.7) # 类别名和保存目录,按你的手势类别改 GESTURE_NAME = "ok" SAVE_DIR = f"dataset/static/{GESTURE_NAME}" os.makedirs(SAVE_DIR, exist_ok=True) 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(f"saved {count}") cv2.imshow("collect", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()逻辑说明:max_num_hands=1表示只检测一只手,手语识别里单手场景居多,设成 1 能减少误检。min_detection_confidence=0.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_classes=len(actions)) model = Sequential([ LSTM(64, return_sequences=False, input_shape=(1, 63)), Dropout(0.3), Dense(32, activation='relu'), Dense(len(actions), activation='softmax') ]) model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy']) model.fit(X, y, epochs=100, batch_size=16, validation_split=0.2) model.save("models/static_model_lstm.h5")逻辑说明:reshape(-1, 1, 63)把每个样本变成时间步为 1 的序列,这是静态手势能塞进 LSTM 的关键。Dropout(0.3)防过拟合,样本少的时候尤其重要。epochs=100配合validation_split=0.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_hands=1, min_detection_confidence=0.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, verbose=0) 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())的顺序完全一致,这是最常见的翻车点——顺序错了,模型输出索引对不上标签,识别结果全乱。verbose=0关掉每帧的预测日志,不然控制台刷屏。
参数怎么改: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(f"dataset/dynamic/{ACTION}/{count}.npy", np.array(sequence)) count += 1 recording = False print("saved", count)逻辑说明:SEQUENCE_LENGTH=30是序列长度,采太短动作信息不全,采太长模型难训且推理延迟高。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, activation='relu'), Dense(num_classes, activation='softmax') ]) model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy']) return model逻辑说明:LSTM(64)和GRU(64)的 64 是隐藏单元数,样本量小可以降到 32,样本多可以升到 128。return_sequences=False表示只取最后一个时间步的输出做分类,这是序列分类的标准写法。
对比经验: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_hands=1, min_detection_confidence=0.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, verbose=0) return actions[np.argmax(pred)] gr.Interface(fn=predict, inputs="image", outputs="text").launch()逻辑说明:这里为了演示简单,把单帧复制 30 次凑成序列,实际动态识别应该用队列缓存最近 30 帧。launch()默认起在本地 7860 端口,加share=True能生成临时公网链接,但答辩现场网络不稳,建议本地跑。
参数怎么改:inputs="image"可以换成"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_port=7861)换端口,或者先netstat查一下谁占着。
6. 进阶技巧:用训练日志曲线反推模型是否值得继续训
拿到这份源码后,很多人跑通就停了,其实logs目录那几张训练曲线图才是写论文的富矿。我的习惯是:每次训练完先把 loss 和 accuracy 曲线画出来,横轴 epoch、纵轴指标,训练集和验证集两条线放一起看。如果训练 loss 一直降、验证 loss 先降后升,那就是过拟合,加 Dropout 或加数据;如果两条线都平着不降,那是学习率太大或模型容量不够。
具体做法是在训练脚本里加一段回调,把每个 epoch 的指标存下来:
import matplotlib.pyplot as plt history = model.fit(X, y, epochs=100, validation_split=0.2) plt.plot(history.history['loss'], label='train_loss') plt.plot(history.history['val_loss'], label='val_loss') plt.plot(history.history['accuracy'], label='train_acc') plt.plot(history.history['val_accuracy'], label='val_acc') plt.legend() plt.savefig("logs/my_train_curve.png", dpi=150)逻辑说明:dpi=150保证论文里插图清晰,legend()必须有,不然审阅老师分不清哪条线是哪个。资源里现成的 png 可以直接对比,但自己重训一遍再画,答辩时讲起来更有底气。
还有一个技巧是拿dynamic_model_lstm_258和dynamic_model_gru_258做交叉验证:同一批测试数据分别喂给两个模型,统计各自混淆矩阵。如果某个类别在两个模型上都错,那大概率是数据采集阶段这个手势样本太少或动作不规范,回去补采比调模型有效得多。我当初就是靠这个发现「谢谢」和「再见」两个手势在动态序列里前 10 帧几乎一样,后来把序列长度从 30 降到 20,只保留区分度高的后半段,准确率直接涨了 8 个点。
从那以后我每次拿到新的时序识别项目,都强制先跑一遍混淆矩阵再谈调参,不然就是盲人摸象。希望这份拆解帮到你,把这份源码真正跑成你自己的东西。
本文还有配套的精品资源,点击获取