红外液体泄漏检测数据集:VOC与YOLO格式转换及训练避坑指南
简介:面向红外液体泄漏检测场景的深度学习目标检测数据集,覆盖2474张640×640分辨率的红外图像,目标类别为单一类“leak”,共包含5816个矩形标注框,可用于YOLO、Faster R-CNN等常见检测模型的训练与评估。数据由labelImg工具人工画框标注,已做数据增强,同时提供Pascal VOC格式的xml标注文件和YOLO格式的txt标注文件,省去格式转换与二次标注的额外成本;统一图像尺寸可直接适配常用目标检测框架,适合开展目标检测算法对比、数据增强策略分析、红外场景识别模型验证等实验。压缩包内共2000个文件,主体为1999个XML标注文件,另附1份TXT说明文件,整体大小约185.64MB。整体内容组织清晰,便于快速按批次读取标注信息。需要留意的是,数据未划分训练、验证与测试集,需使用者自行拆分;作者也明确声明不对训练所得模型或权重文件精度作任何保证。目前已有49人学习下载,适合具备一定目标检测基础、需要扩充红外场景泄漏样本的研究者或开发者。
1. 红外液体泄漏检测数据集:2474 张双格式标注,拿到手先别急着训练
化工厂管道巡检、液压站漏油监测、实验室台面液体残留检测,这类场景里红外相机比可见光相机可靠得多——夜间无光、蒸汽弥漫、油污与水渍在可见光下难以区分时,红外图像能把温度差异变成清晰的明暗对比。但红外检测项目真正卡人的不是相机选型,而是标注数据。普通目标检测数据集大多是可见光下的自然图像,泄漏场景的公开数据集极少,工业现场又涉及保密协议不方便外传,所以一份整理好的红外泄漏检测数据集就显得很关键。
这份资源包含 2474 张红外泄漏检测图像,同时提供 VOC 和 YOLO 两种标注格式,覆盖液体泄漏、飞溅、滴落、残留积液等典型状态。适合做工业安全巡检、智慧工厂视觉方案、红外目标检测模型训练的从业者直接用于模型训练和验证。拿到手之后建议先做标签分布统计和格式校验,再进训练流程,这两步能帮你避开后面大半的坑。
2. 先看清 VOC 和 YOLO 两份标注:坐标体系差异与转换逻辑
2.1 VOC 的 XML 里存的是绝对坐标,YOLO 的 txt 里存的是归一化中心点
这两套格式是目标检测数据集最常见的标注形式,但它们的坐标存储逻辑完全不同。VOC 格式(Pascal VOC)用 XML 文件描述每一个目标,标注信息量很大:不仅有 bounding box 坐标,还包含图片文件名、尺寸、通道数、目标类别、是否截断、是否难分样本等信息。而 YOLO 格式把每个目标拆成一行文本,只保留类别 id、中心点坐标和宽高,而且全部归一化到 0 到 1 之间。
拿一张 640x480 的红外泄漏图像举例,如果标注了一个泄漏点,边界框左上角在 (200, 150),右下角在 (400, 350),那么在 VOC 的 XML 里存的就是这么一段:
<annotation> <filename>leak_001.jpg</filename> <size> <width>640</width> <height>480</height> <depth>3</depth> </size> <object> <name>liquid_leak</name> <bndbox> <xmin>200</xmin> <ymin>150</ymin> <xmax>400</xmax> <ymax>350</ymax> </bndbox> </object> </annotation>同样的目标转到 YOLO 格式,是一行文本:0 0.46875 0.520833 0.3125 0.416667。拆开看,0 是类别 id,0.46875 是中心点 x 坐标((200+400)/2/640),0.520833 是中心点 y 坐标((150+350)/2/480),0.3125 是归一化宽度((400-200)/640),0.416667 是归一化高度((350-150)/480)。
这里有个容易混淆的地方:YOLO 格式存的是归一化后的中心点坐标和宽高,不是左上角右下角。训练时如果直接拿 VOC 的 xmin、ymin、xmax、ymax 塞给 YOLO 使用,模型训练出来的框全是偏的。这类错误在损失函数上表现不太明显,loss 照样下降,但验证集的 mAP 就是上不去——典型的"训练玄学"问题,根源其实是数据预处理。
2.2 为什么这份数据集要同时给两份标注
做训练的人会有一个感受:不同框架对数据集格式的要求不一致。Ultralytics YOLO 直接读 txt 标注文件,而一些做检测的老流程、基于 Detectron2 或者 MMDetection 的项目默认读 VOC 或 COCO 格式。如果数据集只给一种格式,换框架的时候就要自己写转换脚本,转换过程又要处理路径、类别表、图片尺寸读取这些细节,稍不注意就把数据搞坏了。
这份数据集直接提供 VOC 和 YOLO 两份标注,省掉了中间转换这一步。但我的习惯是:即使拿到双格式,也不要直接盲目信任其中某一份,而是抽几张图做交叉验证——打开一份 XML 和对应 txt,手动比对一个目标的坐标换算结果是否一致。两份标注都验过了再进训练,心里才有底。
实际使用时还要注意一个问题:VOC 格式里类别名是字符串,YOLO 格式里类别名变成了数字 id。直接用 YOLO 训练的时候,需要确认类别 id 和类别名的映射关系,也就是那份classes.txt。这份数据集的类别列表在解压后的classes.txt里可以看到,训练前务必核对一遍,别出现本地类别表和数据集类别表顺序不一致的情况——一旦错位,所有标注都等于标错了类。
VOC 与 YOLO 标注格式的核心差异可以用一个表格概括:
| 维度 | VOC 格式 | YOLO 格式 |
|---|---|---|
| 文件类型 | XML | txt |
| 坐标类型 | 绝对坐标(像素值) | 归一化坐标(0~1) |
| 框的表达方式 | 左上角点 + 右下角点 | 中心点 + 宽高 |
| 每张图标注文件数 | 1 个 XML | 1 个 txt(每行一个目标) |
| 附带信息 | 文件名、尺寸、目标状态等 | 只有类别 id 和框坐标 |
| 常见适用框架 | Detectron2、MMDetection 旧版 | Ultralytics YOLO 系列 |
3. 把 VOC 转成 YOLO:转换脚本、归一化计算与目录梳理
3.1 自己动手写一个转换器,理解转换原理
虽然这份数据集已经带了 YOLO 格式,我仍然建议你手动走一遍转换流程。原因很简单:真实项目里你拿到的标注可能是老工程师留下的 VOC 格式,也可能是标注平台导出的自定义格式,转换这套技能绕不开。这里给出一个经过验证的脚本,核心逻辑是用 Python 的xml.etree.ElementTree解析 XML,然后做归一化计算并写出 txt 文件。
import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_file, class_names, output_txt_path): """ 将单个 VOC XML 标注转成 YOLO txt 标注 :param xml_file: VOC XML 标注文件路径 :param class_names: 类别名称列表,索引即类别id :param output_txt_path: 输出的 YOLO txt 文件路径 """ tree = ET.parse(xml_file) root = tree.getroot() # 读取图片宽和高,归一化时需要使用 size = root.find("size") img_width = int(size.find("width").text) img_height = int(size.find("height").text) yolo_lines = [] for obj in root.findall("object"): # 只处理正常标注目标,跳过难分样本和截断样本 difficult = obj.find("difficult") if difficult is not None and int(difficult.text) == 1: continue name = obj.find("name").text if name not in class_names: continue class_id = class_names.index(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) # 计算中心点坐标和宽高,然后归一化 x_center = (xmin + xmax) / 2.0 / img_width y_center = (ymin + ymax) / 2.0 / img_height width = (xmax - xmin) / img_width height = (ymax - ymin) / img_height # 防御性校验:归一化结果必须在0~1之间 if not (0 <= x_center <= 1 and 0 <= y_center <= 1 and 0 <= width <= 1 and 0 <= height <= 1): print(f"警告: {xml_file} 中存在越界坐标: {name} {xmin},{ymin},{xmax},{ymax}") continue yolo_lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}") with open(output_txt_path, "w") as f: f.write("\n".join(yolo_lines))这段代码里值得注意的细节有几个。difficult字段的处理方式——VOC 数据集中常把难以辨认的目标标记为难分样本,转换到 YOLO 格式时一般建议直接跳过,因为 YOLO 训练中这些样本会导致 loss 震荡。归一化时用的是(xmin + xmax) / 2.0 / img_width,而不是(xmin + xmax) / img_width / 2.0,两者数学上等价,但前者更容易让人理解计算顺序。防御性校验加上之后,可以提前发现 XML 里坐标越界这类脏数据。
3.2 批量转换与目录结构梳理
单个文件的转换逻辑确认无误后,下一个步骤是批量处理和目录结构梳理。YOLO 训练时,默认期望的目标目录结构是:images/train、images/val存放图片,labels/train、labels/val存放对应的 txt 标注文件。图片和标注文件名必须一一对应,后缀可以不同但主名必须一致。
下面这个脚本做了三件事:遍历所有 XML 文件执行转换、统计类别分布、同时输出一份类别列表。
import os import glob from collections import Counter def batch_convert_voc_to_yolo(voc_dir, output_dir, class_names): """ 批量转换VOC标注为YOLO格式,并整理目录结构 :param voc_dir: 存放VOC XML文件的目录 :param output_dir: 输出目录,会生成images和labels子目录 :param class_names: 类别列表 """ os.makedirs(os.path.join(output_dir, "labels"), exist_ok=True) xml_files = glob.glob(os.path.join(voc_dir, "*.xml")) class_counter = Counter() for xml_file in xml_files: # 图片和标注在同一目录,文件名主干相同 base_name = os.path.splitext(os.path.basename(xml_file))[0] txt_path = os.path.join(output_dir, "labels", base_name + ".txt") voc_to_yolo(xml_file, class_names, txt_path) # 统计类别分布 tree = ET.parse(xml_file) root = tree.getroot() for obj in root.findall("object"): class_counter[obj.find("name").text] += 1 print("类别分布统计:") for class_name, count in class_counter.items(): print(f" {class_name}: {count} 个目标") # 写类别列表文件 with open(os.path.join(output_dir, "classes.txt"), "w") as f: f.write("\n".join(class_names)) print(f"转换完成,共处理 {len(xml_files)} 个XML文件")这个脚本里我加了类别分布统计功能。做数据集的时候最容易忽略的一步是统计类别平衡性——如果泄漏滴落样本有 2000 个、而喷射样本只有 100 个,模型训练完之后小类别的召回率会非常难看。这个信息在训练前知道,比训练完看混淆矩阵才知道有用得多。
3.3 拿这份数据集时建议保留的目录组织方式
解压这份数据集的压缩包后,我的习惯是直接按下面的结构重新组织一份干净的工作目录:
. ├── images/ │ ├── train/ # 约 1980 张训练图 │ └── val/ # 约 494 张验证图 ├── labels/ │ ├── train/ # 对应训练图标注,每张图一个txt │ └── val/ # 对应验证图标注 └── classes.txt # 类别列表文件数据集原始的划分比例是 8:2,这个比例对万级以下的数据集来说是合理默认值。如果你要自己重新划分,有一条经验:泄漏检测场景中的连续帧图像往往存在时间关联,同一次泄漏过程的连续帧容易被同时分到训练集和验证集,导致验证指标虚高。后面避坑章节会细说这个问题。
4. 训练前先过三关:标签可视化、类别分布检查与 YOLOv8 配置
4.1 标签可视化:把边界框画回原图,一眼看出标注是否错位
拿到数据集直接开训练的后果往往是在损失值下降一段时间后才发现问题——某个类别识别不出来,或者验证集 mAP 始终在低位徘徊。血泪经验告诉我,训练前花 20 分钟做一次标签可视化,能省下 3 个小时的排错时间。
import cv2 import numpy as np def visualize_yolo_labels(image_path, label_path, class_names, save_path=None): """ 将YOLO格式标注画回原图上,检查标注是否错位 :param image_path: 图像文件路径 :param label_path: 对应txt标注文件路径 :param class_names: 类别名称列表 :param save_path: 可视化结果保存路径 """ img = cv2.imread(image_path) height, width = img.shape[:2] with open(label_path, "r") as f: lines = f.readlines() for line in lines: parts = line.strip().split() class_id = int(parts[0]) x_center = float(parts[1]) * width y_center = float(parts[2]) * height box_width = float(parts[3]) * width box_height = float(parts[4]) * height x1 = int(x_center - box_width / 2) y1 = int(y_center - box_height / 2) x2 = int(x_center + box_width / 2) y2 = int(y_center + box_height / 2) color = (0, 255, 0) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, class_names[class_id], (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1) if save_path: cv2.imwrite(save_path, img) else: cv2.imshow("label check", img) cv2.waitKey(0)我建议抽样 30 张图,涵盖不同背景温度、不同泄漏部位、不同泄漏状态的图像,逐一可视化。看的时候重点检查两类问题:一是框是否包住了完整的目标,红外图像中泄漏液体的边缘往往存在温度过渡带,标注如果只框住了高温核心区,训练出的模型预测框就会偏小;二是框的中心点是否落在目标本体上。
4.2 训练集与验证集的划分策略:按图像来源分组,不按单帧随机分
在泄漏检测这类时序密集型场景里,数据划分是一个容易被忽略但影响非常大的因素。同一个泄漏过程拍摄的连续帧,画面内容高度相似。如果随机划分,相近的帧被分到训练集和验证集,验证结果会过度乐观——模型在训练中已经见过几乎一样的画面,验证时只是换了个角度或者换了个时间点,mAP 参考价值很低。
合理的做法是按图像序列或者采集时间分组。这份数据集解压后如果能看到文件名中有时间戳或者场景编号的规律,建议按场景分组再划分训练集和验证集。我处理类似数据集时的习惯是:把同一拍摄场景、同一段连续拍摄的图像归到一个组里,按组划分,确保验证集覆盖训练集未见过的场景。
划分时可以手动按比例抽取,用下面的脚本做分组划分的辅助:
import os import shutil import random from collections import defaultdict def group_split_by_scene(image_dir, label_dir, output_dir, test_ratio=0.2): """ 按文件名的分组前缀划分训练集和验证集 :param image_dir: 图片目录 :param label_dir: 标注目录 :param output_dir: 输出目录(含 images/train, images/val, labels/train, labels/val) :param test_ratio: 验证集比例 """ # 假设文件名格式为 scene001_frame001.jpg,按前后缀提取场景ID scene_groups = defaultdict(list) for img_name in os.listdir(image_dir): scene_id = img_name.split("_")[0] # 根据实际命名规则调整 scene_groups[scene_id].append(img_name) all_scenes = list(scene_groups.keys()) random.shuffle(all_scenes) val_scene_count = max(1, int(len(all_scenes) * test_ratio)) val_scenes = set(all_scenes[:val_scene_count]) for split in ["train", "val"]: os.makedirs(os.path.join(output_dir, "images", split), exist_ok=True) os.makedirs(os.path.join(output_dir, "labels", split), exist_ok=True) for scene_id, img_names in scene_groups.items(): target_split = "val" if scene_id in val_scenes else "train" for img_name in img_names: label_name = os.path.splitext(img_name)[0] + ".txt" shutil.copy(os.path.join(image_dir, img_name), os.path.join(output_dir, "images", target_split, img_name)) shutil.copy(os.path.join(label_dir, label_name), os.path.join(output_dir, "labels", target_split, label_name))对于这份数据集,我建议直接按 8:2 比例保持默认划分,但要检查原始划分是否按场景隔离——如果发现同一场景的连续帧同时出现在 train 和 val 里,宁可自己重新分一次,也不要省这个时间。
4.3 YOLOv8 训练配置文件与启动命令
数据校验完成之后,配置文件就简单了。用 Ultralytics YOLOv8 训练时,yaml 配置文件长这样:
# dataset.yaml path: /path/to/leak_dataset # 数据集根目录 train: images/train # 训练集图片目录 val: images/val # 验证集图片目录 # 类别定义,顺序必须与classes.txt一致 names: 0: drip # 滴落泄漏 1: spray # 喷射泄漏 2: pool # 积液残留 3: stream # 流动泄漏启动训练的命令:
yolo detect train \ model=yolov8s.pt \ data=dataset.yaml \ epochs=100 \ imgsz=640 \ batch=16 \ device=0 \ patience=20这里有几个参数值得解释一下。model=yolov8s.pt是在 COCO 预训练权重基础上做迁移学习,红外图像虽然和自然图像差异很大,但预训练模型已经学会了边缘、纹理、形状等低级特征,迁移到红外场景仍然有效。imgsz=640是输入分辨率,红外泄漏目标通常较小,如果你的场景中目标像素占比很小,可以考虑在 640 基础上增加到 768 或 1024,但要注意显存占用翻倍。patience=20意思是连续 20 个 epoch 验证集指标没有提升就提前停止训练,这个参数能帮你省时间。
5. 避坑指南:红外泄漏检测数据集使用中的六个常见问题
5.1 现象:训练 loss 持续下降,但验证集 mAP@0.5 不到 0.3
这个问题我排查了整整一个下午,最终发现不是网络结构问题,而是数据划分问题。训练集和验证集之间存在相似帧泄漏,模型在验证集上表现虚高才合理,表现虚低反而可疑——原因是验证集中出现了训练集中完全没有的场景类型,模型泛化能力不足。
解决方法是检查数据划分是否按场景隔离。如果原始划分是随机划分的,重新按场景分组——把同一泄漏过程、同一机位拍摄的图像全部归入同一集合。
5.2 现象:红外图像中泄漏区域的边缘有亮圈,标注框偏大导致误检
红外图像的泄漏区域特征和可见光图像差异很大:泄漏液体在热成像中通常表现为低温(暗色)区域,周围可能存在高温边缘过渡带。如果标注时把过渡带整个框进去,模型学到的特征就是"暗区+亮边",在真实场景中遇到其他类型的亮边就会误检。
解决方法是统一标注规范:只框泄漏液体本体,不包含温度过渡带。如果你的场景中泄漏液体与背景温差不大,建议先做对比度增强预处理再标注。
5.3 现象:训练时报错 "assertion failed: height > 0"
这类报错通常出现在标注文件中有越界坐标的情况,比如框的 xmin 大于 xmax、ymin 大于 ymax,或者宽高计算出负数。也可能是图片读取失败后得到空数组,但标注文件仍然引用了它。
解决方法是跑一遍数据校验脚本,检查每个 txt 文件中的坐标是否满足0 < x_center < 1、0 < width < 1的条件,同时检查图片文件是否能正常用 cv2 读取。
5.4 现象:验证集某个类别的 precision 很高但 recall 极低
这是典型的类别不平衡问题。泄漏数据集中"滴落"类样本可能占比极高,而"喷射"类样本占比极低,模型倾向于把所有目标都预测为高频类别——precision 高是因为预测框中确实大部分是滴落,recall 低是因为喷射类目标大量漏检。
解决方法是先看类别分布统计,如果某类样本数过少,考虑做数据增强(尤其是这类样本的翻转变换),或者采用 YOLOv8 的class_weights参数调整损失权重。
5.5 现象:训练过程正常,推理时对小目标完全无响应
红外泄漏检测中,滴落的液体往往只占几十个像素。推理时如果输入分辨率不够,小目标特征在多次下采样后被完全抹掉,模型自然检测不到。
解决方法是推理时把imgsz调整到与训练一致甚至更高,并使用yolo detect predict的conf=0.25参数调低置信度阈值——红外场景干扰少,置信度阈值可以比可见光场景设得更低。
5.6 现象:换了一个场景的测试图之后,漏检明显增多
红外数据集有一个天然问题了:不同材质的背景辐射率差异巨大,管道、阀门、水泥地面在热成像里的表现完全不同。训练集如果只覆盖了某一种背景,模型在换背景后性能骤降。
解决方法是收集数据时尽可能覆盖多种表面材质和入射角度。这份数据集虽然已经涵盖了多种泄漏状态,但实际项目中对新增场景做少量补充标注仍然值得坚持。数据覆盖的边界就是模型性能的边界。
6. 训练后的验证流程:从 PR 曲线到实际场景回灌测试
训练完成后,不要只看终端打印的 mAP 数字就收工。mAP 是一个聚合指标,掩盖了很多细节信息。我建议按下面的流程做一轮完整验证。
第一步是查看每个类别的 PR 曲线和混淆矩阵。YOLOv8 训练完成后会在runs/detect/train/目录下生成PR_curve.png、confusion_matrix.png等文件。看 PR 曲线时重点关注曲线是否在右上角明显弯曲——曲线越贴近左上角说明精确率和召回率平衡得越好。如果某个类别的曲线明显低于其他类别,这个类别的样本量或者标注质量一定有问题。
第二步是用一段独立的视频或者连续帧做回灌测试。选一段训练集和验证集都没出现过的泄漏过程视频,逐帧推理,然后按帧检查漏检和误检。这一步之所以关键,是因为它是检验模型泛化能力的最直接手段。回灌测试时如果出现连续数帧漏检,注意检查推理设置里的imgsz是否与训练一致——一个常见的翻车场景是训练用了 640,推理时为了省时间改成 320,小目标直接消失。
第三步是调试置信度阈值。YOLOv8 默认推理置信度阈值是 0.25,但红外泄漏检测场景干扰少、目标特征明确,实际部署时 0.1 到 0.15 也能获得可接受的误检率。具体调多少,取决于你的业务对误检和漏检的容忍度——泄漏检测场景通常对漏检零容忍,对误检可以通过后续逻辑过滤。
把模型导出为 ONNX 或 TensorRT 格式做推理加速,也是一个值得考虑的步骤:
yolo export model=runs/detect/train/weights/best.pt format=onnx imgsz=640ONNX 格式部署到 CPU 或者 Jetson 平台时,推理速度比 PyTorch 原生快 2 到 3 倍。导出后建议顺手做一次输出一致性验证——用同一张图在 PyTorch 模型和 ONNX 模型上分别推理,对比检测框差异是否在合理范围内。
回到这份数据集本身,我最后想提一个习惯:每次新拿到一份数据集,不管是什么格式、不管是谁整理的,我都会强制走一遍标签可视化、类别统计、数据划分检查这三个步骤,再开始训练。这个习惯让我避开了很多看似玄学、实则是数据问题的训练事故。红外泄漏检测这类场景,模型架构大家都能跑通,真正的差异在数据准备是否扎实。希望这份数据集和这些方法帮到你。
本文还有配套的精品资源,点击获取