简介:本数据集面向计算机视觉与深度学习方向的开发者、学生及算法工程师,聚焦道路积水识别这一具体场景,可用于目标检测模型的训练、验证与调优。资源采用Pascal VOC与YOLO双格式标注,包含2699张jpg图片,并配有等量的xml与txt标注文件,标注类别仅water一类,共3777个矩形框,全部由labelImg工具完成,标注规则统一规范。压缩包为7z格式,内含约2000个文件,以xml标注文件为主,另附一份使用前必读说明,整体约76.78MB,体积轻便便于快速下载与本地部署。目前已有1075人学习下载,适合需要快速构建积水检测基线、验证YOLO或VOC流程的中高级读者参考使用。需要说明的是,该数据集不对训练所得模型或权重文件的精度作任何保证,仅提供准确且合理的标注内容,读者可据此自行评估与调优。
1. 道路积水检测数据集 VOC+YOLO 格式 2699 张 1 类别:这份数据到底能干什么
城市内涝预警、自动驾驶感知降级、园区无人车路径规划,这几个场景背后都绕不开同一个基础问题:路面到底有没有积水、积水在画面里占多大区域。你手上如果只有一份「道路积水检测数据集 VOC+YOLO 格式 2699 张 1 类别.7z」,第一反应大概率是——2699 张、单类别,够不够训一个能用的模型?我的判断是:够跑通一条完整的检测链路,也够做小场景的可行性验证,但别指望它直接上车规级量产。
这份数据集的价值在于「省掉标注」这一步。道路积水这类目标,边界模糊、反光、和湿滑路面高度相似,人工画框成本极高,2699 张已经能覆盖白天、阴天、部分夜间反光等常见工况。它同时给了 VOC 和 YOLO 两套格式,意味着你既可以用 Pascal VOC 那套 XML 走传统检测框架,也可以直接喂给 YOLO 系列做端到端训练。适合谁?做智慧交通、智慧园区、自动驾驶数据闭环的算法同学,以及想拿一个真实长尾场景练手 YOLO 微调的人。下面我按「先看懂标注、再跑通训练、最后避坑」的顺序讲透。
2. 拆开压缩包先看什么:VOC 与 YOLO 双格式的对应关系
拿到.7z别急着解压完就开训,先花十分钟把目录结构和标注格式对齐,这一步省下来的时间后面会加倍还给你。道路积水检测数据集的核心信息全在标注文件里,格式理解错了,训练 loss 会以一种很玄学的方式不下降。
2.1 VOC 的 XML 与 YOLO 的 txt 到底差在哪
Pascal VOC 格式用Annotations/*.xml存标注,每个目标一个<object>节点,里面是name(类别名)和bndbox(xmin/ymin/xmax/ymax,绝对像素坐标,左上角为原点)。YOLO 格式用labels/*.txt,每行一个目标,格式是class_id x_center y_center width height,四个值都是相对整图宽高的归一化值,范围 0~1。
两者可以无损互转,但有两个容易翻车的点:一是 VOC 的坐标是包含边界的整数像素,转 YOLO 时中心点和宽高要除以图像宽高,浮点精度别截断太狠;二是类别名到class_id的映射必须固定,单类别场景下name通常是water或ponding之类,映射成0就行,但一定要确认所有 XML 里的name拼写完全一致,大小写、空格都算。
| 维度 | VOC (XML) | YOLO (txt) |
|---|---|---|
| 坐标类型 | 绝对像素 | 归一化 0~1 |
| 坐标含义 | xmin,ymin,xmax,ymax | x_center,y_center,w,h |
| 类别表示 | 字符串 name | 整数 class_id |
| 一图多目标 | 多个 object 节点 | 多行 |
| 常用框架 | Faster R-CNN、SSD | YOLOv5/v8/v11 |
2.2 用脚本核对图像与标注是否一一对应
解压后第一件事不是训练,是核对。图像数量和标注数量对不上、有图无标注、有标注无图,这三种情况在二手数据集里非常常见。下面这段脚本把 VOC 和 YOLO 两套标注都扫一遍,输出缺失清单。
import os import glob import xml.etree.ElementTree as ET img_dir = "images" # 图像目录 voc_dir = "Annotations" # VOC xml 目录 yolo_dir = "labels" # YOLO txt 目录 imgs = {os.path.splitext(os.path.basename(p))[0] for p in glob.glob(f"{img_dir}/*")} xmls = {os.path.splitext(os.path.basename(p))[0] for p in glob.glob(f"{voc_dir}/*.xml")} txts = {os.path.splitext(os.path.basename(p))[0] for p in glob.glob(f"{yolo_dir}/*.txt")} print("图像数:", len(imgs), "VOC数:", len(xmls), "YOLO数:", len(txts)) print("有图无VOC:", sorted(imgs - xmls)[:10]) print("有VOC无图:", sorted(xmls - imgs)[:10]) print("有图无YOLO:", sorted(imgs - txts)[:10]) # 抽查一个 XML 的类别名和框 sample = sorted(xmls)[0] tree = ET.parse(f"{voc_dir}/{sample}.xml") for obj in tree.getroot().iter("object"): name = obj.find("name").text box = obj.find("bndbox") print(name, [box.find(k).text for k in ("xmin","ymin","xmax","ymax")])逻辑说明:用集合差集找出三类不匹配,比逐个os.path.exists快得多。参数上img_dir要按你解压后的真实目录名改,有的包图像放在JPEGImages,有的直接叫images。抽查那段是为了确认类别名拼写统一,如果打印出water和Water两种,后面训练会多出一个类别,必须提前统一。
提示:单类别数据集最怕的就是「隐性多类别」。跑一遍
grep -h "<name>" Annotations/*.xml | sort | uniq -c,输出只有一行才算干净。
2.3 把 VOC 转成 YOLO 的转换脚本与边界处理
如果压缩包里 YOLO 格式不全,或者你想自己重转一遍保证一致性,用下面这段。它同时处理了坐标越界和空标注两个边界情况。
import os, glob import xml.etree.ElementTree as ET from PIL import Image voc_dir, out_dir, img_dir = "Annotations", "labels", "images" os.makedirs(out_dir, exist_ok=True) classes = ["water"] # 单类别,顺序即 class_id for xml_path in glob.glob(f"{voc_dir}/*.xml"): stem = os.path.splitext(os.path.basename(xml_path))[0] img_path = None for ext in (".jpg", ".png", ".jpeg"): if os.path.exists(f"{img_dir}/{stem}{ext}"): img_path = f"{img_dir}/{stem}{ext}"; break if img_path is None: print("跳过无图:", stem); continue w, h = Image.open(img_path).size lines = [] for obj in ET.parse(xml_path).getroot().iter("object"): name = obj.find("name").text.strip() if name not in classes: continue b = obj.find("bndbox") x1, y1 = float(b.find("xmin").text), float(b.find("ymin").text) x2, y2 = float(b.find("xmax").text), float(b.find("ymax").text) # 裁剪到图像范围内,防止越界产生负值 x1, y1 = max(0, x1), max(0, y1) x2, y2 = min(w, x2), min(h, y2) if x2 <= x1 or y2 <= y1: continue cx, cy = (x1+x2)/2/w, (y1+y2)/2/h bw, bh = (x2-x1)/w, (y2-y1)/h lines.append(f"{classes.index(name)} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") with open(f"{out_dir}/{stem}.txt", "w") as f: f.write("\n".join(lines))逻辑说明:classes列表顺序决定class_id,单类别就是0。坐标裁剪那两行是关键,VOC 标注偶尔会超出图像边界,不裁剪会得到负的宽高,YOLO 训练时直接报NaN。归一化保留 6 位小数足够,再高没意义还占空间。空标注文件也照常写出(内容为空),YOLO 训练时会把纯背景图当负样本用,这对积水检测反而有帮助,能压低误检。
3. 用 YOLOv8 跑通第一轮训练:环境、配置与参数
格式理清之后,进入训练环节。这里以 YOLOv8 为例,因为它对单类别小数据集最友好,pip装完就能跑,不需要编译。热词里常出现的「yolov8训练自己的数据集」「yolo入门」说的就是这条路径。
3.1 环境安装与目录组织
先建虚拟环境,别在系统 Python 里装,否则后面升级版本会互相污染。
conda create -n ponding python=3.10 -y conda activate ponding pip install ultralytics opencv-python pillow # 验证 yolo checksyolo checks会打印 PyTorch、CUDA 是否可用。如果你有 NVIDIA 显卡,确认CUDA那行是可用状态;没有显卡就用 CPU,2699 张单类别在 CPU 上也能跑,只是慢。目录按 YOLO 官方约定组织:
ponding_dataset/ ├── images/ │ ├── train/ # 约 2160 张 │ └── val/ # 约 539 张 ├── labels/ │ ├── train/ │ └── val/ └── data.yaml按 8:2 切分,单类别数据集验证集别少于 400 张,否则 mAP 波动大,看不出真实效果。切分脚本用random.seed(42)固定随机种子,保证每次复现一致。
3.2 data.yaml 与训练命令
data.yaml是 YOLO 的数据入口,写错路径是最常见的翻车点。
path: /abs/path/ponding_dataset # 必须绝对路径 train: images/train val: images/val nc: 1 names: ["water"]path用绝对路径,相对路径在不同工作目录下会解析失败。nc是类别数,单类别写 1,names长度必须等于nc。然后启动训练:
yolo detect train \ model=yolov8n.pt \ data=/abs/path/ponding_dataset/data.yaml \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=20 \ project=runs/ponding \ name=exp1参数说明:model=yolov8n.pt用 nano 版,单类别 2699 张足够,换s或m提升有限但显存翻倍。imgsz=640是默认值,积水区域通常较大,640 够用;如果小面积积水多,可以提到 960,但 batch 要相应降到 8。lr0=0.01是初始学习率,微调预训练权重时这个值合适,从零训要降到 0.001。patience=20表示 20 轮没提升就早停,防止过拟合。
3.3 训练过程看什么指标
训练日志里重点盯三个:box_loss、cls_loss、mAP50。单类别场景cls_loss会很快降到接近 0,因为它只需要区分「积水」和「背景」。真正反映检测质量的是mAP50和mAP50-95。如果box_loss持续震荡不降,多半是标注框有问题,回到第 2 章核对;如果mAP50卡在 0.5 以下,先看验证集里是不是有大量反光误检。
注意:积水检测的难点不在「找得到」,而在「框得准」。水面边界模糊,标注本身就有主观性,mAP50-95 通常比常规目标低 10~20 个点,这是任务特性,不是模型不行。
4. 道路积水检测的避坑与排查:5 个血泪教训
这一章是我自己踩过的坑,按「现象 → 原因 → 解决」写,你对照着排查能省不少时间。
4.1 训练 loss 变 NaN
现象:训练几轮后box_loss突然变成nan,进程不报错但模型废了。 原因:VOC 转 YOLO 时坐标越界,产生了负的宽高,或者归一化时除零(图像尺寸读取失败)。 解决:回到 2.3 的转换脚本,确认裁剪逻辑生效;再检查是否有 0 字节图像文件,用find images -size 0找出来删掉。
4.2 验证集 mAP 虚高但实拍全漏
现象:验证集mAP50到 0.9,拿手机拍一段路面积水视频,模型几乎不响应。 原因:数据集里积水场景和验证集同分布,但实拍的光照、角度、水面反光差异大,模型过拟合到了数据集的成像风格。 解决:从实拍视频里抽 50~100 帧,人工标一部分加进训练集做增量微调;同时开强数据增强,hsv_h=0.015、hsv_s=0.7、hsv_v=0.4,让模型对颜色和亮度不敏感。
4.3 把湿滑路面误检成积水
现象:干燥但有反光的柏油路被框成积水,误检率高。 原因:单类别数据集缺少「负样本」,模型没见过「反光但非积水」的困难样本。 解决:收集一批湿滑路面、金属反光、玻璃反光的图像,不标任何框,作为背景图加入训练集。YOLO 会把无标注图当负样本,显著压低这类误检。
4.4 显存溢出 OOM
现象:batch=16时报CUDA out of memory。 原因:imgsz或batch超过显卡容量,或workers开太多导致内存碎片。 解决:先把batch降到 8,再不行降imgsz到 512;workers设成 CPU 核数的一半。用nvidia-smi实时看显存占用,别盲目调大。
4.5 类别名不一致导致多类别
现象:训练日志显示nc=2或类别数对不上。 原因:XML 里name有water和Water两种写法,转换时被当成两个类。 解决:转换前统一name.strip().lower(),或者用 2.2 的uniq -c命令先查一遍。单类别数据集出现多类别,几乎都是这个原因。
5. 从能跑到好用:提升积水检测精度的三个进阶技巧
跑通训练只是起点,真正决定这份数据集值不值得投入的,是你能不能把它调到可用。下面三个技巧是我反复验证过的,按投入产出比排序。
第一个是难例挖掘闭环。第一轮训练完,用模型跑一遍验证集和实拍视频,把置信度在 0.3~0.5 之间的预测框导出来,人工复核。这些「模型犹豫」的样本信息量最大,挑 100~200 张补标后加入训练集,第二轮 mAP 通常能涨 5~10 个点。导出预测框用yolo detect predict加save_txt=True,再用脚本按置信度过滤。
第二个是多尺度训练。积水区域大小差异大,固定imgsz会让小面积积水吃亏。训练时开multi_scale=True,让输入在 0.5~1.5 倍之间随机缩放,模型对不同尺度的积水都更鲁棒。代价是训练慢 20% 左右,但泛化提升明显。
第三个是导出 ONNX 做部署验证。训练完别只看 mAP,导出 ONNX 跑一遍推理速度,确认在目标硬件上能实时。
yolo export model=runs/ponding/exp1/weights/best.pt format=onnx imgsz=640导出后用onnxruntime加载,测单帧推理耗时。如果超过 50ms,考虑换yolov8n或量化到 INT8。这一步能提前暴露部署问题,别等上线才发现模型跑不动。
| 技巧 | 预期收益 | 额外成本 |
|---|---|---|
| 难例挖掘 | mAP +5~10 | 人工复核 1~2 天 |
| 多尺度训练 | 小目标召回 + | 训练时间 +20% |
| ONNX 导出验证 | 提前发现部署瓶颈 | 半天 |
最后说个习惯:我每次训完模型,都会把best.pt、data.yaml、训练命令和当次 mAP 记在一个experiment_log.md里。积水检测这种任务,换个光照条件效果就可能变,没有记录你根本不知道哪次改动起了作用。这份 2699 张的数据集不大,但足够你把它当成一个长期迭代的基线,慢慢往里加实拍难例,比一次性追求大而全靠谱得多。希望帮到你。
本文还有配套的精品资源,点击获取