☰
基于MediaPipe与CNN的手势控制鼠标:从关键点到事件映射
2026/9/25 21:27:51 网站建设 项目流程

简介:基于OpenCV、MediaPipe与CNN的手势识别鼠标操控项目,是一份面向计算机视觉、人机交互方向毕设、课程设计与入门进阶的完整资源。项目以指尖移动控制鼠标、手势点击、滚动页面、虚拟键盘输入及快捷按键触发为核心功能,覆盖手部关键点检测到CNN分类的完整链路。压缩包共109个文件,约300.75MB,包含38个py源码文件、20个xml配置、15个png界面素材、4个pb模型文件,以及qss样式、ico图标、GIF演示和UI设计文件;同时附有可直接运行的exe程序、README启动说明与设计文档,便于快速体验和二次开发。已有254人学习下载,代码经运行验证稳定,适合作为手势交互类课程项目演示或毕业设计基础。下载后结合设计文档可理解整体架构,借助打包脚本也可自行生成可执行程序。

1. 从画面到指针:手势控制鼠标真正难在哪

把手势识别控制鼠标当成一个图像分类问题来做,第一个版本通常会被狠狠打击:CNN 准确率已经到 98%,光标还是飘、点不准、动不动误触发。因为从摄像头画面到屏幕指针之间隔了投影变换、帧率、抖动和操作习惯,识别只解决“这是什么手势”,剩下的坐标映射、平滑、防抖才决定“好不好用”。下面把整条链路拆开讲:MediaPipe 出关键点,CNN 出 0~9 手势标签,OpenCV 管采集和画面预处理,最后把标签映射成鼠标事件并打包成 exe。适合已经能跑通图像分类、想把手势当真正常用的输入设备的人。

2. MediaPipe 手部关键点提取与特征工程:手势识别的前置管线

2.1 为什么是 MediaPipe Hands 而不是传统肤色分割

早期做手势识别,最省事的方案是 YCrCb 或 HSV 肤色分割:把肤色像素捞出来,找最大连通域,再通过轮廓凹凸点判断手指数量。这套做法在纯色背景、固定光源下能用,搬到普通办公室环境就崩:白色灯光下肤色偏黄,日光灯频闪时轮廓一会儿粗一会儿细,背景里出现木色桌面、黄色纸箱都会让二值图多出一大块。

MediaPipe Hands 检测的是手部关键点而不是肤色区域,内部是“手掌检测 + 关键点回归”两段式:先在整帧里找手掌位置,再在手部区域内回归 21 个关键点坐标。因为检测阶段用的是训练好的模型而不是颜色阈值,肤色、光照、背景复杂度的干扰被压到很低。关键是它 CPU 上就能跑实时,不需要单独配 GPU,这对手势控制鼠标这种桌面工具很重要。

另一个被忽视的点是,MediaPipe 输出的是每个关键点的三维坐标(归一化的 x、y 加相对深度的 z)和可见度。这套结构直接给后续的坐标归一化、CNN 输入甚至规则判断都留好了接口。如果自己训练手部检测模型,数据采集和标注成本会占掉整个项目一半以上的时间。

2.2 21 个关键点的坐标语义与动作相关性

MediaPipe Hands 把一只手定义成 21 个关键点,0 号是手腕,1 到 4 号是拇指,5 到 8 号是食指,9 到 12 号是中指,13 到 16 号是无名指,17 到 20 号是小指。每根手指从根部到指尖依次排列,比如食指的 5 号是掌指关节 MCP,6 号是近指关节 PIP,7 号是远指关节 DIP,8 号是指尖 TIP。

关键点编号名称与控制动作的关系
0WRIST坐标归零的基准点,几乎所有归一化都围绕它做
4THUMB_TIP拇指指尖,参与捏合、点击类手势
8INDEX_FINGER_TIP食指指尖,指针映射首选点
12MIDDLE_FINGER_TIP中指指尖,作为尺度归一化的参考距离端点
16RING_FINGER_TIP派生的 3 号和 4 号数字手势常依赖它区分
20PINKY_TIP小指指尖,区分 5 和 6、8 和 9 时的关键差异点

