资讯动态

基于YOLOv11的石头剪刀布手势识别检测系统:从数据集到PyQt5界面全流程

发布时间:2026/10/19 2:36:29 来源:尧图企业网站定制
石头剪刀布这个题目看起来简单真做起来你会发现它其实是把深度学习、数据集工程、界面开发、用户系统全部串起来的一条完整链路。我最初以为随便找个三分类模型改改就能交差结果卡在数据标注、界面卡顿、登录模块这些看似不起眼的地方反反复复试了好几版才跑通。这篇文章就把我基于YOLOv11做的这套完整检测系统从数据集整理、模型训练到UI联调的整个思路和实操过程梳理出来希望能帮到正在做类似项目或者准备做毕设、课设的朋友。1. 项目定位与技术选型一个手势识别系统为什么值得选YOLOv111.1 这套系统到底要交付什么先把这个项目的边界说清楚。标题里的“石头剪刀布检测系统”不是只做一个能识别三种手势的模型而是一个可以真正打开运行的桌面应用整体拆开包含四层训练层准备一套YOLO格式的石头剪刀布数据集训练出能识别rock、paper、scissors三种手势的YOLOv11模型。检测层用Python加载训练好的权重文件对图片或者摄像头画面做目标检测输出每个手势的检测框、类别和置信度。应用层做一个带登录注册的桌面UI界面用户登录后打开摄像头界面里实时显示检测视频流并对三种手势的出现次数、胜负关系做统计。交付层整个项目包含Python源码、权重文件、数据集目录和UI代码拿到手后配置环境就能复现出来。这四层对应了一个完整项目的“可演示性”。训练一个模型容易但能把模型包装成一个用户能操作、能演示、能截图汇报的系统才是这类题目真正的价值所在。1.2 对比过的方案为什么最后还是选了YOLOv11在做这个项目之前我认真考虑过三条路这里我直接说结论。第一条路是用MediaPipe的手势识别方案。MediaPipe确实开箱即用能检测手部关键点也能区分简单手势但它做的是“手部姿态估计规则判断”不是自定义目标检测。想让它识别你定义的特定手势、输出一个框住手部的矩形检测框其实挺别扭的而且它不支持基于自定义数据集去训练模型等于绕开了深度学习训练环节很多题目根本不买账。第二条路是自己用卷积神经网络写一个三分类的图像分类器。这个方式训练快、结构简单但它只能判断“当前画面里的手势是什么”一旦画面里有两只手或者手不在画面中央分类器就完全失效。石头剪刀布很多时候是两个人对战画面里同时出现两只手的概率很高分类器搞不定这种场景它没有“检测”的概念。第三条路就是最终采用的YOLOv11检测方案。YOLO系列本身是目标检测模型天然支持同时识别画面里的多个目标再加上最新的YOLOv11在特征提取结构上做了优化对小目标和相似类别的区分能力更好。YOLOv11有一个很关键的地方它能在检测出手势的同时输出检测框这个框在UI界面上用可视化的方式展示出来比单纯给一个分类结果直观得多。项目效果演示时界面上的绿色框一出现用户一眼就能看懂系统在做什么。1.3 技术栈与整体架构这套系统的完整技术栈如下模块技术选型说明编程语言Python 3.9全项目统一减少环境匹配问题深度学习框架PyTorch 2.x ultralyticsultralytics库负责YOLOv11的训练与推理目标检测模型YOLOv11n / YOLOv11s桌面演示推荐nano版本速度和内存更友好图像处理OpenCV摄像头读取、图像格式转换、画框界面开发PyQt5登录注册窗口、主检测窗口数据库SQLite存储用户账号密码零配置文件单文件启动部署格式PyTorch权重 / ONNX导出ONNX后在CPU上能明显提速整体流程的连接方式是标注数据 → 配置YAML → 训练模型 → 导出权重 → PyQt5界面加载权重 → 登录后启动摄像头 → OpenCV读取每一帧 → 送入model.predict() → 绘制检测框和结果 → 刷新界面并更新统计。这条链路的每一环在接下来的章节里都会展开讲。2. 数据集制作三种手势的采集、标注与组织2.1 数据来源公开资源与自采视频抽帧结合数据集是整个项目的起点也是最容易被低估的一步。很多人直接拿别人整理好的图片集来训练发现验证集mAP也挺高但一接到自己的摄像头画面就识别不准。原因很朴素训练数据里的手势姿势、背景、光线和你实际运行环境不一致。所以我的做法是混搭。先找一个开放的手势图片集作为基础这些图片通常包含不同肤色、不同角度、不同背景的手势为模型提供泛化能力。然后在自己的环境里用手机或笔记本摄像头录制一段视频录的时候把手势放在画面不同位置、不同距离每个手势录30秒以上后续用脚本按帧抽取补进训练集。这样模型在最终部署的硬件和光线下也能表现稳定。视频抽帧的脚本很简单我用的OpenCV代码大致是import cv2 import os video_path rps_data/videos/paper.mp4 output_dir rps_data/raw/paper os.makedirs(output_dir, exist_okTrue) cap cv2.VideoCapture(video_path) frame_id 0 count 0 while True: ret, frame cap.read() if not ret: break # 每5帧抽1帧避免连续帧过于相似 if frame_id % 5 0: cv2.imwrite(os.path.join(output_dir, fpaper_{count:04d}.jpg), frame) count 1 frame_id 1 cap.release() print(f抽取完成共 {count} 张图片)抽完帧之后还要统一尺寸。YOLO训练时会自己缩放图片但过小的图片比如低于320像素会丢失手势细节过大的图片又拖慢训练。我一般把图片统一处理到640x640附近或者在标注前先统一裁剪到手部区域附近减少背景带来的计算浪费。2.2 标注工具、命名规范与两个容易犯的错训练数据准备好之后下一步是标注。标注工具我用的是一款支持YOLO格式导出的开源图形标注工具这类工具的操作逻辑都一样打开图片目录框住目标选类别保存。关键是类别命名和标注框的边界。类别命名必须与训练配置里的YAML文件严格一致。我的数据集只包含三个类别rock、paper、scissors对应的YOLO标签编号分别是0、1、2。这里有两个非常容易翻车的坑我一开始都踩过。第一个坑是rock类别里混入了类似paper的样本。石头是拳头布是手掌张开这两种手势在视觉上差别很大但标注时如果框选范围太大把背景和部分手臂都框进去了模型就会学到不相关的特征。正确做法是标注框要贴合“手腕到指尖”的范围不要像画物体轮廓一样留太多空白。第二个坑是中文类别名。虽然YOLO的数据集配置文件支持任意字母组合的非中文类别名但中文类别名在后续代码中处理时容易遇到编码问题还会在命令行参数和UI显示时出现乱码。我的建议是数据、配置、代码里一律用英文显示层面上再通过字典映射成中文。2.3 目录结构与YAML配置标注完成后数据集目录结构要严格按照YOLO的约定来组织rps_data/ ├── images/ │ ├── train/ │ │ ├── rock_001.jpg │ │ ├── paper_001.jpg │ │ └── scissors_001.jpg │ └── val/ │ ├── rock_010.jpg │ └── ... ├── labels/ │ ├── train/ │ │ ├── rock_001.txt │ │ ├── paper_001.txt │ │ └── scissors_001.txt │ └── val/ │ ├── rock_010.txt │ └── ... └── rps.yaml每个txt标签文件里是若干行每行五个数字格式为类别编号 x_center y_center width height注意这四个坐标都是相对图片宽高的归一化值不是像素值。rps.yaml文件是整个训练的入口配置文件内容如下# rps.yaml path: /你的绝对路径/rps_data train: images/train val: images/val nc: 3 names: [rock, paper, scissors]数据划分上我用了6:2:2的比例训练集、验证集、测试集分开。每个类别至少准备500张以上三个类别尽量均衡。如果某个类别特别少模型会倾向于把不确定的手势判成多的那一类这是一个很现实的坑。还有一个建议验证集不要做数据增强保持原始图像。这样训练过程的mAP才能反映真实效果而不是被增强过的样本“虚高”。数据增强主要加在训练集上我常用的增强方式包括水平翻转、亮度饱和度微调、随机缩放但尽量不要用太夸张的旋转和裁剪手势识别的语义对方向是敏感的。3. 训练实操环境配置、损失趋势与调参经验3.1 环境搭建步骤训练环境是整个项目里最容易卡住新人的环节。我的建议是不要在全局Python环境里直接装包用虚拟环境隔离避免冲突。python -m venv venv_rps # Windows激活 venv_rps\Scripts\activate # Linux/Mac激活 source venv_rps/bin/activate pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics opencv-python pyqt5先安装PyTorch再安装ultralytics这个顺序能让ultralytics自动识别到对应版本的torch后端。安装完成后用下面这段代码检查GPU是否可用import torch print(CUDA可用, torch.cuda.is_available()) if torch.cuda.is_available(): print(显卡名称, torch.cuda.get_device_name(0))如果没有独立显卡纯CPU也可以训练但训练时间会明显变长。这种场景我的建议是放弃训练大模型直接用YOLOv11n这个最小的nano版本用CPU训练几百张图片的数据集时间在几十分钟到两小时内能接受。如果你已经在跑长时间训练优先确认CUDA版本和PyTorch版本是匹配的一般报错都出在这。3.2 训练命令与关键参数解读ultralytics库的使用方式很简洁既可以命令行直接跑也可以在Python脚本里调用。我习惯用命令行因为参数一目了然yolo detect train \ modelyolo11n.pt \ datarps.yaml \ epochs100 \ batch16 \ imgsz640 \ patience30 \ lr00.0005 \ projectruns/train这里面的参数含义我整理成一张表参数含义我的建议model初始权重用官方yolo11n.pt做迁移学习比从头训练快且稳data数据集YAML路径一定要填绝对路径相对路径经常踩坑epochs训练轮数100足够三分类任务没必要跑到300以上batch批大小根据显存调整16是多数显卡的安全值imgsz输入图片尺寸640是默认标准显存紧张可以降到416patience早停轮数30轮指标不提升就停止能省大量时间lr0初始学习率默认0.01对三分类小数据集偏高我降到0.0005为什么lr0要单独调低YOLO官方预训练权重是在大规模数据集上训出来的迁移到自己的小数据集时如果学习率太大会让预训练的知识在早中期就被破坏掉出现损失震荡。我试过默认学习率训练过程中train_loss正常下降但val_loss后期会反复横跳这是过拟合和学习率过高叠加的结果。降到0.0005之后验证集的loss曲线明显更稳。3.3 训练日志与结果分析训练完成后ultralytics会在project目录下生成一个train_x文件夹里面有一堆关键文件。我每次训练完第一件事不是看精度而是打开results.png看四张曲线训练框损失、训练类别损失、验证框损失、验证类别损失。对于一个三分类手势识别任务正常表现是train/box_loss持续下降并趋于平稳。train/cls_loss下降速度比box_loss更快因为三个手势的差异足够大。val/cls_loss在30轮之后如果出现明显回升说明模型开始过拟合。mAP50稳定在0.95左右mAP50-95在0.85左右算是不错的成绩。我记得第一次训练完验证集mAP50只有0.82后来发现原因是标注时rock类别里混入了几张攥着东西的照片模型学到的是“手上有物体”而不是“拳头”。重新清洗数据后mAP50直接跳到了0.96。这个现象很说明问题数据干净程度对结果的影响往往大于模型调参。还有一个值得注意的点。训练集几百张、验证集一百张这种规模下mAP曲线会有明显波动因为验证集太小几张错判图片就能影响整个指标。遇到这种情况不要太焦虑可以看一眼混淆矩阵确认三个类别之间的错判是否均匀而不是集中在某个类别上。3.4 训练阶段的两个坑训练阶段最容易遇到的两个问题一个是显存不足一个是类别不均衡。显存不足报错通常长这样CUDA out of memory。处理顺序是先把batch从32降到16再不行降到8仍然爆的话把imgsz从640降到416。两个参数都降下来之后显存占用会成倍下降。我不建议为了迁就显存去魔改BatchNorm结构那是自找麻烦。类别不均衡的问题比较隐蔽。比如scissors只有300张rock有800张训练曲线看起来很正常但实际使用时会发现模型容易把剪刀误判成石头。我在清洗数据时会把三类图片数量尽量控制在1:1.2的范围内超过这个比例就通过抽帧补充来拉平。4. 推理与部署从权重到摄像头实时检测4.1 一行命令加载模型做推理训练完成后best.pt就是效果最好的权重文件。推理时直接用ultralytics的YOLO类加载from ultralytics import YOLO model YOLO(runs/train/train_x/weights/best.pt) # 推理单张图片 results model.predict(sourcetest.jpg, conf0.5, imgsz640) # 取出检测框信息 for result in results: boxes result.boxes for box in boxes: cls_id int(box.cls[0]) name model.names[cls_id] confidence float(box.conf[0]) x1, y1, x2, y2 map(int, box.xyxy[0].tolist()) print(name, confidence, (x1, y1, x2, y2))conf0.5表示置信度低于0.5的检测结果会被过滤掉。手势识别场景建议置信度阈值设置在0.4到0.6之间。设置太低会出现大量误检背景中的物品被认成手掌设置太高又会漏检手稍微模糊就检测不到。4.2 导出ONNX在CPU上跑得更快训练和推理都基于PyTorch但PyTorch在CPU上的推理速度偏慢。如果目标设备没有GPU或者想把界面应用分发给别人运行导出ONNX是个很实用的做法。model YOLO(runs/train/train_x/weights/best.pt) model.export(formatonnx, imgsz640, dynamicTrue)导出完成后会生成best.onnx文件它在CPU上的推理速度一般比PyTorch快出不少原因是ONNX Runtime针对不同CPU做了算子优化和线程调度。我实测下来的速度参考如下模型版本推理设备实测帧率YOLOv11nNVIDIA GPU60 FPS以上YOLOv11nCPU12-15 FPSYOLOv11sNVIDIA GPU30 FPS左右YOLOv11sCPU5-8 FPS界面实时检测场景推荐使用YOLOv11n哪怕是在CPU上配合跳帧策略也能达到基本流畅的体验。如果你想追求更高的准确率再用YOLOv11s。4.3 摄像头实时检测的帧率控制摄像头实时检测的核心思路是循环读取帧 → 送入模型 → 画框 → 刷新显示。但有一个容易被忽略的点模型推理速度跟不上摄像头帧率时画面会越来越卡延时越来越大。我使用的方案是“跳帧检测”。摄像头以30 FPS采集但模型每处理一帧就跳过若干帧用上一次的检测结果先顶着这样既保证了界面流畅又不牺牲太多实时性。代码结构大致如下import cv2 from ultralytics import YOLO model YOLO(best.pt) cap cv2.VideoCapture(0) frame_count 0 results None while True: ret, frame cap.read() if not ret: break frame_count 1 if frame_count % 3 0: # 每3帧检测1次 results model.predict(frame, conf0.5, imgsz640, verboseFalse) if results is not None: annotated results[0].plot() # 直接绘制检测框 else: annotated frame cv2.imshow(RPS Detection, annotated) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这段代码里的results[0].plot()会自动在原图上画好检测框和标签开发阶段很方便。实际交付的UI界面不会直接用OpenCV的窗口而是把图像帧转成Qt的显示格式这部分在下一章里展开。5. 登录注册与主界面让检测模型变成一个可交付的系统5.1 为什么用PyQt5而不写Web界面做界面时我考虑过两种方案Web页面和桌面应用。Web方案的优点是界面漂亮、跨平台但需要额外搭建后台服务摄像头视频流传输又要走WebRTC或者WebSocket复杂度高出一大截。桌面应用方案选PyQt5理由有三个PyQt5对摄像头接入非常友好直接用OpenCV读帧后转为QImage就能显示。打包成exe后可以发给别人直接运行不需要部署服务器环境。登录注册、弹窗、按钮这些桌面UI组件PyQt5实现起来很成熟。Tkinter在初学者里也很常见但它的界面风格比较老旧视频流的刷新性能也不如Qt。做这种要展示给答辩、汇报用的项目PyQt5在视觉效果和后续扩展上明显更合适。5.2 注册登录模块的数据库设计登录注册的本质是用户注册时把用户名和密码存起来登录时校验。这里有一个我不能不提的安全底线密码绝对不能用明文存储。我用的方案是SQLite数据库加哈希。SQLite只需要一个文件不需要安装数据库服务非常适合桌面系统。用户表结构如下CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP );密码不存原文存的是哈希值。所谓哈希通俗理解就是一段不可逆的计算结果同一个密码每次计算得到同一个字符串但拿到这个字符串反推不出原始密码。我用的是加盐哈希盐值可以简单理解为一段随机字符串注册时生成并随哈希值一起保存。校验时用盐值重新计算哈希对比是否一致。登录注册的核心逻辑大致如下import hashlib import os import sqlite3 def hash_password(password: str, salt: str) - str: # 加盐哈希把盐值和密码混合后做大量次数的哈希 pw_hash hashlib.pbkdf2_hmac( sha256, password.encode(utf-8), salt.encode(utf-8), 100000 # 迭代次数 ) return pw_hash.hex() def register_user(username: str, password: str) - bool: salt os.urandom(16).hex() pw_hash hash_password(password, salt) conn sqlite3.connect(rps_app.db) try: conn.execute( INSERT INTO users (username, password_hash) VALUES (?, ?), (username, f{salt}${pw_hash}) ) conn.commit() return True except sqlite3.IntegrityError: return False # 用户名重复 finally: conn.close()这里用的pbkdf2_hmac是Python标准库里的密码学算法迭代次数设了100000次目的是拖慢暴力破解的速度。对用户来说注册登录整个过程依然很快但攻击者想拆这个哈希的代价就高得多。5.3 主界面线程模型检测逻辑必须丢进QThread主界面设计是整个项目里最容易“看起来功能全但一用就崩”的地方。最典型的错误是在Qt的UI主线程里直接跑摄像头读取和模型推理。图像预处理的耗时、模型推理的耗时、网络请求的耗时任何一个卡顿都会让界面无响应Windows会直接弹“当前窗口未响应”。正确的做法是把摄像头读取和模型推理放到QThread里UI线程只负责接收信号并刷新界面。线程之间通过Qt的信号槽机制通信。信号槽简单说就是后台线程检测到结果后emit一个信号把图像帧和检测结果发给UI线程UI收到信号后自动更新画面。我设计的线程结构大致是from PyQt5.QtCore import QThread, pyqtSignal class DetectionThread(QThread): frame_ready pyqtSignal(object, list) def __init__(self, model_path): super().__init__() self.model YOLO(model_path) self.cap cv2.VideoCapture(0) self.running True def run(self): while self.running: ret, frame self.cap.read() if not ret: continue results self.model.predict(frame, conf0.5, verboseFalse)[0] # 把检测到的类别和置信度解析成list detections [] for box in results.boxes: detections.append({ name: self.model.names[int(box.cls[0])], conf: float(box.conf[0]) }) annotated results.plot() self.frame_ready.emit(annotated, detections) def stop(self): self.running False self.cap.release() self.wait()UI线程里连接这个信号收到信号后在槽函数里把画面显示到QLabel上把检测结果显示到右边的表格或标签上。这个模式的精髓是UI线程永远不阻塞后台线程永远不碰界面组件。5.4 界面元素与显示逻辑主界面布局我采用左右结构左侧是一个大的QLabel用来显示视频画面整个界面的视觉焦点。右侧是一列信息区当前检测的手势类别、置信度、三个类别的累计计数、一轮石头剪刀布的胜负统计。底部是操作按钮启动检测、停止检测、退出登录。有一个细节值得单独说OpenCV读出来的图像是BGR色彩空间而Qt里显示用的通常是RGB。直接用cv2图像创建QImage颜色会偏蓝。正确做法是先转换色彩空间再转换格式from PyQt5.QtGui import QImage, QPixmap def cv2_to_qimage(cv_img): rgb_image cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB) h, w, ch rgb_image.shape bytes_per_line ch * w qimg QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format_RGB888) return qimg统计逻辑我用了一个计数器字典在槽函数里根据detections里的类别累加count值。石头剪刀布胜负统计也很简单两个人同时出拳的时候按规则比较拳头克剪刀、剪刀克布、布克拳头代码里就是一个映射表加几行条件判断不需要任何算法知识。6. 复现与排坑从零到演示的一手记录6.1 时间成本与设备参考很多朋友拿到这类题目时最关心的是要多久能做完我按自己的节奏做了一个参考表阶段耗时说明数据集采集与标注3-5小时公开图片集做基础自采视频抽帧补充环境搭建与训练1-2小时取决于设备和轮数模型推理脚本0.5-1小时验证best.pt在图片和摄像头上的效果登录注册模块2-3小时数据库建表、注册登录窗口逻辑主界面联调2-3小时摄像头线程、信号槽、实时显示整体调试与测试2-3小时多角度测试、阈值调整、打包验证设备上如果只有CPU训练阶段用yolo11n大概一两个小时界面检测阶段帧率在10帧左右演示完全可用。如果有NVIDIA显卡训练时间大幅压缩检测帧率也能上到60帧以上。这类项目其实不要求很高的硬件核心花的时间都在数据整理和界面联调上。6.2 高频问题排查表我把实际开发和帮别人排查过程中反复出现的问题整理成一张表按照“症状-原因-处理”的思路来列症状常见原因处理方式训练时报数据集路径错误相对路径配置问题在YAML路径里写绝对路径避免中文路径训练后检测完全无结果类别名与YAML不一致检查标签文件里的类别编号重跑标注摄像头画面全黑或闪退摄像头被其他程序占用关闭其他摄像头应用或换一个设备编号界面卡死无响应检测逻辑跑在UI线程将读取和推理移入QThread登录界面注册后无法登录密码哈希对比逻辑错误确认存储格式统一为“盐值$哈希值”模型能跑通但误检率高数据与运行场景差异过大加入实际环境的自采数据重新训练排查时我的一贯思路是先缩小范围。先确认模型本身在单张图片上检测是否正常再确认摄像头读取是否正常最后才看界面代码。直接把断点打在五个地方效率反而低。6.3 几点让我少走弯路的心得项目做完后回头看我最想分享的是下面这几条经验它们不一定写在任何文档里但确实让整个开发过程顺畅了很多。第一先小模型训通再换大模型。我第一次图省事直接选大模型结果数据集小训练时间还长效果反而不如nano版本。对三分类手势识别来说YOLOv11n已经具备足够容量真正决定上限的是数据质量。第二数据质量比调参重要。我清洗一批标注错误的数据后mAP直接涨了14个百分点这比调整任何学习率都明显。标注时框选要贴合手势类别的样本要均衡这两点做好了后面的路就好走了。第三界面不是锦上添花而是让评委和用户快速理解你做了什么的窗口。一个带登录、带实时检测框、带统计的项目演示效果和一个黑底命令行脚本是完全不同的量级。很多朋友把心思全放在模型结构上最后演示时界面乱糟糟效果大打折扣。第四环境依赖要固定。我做到最后一步才发现不同机器上PyTorch的CUDA版本不一致程序跑不起来。后来用pip freeze导出了requirements.txt每次换环境先安装依赖再跑项目就再也没出过兼容性问题。这套项目做完之后我最大的体会是所谓“基于YOLOv11的检测系统”重点其实不在YOLOv11本身而在于把模型、数据、界面、用户系统串成一条完整的可演示链路。你只要按着这个顺序走每一步别贪快最终的交付版本一定能稳稳跑起来。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