基于MLP的手势识别系统:从数据集处理到实时人机交互
2026/9/12 6:33:15 网站建设 项目流程

简介:面向计算机视觉与人机交互的Python源码工程,实现基于手势识别的交互系统,适合计算机相关专业学生用于课程设计、期末大作业或实战练习。压缩包共50个文件,其中39个Python脚本为核心代码,4个Markdown文档说明环境与使用,2张示例图片展示识别效果,另有许可、配置类文件;整体仅433KB,目录结构清晰,代码注释友好,便于阅读复用。工程覆盖图像采集、预处理、手势识别、命令转换与UI界面等完整流程,涉及OpenCV、深度学习等算法知识,可通过摄像头实时捕捉手部动作并映射为鼠标移动、滚动或点击等系统指令。目前已有55人学习/浏览;配套说明文档与示例图片有助于理解项目结构和识别流程,在此工程上可替换模型或扩展控制功能,适合作为人机交互、模式识别方向的项目实践起点。

1. 手势识别的瓶颈在帧加工,不在模型

前几天帮同事调一个人机交互 demo,模型在离线测试集上准确率已经 99%,但一接摄像头就不断误触发。查到最后发现是训练用的视频切片没有对齐手势起始帧,模型学到的是“模糊的肤色块”而不是“手势形状”。这个 Python 项目把完整流程拆成了 dataset_process、gesture_recognition、show_UI 等模块,从原始摄像头视频到鼠标、PPT 控制都能跑通。对做课程设计、期末大作业或者想快速搭建交互原型的开发者,比起从零攒模型,更值得先理解它的数据管线如何组织。读完这一篇,你可以知道每个命令在干什么,也能避开我同事踩过的那个坑。

2. 数据集处理与 MLP 分类:先把训练数据变成可复现的样本

2.1 模块拆分与数据流

首先看dataset_process目录下的文件,它们负责把零散的视频和图片变成choose_train_validation.py可以直接用于训练的文件列表。下表是我对照源码结构梳理的职责划分:

文件功能
deal_video_no_cut.py处理不需要切割的连续动作视频,比如循环挥动
cutVideo.py把视频按时间窗口切成一段段独立片段
deal_video.py主流程,调用抽帧和裁剪,生成统一尺寸的图片样本
deal_video_tem.py / deal_video_adjust.py调整时间窗口和边缘帧,处理切割边界误差
process_dataset.py / check_datasets.py扫描目录、检查样本数量与标签一致性
get_label.py读取子目录名生成标签 ID,保存为 JSON 或 pickle
choose_train_validation.py按比例划分训练集和验证集,避免随机打乱时数据泄露

处理流程通常是:原始视频 ->cutVideo.py切割成片段 ->deal_video.py抽帧 ->get_label.py生成标签 ->choose_train_validation.py划分集合。这个顺序很重要,不能先划分再切割,否则同一段视频的子帧会同时进入训练集和验证集,导致验证指标虚高。我一般会额外写一个assert,确保训练集和验证集中的视频文件名前缀没有交集。另一个容易忽略的点是,deal_video_no_cut.py并不是没用的备胎,它承担了连续手势样本的生成任务,比如“五指张开保持住”这种动作就没有明显的起点和终点,只能靠完整视频直接抽帧。

2.2 抽帧参数与标签生成的坑

deal_video.py里常见的抽帧命令是:

python dataset_process/deal_video.py --video_dir ./raw_videos/num0 --output_dir ./processed/num0 --frame_interval 2 --resize 224 224

--frame_interval表示每隔几帧取一帧。对于静态手势识别,间隔太密会让相邻样本几乎一样,模型学不到变化;间隔太疏又会丢掉关键信息。我一般把 30fps 视频间隔设置为 2,也就是每秒取 15 帧,再配合随机裁剪增广。--resize 224 224是给 MLP 使用的固定输入尺寸,注意这里做的是直接 resize 而不是等比缩放,所以不同原始分辨率的手势会被拉伸,这也是为什么后续需要单独处理 ROI 偏置。

get_label.py会扫描输出目录里的子目录名,比如num0num1,映射成{'0': 0, '1': 1}。这里有一个很隐蔽的坑:目录名不要用带数字前缀的命名,例如digits_0,因为文件系统按字典序读取时digits_10会排在digits_2前面,导致标签错乱。更安全的做法是显式传入类别列表:

python dataset_process/get_label.py --class_names num0 num1 num2 num3 num4 num5 num6 num7 num8 num9 --output label_map.json

这样生成的标签就完全独立于目录遍历顺序。完成这一步后,用check_datasets.py检查每个类别的样本数是否均衡,如果某一类明显偏少,后续训练时要考虑加权采样。项目里choose_train_validation.py通常这样调用:

