
简介面向目标检测学习者和YOLOv5实战用户这份石头剪刀布手势检测数据可直接用于YOLO系列模型的训练与验证。数据包含已划分好的训练集与验证集训练集约六千四百五十五张图片及对应标签验证集约八百八十张图片及对应标签均按YOLOv5标准文件夹结构存放标注格式为类别、中心点坐标、宽度和高度使用相对坐标表示无需转换即可被常见检测框架读取。压缩包内共两千个文件其中一千九百九十九个为标签文本文件另外提供一个可直接运行的Python可视化脚本随机输入一张图片即可绘制出边界框并保存在当前目录可以直观地检验标注框是否贴合目标方便及时发现并修正错误标注。资源包整体约两百一十兆字节已有约一百七十五人学习使用。对于正在推进手势识别项目、需要高质量标注数据或希望压缩前期准备工作量的开发者这份数据集能显著降低数据采集与清洗成本无论是教学演示、算法验证还是竞赛准备都能帮助快速进入模型训练、调优和部署阶段。1. 石头剪刀布手势检测为什么值得你从头搭一套YOLO数据集石头剪刀布手势检测是目标检测里“规模小又五脏俱全”的典型场景。所谓YOLO数据集指按YOLO格式组织、每张图对应一个txt标注、带类别class映射和训练/验证/测试划分的完整数据资产。很多人拿到这类数据会直接开训实际上这套数据最大的价值不只是训练精度而是它把“数据划分、类别映射、可视化脚本”三条流水线焊在一起。它适合正在入门yolov5或yolov8、想跑通第一个检测模型的人也适合需要快速验证数据增强和损失函数改动的调参工程师。class文件让你跳过最烦人的数据准备阶段可视化脚本则是判断数据能不能训、训后为什么翻车的快捷通道。下面按“数据怎么组织 → 脚本怎么用 → 训练参数怎么设 → 踩坑怎么排”的顺序展开。2. 从原始图片到YOLO格式数据划分、class文件与目录结构的搭建2.1 先把目录结构立起来train/val/test的归属决定训练边界拿到一批手势图片第一件事不是写标注而是确定目录结构。yolov5和yolov8读取自定义数据集时读的是images和labels两个母目录其下再按train、val、test子目录组织。这个约定有两个潜在要求images和labels的同名图片必须存在且文件名完全一致否则训练阶段直接跳过该样本test子目录不参与训练只在最终评估时生效。我在实际项目里的标准结构如下datasets/rps/ ├── images/ │ ├── train/ # 训练图片 │ ├── val/ # 验证图片 │ └── test/ # 测试图片出最终指标用 ├── labels/ │ ├── train/ # 与images/train一一对应的txt │ ├── val/ │ └── test/ ├── classes.txt # 类别文件一行一个类名 └── data.yaml # yolo读取的总配置目录树里labels/train的txt必须和images/train的jpg一一对应。很多人只关心图片数量却忽略了一个细节yolo训练时会遍历images目录对每张图片去找同名txt如果某张图没有对应的txt它不会报错而是直接把该图当成背景样本参与训练。手势这种小目标场景里混入大量无标注背景图等于人为制造负样本模型会越来越倾向于“什么都不检测”。按7:2:1还是8:1:1划分要看你后续怎么用。如果只是为了训练并对比不同模型我会把比例压到8:1:1因为手势类别只有三到四类验证集里每类有几十张就足够稳定如果要做超参数搜索或模型选择val占比提到20%更安全。不管是哪种比例随机种子必须固定否则每次划分出的训练集都不一样实验之间没有可比性。提示子目录名统一用小写英文不要用中文。yolo的Dataset类会对路径做字符串处理中文路径在Windows下偶尔触发编码错误这个坑在第5章单独说。2.2 class文件与类别顺序一句话写错整个训练白费class文件在YOLO体系里是最容易被轻视的文件它只做一件事定义类别名到数字id的映射。以石头剪刀布三分类为例classes.txt内容如下rock paper scissors注意三点。第一文件里不写数字行号从0开始就是类别idrock对应0paper对应1scissors对应2。第二标注txt里第一列的整数必须和这个顺序一致如果标注时rock写成1训练时rock对应0模型会把“石头”和“布”的语义完全搞反。第三classes.txt的类别顺序必须和data.yaml里的names列表保持一致两处顺序不一样时yolo训练不报错只会让你得到一个mAP和直觉完全对不上的模型。我一般会把这个校验做成一个两步核对第一步检查所有标注txt里出现的最大类别id是否小于nc第二步用脚本对比classes.txt和data.yaml的names发现不一致立即终止训练。标题里特意把“类别class文件”单列出来说明这套数据里class文件是独立交付物我的建议是把它当作接口来处理不要随手改改它等于重新映射全部标注。2.3 划分脚本随机数种子与按文件前缀划分的取舍如果拿到的原始素材是图片和txt混合散放的就需要自己写划分脚本。这里给出我常用的版本按8:1:1复制到新目录原始文件不动。import os import random import shutil from pathlib import Path src_img Path(raw/images) src_lab Path(raw/labels) dst Path(datasets/rps) random.seed(42) # 固定随机种子保证每次运行划分结果一致 all_ids [p.stem for p in src_img.glob(*.jpg)] random.shuffle(all_ids) n len(all_ids) train_ids all_ids[: int(n * 0.8)] val_ids all_ids[int(n * 0.8): int(n * 0.9)] test_ids all_ids[int(n * 0.9):] for split, ids in [(train, train_ids), (val, val_ids), (test, test_ids)]: (dst / images / split).mkdir(parentsTrue, exist_okTrue) (dst / labels / split).mkdir(parentsTrue, exist_okTrue) for i in ids: img src_img / f{i}.jpg lab src_lab / f{i}.txt if lab.exists(): shutil.copy(img, dst / images / split / f{i}.jpg) shutil.copy(lab, dst / labels / split / f{i}.txt) else: print(f[skip] {i}.jpg 缺少标注已跳过)这个脚本里有三个值得注意的参数。random.seed(42)不是玄学它把随机序列固定下来让“随机划分”变成“可复现划分”如果你和别人同时训一个数据种子不同会直接导致精度差0.2到0.5个点。8:1:1的比例写死在切片位置想改成7:2:1只需调整int(n0.8)和int(n0.9)两个边界。shutil.copy是复制不是剪切原始素材原封不动划分错了随时能重来这是给自己留的后悔药。前面这种随机划分适用于所有图片来自同一种采集方式的场景。但如果你遇到的是多人分批次采集的数据比如5个人各拍300张我强烈建议按文件前缀划分而不是全量随机划分。原因是同一个人的手势在角度、光线、皮肤颜色上高度相似如果这300张被随机拆进train和val验证集里全是“见过的人”模型mAP虚高等真正部署时遇到没参与训练的人准确率会断崖式下跌。按前缀划分的写法只是把random.shuffle换成按前缀分组代价是划分不均衡时某些组要整体归入训练集。2.4 labels的YOLO格式与归一化标注文件为什么每个框占一行划分好目录后还需要理解labels里每行txt的含义否则拿到“划分好的数据集”也改不动它。每行txt对应一个目标框格式是“类别id 中心点x 中心点y 宽w 高h”五个值全部是相对于图片宽高的归一化值。实际例子一张640×480的图片里手掌中心位于像素坐标(320, 240)宽度160像素高度240像素那么这一行是“0 0.5 0.5 0.25 0.5”。归一化有两个直接后果。第一读到标注时想还原像素坐标必须乘回图片宽高所以可视化脚本里必须先读图拿shape。第二目标框越界不会在标注层面报警比如w写成0.4且框中心靠近边缘框就会超出图片边界yolo在读取时会把越界部分裁剪掉这个静默处理会悄悄改变你标注的目标区域。手势检测里常见的情况是手掌在画面边缘被截断标注时把框硬凹进画面训练后模型对截断手势的召回会明显偏低。遇到这种样本正确做法是让标注员把可见部分完整框出或者干脆丢弃这张图不要为了数量把边界样本硬塞进训练集。3. 用数据可视化脚本“看懂”你的数据集标注分布与失败样本排查3.1 可视化脚本到底画什么bbox分布、类别分布与样本抽样数据可视化脚本的职责不是画两张彩图给汇报用而是让你一眼看出“这批数据为什么训不好”。针对手势检测我至少会画三类图类别数量柱状图、bbox中心点分布散点、bbox宽高比直方图。它们分别回答类别是否平衡、目标是否总在固定位置、目标形态是否单一这三个问题。import cv2 from pathlib import Path from collections import Counter import matplotlib.pyplot as plt labels_dir Path(datasets/rps/labels/train) imgs_dir Path(datasets/rps/images/train) class_names Path(datasets/rps/classes.txt).read_text().splitlines() cls_counter Counter() center_x, center_y [], [] aspects [] for lab_file in sorted(labels_dir.glob(*.txt)): img_path imgs_dir / f{lab_file.stem}.jpg if not img_path.exists(): continue h, w cv2.imread(str(img_path)).shape[:2] # 读原始宽高用于还原归一化坐标 for line in lab_file.read_text().splitlines(): cid, x, y, bw, bh map(float, line.split()) cls_counter[class_names[int(cid)]] 1 center_x.append(x) center_y.append(y) aspects.append(bw / bh) fig, axes plt.subplots(1, 3, figsize(14, 4)) axes[0].bar(cls_counter.keys(), cls_counter.values()) axes[1].scatter(center_x, center_y, s2, alpha0.4) axes[2].hist(aspects, bins30) plt.show()这段脚本的输入输出逻辑很好理解遍历labels/train里每个txt找到同名图片读宽高把每行标注拆成五个浮点数分别累加类别计数、收集中心点和宽高比最后用subplots拼出三张子图。class_names从classes.txt读入int(cid)再把txt里的类别数字映射回类名所以这个脚本对任何数据集都通用换数据集只需改路径。最值得盯的是第二张中心点散点。理想情况下散点应均匀分布在画面各区域如果散点全部聚在0.45到0.55之间说明采集时手势永远放在画面中央模型几乎没学过“目标在画面角落”的样子。这类数据就算训练mAP到0.95装到摄像头里一出现侧面入镜的手立刻掉点。宽高比直方图则提示你手势的形态多样性剪刀手势通常竖直且细长宽高比小于1拳头接近正方形宽高比接近1。直方图如果只有一个尖峰说明数据干净得过头泛化反而危险。3.2 从可视化反推标注质量漏标、错框与类别不平衡可视化脚本加上图片叠加后还能直接看出标注质量问题。第三个常用脚本是把一批图拼成大图、把标注框直接画上去存成grid.png人工翻看。import cv2 import math import numpy as np from pathlib import Path imgs_dir Path(datasets/rps/images/train) labels_dir Path(datasets/rps/labels/train) sample_files sorted(imgs_dir.glob(*.jpg))[:64] # 抽查前64张改这里可扩大范围 cols 8 rows math.ceil(len(sample_files) / cols) grid_w, grid_h cols * 320, rows * 320 canvas np.zeros((grid_h, grid_w, 3), dtypenp.uint8) 128 for idx, img_file in enumerate(sample_files): img cv2.imread(str(img_file)) img cv2.resize(img, (320, 320)) lab_file labels_dir / f{img_file.stem}.txt h, w img.shape[:2] if lab_file.exists(): for line in lab_file.read_text().splitlines(): _, x, y, bw, bh map(float, line.split()) x1 int((x - bw / 2) * w) y1 int((y - bh / 2) * h) x2 int((x bw / 2) * w) y2 int((y bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) r, c divmod(idx, cols) canvas[r * 320:(r 1) * 320, c * 320:(c 1) * 320] img cv2.imwrite(check_grid.png, canvas)这个脚本的逻辑是读前64张训练图并统一缩放到320×320把每张图的标注框还原成像素坐标画到图上再按8×8拼成一张大图存盘。统一缩放会让小图里的框变粗不影响你看错标漏标sample_files[:64]决定抽查样本数想全量检查改成[:500]并调大cols即可。从这张拼图里重点看两类问题一类是漏标画面里明明有个剪刀但没有任何框另一类是半框只框住手指没框住手掌或者把另一只手也包进来。石头剪刀布的动作差异集中在手指形态半框对手指检测基本是灾难。还有一个容易被忽略的细节结合第2章的归一化换算这里用(x - bw / 2) * w还原左上角如果你在脚本里写错成x * w - bw / 2视觉效果一样但实际是两种框容易在排查定位时产生误导。建议统一写成先算中心再偏移和yolo官方的格式保持一致。当你发现某个类别的样本数比其他类少一半别急着做数据增强。先看少的那类是不是采集姿势单一如果“布”永远只有手背朝外一种角度欠拟合已经注定单纯复制样本解决不了。常见做法是找这类样本的多样性而不是刷数量。4. 动手训练yolov8加载这套数据集的必调参数与损失函数观察4.1 最简训练命令从data.yaml到第一个模型目录结构确定后离跑通只差一个data.yaml。这是yolo读取数据的总配置内容如下# datasets/rps/data.yaml path: datasets/rps # 相对于yolo项目根目录的路径 train: images/train # 训练集图片目录 val: images/val # 验证集图片目录 test: images/test # 测试集图片目录可选 nc: 3 # 类别数量 names: [rock, paper, scissors] # 顺序必须与classes.txt一致data.yaml里最容易写错的是path字段。yolov8把path当作相对当前工作目录来解析我的习惯是在yolo项目根目录下执行训练命令这样path直接写datasets/rps如果数据集放在别处要么把path改成绝对路径要么给datasets建软链接。用绝对路径省事但换机器就要改配置用相对路径只要项目目录不变就能直接迁移。最简训练命令如下yolov8的CLI方式比配置文件方式噪声小yolo detect train datadatasets/rps/data.yaml modelyolov8n.pt epochs200 imgsz640 batch16这一行执行后yolo会先检查data.yaml路径然后列出train、val的图片数量接着下载官方预训练权重并开始训练。modelyolov8n.pt表示使用nano版本预训练模型参数量最小适合这种小目标少样本场景如果显存有余量换yolov8s.pt精度会更高但训练时间约翻倍。epochs设200对石头剪刀布这种简单任务是够的一般到120个epoch后mAP就不再明显上升跑满200是为了观察过拟合拐点。4.2 三个必调参数batch、imgsz与置信度门限batch和imgsz是互斥博弈的关系两个都吃显存。手势属于中小目标imgsz低于512时手指缝隙这种细节会被严重压缩我建议固定imgsz640再调batch。8G显存跑yolov8nbatch16是相对安全的起点显存不足时先降batch到8再降imgsz到480不要两个同时降否则很难定位是哪一项导致的精度下降。日志里box_loss、cls_loss、dfl_loss三项分开打印如果box_loss一直不降优先怀疑标注框边缘不精确而不是动学习率。推理时的置信度门限单独拎出来说。训练日志里的mAP是模型在所有可能阈值下的综合表现真正上线时你通常定一个conf阈值过滤低置信度框。手势互动的场景里误报比漏报更烦人我一般会把conf从默认0.25提到0.4到0.5。yolo detect predict modelruns/detect/train/weights/best.pt sourcecamera.jpg conf0.45 iou0.45conf0.45的意思是只有置信度超过0.45的框才保留iou0.45控制NMS去重。石头剪刀布三类别形态接近相邻手容易产生大量重复框iou阈值不动、只提conf通常会把这些多出来的虚框一起清掉。要记住conf调高的代价是召回降低三分类任务里0.45这个值够用到真实场景根据误检率再回调。参数建议值说明imgsz640低于512对小目标不友好batch8~16显存吃紧优先降batch再降imgszconf0.4~0.5手势误报场景提阈值更有效iou0.45保持默认不轻易动epochs150~200简单任务120轮后趋于平稳4.3 训练中bn崩溃与loss不收敛怎么处理bn崩溃是yolo训练里一个很常见的现象train/box_loss在某个epoch突然掉到0val/box_loss同步变成0mAP归零后续所有epoch都在同一个水平震荡。第一次遇到的人会以为模型坏了去重训实际上它未必是模型问题而是BatchNorm在统计滑动平均值时被异常输入带偏。我遇到过的触发原因有两种一种是batch太小比如batch4时BN的统计噪声偏大另一种是训练集混入了纯色图或包含全黑全白像素的图BN会把这些极端激活值当作正常分布。解决思路按顺序排查先把batch提到至少8再跑一次数据可视化脚本人工检查有没有黑白高光或过暗的手势图有就剔除。如果以上都没问题在训练命令里加augmentFalse做临时诊断验证是增强的问题后再单独调hsv_h、flipud的值。loss不收敛是另一种情况它表现为loss在一两个大数附近长期震荡不上升也不下降。最直接的怀疑对象是标注文件里出现过大的框比如w0.6、h0.8这种占据整张图的目标。yolo对超出边界的框做裁剪如果大量标注框越界且被人为拉回模型学到的框回归目标自相矛盾。做法是把labels里所有宽高大于0.5的框列出来看手势检测里一个掌心框宽度占整图50%以上极可能是错标。修正标注后重新训练loss通常在一个epoch内就能看到下降趋势。提示排查bn崩溃和loss不收敛前先把训练日志里的图片路径存下来。后期可视化脚本只看训练集日志里才能看到具体是哪一批图触发了异常。5. 常见问题与排查YOLO训练石头剪刀布数据集时的5个坑5.1 训练集里混入无标注背景图模型把空白当背景现象训练的mAP一路上涨但推理时模型频繁漏检画面里手明明很大却只输出一个低置信度框。检查训练集图片数和标注数后发现两者对不上。原因yolo下载或别人交付的“划分好的数据集”里images/train存在没有对应txt的jpg。训练时yolo遍历图片目录遇到无标注图片不会报错而是把它当作纯背景负样本参与训练。手势检测的目标面积小一张无标注图造成的负样本干扰比大目标场景严重得多。解决写一个核对脚本统计images和labels两侧文件名集合的差异把没有标注的图挪出训练目录或者补充标注。逻辑就是对比两个目录的glob结果几分钟跑完比训练结束后再排查省时间。5.2 class文件类别顺序与data.yaml不一致mAP直接崩现象训练过程正常loss正常下降val mAP却只有0.3左右而且三种类别里总有一种识别率奇低。查了图片和标注肉眼看框也没问题。原因classes.txt里rock是第一行但data.yaml的names列表里paper在前。txt标注里的0代表“按classes.txt顺序的第一个类”yolo加载data.yaml后把类别id再映射到names两处顺序错位导致同一个id在不同阶段指向不同类名。训练时模型猜对了目标位置却在类别语义上整体错位mAP自然上不去。解决用一个三行脚本读取data.yaml的names和classes.txt的内容逐行对比不一致就打印警告并停止本轮训练。改的时候以data.yaml为基准重新生成classes.txt不要反过来改data.yaml因为它还承担path、nc等训练入口配置。5.3 手机照片训练2000张仍过拟合现象训练loss很低val loss在100个epoch后持续上升保存下来的best.pt在测试集上的mAP只有训练集的八成左右。原因数据全部来自同一部手机、同一场景、同一光线2000张图看起来数量不少但相机自动白平衡和固定机位让图片特征高度一致。模型记住的是“这张桌面上这只手”而不是“石头剪刀布”的抽象形态本质上还是数据多样性不足。解决按拍摄设备或拍摄者分组划分数据而不是全量随机划分保证val集里出现“没见过的环境”。训练时把mosaic增强打开yolov8默认mosaic1.0如果关闭了可以重新打开。在数据层面补拍侧视角、俯视角和不同亮度下的手势比单纯加hsv颜色抖动有效得多。这属于数据采集的硬功夫脚本救不了。5.4 混淆矩阵总合不唯一别拿它当bug现象验证阶段绘制的混淆矩阵把所有格子数字加起来不等于验证集样本数也不等于各类别样本数之和看起来像统计口径出错。原因混淆矩阵的行列是预测分布每个目标框只要置信度超过阈值就会被计数默认阈值是0.25。一个真样本既可能因为漏检不进任何格子也可能被多个互斥的框重复命中加上大量低于阈值的预测不会入格总合自然不等于标签总数。这属于yolo混淆矩阵的常规行为不是脚本算错。解决看混淆矩阵时先明确自己想看什么。想验证类别是否搞混读按行归一化后的百分比看对角线是不是显著最高想统计漏检去矩阵里找“背景”行或未命中格子的数量而不是纠结总合是否唯一。对比两次实验时固定同一阈值再看矩阵别一次用0.25、一次用0.45来比较两组数字。5.5 可视化脚本中文路径乱码现象在Windows上跑数据可视化脚本报UnicodeDecodeError或其他编码错误图片路径里带“测试”“手势”目录时必现。原因脚本在读取txt或yaml时用了系统默认编码gbk而交付文件是按utf-8写的中文字符按gbk解码直接炸。这跟yolo本身没关系纯属Python读取文件时encoding参数没指定。解决所有open和read_text调用统一加encodingutf-8。Path.read_text()可以传encoding参数但很多人不知道我习惯用open(path, encodingutf-8)。同时项目根目录保持全英文路径避免在yolo源码层把路径再编码一遍。最后的防线是给可视化脚本入口加一个try-except捕获UnicodeEncodeError后打印“当前脚本需要utf-8环境”让接手的人不用查半天。6. 进阶用验证集指标反推数据质量三个实用技巧6.1 用混淆矩阵找“剪刀被识别成拳头”的混淆样本训练完成后yolo会在runs/detect/val目录输出混淆矩阵图。先看哪一对类别的互相误判率最高然后回到验证集里找出这些样本。方法是把验证集图片按类别分组专门挑出被误判的那几张用第3章的可视化脚本叠加输出。很多时候会发现不是模型问题而是某张图的标注框只框了三根手指模型学到的剪刀特征被污染。这一类“指标反查数据”比直接调参效率高得多。6.2 用PR曲线定置信度门限而不是拍脑袋训练日志里的PR曲线图能告诉你在不同置信度下的precision和recall。在曲线上找到precision开始明显下滑前的那个点对应的置信度就是这版模型的最优conf候选再留5%的余量压掉尾部误检。我前面提到的conf0.45就是从PR曲线读出来的不是拍脑袋定的。如果曲线右下角拖了很长的尾巴说明低置信度区域积攒了大量误检先回查数据里的背景图和错框而不是继续调阈值。6.3 给数据集加“难例池”把推理误检回填训练集最后一招是把真实摄像头下误检、漏检的图片截下来按第2章的划分流程补进训练集。我最早做手势检测时在公开样张上mAP约0.91一接真实摄像头就被白色墙面和掌纹干扰误检一堆。后来我把每天误检的图存进hard_examples目录每周补一次数据并重训两个星期后误检率下降了一半以上。这套流程把可视化脚本、数据划分和训练参数串成了闭环比任何调参都更能改善实用性。这也是我在这类项目里最深的体会数据集的交付物里划分好的目录和class文件只能保证能跑通可视化脚本才是让数据持续生长的入口。希望这一套做法能帮你少走几步弯路也建议你在复现时把conf、batch这些参数的实验记录留好回头对比时你会感谢自己。希望帮到你。本文还有配套的精品资源点击获取