ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

道路积水检测数据集实战:VOC与YOLO格式转换及YOLOv8训练全流程

道路积水检测数据集实战:VOC与YOLO格式转换及YOLOv8训练全流程 简介面向道路积水识别的目标检测数据集覆盖2699张道路积水图片并配套Pascal VOC与YOLO双格式标注标注类别仅water一种共包含3777个矩形框适用于路面积水检测、城市内涝预警、辅助驾驶等视觉模型训练与算法验证。压缩包经7z压缩后约76.78MB文件总数约2000个以VOC格式XML标注文件与YOLO格式TXT标注文件为主另附使用说明文档解压后可直接按常规检测流程读取免去标注文件二次转换带来的额外工作量。标注工具采用labelImg矩形框边界清晰便于在PyTorch、Darknet、MMDetection等主流检测框架中直接使用。该数据集已有1064人学习可为道路积水检测项目提供现成的数据基础尤其适合需要快速搭建训练流程并完成积水识别效果评估的研究者与工程开发者。1. 道路积水检测数据集能吃下什么场景2699张图的选型判断道路积水检测数据集VOCYOLO双格式标注2699张图单类别用7z打包——这个货架上的标品其实指向一类很具体的需求给智慧城市、防汛预警或路况监测项目做目标检测训练而不是拿来做学术刷点。单类别的设定意味着你不用为类别平衡头疼把全部标注精力放在“水在哪”这一个问题上。适合谁手头要快速验证YOLO系列模型能不能识别路面积水的人或者要在汛期前拿到一个可复现的基线模型、后续用自有数据继续微调的工程同学。这套数据最大的价值不是数量而是格式到位VOC的XML标注方便你读、查、改YOLO的txt标注让训练入口几乎零转换成本。接下来我直接按“解压 → 读懂标注 → 训练 → 排错”的顺序把整条路走通。2. 看懂数据集内部结构VOC与YOLO两种标注的读取顺序拿到一个压缩包第一反应不应该是直接开训而是先把目录结构和标注格式吃透。这一步决定了后面所有脚本的路径假设是否成立。很多翻车现场不是模型问题是数据喂进去之前就没对齐。2.1 7z 解压命令与参数在 Linux 和 Windows 上都不踩坑这个包的后缀是 .7z不是 .zip所以别用unzip硬解。常见做法是优先用官方 7-Zip 或 p7zip 工具。Linux 环境先确认有没有装 p7zip没装的话按你用的包管理器装一下。# Debian/Ubuntu sudo apt update sudo apt install -y p7zip-full # 解压到指定目录避免散落一地 7z x 道路积水检测数据集VOCYOLO格式2699张1类别.7z -o./road_water_dataset参数说明x是解压并保留目录结构-o指定输出路径注意-o后面不能有空格直接拼路径。解压完成后先看目录长什么样不要急着删压缩包后面校验标注对不上时还能回头重新解压。如果是 Windows 环境右键菜单用 7-Zip 的“提取到当前文件夹”也没问题。但如果你在写自动化脚本或跑批处理建议走命令行7z.exe x而不是调用 GUI否则不好集成进 pipeline。提示包内如果是中文文件名Linux 下解压偶尔会出现文件名乱码。解决办法是加参数-mcp65001强制按 UTF-8 处理或者解压后立即逐个检查目录。解压完先验证文件数量对得上。这个数据集宣称 2699 张图那 images 或者 JPEGImages 目录下图片数量应该等于 2699标签数量也跟着核对一遍。find ./road_water_dataset -type f \( -name *.jpg -o -name *.png -o -name .jpeg \) | wc -l这一行命令的输出决定你后面的心态如果数量明显少于 2699说明压缩包不完整或解压过程有问题趁早换源重下后面所有工作建立在这个数上。2.2 VOC 目录结构与 YOLO 标签的真实差异VOC 格式的目录结构是 Pascal VOC 沿袭下来的惯例这套数据里的 VOC 部分应该是 JPEGImages、Annotations、ImageSets 三个目录。JPEGImages 放原图Annotations 放同名 .xml 文件ImageSets 里面通常还有 Main 子目录放着 train.txt、val.txt 这类划分文件。这种结构的好处是标准、可读任何标注工具如 labelImg 导出的结果都能兼容。但 YOLO 格式完全不一样。YOLO 训练时读取的标签不是 XML而是和图片同名的 .txt每一行代表一个目标框五个字段依次是class_id x_center y_center width height注意这里所有坐标都是归一化到 0~1 的浮点数中心点坐落在图片尺寸的比例上不是像素值。两者最大的差异可以看这张对比表维度VOC (XML)YOLO (txt)坐标像素绝对值 xmin, ymin, xmax, ymax归一化 x_center, y_center, w, h类别字符串如namewater/name整数 class_id每文件目标一个 xml 可有多个object一行一个框一个文件多行可读性人眼友好机器友好人要算一遍才能还原你拿到的数据集既然声明包含两种格式大概率是两种目录同时存在比如 VOC 目录和 YOLO 目录分开放或 YOLO 的标签在 labels 子目录下。先确认是不是同一个框在两种格式下数值能对得上。我一般会抽三张图手动算一次 XML 里的像素坐标除以图片宽高看和 txt 里的小数是否一致。2.3 从 XML 到 txtYOLO 标签怎么算出来的就算数据已经给了 YOLO 格式还是建议你把转换脚本跑熟。因为训练时你很可能要加自己的积水图片打标工具导出的往往是 VOC 格式转成 YOLO 这一手迟早要会用。下面的 Python 脚本是标准做法可以直接存成 xml2yolo.py。import xml.etree.ElementTree as ET import os from pathlib import Path def xml_to_yolo(xml_path, out_path, class_list): 把单个 VOC XML 转成 YOLO txt class_list: 类别名列表索引即 class_id tree ET.parse(xml_path) root tree.getroot() # 图片尺寸在 annotation 的 size 节点里 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_list: print(f[警告] 类别 {name} 不在列表中已跳过) continue class_id class_list.index(name) # bndbox 是像素绝对坐标 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 归一化中心点格式 x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h box_w (xmax - xmin) / img_w box_h (ymax - ymin) / img_h # 防止浮点误差导致越界 x_center max(0, min(1, x_center)) y_center max(0, min(1, y_center)) box_w max(0, min(1, box_w)) box_h max(0, min(1, box_h)) lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) # 同名 txt 写入 out_file Path(out_path) / (Path(xml_path).stem .txt) with open(out_file, w) as f: f.write(\n.join(lines)) class_list [water] # 单类别数据集只有这个 xml_to_yolo(JPEGImages/0001.xml, yolo_labels, class_list)逻辑说明脚本先读取 XML 里的图片宽高再遍历每个object节点取出像素坐标的 bndbox通过中心点和宽高归一化公式换算成 0~1 小数。max(0, min(1, ...))这行是防止浮点误差污染坐标比如x_center算了 1.0000001 这种值训练时会让 anchor 匹配出错。这里有个容易忽略的点转换出来的 txt 文件名必须跟图片名完全一致包括前缀不能带 .xml 的后缀。YOLO 训练是拿图片名去拼标签路径的比如road_001.jpg找road_001.txt对不上就等于是空标签。2.4 批量检查标注有效性的三行命令转换完成后别着急开训先用脚本扫一遍所有标签看有没有文件为空、尺寸为 0、坐标越界这类隐藏问题。我习惯用三行 Python 做快速体检。from pathlib import Path label_dir Path(yolo_labels) empty 0 bad_box 0 for txt in label_dir.glob(*.txt): lines [l.strip() for l in txt.read_text().splitlines() if l.strip()] if not lines: empty 1 print(f空标签: {txt}) continue for line in lines: parts line.split() if len(parts) ! 5: bad_box 1 print(f字段数错误: {txt} - {line}) elif float(parts[3]) 0 or float(parts[4]) 0: bad_box 1 print(f零尺寸框: {txt} - {line}) print(f空标签数: {empty}, 异常框数: {bad_box})参数说明空标签通常是 XML 漏标或转换脚本跳过了未知类别零尺寸框是 xmax 和 xmin 相等线状目标或坐标写反导致的这种框 nms 阶段会触发除零或产生 NaN 损失。跑出来的异常数如果超过 10 个就值得回到对应 XML 检查原标注是不是本身就有问题。3. 用这份数据跑通 YOLOv8训练前要做的划分和配置数据本身干净了接下来才是重头戏——把图片和标注喂进 YOLO 训练。这一步很多人急着直接yolo train结果验证集掉点、类别错乱、损失降不下去。顺序应该是先划分数据集再写 yaml 配置最后才跑训练命令。3.1 按 8:1:1 独立拆分别把划分做成抖动的来源如果你拿到的压缩包里带着官方划分好的 train.txt、val.txt可以直接沿用。如果只有图片和标签没有划分文件那就自己动手拆。拆分的要点是必须保证训练集、验证集、测试集之间图片完全不重叠且每个集里面的正样本数量比例接近原分布。下面这段脚本按 8:1:1 的比例把图片和标签同步复制到三个目录下。import os import random from pathlib import Path import shutil random.seed(42) # 固定随机种子保证每次划分结果一致 images_dir Path(yolo_dataset/images) labels_dir Path(yolo_dataset/labels) train_imgs, val_imgs, test_imgs [], [], [] all_imgs sorted(images_dir.glob(*.jpg)) # 先切分文件名列表 random.shuffle(all_imgs) n len(all_imgs) train_end int(n * 0.8) val_end int(n * 0.9) train_imgs all_imgs[:train_end] val_imgs all_imgs[train_end:val_end] test_imgs all_imgs[val_end:] # 把图片和同名标签一起复制 for split_name, img_list in [(train, train_imgs), (val, val_imgs), (test, test_imgs)]: for img in img_list: label_file labels_dir / (img.stem .txt) if not label_file.exists(): print(f[警告] 标签不存在: {label_file}) continue # 复制图片 dest_img Path(fdataset/{split_name}/images) / img.name dest_img.parent.mkdir(parentsTrue, exist_okTrue) shutil.copy(img, dest_img) # 复制标签 dest_label Path(fdataset/{split_name}/labels) / label_file.name dest_label.parent.mkdir(parentsTrue, exist_okTrue) shutil.copy(label_file, dest_label) print(f训练集: {len(train_imgs)}, 验证集: {len(val_imgs)}, 测试集: {len(test_imgs)})逻辑说明脚本先把所有图片路径打乱按 8:1:1 切三段然后逐张把图片和同名标签复制到目标目录。固定random.seed(42)是为了可复现——你跑一次和跑十次的划分结果完全一致排错时不会因为数据不同而怀疑人生。我特别强调用复制而不是移动是因为原始目录里的文件在做标注质量回查时还有用。训练用的 dataset 目录只是临时产物删了可以再生成原文件丢了就得重新下载。3.2 生成 data.yaml 的正确姿势五个字段一个都不能错YOLO 训练时靠一个 yaml 文件描述数据位置和类别信息。很多新手在这里翻车把names写成[0, 1]或漏了nc字段训练直接报错。正确格式如下# data.yaml path: ../dataset # 相对于当前工作目录的根路径 train: train/images val: val/images test: test/images nc: 1 names: [water]路径用相对路径比绝对路径稳因为换机器跑的时候绝对路径一错全错。train、val指向的是含图片的目录YOLO 会自动根据train/images推导出train/labels去找对应标签。一个常见的疑问是names列表的顺序是不是必须和标签里的 class_id 对应。答案是必须。标签文件里 class_id 是 0那 names[0] 就必须是 water。如果你把 names 写成[puddle, water]模型会把原本的积水框硬拆成两个类训练最后的输出类别维度都是错的。3.3 最小训练命令与损失曲线的读法接下来就是大家搜得最多的yolov8训练自己的数据集这一步。假设你已装好 ultralytics 包一条命令就能训起来yolo detect train modelyolov8n.pt datadata.yaml epochs100 imgsz640 batch16 device0 projectwater_runs nameexp1参数说明modelyolov8n.pt是从 COCO 预训练权重开始微调收敛速度比从零训练快得多迁移学习在积水这种新任务上的收益极其明显。imgsz640对应数据集的原始分辨率如果原图是 1920x1080可以先按 640 跑通流程后期再上 1280。batch16是显存不够时的常用值显存紧张可以一路降到 4。训练过程中重点看两个指标box_loss是回归损失持续下降说明框的位置在变准cls_loss是分类损失降不下去说明模型在区分“有水”和“没水”时遇到困难。但要区分正常波动和异常发散——前 20 个 epoch 损失震荡是正常的如果 50 个 epoch 后还在原地抖动大概率是学习率太高或标注噪声大前者把lr0从 0.01 降到 0.001后者回到标注环节查重。训练结束后模型权重存在project/exp1/weights/best.pt。这里有个容易踩的陷阱best.pt是按验证集 loss 选的最优权重但验证集 loss 低不代表检测效果满足业务需求你还得跑一次测试集或者跑真实验证图。4. 避坑积水检测数据集的5个翻车现场排查用这份数据跑训练的工程里我见过不少队友在同一个地方反复卡住。这一章把最常见的五个坑按“现象 → 原因 → 解决”讲清楚每一坑都是真金白银换来的血泪经验。4.1 标签显示为空YOLO 不认识你的 txt 命名现象训练日志里每张图片的标签数都是 0或者跑验证集时 mAP 一直是 0。打开标签目录一看txt 文件确实存在。原因YOLO 找标签的规则是拿图片名去匹配同名 .txt。如果你的图片叫IMG_20230601_142530.jpg标签文件却叫IMG_20230601_142530.txt没问题——但如果是IMG_20230601_142530.JPG改成.txt或者图片和标签在不同子目录路径就对不上。另一个高频原因是标签文件里面是空的XML 转 txt 时类别名不在列表里被静默跳过。解决先检查图片名和标签名严格同前缀再用 2.4 的体检脚本查空文件。还要确认你的目录结构里 images 和 labels 是平级目录YOLO 只会把images替换成labels去找对应文件不会递归搜索。4.2 类别 ID 错位names 顺序和标签对不上现象训练正常跑完推理时模型把路缘石、栏杆甚至干净的柏油路都标成积水precision 低得离谱。原因数据集的 txt 标签里 class_id 是 0但你 data.yaml 里 names 写成了[water, something_else]或者反过来 names 只有 water 但标签里有 1。单类别数据集最容易出现这个问题的变种是——你后来追加了自己标注的数据新数据的类别 ID 从 1 开始合并时没统一。解决合并任何外部数据前先跑一个脚本把全部标签的 class_id 去重打印。如果出现大于等于nc的值立即统一修正。这里有现成命令# 检查所有标签里出现过的类别 id cat dataset/train/labels/*.txt | awk {print $1} | sort -un输出应该是单个 0对单类别数据来说。出现 1、2 就说明有的文件类别没对齐找出来重新映射。4.3 框越界和零尺寸框边界框三个检查缺一不可现象训练 loss 在某个 epoch 突然变成 NaN或者 loss 曲线正常但是验证集的 Recall 始终上不去单张图的多个目标重叠时只检测出一个。原因XML 标注里有人把 bndbox 的 xmax 写成了 2688但图片宽度只有 1920也有人 xmin xmax 的约定不一致导致宽高算出来是负值。YOLO 训练虽然会对越界坐标做裁剪但这种修正本身会扭曲框的位置相当于用错误监督去教模型。解决不要依赖训练框架的自动修正在写出 txt 之前就做三道检查框坐标按图片尺寸截断、宽高小于 1 像素的直接删除、xmin xmax 且 ymin ymax 必须满足。前文转换脚本里的max(0, min(1, ...))也是这一道工序的一部分。另外提醒一句如果目标贴边归一化后的框就算裁到 0.9999 也会被某些版本的处理逻辑忽略排查时优先怀疑这类贴边框。4.4 7z 解压报错常见的那句“密码正确但一直报错”现象解压时输入密码提示正确但中途报CRC Failed或Data Error in ...jpg文件解压不完整。原因大概率是下载文件不完整或者压缩包在传输过程中损坏。7z 格式对数据完整性校验比 zip 严格损坏到某个文件就直接报错。另一个可能是个别文件被安全软件拦截导致解压线程中断。解决先比对本地的文件大小和下载页面给的校验值如果发布了 MD5/SHA256 的话不一致就重新下载。如果没有校验值用 7z 自带的测试命令也能定位7z t 道路积水检测数据集VOCYOLO格式2699张1类别.7z这个命令只测完整性不解压能快速判断是压缩包坏了还是解压过程出问题。测完发现某张图损坏但其他文件正常可以先用7z e把其他文件解出来应急缺的图看情况找替换。4.5 增强参数把积水变成误检Mosaic 和翻转要慎开现象训练集 mAP 很高验证集也还行但拿到真实街道场景一测正常的路面反光、柏油裂缝全被框成积水。回头查看训练配置开了mosaic1.0和fliplr0.5。原因积水在视觉上和反光、阴影、油渍有天然相似性。Mosaic 增强把四张图拼在一起容易把小尺寸的积水区域切掉一半甚至拼到另一张图上标签跟着错乱。水平翻转虽然合理但因为积水有方向性——比如积水总是沿着路沿弧度分布左右翻转后模型可能学到错误的形状分布。解决第一轮训练关闭增强里的mosaic和mixup只开hsv_h和hsv_s这类颜色扰动让模型先把真实的积水特征学稳。等 baseline 有了再逐步打开强增强看收益。对单类别目标检测我更倾向于把mosaic设为 0.3 而不是全开这能显著减少小目标被截断的问题。5. 把积水检测模型做得更稳三个值得多花半小时的验证动作训练完拿到 best.pt 只是开始离真正能用还差一步确认模型在新场景下的行为符合预期而不是只在测试集上自嗨。我这里分享三个每次都用的验证动作总耗时不超过半小时但能省掉大量后续返工。第一个动作是随机抽 50 张测试集图片用训练好的模型推理把标注框和预测框同时画出来。你可能会看到两种情况需要警惕一是标注框明显比预测框大说明原标注画框时把背景也包进去了二是预测框多个重叠在同一片积水区说明 NMS 阈值设得不够低或用的是nms0.6以下的效果不够。画图命令用 ultralytics 自带的最省事yolo detect predict modelwater_runs/exp1/weights/best.pt sourcedataset/test/images save_txtTrue save_confTrue然后挑几张夜间、逆光、强反光的图单独看。如果这些困难样本几乎全漏检不要急着加数据先把conf_thres降到 0.1 看模型到底是“没检测到”还是“检测到了但置信度低”。这两者解决方案完全不同。第二个动作是调一次置信度阈值并记录 precision/召回率变化。业务上你是想最大化检出率不漏报还是最小化误报不乱报决定了你要卡在哪个阈值上。没有一步到位的 0.5你必须用自己的场景图试出来。第三个动作是记录模型在哪些地方把路沿石阴影误判成积水。这个信息比 mAP 数字值钱得多因为它直接告诉你后续该补什么数据——不是补更多积水照片而是补负样本让模型见过足够多“看着像积水但不是水”的东西。做过这几步之后无论你是要部署到边侧摄像头还是接入到防汛平台心里都有底了。我自己的习惯是每次换数据集或者换模型版本都会把上面的动作重复一遍并写下当时的调参结论。回过头来翻这些记录比翻训练日志更能帮助定位问题。希望帮到你。本文还有配套的精品资源点击获取
返回列表