
简介这款基于OpenCV与Mediapipe的手势控制鼠标及键盘的完整Python源码工程面向想学习计算机视觉、手势识别与桌面自动化交互的中高级开发者。项目利用OpenCV自动追踪手掌位置截取手部图像后交由神经网络对手势分类实现指尖移动控制鼠标、特定手势完成点击与页面滚动同时支持虚拟键盘点选输入、自定义手势映射功能并通过PySide2悬浮窗实时显示手部骨架与当前操控模式整体功能覆盖较全面适合作为人机交互方向的实战参考。包内共108个文件约300.77MB以38个Python源码文件为核心配合20个XML配置、4个PB模型文件、界面相关的QSS/UI资源以及图片图标等素材结构清晰便于按功能模块阅读和二次开发。目前已有187人学习下载适合需要从零搭建手势控制桌面应用或研究Mediapipe多模块配合的开发者参考。1. 用 OpenCV MediaPipe 做手势控鼠这份源码把挥手操控拆成了几步拿普通摄像头当无线鼠标用听起来有点玄学但这份基于 OpenCV MediaPipe 的手势控制鼠标源码确确实实把整条链路打通了摄像头采集画面MediaPipe 实时追踪手掌 21 个关键点神经网络把当前手势分类出来再把手势翻译成鼠标移动、点击、滚动、按键触发这些具体指令。它大概能解决三类需求做桌面端人机交互 Demo、把手势识别接进现有 OpenCV 视觉项目、给产品原型快速验证挥手操控的交互路径。适合的读者是会用 Python、但没搭过完整手势控制管线的人。后面我会把最值得看的骨架逻辑拆出来再补上自己跑工程时踩过的几个坑以及自定义手势该怎么少走弯路。2. 先分清谁在干活MediaPipe、OpenCV 与神经网络各自的职责边界2.1 MediaPipe 为什么是这个项目的底座21 个关键点与两个置信度参数很多第一次拿到这份源码的人会问既然标题写了 OpenCV为什么还要引入 MediaPipe原因很简单——纯 OpenCV 做手势识别传统路子是肤色检测加轮廓拟合在干净背景、固定光线下勉强能用一旦遇到复杂背景、暗光、肤色相近的物体误检率会直线上升属于典型的实验室能用、工位翻车方案。MediaPipe Hands 把这件事变成了一个端到端的神经网络管线输入一帧 RGB 图像输出手部 21 个关键点landmark的坐标。这 21 个点覆盖了手腕、掌心、以及每根手指的四个关节索引是有固定含义的后面写映射时全靠它。最常用的几个4 号点是拇指指尖8 号点是食指指尖12 号点是中指指尖16 号点无名指20 号点小指。项目里指尖移动控制鼠标这一条就是拿 8 号点的运动轨迹去映射光标位置。源码初始化时一般长这样import mediapipe as mp mp_hands mp.solutions.hands hands mp_hands.Hands( static_image_modeFalse, # 视频流模式连续帧间做跟踪而不是每帧重新检测 max_num_hands1, # 只跟踪一只手减少 CPU 计算量 min_detection_confidence0.5, # 首次检测阶段的置信度阈值范围 0~1 min_tracking_confidence0.5 # 跟踪阶段的置信度阈值控制关键点稳定性 )逻辑说明static_image_modeFalse告诉 MediaPipe 这是视频流它会复用上一帧的检测结果来跟踪当前帧速度比每帧全量检测快很多max_num_hands1对于单鼠标控制场景足够了设成 2 会让 CPU 推理时间接近翻倍。逻辑说明就一句话这两个置信度参数决定了多容易把手认出来和认出来后跟不跟得住。参数怎么调有讲究。min_detection_confidence调高比如 0.8误检少但手稍微侧一点就断调低0.3则更容易在画面边缘把手捡回来代价是偶尔把错误区域当手。min_tracking_confidence则影响关键点抖动调高了反而容易丢帧。我一般从 0.5 起步做完鼠标映射测试后再决定往哪个方向收紧。2.2 神经网络的真实分工两级模型协作而不是单一大网络标题里强调神经网络但如果你只盯着 MediaPipe 的接口看会觉得好像没自己训练模型。实际上这份源码里的神经网络是分两级的这是看懂整体架构的关键。第一级是 MediaPipe 内置的 BlazePalm 手掌检测模型。它负责在整帧画面上找哪里有手输出一个手掌边界框。把这块区域裁出来之后再做关键点回归得到 21 个 landmark。整个过程对 CPU 比较友好我实测在 i5-8250U 这种老笔记本上640x480 分辨率大概能跑到 30ms 一帧。第二级才是摘要里说的利用神经网络对手势进行分类。拿到 21 个关键点坐标后需要判定当前手势是张开手掌握拳拇指食指捏合还是比两指。这类分类任务轻量级前馈神经网络就够用了输入是 21 个点的相对坐标一般以手腕点做基准归一化输出是手势类别概率。这个分类器可以单独训练常见做法是用 MediaPipe Model Maker 导出一个 .tflite 手势分类模型跑起来只有几十毫秒。明白了这两级分工你就知道 OpenCV 在里面的位置了负责调摄像头、BGR 转 RGB、把画面镜像翻转、以及截取手部 ROI 区域。它不参与识别推理只做图像采集和预处理。很多人在讲这种项目时把OpenCV、神经网络、MediaPipe混成一锅粥实际边界很干净——MediaPipe 出关键点分类网络出手势OpenCV 管图像搬运pyautogui 管最终输入输出。2.3 从手势到鼠标指令中间层映射的设计核心检测到手势只是第一步怎么把手势变成操作才是决定好不好用的地方。源码的摘要里列了移动、点击、滚动、虚拟键盘、自定义手势五类功能底层是靠一个状态机串起来的。我的理解里这个状态机至少要有这么几个状态空闲态检测到手但没有任何有效动作此时鼠标不响应移动态手掌张开或单指竖起食指指尖的位移映射为鼠标相对位移点击态拇指与食指捏合达到阈值触发 down / up 事件滚动态两指并拢上下移动量乘以滚动速率切换态特定手势比如三指快速张合切换鼠标模式与键盘模式设计的关键在于——不要用单帧判定来决定动作因为单帧的 landmark 抖动会造成明明没捏合却触发了点击的误判。我习惯在每个状态里放一个长度为 3 的滑动窗口连续 3 帧都是同一个手势才认为状态切换成立。这就是防抖和按键消抖一个道理。把这一层搞清楚再去看源码你会发现核心逻辑不在摄像头采集也不在模型推理而是中间这段关键点坐标 → 手势分类 → 状态机 → 输入设备指令的映射代码。后面拆功能的时候所有参数调整也都是围绕这个中间层展开的。3. 把工程从压缩包变成本机进程环境搭建与首次启动验证3.1 依赖安装先装齐这四个包再动代码这份源码的运行依赖不算复杂但装的时候容易踩版本坑。核心依赖是 opencv-python、mediapipe、PySide2 和 pyautogui另外 numpy 是隐式依赖基本每个环节都会用到。第一次跑通前我建议按下面这套顺序装python -m pip install --upgrade pip python -m pip install opencv-python4.8.0.76 python -m pip install mediapipe0.10.9 python -m pip install PySide25.15.2.1 python -m pip install pyautogui0.9.54 numpy参数说明OpenCV 不必追最新版MediaPipe 对 OpenCV 的版本有兼容窗口太新的 OpenCV比如 4.9在部分环境下会出现 cvtColor 或者 VideoCapture 读取异常。MediaPipe 0.10.x 是目前比较稳的版本线手部模型文件内置在包里不需要额外下载权重。PySide2 选 5.15 是因为它对应 Qt 5.15 LTS悬浮窗和主界面在这套组合下编译最省心。pyautogui 负责把操作交给系统注意 macOS 上需要给它开辅助功能权限Windows 上一般不需要特殊设置。3.2 目录结构每条文件都不是摆设解压源码包之后你会发现里面没有太多.py文件直接摊在根目录反而有一批工程文件和配置文件。逐个看它们的作用文件名真实作用package.batWindows 一键打包脚本通常调用 PyInstaller 把 PySide2 界面、模型和图标打成 exeloading.gif启动时的加载动画悬浮窗没起来之前给用户一个正在启动的反馈favicon.ico应用窗口图标打包时会被写进 exe 资源opencv.iml / control_mouse.iml / gesture_control_app.imlPyCharm / IntelliJ IDEA 的模块描述文件记录项目 SDK 与依赖不影响运行但能帮助 IDE 直接识别工程结构.gitignoreGit 忽略规则把虚拟环境目录、缓存、打包产物排除掉这里有个小提示拿到源码后用 PyCharm 打开如果 IDE 能识别出.iml对应的项目模块说明作者是直接用 PyCharm 建的工程运行前把解释器指到装有上述依赖的虚拟环境即可。如果你用的是 VSCode这些.iml文件直接忽略不影响功能。3.3 首次启动验证先跑一段最小闭环代码不要一上来就启动整个界面程序那样出了错你分不清是摄像头问题、模型问题还是悬浮窗问题。我建议先从最小闭环开始——确认 MediaPipe 能在你的摄像头上稳定输出手部关键点import cv2 import mediapipe as mp mp_hands mp.solutions.hands mp_draw mp.solutions.drawing_utils hands mp_hands.Hands(max_num_hands1) cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) while cap.isOpened(): 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_landmarks in result.multi_hand_landmarks: mp_draw.draw_landmarks(frame, hand_landmarks, mp_hands.HAND_CONNECTIONS) cv2.imshow(Hand Tracking, frame) if cv2.waitKey(1) 0xFF 27: # ESC 退出 break cap.release() cv2.destroyAllWindows()逻辑说明这段代码先固定摄像头分辨率为 640x480然后每帧做 BGR 转 RGBprocess()内部会完成手掌检测与 21 点回归。注意cv2.flip(frame, 1)这一步很关键——如果不镜像翻转你手往左移画面里却往右移鼠标控制时方向会非常别扭。参数说明CAP_PROP_FRAME_WIDTH和CAP_PROP_FRAME_HEIGHT是摄像头输出分辨率。很多笔记本摄像头默认 1280x720MediaPipe 在 CPU 上处理 720P 会比 480P 慢一截所以先把分辨率压下来。跑起来之后如果画面里能稳定显示手掌骨架图说明基础链路是通的接下来再进完整工程。4. 核心功能拆解从指尖坐标到鼠标指令的四条实现路径4.1 鼠标移动指尖到屏幕的相对位移与速度系数通过手指的相对移动与移动的速度来操纵鼠标的移动这句功能简介翻译成代码就是两件事取指尖坐标的变化量和一个可配置的灵敏度系数相乘最后用 pyautogui 做相对移动。不提绝对坐标映射是因为绝对定位需要手在画面里的位置对应整个屏幕精度很差而且手一移出画面鼠标就不知道去哪了。示意代码如下import cv2 import mediapipe as mp import pyautogui mp_hands mp.solutions.hands hands mp_hands.Hands(max_num_hands1, min_detection_confidence0.5) cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) screen_w, screen_h pyautogui.size() # 获取屏幕尺寸用于限制光标范围 sensitivity 3.0 # 灵敏度系数值越大手移动同样距离鼠标跑得越远 alpha 0.3 # 指数平滑系数越小越稳定但延迟越高 smooth_x smooth_y 0 prev_x prev_y -1 while cap.isOpened(): 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: hand result.multi_hand_landmarks[0] h, w, _ frame.shape fx int(hand.landmark[8].x * w) # 8 号点食指尖 fy int(hand.landmark[8].y * h) if prev_x -1: prev_x, prev_y fx, fy continue dx fx - prev_x # 相对位移像素单位 dy fy - prev_y prev_x, prev_y fx, fy # 死区位移小于 3 像素直接忽略避免微小抖动造成鼠标漂移 if abs(dx) 3 and abs(dy) 3: continue # 指数平滑减少关键点抖动被放大 smooth_x alpha * (dx * sensitivity) (1 - alpha) * smooth_x smooth_y alpha * (dy * sensitivity) (1 - alpha) * smooth_y cur_x, cur_y pyautogui.position() new_x min(max(cur_x int(smooth_x), 0), screen_w - 1) new_y min(max(cur_y int(smooth_y), 0), screen_h - 1) pyautogui.moveTo(new_x, new_y, duration0) else: prev_x prev_y -1逻辑说明landmark[8]的坐标是归一化的0~1需要乘上画面宽高还原成像素。位移量是帧与帧之间的差值乘上sensitivity之后手在画面里划过的距离和鼠标在屏幕上的移动距离形成放大关系。指数平滑那里smooth_x不断用新值修正旧值本质是一个低通滤波器让光标轨迹更柔。参数说明sensitivity我习惯设在 2.5 到 4 之间太小了手要划很大幅度鼠标才动太大了微调指尖时光标飞出去。alpha是平滑权重0.5 以上响应快但抖0.1 以下稳但拖影严重。还有那个死区判断3 像素是我从工程里学到的经验——不加它手静止的时候光标会有一个肉眼可见的呼吸漂移。4.2 点击与滚动手势阈值的判定与防抖状态机鼠标点击对应的是拇指指尖与食指指尖捏合这个手势。判定逻辑不能只看一帧的距离要用一个连续帧确认的方案来防止误触import numpy as np PINCH_THRESHOLD 0.05 # 捏合距离阈值归一化坐标下的欧氏距离 REQUIRED_FRAMES 3 # 连续帧数达到该帧数才触发点击 cooldown_frames 10 # 点击后的冷却帧数防止一次捏合触发多次点击 pinch_count 0 last_trigger 0 frame_idx 0 def is_pinch(landmarks): 判断拇指指尖(4)与食指指尖(8)是否捏合 thumb np.array([landmarks[4].x, landmarks[4].y]) index np.array([landmarks[8].x, landmarks[8].y]) dist np.linalg.norm(thumb - index) return dist PINCH_THRESHOLD, dist # 每帧调用 def handle_click(landmarks, frame_idx): global pinch_count, last_trigger pinched, dist is_pinch(landmarks) if pinched: pinch_count 1 else: pinch_count 0 return if (pinch_count REQUIRED_FRAMES and frame_idx - last_trigger cooldown_frames): pyautogui.click() # 触发一次左键点击 last_trigger frame_idx pinch_count 0 # 重置计数避免连点逻辑说明pinch_count只有在连续帧都满足捏合条件时才递增中途松开就清零。REQUIRED_FRAMES3意味着至少要连续 3 帧约 100ms捏合才认定是一次有效点击这个窗口足以过滤掉手抖造成的瞬间误触。cooldown_frames是冷却期点击之后 10 帧内不再响应否则捏合动作持续时每一帧都会触发一次 click变成机枪连点。参数说明PINCH_THRESHOLD0.05是在 640x480 画面下的归一化距离大约对应 30 多个像素。手指修长的人可以放宽到 0.06手指短的人要收到 0.04。滚动逻辑与点击类似一般是双指并拢后用当前帧指尖纵向位置与上一帧的差值乘一个滚动速率系数正差向上滚、负差向下滚。4.3 PySide2 悬浮窗骨架图与操控模式的实时刷新悬浮窗是这份源码里体验感提升最明显的一块。它用 PySide2 实现了一个置顶、透明、无边框的小窗口实时画出手部骨架同时显示当前操控模式比如 MOVING / CLICKING / SCROLLING / KEYBOARD。核心在两点窗口样式设置和定时刷新。from PySide2.QtWidgets import QApplication, QWidget from PySide2.QtCore import Qt, QTimer from PySide2.QtGui import QPainter, QPen, QColor class OverlayWindow(QWidget): def __init__(self, parentNone): super().__init__(parent) self.setWindowFlags( Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool # 不占任务栏也不抢焦点 ) self.setAttribute(Qt.WA_TranslucentBackground) # 背景完全透明 self.setAttribute(Qt.WA_TransparentForMouseEvents, True) # 鼠标事件穿透 self.resize(220, 220) self.landmarks [] self.mode MOVE self.timer QTimer(self) self.timer.timeout.connect(self.update) # 触发重绘 self.timer.start(33) # 约 30fps def set_landmarks(self, landmarks): self.landmarks landmarks def paintEvent(self, event): painter QPainter(self) painter.setRenderHint(QPainter.Antialiasing) pen QPen(QColor(0, 255, 100, 200), 2) painter.setPen(pen) if self.landmarks: base_x self.width() / 2 base_y self.height() / 2 # 以手腕点(0)为基准把归一化坐标映射到悬浮窗上 for i, lm in enumerate(self.landmarks): x int(base_x (lm.x - self.landmarks[0].x) * 300) y int(base_y (lm.y - self.landmarks[0].y) * 300) painter.drawEllipse(x - 3, y - 3, 6, 6) painter.drawText(8, 16, self.mode)逻辑说明WA_TransparentForMouseEvents是必须的有了它鼠标点击才能穿透悬浮窗落到下层应用上否则悬浮窗会挡住桌面图标操作。主线程把每帧识别到的 landmark 塞进set_landmarks()QTimer每 33ms 触发一次update()Qt 会自动调用paintEvent重绘。骨架图里的横纵坐标用了手腕点对齐的方式映射保证手在画面里移动时悬浮窗里的手型保持在窗口中心附近而不是飘出窗外。参数说明300 这个缩放系数决定悬浮窗里骨架的大小窗口 220x220 时用 300 倍放大会溢出窗口边缘可以根据自己屏幕缩放比调到 200~350。帧率 33ms 是平衡点再高比如 16ms对识别线程的 CPU 占用压力大再低50ms骨架图会有明显跳变感。4.4 主界面灵敏度、滚动速率与手势映射的配置持久化工程里用户可以修改手势对应的功能这条落地方案通常是把配置写进 JSON主界面读写这份文件。不要在代码里 hardcode 一堆参数那样用户每次改灵敏度都要动代码不符合可自定义的产品定位。import json import os DEFAULT_CONFIG { sensitivity: 3.0, scroll_rate: 50, click_threshold: 0.05, gesture_map: { pinch: left_click, fist: right_click, two_finger: scroll, three_finger: switch_mode } } def load_config(pathconfig.json): if not os.path.exists(path): save_config(path, DEFAULT_CONFIG) return DEFAULT_CONFIG.copy() with open(path, r, encodingutf-8) as f: return json.load(f) def save_config(path, config): with open(path, w, encodingutf-8) as f: json.dump(config, f, indent2, ensure_asciiFalse)逻辑说明load_config首次运行时若发现配置文件不存在就用默认配置生成一份这样用户改坏配置后删掉 JSON 就能恢复出厂设置。gesture_map里的键是手势类型值是动作字符串主界面用一个下拉列表让用户把某个手势绑定到某个动作改完写回 JSON程序下次读配置就生效。参数说明scroll_rate50对应滚动事件里每帧位移换算成的滚动行数取值越大滚得越快一般在 30~80 之间调。click_threshold和前面 4.2 的PINCH_THRESHOLD是同一个概念主界面里暴露出来给用户微调这正是用户及系统设置主界面的意义所在。5. 避坑与常见问题从跑通到好用的五个典型坎5.1 摄像头画面卡顿帧率掉到 15 以下现象MediaPipe 检测正常但画面明显不跟手怀疑是模型太慢实际上模型推理只占一部分。原因摄像头分辨率默认是 1280x720MediaPipe 在 CPU 上处理 720P 帧耗时接近 45~60ms再加上 BGR 转 RGB、后续的骨架绘制、pyautogui 调用总耗时轻松超过 66ms帧率自然跌破 15。解决把分辨率压到 640x480这一步对帧率提升最明显。其次检查是否每一帧都调用了cv2.cvtColor且只调用一次有些代码会在绘制循环里重复转换。最后把max_num_hands保持为 1双手跟踪会显著增加推理时间。5.2 鼠标光标自由漂移手不动它自己慢慢跑现象手完全静止光标却朝某个方向缓慢移动像是有幽灵在推鼠标。原因两个叠加因素。第一MediaPipe 输出的关键点本身带亚像素级抖动隔几帧抖动几个像素第二直接拿归一化坐标乘屏幕尺寸做绝对映射等于把每帧的微小抖动放大到了屏幕上。解决改用相对位移方案4.1 的写法并加指数平滑。我还会在位移小于 3 像素时直接忽略相当于给信号加了死区。实测这一套组合能让静止光标稳定在 1 个像素以内不动。5.3 手势误触想移动鼠标却触发了点击现象只是把手从左边移到右边中途却莫名其妙出现一次左键点击导致误选中文件。原因捏合判定只看了单帧距离。手移动过程中食指和拇指在画面里的投影距离有时会瞬间小于阈值于是单帧就触发了点击逻辑。解决把单帧判定改成连续 3 帧确认机制4.2 的pinch_count并且加 10 帧冷却。如果仍然误触把PINCH_THRESHOLD从 0.05 收紧到 0.03让捏合判定更苛刻一点。5.4 ModuleNotFoundError: No module named opencv现象执行import cv2报错或者跑完pip install opencv-python后依然报错。原因常见于 Python 环境混淆。pip默认指向系统 Python 或者 conda 的 base 环境而 PyCharm 里新建的项目解释器是另一个虚拟环境两边互相看不见。另一种情况是 opencv-python 与旧版 mediapipe 冲突装完被覆盖成了 opencv-python-headless。解决用python -m pip install --force-reinstall opencv-python4.8.0.76强制重装然后检查sys.path开头是不是项目虚拟环境的 site-packages。如果用的是 conda把python -m pip换成conda install或者先在 conda 环境里激活再装。5.5 悬浮窗置顶后遮挡其他窗口且鼠标点不穿现象悬浮窗一直浮在最前面点击它下面的文件或按钮时无效就像一张透明玻璃板挡住了桌面。原因WindowStaysOnTopHint让它置顶了但没加WA_TransparentForMouseEvents鼠标事件被窗口吃掉另外缺少Qt.Tool属性会让它出现在任务栏并抢焦点。解决窗口初始化时三个 flag 一起设FramelessWindowHint | WindowStaysOnTopHint | Qt.Tool并用setAttribute(Qt.WA_TransparentForMouseEvents, True)让鼠标点击穿透。顺便检查loading.gif是否还在循环播放有些实现会把 GIF 加载动画绑在窗口上关闭动画后才能正常穿透。6. 把默认手势改成自己的Model Maker 重训与灵敏度调参这套源码默认只识别几种固定手势想让它听懂你自己的动作习惯完整路径是采集数据 → 训练分类器 → 更新映射 → 验证效果。采集数据的关键是保存相对坐标而不是绝对坐标。直接存landmark.x、landmark.y会导致同一个手势在画面左边和右边特征完全不同分类器很难收敛。我一般以手腕点索引 0为原点把其余 20 个点的坐标减去手腕坐标得到一组相对位置这样手在画面里不管怎么平移特征都保持稳定。import csv import cv2 import mediapipe as mp mp_hands mp.solutions.hands hands mp_hands.Hands(max_num_hands1) def collect_samples(label, frames200, save_pathgesture_data.csv): cap cv2.VideoCapture(0) rows [] count 0 while count frames: ret, frame cap.read() if not ret: continue frame cv2.flip(frame, 1) result hands.process(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) if result.multi_hand_landmarks: lm result.multi_hand_landmarks[0].landmark base_x, base_y lm[0].x, lm[0].y features [] for point in lm[1:]: features.append(round(point.x - base_x, 6)) features.append(round(point.y - base_y, 6)) features.append(label) rows.append(features) count 1 with open(save_path, a, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerows(rows) print(f已采集 {label}: {count} 帧) # 使用示例 collect_samples(pinch, frames200) collect_samples(fist, frames200) collect_samples(two_finger, frames200)逻辑说明lm[1:]跳过手腕点只保留其余 20 个点的相对坐标每个点两个浮点数一共 40 维特征加上标签列就是一条训练样本。round(..., 6)把浮点截断到 6 位小数避免 CSV 文件被长尾小数撑大。训练这一步轻量场景不需要上大的卷积神经网络前馈神经网络或者 MediaPipe Model Maker 导出的手势分类模型足够。我一般先用 sklearn 的 MLPClassifier 跑通流程验证准确率到 95% 以上再用 Model Maker 导出 tflite 模型替换工程里的默认模型。注意导出时的标签顺序要和采集时的 label 对应否则会出现识别结果对不上手势的尴尬情况。数据量方面每个手势采集 200 帧是底线300~400 帧更稳。采集的时候故意让手在画面不同位置、稍微倾斜角度这样分类器不会过拟合到只能在固定位置识别。换新模型后验证环节别偷懒。我的习惯是写一段打印脚本实时输出当前帧的分类结果和置信度而不是直接开主界面看鼠标动不动。确认手势分类正确后再切换鼠标控制能省下大量排错时间。最后一步是调参。灵敏度、滚动速率、捏合阈值这三个参数会互相影响灵敏度调高了捏合时指尖的位移也变大可能让原本够不着的捏合距离变近滚动速率快了双指滑动同样的距离滚更多行误触的概率也会增加。我一般按灵敏度 → 捏合阈值 → 滚动速率的顺序调每次只动一个参数实测一轮再动下一个。这件事做了几遍之后我总结出一个教训任何手势控制项目第一优先永远是把手势判定和指令执行拆成两个独立模块模型可以换、灵敏度可以调、手势映射可以改但中间这条数据通道一旦耦合后面每次改参数都会牵一发动全身。从那以后我每次跑这类源码第一件事就是确认识别逻辑和操作逻辑之间是用配置或接口解耦的再开始调参。希望帮到你。本文还有配套的精品资源点击获取