
简介面向厨房场景的积水检测数据集标注类别为water积水与foam泡沫可支撑YOLO、Faster R-CNN等主流目标检测模型的训练、验证与算法对比。数据集同时提供Pascal VOC和YOLO两种标注格式能覆盖常用训练框架的输入需求适合计算机视觉学习者、算法工程师以及智慧厨房、漏水监测等场景的预研与部署。压缩包共268个文件其中90个txt含88个YOLO格式标注、88个VOC格式xml、88张jpg图像及2个ini文件整体大小18.23MB便于快速下载与解压使用已有161人学习下载。全部标注由labelImg画矩形框完成water共290个框、foam共272个框累计562个真实目标框利用这套数据可以练习标注格式转换、训练脚本调试、mAP评估与数据增强等完整流程也可作为室内积水预警任务的基准训练集。特别说明数据集只保证标注准确合理不对模型精度作担保适合读者自行建立评测基线。1. 厨房积水检测数据集88张双格式图能不能撑起一个能上线的检测模型厨房积水检测数据集听起来是个小到不能再小的题目88张图、2个类别、VOC和YOLO双格式一起打包。可把它当成一个真实检测项目来做你会发现它踩中的是目标检测里最典型的一类痛——样本极少、目标很小、反光干扰重。水槽下方柜体漏水、洗碗机底部渗水、冰箱融霜水漫出来等肉眼发现时柜板往往已经泡废用固定摄像头加一个轻量模型做积水告警是不少智能家居和工程巡检团队验证过的实用路线。这篇笔记就从解包、查标签、训练、排错到实时推理一路走下来帮你判断这份数据集能不能用、怎么用、坑在哪。2. 拆开VOC与YOLO格式的标注目录结构、XML字段和txt换算逻辑2.1 先看目录VOC的Annotations、JPEGImages与ImageSets缺一不可拿到 .7z 先解压别急着训练先确认目录树是不是标准的VOC布局。常见做法是解压后直接得到VOCdevkit/VOC2007/下面有Annotations、JPEGImages、ImageSets/Main三个目录。YOLO版数据则通常放在labels/或yolo_labels/与图片一一对应。用下面命令把结构拉出来看一眼7z x 厨房积水检测数据集VOCYOLO格式88张2类别.7z cd 厨房积水检测数据集VOCYOLO格式88张2类别 tree -L 3逻辑说明tree输出里JPEGImages放原图Annotations放每个图对应的XMLImageSets/Main里是train.txt/val.txt这类索引文件。如果解压后缺少ImageSets自己生成也很简单把图片文件名按8:2随机拆成训练和验证两份即可后面训练脚本只认这个名字列表。参数说明.7z用 7-Zip 解压Linux 服务器上先装p7zip-full否则7z命令不存在。解压后注意看一下图片后缀常见是.jpg少数可能是.png这会影响后面匹配标签的代码先在JPEGImages里ls确认一遍再写脚本。2.2 XML里的bndbox与difficult标注字段逐个读VOC格式的XML核心是object节点里面name是类别名bndbox是四个角点坐标difficult表示该目标是否不易辨认。厨房场景里水渍边界模糊difficult标注经常出现训练时应该过滤掉。用一段小脚本把某个XML完整读出来看看import xml.etree.ElementTree as ET tree ET.parse(VOCdevkit/VOC2007/Annotations/kitchen_001.xml) root tree.getroot() size root.find(size) print(图片宽高:, size.find(width).text, size.find(height).text) for obj in root.findall(object): name obj.find(name).text difficult obj.find(difficult).text 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) print(f{name}, difficult{difficult}, bbox({xmin:.0f},{ymin:.0f},{xmax:.0f},{ymax:.0f}))逻辑说明这里把XML的size和每个目标的类别、困难标记、坐标一起打出来目的是确认两点一是bndbox坐标有没有超出图片宽高的异常值二是difficult1的框占比高不高。如果一多半目标都标了 difficult这批标注的质量反而值得怀疑后面做验证集要小心。参数说明difficult在目标检测里是个老概念VOC官方建议难例不参与训练评估。YOLO 的 txt 标签没有这个字段所以转格式时要么丢掉它们要么单独保留一份难例列表用于后续难例挖掘。对88张的小数据集我的习惯是直接过滤避免它把损失函数拉偏。2.3 YOLO的txt标签五列数据与归一化坐标YOLO格式的每个txt对应一张图每一行是class_id x_center y_center width height坐标全部除以图片宽高做了归一化。随便打开一个标签文件看内容cat labels/kitchen_001.txt假设输出是0 0.682031 0.435937 0.121875 0.078125含义就是类别0目标中心点在图片横向68.2%、纵向43.6%的位置宽度占12.2%高度占7.8%。从VOC的角点格式转过来的公式是x_center ((xmin xmax) / 2) / image_width y_center ((ymin ymax) / 2) / image_height box_width (xmax - xmin) / image_width box_height (ymax - ymin) / image_height参数说明归一化坐标必须是 0 到 1 之间的浮点数一旦出现 1.05 这种越界值YOLO 训练会自动忽略该框作为负样本处理表现就是损失正常下降但 mAP 始终为0。这在小数据集上非常容易翻车所以第3章的转换脚本里专门做越界裁剪。2.4 用小脚本统计88张图的类别分布与目标尺寸88张图2个类别如果两类框数差别悬殊比如积水35张、水渍53张就要考虑类别不平衡。另外积水在厨房地面往往是细长条或小块和轴承缺陷检测里的微小裂纹类似小目标占比高时增强策略和损失权重都要跟着调。写个统计脚本一次看清楚import xml.etree.ElementTree as ET from pathlib import Path from collections import Counter xml_dir Path(VOCdevkit/VOC2007/Annotations) class_counter Counter() box_sizes [] image_wh [] for xml_file in xml_dir.glob(*.xml): root ET.parse(xml_file).getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) image_wh.append((img_w, img_h)) for obj in root.findall(object): if int(obj.find(difficult).text or 0) 1: continue name obj.find(name).text class_counter[name] 1 box obj.find(bndbox) xmin float(box.find(xmin).text) xmax float(box.find(xmax).text) ymin float(box.find(ymin).text) ymax float(box.find(ymax).text) box_area (xmax - xmin) * (ymax - ymin) box_sizes.append(box_area) print(类别框数统计:, dict(class_counter)) print(图片尺寸集合:, set(image_wh)) print(目标面积均值:, sum(box_sizes)/len(box_sizes), 最大面积:, max(box_sizes))逻辑说明这个脚本把类别数量、图片统一尺寸、目标面积均值三个关键指标一次性算出来。如果图片尺寸不统一后面data.yaml里的imgsz就要考虑是否先做缩放如果框面积普遍不到整图面积的5%说明小目标集中需要把mosaic和hsv增强调到合适档位而不是默认值一把梭。3. 从VOC到YOLO的转换脚本、目录摆放与四个边界坑这份数据集本身就带双格式多数情况下用现成的labels/目录就能直接开训。但只要你想把VOC部分合进自己的训练管线或者想把Annotations里那些difficult做一次清洗后再训练就绕不开自己写转换脚本。常见的坑有四个我一个个说。3.1 目录摆放YOLO训练目录的train/val拆分与data.yaml写法先把数据按YOLO风格整理最终目录结构长这样datasets/kitchen_water/ ├── data.yaml ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/data.yaml是这个数据集的配置文件训练时yolo detect train直接指定它。注意path一定要写对如果YOLO命令行在别处运行用绝对路径最稳。names的顺序就是类别编号的顺序txt里写0就对应names第一项写1对应第二项顺序绝对不能乱。# datasets/kitchen_water/data.yaml path: /home/user/datasets/kitchen_water train: images/train val: images/val names: 0: water_puddle 1: wet_stain逻辑说明train和val的值是相对于path的路径。YOLOv8会去images/train找图然后自动到labels/train找同名txt。它通过把images替换成labels的方式定位标签所以图片和标签必须同名同前缀图片叫kitchen_001.jpg标签就必须叫kitchen_001.txt不能有后缀差异。参数说明VOC的ImageSets/Main里可能已经有官方划分好的train.txt、val.txt如果解压包里带了直接按它的清单拷贝图片和标签到对应目录没带就自己随机划分。对88张的规模8:2拆分train约70张、val约18张验证集偏小我建议后面训练时在val上多跑几次取均值而不是信任单次结果。3.2 VOC转YOLO的完整脚本解析XML到生成txt下面这个脚本是我在多个小样本检测项目里改出来的通用版有四个关键处理越界裁剪、difficult过滤、空标注兜底、类别映射校验。直接保存为voc2yolo.py运行。import os import xml.etree.ElementTree as ET from pathlib import Path CLASSES [water_puddle, wet_stain] # 与 data.yaml 的 names 顺序严格一致 def voc_to_yolo(xml_path, img_w, img_h): 把单个VOC XML转成YOLO格式行返回字符串列表 root ET.parse(xml_path).getroot() lines [] for obj in root.findall(object): name obj.find(name).text.strip() if name not in CLASSES: continue # 遇到未注册类别直接跳过 cls_id CLASSES.index(name) difficult int(obj.find(difficult).text or 0) if difficult 1: continue # 难例不参与训练 box obj.find(bndbox) xmin max(0.0, float(box.find(xmin).text)) ymin max(0.0, float(box.find(ymin).text)) xmax min(img_w, float(box.find(xmax).text)) ymax min(img_h, float(box.find(ymax).text)) if xmax xmin or ymax ymin: continue # 裁剪后无效框丢弃 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 lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) return lines def main(voc_root, out_root): annotations Path(voc_root) / Annotations images Path(voc_root) / JPEGImages os.makedirs(out_root, exist_okTrue) for xml_file in sorted(annotations.glob(*.xml)): # 读取同名词图片拿到真实尺寸 img_file images / f{xml_file.stem}.jpg if not img_file.exists(): print(f警告: 图片不存在 {img_file}) continue import cv2 h, w cv2.imread(str(img_file)).shape[:2] lines voc_to_yolo(str(xml_file), w, h) out_label Path(out_root) / f{xml_file.stem}.txt with open(out_label, w) as f: f.write(\n.join(lines)) # 空标注也生成空txt if __name__ __main__: main(VOCdevkit/VOC2007, labels_all)逻辑说明脚本里的铁律是「图片是唯一真相」。XML里写的size未必和真实图片一致有些标注工具导出时会写错所以这里直接用cv2.imread读取图片实际宽高再对坐标做归一化和越界裁剪。difficult直接丢弃没有第二个选项。最后的out_label即使为空也写入文件空txt会让YOLO认为该图没有目标总比找不到标签直接报错强。参数说明CLASSES列表必须和data.yaml的names完全一致。如果你解压后看到classes.txt以它为准把上面两个地方同步改掉。这个脚本只处理.jpg如果解压出来是.png把img_file里的后缀换成.png。3.3 转换后的验证一条命令核对bbox是否错位转完格式不能直接开训先把标签画回图上肉眼看几张这一步能避免大多数「标签在、框不对」的玄学问题。用OpenCV把YOLO的归一化坐标还原成像素框叠加在原图上import cv2 def draw_yolo_label(img_path, label_path, class_names): img cv2.imread(str(img_path)) h, w img.shape[:2] with open(label_path) as f: for line in f: cls_id, xc, yc, bw, bh map(float, line.split()) x1 int((xc - bw / 2) * w) y1 int((yc - bh / 2) * h) x2 int((xc bw / 2) * w) y2 int((yc bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, class_names[int(cls_id)], (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) cv2.imwrite(check_ Path(img_path).name, img) class_names [water_puddle, wet_stain] draw_yolo_label(images/val/kitchen_001.jpg, labels/val/kitchen_001.txt, class_names)逻辑说明这段代码的核心是坐标还原时的括号顺序——xc - bw/2用的是中心点减半宽如果误写成xc - bw框会整体偏移一个目标宽度看起来像标注错位。验证时挑积水地面的图、挑有反光的图各看几张确认积水区域边界和box贴合。4. 88张小样本的YOLOv8训练参数六个必调的数值和它们的极限用YOLOv8训练自己的厨房积水数据模型结构不用纠结直接用yolov8n.pt起步。真正决定成败的是6个参数imgsz、batch、epochs、mosaic、fliplr、patience。下面是88张图场景下我常用的配置和理由。4.1 imgsz、batch、epochs小数据集的三角组合参数建议值理由imgsz640积水目标占图比例小但640已够小样本收敛再大容易过拟合batch8 ~ 1688张图batch16约5个step一个epoch梯度更新太快反而抖动epochs200配合早停实际能跑满100轮就不错patience30验证指标30轮不涨就停避免小数据集过度训练训练命令长这样yolo detect train \ modelyolov8n.pt \ datadatasets/kitchen_water/data.yaml \ epochs200 \ imgsz640 \ batch16 \ patience30 \ mosaic0.3 \ fliplr0.5 \ hsv_h0.03 \ ampTrue逻辑说明batch16对88张图的训练集来说一个epoch只有5个batch。batch越小BN层的统计量越不稳定所以别为了显存把batch压到2或4那样损失曲线会上下乱跳黑匣子问题会更严重。如果显卡显存只有6G优先把batch降到8而不是把imgsz降到416。因为积水是小目标分辨率低了特征直接消失。参数说明ampTrue是混合精度训练16G显存开8到16的batch基本无压力。训练时盯两个文件runs/detect/train/下的results.png和weights/best.pt。best.pt是按验证集mAP挑的不是最后一个epoch这点靠patience机制保证。4.2 mosaic与flip厨房积水场景里增强开关怎么开YOLOv8默认mosaic1.0但对小样本数据集来说mosaic比例太高会让模型看到太多「拼接厨房」的假样本学到的纹理跟真实积水对不上。我一般把mosaic压到0.2到0.4之间让模型每轮有六成概率看到干净原图。fliplr0.5可以放心开积水区域物理上左右对称水滴形状翻转后依然是合理样本。hsv_h0.03是个保守值。厨房灯光普遍偏暖黄积水反光带点蓝绿色hsv扰动太大会把水的颜色改到离谱反而削弱类别区分度。相比之下hsv_s和hsv_v各留0.5左右即可它们模拟的是不同时段灯光亮度和色饱和度的变化。4.3 预训练权重与freeze88张图没有资格从零训练小样本检测圈里有个共识数据少就不要自己当艺术家。用COCO预训练的yolov8n.pt做起点哪怕厨房场景和COCO的物体分布差很远前几层学到的边缘、纹理、基本形状特征依然能迁移。yolov8n参数量最小对88张训练集来说是最不容易过拟合的选择yolov8s或更大模型在小样本下验证集mAP反而可能更低。要不要freeze前10层我的结论是不需要。88张图的训练量下模型就算想破坏预训练特征也没有足够的梯度开不开freeze差别不大。真觉得过拟合了优先加weight_decay0.0005和dropout0.1这比freeze有效得多。4.4 类别不平衡的现实积水框多、水渍框少的处理先用第2.4节的统计脚本看两个类别的框数比。如果是1:1左右正常训练即可如果某个类的框数是另一个的2倍以上给损失函数加类别权重在data.yaml里给尾部追加一行比如weight: 1.5或更直接的办法是从另一个类里复制部分样本做离线增强。对小样本数据集我更推荐后者——几十张图复制成本极低但能让模型在反向传播时看到平衡的类别分布。# 类别不平衡时在 data.yaml 末尾追加 # 表示 wet_stain 类别的损失权重放大 1.5 倍 weight: 0: 1.0 1: 1.5逻辑说明YOLOv8的损失函数由box_loss、cls_loss、dfl_loss三项加权组成类别权重主要放大分类损失。对积水检测这个任务漏检的代价是水泡柜体误报的代价只是多推一条告警所以我更倾向让模型偏向召回权重调好后看recall指标而不是死盯mAP。5. 厨房积水检测训练与推理避坑五个常见报错和结果翻车点5.1 报错「No labels in data.yaml」标签路径与图片文件名错位现象训练启动后YOLO直接抛AssertionError: train: No labels in ...或者提示标签目录为空。原因最常见的是images/train里的文件名带中文或空格比如厨房积水_01.jpg。YOLOv8在定位标签时按basename做匹配它把厨房_01当成label名但标签是英文或纯数字名对不上。另一个原因是我把data.yaml的train写成了images/train/带斜杠路径拼接后找不到目录。解决把图片统一重命名为英文小写basename再跑一次转换脚本重新生成标签。命令级排查看三样find datasets/kitchen_water/images/train -name *.jpg | wc -l find datasets/kitchen_water/labels/train -name *.txt | wc -l find datasets/kitchen_water/labels/train -name *.txt -size 0 | head -20逻辑说明第一行统计图片数第二行统计标签数第三行列出空标签。图片和标签数不一致基本就是文件名匹配失败。空标签出现少量没问题但如果超过20%说明大量标注被difficult过滤或XML解析漏了回头查第3.2节的脚本。5.2 训练loss正常下降但mAP一直为0现象训练日志里box_loss、cls_loss都平稳下降但每个epoch结束验证集mAP打印出来就是0。原因这是小数据集最容易翻车的地方。通常有三个来源一是txt里的类别号写成了1, 2而YOLO要求从0开始模型把所有目标当成背景二是转换脚本里归一化坐标越界且没有裁剪导致验证时标签被当作无效框丢掉三是验证集图片有图无标签。解决把验证集某个txt打印出来检查类别号和坐标范围。然后用第3.3节的画框脚本把验证集标签画回去肉眼确认不是全空白。5.3 目标框画出来偏半个身位现象用3.3节的验证脚本画框发现框跟积水实际位置错开偏左上或右下不是贴边而是整体平移。原因VOC的bndbox坐标是像素角点YOLO是中心点加宽高的归一化值。转换时如果xmin xmax漏了除以2或者把宽度误当成xmax - xmin用了未除以img_w的原始值就会整体偏移。解决转换公式不要背直接对照手工算一张一张640×480的图xmin100, xmax300那么x_center(100300)/2/6400.3125而不是(300-100)/6400.3125——后者算出来的是宽度两个值在小框上恰好接近大框上差得离谱。写脚本时把这两行分开打印核对一眼。5.4 显卡显存OOMbatch和imgsz的平方关系现象batch16, imgsz640一启动就报CUDA out of memory把batch降到8还是崩。原因显存占用和imgsz是平方关系640的分辨率是416的约2.4倍显存占用。很多人只降batch不降imgsz结果依然OOM。还有个隐蔽原因是yolov8m以上模型的参数量翻倍88张图的场景选n就够了没必要用大模型。解决用nvidia-smi -l 1看显存实际占用先设batch8, imgsz640, ampTrue跑一次如果剩余显存还低于50%再降imgsz544。积水目标虽然小但544分辨率配合预训练权重依然可用总比训练都跑不起来强。5.5 推理时检测框抖、置信度乱跳现象模型训练完mAP看着不错放到真实厨房视频里同一块积水时有时无置信度在0.3到0.7之间跳。原因厨房地面反光、不锈钢水槽倒影都会产生与积水纹理相近的特征。训练集里这类负样本太少模型没学会「看起来像水但不是水」的反例。推理时置信度阈值设太低大量误检框被保留。解决推理时把conf调到0.35~0.5iou保持0.45。如果还是抖加一个后处理规则只保留检测框与预置ROI区域交集大于50%的目标。比如水槽下方柜底区域固定一个ROI框中心落在ROI外面直接丢弃这比调模型参数更直接。6. 把积水检测模型压到实时推理TensorRT导出与现场验证训练完拿到best.pt下一步是在边缘设备上跑实时视频流。我习惯先把模型导出成ONNX再转TensorRT这一步骤对积水检测这种小目标场景损失很小但帧率提升明显。yolo export modelruns/detect/train/weights/best.pt formatonnx imgsz640 trtexec --onnxbest.onnx --saveEnginebest_fp16.engine --fp16 --workspace2048参数说明--fp16对积水检测的影响比想象中小因为水的边缘和纹理相对简单半精度损失的推理精度在mAP上波动不到1%换来的速度翻倍很值。--workspace2048限定了显存缓冲上限设备显存按实际调整。显存紧张的板子可以只做FP16不做INT8INT8量化在小目标场景容易把微弱的水迹特征量化掉是一个需要反复校准的活投入产出比不高。验证阶段我跑一段真实厨房视频统计检测框的稳定性。做法很简单每帧跑一次推理把置信度大于0.4的框画到画面上录30秒回放。重点看三处水槽下方、燃气灶台面、冰箱底部。地面瓷砖反光区域如果出现频繁闪烁的框说明误检集中在那里回到第5.5节的ROI过滤做剔除。我落地这类小样本检测项目的一个习惯是保留最近30秒的原始视频帧告警触发后立刻能人工回看修复误报或漏报都比对着日志猜快得多。模型本身是个黑匣子现场数据才是校正它的唯一依据。希望这份从解包到部署的流程能让你的厨房积水检测少走几段弯路也帮你在别的视觉检测场景里更快做出判断——数据少不可怕怕的是格式没理清就急着开训。本文还有配套的精品资源点击获取