这些点里,移动指针最常用食指指尖 8 号,因为符合人的直觉,食指指向哪里光标就去哪里。拇指尖 4 号和食指尖 8 号之间的距离是很多“点击 / 捏合”手势的特征来源。而尺度归一化里我喜欢用 12 号点到 0 号点的距离,因为中指尖到手腕的距离相对稳定,不会因为单独某一根手指弯曲而变化太大。

2.3 坐标归一化:从绝对坐标到相对特征

MediaPipe 返回的 landmark 坐标是相对图像宽高的归一化值,范围在 0 到 1 之间。看着已经“归一化”,其实还不够:手在画面左上角和右下角时,同一根手指的坐标完全不同;手离摄像头近和远时,整手跨度也不同。直接把原始坐标喂给模型,模型会花大量容量去拟合“手在画面的哪个位置”,而不是“手是什么手势”。

常见的做法是以 0 号手腕点为原点,把所有坐标转成相对位移,再用一个尺度因子缩放。尺度因子我用中指尖到手腕的欧氏距离,它对摄像头距离变化最不敏感。

import numpy as np def normalize_landmarks(landmarks): # landmarks 是 mediapipe 返回的 21 个 landmark 对象 pts = np.array([[lm.x, lm.y, lm.z] for lm in landmarks], dtype=np.float32) center = pts[0] # 0 号点: 手腕,作为基准点 rel = pts - center # 去掉平移分量 scale = np.linalg.norm(pts[12] - pts[0]) # 中指尖到手腕的距离 return rel / (scale + 1e-6)

逻辑说明:先取出 21 个点的 x、y、z,0 号手腕做基准点,所有点减掉它之后,手在画面里的位置信息就被消除。尺度因子用中指尖 12 号点到手腕 0 号点的距离,除完之后,手离摄像头远近或者不同人手大小不同,坐标范围都会被压到相近的量级。加 1e-6 是防止手部检测异常时两点距离为 0 导致除零。

参数说明:这段函数同时适用于后续的关键点规则判断和关键点 CNN 输入。如果做的是图像 CNN 而不是关键点特征,归一化步骤换成裁剪手部区域并缩放到固定尺寸,见下一节。

2.4 用 OpenCV 截取手部区域并组织训练样本

图像 CNN 需要固定尺寸的输入,所以采集阶段要把手部区域从整帧里切出来。这个采集脚本本身也是整个项目跑通的第一步,我一般会先用它把 0 到 9 十个手势各采 200 张图,再开始训练。

import cv2 import mediapipe as mp import os mp_hands = mp.solutions.hands hands = mp_hands.Hands( static_image_mode=False, max_num_hands=1, min_detection_confidence=0.7 ) cap = cv2.VideoCapture(0) label_dir = "dataset/0" os.makedirs(label_dir, exist_ok=True) count = 0 while count < 200: 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: lm = result.multi_hand_landmarks[0].landmark h, w = frame.shape[:2] xs = [p.x * w for p in lm] ys = [p.y * h for p in lm] # 手部包围盒,四周扩 20%,避免指尖被切掉 x0 = int(min(xs) - 0.2 * (max(xs) - min(xs))) x1 = int(max(xs) + 0.2 * (max(xs) - min(xs))) y0 = int(min(ys) - 0.2 * (max(ys) - min(ys))) y1 = int(max(ys) + 0.2 * (max(ys) - min(ys))) x0, y0 = max(0, x0), max(0, y0) crop = frame[y0:y1, x0:x1] if crop.size == 0: continue crop = cv2.resize(crop, (96, 96)) cv2.imwrite(f"{label_dir}/{count:04d}.jpg", crop) count += 1 cv2.imshow("collect", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()

逻辑说明:static_image_mode 设为 False,让 MediaPipe 在连续帧之间复用上一帧的追踪结果,单帧处理更快。每检测到手,就从 21 个点的 x、y 坐标算出包围盒,向外扩 20% 防止截图边缘切到手指,然后裁剪、缩放到 96×96 落盘。count 到 200 自动停止,换标签目录后对下一个手势重复执行。

参数说明:min_detection_confidence 是手部检测置信度阈值,室内暗光或摄像头画质一般时建议调到 0.5,否则会频繁检测不到手;扩边比例 0.2 是经验值,扩太大背景噪声多,太小指尖被裁;目标尺寸 96×96 对这个任务够用,128×128 精度略高但训练和推理都会变慢。

