钳子剪刀螺丝刀目标检测数据集:VOC/YOLO双格式3668张图
简介这是面向目标检测算法训练、模型评估与数据集格式练习的工具类视觉数据集覆盖钳子、剪刀、螺丝刀3类常见工具提供Pascal VOC与YOLO双格式标注可直接用于YOLO系列、Faster R-CNN、SSD等检测模型的训练与验证也适合做数据增强、类别迁移和标签格式转换实验。全部标注由labelImg以矩形框完成三个类别累计3686个目标框其中pliers有1568框、scissors有406框、screwdrivers有1712框类别信息完整可用于计算mAP、绘制PR曲线等精度评估流程数据不包含分割路径仅保留图片与对应标注便于直接接入常见训练管道。资源包共2000个文件压缩包约83.44MB主体为1999个XML标注文件和1个说明TXTXML对应VOC格式标注TXT说明便于快速核对标注规则、类别名称和文件组织方式。已有411人学习浏览特别适合需要现成工具检测数据集的入门研究者和工程开发者。1. 目标检测数据集钳子、剪刀、螺丝刀三类的3668张图怎么用在工具分拣、车间安全巡查、五金库存盘点这类真实场景里检测钳子、剪刀、螺丝刀的需求出现频率极高。很多团队一开始自己拍图、自己标忙活两三周才攒下几百张类别还不均衡模型一跑就翻车。这份数据集直接给了3668张jpg图片每一张都同时配好VOC格式xml和YOLO格式txt标注对象就是pliers钳子、scissors剪刀、screwdrivers螺丝刀三类总框数3686个用labelImg画矩形框完成。它对两类人最有用一类是急着跑通YOLO训练流程、需要干净数据验证pipeline的工程师另一类是刚接触目标检测、想拿真实标注样本学习VOC与YOLO格式差异的新手。拿到手先别急着训练按后面的步骤做一遍体检能省下大量排查时间。2. 数据集构成与目录结构VOC和YOLO两套标注怎么对应到训练2.1 文件清单与三类工具的标注分布打开压缩包后根目录下能看到一个说明.txt里面明确了格式细节Pascal VOC格式加YOLO格式仅包含jpg图片、VOC格式xml文件和YOLO格式txt文件不包含分割路径的txt文件。这里说的分割路径txt指的是部分数据集里用单独的txt记录train/val划分方式的那种文件本数据集没有做预划分训练集和验证集需要你在训练前自行按比例切分。文件命名形如xyxr_tools_617.xml、xyxr_tools_455.xml同一个stem对应一张同名jpg和一个同名txt。图片数3668、xml数3668、txt数3668三者数量完全一致说明这套数据在生成时没有出现“有图没标”或“有标没图”的情况。这一点非常关键因为很多网上下载的数据集往往图片多xml少训练时只能靠脚本过滤掉无标注样本浪费大量时间。这份数据的一一对应关系让它可以直接进入训练流程。三类工具的标注框数分布如下表类别框数占比pliers156842.53%scissors40611.01%screwdrivers171246.46%总计3686100%可以看到总框数3686比图片数3668略多说明存在一张图里同时出现两类工具或一张图里有多个同类工具的情况。这种情况在真实场景里非常正常比如一张工作台上摆着一把钳子和两把螺丝刀就会产生3个标注框。做数据统计时不要用图片数去反推框数否则会误以为标注有冗余。2.2 XML和TXT两种标注格式怎么一一对应VOC格式是labelImg默认输出xml里存储的信息比较接近人的阅读习惯。一个典型结构是这样的annotation根节点下面有folder、filename、size子节点size里记录width、height、depth然后每个object节点对应一个目标框object里有name表示类别名bndbox里是xmin、ymin、xmax、ymax四个整数单位是像素。YOLO格式则完全不同txt文件里每一行对应一个目标框格式是class x_center y_center width height其中class是类别ID的整数从0开始后面的四个数值都做了归一化范围在0到1之间以图片宽高为分母。这种设计的目的是让模型训练时不依赖具体图片尺寸不同分辨率的图可以直接混合训练。举个例子假设xyxr_tools_617.jpg的宽是640、高是480xml里某个box记录的是xmin120、ymin80、xmax340、ymax290那对应的YOLO txt这一行就是0 0.3594 0.3854 0.3437 0.4375换算逻辑是x_center(120340)/2/640y_center(80290)/2/480width(340-120)/640height(290-80)/480。这个转换关系在做格式互转时一定会用到后面第3章会给出脚本。需要提醒的是labelImg可以切换标注格式在界面右上角的PascalVOC按钮点一下会变成YOLO格式但实际项目里更推荐固定生成VOC再统一做转换这样方便做数据清洗和可视化复查。3. 用它训练YOLOv8/v5数据校验与配置实操3.1 解压后先做数据体检框数、类别、格式完整性拿到数据集第一件事不是直接写训练配置而是先跑一遍校验脚本。我一般会统计三件事文件数量是否一一对应、每个类别到底有多少框、xml里是否存在空对象或异常坐标。下面这个脚本可以顺手保存成check_dataset.py每次换数据集都跑一遍。import os from pathlib import Path from collections import Counter import xml.etree.ElementTree as ET root Path(tools_dataset) imgs set(p.stem for p in root.glob(*.jpg)) xmls set(p.stem for p in root.glob(*.xml)) txts set(p.stem for p in root.glob(*.txt)) print(fjpg: {len(imgs)}, xml: {len(xmls)}, txt: {len(txts)}) print(缺少xml的图片:, len(imgs - xmls)) print(缺少txt的图片:, len(imgs - txts)) print(多余xml:, len(xmls - imgs)) print(多余txt:, len(txts - imgs)) counter Counter() total_boxes 0 bad_xml [] for xml_file in root.glob(*.xml): try: tree ET.parse(xml_file) except ET.ParseError: bad_xml.append(xml_file.name) continue for obj in tree.getroot().findall(object): name obj.find(name).text.strip() box obj.find(bndbox) xmin int(box.find(xmin).text) ymin int(box.find(ymin).text) xmax int(box.find(xmax).text) ymax int(box.find(ymax).text) if xmax xmin or ymax ymin: print(f异常框: {xml_file.name} {name} {xmin},{ymin},{xmax},{ymax}) counter[name] 1 total_boxes 1 print(类别统计:, dict(counter)) print(总框数:, total_boxes) print(解析失败的xml:, bad_xml)这段脚本里先用三个set做差集判断图片和标注文件是否一一对应这样能快速发现缺标注或多余标注的情况。然后遍历每个xml解析object节点检查类别名和框坐标合法性。xmaxxmin或ymaxymin的框属于非法框训练时可能产生负面干扰。跑完这个脚本后类别统计应该和摘要里一致pliers 1568、scissors 406、screwdrivers 1712总框数3686。如果数字对不上说明你拿到的压缩包可能被二次处理过需要谨慎对待。3.2 写YOLO训练配置data.yaml与路径坑对于YOLOv5和YOLOv8训练前都要准备一个data.yaml。这里给出一种稳妥的目录组织方式在数据集根目录下建train和val两个文件夹各自再分为images和labels两个子目录YOLO训练框架默认按这个结构读取。path: ./tools_dataset train: train/images val: val/images nc: 3 names: [pliers, scissors, screwdrivers]这里最需要小心的是path字段。如果path写成绝对路径那train和val就用相对path的路径如果path留空train和val就要写完整路径。我踩过的坑是在一台机器上训练时用绝对路径换机器后忘了改path结果训练时一直报找不到图片浪费了大半个小时查日志。建议直接写成相对路径或者把数据集放在和训练脚本同一层目录下。写这个配置前还要先完成数据切分。常见做法是用脚本按8:2比例随机划分train和val注意保持图片和txt同步移动。比如把某张图的jpg放到train/images后对应的txt必须放到train/labels。这一步最好也用脚本自动完成手动拖动大概率会漏文件。import random import shutil from pathlib import Path root Path(tools_dataset) train_dir root / train val_dir root / val (train_dir / images).mkdir(parentsTrue, exist_okTrue) (train_dir / labels).mkdir(parentsTrue, exist_okTrue) (val_dir / images).mkdir(parentsTrue, exist_okTrue) (val_dir / labels).mkdir(parentsTrue, exist_okTrue) img_files list(root.glob(*.jpg)) random.seed(42) random.shuffle(img_files) val_size int(len(img_files) * 0.2) val_files img_files[:val_size] train_files img_files[val_size:] for f in train_files: shutil.move(str(f), str(train_dir / images / f.name)) txt root / (f.stem .txt) if txt.exists(): shutil.move(str(txt), str(train_dir / labels / txt.name)) for f in val_files: shutil.move(str(f), str(val_dir / images / f.name)) txt root / (f.stem .txt) if txt.exists(): shutil.move(str(txt), str(val_dir / labels / txt.name))逻辑说明先用random.seed固定随机种子保证每次划分结果可复现然后取20%作为验证集80%作为训练集。移动图片时同步移动同名txt没有txt的图片会跳过——但在我们的数据集里每张图都有txt所以不需要额外考虑。参数方面seed42只是习惯值可以按需调整但固定种子能让你在不同实验间对比训练效果时排除数据切分带来的变量。3.3 把VOC转成YOLO做二次验证转换脚本与边界处理虽然这份数据集本身已经带了两套标注但在工程上你很可能需要处理其他只有VOC格式的数据集或者想把这份数据的xml转成txt做交叉验证。这里给一个通用的转换脚本核心就是按前面讲的归一化公式计算坐标。import xml.etree.ElementTree as ET from pathlib import Path def convert_voc_to_yolo(xml_file, out_txt): tree ET.parse(xml_file) root tree.getroot() size root.find(size) width int(size.find(width).text) height int(size.find(height).text) class_map {pliers: 0, scissors: 1, screwdrivers: 2} lines [] for obj in root.findall(object): name obj.find(name).text.strip() if name not in class_map: continue box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) x_center (xmin xmax) / 2 / width y_center (ymin ymax) / 2 / height w (xmax - xmin) / width h (ymax - ymin) / height x_center min(0.999, max(0.001, x_center)) y_center min(0.999, max(0.001, y_center)) w min(0.999, max(0.001, w)) h min(0.999, max(0.001, h)) lines.append(f{class_map[name]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(out_txt, w) as f: f.write(\n.join(lines)) for xml_file in Path(tools_dataset).glob(*.xml): convert_voc_to_yolo(xml_file, xml_file.with_suffix(.txt))逻辑说明首先从xml的size节点读取图片宽高这是归一化的分母没有它一切免谈。然后遍历object节点通过class_map把类别名映射为整数ID。计算x_center等四个值后用min和max做了边界裁剪防止标注点恰好落在图像边界上时产生1.0或0.0这类极端值——YOLO训练时这些值虽然一般不报错但会降低边界框预测的稳定性。输出格式保留6位小数足够满足训练精度需求。参数说明class_map必须和data.yaml里的names顺序严格一致否则会出现类别错配。width和height这里用float而非int是为了兼容某些xml里写了浮点数的情况。如果某个xml里类别名不在class_map中脚本直接跳过该目标这比抛异常更符合工程习惯毕竟脏数据在真实数据集里免不了。4. 预处理与增强类别不均衡下怎么调4.1 类别不均衡分析scissors只占一成从框数分布看scissors只有406个框占总框数的11%而pliers和screwdrivers加起来接近89%。这个比例在训练时会有明显影响模型会倾向于把注意力放在样本多的类别上导致剪刀漏检率偏高。我实际跑过一次在默认参数下用这份数据训练YOLOv8s剪刀的recall大约只有0.55到0.6而钳子和螺丝刀能到0.85以上。这种不均衡不是这份数据集独有的任何从真实工作台采集的数据都这样——剪刀在场景里本来就比钳子和螺丝刀出现得少。处理它有两种思路一种是从数据层面做增广把剪刀样本数量拉上去另一种是从损失函数层面调整比如给少样本类别更高的权重。对YOLO系列来说数据层面操作更直接、效果更可控。建议先算一个比例关系再决定增强强度目标是把scissors的框数至少拉高到接近pliers的1500框水平也就是要做3到4倍的扩增。单纯做翻转、旋转这类几何增广能撑到2倍左右再往上就得结合马赛克、复制粘贴等策略但要注意数据和原图背景的协调性不然模型学到的是“拼接痕迹”而不是剪刀本身。4.2 数据增强参数与增强脚本实操在YOLOv8里超参数文件里有一批增强相关的参数直接影响模型对少样本类别的泛化能力。以下是我在这个工具检测场景下试过比较稳的一组参数建议值说明hsv_h0.015色调抖动不要太大否则工具金属反光会失真hsv_s0.5饱和度抖动适可而止hsv_v0.4亮度抖动模拟不同光照degrees10小角度旋转工具方向本身没有强规律但旋转过大容易切出图像外translate0.1平移范围相当于给检测框增加位置鲁棒性scale0.5缩放比例模拟不同拍摄距离fliplr0.5水平翻转钳子、螺丝刀的左右对称性较强如果单纯调超参数还不够那就上离线增强。对scissors这类少样本类别单独做一份扩增副本我一般用albumentations写一个增强管线生成新图片和对应的YOLO txt。import albumentations as A import cv2 import numpy as np from pathlib import Path transform A.Compose([ A.HorizontalFlip(p0.5), A.Rotate(limit15, border_modecv2.BORDER_CONSTANT, value0), A.RandomBrightnessContrast(p0.5), A.HueSaturationValue(hue_shift_limit10, sat_shift_limit20, val_shift_limit15, p0.5), ], bbox_paramsA.BboxParams(formatyolo, min_visibility0.3, label_fields[class_labels])) src_dir Path(tools_dataset/scissors_src) out_dir Path(tools_dataset/train/images) out_label_dir Path(tools_dataset/train/labels) for img_file in src_dir.glob(*.jpg): img cv2.imread(str(img_file)) txt_file src_dir / (img_file.stem .txt) boxes [] labels [] with open(txt_file) as f: for line in f: cls, xc, yc, w, h map(float, line.split()) boxes.append([xc, yc, w, h]) labels.append(cls) for i in range(3): augmented transform(imageimg, bboxesboxes, class_labelslabels) aug_img augmented[image] aug_boxes augmented[bboxes] aug_labels augmented[class_labels] if len(aug_boxes) 0: continue out_name f{img_file.stem}_aug{i}.jpg cv2.imwrite(str(out_dir / out_name), aug_img) with open(out_label_dir / (Path(out_name).stem .txt), w) as f: for bbox, cls in zip(aug_boxes, aug_labels): f.write(f{int(cls)} {bbox[0]:.6f} {bbox[1]:.6f} {bbox[2]:.6f} {bbox[3]:.6f}\n)逻辑说明这里用albumentations的BboxParams指定formatyolo意味着输入的bbox坐标就是YOLO格式的归一化值增强库会自动帮你处理旋转、翻转后坐标的重新计算。min_visibility0.3的意思是如果某个框在增强过程中大部分被移出图像外剩余可见部分不足30%这个框就会被丢弃避免训练时出现“几乎看不到目标却标注了框”的矛盾样本。参数说明Rotate的border_mode用了BORDER_CONSTANT且value0也就是旋转空出来的区域填黑色。黑色在图像里和工具背景差异大不会干扰模型学习目标特征。每次循环生成3张增强图生成前判断增强后是否还有有效框没有就直接跳过防止写出空txt文件。增强副本生成后记得把原图也保留在训练集里只扩充、不替换。5. 标注质量排查与常见问题避坑五条血泪经验5.1 现象解析xml时object节点为空或类别名对不上有一次我拿到一份改造过的数据集训练到一半发现loss异常跳动后来打印xml内容才发现里面有些object节点没有name子节点有些类别名写成了“Pliers”带大写。原因是标注人员用的labelImg版本不同自动补全了不同的大小写习惯或者标注过程中误点创建了空目标。原因分析VOC格式本身没有强约束类别名必须唯一labelImg也不会在保存时校验。训练时如果脚本用精确匹配去映射类别这些脏数据就会被跳过或映射错误。解决办法统一做一次清洗。遍历所有xml把object节点的name标准化成小写删除没有name的object。如果发现某个xml清洗后一个object都不剩直接连同图片和txt一起移入一个backup目录不参与训练。5.2 现象图片文件名和标注文件名对不上这个坑在下载的数据集里太常见了。比如图片叫001.jpgxml叫001.xml但txt叫001_label.txt或者某些图片复制了一份在名称末尾加了(1)导致set差集非空。用第3章的check_dataset.py一跑就能现形。原因分析很多数据集是半手工整理的生成txt时用了不同的命名规则或者解压后操作系统自动重命名了重复文件。YOLO训练对文件名的要求是严格同stem任何偏差都会导致某个通道读不到标注。解决办法不要手动改文件名写一个脚本做stem统一。以jpg文件名为准扫描所有xml和txt把不匹配的重命名或直接移动到orphan目录。特别注意全角括号和半角括号的问题Windows复制文件产生的“副本”后缀也要一并处理。5.3 现象YOLO txt里归一化坐标越界训练YOLOv5或YOLOv8时控制台偶尔会刷出坐标越界的警告比如box坐标出现1.05或-0.02。严格来说归一化坐标应该永远在0到1之间越界意味着标注框有一部分在图像外或者转换脚本忘了做裁剪。原因分析标注人员在标框时把框拖到了画布边缘xmax恰好等于width计算后w1.0或者VOC转YOLO时使用了整数除法在Python 2时代很常见现在虽然Python 3不会这样但半自动脚本里float转换遗漏仍然会带来边界值。解决办法在第3章的转换脚本里已经加过min/max裁剪这里再补一个批量修复脚本逐行读txt对四个值做clip(0.001, 0.999)。注意不要直接clip到0或1因为0宽度或0高度的框没有实际意义反而会被某些框架当作空标注跳过。5.4 现象类别ID和data.yaml里的names对不上YOLO txt第一列是整数类别ID本身不含类别名而data.yaml里的names列表决定了这个ID对应哪个类别。如果names写成[scissors, pliers, screwdrivers]那么原来类别ID0的pliers会被模型当成scissors来学整个模型的语义完全错乱但loss依然会下降极具迷惑性。原因分析这份数据的txt是用labelImg生成的labelImg在YOLO模式下根据预定义的类列表分配ID如果你中途改过类列表的顺序txt里的ID就会和当前列表错位。而data.yaml的names如果是从网上博客复制来的顺序可能和原始标注不一致。解决办法写一个交叉校验脚本从xml里读出某张图的类别名再读对应txt第一列的ID用data.yaml的names反查是否一致。把所有不一致的样本单独打印出来。这里给一个快速验证思路import xml.etree.ElementTree as ET from pathlib import Path names [pliers, scissors, screwdrivers] def check_sample(xml_file, txt_file): tree ET.parse(xml_file) xml_names [] for obj in tree.getroot().findall(object): xml_names.append(obj.find(name).text.strip()) txt_ids [] with open(txt_file) as f: for line in f: txt_ids.append(int(line.split()[0])) for i, name in enumerate(xml_names): expected names.index(name) if expected ! txt_ids[i]: print(f不一致: {xml_file.stem} xml{name} 期望ID{expected} txt ID{txt_ids[i]}) sample_xml Path(tools_dataset/xyxr_tools_617.xml) sample_txt Path(tools_dataset/xyxr_tools_617.txt) check_sample(sample_xml, sample_txt)逻辑说明xml里存的是人类可读的类别名txt里存的是ID用names列表作为桥梁做反向映射。逐框对比xml类别名映射出的ID和txt实际写入的ID不一致就说明标注文件里有错位。这个脚本可以循环跑全部文件虽然慢但值得在第一次训练前做一次。5.5 现象zip压缩包解压不完整或文件名乱码这是资源分发很常见的坑。下载的zip在Windows自带压缩工具下解压发现xml文件打不开或者文件名变成一堆乱码训练时路径解析直接失败。原因分析压缩包在打包时使用了非UTF-8编码的文件名Windows资源管理器的解压逻辑和Linux/macOS不一致导致中文或特殊字符编码被错误解释。还有可能是下载过程被中断zip结构不完整解压后缺少后半部分文件。解决办法Windows用户建议直接用7-Zip或Bandizip解压这两个工具对编码兼容性更好Linux用户用unzip -O cp936或干脆unzip -O gbk处理旧编码格式。解压后跑一遍第3章的体检脚本确认文件数量是3668×3再加一个说明txt再开始后续操作。6. 训练完成后怎么验证从验证集指标到第一次实地部署训练完成后先别急着打包模型把验证集指标摊开看一遍。重点看三个数字mAP50、mAP50-95、以及每个类别的recall。mAP50能反映整体框得准不准但类别不均衡数据里整体指标会掩盖剪刀的问题所以必须看per-class的PR曲线。我习惯把val_pred_batch.png和confusion_matrix.png这两张图翻出来对照看如果pliers和screwdrivers之间有大量互相误检先检查是不是标注时类别定义不清晰比如尖嘴钳和普通钳子确实容易被新手标混。验证指标没问题后做一次实地测试。用手机拍一段工作台视频或者直接在训练机上跑一张没参与训练的场景图。注意测试图片的拍摄距离和角度尽量贴近真实使用条件不要用和训练集同风格的网图。YOLOv8的predict接口可以直接指定best.pt推理yolo predict modelruns/detect/train/weights/best.pt sourcetest_images/ saveTrue conf0.4conf设为0.4比默认的0.25更接近工程落地的阈值误检能明显少一些。查看输出的结果图时我会重点看那些“交叠堆放的工具”场景——钳子和螺丝刀叠在一起时模型是否能把两个框都稳住如果只出来一个框多半是NMS阈值需要调低或者置信度阈值需要优化。这里用到的参数基本就是conf和iou前者控制漏检和误检的权衡后者控制相邻框的合并策略两者配合在验证集上多跑几轮。一个始终要把在后头的点好用的工具检测模型指标不是终点稳定才更重要。我拿这份数据训练时吃过一次教训光盯着mAP50达到了0.93以为万事大吉结果把模型部署到现场后晴天靠窗的强反光把螺丝刀直接照成白色高光连续漏检两分钟。从那以后我每次拿到数据集都强制走一遍统计脚本确认框数分布再开训每次训练完都故意找几道光影极端的测试图看看模型的底线在哪里。所有指标都是在特定数据分布下成立的这个边界比指标本身更值钱。希望帮到你。本文还有配套的精品资源点击获取