1. 需求拆解与技术选型思路
1.1 这个项目到底在做什么
先把这个项目掰开揉碎说清楚。标题看起来长长一串,但其实核心就一件事:用 YOLOv11 训练一个能识别手势的深度学习模型,再把它封装成一个带登录注册的桌面应用,让普通用户打开摄像头就能实时检测“握拳、比心、竖大拇指”这类动作。
这类项目在高校课题设计、个人作品集、公司内部 demo 里都非常常见。它天然包含了三个技术板块:目标检测算法、数据集构建、GUI 交互开发。任何一个单独拎出来都够写一篇教程,但把三者串成一个闭环系统,才是这类项目真正的价值所在。我见过不少同学拿到类似课题以后一头扎进训练代码里,模型一通猛训,最后卡在“怎么把模型交给别人用”这个问题上。所以这篇博文我会按照完整项目落地的顺序来写,从环境搭建一路讲到打包交付。
先说结论:这套系统适合三类人参考。第一类是正在做毕设或课设的学生,需要把深度学习模型做成一个能演示的系统;第二类是刚入门目标检测、想做个小而全项目练手的开发者;第三类是想给内部团队做一个人脸或手势识别小工具的从业者。无论哪类人,只要跟着这篇文章把流程完整走一遍,都能收获一套能跑、能演示、能答辩的完整项目。
1.2 为什么是 YOLOv11 而不是别家
选型这件事,我建议你从实际约束倒推,而不是从热度正推。很多课题一上来就说“基于深度学习”,具体用什么网络反而没说。这时候你的任务不是选一个最新的模型,而是选一个“你够得着、能训动、跑得起来”的模型。
YOLOv11 是当前 YOLO 系列里最新的一代,我选择它的理由有三个。
第一,Ultralytics 官方封装得相当完善,安装一个包就能用,训练、验证、导出一条龙,这对项目周期紧张的人来说太重要了。同时它还兼容 YOLOv5/v8 的数据格式,之前积累的标注数据几乎不用改就能迁移到新模型上。
第二,模型体积和精度之间的平衡比较好。yolo11n 模型参数量小,CPU 上跑推理虽然不快,但在 GPU 上能做到很高的帧率。对实时手势识别这种任务来说,检测速度往往比极致精度更关键。你想想,用户对摄像头做一个手势,结果画面卡了大半秒才出结果,体验就非常糟糕。
第三,Anchor-Free 的设计对新手很友好。传统 YOLO 系列需要理解 anchor box 的概念,而 v8 以后的版本直接简化了这个过程,数据标注和训练参数都更直观。
当然,我也得说句公道话。如果你要做的是非常细分的手势识别,比如手语翻译这种需要精细关键点判断的任务,纯检测模型并不够用,还得配合关键点模型或者换用其他网络结构。但如果是“识别几个固定手势并给出反馈”的场景,YOLOv11 是性价比最高的选择。
1.3 整体架构与工作流程
整个系统从用户视角看只是一个窗口、一个摄像头画面、一个登录框,但底层其实是三条并行工作流。
第一条是数据流:摄像头采集帧,送入 YOLOv11 模型进行推理,输出检测框、类别、置信度,然后在 UI 上绘制出来。第二条是业务流:用户打开软件先登录或注册,系统校验用户信息后进入主界面,才能启动摄像头检测。第三条是管理流:登录信息存储、检测结果统计分析、模型文件加载,这些属于后台逻辑,用户不一定看得见,但缺一不可。
从代码架构上讲,我把它分成了四个模块:
- 模型模块:负责加载 ONNX 或 PyTorch 格式的 YOLOv11 模型,封装一个 detect 函数,输入一帧图像,输出检测结果。
- 数据集模块:包含数据采集脚本、标注转换脚本和数据增强配置,这部分是训练之前的工作。
- 训练模块:依托 Ultralytics 框架,负责配置模型参数、启动训练、保存权重。
- UI 模块:基于 PyQt5 实现登录注册和主检测界面,同时用多线程技术把推理放到子线程里,保证界面不卡顿。
这四个模块之间的依赖关系是单向的。也就是说,UI 层只调用模型模块的接口,模型模块不关心 UI 长什么样。这种分层设计有两个实际好处:一是以后想加表情识别、点头检测等功能,直接往模型模块里加就行了,不用动界面逻辑;二是如果训练阶段出了什么问题,你可以在命令行里用脚本验证模型,而不必每次都打开 UI 才能看到结果。
2. 开发环境搭建与数据集准备
2.1 环境配置:版本一致性的坑
这个项目对硬件要求不算离谱,但也别太乐观。训练阶段建议有一块不低于 6GB 显存的 NVIDIA 显卡,推理阶段倒是宽松很多,CPU 也能跑,只是帧率上不去。我在准备环境的时候踩过一个特别典型的坑:GPU 驱动和 PyTorch 版本不匹配,导致 CUDA 识别不到。这个问题排查了整整一个晚上,最后发现是 PyTorch 装成了 CPU 版,教训非常深刻。
我给你们一个可以直接照抄的环境清单:
Python 3.10 CUDA 11.8 PyTorch 2.x ultralytics PyQt5 onnxruntime-gpu(推理阶段用) opencv-python安装顺序很讲究。建议先在本机装好 NVIDIA 驱动和对应版本的 CUDA,然后用 conda 创建一个干净环境,最后再装 PyTorch。
conda create -n hand_gesture python=3.10 -y conda activate hand_gesture pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics pip install PyQt5 onnxruntime-gpu opencv-python这里有个容易忽略的细节:ultralytics 这个包会自动依赖某个特定版本的 torch,如果你先装了 torch 再装 ultralytics,它通常会兼容;但如果你先装了 ultralytics 再补 torch,很可能会被自动装上最新版 torch,导致和 CUDA 版本对不上。所以我习惯先在 conda 里装好指定版本的 torch,再装其他依赖。
提示:如果训练时提示 CUDA unavailable,先在 Python 里执行
torch.cuda.is_available()。如果结果是 False,基本可以确定是 PyTorch 的 CUDA 版本与驱动版本不兼容,不要盲目重装驱动,先卸载 torch 换成匹配版本。
2.2 数据集从哪里来:公开数据加自采补充
数据集是这个项目的命门。标题里明确写了“YOLO数据集”,说明模型是 YOLO 格式的标注数据,每张图对应一个同名的 txt 文本,里面每行是类别编号 x_center y_center width height,坐标是归一化到 0~1 的。
手势识别数据集的来源通常分两种。第一种是直接用公开数据集,比如某个开源手势数据集包含 18 类手势、几十万张图片,数量完全够用。但这里要注意两点:第一,公开数据集的类别定义不一定符合你的需求,比如它把“数字1到9”都作为独立类别,而你可能只想要“点赞、比心、挥手”这几种;第二,公开数据集的图片大多是在固定环境下拍的,光照、角度、背景相对单一,直接部署到真实摄像头下,泛化效果可能打折扣。
因此我强烈建议采用“公开数据做基础,自采数据做补充”的组合方案。具体做法是:
- 从公开数据集中筛选你需要的类别,只保留这些类别的图片和标注。
- 自己录一段 30 秒的视频,用手机或摄像头对着不同背景、不同光照条件下做出目标手势,然后用脚本从视频里抽帧,大概收集三五百张图。
- 把这些补充图片用标注工具标注一遍,格式统一转成 YOLO 格式。
我之前做一个手势系统的时候,经验数据是这样的:只使用公开数据集训练,mAP50 大约在 0.85 左右,但在自然光环境下误检明显变多;加了自采数据以后,mAP50 提升了不到两个点,但实际使用体验好了不少,误检次数明显下降。这说明自采数据的价值不在涨点,而在提升模型的抗干扰能力。
公开数据集的下载这里不展开太细,只说一个筛选技巧:尽量选图片尺寸接近 640×640 的数据,如果图片太大太多,可以先统一缩放到 640 再标注,这样训练时解码更快。另一个技巧是查看数据集的类别标注分布,如果某些类别数量特别少,要么做数据增强补样本,要么干脆上报给数据提供方看有没有其他版本。
2.3 标注规范与数据校验
标注阶段很多人会直接上手硬标,结果标到后面发现类别定义不一致,返工一次就崩溃。我建议在动手之前先花半小时写一个标注规范文档,哪怕就是几条简单规则:
- 一类界定的原则:比如“手比心”和“手指交叉”这种容易混淆的手势,需要约定“以手指接触或遮挡为分界”。
- 遮挡时的处理:如果手势被另一个物体遮挡超过一半,就不标注。
- 模糊帧的处理:低于一定清晰度的模糊帧,直接删除,不强行标注。
标注工具我用的是 LabelImg,它轻量、稳定,还能直接导出 YOLO 格式。流行方案里也有用 Label Studio 做 Web 端标注的,如果你团队协作多人标注,可以用那套;如果是个人项目,LabelImg 足够了。
标注完成以后,千万不能直接拿去训练。先做一次数据校验,把坏数据、空标注、越界坐标清理干净。具体操作是写一个脚本,遍历所有 txt 标注文件,做四件事:
- 读取每行内容,检查类别编号是否大于类别数减一。
- 检查 x_center、y_center、width、height 是否在 0~1 之间。
- 打开对应图片,确认图片没有损坏,能正常读取。
- 统计每个类别的图片数量和标注框数量,输出分布表。
我实际遇到过的情况是:用标注工具保存时偶尔会多保存一个空行,模型训练时把空行解析成一条无类别标注,导致训练进程报错。这种低级问题,一个校验脚本就能挡住。
还有一个很容易被忽略的点:数据集的目录结构必须遵守 YOLO 格式约定,否则 ultralytics 加载时会直接报错。目录结构我贴出来:
dataset/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/图片和标注文件的文件名必须一一对应,比如0001.jpg对应0001.txt。训练集和验证集的比例我习惯设为 8:2,如果数据总量特别少(少于 2000 张),可以把验证集比例适当调高到 3 成,以更准确评估泛化能力。
3. 模型训练与效果调优
3.1 训练启动与超参数解析
数据准备好了,就到了最关键的部分:训练。Ultralytics 框架把训练命令简化到一行,但一行命令背后藏着不少参数要理解。我先贴出我常用的训练脚本:
yolo detect train \ data=hand_gesture.yaml \ model=yolo11n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ project=runs \ name=gesture_exp \ pretrained=True \ optimizer=autohand_gesture.yaml是数据配置文件,内容很简单:
path: /home/user/dataset train: images/train val: images/val names: 0: fist 1: thumbs_up 2: peace 3: heart逐条分析超参数的选择逻辑。
epochs设置为 100,这个数值不是拍脑袋定的。手势识别属于相对简单的目标检测任务,类别少、目标尺寸大,通常在 80~120 个 epoch 内可以收敛。设得太少,模型欠拟合;设得太多,浪费时间且容易过拟合。如果你的数据集规模比较小(比如 2000 张以内),建议把 epochs 减到 80 左右。
batch设成 16 的前提是显存至少有 8GB。如果你的显卡只有 6GB,可以把 batch 降到 8,同时把imgsz从 640 降到 512。不要小看这个调整,显存不足时会爆内存,训练中途直接崩掉。
pretrained=True表示使用 COCO 预训练权重做迁移学习。不建议改成 False。因为 COCO 数据集包含人、手等常见类别,预训练模型已经具备了很强的视觉特征提取能力,用它的初始化参数做微调,收敛速度快,精度高。这对新手来说是百利而无一害。
3.2 训练过程怎么看:loss 曲线与指标
训练启动后,终端里会实时输出每一轮的损失值,同时每隔一定轮次保存模型到runs/gesture_exp/weights/。很多第一次训练的人看到 loss 数值就紧张,以为 loss 越小越好,这其实是个误解。我们需要关注的是 loss 的下降趋势,而不是绝对值。
我训练时习惯打开两个东西:第一个是训练日志,关注 box_loss、cls_loss 这两个指标,它们分别代表边界框损失和分类损失;第二个是验证集指标,关键是 mAP50 和 mAP50-95。mAP50 代表在 IoU 阈值为 0.5 时的平均精度,mAP50-95 则是一个更严格的综合指标。
经验上来说,一个合格的手势检测模型,在验证集上 mAP50 至少应该达到 0.85 以上,mAP50-95 在 0.6 以上。如果你的模型差得远,先别急着调整网络结构,我建议按这个顺序排查:
- 先看训练集的 loss 有没有降到接近稳定值。如果训练 loss 一直很高,说明模型还没学会,可能是学习率太大、数据标注有问题、或样本数量太少。
- 再看验证集指标和训练集指标差距。如果训练指标很好但验证指标差很多,那就是过拟合,需要增加数据增强、加入更多验证数据、或提前停止训练。
- 最后看模型在单独图片上的可视化效果。画出来的检测框是否有明显偏移、有没有把背景误检成手势。
我踩过的最典型一个坑是:训练到 30 个 epoch 时,训练 loss 降得很漂亮,mAP 却几乎没有变化。后来仔细检查发现,训练脚本里data参数的路径写错了,加载到的是另一个更简单的数据集,导致模型虽然在一个简单的分布上收敛了,但在真实验证集上表现平平。所以每次训练前,务必用一行代码确认数据加载是否正确:
from ultralytics.data import YOLODataset ds = YOLODataset('hand_gesture.yaml') print(len(ds))3.3 小技巧:脏数据过滤与类别平衡
很多人以为标注完就能直接训练,实际上绝大多数数据集中都藏着一定比例的脏数据,包括错标、漏标和标注框过大过小。在训练之前,我建议先做一次“人工抽检”。
具体操作是这样的:写一个可视化脚本,把数据集中每张图和对应的检测框画出来,输出到一个文件夹里,然后肉眼翻看。不需要看全部图片,抽20%到30%就行。我每次做这种抽检都能找到几个明显的标注错误:要么是把旁边的人手也标进去了,要么是标注框中心明显偏离手势主体。这类脏数据在训练早期的影响很大,模型会学着预测错误的定位。
类别不平衡也是手势识别里常见的问题。比如“比心”这类动作大家喜欢做,收集了 3000 张;而“单手比数字4”冷门,只有 300 张。这种数据不平衡会导致模型对少数类别的识别效果很差。
处理办法不复杂,优先推荐这几种:
- 对少数类别的图片做数据增强,比如随机旋转、平移、缩放、改变亮度对比度,生成更多的变体样本。
- 在损失函数层面,Ultralytics 内部对类别权重有一定处理,但也可以手动给少数类别的 loss 分配更高权重。
- 如果差距过大(比如 10 倍以上),干脆砍掉少数类别,只保留常见手势。别心疼,项目能用比类别全更重要。
另外我建议训练时开启早停机制。Ultralytics 支持patience参数,设置当验证集指标连续多少个 epoch 没有改善就自动停止。这样你设 100 个 epoch,可能 60 个 epoch 就收敛了,剩下的时间可以拿来做其他事。
4. UI界面与登录注册模块的实现
4.1 登录注册功能:安全不能省
很多类似项目里,登录注册只是做个样子,密码明文存在本地配置文件里。这种做法在课设里能应付,但如果这是一个要交付给外部使用者的系统,就有点说不过去了。哪怕只是一个小工具,用户也会在意自己的账号信息是不是被安全存储的。
我采用的方案是 SQLite 存用户信息,密码用哈希加盐的方式保存。SQLite 轻量、免安装、适合桌面应用;哈希加盐则保证即使数据库文件泄露,攻击者也不能直接拿到明文密码。这里我贴一个简化的实现逻辑:
import hashlib import os import sqlite3 def hash_password(password: str, salt: str = None) -> (str, str): if salt is None: salt = os.urandom(16).hex() hashed = hashlib.pbkdf2_hmac( 'sha256', password.encode('utf-8'), salt.encode('utf-8'), 100_000 ).hex() return salt, hashed def register(username: str, password: str): conn = sqlite3.connect('users.db') cursor = conn.cursor() cursor.execute('SELECT id FROM users WHERE username = ?', (username,)) if cursor.fetchone(): return False, "用户名已存在" salt, hashed = hash_password(password) cursor.execute( 'INSERT INTO users (username, salt, password_hash) VALUES (?, ?, ?)', (username, salt, hashed) ) conn.commit() conn.close() return True, "注册成功"这里有几个细节要注意。第一,password_hash是哈希结果,不是原文。第二,每个用户的salt是随机生成的,也存进数据库。校验登录时,取出对应用户的 salt,再对输入密码做同样的哈希计算,比对结果。第三,pbkdf2_hmac的迭代次数100_000不是随意设的,这个数的意义在于加大暴力破解的成本。虽然我们只是个手势识别工具,但万一被拿到数据库文件,明文密码就全暴露了,用户在其他平台可能还用了同一个密码,风险就扩散了。
注册登录界面的设计,我用的是 PyQt5 的 QStackedWidget,在登录页和注册页之间切换。窗口本身用 QMainWindow 承载,登录成功后跳转主检测窗口。这种做法的好处是用户流程清晰,代码结构也好维护。
4.2 检测主界面的设计
主界面是项目的脸面,也是答辩时评委最先看到的部分。我设计的布局比较简单清晰:上边一行按钮,中间是摄像头画面显示区域,右边固定宽度放检测信息面板。检测信息面板实时显示当前帧的检测数量、各类别的置信度平均值、模型推理耗时。这个信息不是为了炫技,而是让你在答辩时能一眼看出系统的实时性能。
界面代码的核心结构大概是:
class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("手势识别检测系统") self.setMinimumSize(960, 600) central_widget = QWidget() self.setCentralWidget(central_widget) layout = QHBoxLayout() # 左侧视频区域 self.video_label = QLabel("摄像头画面") self.video_label.setAlignment(Qt.AlignCenter) layout.addWidget(self.video_label, stretch=3) # 右侧信息面板 info_panel = QVBoxLayout() self.btn_start = QPushButton("开始检测") self.btn_stop = QPushButton("停止检测") self.label_count = QLabel("当前手势数量: 0") self.label_fps = QLabel("FPS: 0") info_panel.addWidget(self.btn_start) info_panel.addWidget(self.btn_stop) info_panel.addWidget(self.label_count) info_panel.addWidget(self.label_fps) layout.addLayout(info_panel, stretch=1) central_widget.setLayout(layout)这里有个容易被忽略的点:OpenCV 读取的帧是 BGR 格式,而 QLabel 显示需要 QImage 或 QPixmap,且 QImage 默认期望 RGB 格式。如果直接显示,你会发现画面偏蓝偏红。正确做法是先转成 RGB 再构造 QImage。
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch = frame_rgb.shape bytes_per_line = ch * w qimg = QImage(frame_rgb.data, w, h, bytes_per_line, QImage.Format_RGB888) self.video_label.setPixmap(QPixmap.fromImage(qimg).scaled( self.video_label.size(), Qt.KeepAspectRatio))4.3 摄像头实时检测的线程处理
UI 开发里最容易犯的错误,是把耗时操作直接丢在主线程里做。比如打开摄像头后,每取一帧就调用一次模型推理。模型推理耗时可能在几十毫秒到几百毫秒之间,一旦推理时间偏长,界面就会“卡死”,用户点击按钮毫无反应,看起来就像程序崩溃了。
正确做法是:把摄像头采集和模型推理全部放进一个 QThread 子线程,主线程只负责接收结果并刷新界面。这样即使推理耗时较长,也不影响界面按钮响应。
我贴一个线程类的骨架:
class DetectionThread(QThread): frame_ready = pyqtSignal(object, list, float) def __init__(self, model_path): super().__init__() self.model = load_model(model_path) self.running = False def run(self): cap = cv2.VideoCapture(0) while self.running: ret, frame = cap.read() if not ret: continue start = time.time() results = self.model.predict(frame, conf=0.5) infer_time = time.time() - start self.frame_ready.emit(frame, results, infer_time) time.sleep(0.03) def stop(self): self.running = False self.wait()主线程里连接信号:
self.detection_thread = DetectionThread("best.pt") self.detection_thread.frame_ready.connect(self.update_frame) self.detection_thread.start()然后在update_frame槽函数里绘制检测框、更新信息面板。这样界面始终保持流畅,不会出现点击按钮半天没反应的情况。
注意:线程里的
time.sleep(0.03)是强制控制帧率上限,防止摄像头采集过快占用过多 CPU。实测下来,把推理帧率限制在 30 FPS 左右,界面能维持良好交互体验,系统资源占用也不会太高。
5. 常见问题与排查实录
5.1 检测不准怎么排查
检测不准是这类项目里反馈最多的问题。表现各不相同:有的检测框偏小,只框住手的一部分;有的把路人误检成目标手势;有的在某个角度下完全不识别。
我习惯把这类问题按发生环节拆开排查。
先看置信度。默认我是把conf参数设为 0.5。如果你发现很多目标手势没有被检测出来,可以试着把置信度阈值降到 0.25,看看是不是误检率上升。如果降阈值后召回率明显提升,说明模型本身还有能力,只是阈值设太高;如果降阈值也没变化,问题就要往数据和模型层面去找。
再看训练数据。排查思路是统计训练数据里图片的拍摄角度分布。如果大部分图片都是正对镜头拍的,模型对侧脸或低头角度的手势就很难识别。解决办法是补充不同角度的数据,并在训练时加强旋转和透视变换的数据增强。
最后看模型尺寸。如果你用的是 yolo11n(nano 版本),它的精度上限比 s、m 版本要低。如果数据本身没问题,可以考虑换成 yolo11s 重新训练,虽然推理耗时多一点,但精度会明显提升。硬件允许的话,yolo11m 也可以试。
我实际遇到过一个很有趣的排查案例:某个手势类别在训练集里识别率很高,一部署到摄像头就直接失效。后来发现,训练数据里的图片大多是白色背景,而实际使用场景是杂乱的桌面背景,模型学到的其实是“背景+手势”的组合特征,换了个背景就失灵。这种情况的处理方式,是专门采集一批贴近真实使用场景的图片,作为训练集的一部分。这也是我前面强调自采数据很重要的原因。
5.2 界面卡顿与崩溃
界面卡顿多半原因就一个:耗时操作在主线程执行。排查方法很简单,用 PyQt5 自带的调试工具打开线程监控,或者干脆在代码里打日志,看推理函数的调用时间和主线程事件响应时间。如果发现主线程里存在大于 50ms 的耗时操作,基本就是要调整线程模型了。
还有一种特殊情况是摄像头打开失败。有些电脑的摄像头索引不是 0,可能是 1 或 2。安装一个摄像头调用测试脚本,遍历索引找到可用通道:
def find_camera(): for i in range(3): cap = cv2.VideoCapture(i) if cap.isOpened(): print(f"camera index: {i}") cap.release() return i return -1另外摄像头初始化以后,要记得设置帧尺寸。很多人忽略了这一步,导致 OpenCV 默认采集成 320×240 的低分辨率画面,模型推理效果大打折扣。建议统一设置成 1280×720 或 960×540:
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)界面崩溃还有一种比较隐蔽的原因:QLabel 显示 QImage 时,QImage 引用的数据是内存中的地址,如果帧对象在信号传递过程中被垃圾回收了,Qt 访问时就会段错误。解决办法是确保 QImage 的生命周期覆盖到刷新显示之后,通常是把 QImage 对象作为信号参数传递,而不是只传递 QPixmap。
5.3 部署与交付注意事项
项目收尾阶段,很多人的做法是把 Python 脚本、权重文件、数据集一股脑交给导师或客户就完事了。但作为交付物,这远远不够。
我建议至少做好三件事。
第一,写好 README。内容包括:环境版本、安装命令、训练命令、运行命令、目录结构说明、常见问题入口。这份文档的价值在你答辩、面试、交接给别人的时候会体现得淋漓尽致。
第二,打包成可执行文件。如果对方电脑没有 Python 环境,你交付一堆 .py 文件等于白做工。用 PyInstaller 把主程序、模型文件、配置文件一起打包成 exe。打包命令大概是:
pyinstaller -F -w -n GestureApp \ --add-data "best.onnx:models" \ --add-data "users.db:." \ main.py-w表示不显示控制台窗口,-F表示打包成单个文件。不过单文件模式体积偏大,我第一次打包的时候把 ONNX 模型一起打进去,生成文件直接到了 300MB 以上。如果不追求单文件,可以用-D模式生成文件夹,这样启动速度更快。
第三,模型导出要选对格式。PyTorch 格式的.pt文件在部署时需要配套的 Python 环境。我更推荐在交付阶段导出成 ONNX 格式,配合 onnxruntime 做推理。这样一来,目标机器上只需要装一个轻量级的 onnxruntime,不需要装完整 torch,大大简化部署环境。
导出命令很简单:
from ultralytics import YOLO model = YOLO("runs/gesture_exp/weights/best.pt") model.export(format="onnx", opset=12)导出以后建议快速用一个脚本验证,确保 ONNX 模型的输出和 PyTorch 模型一致,再交付出去。
一些个人经验
这个项目做下来,我最深的体会是:技术难点其实不在模型本身,而在工程整合。YOLOv11 的开源程度和 ease of use 已经做得非常好了,训练一个能用的手势识别模型,一周之内就能完成。真正花时间的反而是数据质量、UI 交互、异常处理这些琐碎但决定成败的细节。
我给准备做同类项目的人三个建议。第一,数据集阶段宁可多花三天,也不要急着开始训练。一个干净、合理的数据集,直接决定了模型上限;后面再补数据、再调标注,成本是成倍增加的。第二,训练之前写好数据校验脚本和可视化脚本,磨刀不误砍柴工。第三,UI 和模型分成两个独立模块,先命令行验证模型效果,再接入界面,排错会轻松得多。
最后分享一个小技巧:如果推理速度不达标,除了换小模型、降低分辨率,还可以考虑把图像 resize 到 320×320 再做推理。对手势检测这种大目标场景来说,320 和 640 的精度差距并不大,但帧率能提升一倍。实测下来,从 640 降到 320,mAP50 只掉了约两个点,而推理耗时从 25ms 降到了 12ms。这个取舍在实时交互场景里非常值得。