3. CNN 手势分类:从单帧手部图像到 0 到 9 数字与指令

3.1 静态手势识别为什么必须上 CNN

有人会问:有了 21 个关键点,为什么不用角度和距离直接写规则?简单手势如握拳、张开可以,0 到 9 十个数字手势不行。手在摄像头前的旋转角度、手指弯曲程度、摄像头离手的远近,都会让同一种手势的关键点相对位置发生变化,规则阈值很难覆盖所有情况,经常是调好 3 和 8,又分不清 5 和 6。

CNN 卷积神经网络做的事情,是把原始裁剪图作为输入,在训练过程中自动学习边缘、指缝、指尖朝向这些特征,对平移和小角度旋转都更鲁棒。这里不需要很深的网络,手势识别是静态单帧分类,没有时序依赖,一个三到四层卷积的轻量网络就够用。真正决定准确率的往往不是网络深度,而是训练数据的采集一致性和归一化方式。

3.2 一个小型 CNN 网络结构与参数设计

我用 PyTorch 定义了一个三层卷积的小网络,参数量小,CPU 推理单帧在 10 毫秒以内,不会拖累整体帧率。

import torch.nn as nn class GestureCNN(nn.Module): def __init__(self, num_classes=10): super().__init__() self.features = nn.Sequential( nn.Conv2d(3, 16, 3, padding=1), # 96x96x16 nn.BatchNorm2d(16), nn.ReLU(inplace=True), nn.MaxPool2d(2), # 48x48x16 nn.Conv2d(16, 32, 3, padding=1), # 48x48x32 nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.MaxPool2d(2), # 24x24x32 nn.Conv2d(32, 64, 3, padding=1), # 24x24x64 nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(2), # 12x12x64 ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(64 * 12 * 12, 128), nn.ReLU(inplace=True), nn.Dropout(0.5), nn.Linear(128, num_classes), ) def forward(self, x): return self.classifier(self.features(x))

逻辑说明:输入是 3 通道 96×96 的裁剪图,经过三层“卷积 + 批归一化 + ReLU + 最大池化”,特征图从 96×96 逐层降到 12×12,通道数从 16 涨到 64。展平后接两个全连接层,输出 10 类对应 0 到 9。BatchNorm 放在激活函数之前,把每层输出拉回标准分布,训练时收敛更平稳。

参数说明:Dropout 0.5 加在全连接层前,对这种几千张规模的小数据集能明显压制过拟合。如果采集时把图存成了灰度图,第一层 Conv2d 的输入通道要改成 1。num_classes 按实际手势类别数改,如果项目里除了 0 到 9 还想加“握拳”“张开”等控制手势,就改成对应数量。

3.3 训练数据的目录约定与读取

数据组织方式直接影响训练脚本的复杂度。我按 train 和 val 两个大目录,里面各自放 0 到 9 十个类别子目录,子目录名就是标签名。

dataset/ ├── train/ │ ├── 0/ 0000.jpg 0001.jpg ... │ ├── 1/ ... │ └── 9/ ... └── val/ ├── 0/ ... └── 9/ ...

torchvision 的 ImageFolder 会按子目录名自动生成类别索引,读数据很省事。

from torch.utils.data import DataLoader from torchvision import datasets, transforms train_transforms = transforms.Compose([ transforms.RandomRotation(10), # 手在画面里会轻微旋转 transforms.ColorJitter(0.2, 0.2, 0.2), # 模拟室内光照变化 transforms.ToTensor(), transforms.Normalize([0.5], [0.5]), ]) train_data = datasets.ImageFolder("dataset/train", transform=train_transforms) train_loader = DataLoader( train_data, batch_size=32, shuffle=True, num_workers=2, pin_memory=True ) print(train_data.classes) # ['0', '1', ..., '9']

逻辑说明:ImageFolder 遍历子目录,把每个目录当成一个类别,返回的 sample 是(图像张量, 类别索引)对。RandomRotation 和 ColorJitter 在训练时每次读取都会随机变换,相当于扩充了数据集。Normorlize 用均值 0.5、标准差 0.5 把像素压到 -1 到 1 范围,加速收敛。

参数说明:RandomRotation(10) 角度不要给太大,因为采集时手的方向已经由 MediaPipe 框定,旋转 30 度以上会制造实际不会出现的样本。num_workers 在 Windows 上建议设 0 或 1,多线程 DataLoader 在 Windows 下偶尔会触发重复初始化报错,不是代码问题,降 worker 数就行。

3.4 训练与验证:损失曲线和混淆矩阵怎么看

训练循环本身不复杂,Adam 配交叉熵损失是默认组合。

import torch model = GestureCNN(num_classes=10) opt = torch.optim.Adam(model.parameters(), lr=1e-3) loss_fn = nn.CrossEntropyLoss() for epoch in range(50): model.train() total, correct = 0, 0 for images, labels in train_loader: opt.zero_grad() out = model(images) loss = loss_fn(out, labels) loss.backward() opt.step() correct += (out.argmax(1) == labels).sum().item() total += labels.size(0) if epoch % 10 == 0: print(f"epoch {epoch}: acc {correct / total:.3f}")

逻辑说明:每个 batch 前先清空梯度,前向算出预测和损失,反向传播后更新参数。acc 统计的是当前 epoch 内训练集的分类正确率。验证集单独跑一遍前向,不更新梯度。

参数说明:学习率 1e-3 是 Adam 的常用起点,训练到 30 轮后损失震荡不下降,就把 lr 降到 3e-4 继续跑几轮。看损失曲线时重点关注训练集和验证集准确率的差值:差值超过 5 个百分点是过拟合信号,优先调高 Dropout,而不是加数据增强。保存模型后一定要跑一轮混淆矩阵,0 到 9 里最容易混的是 5 和 6、8 和 9,因为它们在常见视角下手型轮廓接近。如果混淆集中在这几对,先检查采集时手朝向是否统一。

3.5 关键点特征与图像 CNN 的取舍

除了直接吃图像,CNN 也可以吃关键点特征。把 21 个归一化后的关键点 x、y、z 拉平成 63 维向量,用一维卷积或者两层全连接分类,好处是特征维度小、完全不受背景干扰、推理在毫秒级。坏处是数字手势之间的差异本身就在指尖位置的细微差别上,关键点坐标的回归误差会被分类器放大,精度一般不如图像 CNN。

这个项目的实操方案是:优先训练图像 CNN 做主识别器,同时把关键点归一化向量保留下来。一旦换摄像头或者换机位导致准确率下降,用关键点特征快速重训一个轻量分类器做对比,可以快速判断是采集数据问题还是模型结构问题。更进阶的做法是把 63 维关键点向量拼接到 CNN 全连接层后面,让模型同时看到原始像素和几何结构,代价是推理多几毫秒,但区分 8 和 9 这类易混手势时往往有明显改善。

4. 从识别结果到鼠标事件:控制逻辑与参数调优

4.1 指针移动的平滑策略与帧率适配

最直接的映射是拿食指指尖 8 号的归一化坐标,乘上屏幕宽高,得到光标目标位置。这个方案在静止时会有明显抖动,因为 MediaPipe 回归出的指尖坐标本身带噪声,手也不可能完全不动。常见做法是指数平滑加死区。

import pyautogui smooth_x, smooth_y = 0.0, 0.0 alpha, dead_zone = 0.35, 3 def move_pointer(tip_x, tip_y, screen_w, screen_h): global smooth_x, smooth_y raw_x = tip_x * screen_w raw_y = tip_y * screen_h # 指数平滑:新值权重 0.35,旧值权重 0.65 smooth_x = alpha * raw_x + (1 - alpha) * smooth_x smooth_y = alpha * raw_y + (1 - alpha) * smooth_y last_x, last_y = pyautogui.position() # 死区:移动量过小直接忽略,防止原地微颤 if abs(smooth_x - last_x) < dead_zone and abs(smooth_y - last_y) < dead_zone: return pyautogui.moveTo(smooth_x, smooth_y)

逻辑说明:先将归一化的指尖坐标乘以屏幕宽高得到屏幕坐标系下的目标点。平滑系数 alpha 控制新位置对旧位置的修正幅度,alpha 越大光标越跟手,但每帧噪声也越直接被放大;alpha 越小越稳,但大幅移动时会有明显拖尾。死区逻辑用 pyautogui 读取当前鼠标位置,平滑后的目标位置和当前位置差距小于 3 像素就不执行移动。

参数说明:这个示例里 alpha 取 0.35 适合 30fps 左右的帧率。如果摄像头能跑到 60fps,可以降到 0.25 左右,因为帧率高时单帧噪声影响小;如果帧率只有 15fps,alpha 建议提到 0.45,否则光标移动会非常迟钝。死区 3 像素是经验值,屏幕分辨率高时可以适当加大到 5。

4.2 点击、拖拽、右击的阈值与防抖

识别模型的输出是每一帧的独立结果,哪怕模型准确率 98%,也架不住连续 30 帧里偶尔出两帧错检。直接拿单帧标签触发鼠标事件,会出现连点、误点。业界通用做法是加一个简单的状态机,让同一标签连续出现 N 帧后才触发动作,并且动作触发后进入冷却时间。

import time class GestureAction: def __init__(self, hold_frames=6, cooldown=0.3): self.hold = hold_frames self.cooldown = cooldown self.count = 0 self.last_action = 0 self.last_time = time.time() def update(self, label): now = time.time() if now - self.last_time < self.cooldown: return None if self.last_action == label: self.count += 1 else: self.last_action = label self.count = 1 if self.count >= self.hold: self.last_time = now self.count = 0 return label return None

逻辑说明:update 方法每帧调用一次,传入 CNN 的识别结果。先检查是否在冷却时间内,是就直接返回 None。然后判断当前帧标签和上一帧是否相同,相同就累计 hold 计数,不同就重置。累计到 6,认为这个手势稳定出现,触发一次动作并清空计数、记录冷却时间。

参数说明:hold_frames 设 6,在 30fps 下相当于 0.2 秒的确认窗口,能过滤掉绝大多数单帧误检。cooldown 设 0.3 秒,防止一次手势在持续期间内触发多次鼠标事件。拖拽类手势触发后不要立即冷却,而是用手势持续状态配合位移阈值判断,见下一节。

4.3 0 到 9 手势到鼠标动作的映射表

手势最终要落到具体鼠标操作上,映射表按自己的操作习惯调整,这里给出一套完整可用的参考。

手势鼠标动作触发方式
0左键单击GestureAction 触发后立即执行
1双击触发后执行 pyautogui.doubleClick()
2右键单击触发后执行 rightClick()
3拖拽模式触发后 mouseDown(),手势结束 mouseUp()
4向上滚动触发后执行 scroll(3)
5向下滚动触发后执行 scroll(-3)
6指针加速模式切换 alpha 到 0.15,进入精准模式
7空闲锁定指针保持不动,不响应移动
8取消当前操作触发后执行 mouseUp(),终止拖拽
9退出程序触发后调用 sys.exit()

映射设计的原则是高频操作优先给最自然的手势。0 到 9 里,握拳对应 0,单指伸出对应 1,这两类手势最容易做也最稳定,所以留给左键单击和双击。拖拽放进 3,因为要按住不放,数字 3 的三根手指姿态在视频里容易保持稳定。6 号切换精准模式是为了做精细操作时关掉平滑,代价是抖动变大,适合从文件夹拖动小文件这种场景。

4.4 手部丢失、误检与多帧投票处理

手移出画面或者被身体挡住时,MediaPipe 会返回空结果,这时候不能沿用上一帧坐标,否则光标会突然甩到屏幕角落。处理方式是在主循环里维护一个 lost_frames 计数器,连续 20 帧检测不到手就清空平滑值,让光标停在原地。检测到手但置信度低于下限的帧直接丢弃,不送入 CNN 也不更新平滑状态。

CNN 输出的误检再用一个滑动窗口做多数投票。

from collections import deque, Counter vote_queue = deque(maxlen=5) def robust_label(probabilities): # probabilities 是 CNN 输出的 10 类得分向量 vote_queue.append(probabilities.argmax()) if len(vote_queue) < vote_queue.maxlen: return None label, count = Counter(vote_queue).most_common(1)[0] return label if count >= 3 else None

逻辑说明:把最近 5 帧的预测类别存进一个定长队列,每帧把队首旧值挤掉。队列不满时不返回结果,保证至少看到 5 帧后再做判断。用 Counter 统计 5 帧里各类别出现次数,出现 3 次及以上的标签才被认为是稳定结果。

参数说明:deque 的 maxlen 和投票阈值 3 是配套的,5 帧里同类别过半数即采纳。这个方案把单帧误检降低到几乎消失,代价是动作响应延迟增加约 2 帧,30fps 下大约 67 毫秒,人基本感知不到。如果觉得延迟明显,把窗口缩到 3 帧、阈值设 2,误检率会略升但响应更快。

提示:投票和多帧确认是两个环节,先做投票得到稳定标签,再用 GestureAction 做动作防抖,两者不要合并成一个计数器,否则调参时很难定位到底是谁在丢帧。

5. 打包 exe 的注意事项与几个提升体验的技巧

5.1 PyInstaller 打包:模型路径与 MediaPipe 的隐藏导入

PyInstaller 打包这种项目,最常翻车的地方是模型资源和 MediaPipe 的动态库没有被带进 exe。程序运行时资源文件的路径不能直接写相对路径,因为一档打包后代码被解压到临时目录,当前工作目录并不是程序所在目录。用 sys._MEIPASS 拿解压路径是标准做法。

import sys import os def resource_path(rel): base = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__))) return os.path.join(base, rel)

