
简介面向道路积水检测与智能交通视觉任务提供一套Pascal VOC与YOLO双格式标注数据集。数据集共包含两千六百九十九张道路积水场景图片每张图片均对应VOC格式XML标注与YOLO格式TXT标注检测类别统一为water矩形框总数为三千七百七十七个标注工作由labelImg完成画框规则清晰一致可直接用于YOLOv5、YOLOv8等主流目标检测模型的训练与验证免去自行采集和格式转换。压缩包整体约七十六点七八兆字节内含两千个文件文件类型以XML标注文件与TXT说明为主便于按目录快速筛选与使用。目前学习浏览人数已有一千零六十八人。数据集不对模型精度作任何保证但标注相对准确合理适合需要标准VOC/YOLO格式积水样本的开发者、研究者可直接投入实验与算法验证。1. 道路积水检测的样本需求2699张单类图够训练到什么程度道路积水检测在市政内涝预警与低洼路段巡检里一直是刚需但真正可用的标注数据很难找。监控画面中沥青路面被雨水打湿后的反光特性和真正积水区域极为相似传统图像规则会把整片湿润路面误判成目标。想训练一个专门的道路积水检测模型最缺的不是网络结构而是带框标注的样本——道路积水检测数据集VOCYOLO格式2699张1类别.7z这类资源的价值就在这2699张、单类别积水同时给VOC与YOLO两套标准解压后能直接进YOLO训练流程省掉自采数据、人工标注、格式转换三件事。适合市政积水监测验证、YOLO训练实践和迁移学习基线搭建。2. VOC与YOLO两套格式并存目录、XML/TXT坐标体系与互相转换标题里“VOCYOLO格式”不是冗余卖点。同一批图片两套标注描述的是完全不同的坐标体系VOC用像素绝对坐标写进XMLYOLO用归一化中心点坐标写进TXT。新手在把XML转成TXT时最容易算错的就是边界框换算一旦算错模型训练全程不报错但mAP会一直挂在低水平。所以这一段先讲两种格式的构成再给出一个我自己常用的转换脚本和对应的三个边界坑。2.1 VOC格式的XML字段一套资料在目录上可能怎么摆VOC格式源自PASCAL VOC目标检测挑战赛经典布局是JPEGImages放图片、Annotations放XML、ImageSets/Main放train/val列表。不过实际发布到网络上的数据集不会百分之百按原版结构来有些作者会把图片平铺到images目录XML单独放annotations目录ImageSets直接省掉。拿到压缩包先列目录比照着原版VOC规范猜路径更靠谱cd 道路积水检测数据集 find . -maxdepth 2 -type d | head -20如果图片和标注分处两个目录后面组织YOLO格式时也要跟着对齐。打开任意一个XML结构基本是annotation folderJPEGImages/folder filenamewater_001.jpg/filename size width1920/width height1080/height depth3/depth /size object nameroad_water/name bndbox xmin412/xmin ymin355/ymin xmax889/xmax ymax641/ymax /bndbox /object /annotation字段说明filename只代表这张图在源工程里的命名不代表训练时实际读取路径最终路径靠遍历图片目录拼出来size里的width/height是坐标换算的基准一旦图片被缩放XML里的框全部失效object每出现一次代表一个目标name是类别名bndbox四个值是像素角点有些标注工具会给出xmin大于xmax的反向框转换脚本里要兜底处理在VOC世界里坐标是绝对像素换分辨率就失效。这也是为什么各训练框架都不直接接受XML进训练而要先把XML转成归一化TXT。2.2 YOLO格式的TXT标注一行的五个数字藏着中心点语义YOLO系列训练时读的是每张图一个同名txt内容形如0 0.3388 0.4620 0.2484 0.2648这五个数字的含义分别是第一个是类别索引单类数据集永远是0对应data.yaml里names列表的第0号元素第二、三个数字是边界框中心的x和y除以图片宽高做了归一化取值范围0到1第四、五个数字是框的宽和高同样归一化不需要再补分母记忆顺序是中心点加宽高不是左上角加宽高这条跟VOC的bndbox差异最容易被忽略举个例子一张1920x1080的图VOC给出xmin412、ymin355、xmax889、ymax641。换算过程框宽477框高286中心x约等于650.5除以1920得到0.3388中心y约等于498除以1080得到0.4620归一化宽高是0.2484和0.2648。整个换算看着简单但常见的翻车点是除法用错图尺寸或者直接把xmin当成了中心点。2.3 自己写VOC转YOLO脚本坐标换算与三个边界坑即使压缩包里已经提供了YOLO txt我自己拿到手也会核对一遍因为作者在转格式时可能做过类别合并。我常用的转换脚本如下import xml.etree.ElementTree as ET from pathlib import Path def voc_to_yolo(xml_path: Path, class_names: list[str]) - list[str]: tree ET.parse(xml_path) root tree.getroot() # 转换时必须以XML声明的图长宽为基准不能拿后期resize过的尺寸套 img_w int(root.find(./size/width).text) img_h int(root.find(./size/height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_names: # 如果XML里有多个类别只保留需要训练的目标类 continue cls_id class_names.index(name) xmin float(obj.find(bndbox/xmin).text) ymin float(obj.find(bndbox/ymin).text) xmax float(obj.find(bndbox/xmax).text) ymax float(obj.find(bndbox/ymax).text) # YOLO要求中心点加宽高全部归一化 box_w xmax - xmin box_h ymax - ymin cx (xmin box_w / 2.0) / img_w cy (ymin box_h / 2.0) / img_h nw box_w / img_w nh box_h / img_h # 对越界坐标截断避免训练崩溃但真实越界数量要单独统计 cx max(0.0, min(1.0, cx)) cy max(0.0, min(1.0, cy)) nw max(0.0, min(1.0, nw)) nh max(0.0, min(1.0, nh)) lines.append(f{cls_id} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}) return lines参数说明class_names传列表单类就写[road_water]多个类别时顺序必须和训练时data.yaml里names的顺序一致否则类别索引整体错位bndbox里的数据不一定都是整数用float解析更稳越界截断只是兜底它会掩盖原始标注问题建议在截断前把异常行单独写到日志最后统计一个越界率超过5%就该回头修XML坑一图片被resize过却仍拿旧XML的size做转换。如果数据集的原始图分辨率不是训练分辨率转标注前应先按目标分辨率重新换算一遍坐标否则训练增强裁剪后框和真实积水位置对不上。坑二类别索引从1开始。单类永远是0但把VOC多类合并成1类时很容易把原类别编号1直接写进TXT。这样训练不报错混淆矩阵里永远看不到第0类有样本。坑三文件名大小写冲突。Windows下Image_001和image_001可能被当成同一个文件解压到Linux后其中一个被覆盖导致图片总数对不上标注总数。核对时如果发现缺了几十张图先按大小写不敏感去重查一遍。3. 从7z压缩包到可训练工程解压校验与数据集拆分数据以.7z格式打包好处是压缩率高坏处是Linux默认不带解压工具。很多人在这一步卡住不是数据集有问题而是7z命令用错了。解压之后的校验和拆分比解压本身更重要。3.1 Linux解压7z文件p7zip安装、查看与完整性校验Ubuntu或Debian上第一步是安装p7zip-fullsudo apt-get update sudo apt-get install -y p7zip-full # 列出压缩包内容不解压先看数据结构 7z l 道路积水检测数据集VOCYOLO格式2699张1类别.7z | tail -20 # 解压到指定目录注意-o后面不能有空格 mkdir -p /data/road_water 7z x 道路积水检测数据集VOCYOLO格式2699张1类别.7z -o/data/road_water -y # 校验解压结果 find /data/road_water -name *.jpg | wc -l find /data/road_water -name *.txt | wc -l命令背后的逻辑7z l只列出压缩包内部文件清单不解压能提前看到顶层是否有嵌套目录避免写data.yaml路径时多写一层-o输出目录后面必须紧跟着路径写成-o /data会解压到当前目录并生成一个误命名文件夹这是最常见的7z误用图片格式不一定是jpg也可能是png或jpegfind的-name通配符要轮着查不能因为标题写2699张就认定全是jpg解压完成后不要只看文件数对上就开训。建议用下面这个Python脚本做图像与标注的配对核对这条流程每次拿新数据集我都会跑一遍。提示解压过的压缩包先别删。训练期间如果发现缺少文件或日志异常还能回到压缩包重新验原始数据完整性。3.2 图片与标注配对核对缺失、空标注与重名覆盖不管作者把目录命名成imageslabels还是JPEGImagesAnnotations核对逻辑都一样拿所有图片的文件名主名去对标注文件名主名。以最常见的平铺目录为例from pathlib import Path from collections import Counter root Path(/data/road_water) # 假设YOLO格式这里一层的目录images/ 和 labels/ img_files {p.stem: p for p in root.glob(images/*.jpg)} label_files {p.stem: p for p in root.glob(labels/*.txt)} missing_label [s for s in img_files if s not in label_files] missing_img [s for s in label_files if s not in img_files] print(f图片总数: {len(img_files)}, 标注总数: {len(label_files)}) print(f缺少标注: {len(missing_label)}) print(f缺少图片: {len(missing_img)}) # 统计空标注文件也就是该图没有任何积水框 empty [s for s in label_files if label_files[s].stat().st_size 0] print(f空标注文件数: {len(empty)}) # 统计每张图画了多少个框排查脏数据 line_count Counter() for s, p in label_files.items(): line_count[sum(1 for _ in open(p))] 1 print(单图框数分布:, line_count.most_common())脚本里三个关键动作glob时images/*.jpg只匹配一层如果作者把图片按train和val分了两层这里就需要改成rglob或者分别统计空标注文件不是错误它代表该图没有积水区域这类负样本对抑制背景误检有正向作用但如果占比超过10%说明数据里大量无积水画面训练时要在data.yaml的val里单独留一个负样本目录做参考单图框数分布能暴露脏数据比如某张图被标了20多个框大概率是标注员把倒影拆了很多小块这类标注会干扰模型对小目标的判断配对这个动作不是可选项。2699张图若出现几十张缺失标注训练时loss曲线看不出异常但验证集mAP会上下抖动容易被误判成训练随机性。3.3 按8:2重写train/val拆分并统计类别分布YOLOv8以后的数据集不再强制要求train.txt/val.txt列表文件只要data.yaml指向images/train和images/val两个目录即可。但很多二手项目还在沿用YOLOv5的列表方式稳妥做法是直接整理成标准目录结构mkdir -p /data/road_water/images/{train,val} mkdir -p /data/road_water/labels/{train,val}用Python做一次80/20拆分import random import shutil from pathlib import Path root Path(/data/road_water) src_img root / images src_lbl root / labels random.seed(42) # 固定随机种子保证实验可复现 stems [p.stem for p in src_img.glob(*.jpg)] random.shuffle(stems) split int(len(stems) * 0.8) val_stems set(stems[split:]) for stem in stems: img_src src_img / f{stem}.jpg lbl_src src_lbl / f{stem}.txt if stem in val_stems: shutil.move(str(img_src), str(root / images/val / img_src.name)) if lbl_src.exists(): shutil.move(str(lbl_src), str(root / labels/val / lbl_src.name)) else: shutil.move(str(img_src), str(root / images/train / img_src.name)) if lbl_src.exists(): shutil.move(str(lbl_src), str(root / labels/train / lbl_src.name))参数说明random.seed(42)固定拆分调参时实验之间拆分不一致mAP对比就没有意义如果原压缩包里带了ImageSets/Main/val.txt用作者给的拆分比随机拆分更可靠因为原始划分通常会避免同一路段相邻帧同时进train和val防止数据泄漏标注文件移动前必须做exists判断空标注文件也要一并移动否则训练时图片在但标签缺失会直接跳过该图针对道路积水这种场地强相关的数据我还会额外检查train和val里是否出现了场景近似。比如两张图片文件名相近、拍摄时间相差不到一秒如果被拆到两个集合模型在验证集上看到的画面和训练集几乎是同一帧mAP会虚高换到真实监控视频立刻打回原形。最后做一次类别分布确认wc -l /data/road_water/labels/train/*.txt | tail -1 head -5 /data/road_water/labels/train/water_001.txthead看到的每一行五个数字都必须落在合法范围重点看是否出现负数或大于1的值。4. 用YOLOv8在2699张积水图上跑通首个训练配置、命令与曲线读法2739张单类数据规模不大但也不小关键在于训练配置是否收敛。YOLOv8是当前最容易跑起来的版本下面按我一贯的流程展开。4.1 写data.yaml路径、类别名与验证集指向YOLOv8训练时读的配置文件只需要几个字段path: /data/road_water train: images/train val: images/val nc: 1 names: 0: road_water有两个地方容易出错path写成绝对路径后train/val尽量写相对path的子目录这样整个项目换机器不用改yamlnames里的键必须从0开始即使只有一个类别也必须写“0: road_water”不要写“1: road_water”YOLO内部索引是从0开始的这里额外提一句预训练权重。yolov8s.pt这个文件发布在Ultralytics官方仓库首次运行train命令时会自动下载到当前目录的权重缓存里。网络不通时也可以手动把pt文件放到指定目录否则模型会从随机权重开始训练2699张图从头train基本不会收敛。4.2 train命令与关键超参数batch、imgsz、epochs怎么调第一次训练我推荐的配置是yolov8s而不是更轻的n原因很直接积水区域和路面积水反光在灰度特征上高度重合模型容量太小学不到区分度yolo train \ data/data/road_water/road_water.yaml \ modelyolov8s.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.001 \ patience20 \ cacheTrue参数取舍逻辑modelyolov8s.pt表示使用small规模预训练权重检测头会在训练时自动按类别数重初始化骨干部分使用COCO预训练参数这是小数据集的命根子imgsz640是比较平衡的输入尺寸积水区域往往占画面比例较高不需要强行用1280提升小目标精度而且1280会让显存占用翻倍batch16对应单张V100或3090级别的显存跑之前用nvidia-smi确认显存不够就把batch降到8lr00.001比官方默认值0.01低一个数量级。这是小数据集最重要的调整之一学习率过大的直接后果是BN层统计量漂移loss前期正常后期突然变成NaNpatience20提前停止验证集mAP连续20个epoch不刷新就结束单类小数据训练耗时短早停能省下不少算力如果只有RTX 3060或4060这类12G显存显卡batch8、imgsz640也可以跑。batch不能再低于4否则BN崩溃概率直线上升。我自己在积水数据集上踩过batch2直接训到NaN的坑后面在第5章细说。4.3 训练曲线读法BN崩溃识别与第一轮迭代重点训练开始后第一件事不是看mAP而是盯loss曲线。YOLOv8的loss由三部分组成box_loss衡量框回归误差、cls_loss衡量分类误差、dfl_loss衡量框分布损失。单类积水数据集的cls_loss应该在第一个epoch就降到1.0以下如果一直停在2到3之间缓慢下降说明负样本占比过高模型在花大量容量学背景抑制。BN崩溃是这一阶段最容易误判成数据集问题的坑。现象是前5到10个epoch完全正常某个epoch开始loss出现NaN或骤升之后永远回不来。原因主要是batch太小加lr0偏高BN层在少量样本上统计方差过大。处理顺序先降lr0到0.0001重启训练观察前3个epoch若还崩把batch提到8以上若还不行在train命令里加augmentFalse关掉mosaic排除增强过头导致的数值不稳定实在不行换yolov8n先跑通一版再在n的基座上加s的检测头做蒸馏这里有个容易被忽略的玄学点道路积水图片经常有大面积暗色区域和过曝反光区mosaic增强会把这四类极端画面拼到一起BN统计极容易被打崩。所以积水检测这类强光照变化的场景关掉mosaic反而更稳。训练结束后去看runs/detect/train目录下的confusion_matrix.png。如果发现混淆矩阵的总和不是1先别慌。YOLO的混淆矩阵在绘制时对每一行做了行方向归一化按列加总当然不等于1背景类还可能不参与计算。要判断类别好不好分直接看precision、recall、F1三个指标比纠结矩阵归一化方式有意义得多。5. 常见问题排查五处从解压到训练最容易翻车的点这个数据集从拿到解压到训练出结果踩坑点集中在5个地方每条都是现象、原因、解决三个维度。5.1 7z解压提示密码正确但一直报错现象输入了发布方给的密码7z没有立刻提示错误但跑到某个文件时报CRC校验失败或Data Error解出来的文件不完整。原因这种在Windows上用中文版压缩软件打的7z包文件名编码通常是cp936Linux默认按UTF-8解析中文文件名和特殊字符路径就会解压失败。少数情况是压缩包在传输过程中损坏尤其是网盘断点续传之后。解决先用7z l看压缩包内部文件名是否能正常显示乱码就用7z e把所有文件提取到临时目录再用convmv转编码如果是具体文件CRC错误用7z t做一次完整测试把损坏的文件名记下来重新下载或联系作者补包。千万不要在报错后直接7z x加-y强制覆盖继续那可能把后续文件也弄脏。5.2 YOLO txt坐标越界框的值不在0到1之间现象训练报错或验证集mAP极低打开可视化结果发现边界框落在画面外。原因VOC转YOLO时使用了XML里的size字段但该XML的宽高和实际图片不一致比如手机照片有EXIF旋转信息width和height被互换或者XML里本身就存在负坐标标注员拉框时拖出了图片边界。解决训练前扫一遍所有TXTfrom pathlib import Path bad [] for lbl in Path(/data/road_water/labels).rglob(*.txt): for line in lbl.read_text().strip().splitlines(): parts list(map(float, line.split())) if len(parts) ! 5: bad.append((str(lbl), 列数不对, line)) continue if any(v 0 or v 1 for v in parts[1:]): bad.append((str(lbl), 坐标越界, line)) print(问题文件数:, len(bad)) for item in bad[:20]: print(item)扫出来之后按2.3节的脚本重新转换一份转换时记录越界行数。单条越界丢弃即可大范围越界说明XML源数据有问题需要回到原始标注修正。5.3 BN崩溃loss突然变成NaN而且永远回不来现象训练前多个epoch正常突然一个epoch开始loss变NaN或在最开始就NaN几乎无法启动。原因batch太小导致BN统计量只依赖一两张图加上lr0偏高梯度一步跨出稳定区道路积水图本身有大面积暗色和过曝高光增强后数值分布极端也会加剧BN漂移。解决降lr0到0.0001、batch提到8以上、关闭mosaic三条按顺序试。如果这都不行检查是否存在全黑或全白图片删除后重新训练。实践经验是积水数据集的BN崩溃大多数发生在分辨率为1280且batch等于4的情况下所以首次训练不推荐盲目上大分辨率。5.4 混淆矩阵总和不是1这是归一化方式不是bug现象训练结束看runs/detect/train目录里的confusion_matrix.png按下标加总各行列发现总和不是1怀疑训练有错。原因YOLO画混淆矩阵时对每行做了行方向归一化真值视角的加总和与列方向不一致背景行通常表示了未命中背景类样本的比例也可能让加总偏离1。不同YOLO版本对背景占位的处理方式并不统一。解决不要在这个图上纠结归一化。要看类别可分性直接看验证集最后的mAP50、mAP50-95、precision、recall。单类积水检测的重点是召回率即真实积水区域有多少被框出来了而不是矩阵对角线是否完美。5.5 mAP高但可视化误检多积水和湿路面长得像现象验证集mAP达到0.9以上但拿雨后实拍监控画面去测试几乎每一帧都把大片湿润反光误判成积水。原因发布数据集时对积水的标注口径可能只框了水体面积较大的区域模型学到的语义偏向于面积大、反光强而实际监控中面积较小的水洼和整片湿路面反光也很像。这不是过拟合验证集而是标注口径和应用口径不一致。解决先把置信度阈值调到0.5以上mAP虚高通常是在低置信度下把易分样本计入了真阳。然后收集几十张负样本如湿路面、雨天无积水、倒影明显的画面单独推理统计误报率。如果误报仍然高回到训练阶段给负样本加小权重微调相应调高cls_loss权重。这一步是积水检测项目里真正拉开效果差距的地方。6. 让检测结果真正可用的三个验证手段积水检测模型交付前不要只看训练脚本里的mAP。我在验证集之外还会再做三件事这三件事每次都能帮我发现训练残留的问题。第一按面积过滤伪目标。道路积水检测的框通常占图面积比例较高真正会淹到车轮的积水区域面积都不小。推理时按像素占比过滤掉过小的框from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) results model.predict( /data/test_rain/, conf0.4, imgsz640, ) for r in results: img_h, img_w r.orig_shape for box in r.boxes.xyxy.cpu().numpy(): area_ratio ( (box[2] - box[0]) * (box[3] - box[1]) / (img_w * img_h) ) if area_ratio 0.01: # 过小的检测框大概率是反光点直接丢弃 continue第二用固定阈值做时序稳定性测试。同一监控点连续拍20帧积累水的框应该稳定存在而反光和倒影会闪烁。写个小循环统计连续相邻两帧检测框IoU大于0.5的帧数占比占比低于60%就要考虑模型对湿滑反光过于敏感。第三对天气变量做对照。数据集内部可能是晴天加雨天的混合我会分别建立晴天测试文件夹和雨天测试文件夹做推理。两个目录的precision一对比就能判断模型依赖的是道路本身的积水特征还是某种特定的光照反光模式。这三个检查做下来数据集才算真正用到位。我的习惯是每一次训练都把这些结果写进实验笔记下次换参回来对比。这个先解压、再核对、后训练、最后上测试集的顺序帮我避免过很多次在错误数据上白白消耗GPU时间的场面。希望帮到你。本文还有配套的精品资源点击获取