简介:本资源为室内房间积水检测目标检测数据集,面向从事计算机视觉、安防巡检或智能家居场景的算法开发者与学习者,可用于训练和验证积水区域识别模型。压缩包共1073个文件,包含357张jpg图片、357个VOC格式xml标注文件及359个txt文件(含YOLO格式标注),整体约84.65MB,图片与标注一一对应,方便直接接入Pascal VOC或YOLO训练流程。数据集仅设一个类别jishui,共标注378个矩形框,采用labelImg完成画框标注,标注准确合理。目前已有216人学习下载,适合作为小样本目标检测练手或积水识别任务的起步数据。读者可获取完整图片与双格式标注,快速搭建训练与评估环境,省去自行采集和标注的成本,并在此基础上做数据增强、模型微调与效果对比。
1. 室内积水检测数据集:357 张 VOC+YOLO 双格式到底能训出什么
室内房间积水检测这个方向,听起来冷门,但做过机房、仓库、地下车库巡检的人都知道它有多刚需。摄像头架在角落,地面反光、水渍、阴影混在一起,人眼都要盯两秒才能确认,更别说让模型自动报警。这个标题给的是一个 357 张、VOC 和 YOLO 双格式打包的目标检测数据集,专门标注室内地面的积水区域。357 张不算大,但足够跑通一条从数据校验到 YOLO 训练再到推理验证的完整链路,适合做原型验证、课程设计、或者给现有巡检系统加一个积水检测分支。它解决的不是“通用目标检测”问题,而是“小样本、单类别、强反光干扰”这个具体场景。如果你手头正好有室内巡检需求,或者想找一个真实的小数据集练 YOLO 全流程,这个方向值得花一个下午跑一遍。
2. 拿到压缩包先别急着训:VOC 与 YOLO 双格式的校验与转换
2.1 为什么这个数据集同时给了 VOC 和 YOLO 两套标注
VOC 格式用 XML 存标注,每个文件对应一张图,里面记录了图片尺寸、目标类别和边界框的左上角、右下角坐标。YOLO 格式则是每张图配一个 txt,每行是类别索引 中心x 中心y 宽 高,坐标全部归一化到 0 到 1 之间。两套格式同时存在,通常意味着标注工作是在 VOC 体系下完成的,然后通过脚本转了一份 YOLO 出来。对使用者来说,VOC 适合做数据审查和可视化,因为 XML 可读性强,能直接看到原始像素坐标;YOLO 格式则直接喂给 YOLOv5、YOLOv8 这类框架,省去训练前的转换步骤。
但这里有个常见坑:转换脚本如果没处理好边界框越界或者图片尺寸读取错误,YOLO 的 txt 里会出现负数或者大于 1 的值。所以拿到数据后第一件事不是配置训练,而是做一轮完整性校验。357 张的规模,校验脚本跑起来也就几秒钟,但能帮你省掉训练到一半 loss 变 NaN 的后悔药。
2.2 用 Python 做一轮 VOC 标注完整性检查
下面这段脚本遍历 VOC 的 XML 文件,检查图片是否存在、尺寸是否匹配、边界框是否越界。你只需要把voc_dir和img_dir换成实际路径。
import os import xml.etree.ElementTree as ET from PIL import Image voc_dir = "./VOC/Annotations" img_dir = "./VOC/JPEGImages" problems = [] for xml_file in os.listdir(voc_dir): if not xml_file.endswith(".xml"): continue tree = ET.parse(os.path.join(voc_dir, xml_file)) root = tree.getroot() filename = root.find("filename").text img_path = os.path.join(img_dir, filename) if not os.path.exists(img_path): problems.append(f"图片缺失: {filename}") continue img = Image.open(img_path) w, h = img.size for obj in root.findall("object"): name = obj.find("name").text bbox = obj.find("bndbox") xmin = int(bbox.find("xmin").text) ymin = int(bbox.find("ymin").text) xmax = int(bbox.find("xmax").text) ymax = int(bbox.find("ymax").text) if xmin < 0 or ymin < 0 or xmax > w or ymax > h: problems.append(f"越界: {filename} {name} ({xmin},{ymin},{xmax},{ymax}) 图片尺寸 {w}x{h}") if xmax <= xmin or ymax <= ymin: problems.append(f"无效框: {filename} {name}") print(f"共发现 {len(problems)} 个问题") for p in problems[:20]: print(p)这段代码的逻辑很直接:先确认 XML 里记录的图片文件真实存在,再读取图片实际宽高,然后逐个目标检查边界框是否超出图片范围,以及右下角坐标是否大于左上角。参数方面,voc_dir指向 Annotations 文件夹,img_dir指向 JPEGImages 文件夹。如果你拿到的压缩包目录结构不同,比如图片和 XML 混在一起,改一下路径拼接就行。跑完如果输出“共发现 0 个问题”,说明 VOC 侧是干净的,可以进入下一步。
2.3 VOC 转 YOLO 的脚本与四个边界坑
如果压缩包里已经带了 YOLO 格式的 txt,你可以跳过转换,但建议还是用脚本重新生成一遍,确保坐标归一化正确。下面这个转换脚本把 VOC 的 XML 转成 YOLO 需要的 txt,类别名统一映射为water。
import os import xml.etree.ElementTree as ET from PIL import Image voc_dir = "./VOC/Annotations" img_dir = "./VOC/JPEGImages" out_dir = "./YOLO/labels" os.makedirs(out_dir, exist_ok=True) classes = ["water"] # 单类别,索引为 0 for xml_file in os.listdir(voc_dir): if not xml_file.endswith(".xml"): continue tree = ET.parse(os.path.join(voc_dir, xml_file)) root = tree.getroot() filename = root.find("filename").text img_path = os.path.join(img_dir, filename) img = Image.open(img_path) w, h = img.size lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in classes: continue cls_id = classes.index(name) bbox = obj.find("bndbox") xmin = float(bbox.find("xmin").text) ymin = float(bbox.find("ymin").text) xmax = float(bbox.find("xmax").text) ymax = float(bbox.find("ymax").text) # 裁剪到图片范围内,防止越界 xmin = max(0, min(xmin, w)) xmax = max(0, min(xmax, w)) ymin = max(0, min(ymin, h)) ymax = max(0, min(ymax, h)) cx = (xmin + xmax) / 2.0 / w cy = (ymin + ymax) / 2.0 / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") txt_name = os.path.splitext(filename)[0] + ".txt" with open(os.path.join(out_dir, txt_name), "w") as f: f.write("\n".join(lines))四个边界坑需要特别注意。第一,xmin和xmax可能因为标注工具的问题出现浮点数,直接取整会丢精度,这里用 float 处理。第二,如果标注框超出图片边界,归一化后会出现大于 1 或小于 0 的值,YOLO 训练时虽然不会立刻报错,但会影响回归损失,所以脚本里做了裁剪。第三,图片文件名和 XML 里的filename字段可能不一致,比如大小写或者扩展名不同,脚本用 XML 里的字段去拼路径,如果找不到图片会直接抛异常,建议先跑一遍 2.2 的校验。第四,类别名如果有空格或者中文,映射到索引时容易出错,这里统一用英文water,如果你的数据集里类别名不同,改classes列表即可。
2.4 生成训练集与验证集的划分文件
YOLO 训练需要两个 txt 文件,分别列出训练图和验证图的路径。357 张按 8:2 划分,训练集 285 张,验证集 72 张。下面这段脚本随机打乱后生成train.txt和val.txt。
import os import random img_dir = "./VOC/JPEGImages" out_dir = "./YOLO" os.makedirs(out_dir, exist_ok=True) imgs = [f for f in os.listdir(img_dir) if f.lower().endswith((".jpg", ".png", ".jpeg"))] random.seed(42) random.shuffle(imgs) split = int(len(imgs) * 0.8) train_imgs = imgs[:split] val_imgs = imgs[split:] with open(os.path.join(out_dir, "train.txt"), "w") as f: for name in train_imgs: f.write(os.path.abspath(os.path.join(img_dir, name)) + "\n") with open(os.path.join(out_dir, "val.txt"), "w") as f: for name in val_imgs: f.write(os.path.abspath(os.path.join(img_dir, name)) + "\n") print(f"训练集 {len(train_imgs)} 张,验证集 {len(val_imgs)} 张")random.seed(42)保证每次划分结果一致,方便复现。路径用绝对路径,避免训练时因为工作目录不同找不到图片。如果你的图片分散在多个子文件夹,把os.listdir换成os.walk递归收集即可。
3. 用 YOLOv8 在 357 张图上跑通训练:配置文件与关键参数
3.1 数据集 YAML 怎么写才不出错
YOLOv8 需要一个 YAML 文件告诉它去哪里找图片和标签。在项目根目录新建water.yaml,内容如下:
path: /absolute/path/to/dataset train: YOLO/train.txt val: YOLO/val.txt names: 0: waterpath是数据集根目录的绝对路径,train和val是相对于path的 txt 文件路径。names里类别索引从 0 开始,和转换脚本里的classes列表顺序一致。常见错误是path写了相对路径,训练时从其他目录启动就找不到文件;或者names里写了中文,YOLOv8 读取时编码报错。单类别场景下,nc不需要单独写,YOLOv8 会根据names的长度自动推断。
3.2 训练命令与 batch size 的选择
357 张图属于小数据集,显存压力不大。如果你用的是 8GB 显存的卡,batch=16比较稳;4GB 显存降到batch=8。下面这条命令从预训练权重开始微调:
yolo detect train \ data=water.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=20 \ project=runs/water \ name=exp1model=yolov8n.pt是最小的 nano 版本,357 张图用 nano 足够,参数量小、训练快,过拟合风险也低。epochs=100配合patience=20,如果 20 轮验证指标没提升就提前停止,避免无效计算。imgsz=640是 YOLOv8 的默认输入尺寸,如果你的图片分辨率远大于 640,比如 1920x1080,可以保持 640,让框架自动缩放;如果积水区域在图中占比很小,比如远距离摄像头拍的地面,可以尝试imgsz=1280,但显存占用会翻倍。lr0=0.01是初始学习率,小数据集上如果 loss 震荡厉害,降到 0.001 再试。
3.3 训练过程中该盯哪些指标
YOLOv8 训练时会在终端输出每一轮的 box_loss、cls_loss、dfl_loss 和 mAP50。357 张图、单类别,正常情况下 mAP50 在前 30 轮就能爬到 0.7 以上。如果 50 轮后还在 0.3 附近晃,大概率是标注有问题或者学习率太大。box_loss 反映边界框回归误差,cls_loss 反映分类误差,dfl_loss 是分布焦点损失,三个一起看:box_loss 下降但 cls_loss 不降,说明框的位置学得还行但类别分不清,单类别场景下 cls_loss 理论上应该很快接近 0;如果 cls_loss 一直很高,检查 YAML 里的names和 txt 里的类别索引是否对得上。
验证集 mAP50 是最终参考指标,但小数据集上波动大,不要因为某一轮掉了 0.05 就停。看趋势,连续 10 轮没有新高再考虑调参。训练结束后,runs/water/exp1/weights/best.pt就是验证集上表现最好的权重。
4. 室内积水检测的避坑与排查:反光、小目标、过拟合
4.1 地面反光被误检成积水
现象:模型在验证集上 mAP 不错,但实际推理时把光滑地面的反光、瓷砖高光、甚至白色地板都框成积水。原因:357 张图里如果反光样本和积水样本外观接近,模型学到的特征可能是“亮色区域”而不是“水的纹理”。解决:在标注阶段就把明显反光的非积水区域标成背景,或者收集一批纯反光负样本加入训练集。推理时把置信度阈值从默认 0.25 提到 0.5,能过滤掉一部分低置信误检。
4.2 小面积积水在 640 输入下丢失
现象:图片里积水区域只占几十个像素,训练后模型完全检测不到。原因:YOLOv8 在 640 输入下,特征图最小 stride 是 32,对应原图 32x32 像素的区域,如果积水小于这个尺寸,特征图上就没了。解决:把imgsz提到 1280,或者用 YOLOv8 的 P2 变体(如果框架支持),增加浅层特征图。另一个办法是在数据增强里关掉mosaic,因为 mosaic 会把四张图拼成一张,小目标进一步缩小。
4.3 训练 loss 正常但验证 mAP 为 0
现象:训练日志里 box_loss 在降,但验证集 mAP50 一直是 0。原因:最常见的是val.txt里的图片路径和train.txt不一致,或者验证集的标签文件没有放到 YOLO 期望的目录结构里。YOLOv8 默认从图片路径推导标签路径,把images替换成labels,如果你的目录不是这个结构,需要在 YAML 里显式指定val的标签路径。解决:检查val.txt里的路径是否存在,标签 txt 是否和图片同名同目录层级。
4.4 过拟合:训练 mAP 0.95,验证 mAP 0.6
现象:训练集上指标很高,验证集差一大截。原因:357 张图对 YOLO 来说偏少,模型容易记住训练样本。解决:开启数据增强,YOLOv8 默认已经带了 mosaic、HSV 抖动、随机翻转,可以额外加degrees=10做小角度旋转,translate=0.1做平移。如果还不行,换更小的模型,比如 yolov8n 换成 yolov8n 的剪枝版本,或者直接减少训练轮数,在验证 mAP 最高点停。
4.5 推理时图片尺寸和训练不一致导致漏检
现象:训练时用 640,推理时直接喂原图 1920x1080,结果什么都检测不到。原因:YOLO 推理时会自动缩放,但缩放后的长边如果远大于 640,小目标会被压缩到消失。解决:推理时显式指定imgsz=640,让框架按训练时的尺寸处理。如果原图分辨率很高,先把图缩到 1280 再推理,或者用滑动窗口切图检测。
5. 小数据集的进阶玩法:从 357 张里榨出更多信息
357 张图训一个单类别检测器,上限就在那里。想让模型真正可用,得在数据层面做文章。我一般会做两件事:一是用训练好的模型去跑一批未标注的室内地面图,把高置信度的检测结果导出来,人工修正后加入训练集,这叫自训练或者伪标签,能把有效样本量翻两三倍;二是把检测框裁出来,单独训一个分类器判断“真积水”还是“反光”,检测器负责召回,分类器负责精度,两级级联在误检率上比单模型好很多。
验证模型是否真的学到了积水特征,而不是记住了背景,有个简单办法:把验证集图片的地面区域裁出来,贴到一张纯色背景上再推理。如果模型还能框出来,说明它关注的是水本身的纹理;如果框不出来了,说明它依赖的是地面上下文,换个房间就可能翻车。这个测试我每次做完小数据集都会跑一遍,翻车次数不少,但每次都能找到该补什么样的数据。
最后一个习惯:不管数据集多小,训练前一定把train.txt和val.txt里的图片路径打印出来,随机抽几张用画框脚本可视化一遍。我见过太多次因为路径写错、标签错位、类别索引对不上导致的白跑一晚上。357 张图的可视化也就几分钟,但这几分钟能帮你确认标注框是不是真的落在积水区域上。希望帮到你。
本文还有配套的精品资源,点击获取