这个函数运行时,如果是在 PyInstaller 打包后的环境里,_MEIPASS 存在,取它作为基准路径;如果是直接跑 Python 脚本,_MEIPASS 不存在,退回脚本所在目录。模型加载时写 model.load_state_dict(torch.load(resource_path("model.pth"))) 就能兼容两种情况。

pyinstaller -F -w mouse_control.py \ --collect-all mediapipe \ --add-data "model.pth;." \ --add-data "config.json;."

参数说明:-F 打单文件 exe,-w 隐藏控制台窗口,但调试阶段建议去掉 -w,不然看不到报错信息。--collect-all mediapipe 会把 MediaPipe 的 proto 文件和动态库完整收进包,漏掉这一项最常见的症状是 exe 启动后提示找不到 mediapipe 相关模块。--add-data 在 Windows 下的分隔符是分号,冒号是 Linux 的写法,写错会直接打包失败。

5.2 不同摄像头分辨率下的自适应缩放

MediaPipe 输出的坐标是归一化的,天然不依赖摄像头分辨率,但裁剪时用的包围盒计算依赖 frame.shape。如果代码里写死了 640×480,换一个 1280×720 的摄像头,裁剪框比例就错了。所有涉及图像尺寸的地方都用 h, w = frame.shape[:2] 现场取,不要另外定义常量。