python dataset_process/choose_train_validation.py --dataset_dir ./processed --val_ratio 0.2 --seed 42

--seed固定之后,多次训练的结果才可复现,这也是课程设计答辩时老师最常追问的点之一。

2.3 MLP 训练:模型结构、优化器与参数解释

gesture_recognition/MLPmodule.py里定义了一个简单多层感知机,输入层是224*224*3 = 150528维向量,中间是两层全连接加 ReLU。对新手来说,这个模型不一定是最优的,但作为课程设计,它有以下好处:不依赖 GPU、训练速度快、参数透明。一个可复现的训练入口是:

python gesture_recognition/MLPmodule.py --data_dir ./processed --epochs 50 --batch_size 32 --lr 1e-4 --dropout 0.3

模型内部写法和我常用的一致:

import torch.nn as nn class MLP(nn.Module): def __init__(self, input_dim=224*224*3, hidden=512, num_classes=10): super().__init__() self.net = nn.Sequential( nn.Linear(input_dim, hidden), nn.ReLU(), nn.Dropout(0.3), nn.Linear(hidden, 128), nn.ReLU(), nn.Linear(128, num_classes) ) def forward(self, x): return self.net(x)

这里hidden=512是一个起点值。如果样本量在几千级别,512 可能过大,会让验证集 loss 出现抖动;降到 256 会更稳。dropout设 0.3 而不是 0.5,是因为中等规模数据下过大的 dropout 会让模型欠拟合。优化器用 Adam 而不是 SGD,主要原因是 MLP 输入是展平的像素,特征尺度差异明显,Adam 对学习率的敏感度更低。训练完成后把model.state_dict()保存为.pth文件,供实时识别模块调用。如果训练过程中发现 loss 不下降,先检查输入数据是否归一化到了[0,1],而不是直接找模型结构的问题。

3. 实时手势定位与模型推理:从摄像头帧到分类标签

3.1 摄像头读取与手部区域裁剪

实时识别入口在gesture_recognition/main.pygesture_location_system.pymain.py负责启动摄像头,gesture_location_system.py负责定位手部并调用模型。下面是一段典型的捕获与轮廓提取代码,很多版本里都保留了这个思路:

