简介:毕设项目《基于手势识别的人机交互系统》Python源码包,面向计算机、人工智能、通信工程、自动化等专业的学生及毕业设计人群。系统将MFF运动融合帧深度学习方法与传统视觉手势识别算法结合,实现较准确的手势识别与非接触式体感交互,可作为课程设计、毕业设计或初期立项演示的基础方案。压缩包共49个文件,以39个Python脚本为核心,覆盖数据集预处理、模型训练与推理、指令通信、界面控制与一键运行等模块;另含4个Markdown说明文档、2张网络架构与流程图、使用说明文本及环境依赖文件,整体约431KB,目录划分清晰,便于按模块阅读理解。目前已有164人学习下载。代码经作者测试运行成功,答辩评审平均分96分,并配有文档说明与数据集处理流程,涵盖视频切帧、标签生成与训练验证集划分等关键环节;读者既可快速复现手势识别系统,也可基于现有模块修改扩展,实现更多交互功能。
1. 手势识别人机交互:从MFF运动融合帧开始的毕设路线
做体感交互的人都知道,手势识别最怕的不是模型不够深,而是把每一帧单独拿出来分类。手在快速移动或轻微模糊时,单帧信息严重不足,误判率立刻被拉高。这个毕设项目给的解法比较直接:用MFF运动融合帧把一段视频里的时间维运动信息压进一张图,再交给深度学习分类器,同时保留传统视觉的手势轨迹校验,双路判断之后才输出指令。整条链路覆盖数据预处理、模型训练、socket通信、UI控制和PPT翻页,适合计算机视觉方向毕业设计、HCI课程设计,也适合想快速验证"手势控制鼠标和PPT"的工程实践。项目代码结构按数据处理、模型、界面、通信四个包拆开,只用Python和PyTorch就能跑通,下面按我拆代码的顺序来梳理。
2. dataset_process数据管线:视频切帧、标签生成与训练集划分
2.1 MFF融合帧的生成逻辑:把时序运动压进单张图
先说清楚MFF运动融合帧在做什么。摄像头采集到的连续帧里,手势的运动信息分布在时间轴上,单帧图像只能表达某一时刻的空间位置。如果直接拿单帧训练分类器,模型能记住的只有"手放在哪里",记不住"手从哪边划过来"。MFF的做法是取一个时间窗口内的多帧,先做相邻帧差分,保留运动轮廓,再把差分结果按时间衰减权重叠加到一张图上,最终输出一张"运动能量图"。
dataset_process目录下的deal_video.py和deal_video_tem.py就是干这件事的。核心逻辑可以用下面这段伪Python概括:
import cv2 import numpy as np def build_mff_frame(video_path, window_size=5, decay=0.8): cap = cv2.VideoCapture(video_path) frames = [] while True: ret, frame = cap.read() if not ret: break gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) frames.append(gray) if len(frames) > window_size: frames.pop(0) if len(frames) >= 2: # 相邻帧差分,提取运动区域 diff = cv2.absdiff(frames[-1], frames[-2]) # 运动图叠加,越早的帧权重越低 if 'motion_map' not in locals(): motion_map = diff.astype(np.float32) else: motion_map = motion_map * decay + diff.astype(np.float32) _, motion_map = cv2.threshold(motion_map, 25, 255, cv2.THRESH_BINARY) cap.release() return motion_map.astype(np.uint8)这段代码里window_size控制融合帧的时间跨度,窗口越大,一次能表达的手势轨迹越长,代价是相邻两个不同手势的边界会被模糊掉;decay是历史帧的衰减因子,值越大,运动残影留得越久。阈值25是用来去掉传感器噪声的,如果你用的是USB摄像头而不是笔记本内置摄像头,这个阈值可能需要抬到30到40,因为外接摄像头的暗部噪声通常更大。MFF把原本需要LSTM或3D卷积才能处理的时间维度问题,降维成了单帧图像的分类问题,这是整个毕设能跑得动的关键。
2.2 dataset_process脚本职责:哪些先跑,哪些可以跳过
这个目录下的脚本不是都要挨个执行,它们对应不同的数据准备场景。用表格拆开看会更清楚:
| 脚本 | 作用 | 什么情况下运行 |
|---|---|---|
| deal_video_no_cut.py | 直接对完整视频生成MFF帧,不切片段 | 单个手势视频本身就是一次完整动作时 |
| deal_video_cut.py | 按动作起止切分视频后再生成MFF帧 | 一个视频里录了多个手势,需要分段 |
| deal_video_adjust.py | 调整视频亮度、对比度后再处理 | 采集环境光照不稳定时 |
| process_dataset.py | 遍历整个目录,统一调用MFF生成逻辑 | 数据集批量处理的唯一入口 |
| get_label.py | 读取目录名或文件名,生成对应的标签文件 | 每次新增手势类别后 |
| choose_train_validation.py | 按比例切分训练集和验证集 | 数据全部处理完之后 |
批量处理数据集的入口是process_dataset.py,我在自己跑的时候习惯先把原始视频按手势类别放到单独目录里,目录名就是标签,比如0到9的目录对应0到9号手势,然后执行:
python process_dataset.py --data_root ./dataset/raw --output ./dataset/mff --window_size 5 --decay 0.8跑完之后检查输出目录结构,每个类别的MFF帧会落在对应子目录下。中途如果某个视频解码失败,进程会报一个跳过警告,不会中断整批任务,这对几百段视频的数据集来说很实用。数据量大时不要一次性把所有视频塞进一个目录,Python的os.listdir在文件数量上万后遍历会明显变慢,按类别分子目录能省不少时间。
2.3 标签生成与训练验证划分的注意点
get_label.py做的事本质上是把目录名映射成整数ID,并输出一个JSON或CSV格式的映射文件。独热编码和交叉熵损失都要求标签从0开始连续编号,所以不要出现"手势1"和"手势01"这种重复表达。choose_train_validation.py会按比例划分数据集,常见做法是训练集占80%,验证集占10%,测试集占10%。
划分时有一个容易踩的坑:不要把同一个视频的连续帧同时分进训练集和验证集。MFF融合帧虽然已经压缩了时序信息,但相邻窗口生成的图仍然高度相似,这些帧一旦跨集合,验证集就失去了意义。choose_train_validation.py在处理时会按视频文件分组再随机打乱,保证同一个视频的所有帧只落在同一个集合里。如果你要复现,最稳妥的做法是先检查划分结果里有没有同一个视频名的帧出现在两个集合中。
3. gesture_recognition:MLP分类器如何融合肤色与运动特征
3.1 目录结构与模型选型:为什么是MLP而不是CNN
gesture_recognition包的几个核心文件值得先说清楚。MLPmodule.py是分类器主体,gesture_system.py负责组装"特征提取+分类"的完整流程,models.py里放了网络结构的备选方案,opts.py统一管理训练超参数,datasets.py和datasets_video.py分别是图像数据集和视频数据集的加载器。
MLP在全连接层中接收的特征向量由两部分拼接而成:一部分是MFF融合帧下采样后的运动特征,另一部分是肤色检测得到的HSV空间统计特征。选择MLP而不是直接上CNN,是因为MFF已经把运动信息压缩进单帧,后续不需要再进行复杂的空间特征提取,MLP在参数量和推理速度上有优势。实验时如果想换ResNet这类backbone,把features的提取函数替换掉就行,gesture_system.py外层的接口不需要动。
MLPmodule.py里网络结构大致是这样的:
import torch.nn as nn class MLPModule(nn.Module): def __init__(self, input_dim, hidden_dims=[256, 128], num_classes=10): super().__init__() layers = [] prev = input_dim for h in hidden_dims: layers.append(nn.Linear(prev, h)) layers.append(nn.BatchNorm1d(h)) layers.append(nn.ReLU()) layers.append(nn.Dropout(0.3)) prev = h layers.append(nn.Linear(prev, num_classes)) self.net = nn.Sequential(*layers) def forward(self, x): return self.net(x)网络结构上,hidden_dims定义了两个隐藏层,第一层256个神经元,第二层128个。BatchNorm1d的作用是让每一层的输入分布相对稳定,这样学习率可以适度调大,Dropout设成0.3是为了防止在中等规模数据集上过拟合。input_dim这个参数由特征向量的维度决定,如果你的特征不是来自HSV统计量而是换成了其他描述子,需要在实例化时同步修改。
3.2 opts.py超参数与特征提取的配合
optspy里面常见的训练参数可以参考下面这张表,我已经按实际跑通的方式给了建议值:
| 参数 | 建议值 | 说明 |
|---|---|---|
| lr | 1e-3 | Adam优化器下够用,下降到平台期后乘0.1 |
| bsz | 64 | 单卡显存小于4G时改32 |
| epochs | 30 | MFF特征简单,30轮基本收敛 |
| num_classes | 10 | 对应0到9手势,按实际类别改 |
| input_dim | 128 | MFF特征和肤色特征的拼接维度 |
gesture_system.py里特征提取这一段是传统视觉和深度学习的交汇点。肤色检测一般用HSV色彩空间,把手势从背景里分离出来。常见范围是H在0到25或170到180之间,S大于60,V大于40。这个范围在黄种人肤色上表现尚可,但换了光照环境一定要重新标定:
import cv2 def extract_skin_features(mff_frame): hsv = cv2.cvtColor(mff_frame, cv2.COLOR_BGR2HSV) mask = cv2.inRange(hsv, (0, 60, 40), (25, 255, 255)) mask |= cv2.inRange(hsv, (170, 60, 40), (180, 255, 255)) # 形态学开闭运算去噪 kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) # 计算肤色区域比例和质心偏移作为补充特征 ratio = mask.sum() / mask.size moments = cv2.moments(mask) if moments["m00"] > 0: cx = moments["m10"] / moments["m00"] cy = moments["m01"] / moments["m00"] else: cx, cy = 0.0, 0.0 return [ratio, cx, cy]这段代码把肤色区域面积占比、质心横坐标和纵坐标压缩成了一个三维补充特征,拼接到MFF特征向量后面。注意第二段inRange是专门处理深红色和暗光下的肤色偏移的,如果你的采集环境灯光偏暖,这段不能省。每次改完HSV范围后,建议把mask用cv2.imwrite导出来看几帧,确认没有把背景里的木色家具或红色物体误判成手。
3.3 训练流程与实验记录的取舍
训练入口在experiment.py,它会加载datasets.py构造的训练集和验证集。训练命令大致是这样:
python experiment.py --model mlp --lr 0.001 --epochs 30 --batch_size 64 --dataset_root ./dataset/mff每轮训练结束会打印准确率、损失值和当前学习率,模型权重保存到checkpoints目录。我一般只看两个指标:验证集准确率和训练集验证集的损失差值,前者低于0.95说明特征提取部分有问题,后者超出0.1说明过拟合,需要增大Dropout或加数据增强。
数据集增强方面,transforms.py里提供了随机旋转、平移和水平翻转。注意不要加竖直翻转,因为手势语义里上下往往是有方向性的,比如"向上挥手"和"向下挥手"翻转后就混淆了。这一点在代码注释里特意标注过,属于容易忽视但直接影响识别精度的细节。
4. communication与UI:识别结果如何变成PPT翻页指令
4.1 run_all主流程:摄像头采集、识别、发送如何串联
run_all.py是整套系统的主入口,它把摄像头采集、手势识别、Socket通信和UI刷新四个环节串成一条流水线。每帧图像经过gesture_system.py得到类别ID和置信度,置信度高于阈值才进入指令映射模块,否则丢弃。这样做的好处是误判不会触发无意义的PPT翻页或鼠标移动。
主流程的调度逻辑我在复现时重新梳理了一遍,大致是:
import cv2 def main(): cap = cv2.VideoCapture(0) gesture_sys = load_gesture_system() sender = CommandSender(port=8888) while True: ret, frame = cap.read() if not ret: break label, conf = gesture_sys.predict(frame) if conf > 0.85: command = map_label_to_command(label) sender.send(command) cv2.imshow("gesture_ui", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release()这里的置信度阈值0.85是个经验值,我测试下来比较平衡。阈值调低到0.7,响应变快但PPT容易误翻页;调高到0.95,误触减少,但手停在半空想触发命令时经常没反应。摄像头索引0是笔记本内置摄像头,外接USB摄像头把参数改成1或2,这个在代码里是硬编码的,换设备时记得改。主循环里waitKey(1)控制帧率上限大约30FPS,CPU模式下识别耗时高时,实际帧率会自然降到15FPS左右,不影响交互。
4.2 send.py与receive.py:Socket通信和指令格式
communication包把识别端和受控端解耦,send.py运行在识别主机上,receive.py运行在被控端,两边通过局域网UDP通信。UDP丢包对PPT控制这类指令场景影响不大,反而比TCP更不容易卡界面。send.py发送的指令格式是JSON字符串,包含时间戳、手势类别和置信度:
import json import socket import time class CommandSender: def __init__(self, ip="127.0.0.1", port=8888): self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.addr = (ip, port) def send(self, command, conf): msg = json.dumps({ "ts": time.time(), "cmd": command, "conf": conf }) self.sock.sendto(msg.encode("utf-8"), self.addr)receive.py这边监听同一个端口,解析JSON后根据cmd字段执行动作。指令映射表是整个交互逻辑的核心,下面给一份默认映射:
| 手势类别ID | 指令 | 动作 |
|---|---|---|
| 0 | next_page | PPT下一页 |
| 1 | prev_page | PPT上一页 |
| 2 | mouse_up | 鼠标上移 |
| 3 | mouse_down | 鼠标下移 |
| 4 | mouse_left | 鼠标左移 |
| 5 | mouse_right | 鼠标右移 |
| 6 | left_click | 鼠标左键单击 |
| 7 | right_click | 鼠标右键单击 |
| 8 | scale_up | 放大 |
| 9 | scale_down | 缩小 |
ip地址默认是127.0.0.1,意思是识别端和被控端在同一个台机器上。如果你要用一台电脑识别、另一台电脑显示PPT,send.py这行就要改成被控端的局域网IP。跨机器传输时还有一个注意点:windows防火墙默认会拦截UDP入站流量,需要在高级设置里放行对应端口,否则receive.py永远收不到数据,这个问题在实验室环境里特别常见。
4.3 show_UI与ppt_control:界面层和控制层的配合
show_UI目录用的是PyQt或类似方案封装的一个轻量界面,imageview_control.py负责把摄像头画面实时渲染到控件上,state_show.py显示当前识别状态和置信度,main_control.py是界面主控制器。UI层不参与手势识别逻辑,只负责展示,这层拆分的意义在于你可以在不打开界面的情况下调用gesture_recognition做算法实验。
ppt_control.py调用win32com或pyautogui这类库模拟键盘和鼠标事件。PyAutoGUI操作PPT时需要注意当前焦点窗口,如果演示窗口没有置顶,按键事件会落到其他应用上。一个稳一点的写法是调用PPT的OLE接口直接控制,但复杂度会增加不少。毕设答辩演示场景下,把keep on top插件打开再跑pyautogui是最省事的方案。代码里没有硬编码这一层,接口留在了ppt_control.py里,换成OLE方式不用动其他模块。
5. 训练自有手势数据集的流程与mediapipe交叉验证
5.1 从采集到训练的完整命令序列
拿到源码后最值得做的事,是采集自己的手势数据重新训练一遍,而不是直接用别人训好的权重。流程按四步走:采集视频、生成MFF融合帧、划分数据集、训练模型。
# 1. 采集每个手势的视频,按类别放好 mkdir -p dataset/raw/gesture_0 dataset/raw/gesture_1 # 2. 批量生成MFF帧 python dataset_process/process_dataset.py --data_root ./dataset/raw --output ./dataset/mff # 3. 划分训练和验证集 python dataset_process/choose_train_validation.py --dataset ./dataset/mff --ratio 0.8 # 4. 训练MLP分类器 python gesture_recognition/experiment.py --window_size 5 --epochs 30 --batch_size 64采集时每个手势至少录3组视频,每组5秒以上,手的运动幅度要有区分:大范围滑动和原地比划分开录,这能让MFF融合帧学到的运动模式更丰富。如果录出来的视频里有明显的手势与背景同色区域,先用deal_video_adjust.py做一次色彩均衡,再进process_dataset.py。
5.2 用mediapipe手部关键点标记校验标签质量
数据集质量直接影响识别效果,一个快速且廉价的校验方式是引入mediapipe手部关键点检测,对原始视频逐帧打出手部骨架,观察标签和手势形态是否匹配。mediapipe的解决方案在嵌入式和PC端都跑得动,也不需要额外训练:
import cv2 import mediapipe as mp mp_hands = mp.solutions.hands hands = mp_hands.Hands(max_num_hands=1, min_detection_confidence=0.5) cap = cv2.VideoCapture("./dataset/raw/gesture_0/01.mp4") while cap.isOpened(): ret, frame = cap.read() if not ret: break rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results = hands.process(rgb) if results.multi_hand_landmarks: for lm in results.multi_hand_landmarks[0].landmark: x, y = int(lm.x * frame.shape[1]), int(lm.y * frame.shape[0]) cv2.circle(frame, (x, y), 3, (0, 255, 0), -1) cv2.imshow("mediapipe_hand", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release()这段脚本不需要改训练代码,只是用来做数据标注的快速预览。mediapipe打出来的21个关键点如果和目标手势在语义上对不上,比如0到9手势识别任务里应当握拳的样本伸出了手指,那就说明录制或切帧环节出了问题。媒体管道的关键点坐标也可以作为额外特征输入MLP,但需要把归一化坐标拼进特征向量,输入维度同步修改,否则模型加载时会报维度不匹配。验证无误后,主训练流程没有任何改动要求,可以稳定复现。
5.3 路径硬编码、CPU推理与模型加载的常见坑
最后一个实用建议是排查三个高频问题。第一,代码里多处用了相对路径,直接从项目根目录之外的地方执行脚本会因为找不到label_mapping.json或checkpoints目录被中断,统一用os.path.join(os.path.dirname(file), ...)把路径锚定到脚本所在目录。第二,MLP推理在CPU上单帧约5到10毫秒,瓶颈通常在MFF的帧间差分和肤色掩膜计算上,可以先用cv2.resize把输入压到240乘240再进特征提取,精度损失很小但帧率能提升一大截。第三,requirements.txt里的PyTorch默认是CPU版,有NVIDIA显卡的同学需要按CUDA版本重新安装torch,这个顺序做反了会导致训练时CUDA不可用而静默退回CPU模式,一个epoch时间翻好几倍。
本文还有配套的精品资源,点击获取