简介:这份深度学习积水目标检测数据集专为自动驾驶、智慧城市与道路安全监测场景设计,面向计算机视觉研究者、算法工程师及入门学习者。资源聚焦路面坑洼积水识别,包含作者亲自标注的精确边界信息,可直接用于训练Faster R-CNN、YOLO系列或Mask R-CNN等主流目标检测模型,帮助提升模型在复杂路面环境下的积水识别与泛化能力。压缩包共194个文件,其中97张jpg原始图像与97个xml标注文件一一对应,xml中记录积水区域坐标,便于直接接入常见检测框架;整体包体仅11.83MB,轻量易下载,适合快速开展实验或教学演示。目前已有3897人学习下载,数据经人工仔细标注,边界准确,覆盖多种积水形态,可作为模型微调、算法对比或课程实践的优质素材。对需要构建积水检测原型系统或验证模型效果的开发者而言,这份数据集提供了清晰、标准的数据基础,省去繁琐的采集与标注环节,可显著提升研发效率。
1. 积水目标检测数据集到底解决什么问题:给反光水体一个稳定定义
路上的积水对自动驾驶和道路养护来说都是危险信号,但它在目标检测里是个很尴尬的目标:没有固定形状、没有稳定纹理,晴天是蓝天白云的倒影,阴天是一块接近路面的深色,夜间车灯一照又变成高亮白斑。深度学习积水目标检测数据集,就是把“坑洼积水”从通用目标检测里单独拆出来,用专门采集的样本教会模型识别水体边界,而不是靠纹理猜物体。我见过不少团队拿通用数据集跑出来的权重直接识别道路积水,结果把黑色湿路面判成深坑、把倒影当成实体,漏检和虚警同时存在。这篇笔记我从采集、清洗、标注、训练到验证的完整链路拆开讲,给你一条能照着走的路。
2. 坑洼积水数据集的采集与标注:先定机位、类别和边界规则再动手
数据集的构建顺序里,采集和标注的决策比训练参数更影响上限。积水检测尤其如此:同一片水,从车头前视看是横条状反光,从无人机俯视看是一块形状不规则的地面变色。如果机位混乱、类别定义模糊、边界规则不统一,后期的清洗和训练都会在数据质量上反复返工,等于把时间浪费在“标完再发现标错了”上。
2.1 机位决定目标形态:前视、监控与俯视的标注差异
采集机位决定了目标在图像里长什么样,也决定了边界框怎么打才合理。常见做法是把采集分成三种机位。车头前视是最常见的车载场景,积水在画面里是一个压扁的椭圆或横向长条,边界框应贴着水体可见区域框住,但不要把倒影里的天空、路灯框进去;路侧监控是固定机位,视野中积水区域比较大,近处的区域框得准、远处的小目标几乎只有几个像素,这类样本对检测器的小目标能力很有帮助;无人机俯视则完全不一样,积水顶面完整可见,边界清晰,但透视变形和飞行高度变化会让目标尺度剧烈波动。
| 机位 | 典型设备 | 目标形态 | 标注习惯 |
|---|---|---|---|
| 前视 | 行车记录仪、前向相机 | 横向长条形、尺寸中等 | 框住可见水体本身,倒影不纳入 |
| 监控 | 枪机、球机 | 近大远小、尺度悬殊 | 近处精标,远处宁可漏标也不要乱框 |
| 俯视 | 无人机、高位相机 | 边界较完整、形状不规则 | 沿可见边界画紧框,少留空白 |
一个数据集如果混入三种机位,建议在文件名前缀里区分Source_A、Source_B、Source_C,否则后面做数据集划分时,同一路视频的画面会同时出现在训练集和验证集里,造成指标虚高。部署阶段一般只会面对一种机位,所以在数据量足够的情况下,按机位分别训练比混合训练更实用。
2.2 类别设计:单类“积水”起步还是按形态拆多类
我惯用的做法是先做单类。坑洼积水数据集从“有没有水”这一步开始,类别就定为water_puddle,目标定义是:画面中可见且能判断出边界的水体区域,包括坑洼积水、路面积水、薄水膜覆盖的凹陷区域。不需要一开始就区分“坑洼积水”和“薄水膜”,因为标注员对这两种类型的主观判断会很不一致,同一个标注员隔两天都可能给出不同标签,最后变成纯噪声,模型学到的不是形态差异,而是标注习惯差异。
等单类模型质量能达标、误检集中在某类固定物体上时,再把误检样本收集起来,拆成第二个类别或加入背景。多类拆分的建议是按风险等级拆,而不是按形态拆:比如“大面积积水”和“小水坑”分开,让下游决策可以根据类别做不同的通过策略。这个拆分最好在采集阶段就有意识地在文件名或备注里标记,而不是等标注完再靠画框猜。
2.3 时段与环境配比:反光、弱光、湿滑路面的样本构成
采集时最容易犯的错是只拍晴天白天的积水,因为好看的积水照片几乎都出现在阳光充足的时段。但真正需要算法工作的场景是雨后、清晨和夜间。数据配比上我一般按5:3:2走:50%白天正常光照,30%阴天或弱光,20%夜间车灯或路灯场景。这个比例不一定精确,但强烈反光的样本至少要占三成,否则模型学到的特征是“深色低纹理区域”,会把新铺的沥青路大面积误检成积水。
反光和弱光样本的采集有个硬条件:尽量让积水区域在画面里占足够比例。很多团队用行车记录仪直接抽帧,水坑在远处只有几十个像素,标注员看了都费劲,模型学到的是“远处小黑点”,毫无泛化价值。采集时宁可放慢车速或调整机位角度,让目标占画面长边的三分之一以上,再抽帧保存。夜间的样本如果实在拍不到清晰的边界,优先保“有人眼确认是水”的帧,那种连人都分不清的图直接丢弃。
2.4 标注规范的三条硬规则:贴边、倒影、遮挡
标注规范是坑洼积水数据集里最容易被忽视、却直接决定训练效果的部分。积水本身边界模糊,标注员如果凭感觉框,同一张图片在不同人手里能标出三种结果。我给自己和团队定的规则有三条。第一条是贴边:边界框必须紧贴可辨认的水体边缘,宁收勿放,框外带的环境背景面积不要超过框面积的15%。第二条是倒影不标:水域里映出的天空、建筑、灯光,哪怕再清晰,也不作为目标的一部分。第三条是遮挡处理:当积水部分被车辆、行人或路障遮挡时,只标注可见部分,不脑补被遮挡的区域。
规则定完之后还要配一个质检动作:标注完成后按图片批量统计框的宽高比分布。正常情况下,前视积水的框宽高比集中在2:1到5:1之间,如果出现大量接近正方形的框,说明标注员把倒影或者整个水洼外围一起框进来了,需要回改。宽高比统计脚本不复杂,用Python读一遍所有txt标注文件就能跑出来。
提示:标注规范最好以文本形式随数据集一起保存,后续标注员更换或模型误检溯源时,它就是唯一的判定依据。
3. 把原始图像清洗成可训练的数据集:筛图、标注与VOC转YOLO脚本
采集回来的原始图像不能直接进标注。积水的反光特性意味着模糊和过曝问题在原始数据里大量存在,尤其是夜间车灯场景,一帧过曝会让整块区域变成纯白,连标注员都看不清。清洗阶段先把这类图扔掉,能省下大量无效标注工时,也避免模型把“纯白高亮”学成积水的特征。
3.1 清洗脚本:先扔模糊图再过曝图,避免标注员浪费时间
我一般用OpenCV做模糊和过曝的两道初筛。模糊检测用Laplacian方差,积水场景整体纹理偏少,阈值要适当调低,不能拿通用检测的“模糊图阈值100”去套,那个阈值会把大量暗光环境下其实很清晰的积水图误删:
import cv2 def is_blurry(path, threshold=40): img = cv2.imread(path, cv2.IMREAD_GRAYSCALE) if img is None: return True lap_var = cv2.Laplacian(img, cv2.CV_64F).var() return lap_var < threshold积水图像本身平滑区域多,Laplacian方差天然偏低,低于40基本是肉眼可见的糊,低于20则完全是运动模糊或失焦。这个阈值可以在10张典型清晰图和10张典型模糊图上先跑一遍分布再定,比直接抄通用参数靠谱。过曝检测用亮度直方图。积水区域反光时容易把一小块区域打成纯白,但整图均值不一定高,所以要统计高亮像素的比例:
import cv2 import numpy as np def is_oversaturated(path, bright_ratio_thr=0.3): img = cv2.imread(path) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) hist = cv2.calcHist([gray], [0], None, [256], [0, 256]) bright_pixels = np.sum(hist[220:]) return bright_pixels / gray.size > bright_ratio_thr把超过30%像素都落在220以上的图挑出来,这些图里的积水已经看不出水体和路面的分界线,标注员只能瞎猜。夜间车灯场景不要急着把所有高亮图都扔掉,可以单独放在night_highlights目录里,等标注完白天数据后由有经验的人评估是否保留,否则夜间样本容易直接清零。
3.2 标注工具怎么选:单机labelImg到团队CVAT
清洗完后进入标注环节。目标检测常用标注工具里,最稳妥的是三选一:单人小规模用labelImg,想提高效率用X-AnyLabeling的半自动分割再转框,团队多人协作用CVAT。坑洼积水数据集的核心难点不在工具,在标注规范的一致性,所以我不建议一上来就用全自动预标注,除非你已经有一个在类似积水场景上效果可靠的模型。如果确实想用预标注提效,优先试开放词汇检测模型,它至少能在干净场景下给出比较合理的初始框,再由人工修正。
X-AnyLabeling走的是“分割出水体轮廓再自动生成外接框”的路线,对不规则积水比手动画框更贴合,但它需要你提前准备好一个分割模型,否则自动分割的边界会覆盖成一大片。团队场景下CVAT可以多人并行标注同一批数据,内置的标注冲突检查能明显减少边界不统一问题。无论选哪个工具,导出格式统一走VOC XML,后续转YOLO或COCO都方便。
3.3 VOC转YOLO:转换脚本与越界、空标签四个坑
标注完成后,大多数训练流程要求的是YOLO格式的txt文件。转换脚本本身逻辑不复杂,但四个边界坑几乎每个人都会踩一遍。下面是我现在还在用的转换脚本,异常处理都写在里面:
import xml.etree.ElementTree as ET CLASS_NAMES = ["water_puddle"] def voc_to_yolo(xml_path, out_path): tree = ET.parse(xml_path) root = tree.getroot() size = root.find("size") img_w = int(size.find("width").text) img_h = int(size.find("height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in CLASS_NAMES: continue cls_id = CLASS_NAMES.index(name) box = obj.find("bndbox") xmin = float(box.find("xmin").text) ymin = float(box.find("ymin").text) xmax = float(box.find("xmax").text) ymax = float(box.find("ymax").text) # 越界修正:标注框偶尔会画出图片边缘 xmin = max(0.0, min(xmin, img_w - 1)) ymin = max(0.0, min(ymin, img_h - 1)) xmax = max(0.0, min(xmax, img_w - 1)) ymax = max(0.0, min(ymax, img_h - 1)) if xmax <= xmin or ymax <= ymin: continue x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") with open(out_path, "w") as f: f.write("\n".join(lines))转换后必须做一次统计检查,最常见的问题是四个。一个是图片宽高写在XML里是0或1,多半是标注工具导出异常,转换出来的归一化坐标全部失真;第二个是类别索引错位,XML里的类别名和CLASS_NAMES顺序对不上,模型训练时目标全部被当成背景;第三个就是越界框,如果教程给的脚本没做clip处理,训练时会出现负坐标警告,YOLOv8能容忍一部分,但anchor匹配会乱;第四个是空标签,有些XML只有文件名没有目标,转换后生成0字节txt,训练时会被当成背景样本,如果这样的文件过多,模型会偏向输出低置信度。
提示:转换完成后,我习惯把所有txt重新读一遍,统计每类目标数量、目标宽高比例分布和无效文件数量,这个动作能在30分钟内发现大部分标注事故。
4. 用YOLOv8在积水数据集上训练检测模型:划分、增强与三个必调参数
数据到位后,最常出现的动作是随便找一份目标检测训练教程照抄命令。积水数据的特性决定了不能完全照搬,尤其是数据集划分方式和图像尺寸,直接决定模型能不能在小目标、多反光场景里可靠输出。
4.1 目录组织与划分逻辑:按视频片段切分而不是随机切图
YOLOv8训练自己的数据集时,目录结构相对固定。我习惯把数据集放在一个独立目录里,images和labels下各分train/val两个子目录。这个结构本身没什么玄学,关键是划分方式:不要对全部图片随机抽样切分。
路面积水数据通常来自连续视频抽帧,同一片水坑在相邻几十帧里都有出现。全局随机切分会让同一片水的不同帧同时进入训练集和验证集,验证集指标虚高,真正到另一条路、另一段拍摄时间就原形毕露。我一般按视频片段或路段名分组划分,先用文件名前缀把帧归组,再把组随机分配到训练和验证:
import os, random, shutil from glob import glob frames = sorted(glob("raw/*.jpg")) groups = {} for f in frames: key = os.path.basename(f).rsplit("_", 1)[0] # 例如 roadA_0001.jpg -> roadA groups.setdefault(key, []).append(f) keys = list(groups.keys()) random.seed(42) random.shuffle(keys) val_ratio = 0.2 val_keys = set(keys[: int(len(keys) * val_ratio)]) for key, files in groups.items(): for f in files: name = os.path.basename(f) if key in val_keys: shutil.copy(f, "datasets/water_puddle/images/val/" + name) else: shutil.copy(f, "datasets/water_puddle/images/train/" + name)这里的同名Label也要按同样的分组规则复制到labels目录,不能只移图片不移标注文件。按组划分会让验证集看起来更难,但更接近真实部署的处境。如果数据集本身采集自多个来源,比如车载和监控混在一起,优先保证同一个来源不出现在两个集合里。
4.2 三个必调参数:imgsz、mosaic和epochs的设定思路
积水检测训练时,损失函数和网络结构基本不用动,但有两个参数必须按积水数据的特点调整。第一是图像尺寸imgsz。通用检测任务用640,积水场景建议直接用1280。积水目标在画面里的实际像素往往只有几十到一百出头,属于小目标,640分辨率下特征图上的响应区域太小,漏检率明显上升。显存不够时优先用1280训练、把batch降到4,也不要退回640,这是积水检测数据集上性价比最高的改动。
第二是mosaic。YOLOv8默认开启mosaic数据增强,把四张图拼成一张训练图,对通用目标很有效,但积水是低纹理、靠环境反光识别的目标,四张图拼接会产生大量伪边界,模型容易学到“有接缝就有积水”。我一般直接关掉mosaic或只在最后几个epoch关闭:
yolo detect train \ model=yolov8n.pt \ data=water_puddle.yaml \ imgsz=1280 \ batch=16 \ epochs=120 \ mosaic=0.0第三是epochs。积水数据量通常不大,几百到两三千张图为主,模型在30到50个epoch就拟合得差不多,但训练更久之后验证集的mAP会缓慢波动。我的习惯是先跑80个epoch看验证集趋势,如果val曲线在第60个epoch后已经平了,再跑40个epoch基本是浪费时间。早停不是给通用任务准备的借口,在积水这种类内差异大的数据集上,训练过久反而会让模型记住特定水坑的纹理。
4.3 小样本与类不平衡:预训练权重、负样本与增强策略
小样本是积水检测绕不开的问题。应对小样本最有效的做法不是堆数据增强,而是用COCO预训练权重做初始化。yolov8n.pt或yolov8s.pt自带的特征提取能力足够迁移到积水场景,重点在于让模型学会“反光区域”这个概念,而不是从零学边缘检测。类别数配置要在data.yaml里写清楚,只保留一个类别:
path: /path/to/water_puddle train: images/train val: images/val nc: 1 names: 0: water_puddle除了训练图,我强烈建议专门留一部分不标注任何目标的“负样本”图片放进训练集,对应空的txt文件。这些图片的作用是告诉模型:干沥青、人行道、绿化带、湿路面反光但没积水,都不是目标。很多积水模型误检率高,不是因为正样本不够,而是负样本缺失。负样本最好覆盖和正样本相同的道路类型,数量按正样本的20%到30%准备。
增强策略上,积水对亮度变化极其敏感,HSV增强里的饱和度扰动可以开大,但色相扰动不要超过0.05,否则会把路面颜色改成离谱的紫色,模型学到的是“浅紫色区域”。另外随机翻转对积水目标完全可用,但上下翻转不要做,车头前视的数据里积水永远在地面下半部分,上下翻会让模型困惑。
5. 积水检测训练避坑:五条高概率踩坑记录与排查路径
积水检测训练的坑比通用目标检测多,原因是积水本身缺乏语义边界。下面五条是我在调试中反复遇到的典型问题,每条都按现象、原因、解决三步来写。模型微调崩了这种事,大多数时候不是代码问题,而是数据边界出了问题。
5.1 模型把黑色裂缝当积水:缺负样本的典型症状
现象:训练完的模型对沥青路面上的裂缝、补丁和阴影区域给出高置信度积水框,甚至比真积水的分数还高。原因:数据集中负样本太少,模型没见过“干路面但低纹理、高反光”的区域,把低纹理深色区域当成了积水的类别特征。解决:从训练集里挑出所有不含目标的干路面、裂缝路面、阴影路面图,复制到训练集images目录并生成空txt文件,重新训练一轮,误检数量通常会显著下降。如果这些图片不够,直接把误检来源的视频抽帧补充进去,效果比调置信度阈值彻底。
5.2 验证集mAP高、实拍全漏:训练集过拟合了反光纹理
现象:验证集上mAP达到0.8以上,但换个路段、换个时间实拍,模型几乎全部漏检。原因:数据集划分时用了全局随机切分,同一个水坑的相邻帧同时出现在训练和验证集里,模型学的是“这个特定水坑的反光纹理”,而不是“水”这个概念。解决:按视频片段重新划分数据,删除所有跨集合的相同片段,重新训练。排查时先看验证集图片和训练集图片是否来自同一段连续视频,如果是,问题基本确认。
5.3 同一片积水晴天检测正常、雨天翻车:纹理依赖过强
现象:同一个部署点,晴天积水检测准确、置信度高,雨天或阴天同场景积水大量漏检。原因:雨天气压低、水面失去镜面反光,积水不再有明显的天空倒影,模型靠反光纹理做决策的那部分特征全部失效。解决:在训练数据里补强阴天、弱光样本,并把HSV增强里的亮度抖动范围放大到±40,逼模型学会用“路面凹陷、水色与边缘色差”这些更稳定的线索,而不是只依赖强烈反光。没有条件采集阴天样本时,可以先用亮度归一化工具把晴天积水图拉暗,缓解一部分,但效果有限。
5.4 标注框贴边不统一,AP曲线剧烈震荡:规范与质检缺失
现象:训练过程中AP曲线震荡幅度大,不同epoch之间的精度忽高忽低,且训练集损失低、验证集损失高。原因:标注员各自按感觉贴边,有的框住整个水洼包括外围湿润路面,有的只框中间亮面,同一目标的IoU分布混乱。解决:回到标注阶段,按2.4节的贴边规则统一,对宽高比分布异常的文件批量筛选回改。代码层面可以写一个统计脚本,读取所有txt,把宽高比大于6或小于0.5的框逐一打印出来核对,这类框往往是倒影或错误框。
5.5 加载预训练权重后损失居高不下:类别数配置与参数冻结冲突
现象:用yolov8m.pt做初始化,训练几十个epoch,损失始终在2.5以上不降,验证集几乎不出框。原因:最常见的是自定义数据集类别数和预训练头不匹配,比如直接把COCO 80类的模型改到自定义数据集时没重建检测头;或者误用了冻结backbone的脚本,冻结层不更新导致新任务学不到积水特征。解决:确认data.yaml的nc字段和names数量一致,使用YOLOv8官方命令行时检测头会自动重建,不必手动改网络结构;冻结训练只适合用已有积水权重做二次微调,从COCO权重起步时不建议冻结backbone。排查时看训练日志里head部分的参数是否在更新,如果只有backbone在动,基本就是冻结配置出错。
6. 验证模型是否真的可用:时间维度、帧间一致性与保守阈值
训练完不代表能上线。积水检测的落地验证比训练更需要设计,因为在真实场景里,积水是随天气和时间变化的,模型如果只在某一批数据上表现好,几乎没有部署价值。
6.1 按时间组织验证集,别让同一场雨同时进训练和验证
我会在采集阶段就给每张图片打上采集日期和路段信息,验证集严格按时间段切,而不是按文件随机抽。比如用前两天的雨后数据训练,第三天的独立路段数据做验证。这个做法牺牲了验证集的大小,但换来的是对“这个模型有没有真的泛化”的诚实回答。如果换一天的数据后mAP从0.86掉到0.5,说明模型记住的是当天特定光照,回到采集环节补样本比调参有效。
6.2 用IoU链做帧间一致性过滤:单帧误检现原形
积水在连续视频帧中应该持续存在,位置变化平缓;误检往往单帧出现、下一帧就消失。我在部署代码里加一段非常轻的跟踪逻辑,把当前帧检测框和上一帧框做IoU匹配,连续三帧以上有重叠才算有效报警:
def match_prev(prev_boxes, cur_boxes, iou_thr=0.3): matched = [] for cb in cur_boxes: for pb in prev_boxes: iou = calc_iou(cb, pb) # 两个框的交并比 if iou >= iou_thr: matched.append(cb) break return matched这段代码不需要训练,IoU阈值0.3即可。检测硬件性能允许的话,还可以把模型输出置信度阈值调低到0.25,先保证不漏,再靠帧间连续性过滤单帧虚警,这一套组合在实际路段上的表现通常比单纯调高置信度阈值要好得多。我的习惯是:置信度阈值永远最后调,先用多帧逻辑压误报,实在压不干净才动阈值。这套验证流程看着简单,但它帮我躲过了很多次“测试集好看、上路翻车”的局面。希望帮到你,少绕几圈。
本文还有配套的精品资源,点击获取