YOLOv12实战:Python实现石头剪刀布识别与登录检测系统 把“石头剪刀布”这种经典手势识别做成一个带登录注册、带界面、带完整训练的Python项目确实是个很适合入门深度学习的人门级实战。拆开来看这个项目里既有目标检测的核心流程——数据采集、标注、训练、评估又有工程化的部分——用界面封装模型、做用户系统、处理摄像头实时检测。日常很多拿到这类标题的朋友其实不是看不懂YOLO怎么用而是卡在“不知道整套系统怎么串起来”。我把它从数据到界面完整拆一遍把踩过的坑和关键参数一并写清楚。1. 项目整体设计与技术选型1.1 为什么选YOLOv12而不是老牌YOLO版本很多刚接触目标检测的同学上来就问“用v5还是v8”但既然标题定了YOLOv12我建议首选v12。原因不复杂v12在检测头设计上做了调整用了改进的注意力机制替代部分传统卷积结构在保证实时性的前提下对小目标手势这类任务更友好。做一个对比帮助理解版本差异特性YOLOv5YOLOv8YOLOv12成熟度高资料多高社区活跃较新源码可用模型结构CSPDarknetC2f结构引入注意力机制改进小目标表现一般尚可明显提升部署难度低低中等适合场景通用检测通用检测与实例分割对精度有更高要求的实时检测石头剪刀布这类手势有一个特点手部区域占比通常不大尤其是摄像头距离稍远时目标框可能只有几十个像素。YOLOv12对特征提取阶段做了优化能更好保留小目标的语义信息实测下来对指尖、手指缝隙这类细节更友好。需要注意v12的环境依赖比v5稍新Python版本建议3.9及以上PyTorch建议2.0以上。如果你的显卡驱动比较老务必先去NVIDIA官网确认CUDA版本再装对应PyTorch否则后面训练时大概率会报nvrtc错误。1.2 系统的整体功能闭环这个项目表面上看是“识别剪刀石头布”实际上包含三块功能检测识别通过摄像头视频流或单张图片实时框出三种手势并给出置信度账户体系登录/注册界面用户信息持久化保存可作为独立小系统的演示模块界面交互把摄像头画面、检测结果、用户状态整合在同一个图形界面里我见过不少人只关注第一块以为训练完模型就结束了结果最后的界面登录功能才是工作量的大头。一个可交付的Demo必须让用户能打开、登录、看到检测结果而不是只在黑色终端里打印坐标。整个系统的数据流可以这样理解摄像头采集 - 逐帧预处理 - YOLOv12推理 - 解析检测框和类别 - 界面绘制 - 结果展示 ^ 登录注册模块用户表密码校验 —— 控制软件入口从工程角度看登录模块和检测模块完全解耦两者只在上层界面里关联。后期想替换成指纹登录、刷卡登录基本不用动检测核心。1.3 目录结构和模块划分建议规范的目录结构能让你后期维护省很多事。我按实际项目经验推荐这种组织方式RPS_Detection/ ├── data/ # 数据集目录 │ ├── images/train/ │ ├── images/val/ │ ├── labels/train/ │ └── labels/val/ ├── models/ # 训练好的权重文件 ├── rps_yolo/ # 核心代码 │ ├── train.py # 训练脚本 │ ├── detect.py # 检测脚本 │ ├── ui_main.py # 主界面入口 │ ├── login_window.py # 登录注册窗口 │ └── utils.py # 工具函数 ├── requirements.txt └── README.md数据集和代码分离的好处是中途换数据集不用动代码只改路径配置即可。而且训练生成的权重放在models下界面代码引用时路径清晰不容易出现“跑着就跑错文件”的问题。2. 数据集构建与预处理2.1 采集原始数据的正确姿势石头剪刀布的数据集规模不需要像通用物体检测那样动辄几万张2000到4000张高质量图片完全够用。关键是多样性不同人种的手肤色深浅差异对特征提取有影响不同角度手心、手背、侧面、俯拍不同距离近景特写、半身画面不同光照室内暖光、冷光、自然光不同背景纯色背景、复杂纹理背景采集方式主要有三种各有利弊直接摄像头拍摄真实场景匹配度最高但效率低建议拍几百张后利用数据增强扩展滚动手势变换拍视频开着摄像头让手不停切换三种手势建议30秒到1分钟后期抽帧下载公开数据集效率高但要注意license和使用规范尽量找允许研究使用的来源我自己更推荐“视频抽帧”策略。拍一个包含多种手势变换的视频用OpenCV每隔5到10帧保存一张。这样一秒25帧的视频抽出来就有十几张图片动作变换过程中的中间姿态也一并覆盖了模型对这种边界状态更鲁棒。2.2 标注工具选择与标注规范标注目标检测数据的工具推荐两个LabelImg和X-AnyLabeling。前者轻量简单后者支持自动标注能用yolo系列模型先跑一遍预标注再手工修正节省不少时间。标注时有几条硬经验尽量框住整个手掌包括手指指尖和手腕根部框不要贴边留出10像素左右的边距三类标签名称统一用rock、paper、scissors英文名避免中文路径编码问题一张图只标注一张手时最简单如果有多只手全部标出来反而能提升模型泛化能力标注完成后生成的是txt文件每行格式是类别id 中心点x 中心点y 框宽w 框高h注意x、y、w、h都是归一化到0-1的浮点数不是像素值。这是我见过最容易出错的地方——用像素坐标训练会直接导致loss爆炸。2.3 数据划分与增强策略数据集划分按经验比例训练集70%验证集20%测试集10%。用脚本划分时要保证同一来源的图片不分散到不同集合否则验证集和训练集太相似没有评估意义。数据增强我在这个项目里只用了几种轻量手段水平翻转石头剪刀布不区分左右手安全亮度对比度微调模拟不同光照随机旋转±15度手指的姿态不那么死板没有用马赛克增强Mosaic因为手势目标尺寸相对一致Mosaic对密集小目标更有帮助在这里反而会引入不自然的拼接边界。2.4 数据集格式转换YOLOv12的训练代码默认读取YOLO格式的txt标注。如果你自己扒的数据是COCO格式json或者VOC格式xml需要写一个转换脚本。核心逻辑是把xml里的xmin, ymin, xmax, ymax转成中心点格式import os import xml.etree.ElementTree as ET def convert_voc_to_yolo(xml_file, output_txt, class_map): tree ET.parse(xml_file) root tree.getroot() width int(root.find(size/width).text) height int(root.find(size/height).text) lines [] for obj in root.findall(object): cls_name obj.find(name).text if cls_name not in class_map: continue cls_id class_map[cls_name] box obj.find(bndbox) x1 float(box.find(xmin).text) y1 float(box.find(ymin).text) x2 float(box.find(xmax).text) y2 float(box.find(ymax).text) center_x (x1 x2) / 2 / width center_y (y1 y2) / 2 / height box_w (x2 - x1) / width box_h (y2 - y1) / height lines.append(f{cls_id} {center_x:.6f} {center_y:.6f} {box_w:.6f} {box_h:.6f}) with open(output_txt, w, encodingutf-8) as f: f.write(\n.join(lines))转换完务必随机抽3-5张图把标注可视化出来检查一次。3. 模型训练核心参数与过程3.1 环境配置注意事项训练前把环境调好是最磨人的一步。我给出一个实测可用的配置组合Python 3.9 PyTorch 2.0.0 CUDA 11.8 ultralyticsYOLOv12包办训练与推理 opencv-python PyQt5先检查显卡是否支持CUDA计算能力8.6及以上如果不支持v12的某些模块会退化到CPU计算速度慢到无法实时。安装依赖时我建议逐个装而不是一次性pip install -r requirements.txt。遇到版本冲突时能立刻定位。PyQt5和numpy在某些版本存在兼容问题如果出现np.int报错把numpy降到1.24.x即可。3.2 训练超参数选择与理论依据训练脚本基于ultralytics框架模型配置文件选yolo12nnano版本适合快速验证或yolo12ssmall版本精度更稳。两者参数量相差明显实时检测场景更推荐nano版轻量且够用。数据集配置yaml如下path: ./data train: images/train val: images/val test: images/test nc: 3 names: 0: rock 1: paper 2: scissors训练命令核心参数yolo train \ modelyolo12n.pt \ datadata.yaml \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ optimizerSGD \ patience20 \ device0 \ projectruns \ namerps_yolo12几个关键参数的选择逻辑imgsz640手势目标不算特别小640平衡速度和精度batch16显存8G左右刚好塞下如果显存不足降为8同时可配合amp混合精度lr00.01YOLO系列默认学习率从0.01开始比较稳加了权重衰减能防止过拟合patience20训练连续20轮验证集指标不升就早停避免无效训练浪费时间3.3 训练过程监控与评估指标训练过程中需要盯三个东西train loss、val loss、mAP。loss曲线是缓慢下降不会有明显突变。如果训练到一半loss突然抬高一个台阶很有可能是学习率太高导致参数震荡。我习惯在训练完成后输出验证集指标。三类手势的AP会在同一个数量级如果某一类AP明显低于其他类典型原因有三种该类图片数量不够尤其“剪刀”的标注容易漏手指区域该类与其他类在某些姿态下混淆比如“布”的手掌翘起会被误检为“剪刀”数据集中某类背景过于单一测到复杂背景时掉精度这时不是急着调参而是回到数据本身补充样本效果远好于换网络。3.4 权重选择与导出训练结束会在runs/rps_yolo12/weights/下生成best.pt和last.pt。界面应用时一定选best.pt它是验证集表现最好的权重而不是最后一轮的。如果想把模型转为onnx部署from ultralytics import YOLO model YOLO(runs/rps_yolo12/weights/best.pt) model.export(formatonnx, dynamicFalse, simplifyTrue, imgsz640)这个onnx文件可以脱离PyTorch环境用onnxruntime推理适合那种只装了CPU环境的部署机。4. 登录注册界面与主检测界面的实现4.1 登录注册模块的数据存储方案这个模块用SQLite做存储最合适单文件免配置。建表语句CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_login TIMESTAMP );密码存储不能明文。用Python标准库的hashlib做加盐哈希简单可靠import hashlib, os def hash_password(password: str, salt: str None) - tuple: if salt is None: salt os.urandom(16).hex() hash_obj hashlib.sha256((salt password).encode(utf-8)) return hash_obj.hexdigest(), salt def verify_password(password: str, salt: str, hashed: str) - bool: calc_hash, _ hash_password(password, salt) return calc_hash hashed登录界面流程输入用户名密码点登录程序查询库里是否有该用户有则校验密码通过进入主界面无则要用户先去注册页面创建账户注册时需要再确认一次密码两次必须一致用户名不能重复字段不能为空。这几条校验逻辑在界面层就能拦下来不用频繁访问数据库。4.2 PyQt5主界面布局主界面用PyQt5设计布局可以这样规划左侧是摄像头实时画面用QLabelQPixmap显示注意画面自动缩放右侧是检测信息面板包括当前用户、检测结果、置信度、系统时间底部是一排操作按钮开启摄像头、关闭摄像头、截图保存、退出登录顶部菜单栏放一个“关于”弹窗布局代码关键部分from PyQt5.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton from PyQt5.QtGui import QImage, QPixmap from PyQt5.QtCore import QTimer, Qt class MainWindow(QMainWindow): def __init__(self, username): super().__init__() self.username username self.model YOLO(models/best.pt) self.cap None self.timer QTimer() self.timer.timeout.connect(self.update_frame) self.setWindowTitle(f剪刀石头布检测系统 - 当前用户{self.username}) self.setup_ui() def setup_ui(self): central_widget QWidget() self.setCentralWidget(central_widget) layout QHBoxLayout(central_widget) # 左侧画面区域 self.video_label QLabel(未开启摄像头) self.video_label.setFixedSize(640, 480) layout.addWidget(self.video_label) # 右侧信息区域 right_panel QVBoxLayout() self.result_label QLabel(检测结果无) right_panel.addWidget(self.result_label) self.conf_label QLabel(置信度--) right_panel.addWidget(self.conf_label) btn_start QPushButton(开启摄像头) btn_start.clicked.connect(self.start_camera) right_panel.addWidget(btn_start) btn_capture QPushButton(截图保存) btn_capture.clicked.connect(self.capture_image) right_panel.addWidget(btn_capture) layout.addLayout(right_panel)4.3 摄像头实时检测与画面合成摄像头采集用OpenCV的VideoCaptureQTimer定时器控制刷新帧率一般每30毫秒抓一帧约33FPS。每帧图像先做BGR转RGB再送入YOLO模型推理返回结果后把检测框和类别标在图像上。检测时常见的线程问题必须处理PyQt主线程不能长时间阻塞否则界面会假死。推理操作要放进独立线程。简单做法是把检测过程封装成一个线程类from PyQt5.QtCore import QThread, pyqtSignal import cv2 from ultralytics import YOLO class DetectThread(QThread): result_ready pyqtSignal(object, object, float) def __init__(self, model_path, parentNone): super().__init__(parent) self.model YOLO(model_path) self.running True def run(self): import numpy as np while self.running: ret, frame self.cap.read() if not ret: continue results self.model(frame, conf0.4, verboseFalse) data results[0].boxes if data is not None and len(data) 0: cls_id int(data.cls[0]) conf float(data.conf[0]) self.result_ready.emit(frame, cls_id, conf) else: self.result_ready.emit(frame, -1, 0.0) # 控制帧率 self.msleep(30)这里conf设为0.4比较合适太低会出现误检太高会把部分模糊手势滤掉。4.4 单一手势选择逻辑一帧画面里可能出现多只手检测结果可能有多个框。小项目里不需要复杂的多目标跟踪我们只取置信度最高的那个框作为结果。这在UI展示时逻辑清晰也不会因为多个目标来回跳导致界面闪烁。如果想认真处理多目标需要在帧间做简单的空间去重距离过近的检测框视为同一目标只保留置信度高的。后面想要扩展这算是一个不错的升级点。4.5 截图保存与历史记录截图功能很简单把当前带检测框的画面用OpenCV保存到本地目录def capture_image(self): if self.current_frame is not None: timestamp datetime.now().strftime(%Y%m%d_%H%M%S) filename fcaptures/{self.username}_{timestamp}.jpg cv2.imwrite(filename, self.current_frame) QMessageBox.information(self, 提示, f截图已保存至 {filename})更完整一点可以在SQLite里建一张login_log表记录每次登录时间、检测次数、截图路径。这样登录注册系统就有了实际业务落地不再只是好看的样子货。5. 训练与界面联调中的常见问题与排查技巧5.1 训练阶段常见报错错误信息原因解决方案CUDA out of memorybatch太大或分辨率太高调小batch开启amp混合精度No labels found in path标注txt路径写错检查data.yaml路径确认images和labels文件一一对应assertion m 0.98数据集类不平衡检查各类样本数量补全过少类别的样本cuda runtime error 999显卡被其他程序占用卸载其他进程设置CUDA_VISIBLE_DEVICES0TypeError: np.int was removednumpy版本过新降级numpy到1.24.x或升级依赖包5.2 检测效果不佳的排查顺序检测不准不要一上来就调conf阈值按这个顺序排查看验证集样本的标注框是否紧贴手势内容看是否有类别混淆的数据样本比如“布”和“剪刀”标注错误看测试图片的分辨率与模型训练时的imgsz差距是否过大如果训练集样本少于1000张先补数据比调参更有效最后再尝试调整置信度阈值或者切换到yolo12s模型我遇到过一个比较典型的场景某同学采集的“布”全是手掌正对镜头而实际使用时手掌略微倾斜识别率骤降。往数据里补充不同角度的手掌后mAP直接提升6个点比任何参数改动都立竿见影。5.3 界面卡顿与摄像头延迟问题界面卡顿有三个常见来源摄像头帧率过高而推理跟不上设置定时器when 50ms一帧或将摄像头分辨率从1080p降到720p推理阻塞线程务必用QThread不要把model(frame)直接放主线程QLabel刷新过于频繁QPixmap的setPixmap是耗时操作每帧都刷新可能引起UI重绘瓶颈我通常把刷新频率控制在25FPS左右人类视觉上已经非常流畅且CPU和GPU负载都不高。检测结果文本不频繁重绘只在结果变化时更新一次。5.4 登录界面与主界面的跳转bug常见问题是登录成功后主界面先闪现再退出。检查槽函数里有没有把主窗口对象赋给局部变量局部变量在函数返回时被Python垃圾回收窗口就关闭了。正确做法是把主窗口设为应用级对象def login_success(self, username): self.main_window MainWindow(username) self.main_window.show() self.close()我把登录窗口和主窗口都挂在QApplication的主要流程上切换时用show和close不要用exec_嵌套循环否则会阻塞事件循环导致界面响应异常。5.5 摄像头权限问题在部分Windows机器上OpenCV的VideoCapture(0)可能返回False。先检查系统相机权限是否被其他软件占用。给OpenCV单独指定摄像头后端可以解决一部分兼容性cap cv2.VideoCapture(0, cv2.CAP_DSHOW)在macOS和Linux上可以用CAP_AVFOUNDATION或CAP_V4L2。建议代码里做一个备选摄像头源的尝试0失败就尝试1最大程度适配不同环境。6. 实操总结与后续扩展思路到这里整套“YOLOv12石头剪刀布识别检测系统”的主干就全部搭完了。数据和模型是效果上限界面和登录是可用性下限两条线都不能偏废。这个项目的价值在于它覆盖的不只是一个识别模型更是一套完整的小型软件系统。如果想在这个基础上升级可以根据自己的能力方向选这几条路把摄像头检测改为本地视频文件检测适配更多输入加入手势历史统计和简单图表展示变成一个小型数据平台换成更强的yolo12s或yolo12m模型接受一点速度损失换精度把系统改成命令行的批量图片检测工具方便处理大量离线图片接入语音播报识别结果提升交互感从工程角度看这套代码稍加修改还能移植到“是否佩戴口罩”、“垃圾分类识别”、“常见物品识别”等任务上。数据换成你自己的标注集类别数改一改训练流程完全复用。最后分享一个我自己的习惯模型训练完成后别急着写界面先用命令行检测脚本对几十张“没见过的图”做一次盲测亲身感受模型的强项和弱项。然后根据盲测结果决定要不要补数据、调参数。界面只是模型的包装模型本身的可靠性才决定用户真实体验。训练和迭代的功夫下足了展示层的开发反而会顺利很多。