VOC/YOLO双格式工具检测数据集实践:从校验到YOLO训练的完整流程

发布时间:2026/10/10 13:49:32
VOC/YOLO双格式工具检测数据集实践:从校验到YOLO训练的完整流程 简介面向目标检测任务的三类工具数据集包含钳子、剪刀、螺丝刀共3668张图片采用Pascal VOC与YOLO两种格式的标注文件可直接用于YOLO系列、Faster R-CNN等模型的训练与验证。标注由labelImg完成采用矩形框规则全量标注框数为3686其中pliers 1568个、scissors 406个、screwdrivers 1712个螺丝刀与钳子样本量较多剪刀相对少适合做细粒度工具识别、类别不均衡训练或工业分拣场景的算法实验也可直接按类别划分训练集与验证集。压缩包共2000个文件以1999个XML标注文件为主并附说明txt总体积83.44MBXML与txt标注均与原始图像同名对应类别标签名称统一为pliers、scissors、screwdrivers接入常用检测框架时几乎无需额外转换。目前已有411人学习下载适合目标检测初学者、算法工程师及机器人视觉项目开发者直接使用可显著节省自行采集和标注的成本。1. 工具检测数据集3668张图、3类标注第一眼要确认什么很多做目标检测的同行尤其做工业视觉和作业安全场景的经常在“有没有现成标注数据”这件事上耗掉大量时间。这份工具检测数据集的实际价值在于钳子、剪刀、螺丝刀三类目标图片和标签文件一一对应VOC和YOLO格式都齐了。它最常见的用途是给工器具识别、智能工具柜盘点、操作规范审核等模型当底料也可以用来跑YOLO系列算法的baseline。不过我刚拿到它并没有急着训练因为三类框数分布不均衡pliers只有1568框scissors只有406框screwdrivers达到1712框。这种不平衡会在训练时直接变成“剪刀类漏检”后面第3章会专门说怎么处理。接下来按格式校验、训练配置、划分和避坑的顺序拆开。2. VOC与YOLO双格式目录结构、坐标规则与校验脚本2.1 先看懂三种文件的对应关系解压后能看到说明.txt、xyxr_tools_617.xml、xyxr_tools_455.xml这类XML文件以及同名的jpg和txt。每个样本由三个同名文件组成。VOC格式的XML负责记录“类别名 左上角像素坐标 右下角像素坐标”便于人类阅读和可视化YOLO格式的TXT则记录“类别索引 归一化的中心点x/y 归一化的宽高”是YOLO系列训练时直接读取的形式。两种格式可以互相转换这份数据两个都给你省掉了最麻烦的转换步骤但并不能省掉校验。格式文件后缀坐标信息训练适用度Pascal VOC.xmlxmin,ymin,xmax,ymax像素适合标注工具、可视化YOLO.txtclass_id,cx,cy,w,h归一化主流检测框架直接读取VOC的XML结构和我常用的labelImg导出结构一致根节点annotation下每个object有name和bndbox。下面是一个典型的片段数值仅为示意annotation filenamexyxr_tools_617.jpg/filename object namepliers/name bndbox xmin120/xmin ymin80/ymin xmax260/xmax ymax190/ymax /bndbox /object /annotation这段XML里bndbox四个值都以像素为单位左上角是(120,80)右下角是(260,190)类别写在name标签里。YOLO训练前会把这类坐标转成0到1之间的比例值例如对应一张640×426的图中心点约在(0.297,0.317)宽高约(0.219,0.258)。如果不确定原图尺寸可以直接用XML里的filename去翻图片的尺寸再用脚本转换不要凭感觉写归一化数值。YOLO的TXT内容每行一组框顺序固定为“类别索引 中心点x 中心点y 宽度 高度”全部是归一化后的浮点数。类别索引不是随意定的而是按你后续data.yaml里names的顺序来的。比如这份数据的三个类别我一般固定为pliers0、scissors1、screwdrivers2。如果你把顺序改成screwdrivers在前那么TXT里第一列原本代表pliers的数字就会被错误解释成screwdrivers这是训练前必须处理的问题。2.2 写脚本检查三个文件是否齐全、坐标是否合法拿到双格式数据集我一般会先跑一段很小的校验脚本而不是直接开训。原因有两个第一VOC和TXT是两套独立文件解压、拷贝、改名的过程中很容易出现某张图有XML但没有TXT第二YOLO坐标一旦越界或宽高为负训练时loss会异常甚至出现“什么都检不到”的诡异现象。下面这段脚本从XML侧做基础合法性检查import xml.etree.ElementTree as ET from pathlib import Path voc_dir Path(/data/Annotations) # XML目录 yolo_dir Path(/data/labels) # TXT目录 img_dir Path(/data/JPEGImages) # JPG目录 classes [pliers, scissors, screwdrivers] # 顺序要与data.yaml一致 for xml_path in sorted(voc_dir.glob(*.xml)): stem xml_path.stem img_path img_dir / (stem .jpg) txt_path yolo_dir / (stem .txt) if not img_path.exists() or not txt_path.exists(): print(f[文件缺失] {stem}) continue root ET.parse(xml_path).getroot() for obj in root.findall(object): name obj.find(name).text if name not in classes: print(f[类别未定义] {stem}: {name}) bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) if xmin 0 or ymin 0 or xmax xmin or ymax ymin: print(f[坐标不合法] {stem}: ({xmin},{ymin})-({xmax},{ymax}))这段脚本遍历所有VOC XML检查对应的图片和TXT是否存在再检查每个object的类别名是否在classes列表内最后一组if判断坐标矩形是否退化或越界。voc_dir、yolo_dir、img_dir三个路径要改成你本机解压后的实际位置classes列表的顺序必须和之后data.yaml里的names顺序一致因为后面转换TXT时要用同一个顺序。脚本里用Path对象拼接文件名避免手写字符串加文件名容易漏斜杠的问题。接着从TXT侧检查每行是否真的满足“一行五个数”的格式from pathlib import Path yolo_dir Path(/data/labels) for txt_path in sorted(yolo_dir.glob(*.txt)): for lineno, line in enumerate(txt_path.read_text().strip().splitlines(), 1): parts line.split() if len(parts) ! 5: print(f[格式异常] {txt_path.name}:{lineno} 行有 {len(parts)} 个字段) continue cls_id, cx, cy, w, h parts cx, cy, w, h map(float, (cx, cy, w, h)) if not 0 cx 1 or not 0 cy 1: print(f[中心点越界] {txt_path.name}:{lineno}) if w 0 or h 0: print(f[宽高异常] {txt_path.name}:{lineno})提示这里的XML坐标片段是示意实际数值以你解压后的标注为准。这段的重点是判断中心点是否落在0到1之间宽高是否为正。实际数据集里偶尔会出现标注框超出图像边界导致的归一化值越界或者标注软件导出时小数位被截断成0的情况训练时有这些行会拉高损失或直接让模型输出空检测框。两个脚本合在一起基本能保证输入到YOLO的数据是干净的。如果检查出来的问题样本不多我一般直接把有问题的文件移动到backup目录而不是在原始目录里改来改去防止误删。2.3 框数与类别分布训练前先看统计数据摘要里已经写了总框数和每类框数pliers 1568、scissors 406、screwdrivers 1712加起来3686框。不过拿到手后我仍会自己重新统计一次因为文件在拷贝途中可能丢过或者某张图的标签被覆盖过。统计方式很简单直接数labels目录下每个TXT里的第一列# 在labels目录下执行统计每个类别索引出现次数 cut -d -f1 xyxr_tools_*.txt | sort | uniq -c这一行命令把每个TXT的第一列取出来排序后按类别去重计数。输出大概是“1712 2”表示类别索引2出现了1712次。如果和摘要里的数字对不上就要回看是丢了文件还是转换脚本里类别映射错了。注意在这里我只统计框数不统计“每张图里平均几个框”因为目标检测关注的是每个类别的样本容量scissors只有406框意味着模型见到它的机会远少于另外两类后面所有针对性处理都建立在这个数字上。把类别索引固定下来后再根据这些数字决定后面的训练策略而不是看到一个保存好的数据集就盲开训练。3. YOLO训练配置data.yaml、损失函数和类别不平衡的取舍3.1 先把data.yaml写对路径、类别顺序不能靠猜YOLO训练时第一个读入的就是data.yaml它负责告诉训练器数据放哪、有几类、各类叫什么。很多新手在这一步就把路径写错导致训练前就报错。我一般把目录重组成下面结构再填yamltool_dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ └── labels/ ├── train/ ├── val/ └── test/这个结构是Ultralytics YOLO默认约定训练时只写images路径框架会自动把images替换成labels去同一级找对应标签。比如你写了train: images/train它就会自动找labels/train。所以两个目录必须真实存在且同名文件一一对应。在此基础上data.yaml可以这样写path: /data/tool_dataset # 改成你本机的绝对路径 train: images/train val: images/val test: images/test # 可选没有就把这行删掉 nc: 3 names: 0: pliers 1: scissors 2: screwdrivers其中path是根路径建议用绝对路径而不是../这种相对路径因为训练命令的执行位置不同会直接影响相对路径解析。names的顺序必须和labels目录下TXT里的第一列索引一致这条我在2.3已经强调过但每次整理新数据集时依然是最容易出错的点。如果你下载的资源里TXT是用另一套工具生成的它可能把“scissors”排在“0”位而你yaml里把pliers排为0那训练出来的模型会把pliers叫成scissors而且从loss曲线看一切正常找错很费时间。3.2 训练命令和关键参数小目标场景怎么设imgsz和batch基础训练命令如下我以YOLOv8为例cd /data/tool_dataset yolo detect train \ modelyolov8s.pt \ datatool_dataset.yaml \ epochs100 \ imgsz640 \ batch16 \ patience20 \ project./runs \ nametool_det说明一下几个关键参数。modelyolov8s.pt是推荐起点比n版精度高比m版显存占用小还没跑先不用纠结选哪个先用s跑通流程。imgsz640是速度与精度的中间值工具这类目标在画面里通常比较小如果你显卡显存有富余可以改成imgsz768或896来提升小框召回但训练时间会涨batch16取决于显存6G显卡建议用812G以上再考虑16或32。patience20表示验证指标连续20轮没有提升就提前停止避免浪费算力。如果不加默认100轮跑满遇到已经收敛的情况会多烧很多电。上面几个参数可以先用一组“保守但不翻车”的默认值参数参考值适用前提modelyolov8s.pt显存6G以上追求平衡imgsz640目标占比小可提高到768batch16显存12G以上6G用8epochs100有patience早停不会空跑patience20连续20轮无提升即停ampTrue默认开启省显存这张表不是给你照抄的而是用来排查如果报显存不足就先改batch如果mAP上不去再改imgsz如果loss还在降但提前停了把patience加到30。实际训练中超参数对结果的影响往往没有数据质量大所以我建议把更多时间花在校验标签和看误检图上而不是反复调参。另外“yolo损失函数”在这里真实影响你的判断YOLOv8的损失由box loss、cls loss和DFL三块组成。box loss管检测框贴合程度cls loss管类别判断DFL管框边缘的细粒度分布。像钳子、螺丝刀这种细长物体DFL和box loss比cls loss更值得盯。训练结束后Ultralytics会输出results.csv里面逐轮记录了box_loss、cls_loss、dfl_loss和mAP。如果box_loss迟迟不降优先怀疑标签坐标不干净而不是盲目加轮数。3.3 类别不平衡scissors只有406框直接训会怎样这是这份数据集最需要现场处理的地方。三个类的框数分别是pliers 1568、scissors 406、screwdrivers 1712scissors占比约11%。对于一个三分类目标检测模型来说这类样本量不算富裕但也没到完全不能用的程度。我的做法是第一轮不做任何特殊处理用上面命令跑一次baseline拿到per-class的mAP50。然后根据结果决定下一步而不是训练前就乱七八糟地加权重。常见处理方案有三条。第一如果scissors的recall明显低比如低于0.6可以做离线增强把包含剪刀的图片做90°、180°、270°旋转或者水平翻转后重新生成标签扩到至少800张再训。注意旋转90°的标签坐标要换算水平翻转只需要把归一化中心点的x改成1-x。第二如果不想动标签可以在训练时提高mosaic增强的使用程度但这招对细长工具不一定有效因为马赛克拼接会把物体切得更碎需要自己试。第三最后一招是降低类别阈值推理时把conf从0.25降到0.1相当于更愿意“相信”模型输出recall会提升但误检也会变多这个只能作为最后一步。这里补充一个非常重要的边界不要试图用一个per-class权重参数去硬掰这个不平衡YOLOv8默认没有直接暴露每个类别的loss权重调cls只会整体改变分类损失占比反而可能把大头类学得更激进。用数据增广比调超参数更可控而且整套流程可复现。跑完baseline后一定要保存好results.csv和confusion_matrix.png混淆矩阵会直接告诉你scissors是不是被大量错判成了screwdrivers。另外提醒一句训练日志里的P和R要分开看。总mAP50看起来挺高不代表scissors的召回就好。我的习惯是把每类的PR曲线单独截图尤其看scissors的曲线是不是整体贴着左下方。如果是说明这个类确实没有被充分学到后面的数据增强才值得做。这种做法比看单一数字更能定位问题。4. 训练集划分与目录重组按图片切分别让标签错位4.1 为什么不能把原始目录直接喂给训练器原始解压目录里jpg、xml、txt混在一起训练器虽然能在某些配置下自己找标签但无法满足train/val/test的划分要求。直接全量训练会导致验证结果虚高因为你是在拿训练过的数据验证。另一边随便用脚本按“前80%的txt”划分也会翻车因为TXT的排序不一定和JPG的排序一致划分出来的val会包含train里已经出现过的图片的标签造成数据泄漏。因此我坚持的原则是以图片文件名作为唯一主键来划分图片和标签必须同步走。先分图片再根据同名规则复制对应TXT是保证不错位的最稳路径。使用比例上常见做法是8:1:1。做小数据集baseline时也可以用9:0.5:0.5让训练样本尽量多验证集只保留每个类别至少10框。对这份数据我建议保持8:1:1因为总图数366810%的验证集也有366张测试集有366张足够稳定地评估mAP。4.2 一份可复现的划分脚本下面脚本按图片文件名划分并同步复制TXTimport random import shutil from pathlib import Path src Path(/data/xyxr_tools) # 原始数据目录jpg/xml/txt混在一起 dst Path(/data/tool_dataset) # 重组后的目录 random.seed(42) # 建立目标子目录 for split in (train, val, test): (dst / images / split).mkdir(parentsTrue, exist_okTrue) (dst / labels / split).mkdir(parentsTrue, exist_okTrue) imgs sorted(src.glob(*.jpg)) random.shuffle(imgs) # 固定随机种子后顺序可复现 n len(imgs) n_train int(n * 0.8) n_val int(n * 0.1) # 余下0.1作为test for i, img_path in enumerate(imgs): if i n_train: split train elif i n_train n_val: split val else: split test stem img_path.stem shutil.copy2(img_path, dst / images / split / img_path.name) txt_src src / (stem .txt) if txt_src.exists(): shutil.copy2(txt_src, dst / labels / split / (stem .txt)) else: print(f[警告] {stem} 没有对应txt训练时会少一个标签)脚本的关键是random.seed(42)固定种子后无论跑几次同一个文件名都会落进同一个split里方便复现。sorted(src.glob(*.jpg))保证初始顺序稳定shutil.copy2会在复制文件的同时保留元数据。如果你要把XML也留下来做可视化可以在循环里再加一行shutil.copy2(src/(stem.xml), dst/annotations/split/...)不影响检测训练但对后续人工复核有帮助。注意这个脚本只按图片划分不关心每张图里有几个框。这样做虽然简单但可能出现val里scissors框特别少的极端情况。如果跑完分布统计发现验证集里某类只有几框就重新换一个随机种子比如42改成2026再划分直到每个类别在train和val里都有足够样本这是常见做法。4.3 划分后的统计确认别让验证集变成“死角”划分完先不要急着训练花一分钟确认目录数量。在tool_dataset根目录下执行for s in train val test; do echo $s echo images: $(find images/$s -name *.jpg | wc -l) echo labels: $(find labels/$s -name *.txt | wc -l) done这条命令分别统计三个子集的图片和标签数量数量不一致说明划分脚本有遗漏优先检查源码中txt_src路径有没有写错。再进一步统计每个split里各类别的框数for s in train val test; do echo $s cut -d -f1 labels/$s/*.txt | sort | uniq -c done如果输出显示val里只有pliers和screwdrivers两类scissors框数为0那后面验证出来的scissors mAP全是0没有任何参考意义。出现这种情况不代表数据有问题而是随机划分恰好把包含scissors的图都分到了train里换一个随机种子重新划分即可。这一步看似简单却是很多数据集“看起来能跑、结果却稀碎”的根源之一。宁可多花五分钟确认也不要等训练10个小时后才发现验证集不完整。这里还要提一个数据泄漏的细节test集一旦划分出来就不要参与任何训练前的统计比如计算类别分布、图像均值甚至不要用来做置信度阈值的选择。很多人习惯把test来回试阈值试完再报mAP实际上等于把test当成val用了最后拿到手的效果会虚高一截。我一般把test目录锁起来只在全部训练和调参结束后跑一次。5. 避坑指南路径、空标签和类别顺序三大块都是翻车点5.1 训练启动阶段的路径坑坑一路径没写绝对路径训练一开始就报images not found。 现象命令行输入yolo detect train datatool_dataset.yaml ...后几秒内报错提示Dataset tool_dataset.yaml images not found或No labels found in /.../labels/train。 原因data.yaml里的path用了相对路径或者train: images/train是从当前工作目录解析的而当前目录和数据集根目录不是同一个地方。Ultralytics会在启动时拼接pathtrain任何一段错都会导致找不到文件。 解决把data.yaml里的path改成绝对路径比如path: /data/tool_dataset同时确认train: images/train、val: images/val真实存在。可以先在终端执行ls /data/tool_dataset/images/train | head确认目录没拼错再跑训练。坑二train下全是图片但labels/train是空的。 现象训练日志里出现WARNING: no labels found in .../labels/train但images目录有几千张图片。训练照样会跑但每一轮loss都是0或NaN。 原因很多数据集只提供“图片标注混放”的目录重组目录时只建了images分支忘了建labels分支或者标签文件仍然留在原始目录没有复制过去。 解决回到第4章的划分脚本确认labels/train目录已经通过mkdir创建且TXT文件被复制。也可以用find labels/train -name *.txt | wc -l对比图片数量不一致就重新复制。5.2 数据与训练过程中的坑坑三某张图没有TXT训练时被悄悄跳过。 现象训练日志里有WARNING: skipping label (no labels found)但最后mAP还是能出来很多人不会在意。 原因原始数据里存在个别图没有标注或者转换过程中TXT被漏生成。YOLO默认跳过这些无标签图你甚至不会发现。 解决用第2章校验脚本检查“图片有但TXT没有”的文件把它们从数据集里移出或者补标注。我的习惯是直接移动到一个_unlabeled目录不在训练集里留下任何一张无标签图否则模型的loss曲线会出现不规则抖动。坑四类别索引错位模型把钳子叫成螺丝刀。 现象训练多个epoch后mAP50都正常但打开result图pliers和screwdrivers的名字互相错位甚至出现“同一个类名对应两套标签”的情况。 原因TXT文件里的0、1、2和data.yaml里names的排序不一致。比如原始TXT是用pliers0、scissors1、screwdrivers2生成的而yaml里把screwdrivers排在了0scissors在1pliers在2。 解决锁死类别顺序在第2章统计框数时就要记录0对应pliers1对应scissors2对应screwdrivers。生成yaml和TXT时都使用同一个classes列表。我一般会在项目根目录放一个classes.txt内容严格按这个顺序写之后任何脚本都从它读取不再手写变量。坑五显存不够直接CUDA out of memory。 现象训练伊始报错RuntimeError: CUDA out of memory. Tried to allocate ... MiB。 原因imgsz640、batch16对大多数消费级显卡偏大特别是一边训练一边开可视化工具的场合。 解决先把batch降到8或4再不行就换yolov8n.pt或者把imgsz降到480。注意workers也可能吃内存如果数据加载慢不要盲目调大workers。训练卡死时看的是显存不是内存用nvidia-smi -l 1观察显存占用最直接。6. 从mAP到实际图看漏检、导ONNX的收尾技巧6.1 用验证集图片替代指标直接看漏检训练出best.pt后我第一件事不是看mAP而是跑一批真实图片看模型实际表现。因为mAP是统计值它会掩盖“剪刀类漏掉一半”这种问题。下面代码用best.pt对验证集做预测并保存结果图from ultralytics import YOLO model YOLO(runs/detect/tool_det/weights/best.pt) results model.predict( source/data/tool_dataset/images/val, conf0.25, saveTrue, project./runs/val_vis, nametools, )这段代码会对val目录下每张图推理并保存带框图片。参数里conf0.25是常规起点如果漏检多就降到0.1如果误检多就升到0.4。saveTrue会把结果写到project/name目录下。看结果图时优先找两类图片一类是含scissors但没被检出的另一类是框比物体明显大一圈的。这两类分别指向recall问题和回归框质量问题要比总mAP数字更有指导意义。6.2 导出ONNX验证部署侧的可靠性做工具检测的大多数场景要跑在边缘设备或工业PC上不会一直拖着PyTorch环境。用ultralytics一键导出ONNX是快速验证部署可行性的办法yolo export modelruns/detect/tool_det/weights/best.pt formatonnx imgsz640导出完成后会生成best.onnx可以用onnxruntime在CPU上跑同样的图片对比PyTorch结果的框是否一致。这一步能暴露框架间算子的微小差异比如某些策略在GPU和CPU上结果不完全一致。对检测类模型我一般只验证两条导出是否成功、输入输出shape是否符合预期。至于量化等模型在目标设备上确认精度后再做不要在导出阶段就加一堆优化选项否则会增加排查难度。从那以后我每次拿到新的目标检测数据集都强制走一遍“读说明→统计框数→校验标签→重组目录→跑一轮baseline→翻误检图”的流程很少再被所谓“直接可用”的数据集坑过。你下载这份工具检测数据集后也可以先跑一遍这套流程再调整scissors类的数据增广。希望帮到你。本文还有配套的精品资源点击获取