摄像头分辨率变了还要重测两个参数:扩边比例和 CNN 输入尺寸。分辨率提高后手部区域像素更多,扩边比例可以适当减小,比如从 0.2 降到 0.15,减少背景噪声;分辨率降低但帧率升高,则优先保证实时性,CNN 输入 96×96 不要轻易加大到 128×128。设计文档里把这些依赖项写清楚,换设备时照着检查。

5.3 常见问题排查顺序与帧率优化技巧

按这套顺序排查,能覆盖大部分运行问题。OpenCV 打不开摄像头,先把 VideoCapture 的 index 从 0 换到 1,再检查摄像头是否被其他软件占用。MediaPipe 一启动就报错,先看 Python 版本和 MediaPipe 的兼容性,最常见的坑是 Python 3.11 以下版本装不上新版 MediaPipe,换个 Python 3.9 环境通常能解决。CNN 推理正常但帧率只有个位数,先看画面窗口是不是用 imshow 在阻塞主循环,把识别逻辑放到独立线程,主线程只负责显示和响应键盘事件。

帧率优化通常按这个顺序尝试:先降摄像头采集分辨率,从 640×480 降到 320×240;再关掉画面显示窗口,结果日志改用终端打印;最后才是换更小的 CNN。多数情况下,把 MediaPipe 和 CNN 推理放到同一个后台线程,用队列把识别结果传给主线程,就能从 15fps 拉到 30fps,这个改动比换轻量模型的收益更直接。

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

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

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

立即咨询