ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

水下垃圾检测数据集实战:三种标注格式与YOLO训练避坑指南

水下垃圾检测数据集实战:三种标注格式与YOLO训练避坑指南

简介:面向水下机器人作业与水域环境监测场景,这份水下垃圾检测数据集提供5328张真实拍摄图像,覆盖水下生物、塑料垃圾、金属垃圾、木头垃圾、橡胶垃圾、布料垃圾及捕鱼工具垃圾7个类别,标注精准且分布均匀,适合用于课程设计、毕业设计、算法比赛及实际项目中的多类目标检测模型训练与验证。数据集包共包含21313个文件,其中jpg原图5328张,并同时提供voc(xml)、yolo(txt)与json三种标签格式,可直接对接主流目标检测框架,省去格式转换工作。压缩包整体大小为127.54MB,目录结构简洁,便于按图像编号快速检索与划分训练集、验证集。当前已有325人浏览学习,适用于从入门到进阶的目标检测开发者快速构建水下垃圾识别模型。整体而言,该数据集贴近真实水下环境,类别划分细致,对水环境智能监测、水域垃圾自动识别等应用具有直接参考价值,拿到后可直接开展模型训练与效果评估。

1. 水下垃圾检测数据集,为什么值得先拆开再训练

拿到这个水下垃圾检测数据集的第一反应,多数人会直接把它丢进训练脚本里跑,结果被格式不匹配、类别名错位、标签文件缺失这些问题连续卡住。这份资源本身包含5328张水下机器人拍摄的真实图像,覆盖水下生物、塑料、金属、木头、橡胶、布料、捕鱼工具七个类别,而且同时提供VOC XML、YOLO TXT和JSON三种标注格式,本质上就是把数据整理和格式适配的工作替你做了大半。适合做水域环境监测、课程作业或比赛项目的人,也适合刚接触目标检测但不想把时间耗在数据清洗上的从业者。但我的建议是,拿到手先别急,花半小时拆结构,后面能省出两天的排错时间。

2. 七类的标注口径和三种格式,决定模型上限的细节

数据集的价值不在图片数量,而在标注是否一致、类别边界是否清晰、格式是否通用。5328张图只能说明工作量,真正影响YOLO模型训练效果的,是类别怎么定义、边界怎么画、标签以什么结构存储。这三件事没搞清楚之前就开训,后续每个环节都在还账。

2.1 七类别的实际含义与容易混淆的边界

七个类别里,水下生物单独作为一类而不是垃圾,它的作用在目标检测里接近“干扰项”。水下环境中鱼、蟹、海龟这类生物和垃圾同时出现,如果模型把生物也当成垃圾,下游的清理决策会出错;如果完全不学,生物和塑料同时出现时又容易误检。让模型学会区分“要清理的”和“不用动的”,是这个类存在的意义。

另外六类都属于垃圾:塑料垃圾以袋子、瓶子、包装膜为主,金属垃圾常见罐体、零件和线缆,木头垃圾是树枝、木板、加工废料,橡胶垃圾主要是轮胎和密封圈,布料垃圾以衣物和渔网碎片为主,捕鱼工具垃圾则包括整张渔网、鱼线、笼具和浮标。

真正麻烦的是类别边界。湿润状态下塑料和橡胶的反光特征非常接近,渔网碎片在图像里和布料纹理几乎一样,木头长时间浸泡后会变黑,和金属废料的颜色区分度下降。标注人员如果标准不统一,同一张图在不同批次里可能出现不同标签,模型就会学到互相矛盾的特征。

拿到数据集后先做一次标注一致性检查。别急着训练,抽几十张图把每类的标注框可视化叠在原图上,重点看塑料和橡胶、布料和渔网这两组容易混的类别,确认标注口径一致后再进入训练环节。

统计一下各类别的分布,用脚本读取所有XML文件,数一数每个类别出现的次数和标注框数量,代码如下:

import glob import xml.etree.ElementTree as ET from collections import Counter xml_files = glob.glob('./UnderwaterGarbage/xmls/*.xml') counter = Counter() for xml_path in xml_files: tree = ET.parse(xml_path) root = tree.getroot() for obj in root.iter('object'): name = obj.find('name').text.strip() counter[name] += 1 print('类别总数:', len(counter)) for cls_name, count in counter.most_common(): print(f'{cls_name}: {count}')

这段代码遍历所有VOC格式的XML文件,用iter('object')取出每个标注目标,并统计类别出现次数。运行结果能直观反映出类别是否均衡。如果某个类别只占总数5%以下,后续训练时就需要考虑针对性增强或调整损失权重,否则模型大概率会忽略这个类。

顺便加一句,统计时直接把XML当作唯一数据源,是因为XML里保留了完整的像素坐标和目标名称,不依赖额外信息,也是最不容易出错的检查路径。统计完分布再看看标注框面积和长宽比,水下目标普遍偏小,这会直接影响anchor尺寸设置。

2.2 三种标注格式的结构差异与选型场景

这份数据集同时给出VOC XML、YOLO TXT、JSON三种格式,不是简单的重复存储,三种格式的适用场景完全不同。

VOC XML是树状结构,每个object节点下有name、bndbox两个关键字段,坐标用像素值表达,可读性好,打开一个XML文件就能直接看到目标位置。适合做可视化检查、人工复核和手工修正。缺点是文件体积大,解析慢,训练框架基本不直接读取它。

YOLO TXT是逐行存储的归一化坐标,每行格式为class_id cx cy w h,四个数值全部归一化到0到1区间。训练YOLO系列模型时框架直接读取这种格式,是实际训练入口。缺点是人眼不可读,坐标对不对很难从文件本身判断。

JSON格式要看内部结构。如果按COCO规范组织,通常包含images、annotations、categories三个顶层数组,annotations里的bbox是像素坐标,categories定义类别id到名称的映射。这个结构被pycocotools等评估工具直接支持,适合做指标评估和自定义数据管道。

三种格式的实际用途可以归纳如下:

格式文件结构适用场景注意点
VOC XML树结构,object含像素坐标可视化检查、人工复核每张图对应同名XML文件
YOLO TXT每行class + 归一化中心点与宽高YOLO系列训练入参文件名必须与图像完全一致
JSONimages/annotations/categories数组COCO评估、自定义管道需确认是否严格COCO结构

我的使用习惯是:用XML做人工排查,用JSON做自动化验证,用TXT作为训练输入。三种格式互相印证的逻辑在于,它们是同一批标注数据的不同投影,格式转换过程中如果出现问题,比对三种格式能快速定位是原始标注问题还是转换逻辑问题。

3. 把数据集接进YOLO训练流程:目录重组与配置实操

知道类别和格式之后,下一步是把下载的原始目录改造成YOLO训练需要的结构。这个环节是翻车重灾区,常见的错误是图片划分了、标签没有跟着划分,或者目录结构不符合框架要求,训练启动就直接报错。

3.1 目录重组与train/val划分脚本

我拿到一份带多种格式的数据集时,一般先看原始目录组织方式。这个数据集大概率分为images、xmls、yolo_labels、jsons四个文件夹。而YOLO系列框架期望的输入结构是:一个外层目录下分train和val两大部分,每部分里面再分images和labels两个子目录,且图片与标签文件同名。

手工复制几千个文件很容易出错,直接写一个划分脚本一次性解决:读取全部图片文件名,按比例切分后,基于同一个文件名列表同时复制图片和对应的TXT标签文件。

