简介:面向厨房场景的积水检测数据集,标注类别为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 厨房积水检测数据集VOC+YOLO格式88张2类别.7z cd 厨房积水检测数据集VOC+YOLO格式88张2类别 tree -L 3逻辑说明:tree输出里,JPEGImages放原图,Annotations放每个图对应的XML,ImageSets/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坐标有没有超出图片宽高的异常值;二是difficult=1的框占比高不高。如果一多半目标都标了 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_ok=True) 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:小数据集的三角组合
| 参数 | 建议值 | 理由 |
|---|---|---|
| imgsz | 640 | 积水目标占图比例小,但640已够小样本收敛,再大容易过拟合 |
| batch | 8 ~ 16 | 88张图batch=16约5个step一个epoch,梯度更新太快反而抖动 |
| epochs | 200 | 配合早停,实际能跑满100轮就不错 |
| patience | 30 | 验证指标30轮不涨就停,避免小数据集过度训练 |
训练命令长这样:
yolo detect train \ model=yolov8n.pt \ data=datasets/kitchen_water/data.yaml \ epochs=200 \ imgsz=640 \ batch=16 \ patience=30 \ mosaic=0.3 \ fliplr=0.5 \ hsv_h=0.03 \ amp=True逻辑说明:batch=16对88张图的训练集来说,一个epoch只有5个batch。batch越小,BN层的统计量越不稳定,所以别为了显存把batch压到2或4,那样损失曲线会上下乱跳,黑匣子问题会更严重。如果显卡显存只有6G,优先把batch降到8,而不是把imgsz降到416。因为积水是小目标,分辨率低了特征直接消失。
参数说明:amp=True是混合精度训练,16G显存开8到16的batch基本无压力。训练时盯两个文件:runs/detect/train/下的results.png和weights/best.pt。best.pt是按验证集mAP挑的,不是最后一个epoch,这点靠patience机制保证。
4.2 mosaic与flip:厨房积水场景里增强开关怎么开
YOLOv8默认mosaic=1.0,但对小样本数据集来说,mosaic比例太高会让模型看到太多「拼接厨房」的假样本,学到的纹理跟真实积水对不上。我一般把mosaic压到0.2到0.4之间,让模型每轮有六成概率看到干净原图。fliplr=0.5可以放心开,积水区域物理上左右对称,水滴形状翻转后依然是合理样本。
hsv_h=0.03是个保守值。厨房灯光普遍偏暖黄,积水反光带点蓝绿色,hsv扰动太大会把水的颜色改到离谱,反而削弱类别区分度。相比之下hsv_s和hsv_v各留0.5左右即可,它们模拟的是不同时段灯光亮度和色饱和度的变化。
4.3 预训练权重与freeze:88张图没有资格从零训练
小样本检测圈里有个共识:数据少就不要自己当艺术家。用COCO预训练的yolov8n.pt做起点,哪怕厨房场景和COCO的物体分布差很远,前几层学到的边缘、纹理、基本形状特征依然能迁移。yolov8n参数量最小,对88张训练集来说是最不容易过拟合的选择;yolov8s或更大模型在小样本下验证集mAP反而可能更低。
要不要freeze前10层?我的结论是:不需要。88张图的训练量下,模型就算想破坏预训练特征也没有足够的梯度,开不开freeze差别不大。真觉得过拟合了,优先加weight_decay=0.0005和dropout=0.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的图,xmin=100, xmax=300,那么x_center=(100+300)/2/640=0.3125,而不是(300-100)/640=0.3125——后者算出来的是宽度,两个值在小框上恰好接近,大框上差得离谱。写脚本时把这两行分开打印核对一眼。
5.4 显卡显存OOM:batch和imgsz的平方关系
现象:batch=16, imgsz=640一启动就报CUDA out of memory,把batch降到8还是崩。
原因:显存占用和imgsz是平方关系,640的分辨率是416的约2.4倍显存占用。很多人只降batch不降imgsz,结果依然OOM。还有个隐蔽原因是yolov8m以上模型的参数量翻倍,88张图的场景选n就够了,没必要用大模型。
解决:用nvidia-smi -l 1看显存实际占用,先设batch=8, imgsz=640, amp=True跑一次,如果剩余显存还低于50%,再降imgsz=544。积水目标虽然小,但544分辨率配合预训练权重依然可用,总比训练都跑不起来强。
5.5 推理时检测框抖、置信度乱跳
现象:模型训练完mAP看着不错,放到真实厨房视频里,同一块积水时有时无,置信度在0.3到0.7之间跳。
原因:厨房地面反光、不锈钢水槽倒影都会产生与积水纹理相近的特征。训练集里这类负样本太少,模型没学会「看起来像水但不是水」的反例。推理时置信度阈值设太低,大量误检框被保留。
解决:推理时把conf调到0.35~0.5,iou保持0.45。如果还是抖,加一个后处理规则:只保留检测框与预置ROI区域交集大于50%的目标。比如水槽下方柜底区域固定一个ROI,框中心落在ROI外面直接丢弃,这比调模型参数更直接。
6. 把积水检测模型压到实时推理:TensorRT导出与现场验证
训练完拿到best.pt,下一步是在边缘设备上跑实时视频流。我习惯先把模型导出成ONNX再转TensorRT,这一步骤对积水检测这种小目标场景损失很小,但帧率提升明显。
yolo export model=runs/detect/train/weights/best.pt format=onnx imgsz=640 trtexec --onnx=best.onnx --saveEngine=best_fp16.engine --fp16 --workspace=2048参数说明:--fp16对积水检测的影响比想象中小,因为水的边缘和纹理相对简单,半精度损失的推理精度在mAP上波动不到1%,换来的速度翻倍很值。--workspace=2048限定了显存缓冲上限,设备显存按实际调整。显存紧张的板子可以只做FP16不做INT8,INT8量化在小目标场景容易把微弱的水迹特征量化掉,是一个需要反复校准的活,投入产出比不高。
验证阶段我跑一段真实厨房视频,统计检测框的稳定性。做法很简单:每帧跑一次推理,把置信度大于0.4的框画到画面上,录30秒回放。重点看三处:水槽下方、燃气灶台面、冰箱底部。地面瓷砖反光区域如果出现频繁闪烁的框,说明误检集中在那里,回到第5.5节的ROI过滤做剔除。
我落地这类小样本检测项目的一个习惯是保留最近30秒的原始视频帧,告警触发后立刻能人工回看,修复误报或漏报都比对着日志猜快得多。模型本身是个黑匣子,现场数据才是校正它的唯一依据。希望这份从解包到部署的流程,能让你的厨房积水检测少走几段弯路,也帮你在别的视觉检测场景里更快做出判断——数据少不可怕,怕的是格式没理清就急着开训。
本文还有配套的精品资源,点击获取