import cv2 cap = cv2.VideoCapture(0) while True: ret, frame = cap.read() hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) mask = cv2.inRange(hsv, (0, 30, 60), (20, 150, 255)) mask = cv2.medianBlur(mask, 5) contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if contours: hand = max(contours, key=cv2.contourArea) x, y, w, h = cv2.boundingRect(hand) cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) cv2.imshow("frame", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break

HSV 范围(0, 30, 60)(20, 150, 255)是在实验室正常光照下标定出来的肤色区间,它只对黄种人肤色有效。如果换一个环境,最直接的现象是手部区域检测破碎或包含脸的一部分。这时应该先打印 mask 的直方图,而不是直接在inRange参数上调高阈值。medianBlur的核大小 5 可以滤掉传感器噪点,但核太大会侵蚀掉手指之间的缝隙。这里max(contours, key=cv2.contourArea)表示只保留面积最大的轮廓,能避免背景里其他肤色物体干扰,但代价是当手不在画面里时,模型会把最大面积的脸部区域当作输入。

3.2 固定 ROI 与预处理

检测到 boundingRect 后,不能直接把整个框送入模型。真实项目里手部框经常抖动,如果框大小变化太大,模型的输入分布就漂移。gesture_location_system.py的常见做法是:

center_x, center_y = x + w // 2, y + h // 2 side = max(w, h) * 1.2 x0 = max(0, int(center_x - side // 2)) y0 = max(0, int(center_y - side // 2)) x1 = min(frame.shape[1], int(center_x + side // 2)) y1 = min(frame.shape[0], int(center_y + side // 2)) roi = frame[y0:y1, x0:x1] roi = cv2.resize(roi, (224, 224))

注意side = max(w, h) * 1.2,放大的 1.2 倍让手部不至于占满整个框,否则模型识别时会丢失手指边缘信息。opts.py里会暴露roi_scale参数,我一般设 1.2-1.3,太大了背景占比过高。cv2.resize的默认插值方法是双线性,如果 ROI 被放大得比较多,建议显式指定interpolation=cv2.INTER_CUBIC,这样手指边缘的锯齿会少一些,但这个区别在 MLP 上并不明显,换成更复杂的模型后才需要注意。

3.3 模型推理与后处理

实时推理部分要跟训练时的预处理严格保持一致。通常写在这几个文件里:models.py定义模型结构,transforms.py定义归一化与尺寸变换,gesture_system.py负责串联调用。一个可读的推理封装如下:

import numpy as np import torch from gesture_recognition.models import create_model model = create_model(model_name="mlp", num_classes=10) model.load_state_dict(torch.load("checkpoints/mlp.pth", map_location="cpu")) model.eval() def infer(frame, box): roi = crop_roi(frame, box, scale=1.2) roi = cv2.resize(roi, (224, 224)) x = roi.reshape(1, 224*224*3).astype(np.float32) / 255.0 with torch.no_grad(): prob = torch.softmax(model(torch.from_numpy(x)), dim=1) idx = int(prob.argmax(dim=1)) conf = float(prob.max(dim=1).values) return idx, conf

reshape之前要把像素值除以 255 归一化。这里没有做均值减除,因为 MLP 是从像素直接学习的,减均值反而会破坏输入分布。如果模型是在transforms.py里用 ImageNet 的均值和方差做归一化的,那训练和推理必须保持一致,否则输出概率会整体偏移。map_location="cpu"是为了让没有 GPU 的机器也能正常加载训练好的权重;如果训练时用的是 GPU,这一行能避免 CUDA 报错。推理阶段必须用torch.no_grad(),否则模型会缓存梯度,内存占用持续增长,实时程序跑几分钟就会被 OOM 杀掉。

3.4 阈值、平滑与参数速查

实时识别最典型的问题是单帧误判。比如数字 0 和 6,在手指没有完全张开时很容易混淆。解决方法是加时间平滑。下表对比了几种常见策略:

策略窗口/参数触发条件延迟
多数投票5 帧至少 3 帧标签相同约 5 帧
置信度阈值score_threshold=0.7单帧置信度高于阈值约 1 帧
阈值 + 多数投票5 帧置信度高于 0.7 且 3 帧一致约 5 帧

最简单的实现是collections.deque(maxlen=5),每帧把预测标签加入队列,然后统计出现次数最多的标签。阈值加投票的组合稳定性最高,适合用来做点击这类不可逆操作。平滑策略的选择取决于场景:鼠标移动需要低延迟,用score_threshold=0.7直接放行;点击和 PPT 翻页需要高可靠性,用阈值加多数投票。opts.py里通常会提供--score_threshold--smooth_window两个参数,默认值分别给到 0.7 和 5 是比较合理的起点。

4. 命令转换与人机交互控制:标签如何变成鼠标移动和 PPT 翻页

4.1 communication 模块:跨进程消息转发

本项目在communication目录下拆了send.pyreceive.py,意味着识别进程和控制进程可以是分离的。识别端负责摄像头和模型,控制端负责执行动作。这样设计的好处是识别过程中即使 UI 卡住,也不会阻塞摄像头采集。发送端代码通常长这样:

import socket import json sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) def send_command(cmd, params=None): msg = json.dumps({"cmd": cmd, "params": params or {}}) sock.sendto(msg.encode("utf-8"), ("127.0.0.1", 9999))

SOCK_DGRAM用 UDP 而不是 TCP,因为手势控制对实时性要求高于可靠性;偶尔丢一帧点击命令不影响整体体验,但 TCP 重传会累积延迟。receive.py那边用一个while True循环接收并分发:

data, addr = recv_sock.recvfrom(1024) obj = json.loads(data.decode("utf-8")) handle(obj["cmd"], obj["params"])

这里的handle是一个统一入口,内部根据cmd字段路由到mouse_controlppt_control。用 JSON 作为协议格式的好处是后续扩展手势命令时不需要改网络层,只需要在接收端增加一个cmd分支。如果你在本地运行,127.0.0.1就够了;如果打算用树莓派或另一台电脑做识别端,需要把 IP 改成局域网地址,同时注意防火墙允许 UDP 9999 端口。

4.2 鼠标控制与坐标映射

show_UI下的main_control.py负责把识别窗口里的手势坐标映射到屏幕坐标。常见映射逻辑是:

import pyautogui screen_w, screen_h = pyautogui.size() view_w, view_h = 640, 480 def to_screen(hand_cx, hand_cy): return hand_cx * screen_w / view_w, hand_cy * screen_h / view_h

注意如果摄像头画面是 640x480,屏幕是 1920x1080,那么直接用比例映射会让鼠标移动过快或过慢。更稳妥的做法是加一个灵敏度系数sensitivity,例如:

sensitivity = 1.5 screen_x = int((hand_cx / view_w - 0.5) * 2 * sensitivity * screen_w + screen_w / 2) screen_y = int((hand_cy / view_h - 0.5) * 2 * sensitivity * screen_h + screen_h / 2)