import os import random import shutil random.seed(42) img_dir = './UnderwaterGarbage/images' txt_dir = './UnderwaterGarbage/yolo_labels' val_ratio = 0.2 image_files = [f for f in os.listdir(img_dir) if f.endswith('.jpg')] random.shuffle(image_files) split_point = int(len(image_files) * (1 - val_ratio)) train_files = image_files[:split_point] val_files = image_files[split_point:] def copy_batch(file_list, src_img, src_label, dst_img, dst_label): os.makedirs(dst_img, exist_ok=True) os.makedirs(dst_label, exist_ok=True) for f in file_list: stem = os.path.splitext(f)[0] shutil.copy2( os.path.join(src_img, stem + '.jpg'), os.path.join(dst_img, stem + '.jpg') ) shutil.copy2( os.path.join(src_label, stem + '.txt'), os.path.join(dst_label, stem + '.txt') ) copy_batch(train_files, img_dir, txt_dir, './dataset/train/images', './dataset/train/labels') copy_batch(val_files, img_dir, txt_dir, './dataset/val/images', './dataset/val/labels') print(f'train: {len(train_files)} 张, val: {len(val_files)} 张')

脚本的核心逻辑是先用random.seed(42)固定随机种子,保证每次运行划分结果一致。然后用列表切片按比例分出train和val两部分,再通过copy_batch函数将图片和标签同步复制到目标目录。这里的文件名起到唯一关联作用,所有操作都基于文件名stem拼接,从根本上避免图片和标签错位。

参数说明:val_ratio控制验证集比例,一般目标检测用0.2比较稳妥;seed=42是固定随机种子,后续想复现实验结果就必须保持这个值不变。

划分完成后做一次完整性校验,统计train和val目录下的图片数量和标签数量是否一一对应。这一步可以用一段简短的命令行完成:

echo "train images: $(ls ./dataset/train/images/*.jpg | wc -l)" echo "train labels: $(ls ./dataset/train/labels/*.txt | wc -l)" echo "val images: $(ls ./dataset/val/images/*.jpg | wc -l)" echo "val labels: $(ls ./dataset/val/labels/*.txt | wc -l)"

不需要完全一致才叫通过,核心是train下图片等于标签,val下图片等于标签。如果出现差异,回到划分脚本里检查TXT文件名的后缀是否与图片同名,这是最常见的缺失原因。

3.2 数据配置文件与训练超参数调整

目录就绪后,需要在数据集根目录创建data.yaml,这个文件被YOLO框架读取,用于定位训练、验证数据和定义类别名称。

path: ./dataset train: train/images val: val/images nc: 7 names: 0: underwater_bio 1: plastic 2: metal 3: wood 4: rubber 5: fabric 6: fishing_tool

这里的path是相对data.yaml所在目录的根路径,train和val分别指向images子目录。框架会自动在同一级目录下寻找labels文件夹,所以不需要显式写labels路径。

类别顺序的注意事项要单独说明:data.yaml里的names顺序必须和TXT标签里的class_id一一对应。也就是说,如果你发现训练时预测结果里的class 0变成了木头,而实际应该对应水下生物,说明类别顺序没有对上。我吃过这个亏,当时靠逐一打印预测框才发现names列表和训练时的类别映射错位了。

训练超参数上,针对水下垃圾场景,我一般这样设置:图像尺寸imgsz取640起步,水下目标普遍偏小但图像特征不算极端,640能兼顾速度和精度;batch大小根据显存决定,16是常见选择;epochs先跑50到100观察loss曲线,不要一上来就配300。

数据增强参数需要重点调整。水下图像整体偏蓝绿,对比度偏低。YOLO默认的hsv_h增强会大幅改变色相,但水下场景的颜色分布本来就特殊,色相变化过大会让模型学到错误的颜色映射关系。实际项目里我把增强参数调成了更保守的组合:关闭水平翻转可能会损失部分样本泛化能力,但考虑到水下机器人拍摄角度相对固定,翻转带来的增益有限;mosaic增强可以保留,能让模型在拥挤目标场景下更鲁棒。对比度增强适当调高,这比色相变换更适合水下图像特征。

