简介:水污染检测数据集以VOC与YOLO双格式发布,包含2487张已标注jpg图片及对应xml/txt标注文件,不包含图像分割路径信息,面向从事环境监测、目标检测算法研究的开发者与学习者,适合用于训练水质分类与污染识别模型。数据集中共标注2489个矩形框,涵盖浑水、污染、杂物、正常四个类别,各类别框数分别为567、746、587、589,类别顺序以labels/classes.txt为准,可满足目标检测模型训练与验证需求。标注工具为labelImg,规则是对不同类别画矩形框,同时提供格式兼容性说明,便于直接接入YOLO、Faster R-CNN等主流深度学习框架。压缩包仅含1个docx文档,大小2.86MB,文件类型单一,内容涵盖数据说明、格式差异提示与使用限制。目前已有82人浏览学习,适合需要快速获取规范数据集并开展水污染检测实验的入门及进阶用户;资源可帮助使用者结合具体场景评估数据适用性,为后续模型训练、算法验证及环境监测应用提供基础数据支撑。
1. 水污染检测数据集:VOC和YOLO双格式到底解决什么问题
做水污染检测的视觉项目,很多人第一步就卡在数据集上:要么从公开渠道找到一批水质图像,但标注格式是老的VOC XML;要么自己用LabelImg标了一批图,导出时又搞不清该存成哪个目录结构。我见过不少团队在这个环节耗掉两三周,最后却因为训练时类别ID和标注框坐标对不上,模型跑出来的效果跟随机猜测差不多。这份2487张、4类别、同时提供VOC和YOLO两种格式的水污染检测数据集,价值在于它把「标注格式迁移」这个最磨人的环节直接省掉了——拿到手就能用YOLO系列直接训练,也能退回VOC管道做经典检测器实验。
这里面的实际意义很具体:2487张图不算多,但对于4类水污染目标的检测任务,配合数据增强和预训练权重,足够把一个可跑的基线模型拉起来。4个类别覆盖了水污染场景中最常被关注的几类目标,比如水面漂浮垃圾、油污区域、藻类爆发、废水排放口等。对想快速验证算法效果、做课程设计、或者在企业内部做可行性评估的开发者来说,这套双格式数据集意味着你不用再写一遍格式转换的胶水代码,直接可以开始调参。
适合谁读?想用YOLOv5/YOLOv8跑水污染检测的开发者、需要做数据集格式转换但不想踩坑的新手、以及正准备评估这个方向值不值得投入的技术负责人。下面我从格式结构、转换脚本、训练参数到常见坑位,把这条路完整走一遍。
2. 先看懂VOC格式:2487张图的目录结构、XML标注与4个类别配置
2.1 VOC标准目录和这份数据集的对应关系
VOC格式来自目标检测领域一个经典的公开数据集组织方式,它的核心是三个目录加一个划分文件:JPEGImages存放原始图像,Annotations存放与图像同名的XML标注文件,ImageSets/Main存放训练集和验证集的划分列表。这份水污染检测数据集既然标了VOC格式,正常情况下就是按这个结构组织的,你可以直接沿用,不需要额外改动目录名。
我刚拿到这类数据集时会先做三件事:数一下JPEGImages里的图片数量是否和标注文件数量一致、抽查几张XML里的标注框是否在图像尺寸范围内、确认类别名称不包含中文或空格。这三项如果没问题,基本可以认定数据集是干净可用的。2487张图配合4个类别,平均每个类别大约600张样本,存在一定的类别不均衡风险,训练时建议按类别统计一下分布。
结构层面有一个细节值得注意:ImageSets/Main下有train.txt和val.txt两个文件,里面存的是图片文件名(不带扩展名),YOLO训练时会按这个列表去读取图像和标签。如果你打算用YOLO训练,这个划分文件不直接使用,但建议保留它,方便后期做交叉验证。我一般会先打开val.txt数一下行数,确认验证集比例是否在20%左右,太少的验证集会让你对模型真实效果产生误判。
2.2 XML标注文件的结构和容易被忽略的字段
一个标准的VOC XML文件通常包含filename、size、object三个关键部分。filename对应图像文件名,size里记录图片的width、height和depth,object节点下是目标的name和bndbox坐标。这份数据的4个类别名称可能各不相同,训练前一定要写一个脚本把所有XML里的name字段去重打印出来,确认没有拼写不一致的情况——比如同一个类别出现过“garbage”和“Garbage”两种写法,这会导致YOLO转换后的类别数超过4个。
坐标字段是VOC转YOLO最需要小心的部分。XML里存储的是绝对像素坐标,即xmin、ymin、xmax、ymax,而YOLO格式要求的是归一化后的相对坐标。转换时必须用xmin除以图像宽度,而不是直接拿原值用。这里还有一个边界问题:如果某个标注框的坐标是xmax=640而图像宽度也是640,归一化后结果为1.0,这在YOLO训练中会被视为越界框,轻则警告,重则影响精度。我习惯在转换代码里加一个裁剪逻辑,把坐标值强制限制在0到1之间,见后面的脚本。
对于这份数据集,我建议你先跑下面这段代码盘点类别分布和标注框数量,不要急着训练。它能帮你发现大部分潜在问题,比训练到一半报错再回来查要高效得多:
import xml.etree.ElementTree as ET import glob from collections import Counter xml_files = glob.glob('VOCdevkit/VOC2007/Annotations/*.xml') class_counter = Counter() box_counter = 0 for xml_file in xml_files: tree = ET.parse(xml_file) root = tree.getroot() for obj in root.findall('object'): name = obj.find('name').text.strip() class_counter[name] += 1 box_counter += 1 print('标注框总数:', box_counter) print('每个类别的目标数:') for cls, cnt in class_counter.items(): print(f' {cls}: {cnt}')这段代码逐个解析XML文件,统计每个类别出现的次数以及标注框总数。注意我用了strip()清理类别名两端的空格,因为有些标注工具会在类别名后面意外加上回车或空格。如果打印出来的类别数超过4个,说明存在名称不一致的问题;如果某个类别数量特别少(比如只有几十个),训练时这个类别会很难收敛,你需要重点考虑数据增强或收集更多样本。
2.3 图像质量检视:哪些图会拖垮训练
图像集里有少量低质量样本几乎是必然的。这份数据集有2487张图,我建议你从中挑出模糊、过曝、欠曝的样本看看它们是否还保留着标注。删除图像时必须同步删除对应的XML和划分列表里的行,只删图片不删标注会导致加载时报错,虽然YOLO训练时会跳过缺失文件,但你会浪费大量时间在排查上。
另一个常见问题是图像尺寸不一致。YOLO训练时默认会做letterbox缩放,即把图像等比例缩放到640×640并填充灰边,所以尺寸不一致本身不影响训练。但如果存在极端细长图,比如宽度是高度的5倍,letterbox之后目标会被压缩得很小,训练效果不会好。我会把这类图单独拎出来看一遍,是保留还是剔除,取决于目标在缩略图上是否还能辨认。
3. 把VOC转成YOLO格式:数据划分脚本与坐标归一化的四个关键点
3.1 转换脚本的完整实现
数据集的VOC格式不能直接喂给YOLO,YOLO系列需要的是每张图对应一个同名的txt文件,每一行代表一个标注框,格式为:类别ID 中心点x坐标 中心点y坐标 宽度 高度,所有坐标都是相对于图像尺寸的归一化值,范围0到1。下面是我常用的转换脚本,它一次性完成XML解析、坐标归一化、数据集划分三项工作:
import os import random import xml.etree.ElementTree as ET # 配置区:按实际路径修改 voc_annotations = 'VOCdevkit/VOC2007/Annotations' voc_images = 'VOCdevkit/VOC2007/JPEGImages' yolo_labels = 'VOCdevkit/VOC2007/labels' image_list = 'VOCdevkit/VOC2007/ImageSets/Main' os.makedirs(yolo_labels, exist_ok=True) random.seed(42) classes = ['garbage', 'oil_slick', 'algae', 'waste_outlet'] # 按你的数据实际类别修改 def convert_annotation(xml_path, img_width, img_height): """解析单个XML并转换为YOLO行格式""" tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.findall('object'): cls_name = obj.find('name').text.strip() if cls_name not in classes: print(f'跳过未知类别 {cls_name},文件 {xml_path}') continue cls_id = classes.index(cls_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) # YOLO格式:中心点坐标 + 宽高,做归一化并上限1.0 x_center = max(0.0, min(1.0, (xmin + xmax) / 2.0 / img_width)) y_center = max(0.0, min(1.0, (ymin + ymax) / 2.0 / img_height)) width = max(0.0, min(1.0, (xmax - xmin) / img_width)) height = max(0.0, min(1.0, (ymax - ymin) / img_height)) lines.append(f'{cls_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}') return lines # 遍历所有XML生成YOLO标签 all_images = [] for xml_file in os.listdir(voc_annotations): if not xml_file.endswith('.xml'): continue xml_path = os.path.join(voc_annotations, xml_file) img_name = xml_file.replace('.xml', '.jpg') # 根据实际扩展名修改 img_path = os.path.join(voc_images, img_name) if not os.path.exists(img_path): img_name = xml_file.replace('.xml', '.png') img_path = os.path.join(voc_images, img_name) # 读取图像尺寸 from PIL import Image with Image.open(img_path) as img: img_w, img_h = img.size lines = convert_annotation(xml_path, img_w, img_h) label_path = os.path.join(yolo_labels, xml_file.replace('.xml', '.txt')) with open(label_path, 'w') as f: f.write('\n'.join(lines)) all_images.append(img_name) if len(lines) == 0: print(f'警告:{xml_file} 没有有效标注') print(f'转换完成,共处理 {len(all_images)} 张图片') # 按比例切分训练集和验证集 random.shuffle(all_images) val_ratio = 0.2 val_count = int(len(all_images) * val_ratio) val_images = all_images[:val_count] train_images = all_images[val_count:] def write_split(images, split_name): with open(os.path.join(image_list, f'{split_name}.txt'), 'w') as f: for img in images: f.write(img.replace('.jpg', '').replace('.png', '') + '\n') write_split(train_images, 'train') write_split(val_images, 'val') print(f'训练集 {len(train_images)} 张,验证集 {len(val_images)} 张')这段脚本做了四件事:解析每个XML提取类别和坐标、归一化坐标并写入YOLO格式的txt文件、自动读取图像尺寸保证归一化准确、最后按8:2比例划分为训练集和验证集。脚本里对归一化坐标做了裁剪,把越界值强制修正到0到1范围内,这能避免一些标注工具产生的负坐标或超宽框影响训练稳定性。随机种子设为42,保证每次运行划分结果一致,方便复现实验。
实际使用时你要注意两点。一是classes列表顺序必须和最终训练时的类别ID保持一致,YOLO训练时会有一个data.yaml文件记录类别名,顺序错了模型输出的含义就全错了。二是如果图片扩展名不统一,比如同时存在jpg和png,脚本里做了二次匹配处理,但更稳妥的做法是先统一扩展名再跑转换。
3.2 四个容易出错的关键位置
转换过程中有四个位置最容易翻车,我把它们单独拿出来说。第一是类别名称的大小写和空格问题,XML里的“Oil_slick”和“oil_slick”会被识别成两个类别,脚本里我加了strip()但没做统一大小写,你在跑之前应该先确认所有类别名完全一致。第二是图像宽高信息的获取,用PIL读取是可靠的做法,不要从XML的size字段读取,因为有些标注工具写入的尺寸和实际图像有偏差。
第三是输出路径的扩展名。YOLO要求标签文件和图像文件保持相同的文件名前缀,扩展名必须是.txt。有些转换工具会把输出记为.xml.txt,文件名前缀对不上会导致训练时找不到标签。第四是空标注文件的处理。如果某张图没有任何有效标注,脚本会写出一个空txt文件,YOLO训练时会跳过这个样本,但如果你用某些老版本训练器,可能会把空文件所在的图片直接报错丢弃,数据量少的时候这会进一步加剧类别不平衡。
验证转换是否正确的标准做法是:随机挑几张图,把txt内容画回原图上检查。如果标注框位置明显偏移,极有可能是图像读取尺寸和标注尺寸不一致造成的。另一个快速检查方法是用YOLO训练器自带的校验模式,YOLOv5和YOLOv8都支持在训练前输出标签分布图和标注框可视化,花两分钟跑一下这个校验,能省掉后面几小时的排查时间。
3.3 用可视化脚本确认标签画对了
转换完之后不要急着训练,先跑一段可视化检查。这段代码把YOLO格式的标签画回原图,能直观判断坐标转换是否正确:
import cv2 import os labels_dir = 'VOCdevkit/VOC2007/labels' images_dir = 'VOCdevkit/VOC2007/JPEGImages' output_dir = 'check_visualize' os.makedirs(output_dir, exist_ok=True) class_colors = { 0: (0, 0, 255), # garbage: 红 1: (0, 255, 0), # oil_slick: 绿 2: (255, 0, 0), # algae: 蓝 3: (0, 255, 255) # waste_outlet: 黄 } # 随便取10张图做可视化 label_files = os.listdir(labels_dir)[:10] for label_file in label_files: img_name = label_file.replace('.txt', '.jpg') img_path = os.path.join(images_dir, img_name) if not os.path.exists(img_path): img_name = label_file.replace('.txt', '.png') img_path = os.path.join(images_dir, img_name) img = cv2.imread(img_path) h, w = img.shape[:2] label_path = os.path.join(labels_dir, label_file) with open(label_path, 'r') as f: for line in f.readlines(): parts = line.strip().split() if len(parts) < 5: continue cls_id = int(parts[0]) x_center, y_center, box_w, box_h = map(float, parts[1:5]) x1 = int((x_center - box_w / 2) * w) y1 = int((y_center - box_h / 2) * h) x2 = int((x_center + box_w / 2) * w) y2 = int((y_center + box_h / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), class_colors.get(cls_id, (255, 255, 255)), 2) cv2.putText(img, str(cls_id), (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, class_colors.get(cls_id, (255, 255, 255)), 1) cv2.imwrite(os.path.join(output_dir, img_name), img) print(f'已生成 {os.path.join(output_dir, img_name)}')这个脚本从YOLO标签的归一化坐标反算回绝对像素坐标,把标注框画出来。你可以直接目测检查框是否贴合目标。反算过程中最容易出的问题就是忘记乘以图像宽高,导致框画出来集中在左上角——如果看到这种现象,回到转换脚本里检查归一化那段代码。
4. 用YOLO训练水污染模型:从YAML配置到训练命令的参数清单
4.1 data.yaml文件:4个类别的映射关系
YOLO训练前需要准备一个data.yaml配置文件,里面指定训练集和验证集图片路径、类别数量、类别名称列表。它的核心作用是告诉训练器「图片在哪、标签在哪、一共几类、每类叫什么」。我习惯把这个文件放在数据集根目录下,方便后续复用。
下面是针对这份2487张4类别数据集的data.yaml模板:
# 数据配置文件:水污染检测4类别 train: VOCdevkit/VOC2007/images/train val: VOCdevkit/VOC2007/images/val nc: 4 names: ['garbage', 'oil_slick', 'algae', 'waste_outlet']这个文件有几个容易出错的细节。train和val字段指向的是存放图片的目录,而不是图片列表文件,YOLO会自动去找同目录下对应的labels子目录里的txt文件。如果你的目录结构和我不一样,记得调整路径。nc必须和names列表长度一致,否则训练直接报错。names顺序必须和转换脚本里的classes顺序保持一致,这一点再怎么强调都不为过。
4.2 训练命令和关键参数说明
YOLO训练的命令看起来简单,但参数搭配很讲究。对于2487张图的数据量,模型规模建议从YOLOv8s起步,不要直接上YOLOv8x——小数据量配大模型很容易过拟合,训练速度慢且精度未必更好。这里给出我实验后觉得合理的训练命令:
yolo detect train data=data.yaml model=yolov8s.pt epochs=120 imgsz=640 batch=16 lr0=0.01 optimizer=SGD patience=20 augment=True plots=True这条命令里的每个参数都有讲究。model参数设置为主干和预训练权重,yolov8s.pt会加载在大型通用数据集上预训练好的权重,这对小数据量训练很关键,相当于模型已经学会了基本的边缘和纹理特征,你只需要让它适配水污染目标的分布。epochs设120轮,配合patience=20做早停——如果连续20轮验证集精度没有提升就提前终止,防止浪费时间。batch=16取决于显卡显存,如果你的卡只有8G显存,imgsz=640加batch=16可能放不下,可以降到batch=8。
优化器和学习率的组合值得说道一下。img尺寸设为640是YOLO系列的默认值,但这份数据集如果存在大量小目标(比如远处的油污区域),可以考虑把imgsz加大到960,代价是显存占用和训练时间显著增加。直观的判断方法是用imgsz=640训练一版,观察验证集里小目标的召回率,如果偏低再尝试更大分辨率。
4.3 从训练日志里读出精度之外的信息
训练过程中不要只看最后的mAP数字,更要关注三个信号。一是训练损失和验证损失的差距,如果训练损失持续下降但验证损失在某个点后反弹,说明过拟合开始,早停机制会帮你停下,但你可以提前观察到。二是每个类别的AP值分布,4个类别不可能一样高,哪个类别AP明显偏低,说明它的样本量不足或标注一致性差。三是混淆矩阵里最容易互相误判的两个类别,比如油污和藻类在颜色上很像,这类错误靠增加更多难样本比调整参数更有效。
训练结束后,YOLO会在runs/detect目录下输出结果,包括weight文件、PR曲线、混淆矩阵、每种类别的精确率和召回率。我用一张表格来校准「这个模型到底行不行」:目标是水污染检测场景,如果漂浮垃圾类别的精确率低于0.7,说明误检较多,可能是水面波纹被当成垃圾;如果召回率低于0.6,说明漏检严重,需要补充更多该类别样本。这两个指标在不同项目里的权重不一样,你的场景更看重哪个,需要自己想清楚。
5. 数据与训练避坑:5条最容易翻车的记录和排查方法
5.1 标签文件缺失导致训练集实际少于2487张
有一次我在某个项目里发现,数据集目录里图片有2400多张,但训练时YOLO报出只有1800多张图参与训练。原因是部分图片没有对应txt标签文件,有些是转换脚本漏掉的,有些是原始XML文件本身缺失。现象是训练日志开头的图片数量统计少于预期。排查方法是遍历对比图片文件名和标签文件名,找出差异列表,再补生成缺失的标签。解决这类问题的最快路径是写一个校验脚本,核心逻辑就是取两份文件名的集合做差集。
5.2 类别ID从1开始导致模型分类混乱
某次用另一个转换工具时,输出的txt文件里类别ID是1、2、3、4,而YOLO期望的是0、1、2、3。现象是训练能正常进行,但预测结果总是错一位——预测出的类别永远比实际类别靠后一个。这个问题非常隐蔽,因为训练过程不会报错。排查时随机查看几个txt文件的第一列,确认ID范围是从0开始。解决方法是批量减1或用字符串替换重写ID。自此之后,我每次拿到新转换的数据,第一件事就是看txt第一列的最小值是不是0。
5.3 归一化坐标越界引发精度剧烈波动
VOC转换时坐标除以图像宽高,如果某条标注的xmin是负数或xmax超过图像宽度,归一化后会得到负值或大于1的值。训练时YOLO会打印警告并要求自动清洗这些异常框。现象是训练过程中频繁出现坐标越界警告,验证集精度忽高忽低。原因是个别标注框画出了图像边界,处理方式是维持转换脚本里的min/max裁剪逻辑,把越界值限制在0到1之间。这种情况在手工标注的数据集里不少见,一定要在转换层兜底。
5.4 数据集划分重合导致验证集泄露
如果train.txt和val.txt是从全量数据里分别随机抽取后直接写出的,而不是先洗牌再按比例切成两段,很可能某些图片同时出现在训练集和验证集里。现象是训练时验证精度很高,但部署到新图片上效果大幅下降,模型泛化能力很差。排查方法是对比两个文件的行内容是否有交集。解决方式就是上一章脚本里的写法——先随机打乱,再按比例切分,保证两部分无交集。这是一个原则性问题,我在每次划分后都习惯性跑一遍交集检查。
5.5 图片中有EXIF旋转信息导致标注错位
手机或无人机拍摄的图片经常带有EXIF方向信息,某些图像处理库读取时会自动旋转,有些则不会。如果转换脚本里读取图像尺寸时用了旋转后的尺寸,但目标检测训练时图像又被翻转了一次,标注框就会全部偏移。现象是可视化检查时标注框和真实目标错位,且错位方向有规律。解决方法是转换前用工具将图片统一转为标准方向,或者读取尺寸时强制关闭自动旋转操作。这个问题只出现在某些带方向信息的图片上,比例不高但一旦出现会很奇怪。
6. 验证技巧:用PR曲线和误检图把模型调到能上线的程度
训练完成后,验证工作不应该停留在看一个总的mAP数字上。我常用的做法分三批:先看PR曲线确认每个类别在不同置信度阈值下的行为,再翻检误检和漏检的具体图片,最后用小批量现场视频或新采集图片做推理测试。这步做完,模型才算真正「能上线」。
PR曲线是最好的诊断工具。如果某个类别的曲线面积小且形状别扭,说明该类别学习得不好,接下来要么补样本要么加权重。曲线靠近右上角的部分,决定模型在低误检条件下的表现;靠近右下角的部分,反映模型的召回潜力。实际部署时我会在PR曲线上找一个「精确率够用但召回率不太差」的点,把对应置信度阈值写进推理参数里,而不是用默认的0.25。
误检图分析时,我习惯把置信度高于0.3但预测错误的图片单独导出,逐张看误检目标的形态特征。比如水面反光经常被误检为油污,这种误检靠增大油污类别的边界框回归权重或增加反光负样本能明显改善。漏检分析的思路正相反,统计漏检目标在图中的占比和位置,如果漏检的多是小尺寸远处目标,优先考虑加大imgsz或增加多尺度训练。
最后跑一个新场景的图片或视频做泛化测试。选图的标准是:包含数据集中出现过的水污染类型、但不包含训练集中的同一张图片。我会手动数出视频里的目标数量,跟模型输出的结果对比,计算一个「现场准确率」。这个数字比离线指标更有说服力,因为现场场景里的光照、角度、水面状态都和训练集有差异。
做完这四步,这份水污染检测数据集就算真正被利用起来了。我个人最深的教训是:转换后的可视化检查不可跳过,每次直接训练都会在几天后因为某个坐标问题回溯重跑。建议你也在转换脚本后固定一个可视化校验的步骤,形成习惯。希望这套流程能帮你把这个方向快速跑通,少走我走过的弯路。
本文还有配套的精品资源,点击获取