简介:这份基于python的手势识别人机交互系统源码,是一套获评98分的课程设计项目,主要面向计算机专业正在完成课程设计、期末大作业的学生,也适合需要完整项目实战经验的开发者。系统借助摄像头捕捉手部动作,实现界面控制、演示翻页等人机交互功能,覆盖从数据预处理、模型构建到交互控制的完整开发流程。压缩包共50个文件,以39个py源码为主要实现载体,涵盖手势识别、界面交互、数据集处理、通信传输等独立子模块;另有4个md文档、2张jpg架构图,以及txt、license、dll等辅助配置,整个资源包约433KB。项目目录结构清晰,便于围绕课程设计或工程实战逐模块学习与二次开发。已有244人学习下载。借助这份源码,使用者能快速理解整个系统架构,获得经过验证的代码实现、数据处理思路与模块集成方案,为课程设计或项目实战提供直接参考。
1. 手势识别人机交互:一个能靠手势完成 PPT 翻页与图片浏览的课程设计项目
课程设计里“基于手势识别的人机交互”是那种一听很唬人、一选就后悔的方向——网上教程九成只教你训一个简单分类器,跑起来没问题,真拿去演示撑不过三分钟。这个项目源码不一样,它把一份课程设计拆成了数据清洗、模型训练、实时推断、通信、UI 控制五层,最终落成两个可演示的场景:手指左右滑动切换 PPT 页面,通过握拳和张开来控制图片浏览。如果你正在做课程设计、期末大作业,或者想找一个真能跑起来、能扩展的人机交互工程,这份源码值得一层层拆开看。下面我会从目录结构讲起,把手势识别怎么训练、怎么通信、怎么接到控制端,一直讲到二次开发时从哪里下手改。
2. 系统架构:从摄像头画面到 PPT 翻页的完整链路
2.1 五个模块,一条完整流水线
拿到源码之后,根目录叫gesture_interaction,往下挂着五个业务模块,对应从“原始视频”到“最终控制效果”的五段旅程。先放一张目录级映射表,方便对照后面要讲的每一层:
| 顶层目录 | 负责的事情 | 关键文件 |
|---|---|---|
| gesture_interaction/gesture_recognition | 模型定义、数据加载、训练、推理入口 | MLPmodule.py、models.py、dataset.py、datasets_video.py、transforms.py、opts.py、main.py |
| dataset_process | 原始视频清洗、切帧、标注、划分 | cutVideo.py、get_label.py、deal_video.py、choose_train_validation.py、check_datasets.py |
| communication | 跨进程与跨设备通信 | send.py、receive.py |
| show_UI | 交互控制端,负责 PPT 和图片控制 | main_control.py、ppt_control.py、imageview_control.py、state_show.py |
| show_prepare | 环境准备与冒烟测试 | show_prepare.py、test.py |
这个分层方式很值得学习:训练、数据处理、通信、界面控制在物理上分成不同目录,意味着每一层都能单独替换。你不想用它的 UI,可以只保留gesture_recognition的模型和communication的收发协议;你不想用它的数据集脚本,也可以换成自己的数据来源,模型训练入口完全不需要改动。
2.2 数据流:从原始视频到动作被执行
整个系统跑起来之后,数据是单向流动的。我用文字把这个链路描述出来,你对照目录里的文件看就会很清楚:
- 摄像头或提前录好的手势视频,先进入
dataset_process那一堆deal_video*.py脚本,被切成单帧图片,并配上手势类别标签,形成训练集。 gesture_recognition里的dataset.py和datasets_video.py负责把图片帧读进 PyTorch 的 Dataset 结构,transforms.py做数据增强,main.py启动训练,最终产出一份模型权重。- 部署阶段,摄像头实时采集画面,
gesture_system.py加载模型权重,对每一帧画面做推理,输出一个手势类别 ID。 - 这个类别 ID 通过
communication/send.py发出去,show_UI里的receive.py收到消息后,交给main_control.py分发。 main_control.py根据配置好的映射关系,调用ppt_control.py模拟键盘翻页,或调用imageview_control.py切换图片,同时state_show.py把当前状态显示到界面上。
这条链路最巧妙的地方在第四步:识别结果不直接调用 UI,而是走了一层通信。你完全可以把show_UI跑在另一台电脑上,或者换成任何自己写的客户端。通信层相当于给整个系统加了一个解耦口,这也是它能从课程设计变成可扩展工程的关键原因。
2.3 目录里容易被忽略的隐藏设计
第一次看这个目录时,有几个文件容易被当成杂项跳过,其实它们藏着设计意图:
gesture_location_system.py是手势定位模块,不只是分类,而是先框出手部区域,再在区域内做手势类别判断。MLPmodule.py单独拆出来,说明模型采用的是“特征提取 + MLP 分类头”的结构,后面第四章我会展开讲。.gitmodules说明项目曾经引用过外部子模块,而old_interface是旧版接口目录,对比新旧代码能看到功能演进过程的痕迹。
motion_fused_frames.jpg和network_arch.jpg是训练过程的可视化产物,前者展示多帧融合后的运动信息,后者是网络结构图。对写课程设计文档来说,这两张图直接放进论文就是现成的实验结果展示材料,省去了重新画图的时间。
3. 数据集处理:从原始视频到可训练样本的清洗流水线
3.1 视频切帧与文件整理,一个脚本搞定一半工作量
dataset_process目录下cutVideo.py、deal_video.py、deal_video_no_cut.py、deal_video_tem.py、deal_video_adjust.py这一串脚本,看着命名随意,其实分别对应不同的数据处理策略:切帧处理、不切帧直接处理、带模板处理、做尺寸或亮度调整。核心思路只有一个——把连续的视频变成模型能吃到的离散图片帧。
cutVideo.py是这里最基础的脚本,核心逻辑用 OpenCV 读取视频流,每隔一定帧数抽一帧保存为图片。代码示意大致如下:
import cv2 import os def cut_video(video_path, output_dir, interval=3): cap = cv2.VideoCapture(video_path) if not cap.isOpened(): print(f"无法打开视频: {video_path}") return os.makedirs(output_dir, exist_ok=True) frame_count = 0 saved_count = 0 while True: ret, frame = cap.read() if not ret: break if frame_count % interval == 0: filename = os.path.join(output_dir, f"frame_{saved_count:05d}.jpg") cv2.imwrite(filename, frame) saved_count += 1 frame_count += 1 cap.release() print(f"共处理 {frame_count} 帧,保存 {saved_count} 张图片")这里interval是抽帧间隔,间隔越小,同一段视频产出的样本越多,但相邻帧高度相似,容易造成训练集冗余。我一般会把间隔设在 2 到 5 之间,让同类别样本有足够多样性,又不会让数据量爆炸。保存的图片命名统一用五位数字补零,方便后面排序读取。
copyVideo.py负责把散落在不同文件夹里的原始视频复制到一个统一目录,避免训练脚本在数据路径上踩坑。这个脚本看着简单,实际作用很大,因为手势数据往往要在不同时间、不同环境录好几批,统一路径管理能省掉很多后续排查的时间。
3.2 打标签与数据集划分:避免数据泄漏的关键
数据切完帧之后,紧接着要解决的是标签问题。get_label.py负责生成图片路径与类别 ID 的对应关系。常见做法是生成一个 CSV 标注文件,每一行是图片路径和标签数字,下面这个示例结构基本能代表它的输出:
data/train/rock/frame_00001.jpg,0 data/train/rock/frame_00002.jpg,0 data/train/open_palm/frame_00001.jpg,1 data/train/open_palm/frame_00002.jpg,1标签怎么和视频对应,取决于录制时的组织方式。常见的做法是每录制一个动作放进一个文件夹,文件夹名就是手势名称,get_label.py遍历文件夹生成映射表,再按名称排序映射成整数 ID。整个过程最关键的一个点:标签文件生成之后,一定要抽样看几张图片确认标签没贴错,否则后面模型训出来所有数字都是错的,这个问题越到后面越难排查。
choose_train_validation.py是我建议重点读的脚本,因为它处理了一个新手经常会踩数据泄漏的坑。如果先把所有视频切帧,再随机把帧图片划分成训练集和验证集,那同一段视频的连续帧很可能同时出现在两个集合里,验证集的准确率会虚高。正确做法是先按视频文件划分,一部分视频整体进训练集,另一部分整体进验证集,再分别切帧。这个脚本做的事就是先读视频列表,按比例划分,把划分结果写进配置文件,供后续切帧和训练共用。
3.3 数据质量检查:清洗花的时间往往比训练还长
check_datasets.py负责做数据集体检,重点检查三件事:图片能否正常打开、图片尺寸是否一致、每个类别的样本数量是否均衡。代码逻辑一般是遍历标注文件里所有图片路径,用 OpenCV 读取并判断,如果读取失败就打印损坏文件路径,同时按类别统计样本数。
这类脚本不需要复杂逻辑,但执行一次的价值非常高。我见过太多课程设计项目,训练一半报错,最后发现是某张图片文件损坏或者尺寸不一致导致的。传输中断、手机竖屏拍摄都会产生这类脏数据,早检查早省心。
至于类别均衡性,我的经验是:如果某个手势的样本数明显少于其他类别,先不要急着调损失函数,优先去补录一段该手势的视频,用copyVideo.py收进来再跑一遍切帧。数据补录通常比调参的效果来得更直接。
4. 手势识别核心:MLP 模型、训练实验与实时推断
4.1 数据集类与数据增强,PyTorch 的标准姿势
gesture_recognition/dataset.py继承torch.utils.data.Dataset,核心职责是把标注 CSV 里的图片路径和标签转成训练时用的样本。常见的实现逻辑是初始化时读取全部标注,存成(路径, 标签)的列表,__getitem__里按索引读图、做增强、返回图片和张量标签。代码大致长这样:
import torch from torch.utils.data import Dataset from PIL import Image class GestureDataset(Dataset): def __init__(self, label_file, transform=None): self.samples = [] self.transform = transform with open(label_file, 'r') as f: for line in f: path, label = line.strip().split(',') self.samples.append((path, int(label))) def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label = self.samples[idx] image = Image.open(path).convert('RGB') if self.transform: image = self.transform(image) return image, labeltransform参数把数据增强和数据集类解耦了,训练时可以传带随机翻转、色彩抖动的增强组合,验证时传只做尺寸调整和归一化的组合。datasets_video.py额外扩展了视频级别的读取逻辑,一次返回一小段连续帧,这是考虑到手势是动态动作,单帧静态图可能丢失运动信息。
transforms.py则是用 torchvision 的Compose把这些操作串起来,常见的组合是:先 Resize 到 224x224,再做随机水平翻转,接着转成 Tensor,最后用 ImageNet 的均值方差做归一化。
from torchvision import transforms train_transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(10), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])RandomRotation(10)是给手部图像加一点小角度旋转,模拟摄像头角度的轻微变化,这个增强对提升真实场景识别率帮助很大。归一化参数直接用 ImageNet 预训练模型的标准值就行,不用自己算数据集方差,这是个能省不少事的小技巧。
4.2 模型结构:特征提取加 MLP 分类头
MLPmodule.py单独抽出来,说明整个模型走的是“特征提取 + MLP 头”的组合路线。常见做法是:用预训练的 ResNet 作为 Backbone,把图片转成高维特征向量,再用 MLP 层做手势分类。models.py负责把这两段拼起来,核心结构类似:
import torch.nn as nn from torchvision import models class GestureModel(nn.Module): def __init__(self, num_classes=6, pretrained=True): super().__init__() backbone = models.resnet18(pretrained=pretrained) self.features = nn.Sequential(*list(backbone.children())[:-1]) self.flatten = nn.Flatten() self.mlp_head = nn.Sequential( nn.Linear(512, 256), nn.ReLU(), nn.Dropout(0.3), nn.Linear(256, num_classes) ) def forward(self, x): x = self.features(x) x = self.flatten(x) return self.mlp_head(x)num_classes是手势类别数,取决于你在数据集里定义了几类手势,课程设计场景一般 4 到 8 类比较合理,比如左右滑动、握拳、张开手掌、竖起食指等。Dropout(0.3)是为了防止 MLP 头过拟合,因为 Backbone 的特征已经很丰富,全连接层特别容易把训练集死记硬背下来。
这里有个设计细节值得注意:把 Backbone 和分类头拆成两个模块,意味着二次开发时可以只更换 Backbone,比如把 ResNet18 换成 MobileNetV3 来提速,分类头部分完全不用动。gesture_location_system.py则是在模型外面套了一层手部定位逻辑,先通过简单的肤色模型或者目标检测框出手部区域,再把裁剪后的区域送进分类网络,这样分类器不会被复杂背景干扰。
4.3 训练入口与推理脚本,改参数就能复现实验
opts.py是超参数配置中心,把所有实验相关参数集中到一处管理。常见的配置项包括训练轮数、批次大小、学习率、类别数、数据路径。main.py是训练入口,整体流程是加载数据、初始化模型、定义损失函数和优化器、开始迭代训练、每个 epoch 结束后在验证集上评估一次,效果更好就保存权重。训练循环的核心代码大概是这样:
import torch import torch.nn as nn device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = GestureModel(num_classes=opts.num_classes).to(device) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=opts.lr) for epoch in range(opts.epochs): model.train() total_loss = 0 for images, labels in train_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) loss = criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() total_loss += loss.item() # 每个 epoch 后验证一次并保存最优权重 val_acc = evaluate(model, val_loader) if val_acc > best_acc: torch.save(model.state_dict(), 'best_model.pt')experiment目录存的就是每次实验的日志、训练曲线和最终权重,多跑几组对比实验时,这个目录能帮你理清哪组参数效果好。run_deal_video.py是推理脚本,加载训练好的权重,对每一帧图像做预测,输出每个手势类别的置信度。推理时通常还会对连续几帧的预测结果做平滑,比如取最近 5 帧投票,避免单帧误判导致 PPT 被误翻页。
训练完之后,gesture_system.py把模型对象、预处理流程、后处理逻辑封装成一个完整的手势识别系统,对外只暴露一个predict(frame) -> action_id接口。这也是整个项目里最值得仿照的设计:训练和部署共用一套模型代码,但通过封装把内部复杂度藏起来,部署端只需要关注输入输出。
5. 避坑记录:视频切帧、训练内存与实时延迟的排查过程
5.1 训练准确率虚高,一到真实场景就翻车
现象:验证集准确率能到 98%,但把模型接到摄像头实时识别时,实际效果很差,频繁误判。原因:数据集划分方式有问题,先切帧再随机划分,导致同一段视频的连续帧同时出现在训练集和验证集,模型相当于开卷考试,验证集成绩虚高。解决:严格按照choose_train_validation.py的思路,先按原始视频文件划分训练集和验证集,再对每个集合单独切帧,保证验证集里的画面和训练集完全不来自同一段视频。
5.2 实时识别时画面掉帧严重,操作明显卡顿
现象:摄像头预览很流畅,但打开识别功能后画面变成幻灯片,挥手要等一两秒才有反应。原因:每一帧都执行完整模型推理,而 30 帧摄像头加上 224x224 输入的模型推理,耗时超过 30 毫秒,主循环被推理阻塞。解决:做跳帧推理,每 2 到 3 帧取一帧做识别;同时把推理放到独立线程,用队列把识别结果传给 UI 线程;条件允许的话,把输入尺寸从 224 缩到 112,速度能提升一到两倍,手势识别这个任务精度损失不大。
5.3 中文路径下图片读取失败,训练中途报错
现象:训练到一半,Image.open报文件找不到,但打开路径看文件明明存在。原因:Windows 中文用户名路径下,OpenCV 的imread在部分环境不支持非 ASCII 路径,返回空对象,而代码没做空值判断。解决:统一把所有数据放在纯英文路径下,比如D:\gesture_data\;如果项目代码里用的是cv2.imread,可以改用 PIL 读图再转成 numpy 数组,PIL 对中文路径兼容性更好。
5.4 某个手势的准确率特别低,其他手势都正常
现象:六个手势里五个都能到 90% 以上,唯独“握拳”只有 50% 左右,而且误判都是识别成“张开手掌”。原因:类别样本不均衡,或者两个动作在少量样本下特征差异不明显。解决:先跑check_datasets.py看每个类别的样本数量,如果数据量差距明显,优先补录对应手势的视频,而不是急着加权损失函数;确认数据量没问题后,再给分类损失加上类别权重,torch.nn.CrossEntropyLoss支持直接传weight参数。
6. 二次开发:把手势识别模块接进你的下一个应用
6.1 把手势类别映射成你自己的动作
这个项目的 UI 层把手势类别 ID 和动作绑定了,改造时最省事的方法就是改映射表。翻一下show_UI里的main_control.py,你会看到类似的结构:收到一个手势 ID,查表得到动作名称,再执行对应控制函数。我建议你按下面这个思路改:
ACTION_MAP = { 0: ('next_page', ppt_control.next_page), 1: ('prev_page', ppt_control.prev_page), 2: ('zoom_in', imageview_control.zoom_in), 3: ('zoom_out', imageview_control.zoom_out), } def handle_action(action_id): action_name, handler = ACTION_MAP.get(action_id, (None, None)) if handler: handler() print(f"执行动作: {action_name}")把动作名和控制函数本身也放进映射表,以后加新手势只需要加一行,不需要动分发逻辑。
6.2 一个实用的接口约束习惯
接手这个项目做二次开发时,我给自己定了一个约束:所有外部调用只通过gesture_system.py暴露的predict接口,前端永远不直接接触模型对象。这个习惯让我后面替换模型时省了大力气,我把 ResNet 换成更轻量的 MobileNet 时,只需要改模型的初始化部分,接口的输入输出完全不变,UI 端的控制逻辑一行都没动。
从那以后,我每次接手带模型的项目,都先把推理接口的参数类型、返回格式固定下来,再动内部实现。希望这篇拆解能帮你在课程设计或项目实践里少走几步弯路。
本文还有配套的精品资源,点击获取