简介:面向水下垃圾自动识别场景,这份数据集可用于训练和评估目标检测模型,适合从事水下环境监测、海洋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,后面四列是归一化到 0~1 的 x_center、y_center、width、height;VOC 格式把每张图的标注写成一个 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 |
| 类别定义 | <object><name>标签 | 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_ok=True) 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 | 目标总数 | 包含该类的图片数 | 图片占比 |
|---|---|---|---|
| 0 | 18420 | 862 | 78.4% |
| 1 | 2234 | 191 | 17.4% |
| 2 | 1730 | 202 | 18.4% |
| 3 | 842 | 96 | 8.7% |
| 4 | 386 | 47 | 4.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(f"datasets/trash/{split}/images", exist_ok=True) os.makedirs(f"datasets/trash/{split}/labels", exist_ok=True) shutil.copy(f"images/{name}.jpg", f"datasets/trash/{split}/images/") shutil.copy(f"labels/{name}.txt", f"datasets/trash/{split}/labels/")脚本先把同一场景的所有图片放进一个桶,打乱的是场景列表,而不是单张图片,这样同一个场景的视频帧不会同时落在训练集和验证集里。场景划分完之后,还需要配套一份 data.yaml:
train: 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 data=datasets/trash/data.yaml model=yolov8n.pt epochs=50 imgsz=640 batch=16 patience=10三个关键参数值得展开。model=yolov8n.pt用的是 nano 规模预训练权重,参数量最小,一轮训练时间最短,用来验证数据管线足够;如果你之前用 yolov5 训练过自己的数据集,会发现 v8 的这条入口命令把此前分散的配置文件收敛进了子命令里,从指定数据到指定模型都在一行内完成;patience=10意味着验证集指标连续 10 个 epoch 不提升就早停,水下数据噪声大、训练曲线震荡频繁,没有早停会让实验时间不可控;imgsz=640是 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.3~0.7 之间的预测框导出来,人工过一遍。这些框里通常包含两类结果——模型检测到了但标注里没有的「漏标目标」,以及预测框与真实框边界错位的「弱标注框」。
yolo predict model=runs/detect/train/weights/best.pt source=datasets/trash/test/images conf=0.3 save_txt=True save_conf=Truesave_conf=True让每个 txt 里多输出一列置信度,便于后续按置信度分桶筛选。把 0.3~0.7 区间的预测框单独导出成一张拼图,标记出置信度最高的前 50 个误检框,回原图上人工复核,补齐漏标目标,修正错位边界,再把标签合并回 train 集合重新训练。这样一轮难例挖掘对 mAP50-95 的提升,通常比从 yolov8n 换到 yolov8s 带来的收益更大——因为水下数据集的瓶颈从来不在模型容量,而在标注与分布本身。
本文还有配套的精品资源,点击获取