启动训练时建议加载预训练权重来加速收敛。底层特征如边缘、纹理、形状是通用的,水下垃圾同样具备这些特征。训练命令用Ultralytics框架的一套标准指令即可:

yolo train model=yolov8s.pt data=/path/to/your/data.yaml epochs=80 imgsz=640 batch=16

如果不加载预训练权重,5328张图从零开始训练,收敛速度会明显变慢,而且早期epoch的loss波动会很大,不容易判断模型是否在正常学习。加载后第一轮epoch就能看到loss明显下降,这个反馈很重要。

4. 三种标注格式互转的边界条件:从XML到TXT再到JSON验证

数据集虽然自带三种格式,但实际使用中经常遇到需要转换的场景:想换一个不在列表里的类别名称、想过滤掉某些小目标、想合并类别,都可能需要从一种格式重新生成另一种格式。这个环节的坑集中在归一化计算和坐标边界处理上。

4.1 XML转YOLO TXT的归一化计算与坐标截断

VOC XML记录的是像素坐标,YOLO TXT需要归一化的中心点坐标和宽高。核心公式是:

  • 中心点x = (xmin + xmax) / 2 / 图片宽度
  • 中心点y = (ymin + ymax) / 2 / 图片高度
  • 归一化宽 = (xmax - xmin) / 图片宽度
  • 归一化高 = (ymax - ymin) / 图片高度

公式本身不难,但实际转换中很容易在坐标越界这个问题上翻车。人工标注时偶尔会把框画得超出图像边界一点,或者某些转换工具产生负数坐标,这些值直接归一化后会出现cx、cy大于1的情况,YOLO训练框架会静默丢弃这些标注行。结果就是训练时少数样本缺失标签,模型精度莫名其妙地受影响。

转换脚本里加上边界截断处理,统一把所有坐标clip到图像尺寸范围内:

import os import xml.etree.ElementTree as ET CLASS_NAMES = ['underwater_bio', 'plastic', 'metal', 'wood', 'rubber', 'fabric', 'fishing_tool'] def convert_xml_to_yolo(xml_path, out_dir, img_width=0, img_height=0): tree = ET.parse(xml_path) root = tree.getroot() size = root.find('size') if img_width and img_height: width, height = img_width, img_height else: width = int(size.find('width').text) height = int(size.find('height').text) output_lines = [] for obj in root.iter('object'): name = obj.find('name').text.strip() if name not in CLASS_NAMES: print(f'未知类别 {name} 在 {xml_path}') continue cls_id = CLASS_NAMES.index(name) box = obj.find('bndbox') xmin = max(0, min(float(box.find('xmin').text), width)) ymin = max(0, min(float(box.find('ymin').text), height)) xmax = max(0, min(float(box.find('xmax').text), width)) ymax = max(0, min(float(box.find('ymax').text), height)) bw, bh = xmax - xmin, ymax - ymin if bw <= 0 or bh <= 0: continue cx = (xmin + xmax) / 2.0 / width cy = (ymin + ymax) / 2.0 / height w_norm = bw / width h_norm = bh / height output_lines.append( f'{cls_id} {cx:.6f} {cy:.6f} {w_norm:.6f} {h_norm:.6f}' ) base = os.path.splitext(os.path.basename(xml_path))[0] os.makedirs(out_dir, exist_ok=True) with open(os.path.join(out_dir, base + '.txt'), 'w') as f: f.write('\n'.join(output_lines))

这个脚本里最关键的是clip操作。用max(0, min(value, width))把每个坐标值限制在合理范围内,防止标注框越界产生非法归一化值。外层支持传入img_width和img_height参数,用于处理XML文件里size信息和实际图片尺寸不一致的情况——这种情况在某些标注工具生成的XML里很常见。

运行转换后必须做一次反向验证,随机挑几张图把TXT里的框画回图片上看对齐情况。这一步是在给自己上一道保险,确认转换结果不是黑匣子产物。

4.2 JSON标注结构的读取与异常检测

