简介:这是一套面向目标检测研究者和工程师的地面油污水渍检测数据集,聚焦油污水渍的自动识别与定位,可广泛应用于环境监控、公共安全、工业现场巡检等场景。资源包共2000个文件、大小约70.05MB,以VOC格式的XML标注文件为主体,并附有说明TXT,兼容Pascal VOC与YOLO两种标注格式,研究者和开发人员可直接在主流目标检测框架中加载使用。数据集包含2093张jpg图片及对应标注,唯一标注类别Spill共标注了2563个目标框,由labelImg按统一规则绘制矩形框完成;由于已做增强处理,能有效提升模型在不同光照、角度和遮挡条件下的泛化能力。资源包内文件组织规整,标注与图片对应关系明确,便于批量处理和迁移学习;对于希望快速搭建检测流程的用户,可省去自行收集与标注数据的环节。已有178人学习/下载,适合需要训练油污水渍检测模型、进行算法对比验证或从事相关课题研究的技术人员获取,既可用于模型训练,也可作为评测基准或数据增强参考。
1. 地面油污水渍检测数据集:2000张增强图背后的三个关键问题
地面油污水渍检测是目标检测里典型的“看着简单、做起来上头”的任务。油污和水渍的共同特征是低纹理、边界模糊、受光照反射影响极大,和环氧地坪、瓷砖、水泥地面的视觉差异往往只有几个灰阶和一层高光的区别。通用目标检测模型在这个任务上表现不稳定,不是因为YOLO网络不够强,而是缺乏与现场材质分布匹配的训练数据。以“2000张VOC+YOLO(已增强)”这套数据作为起点,可以直接跳过了凑图、清洗、初标这一类最耗时的环节,把精力集中在格式转换、标注校验和训练参数调校上。适合它的读者有两类:一类是做工业巡检或商用机器人POC,需要在几天内跑通一个像样的检测结果;另一类是已有现场目标检测经验、想专门补油污水渍专项数据的实施工程师。下面按一条可完整复现的路径展开:先理解VOC与YOLO两套格式的内部逻辑,再做转换和增强后校验,接着接入YOLOv8训练流程,最后给出部署阶段能直接用上的阈值校准和形态学后处理技巧。
2. 地面油污水渍检测数据集的结构:VOC标注与YOLO归一化坐标的对齐逻辑
2.1 油污水渍的类别划分与标注边界约定
拿到任意一套“VOC+YOLO”格式数据,第一件要做的事往往被跳过:固定类别语义。对地面油污水渍任务来说,工程上最稳妥的类别划分方式是二分类,oil_stain和water_stain各占一列。VOC格式里对应object.name,YOLO格式里对应txt首列的整数ID。如果数据集中把油污和水渍合并成单一stain类,训练看起来简单,但到了现场就会发现“需要区分漏油和积水”这个需求完全没法满足。漏油意味着设备泄漏,需要走维修工单;积水更多与清洁有关。类别语义直接影响后续工单分发逻辑,属于方案设计阶段就要定死的东西。
标注边界约定是另一个决定数据质量的关卡。油污在环氧地坪上呈现为黄褐色半透明膜,水渍则是深浅不一的湿润色斑,二者都没有刚性轮廓。常见的工程约定是:油污以主油膜的扩散外沿为边界,零散油滴要么忽略要么单独成框,不能全部包进主框里;水渍以潮湿反光边缘为准,不要把周边地面上的镜面反射一起框进来。如果发现数据集中大量边界框比油污实际面积大出20%以上,说明标注尺度整体偏松。与其在训练后反复调anchor,不如先把这些边界框按统一标准整理一遍。
2.2 VOC格式XML标注里值得逐项核对的内容
VOC格式以xml文件存放标注,核心结构是annotation下挂filename、size和若干object节点。每个object里有name、truncated、difficult和bndbox。对油污水渍检测最有区分度的是truncated和difficult两个字段:truncated表示目标是否被图像边缘截断,difficult表示这个目标是否属于难例。很多发布版数据集为了压缩体积,会把这两个字段重置为0,但实际场景里,摄像头俯拍地面时,画面边缘出现半块油污的概率相当高。
一个实用的检查方式是先统计这两个字段的分布,不要在训练之后才去排查数据问题:
import xml.etree.ElementTree as ET import glob stats = {"truncated": 0, "difficult": 0, "total": 0} for xml_path in glob.glob("annotations/*.xml"): root = ET.parse(xml_path).getroot() for obj in root.iter("object"): stats["total"] += 1 if obj.findtext("truncated") == "1": stats["truncated"] += 1 if obj.findtext("difficult") == "1": stats["difficult"] += 1 print(stats) print(f"truncated ratio: {stats['truncated'] / stats['total']:.2%}")这段脚本每跑一次,就是在对标注集做一次体检。如果truncated比例低于1%同时图像里又频繁出现大块贴边污渍,说明训练集和真实部署视角不一致,验证集mAP再高,换一个机位就失效。difficult字段同理,它告诉模型哪些目标即使人眼都难判断,如果全部是0,不代表没有难例,只代表标注者没有把难易信息记录下来。
2.3 YOLO格式txt归一化坐标与VOC的换算关系
YOLO的txt每行代表一个目标,五个字段依次是类别ID、归一化中心x、归一化中心y、归一化宽度w、归一化高度h。VOC则记录矩形框的左上角和右下角像素坐标。把VOC转换为YOLO格式,用下面这组公式:
| 转换目标 | VOC字段 | YOLO字段 | 计算公式 |
|---|---|---|---|
| 中心点x | xmin, xmax | cx | (xmin + xmax) / 2 / width |
| 中心点y | ymin, ymax | cy | (ymin + ymax) / 2 / height |
| 宽度 | xmin, xmax | w | (xmax - xmin) / width |
| 高度 | ymin, ymax | h | (ymax - ymin) / height |
反向还原成像素坐标也很简单:xmin = (cx - w / 2) * width,xmax = (cx + w / 2) * width,ymin和ymax同理。这里唯一容易踩的坑是分子分母的单位问题,公式中的width和height必须是图片原始像素宽高,而不是训练时设置的imgsz=640。有人会在读取图片时误用模型输入尺寸做归一化,导致所有框的坐标同时放大或缩小,mAP直接崩到不可用。
2.4 增强操作给标注带来的三类漂移
数据集后缀标着“已增强”,增强本身也是标注错误的高发来源。水平翻转是最容易处理的,cx会变成1 - cx,但w保持不变;Mosaic拼接是重灾区,四张图被重新拼到一张画布上后,每个子图的偏移量必须重新加回到归一化坐标里,少加一个偏移就会整块偏出图像范围;HSV色相扰动虽然不影响坐标,但对油污水渍这类颜色区分类别的任务影响很大,色相偏移过大时,油污的琥珀色特征会滑向水渍的浅灰区域,类别可分性被增强器自己揉掉了。
一个工作习惯值得固定下来:增强后的数据不仅要看图像,还要抽样把txt框画回图上逐张过目。人可以一眼看出框和油污边缘是否贴合,但机器只能从损失函数里间接感知。这个“画框回看”的步骤,比任何后续的网络结构改动都省时间。
3. 把VOC转成YOLO格式:转换脚本、增强后一致性校验与尺寸分布统计
3.1 一份可直接改用的VOC转YOLO脚本
在YOLO训练流程里,txt是最终喂给数据加载器的格式,xml只作为中间存储。从一个典型的数据集压缩包起步,把它整理成“images目录存图、labels目录存txt”的标准形态,需要一段可直接改用的脚本:
import os import glob import xml.etree.ElementTree as ET # 类别顺序必须与data.yaml中names保持一致 CLASSES = ["oil_stain", "water_stain"] def voc_to_yolo(xml_path, out_dir): tree = ET.parse(xml_path) root = tree.getroot() width = int(root.findtext("size/width")) height = int(root.findtext("size/height")) lines = [] for obj in root.iter("object"): name = obj.findtext("name") if name not in CLASSES: continue bbox = obj.find("bndbox") xmin = float(bbox.findtext("xmin")) ymin = float(bbox.findtext("ymin")) xmax = float(bbox.findtext("xmax")) ymax = float(bbox.findtext("ymax")) cx = (xmin + xmax) / 2.0 / width cy = (ymin + ymax) / 2.0 / height w = (xmax - xmin) / width h = (ymax - ymin) / height lines.append(f"{CLASSES.index(name)} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") base = os.path.basename(xml_path).replace(".xml", ".txt") with open(os.path.join(out_dir, base), "w") as f: f.write("\n".join(lines)) if __name__ == "__main__": os.makedirs("labels", exist_ok=True) for xml_file in glob.glob("annotations/*.xml"): voc_to_yolo(xml_file, "labels")脚本里有两段逻辑容易被忽略:CLASSES的顺序必须与训练时的类别定义完全一致,否则类别ID集体错位;w = (xmax - xmin) / width要求xmax、xmin都是像素坐标,如果原始xml里出现负数或超宽坐标,这里需要在转换前先过滤一次。转换完成后统计一下labels目录下的txt数量,数值应等于xml数量。
3.2 增强后标注与图片对齐的四项检查
“已增强”三个字意味着jpg和txt已经成对生成过,但增强管线里任何一个环节写错,图片和txt就会脱钩。最直接的校验方式是逐项检查四个维度:文件名一一对应、图片尺寸匹配、归一化坐标是否越界、类别ID是否都在合法范围。
下面是一段独立的检验脚本,运行成本低到可以每次使用数据集前都跑一遍:
import os from PIL import Image IMG_DIR, LAB_DIR = "images", "labels" bad = [] for img_name in sorted(os.listdir(IMG_DIR)): if not img_name.endswith(".jpg"): continue img = Image.open(os.path.join(IMG_DIR, img_name)) w, h = img.size txt_path = os.path.join(LAB_DIR, img_name.replace(".jpg", ".txt")) if not os.path.exists(txt_path): bad.append((img_name, "missing_txt")) continue for line in open(txt_path): parts = line.strip().split() if len(parts) != 5: bad.append((img_name, "line_format")) break cid, cx, cy, bw, bh = float(parts[0]), float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4]) if cid not in (0.0, 1.0): bad.append((img_name, f"unknown_class_{int(cid)}")) break if not (0 <= cx <= 1 and 0 <= cy <= 1 and 0 < bw <= 1 and 0 < bh <= 1): bad.append((img_name, "coord_out_of_range")) break print("total images:", len([x for x in os.listdir(IMG_DIR) if x.endswith('.jpg')])) print("problems:", bad[:20])第二项“图片尺寸匹配”值得单独强调:如果txt里的w和h是从1920×1080图片算出来的,而同一文件名对应的jpg被增强器缩成了1280×720,坐标虽然仍在0到1范围内,但框的位置整体偏移。用上面这段脚本无法捕捉这类错位,需要额外对比图片实际读出的宽高和标注生成记录里的宽高。批量数据整理时,我会把这一步放进pipeline的固定环节,防止它变成一次性手工检查。
3.3 用长宽比与面积分布理解小目标比例
油污水渍数据集里,小目标检测是最常见的失效模式。统计每个标注框的宽度和高度分布,可以提前知道模型会面对怎样的目标尺度。把这段统计做成文本输出:
import os import numpy as np areas, ratios = [], [] for txt_name in sorted(os.listdir("labels")): for line in open(os.path.join("labels", txt_name)): parts = line.strip().split() if len(parts) != 5: continue w, h = float(parts[3]), float(parts[4]) areas.append(w * h) if h > 0: ratios.append(w / h) areas = np.array(areas) ratios = np.array(ratios) print(f"样本数: {len(areas)}") print(f"面积占比: p50={np.median(areas):.4f}, p75={np.percentile(areas, 75):.4f}") print(f"宽高比: p50={np.median(ratios):.2f}, p90={np.percentile(ratios, 90):.2f}")如果面积占比的中位数低于0.01,意味着在640×640输入下,目标边长往往不足64像素,属于典型的小目标。油污的宽高比一般大于1.2,呈横向铺开;水渍则接近1,形态更圆整。这个差异可以用在NMS策略和anchor尺寸上,但更实用的地方在于验证集评估时,单独看小尺寸档位的AP,不要被整体mAP掩盖了漏检问题。
4. 用YOLOv8训练自己的数据集:目录组织、训练参数与损失曲线怎么读
4.1 按YOLOv8约定组织VOC+YOLO数据
把上一步生成的labels目录和原始images目录按YOLOv8的期望结构摆放:
ground_stain_dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── oil_water.yaml目录命名必须严格用train和val,图片和与它同名的txt必须放在对应的train或val子目录里。关于划分比例,2000张图的规模下,我一般按8:2切分,也就是1600张训练、400张验证。切分时按图片编号随机抽样,并保证两个类别的样本比例在训练集和验证集里都接近原始分布,避免某一类全落在验证集里。
oil_water.yaml配置如下:
path: ./ground_stain_dataset train: images/train val: images/val names: 0: oil_stain 1: water_stain这里有个容易踩的路径坑:path字段在YOLOv8里是相对或绝对路径的根,train和val再相对path去解析。如果把path写成绝对路径,换机器或换目录后必须同步更新。做实验的可复现性考虑,建议在项目目录里用一个固定的数据路径,data.yaml本身也纳入版本管理。
4.2 训练命令与需要优先调整的参数
YOLOv8训练命令通常长这样:
yolo detect train \ data=oil_water.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=15 \ seed=42 \ project=runs/detect \ name=stain_exp01几个参数对结果的影响优先级远高于其他项:
| 参数 | 建议值 | 为什么优先调它 |
|---|---|---|
| model | yolov8n.pt起步 | 小数据集先用轻量模型把pipeline跑通 |
| imgsz | 640或1280 | 油污水渍目标偏小,加大输入尺寸能显著提升小目标召回 |
| batch | 以显存上限为准 | batch过小时BN统计不稳定,损失曲线震荡幅度变大 |
| lr0 | 0.005到0.01 | 油污水渍的语义简单,学习率偏大容易在早期震荡 |
| patience | 10到15 | 验证集mAP连续多轮不升即停,省去无意义的等待轮次 |
在8GB显存级别,batch=16配合yolov8n能在640输入下稳定运行,如果改用1280输入,batch要相应降到8或4。训练前先设置seed=42固定随机种子,同一个数据集的两次训练结果才具备可对比性,这在调参数的过程中能省下大量自我怀疑的时间。
4.3 从box_loss、cls_loss和dfl_loss曲线判断数据问题
训练日志里有三条核心损失曲线:box_loss是边界框回归的损失,cls_loss是分类损失,dfl_loss是分布焦点损失。油污水渍这类边界模糊目标的典型表现是cls_loss降得很快,第一轮就从1.5跌到0.5以下,但box_loss一直缓慢下行。
| 曲线现象 | 常见数据原因 | 直接对策 |
|---|---|---|
| cls_loss快速下降后平台期 | 类别特征重叠,油污与水渍难以区分 | 回查标注边缘是否含大量反光区域 |
| box_loss长时间不平滑 | 标注框尺度不统一 | 重新整理外接矩形,统一标注口径 |
| 三条曲线在验证集上反弹 | 增强强度过猛,验证集分布被甩开 | 调低hsv_h、hsv_s扰动,或暂时关闭Mosaic |
| dfl_loss持续高 | 目标边缘楔形严重 | 加大imgsz,让边界细节更好地映射到特征图上 |
更重要的一点是:不要只看train loss,要同时看验证集loss。如果train loss稳步下降而val loss在某个epoch开始反弹,说明模型进入过拟合,patience参数会自动触发早停。打断训练后,优先怀疑的不是网络复杂度,而是训练集里有效样本的多样性不足——2000张图里如果大量是同一地砖场景,模型很快就把材质纹理记住了,而不是学会区分油污水渍本身。
4.4 增强配置对地面场景的特殊处理
YOLOv8默认开启Mosaic和HSV增强。Mosaic在这个任务上需要谨慎:四张不同材质的地面拼在一起,油污边界被切碎的概率很高,模型学到的是“半块油污也能算正样本”。一种常见的做法是训练后期把Mosaic关闭,让模型适应完整目标;另外,把hsv_h从默认0.015调整为0.01,hsv_v从0.4降到0.3,因为油污水渍在光度变化剧烈时特征漂移明显,过大的光度扰动会让网络把阴影误判成污渍。
还有一个需要明确的边界:YOLO系列输出的检测框天然是矩形,油污水渍的形状再不规则,后处理阶段得到的也是外接矩形。数据集里如果放了不规则多边形的标注,要么转成外接矩形,要么换用yolov8-seg做实例分割——但这份数据既然以VOC+YOLO方形框为标准,就按矩形检测的逻辑走,不要在图省事的前提下追求自由形状的拟合。
5. 部署阶段的调参技巧:置信度阈值校准与形态学后处理
5.1 用验证集PR曲线定置信度阈值,而不是拍脑袋
推理时直接套用0.25的默认conf值,往往会让地面巡检模型误检频发。建议训练完成后单独跑一遍验证集,输出PR曲线数据:
yolo detect val \ model=runs/detect/stain_exp01/weights/best.pt \ data=oil_water.yaml \ conf=0.001 \ plot=False不需要只依赖YOLO终端打印的mAP,也顺便读一下results.csv里的precise和recall对应行,找到precision超过0.85时对应的置信度阈值。然后把该值写进部署端的conf参数。工业现场对误检的容忍度通常远低于漏检,宁可漏掉少量水渍,也不能让系统三天两头把地砖反光当成油污报工单。
5.2 形态学闭运算合并被拆成两块的油污框
油污面积大且边界模糊时,模型常常把它检测成两个相邻框,NMS又因为IoU不够高而没合并。在推理输出后加一段形态学处理,把二值化的检测掩膜做闭运算,再重新生成外框:
import cv2 import numpy as np boxes = [(312, 520, 640, 870), (650, 545, 1140, 900)] # 假设检测输出 mask = np.zeros((1080, 1920), dtype=np.uint8) for x1, y1, x2, y2 in boxes: mask[y1:y2, x1:x2] = 255 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (40, 40)) merged = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) cnts, _ = cv2.findContours(merged, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) final_boxes = [cv2.boundingRect(c) for c in cnts if cv2.contourArea(c) > 2000] print(final_boxes)结构元尺寸不能用固定值。分辨率越高,同样大小的油污在像素上占得越多,结构元尺寸应与输入图像宽度挂钩,1080p下用40×40,720p下缩到24×24,这样合并行为在不同摄像头之间保持一致。这一步纯粹是后处理,不要把它反向加到训练集里,训练时该用原始框就用原始框。核心思路只有一个:让阈值和合并策略都服从于现场视频流的分辨率与误报率要求,而不是依赖默认配置让模型自由发挥。
本文还有配套的精品资源,点击获取