
简介面向水下垃圾自动识别场景这份数据集可用于训练和评估目标检测模型适合从事水下环境监测、海洋AI应用的学生和工程师使用。压缩包大小为173.21MB解压后包含7685个xml标注、7684个jpg图像和7684个txt文本共两万余个文件资源按train、test、val划分为训练集、验证集和测试集图像与标注一一对应其中xml和txt均为标注文件可分别适配常规检测框架和YOLO格式。标注信息涵盖物体类别与目标位置可直接用于YOLO、Faster R-CNN等主流检测算法的训练与评估已有659人浏览学习适合用来构建和优化水下垃圾识别模型。使用中需关注水下光照、透明度等因素对图像质量的影响可结合预处理手段增强模型泛化能力。对于需要系统掌握水下目标检测数据组织方式、快速开展模型训练的开发者这是一份结构清晰、即取即用的基础数据集。1. 水下垃圾检测数据集.zip打开之前先确认里面装的是什么这份 zip 通常来自论文附件、网盘分享或课题组的团队盘体积从几百 MB 到几 GB 不等口碑两极分化有的人解压即用mAP 直接到 0.6 以上更多的人解压后目录混乱、标注缺行、类别错位光整理就耗掉一两天。原因在于水下垃圾检测横跨水下视觉与环保工程数据集的发布者往往是环境或海洋学科的团队采集设备五花八门ROV、潜水员手持、监控相机而标注规范又常引用不同框架导致「zip 里是什么格式、质量如何」完全不可预知。下面整个处理流程要解决的是拿到这样一份 zip怎么系统地校验和解开它怎么把零散的原始标注整理成能直接喂给 YOLOv8 的可训练数据集以及怎么通过一轮最小训练快速判断这份数据的真实价值。整个过程不依赖任何付费工具把校验、格式转换、类平衡统计、划分和验证检验串成同一条管线。2. 水下垃圾检测数据集的构成与标注格式先搞清 zip 里装的是 YOLO、VOC 还是 COCO拿到 zip 之后先想清楚一件事这份数据的「声明格式」和「实际格式」是否一致。公开渠道流传的目标检测数据集标注组织方式基本逃不出三种YOLO、VOC 和 COCO。YOLO 格式每张图片对应一个 .txt第一列是 class_id后面四列是归一化到 01 的 x_center、y_center、width、heightVOC 格式把每张图的标注写成一个 xml 文件坐标是 xmin、ymin、xmax、ymax 的绝对像素COCO 格式则是把所有标注汇总进一个 json通过 images、annotations、categories 三个数组索引。水下垃圾数据集的一个常见问题是 README 里写着「支持 YOLOv5/YOLOv8 直接训练」解压后发现标注却全部是 xml 或 json。2.1 三种标注格式的核心差异与快速识别先看扩展名再看目录名基本就能判断格式不需要打开标注文件逐行阅读。下面是日常处理时最常用的判别速查表观察特征VOC 格式COCO 格式YOLO 格式标注文件扩展名.xml.json.txt每图一个坐标表示xmin, ymin, xmax, ymax 绝对像素bbox 为 x, y, w, h另有 segmentation归一化 x_center, y_center, w, h类别定义objectname标签categories 数组里的 id 与 name 映射每行首列的 class_id 整数目录规律JPEGImages/ 与 Annotations/ 平级annotations/ 下按 train/val 分目录images/ 与 labels/ 一一对应还有一种更省事的识别命令unzip -l只看目录路径不把整个压缩包解出来就能看到全貌。下面这条命令会列出压缩包内所有去重后的顶层目录unzip -l underwater_trash_dataset.zip | awk {print $4} | sed s|/[^/]*$|| | sort -u | head -20这条命令的执行逻辑是用 unzip -l 列出包内所有文件路径awk 取出文件名那一列sed 把每行最后一个斜杠及之后的内容删掉得到目录路径sort -u 去重排序后整个包的结构就打印出来了。看到 labels 目录基本可以认定是 YOLO 系看到 Annotations 加 JPEGImages 就是 VOC只有单个大 json 则十有八九是 COCO。这一步的成本几乎为零却能把后面所有流程的方向定下来。2.2 水下成像环境对标注质量的直接影响格式问题靠识别就能解决标注质量问题却没办法靠任何脚本自动根治。水下成像有几个特定现象水体对红光的吸收最严重所以图像大面积偏蓝绿色对比度天然低于地面数据悬浮颗粒marine snow会在画面里形成大量和垃圾外观相似的小亮点垃圾在底床或水面堆叠时标注员只能凭经验决定边界画到哪里。这些现象直接造成两个结果近景、清晰样本的标注质量高而远景与模糊区域的标注质量波动很大类别判定主观性强「塑料片」和「海藻碎片」这类目标在低照度下可能被不同的标注员标成不同的类。处理水下数据时很多人直接套用通用目标检测比如无人机航拍数据集、KITTI、DOTA 这类明亮场景的预处理和增强配置这其实会削弱水下数据本身的特征。常见做法是先做白平衡校正再在训练管线的数据增强里控制颜色扰动强度。以 YOLOv8 的 HSV 增强参数为例把 hsv_h 降到 0.015 以下、hsv_s 保持在 0.5 左右既保留了模型对光照变化的鲁棒性又不至于用随机色偏把已经偏蓝的水下原图污染成另一个域。2.3 解压后的第一项检查图片与标注一一对应任何训练脚本跑起来之前我习惯先做一个完整性检查统计图片数、标注数、孤儿文件并打印分辨率分布。这一步能一次性暴露目录错位、扩展名不一致、图片损坏等基础问题。下面这个脚本适用于 YOLO 风格的目录组织import os from collections import Counter from PIL import Image img_dir images label_dir labels names sorted([os.path.splitext(f)[0] for f in os.listdir(img_dir) if f.endswith((.jpg, .jpeg, .png))]) label_names sorted([os.path.splitext(f)[0] for f in os.listdir(label_dir) if f.endswith(.txt)]) set_names, set_labels set(names), set(label_names) print(f图片总数: {len(set_names)} 标注总数: {len(set_labels)}) print(f有图无标注: {len(set_names - set_labels)} 有标注无图: {len(set_labels - set_names)}) if set_names - set_labels: print(孤儿图片示例:, sorted(set_names - set_labels)[:5]) dims Counter() for name in names[:500]: for ext in (.jpg, .jpeg, .png): path os.path.join(img_dir, name ext) if os.path.exists(path): with Image.open(path) as im: dims[im.size] 1 break print(前500张图片分辨率分布(前5):, dims.most_common(5))脚本先把 images 和 labels 下的文件主名各读成一个集合用集合差找出「有图无标注」和「有标注无图」两类问题然后对前 500 张图片做分辨率统计。之所以只取前 500 张是因为分辨率分布这种粗粒度指标不需要全量扫描采样足够暴露问题。如果输出里同时出现多种宽高比比如 1920×1080 和 640×384 混在一起后续训练时必须注意 imgsz 参数的选择否则大量图片会被强制缩放并在两侧补灰边既浪费计算量又干扰特征学习。图片总数和标注总数差很多时先不要继续往下走回到第 2.1 节的目录识别多半是格式判断错了。3. 水下垃圾检测数据集 zip 的校验与解压排错从完整下载到目录落地数据集压缩包普遍体积偏大几 GB 的文件经网盘或者 HTTP 传输中途损坏的概率并不低。常见的现象是解压到一半报错或者解压后个别图片损坏、部分标注文件解不出来。与其等到训练时 Image 打开失败才回头处理不如在解压阶段就把压缩包本身的安全性确认完。3.1 先做哈希与结构校验解压不是第一步发布者如果提供了 SHA256 哈希值先做一步比对成本极低。没有的话也要用unzip -t对压缩包做一次完整性测试这会遍历整个 zip 重新计算每个文件的 CRC 并和记录值比对期间不写任何文件到磁盘# 有官方哈希值时做精确比对 echo 官方SHA256 underwater_trash_dataset.zip | sha256sum -c - # 没有官方哈希时至少跑一次 CRC 遍历校验 unzip -t underwater_trash_dataset.zip | tail -5sha256sum -c -从管道读取格式为「哈希值 空格 空格 文件名」的校验文本匹配成功会输出文件名加 OK否则列出 mismatch。unzip -t末尾几行显示校验统计正常时最后能看到 No errors detected 或等价信息出现bad zipfile offset一类的文本说明包内结构已经有问题即使个别文件能解出来也不能信任。顺带说一个反直觉的点zip 的 file header 里有每个文件的 CRC 值但很多下载器只校验整体文件大小不校验内容这会导致「大小完全一致但内容损坏」的包被误认为完整。所以数据集发布方在 README 里写清楚 SHA256是比文件大小可靠得多的做法。3.2 三类高频 zip 报错的识别与处理压缩包出错的问题常年集中在三种情况下载不完整、分卷压缩、加密保护。下载不完整典型报错是error read zip archive以及invalid zip archive: could not find eocd。这里 EOCD 是 zip 的中央目录结尾记录End of Central Directory Record它固定在文件末尾EOCD 缺失说明下到的文件根本没到末尾靠修复工具很难救回。分卷压缩常见于网盘拆包表现为data.z01、data.z02加一个data.zip直接打开主文件会提示损坏需要把全部分卷放同一目录再用 7-Zip 打开.zip主文件解压如果缺了某个 z01会停留在等待该分卷的状态。加密保护则表现为解压时反复提示输入密码。标准处理方式整理如下现象或报错直接原因标准处理error read zip archive传输损坏或磁盘坏道尝试zip -FF damaged.zip --out repaired.zip修复一次失败就重新下载invalid zip archive: could not find eocd文件尾部缺失下载未完整核对文件大小与源站是否一致续传或重下提示需要密码发布者设置了压缩包密码在 README 与发布页面找密码联系发布者获取不要使用网络上的 zip 密码移除工具这里需要明确一点对数据集场景密码破解没有性价比。数据集 zip 加密的目的大多是防止分享链接被爬走后直接转卖密码往往就写在同一个分享说明里。网络流传的这类工具要么在本地穷举字典耗费数小时要么本身捆绑风险代码把整个数据集和运行环境的账号安全搭进去完全不值得。注意压缩包报错后先保留原文件再重下避免新下载中断后两头都没有可用的完整包。3.3 解压后的文件配额核对确认没有丢文件解压完成后自己再核对一次文件总配额。这一步是为了防止「解压过程被中断但没报错」或者「解压工具默认跳过某些文件」的情况做法是把压缩包内记录的文件总数与磁盘上实际文件数对比# 压缩包内文件总数最后一行 Total unzip -l underwater_trash_dataset.zip | tail -1 # 当前目录实际文件数排除隐藏目录 find . -type f -not -path ./.* | wc -lunzip -l的末尾输出形如1975 files, 2847190330 bytes而 find 统计的是磁盘上真实落地的文件数。两者一致基本可以确认解压完整不一致时用ls -la查一下是否生成了解压日志或临时文件将数字抬高排除后再考虑是解压流程丢文件还是压缩包本身目录条目与内容不符。这一步做完数据文件层面的准备才算真正落地。4. 从 zip 到可训练数据集水下垃圾检测数据的格式转换、类平衡与划分数据完整落盘后进入格式转换与整理的阶段。这个阶段做的事情是把原始标注统一成目标框架需要的格式统计类别分布并做必要的增强处理再按场景分组划分训练集、验证集、测试集。4.1 把 VOC/COCO 统一转成 YOLO 格式的转换脚本YOLOv8 训练时通过 data.yaml 指定数据集路径标注文件必须是每图一个 txt如果这份 zip 里是 VOC 格式需要先做转换。下面这段脚本处理最常见的 VOC xml 转 YOLO txt并显式维护一个类别映射表import os, glob from xml.etree import ElementTree as ET class_map {plastic: 0, fishing_net: 1, glass: 2, metal: 3, rubber: 4, marine_plant: 5} def convert_voc_to_yolo(xml_path, out_dir): tree ET.parse(xml_path) root tree.getroot() size root.find(size) w int(size.find(width).text) if size is not None else 640 h int(size.find(height).text) if size is not None else 640 out_name os.path.splitext(os.path.basename(xml_path))[0] .txt with open(os.path.join(out_dir, out_name), w) as f: for obj in root.iter(object): cls obj.find(name).text if cls not in class_map: print(f跳过未映射类别: {cls}, 文件: {os.path.basename(xml_path)}) continue 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) x_c ((xmin xmax) / 2) / w y_c ((ymin ymax) / 2) / h bw (xmax - xmin) / w bh (ymax - ymin) / h f.write(f{class_map[cls]} {x_c:.6f} {y_c:.6f} {bw:.6f} {bh:.6f}\n) os.makedirs(labels, exist_okTrue) for xml_file in glob.glob(annotations/*.xml): convert_voc_to_yolo(xml_file, labels)这段脚本的重点在归一化计算VOC 的 bndbox 是左上角和右下角的绝对坐标YOLO 需要的是中心点坐标加宽高且都必须除以图片宽高。脚本优先从 xml 的 size 字段读宽高因为部分数据集的 xml 与图片实际尺寸不一致用 size 字段能保证与标注时的参照框一致。脚本会把 class_map 之外的类别打印出来跳过这类未映射类别在转换后一定要人工确认否则类别总数和 data.yaml 对不上训练时类别错位是极难排查的问题。如果源格式是 COCO 的 json处理路径不同json 里 images 数组记录每张图的 id 与尺寸annotations 数组用 image_id 关联标注。转换时先按 image_id 建立图片信息字典再遍历每条 annotation 计算归一化坐标。业界常用做法是自己写一次性的转换脚本不要引入过多第三方依赖因为每个数据集 json 的字段命名都有细微差别通用工具往往这类脏数据上翻车。4.2 类别分布统计找出占比不高但决定 mAP 的尾类格式统一后先跑一个类别分布统计确认数据集的「底色」。水下垃圾数据集的类别不平衡非常典型塑料类目标占了大头渔网、玻璃、金属这类目标数量少且形态复杂。一个直观的统计脚本import os, glob from collections import Counter total_boxes Counter() class_img_count Counter() boxes_per_img [] for label_file in glob.glob(labels/*.txt): with open(label_file) as f: lines [ln.strip().split() for ln in f if ln.strip()] boxes_per_img.append(len(lines)) for fields in lines: cls_id int(fields[0]) total_boxes[cls_id] 1 class_img_count[cls_id] 1 img_count len(glob.glob(images/*.jpg)) print(f{类别:6}{目标数:8}{图片数:8}{图片占比}) for cls_id in sorted(total_boxes): ratio class_img_count[cls_id] / img_count * 100 print(f{cls_id:6}{total_boxes[cls_id]:8}{class_img_count[cls_id]:8}{ratio:.1f}%) print(f平均每图目标数: {sum(boxes_per_img) / len(boxes_per_img):.2f})脚本的核心是区分「目标总数」和「出现图片数」这两个指标一个类别目标数很多但集中在少数几张图里模型容易对这少数图片过拟合目标数中等但分散在很多图片里反而是健康的分布。下面是一份典型的输出形态类别 ID目标总数包含该类的图片数图片占比01842086278.4%1223419117.4%2173020218.4%3842968.7%4386474.3%类别 4 只出现在 4.3% 的图片里这类尾类决定了模型的召回下限。对于尾类常见做法是不要急着调 loss 权重先把包含尾类的图片做一次离线增强复制拷贝加旋转、亮度扰动放进训练集让模型在每个 epoch 里能看到它们更多次。更进阶的做法是抠出尾类目标粘贴到其他水下背景上做合成数据这在「数据不够、补标太贵」的场景下比任何模型侧的改动见效都快。4.3 train/val/test 划分按场景分组而不是按图片随机划分阶段有个容易被忽略的坑数据如果源自一段段水下视频的抽帧连续几帧画面高度重叠。直接按图片随机划分验证集会包含大量训练集的近邻帧val 指标虚高部署到新水域时立刻打回原形。正确的做法是按场景分组再划分。假设文件名带场景前缀划分脚本可以写成import os, random, shutil from collections import defaultdict names [os.path.splitext(f)[0] for f in os.listdir(images) if f.endswith(.jpg)] groups defaultdict(list) for name in names: scene name.split(_)[0] # 按文件名前缀分场景 groups[scene].append(name) random.seed(42) scene_list list(groups.keys()) random.shuffle(scene_list) n_scene len(scene_list) train_scenes scene_list[:int(n_scene * 0.8)] val_scenes scene_list[int(n_scene * 0.8):int(n_scene * 0.9)] test_scenes scene_list[int(n_scene * 0.9):] for split, scenes in [(train, train_scenes), (val, val_scenes), (test, test_scenes)]: for sc in scenes: for name in groups[sc]: os.makedirs(fdatasets/trash/{split}/images, exist_okTrue) os.makedirs(fdatasets/trash/{split}/labels, exist_okTrue) shutil.copy(fimages/{name}.jpg, fdatasets/trash/{split}/images/) shutil.copy(flabels/{name}.txt, fdatasets/trash/{split}/labels/)脚本先把同一场景的所有图片放进一个桶打乱的是场景列表而不是单张图片这样同一个场景的视频帧不会同时落在训练集和验证集里。场景划分完之后还需要配套一份 data.yamltrain: datasets/trash/train/images val: datasets/trash/val/images test: datasets/trash/test/images nc: 6 names: [plastic, fishing_net, glass, metal, rubber, marine_plant]最后用一行命令核对三个集合的图片数量确认与划分比例预期一致for s in train val test; do echo $s: $(ls datasets/trash/$s/images | wc -l); done5. 用 YOLOv8 验证水下垃圾检测数据集的真实质量最小训练与难例挖掘到这一步数据已经整理成可训练的形态但「整理对没对」需要一次真实的训练来验证。最小训练的思路是用最小的模型、最少的 epoch把整条数据管线的正确性验证一遍。真正的精度实验可以之后再做这一轮的目的只有一个——让数据的问题在 30 分钟内暴露出来。5.1 最小训练配置与三个关键参数yolo detect train datadatasets/trash/data.yaml modelyolov8n.pt epochs50 imgsz640 batch16 patience10三个关键参数值得展开。modelyolov8n.pt用的是 nano 规模预训练权重参数量最小一轮训练时间最短用来验证数据管线足够如果你之前用 yolov5 训练过自己的数据集会发现 v8 的这条入口命令把此前分散的配置文件收敛进了子命令里从指定数据到指定模型都在一行内完成patience10意味着验证集指标连续 10 个 epoch 不提升就早停水下数据噪声大、训练曲线震荡频繁没有早停会让实验时间不可控imgsz640是 YOLOv8 的默认值但前面分辨率检查如果发现宽高比割裂严重这里建议改成 800 或直接按数据集中最主流的比例裁剪。训练结束后主要看 val 输出里的 mAP50 和 mAP50-95前者反映框选得对不对后者反映框定位精度和对尺度变化的表达能力。首轮结果的判读不能只盯一个指标要两个值放在一起看下面这个经验区间可以帮助快速定位问题指标范围可能问题优先处理mAP50 0.3标注质量差或 train/val 场景重叠可视化标注框抽查检查划分脚本是否按场景分组0.3 ≤ mAP50 0.6尾类样本过少或小目标占比高复查 4.2 节类别统计对尾类做增强复制mAP50 ≥ 0.6 且 mAP50-95 0.35框回归精度不足或目标尺度差异大提高 imgsz 到 800换 yolov8s 及以上模型5.2 用混淆矩阵定位最容易被搞混的两个类训练输出的 runs/detect/train/ 下会生成 confusion_matrix.png。水下数据里典型的混淆模式是 plastic 与 fishing_net 互相污染以及 marine_plant 被误报成 glass。看矩阵时关注两类位置真实类别行上背景列占比较高说明这类目标大面积漏检预测类别列上背景行占比较高说明误检严重。针对这些类不要把类别权重盲目调大更立竿见影的做法是把互相混淆的类别图片挑出来做白平衡后再增强让模型见过「同样目标在不同光照下的样子」这比任何 loss 改动都更贴近问题本质。5.3 难例挖掘低置信度预测框补标当 mAP 曲线已经平稳但达不到业务指标时问题大概率不在模型而在标注。这是难例挖掘最好的切入时机跑一遍推理把置信度在 0.30.7 之间的预测框导出来人工过一遍。这些框里通常包含两类结果——模型检测到了但标注里没有的「漏标目标」以及预测框与真实框边界错位的「弱标注框」。yolo predict modelruns/detect/train/weights/best.pt sourcedatasets/trash/test/images conf0.3 save_txtTrue save_confTruesave_confTrue让每个 txt 里多输出一列置信度便于后续按置信度分桶筛选。把 0.30.7 区间的预测框单独导出成一张拼图标记出置信度最高的前 50 个误检框回原图上人工复核补齐漏标目标修正错位边界再把标签合并回 train 集合重新训练。这样一轮难例挖掘对 mAP50-95 的提升通常比从 yolov8n 换到 yolov8s 带来的收益更大——因为水下数据集的瓶颈从来不在模型容量而在标注与分布本身。本文还有配套的精品资源点击获取