JSON格式的用途在这里需要确认它是否遵循COCO规范。COCO结构的JSON里,annotations的bbox字段被定义为[x, y, width, height]格式,单位是像素。这个定义在评估阶段非常重要,因为pycocotools计算mAP时就是直接读这个字段,格式错误会导致评估结果诡异。

写一段JSON验证脚本,检查JSON文件是否存在结构问题和越界标注:

import json from collections import Counter with open('UnderwaterGarbage/annotations/instances.json', 'r') as f: coco = json.load(f) print('categories 数量:', len(coco['categories'])) print('images 数量:', len(coco['images'])) print('annotations 数量:', len(coco['annotations'])) img_map = {img['id']: img for img in coco['images']} cat_map = {cat['id']: cat['name'] for cat in coco['categories']} orphan_anns = [a['id'] for a in coco['annotations'] if a['image_id'] not in img_map] if orphan_anns: print(f'警告: {len(orphan_anns)} 条标注没有对应图片') else: print('检查通过: 所有标注都关联到有效图片') for cat_id, count in Counter(a['category_id'] for a in coco['annotations']).items(): print(f'{cat_map[cat_id]}: {count} 个标注框') out_of_bounds = [] for ann in coco['annotations']: img = img_map[ann['image_id']] x, y, w, h = ann['bbox'] if x < 0 or y < 0 or x + w > img['width'] or y + h > img['height']: out_of_bounds.append(ann['id']) if out_of_bounds: print(f'越界标注数量: {len(out_of_bounds)}') else: print('检查通过: 所有bbox都在图像范围内')

这个脚本主要做三件事:检查是否有标注关联到不存在的图片ID,统计各类别框分布,检测bbox是否越界。前两类问题通常是原始标注生成时就产生的,第三种则是格式转换时常见的错误。运行得到的结果如果显示越界标注超过总标注量的2%,建议回到源标注重新处理,而不是在JSON层修复,因为越界往往意味着原始的XML或TXT里也存在同样的脏数据。

5. 避坑:水下垃圾数据集训练时的常见问题与排查

这几条问题来自我实际跑这个数据集时的经历,每一条都对应过真实的训练翻车,按现象、原因、解决三个维度拆开讲。

5.1 类别顺序错位导致预测结果全部混乱

现象:训练正常完成,验证loss正常下降,但可视化预测结果时发现类别标签对不上,塑料预测成金属,木头预测成水下生物。

原因:data.yaml里的names顺序和TXT标签里的class_id编码不一致。比如XML里类别顺序是A、B、C,但转换脚本里硬编码的CLASS_NAMES列表顺序不同,class_id就会错位。

解决:统一所有环节使用同一份类别映射。建议把类别映射表单独放在一个文件里,XML转换脚本、data.yaml、JSON的categories三处都从这份文件生成,不要在多个地方手工维护列表。

5.2 XML里的size与实际图片尺寸不一致

现象:转换后的TXT文件里出现cx、cy大于1的值,或者训练时提示大量标注被过滤。

原因:部分标注工具在XML里写的是固定默认尺寸,没有根据实际图片更新,导致归一化时分母错误。

解决:转换脚本增加一个参数,强制从真实图片读取宽高,不依赖XML里的size节点。用PIL或OpenCV读一下就能解决:

from PIL import Image img = Image.open(image_path) real_w, real_h = img.size

转换时传入img_width=real_w, img_height=real_h,覆盖XML里的错误值。

5.3 YOLO标签文件出现空文件

现象:某个TXT文件大小为0字节,训练时框架提示labels为空。

原因:原图里没有标注任何目标但依然生成了XML文件,或者XML里没有object节点,又或者所有目标在转换时被坐标过滤逻辑剔除。

解决:先区分是空白样本还是转换丢失。如果图片本身没有目标,这种空文件要保留,YOLO框架允许背景图存在;如果是转换丢失,需要检查边界截断逻辑。区分方法是用脚本扫描所有XML,记录没有object节点的文件名,与空TXT文件列表做对比。