这个公式把“以画面中心为原点的相对偏移”乘上灵敏度再叠加屏幕中心,避免鼠标只出现在画面左上区域。课程设计里这部分最容易忽略,结果手势识别没问题,但鼠标移动范围总是不对。还有一个细节:如果手部检测框本身是抖动的,映射后的鼠标坐标也会高频抖动,所以坐标也要平滑。常见做法是把上一帧的屏幕坐标和当前帧做线性插值,或者再加一层 EMA,这里和第 5 章的预测平滑是两码事。

4.3 点击、滚动与 PPT 控制

ppt_control.py里的操作通常依赖 pyautogui 的跨平台接口。下面是常见的动作映射表:

手势标签命令实现方式
0(握拳)左键单击pyautogui.click()
1(食指)移动鼠标pyautogui.moveTo(x, y)
2(剪刀)双击pyautogui.doubleClick()
3(三指)向上滚动pyautogui.scroll(3)
4(四指)向下滚动pyautogui.scroll(-3)
5(五指张开)PPT 下一页pyautogui.press("right")
6(竖拇指)PPT 上一页pyautogui.press("left")

注意pyautogui.scroll在 Windows 上是行数,在 macOS 上被解释为像素级滚动,所以如果要跨平台,最好在receive.py里统一调用platform.system()做分支。ppt_control.py里我一般直接使用方向键,这样兼容大部分 PowerPoint 和 Keynote。另外,pyautogui在 macOS 上需要辅助功能权限,第一次运行会在系统设置里弹出授权,这一步不是代码问题,但课程设计演示前一定要提前验证,否则现场容易出现点击无效的尴尬。

4.4 启动参数与运行模式

在同一份源码里,在线识别入口是main.py,离线验证入口是run_deal_video.py

python gesture_recognition/main.py --camera 0 --model_path checkpoints/mlp.pth --score_threshold 0.7 python gesture_recognition/run_deal_video.py --video demo.mp4 --model_path checkpoints/mlp.pth --output out.avi

--camera 0指定系统默认摄像头;--score_threshold是置信度门限;--output用来把识别结果保存成视频,方便复盘。show_prepare.py则用于单独调试预处理环节,它在所有动作之前先把中间结果输出到窗口,便于确认整个数据链路的哪一环出了问题。如果你在笔记本上运行,建议先把--camera 0改成--camera 1试试,有些电脑自带摄像头和 USB 摄像头的索引不是 0。

5. 调试与调参技巧:用 show_prepare.py 和 EMA 快速收敛到可用状态

5.1 用 show_prepare.py 定位数据管线问题

show_prepare.py是一个可视化工具。运行它时,左边显示原始帧,右边显示经过deal_video.py的裁剪、缩放、归一化后的图像。如果训练时准确率很高但实时识别很差,先跑这个脚本,重点看三处:手部区域是否被矩形框准确包围、裁剪后的图像中手是否居中且完整、不同光照下 ROI 是否出现剧烈抖动。这三个问题分别对应 HSV 阈值、roi_scalemedianBlur核大小。把show_prepare.py输出的帧保存成 PNG,和原始视频逐帧叠在一起做对比,可以非常清楚地看到 ROI 是不是在手指张开瞬间包含了背景区域。

5.2 指数移动平均平滑预测

除了多数投票,更轻量的做法是对每个类别的概率做 EMA:

alpha = 0.6 # ema_probs 长度等于类别数,初始化为0 ema_probs = alpha * ema_probs + (1 - alpha) * current_probs label = int(ema_probs.argmax())

alpha越大,平滑越强,延迟也越大。0.6 是交互场景里比较平衡的值。触发点击命令时,最好清空一次ema_probs,否则上一次手势的残留概率会拖慢下一次手势的响应。这里有一个小技巧:把ema_probs里低于 0.1 的概率直接截断为 0,能减少随机噪声带来的标签抖动。

5.3 错误模式对照表

现象可能原因调整建议
鼠标指针颤抖手部 boundingRect 边缘不稳定增大 medianBlur 核大小,或减小roi_scale
换灯光环境后无法识别HSV 阈值固定记录环境色样,改为动态阈值或自动白平衡
训练集准确率高、验证集低视频切割时子帧泄露检查choose_train_validation.py是否按视频前缀划分
点击延迟明显平滑窗口太大或 alpha 太大把窗口从 5 降到 3,或把 alpha 从 0.6 调到 0.4
背景中有人脸时误触发最大轮廓选错目标在 ROI 内增加手部中心点先验,避免脸部进入画面中心

最后再强调一个不起眼但影响很大的细节:opts.py里的--roi_scale不要随手改成 1.0,那样手部贴满整个 ROI,训练时靠边缘特征区分的 0 到 9 手势很容易互相混淆。保持在 1.2 附近,识别稳定性会明显上一个台阶。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询