
简介这是一套面向目标检测任务的小型汽油泄漏检测数据集包含237张JPEG图像及配套VOC格式与YOLO格式标注可直接用于训练YOLO系列或SSD等常见检测模型。数据集中标注了两个类别petrol212个框与water94个框涉及矩形框目标共计306个全部使用labelImg人工标注适合初学者练习模型训练流程或进行泄漏检测方向的快速验证。压缩包共713个文件其中237个jpg为原始图像237个xml为Pascal VOC标注另有239个txt文件提供YOLO格式标签与相关配置整体仅16.61MB便于下载和迁移。需要提醒的是真实汽油泄漏图像很难获取因此大部分图片属于增强样本建议先查看预览图再决定是否使用数据集保证标注合理准确但不承诺训练所得模型的精度表现。目前已有288人浏览学习可作为目标检测数据集制作与调参的参考。1. 237张、2类别的泄漏检测数据集为什么值得拿来当基线加油站、油库管道的泄漏检测场景里真正难的不是模型结构而是现场数据太少、目标太小、反光太多。手头这份“汽油检泄漏检测数据集VOCYOLO格式237张2类别.7z”属于典型的工业视觉小样本数据237张图、2个类别同时给了VOC的xml框和YOLO的txt框。这种双格式很有价值省去了到处找转换脚本的时间但也埋了不少坑。目录没摸清就训练、类别顺序搞错、验证集混进训练集都会让结果看起来很漂亮一上现场就翻车。这篇笔记按我实际做过一遍的方案来写从解压、目录摸底、坐标转换一路到训练参数和验证技巧适合准备用YOLO做加油站泄漏识别或正在被标注格式折磨的开发者参考。2. 解压与目录摸底在Linux里用7z解开VOCYOLO双格式先看结构再动手拿到.7z以后第一步不是急着找训练命令而是把它安全解开、把目录结构看清楚。很多小数据集压缩包里的VOC目录和YOLO目录并不完全对应比如xml有238个、txt只有200个或者标签里混入空文件。这些问题在训练前不暴露训练中就会变成loss异常和mAP为0的玄学故障。2.1 在Linux下解压7zp7zip安装与三条验证命令7z格式在Windows下双击就能解但Linux服务器上往往会提示找不到7z命令。常见做法是安装p7zip系列工具然后先列压缩包内容、再解压# Debian/Ubuntu 安装 p7zip apt-get update apt-get install -y p7zip-full # 只查看压缩包里的目录结构不做解压 7z l 汽油检泄漏检测数据集VOCYOLO格式237张2类别.7z # 解压到指定目录-o 参数后面不能有空格 7z x 汽油检泄漏检测数据集VOCYOLO格式237张2类别.7z -o./gasoline_leak_data -y7z l 比 unzip -l 更实用能直接看到压缩包里是不是自带一层顶层目录。如果直接解压出来一堆文件混在当前目录后面对比文件数量会很痛苦。7z x 的 -y 参数表示遇到同名文件时直接覆盖避免交互卡住。注意 -o 和目录之间不要写空格否则会被当成一个额外参数。解压完成后先用 find 快速看两层目录结构# 看目录树没有 tree 就用 find find ./gasoline_leak_data -maxdepth 2 -type d | sort一个标准的双格式数据集通常会包含两类内容一类是VOC风格的JPEGImages、Annotations、ImageSets/Main另一类是YOLO风格的images、labels。前者管原始图片和xml描述后者管归一化坐标的txt文件。两者共用同一批图片只是标注表达方式不同。还没完先对一下数量# 统计图片、xml、txt数量先和237这个数对一下 find ./gasoline_leak_data -name *.jpg | wc -l find ./gasoline_leak_data -name *.xml | wc -l find ./gasoline_leak_data -type f -path *labels* -name *.txt | wc -l如果三个数字差得不多说明包基本健康。如果txt数量明显少于图片数量就要考虑有部分目标没被标注或存在空标签文件。这个检查比任何训练参数都重要因为YOLO会把没有txt的图片当作无目标样本直接把背景类学死了。2.2 双格式长什么样从XML的像素框到TXT的归一化框VOC格式的核心是Annotation目录下的xml文件。随便取第一个xml看一下xml_file$(find ./gasoline_leak_data -name *.xml | head -1) head -n 30 $xml_file正常情况下能看到类似这样的结构annotation folderJPEGImages/folder filename0001.jpg/filename size width1920/width height1080/height depth3/depth /size object nameleak/name bndbox xmin734/xmin ymin412/ymin xmax988/xmax ymax566/ymax /bndbox /object /annotation这里最关键的字段是size和bndbox。size里的宽高必须和jpg真实分辨率一致否则后面转YOLO坐标时所有框都会偏移。object可能不止一个每一组bndbox都要读出来不能只取第一项。还有部分xml带difficult字段表示这个目标很难识别转换时需要决定保留还是过滤。再看YOLO格式的txtyolo_file$(find ./gasoline_leak_data -name *.txt | grep -v classes | head -1) cat $yolo_file每一行类似0 0.448 0.453 0.132 0.142含义是类别ID、目标中心x、中心y、目标宽、目标高后四项都是相对图片宽高的比值取值在0到1之间。注意“类别ID”是整数对应classes列表中的顺序而不是类别名称。VOC里写的是leakYOLO目录里可能就变成了0或1这个映射关系必须靠并发读取xml和txt来确认不能靠猜。两种标注格式的对比如下项目VOC(xml)YOLO(txt)坐标含义左上角x, 左上角y, 右下角x, 右下角y中心x, 中心y, 宽, 高坐标单位像素相对图片宽高的比值类别表达name字符串class_id整数文件扩展名.xml.txt典型目录Annotationslabels现在把双格式目录都摸清了接下来才轮到坐标转换。别跳过这步直接用自带的txt训练是可以但一旦发现txt和xml对不上到时候再排查会痛苦得多。3. 从VOC到YOLO坐标归一化脚本与三个容易错的地方虽然压缩包标题里写明同时包含VOC和YOLO格式但实际使用中经常遇到YOLO目录里的txt是从其他地方拷贝过来的类别顺序、图片分辨率对不上。我一般会直接把VOC目录当成原始标注来用自己生成一遍YOLO标签这样心里有底。3.1 用Python把VOC的xml转成YOLO的txt转换的原理不复杂xml里给的是左上角和右下角的像素坐标YOLO需要的是目标中心和宽高的归一化值。核心公式如下x_center (xmin xmax) / 2 / widthy_center (ymin ymax) / 2 / heightbox_w (xmax - xmin) / widthbox_h (ymax - ymin) / height下面这段脚本可以直接在数据集根目录运行import os import glob import xml.etree.ElementTree as ET VOC_ROOT ./gasoline_leak_data/VOC ANN_ROOT os.path.join(VOC_ROOT, Annotations) YOLO_LABEL_DIR ./gasoline_leak_data/YOLO/labels # 这里先按占位写实际要以xml里的name字段为准 # 顺序一旦定下来后续所有训练配置都要跟它一致 class_names [leak, gasoline] os.makedirs(YOLO_LABEL_DIR, exist_okTrue) def convert_annotation(xml_path, out_dir): tree ET.parse(xml_path) root tree.getroot() size root.find(size) w int(size.find(width).text) h int(size.find(height).text) if w 0 or h 0: print(fwarning: {xml_path} size error) return img_name os.path.splitext(os.path.basename(xml_path))[0] out_lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_names: print(fwarning: {xml_path} contains unknown class {name}) continue class_id class_names.index(name) box obj.find(bndbox) xmin int(float(box.find(xmin).text)) ymin int(float(box.find(ymin).text)) xmax int(float(box.find(xmax).text)) ymax int(float(box.find(ymax).text)) # 把坐标限制在图像边界内防止无效框 xmin max(0, min(xmin, w - 1)) ymin max(0, min(ymin, h - 1)) xmax max(xmin 1, min(xmax, w - 1)) ymax max(ymin 1, min(ymax, h - 1)) x_center (xmin xmax) / 2.0 / w y_center (ymin ymax) / 2.0 / h box_w (xmax - xmin) / w box_h (ymax - ymin) / h out_lines.append( f{class_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f} ) if out_lines: out_path os.path.join(out_dir, img_name .txt) with open(out_path, w) as f: f.write(\n.join(out_lines) \n) for xml_path in glob.glob(os.path.join(ANN_ROOT, *.xml)): convert_annotation(xml_path, YOLO_LABEL_DIR) print(converted done)逻辑上先解析xml根节点拿到width和height。接着遍历所有object节点每得到一个有效目标就按公式换算。class_names的索引就是class_id因此这里要求class_names的顺序必须和之后训练用的data.yaml保持一致否则ID对不上模型会把汽油识别成漏油。脚本里还做了一步边界保护xmin、ymin、xmax、ymax先转成int再夹在图像有效范围内。这一步不是多余处理很多标注工具的标注框会轻微越界不做clip的话归一化坐标会出现负数或大于1训练时损失函数直接崩掉。3.2 验证转换结果把坐标画回去看最直观转换脚本写完别急着训练先随机抽几张图把txt画回去看框是否贴合目标import cv2 img cv2.imread(./gasoline_leak_data/VOC/JPEGImages/0001.jpg) if img is None: print(image not found) else: h, w img.shape[:2] with open(./gasoline_leak_data/YOLO/labels/0001.txt) as f: for line in f: parts line.strip().split() if len(parts) 5: continue _, xc, yc, bw, bh map(float, parts) x1 int((xc - bw / 2) * w) y1 int((yc - bh / 2) * h) x2 int((xc bw / 2) * w) y2 int((yc bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite(./check_visualize.jpg, img)画完之后如果框整体偏上、偏左或者和画面里的目标错位优先怀疑size字段和实际jpg分辨率不一致。如果框都挤在图片角落多半是归一化时用错了分母比如把短边当成宽高。三个容易错的地方第一是xml里difficult字段没有处理。VOC里difficult1的目标属于很难被人工判定但确实存在的目标直接转进YOLO后模型会被迫学习一个矛盾的标签。小数据样本建议保留但要在训练时记录这些框观察模型是否过度拟合难例。第二是class_names写死为某个名称而xml里实际是别的拼写比如gasoline写成gas、leak写成leakage脚本会静默跳过尤其危险。第三是txt路径和jpg路径没有完全对应YOLO要求标签文件和图片文件同名、同目录结构。4. 训练YOLO前的参数边界预训练权重、数据划分、学习率与损失函数有了一份能用的YOLO标签接下来的问题是237张图、2个类别应该怎么训练。很多教程直接复制默认参数但小样本下默认配置很容易让模型过拟合或者验证集表现极不稳定。4.1 数据划分与data.yaml先固定验证集再谈随机种子237张图如果全扔进训练验证集只有不到几十张对泄漏这种目标占画面比例很小的任务来说mAP浮动会非常大。我一般会按8:2划分也就是训练约190张、验证47张。关键是每次划分固定随机种子保证可复现import os import random import shutil from glob import glob random.seed(42) image_dir ./gasoline_leak_data/YOLO/images label_dir ./gasoline_leak_data/YOLO/labels images glob(os.path.join(image_dir, *.jpg)) random.shuffle(images) val_count int(len(images) * 0.2) val_images images[:val_count] train_images images[val_count:] for split, imgs in [(train, train_images), (val, val_images)]: os.makedirs(os.path.join(image_dir, split), exist_okTrue) os.makedirs(os.path.join(label_dir, split), exist_okTrue) for img_path in imgs: img_name os.path.basename(img_path) label_path os.path.join(label_dir, img_name.replace(.jpg, .txt)) shutil.move(img_path, os.path.join(image_dir, split, img_name)) if os.path.exists(label_path): shutil.move(label_path, os.path.join(label_dir, split, img_name.replace(.jpg, .txt)))移动完成后labels目录下的txt也划分进同名子目录。这一步容易踩坑只移动图片不移动标签训练时YOLO会认为验证集里所有图片都没有目标mAP曲线变成一条水平线。data.yaml也按划分后的目录写path: ./gasoline_leak_data/YOLO train: images/train val: images/val nc: 2 names: 0: leak 1: gasolinenames的顺序必须和上一章转换脚本里的class_names保持一致。这一条说了无数次但确实是我见过最多翻车的原因。验证集不想移动的话可以用软链接或复制但路径要是相对path的路径写绝对路径会损失可移植性。4.2 预训练权重、学习率与损失函数小样本的核心约束YOLO系列的工程实现里训练命令通常长这样yolo train modelyolov8n.pt datagasoline.yaml epochs200 imgsz640 batch16 lr00.001 project./runs namegasoline_leak第一次跑工程时让官方脚本自动拉预训练权重。这种小数据集直接用olov8n或者yolo11n这类轻量权重不需要用大模型因为只有237张图模型复杂度越高越容易过拟合。imgsz先固定640泄漏目标如果不是特别小640够用目标宽度小于32像素再考虑把imgsz提到768或1024但代价是显存和时间成倍增长。batch受显存限制8G显存建议batch8再小就会出现训练中BN崩溃的现象——loss输出变成nan或者验证mAP直接掉零。原因在于批量太小导致BN统计量在每一批之间剧烈抖动小样本下尤其明显。解决方案不是调大学习率而是先保证batch至少在8以上再把lr0从默认的0.01降到0.001。预训练权重已经给了比较好的起点学习率过高只会让模型快速丢失初始特征。训练日志里的损失函数通常分三类box_loss、cls_loss、dfl_loss。box_loss衡量预测框和真实框的差异cls_loss衡量分类概率分布dfl_loss是针对目标框分布建模的损失项。当val_loss明显高于train_loss时不要只盯总损失先看box_loss和cls_loss哪个涨得快。如果box_loss涨得快一般是目标太小、先验框不够匹配如果cls_loss涨得快更可能是类别不均衡比如泄漏样本远少于汽油样本。小数据集还有一个比较隐晦的参数mosaic增强。默认开启的mosaic会把四张图拼接成一张训练标签也随机裁剪。对目标小、背景复杂的泄漏检测任务过度拼接会让目标变成碎片反而不利于收敛。建议关闭或调低mosaic保留fliplr和轻微的颜色抖动就够了yolo train modelyolov8n.pt datagasoline.yaml epochs200 imgsz640 batch16 lr00.001 hsv_h0.01 hsv_s0.6 hsv_v0.4 mosaic0.5 project./runs这些改动不是玄学是针对小目标小样本场景做出的实际取舍。mosaic0.5意味着平均两张图里会有一张做拼接既能保留多样性又不会让目标特征过度破坏。5. 避坑小样本数据集的4个典型事故从7z密码到BN崩溃这类数据集的每个环节都有坑。下面四条是我印象最深、在多个项目里反复遇到的问题按现象、原因、解决三个步骤写清楚。5.1 解压与标注文件的事故第一个事故7z压缩文件密码是正确的但解压报错。现象是密码输对后代码直接报Data Error或CRC Failed。原因通常不是密码而是压缩包在传输过程中损坏或者多个分卷没合并完整。解决方法是先重新下载一次再用7z t做完整性测试7z t 汽油检泄漏检测数据集VOCYOLO格式237张2类别.7z如果测试通过再解压如果仍然CRC报错把文件复制到本地磁盘再测不要通过网络共享盘直接解压。第二个事故图片数量和标注数量对不上。现象是训练时总提示Missing annotation或者val集mAP奇高但实际推理一张不漏。原因在于有些图片有xml但没有jpg有些txt是空文件。解决方法是写一条命令找出具体名单# 找出存在jpg但不存在对应txt的图片 for f in $(find ./gasoline_leak_data/YOLO/images -name *.jpg); do txt${f%.jpg}.txt if [ ! -f $txt ]; then echo missing: $txt fi done找到后建议用满足以下条件的图片既有标注又不过于模糊。如果一个txt为空YOLO会把这张图当成纯背景样本。237张图中如果混入40张纯背景模型会倾向于把所有目标都预测成背景。5.2 训练与验证阶段的事故第三个事故mAP始终为0或者所有目标都被检测为同一个类别。现象是训练过程看起来正常损失也下降了但在验证集上PR曲线等于一条直线。原因大概率是类别ID错位。比如xml里第一个类叫leak转换脚本里class_names写成了[gasoline, leak]txt的第一列0就变成了gasoline但names顺序里0仍对应gasoline如果data.yaml里names顺序和脚本不一致就会把汽油当泄漏、泄漏当汽油。解决方法是把所有xml里出现过的name聚合成一个集合再按字母序固定顺序import glob import xml.etree.ElementTree as ET names set() for xml_path in glob.glob(./gasoline_leak_data/VOC/Annotations/*.xml): tree ET.parse(xml_path) for obj in tree.getroot().findall(object): names.add(obj.find(name).text) print(sorted(names))打印结果后严格按这个顺序写class_names和data.yaml。如果发现需要的类别不在集合里说明图里有未预期的目标先清洗数据。第四个事故训练到一半loss变成nan或者val_loss曲线在几十个epoch后突然冲到天上。现象是train/box_loss还在下降但val/box_loss抖动剧烈有时还会出现BN崩溃。原因在小数据集里非常典型训练集样本太少随机到一批全部是小目标或低对比度目标BN统计量失稳。解决方法是先冻结预训练权重的backbone只训练head部分等loss稳定后再解冻全部层yolo train modelyolov8n.pt datagasoline.yaml epochs100 freeze10 lr00.0005 batch16freeze10表示冻结前10层再用低学习率微调。如果还是nan检查图片里有没有全黑、全白或小于imgsz的异常图这些图会导致特征图尺寸异常。把异常样本过滤掉再训。6. 用交叉验证和混淆矩阵验证237张图的检测效果小数据集的验证不只是看一次train/val划分的结果。只有237张图一次划分的随机性可能让结果完全不可信。我习惯的做法是跑五折交叉验证把237张图切成5份每次拿4份训练、1份验证最后把5次验证结果综合起来看稳定度。五折不需要写复杂脚本只要把上一章划分代码的random.seed分别换成0到4每次生成一组train/val文件夹训练结束后把验证集mAP记录下来。重点关注两个值五折之间mAP的标准差以及最差一折的表现。如果某折mAP特别低大概率是那一折里包含了大量困难样本比如逆光、油渍和阴影混淆。这个信息比平均mAP更有价值能告诉你模型在哪些光照条件下会崩。训练完成后不要只看最终的mAP曲线打开runs目录下的混淆矩阵图。YOLO生成的混淆矩阵里列总和通常不是100%因为有些目标没有被任何一个类别预测命中会落到background列。这是YOLO的统计逻辑决定的不是代码bug。真正的重点是看属于泄漏类别的样本被分到了哪个格子里。如果泄漏那一行有大量样本被预测成汽油说明两个类别在视觉上确实太像光调参没用需要去检查标注框有没有把油迹标错类。对237张图这种量级我不建议反复调超参数去追验证集上的mAP那样只会造成对验证集的过拟合。更务实的做法是固定一组保守参数把时间花在检查误检样本上从混淆矩阵里挑出容易混类的图片人工复核标注框是否准确。一个标注框偏移十几像素可能让一个小目标完全变了样。训练小数据集最值钱的不是那零点几个点的mAP而是让每一张图都经得起人工检查。希望帮到你也希望你手里的泄漏检测方案能少踩几个我踩过的坑。本文还有配套的精品资源点击获取