5.4 验证精度高但实际水下视频检测效果差

现象:在val集上mAP到0.8以上,拿到真实水下视频测试却大量漏检。

原因:数据分布漂移。训练集的5328张图可能集中在一定水域深度和光照范围,真实环境的浑浊度、色偏、目标尺度不同,模型泛化能力自然下降。

解决:训练时加入针对水下图像的增强策略,重点做对比度拉伸、亮度抖动、通道扰动,不要只依赖通用增强。模型选型上也可以考虑在YOLO的基础上并联一个浅层特征分支,专门负责小目标检测,这个在后续章节细说。

5.5 划分数据时图片和标签没有同步

现象:训练启动报错,某张图片找不到对应的标签文件。

原因:划分脚本只处理了图片列表,标签文件沿用了另一个列表,或者shuffle后两个列表的顺序不一致。

解决:用文件名作为唯一关联键,先构造一个包含完整文件名的列表,shuffle之后基于stem名拼接对应路径来同步复制。代码示例见3.1节的copy_batch函数,核心是不允许图片和标签分别独立shuffle。

5.6 验证集划分不固定导致精度虚高

现象:每次训练mAP波动很大,看起来有0.1以上的差距,但代码和数据没变过。

原因:随机种子没有固定,每次启动训练时验证集自动重新划分,模型实际上在不一样的验证子集上评估,数值没有可比性。

解决:固定随机种子并保存一份验证集文件名列表到磁盘,后续每次实验用同一个列表参与评估。文件列表本身就是一套完整的验证集定义,比在代码里依赖随机状态可靠得多。

问题关键现象排查思路
类别错位预测label与目标不符检查class_id映射
尺寸不符cx、cy超界强制用真实图片尺寸
空标签文件0字节label文件区分空白样本与转换丢失
泛化不足val高、实测差增强对比度与亮度抖动
标签不同步图片无对应label按文件名同步复制
验证集漂移mAP波动大固定种子并保存列表

6. 进阶用法:从类别均衡检查到小样本快速验证

数据集自带5328张图和七类标注,但类别均衡程度不会天然完美。实际使用时要先跑一次长尾分布检查,确认是否有类别框数严重偏少。如果发现某类占比低于5%,两个方向可以调整:一是把这类目标单独复制做几次mosaic增强,补足训练样本量;二是调整类别损失权重,让模型对少样本类别更加敏感。前者的风险是可能过拟合,后者的风险是其他类别精度下降,实践中两种方法配合使用效果好一些。

确定类别分布没有大问题后,在我自己的项目习惯里,不会直接跑全量80到100个epoch。先用20个epoch、小batch做一轮管道验证,目标是确认数据读取、目录结构、标签映射、loss下降趋势都没有异常。这个过程相当于给整个训练流程做冒烟测试,能省掉大量等待全量训练结束后才发现配置错误的返工时间。检查点很明确:loss曲线是否在前几个epoch明显下降,训练分类别指标是否正常,验证集mAP是否在合理区间。如果这些都没问题再上全量训练。

整个流程跑通之后,最后一步是分类别评估。整体mAP是一个平均指标,弱类被强类掩盖是常有的事。用pycocotools跑COCO指标时,按类别输出AP值,找出模型表现最差的两三类,针对性做增强或补充样本,比盲目调超参数更有效。

我这个习惯是被一次比赛经历逼出来的。当时调了一周的参数,整体mAP稳步上涨,但提交时才注意到模型对“橡胶垃圾”这个类别几乎完全失效,原因是训练时忽略了类别均衡检查,橡胶类样本少且与塑料类高度混淆,模型把所有亮点物体都预测成了塑料。从那以后,我每次拿到新数据集都强制先走一遍分布统计、小样本验证、分类别评估这三个步骤,再谈调参。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进