简介面向计算机视觉与人机交互的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 或 picklechoose_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会扫描输出目录里的子目录名比如num0、num1映射成{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_dim224*224*3, hidden512, num_classes10): 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)这里hidden512是一个起点值。如果样本量在几千级别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.py和gesture_location_system.py。main.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, keycv2.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): breakHSV 范围(0, 30, 60)到(20, 150, 255)是在实验室正常光照下标定出来的肤色区间它只对黄种人肤色有效。如果换一个环境最直接的现象是手部区域检测破碎或包含脸的一部分。这时应该先打印 mask 的直方图而不是直接在inRange参数上调高阈值。medianBlur的核大小 5 可以滤掉传感器噪点但核太大会侵蚀掉手指之间的缝隙。这里max(contours, keycv2.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 被放大得比较多建议显式指定interpolationcv2.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_namemlp, num_classes10) model.load_state_dict(torch.load(checkpoints/mlp.pth, map_locationcpu)) model.eval() def infer(frame, box): roi crop_roi(frame, box, scale1.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)), dim1) idx int(prob.argmax(dim1)) conf float(prob.max(dim1).values) return idx, confreshape之前要把像素值除以 255 归一化。这里没有做均值减除因为 MLP 是从像素直接学习的减均值反而会破坏输入分布。如果模型是在transforms.py里用 ImageNet 的均值和方差做归一化的那训练和推理必须保持一致否则输出概率会整体偏移。map_locationcpu是为了让没有 GPU 的机器也能正常加载训练好的权重如果训练时用的是 GPU这一行能避免 CUDA 报错。推理阶段必须用torch.no_grad()否则模型会缓存梯度内存占用持续增长实时程序跑几分钟就会被 OOM 杀掉。3.4 阈值、平滑与参数速查实时识别最典型的问题是单帧误判。比如数字 0 和 6在手指没有完全张开时很容易混淆。解决方法是加时间平滑。下表对比了几种常见策略策略窗口/参数触发条件延迟多数投票5 帧至少 3 帧标签相同约 5 帧置信度阈值score_threshold0.7单帧置信度高于阈值约 1 帧阈值 多数投票5 帧置信度高于 0.7 且 3 帧一致约 5 帧最简单的实现是collections.deque(maxlen5)每帧把预测标签加入队列然后统计出现次数最多的标签。阈值加投票的组合稳定性最高适合用来做点击这类不可逆操作。平滑策略的选择取决于场景鼠标移动需要低延迟用score_threshold0.7直接放行点击和 PPT 翻页需要高可靠性用阈值加多数投票。opts.py里通常会提供--score_threshold和--smooth_window两个参数默认值分别给到 0.7 和 5 是比较合理的起点。4. 命令转换与人机交互控制标签如何变成鼠标移动和 PPT 翻页4.1 communication 模块跨进程消息转发本项目在communication目录下拆了send.py和receive.py意味着识别进程和控制进程可以是分离的。识别端负责摄像头和模型控制端负责执行动作。这样设计的好处是识别过程中即使 UI 卡住也不会阻塞摄像头采集。发送端代码通常长这样import socket import json sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM) def send_command(cmd, paramsNone): 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_control或ppt_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.pypython 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_scale和medianBlur核大小。把show_prepare.py输出的帧保存成 PNG和原始视频逐帧叠在一起做对比可以非常清楚地看到 ROI 是不是在手指张开瞬间包含了背景区域。5.2 指数移动平均平滑预测除了多数投票更轻量的做法是对每个类别的概率做 EMAalpha 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 附近识别稳定性会明显上一个台阶。本文还有配套的精品资源点击获取