☰
水下垃圾检测实战:YOLOv8数据集训练与标注全流程解析
2026/10/8 17:29:53 网站建设 项目流程

简介:面向水下环境监测与深度学习目标检测研究者,这份数据集围绕水下垃圾自动识别与定位任务构建,提供了一套结构完整的训练与验证语料。压缩包整体约173.21MB,共2000个文件,主要包含jpg原始图像、xml标注文件和txt标签说明,其中xml记录目标类别及边界框位置,txt便于在YOLO等框架中直接读取,文件按test、train、val划分为三个目录,分别用于模型评估、参数学习和训练调优。目前已有659人学习下载。数据集内容贴近真实水下场景,可帮助使用者减少数据采集与清洗成本,直接投入到垃圾检测模型的实验比对中。结合YOLO、Faster R-CNN等主流算法,能够系统练习数据预处理、模型训练、验证集调参和测试集泛化评估的完整流程,适合环境保护领域的研究人员、学生以及计算机视觉开发者作为标准化训练基准。

1. 从标注文件到自动清理:这个数据集解决了水下感知的哪个关键缺口

做水下垃圾检测的人,往往先被数据劝退:公开的 KITTI 数据集以道路场景为主,水下环境缺少带标注样本,而自己下水拍摄又涉及设备、光照和标注成本。这份水下垃圾检测数据集.zip把 train/val/test 三部分和 YOLO 格式的 txt 标注直接打包,解压后就能用于训练。第一次接触时,真正需要花时间的不是模型结构,而是理解标注的归一化坐标、调整水下图像增强策略,以及正确配置 YOLOv8 的训练流程。下面从标注文件本身拆起,给出检查脚本、训练命令和部署验证路径,让拿到压缩包之后可以直接动手复现。

2. YOLO 标注格式与数据集目录剖析:先别急着开训

2.1 解压后先看目录:文件分布与命名规则

拿到 zip 后,我一般会先把它解压到一个不含中文和空格的路径,例如~/datasets/underwater_garbage,然后使用tree或find快速看清顶层结构。这个数据集的压缩包内部通常遵循images与labels分离的布局,train、val、test 三个子集要么平铺在各自目录下,要么再按images/train、labels/train分两层。从文件名列表来看,标注文件形如obj1663_frame0000562.txt,其中obj1663表示原始视频采集中某一段目标的编号,frame0000562表示帧序号,这种命名保留了下水采集源的时间信息,方便后续在视频上做序列级验证。

# 解压到指定目录,-d 保证不散落当前路径 unzip underwater_garbage_detection.zip -d ~/datasets/underwater_garbage cd ~/datasets/underwater_garbage # 查看所有子目录,确认 train/val/test 是否齐备 find . -type d | sort # 统计标注文件与图片数量,先做初步匹配 find . -name "*.txt" | wc -l find . \( -name "*.jpg" -o -name "*.png" \) | wc -l

第一条命令的-d指定解压目录,避免解压后文件堆在当前工作目录;随后cd进入数据目录。第三条命令按字母顺序打印所有子目录,能立刻看出是否存在 test、train、val 三个集合。第四条统计 txt 总数,第五条统计 jpg 和 png 图片总数;两者数量接近才说明标注基本配套。如果图多标少,常见原因是某些视频帧没有垃圾对象,对应的空 txt 被省略,这需要在训练时让 YOLO 接受空标签图。

还需要检查同一个文件前缀在images和labels下是否都存在。若只有obj1663_frame0000562.txt而没有对应图片,通常是压缩包不完整或者切分脚本误删了样本,这种文件会导致后续训练时 YOLO 报出类似image not found的错误。常见做法是写一个循环:

# 遍历 labels 下的每个 txt,验证是否有同名图片 for f in labels/*.txt; do base=$(basename "$f" .txt) if [ ! -e "images/$base.jpg" ] && [ ! -e "images/$base.png" ]; then echo "missing image for $base" fi done

命令中basename "$f" .txt去掉后缀得到obj1663_frame0000562这样的前缀,然后分别检查.jpg和.png是否存在。若输出内容过多,可以先重定向到文件再人工抽查,因为单张缺失图并不会立刻中断训练,只会在某个 epoch 报错,提前排查能节省大量时间。

2.2 txt 标注中的归一化坐标:四字段的完整解读

YOLO 格式的 txt 每一行代表一个目标,格式与 COCO 的[xmin, ymin, xmax, ymax]完全不同,很多新手会在这里被卡住。打开任意一个标注文件,内容大概是这样:

0 0.482812 0.523611 0.184375 0.252778 1 0.317188 0.659722 0.109375 0.136111

第一列是类别编号,必须从 0 开始;第二、三列是目标框中心点的横纵坐标,都除以了图像宽高;第四、五列是框的宽和高,也做了同样的归一化,所以后四个值理论上都在 0 到 1 之间。这种设计的好处是与图像绝对尺寸无关,模型不需要在输入层再做一次像素映射。

字段含义值域
第1列类别编号0 ~ (类别数-1)
第2列中心点 x / 图像宽0 ~ 1
第3列中心点 y / 图像高0 ~ 1
第4列目标框宽 / 图像宽0 ~ 1
第5列目标框高 / 图像高0 ~ 1

这里需要特别注意:类别编号是否连续、是否从 0 开始,直接决定了后面 YAML 文件里的nc值。如果某个 txt 中出现类别编号 9,那么至少要有 10 个类别,即使其中有些类别在训练集中一个样本都没有,也必须把空位保留,否则模型会无法对齐输出层。有的工具导出的坐标是像素绝对值,例如[xmin, ymin, xmax, ymax],这时需要转换:

# COCO/像素坐标 转 YOLO 归一化坐标 x_center = (xmin + xmax) / 2 / image_width y_center = (ymin + ymax) / 2 / image_height w = (xmax - xmin) / image_width h = (ymax - ymin) / image_height

转换公式的逻辑是先把左上角与右下角坐标取平均得到中心点,再用像素宽度除以图像宽度做归一化,最终结果必须落在 0~1 区间内。如果不确定image_width从哪来,用cv2.imread读图后取shape[1]和shape[0]即可。

2.3 写一个检查脚本,把坏标注挡在训练前

训练前最值得做的一件事是批量校验标注文件的合法性。我一般会在数据根目录下放一个check_labels.py,用几秒钟跑完所有文件,避免训练中途才发现标注格式错误。脚本如下:

# check_labels.py:校验 YOLO txt 标注的字段数量与取值范围 from pathlib import Path labels_dir = Path("labels") totals = {} errors = [] for txt in labels_dir.glob("*.txt"): for line in txt.read_text().strip().splitlines(): parts = line.split() if len(parts) != 5: errors.append(f"wrong field count: {txt.name} -> {line}") continue try: cls, x, y, w, h = map(float, parts) except ValueError: errors.append(f"not float: {txt.name} -> {line}") continue if not (0 <= cls <= 100 and 0 <= x <= 1 and 0 <= y <= 1 and 0 <= w <= 1 and 0 <= h <= 1): errors.append(f"out of range: {txt.name} -> {line}") continue totals[int(cls)] = totals.get(int(cls), 0) + 1 print("class frequency:", dict(sorted(totals.items()))) print("error count:", len(errors)) for e in errors[:20]: print(e)

脚本分为三部分:第一部分读入整个 txt,用read_text().strip().splitlines()拿到非空行,若文件内容为空则不会进入循环,因此空标注不会报错;第二部分对每一行按空格拆分,字段数不是 5 就记录错误,再通过map(float, parts)做类型转换,转换失败说明混入了标点或特殊字符;第三部分做范围检查,cls的界限放宽到 100,只排除明显异常,四个坐标则严格要求在 0~1 之间。最后打印每个类别的样本次数和错误列表,可以快速评估类别分布是否均匀。

要注意labels_dir.glob("*.txt")只覆盖当前目录,如果数据按 train/val/test 拆分,最好改用labels_dir.rglob("*.txt")递归查找子目录。另外不要只看错误数,还要看 class frequency 是否为 0。比如类别 3 完全没有样本,可能是原始标注里该物体太少,也可能是切分时把包含该类别的图片全分到了测试集,这时候你需要在全部数据范围内重新统计一遍,而不是靠训练集样本数去猜。

3. YOLOv8 训练水下垃圾检测模型:从 yaml 到完整流程

3.1 按 YOLOv8 的预期重新组织数据目录

YOLOv8 的训练流程和 YOLOv5 非常接近,都需要一个 YAML 文件来描述数据根目录、训练集图片路径、验证集图片路径、测试集图片路径、类别数和类别名。许多自带标注的数据集压缩包并不一定按这个规范组织,常见的问题是图片和标注混放在同一个目录,或者 train/val/test 是按原始采集日期分的,而不是随机分配。如果你之前只跑过YOLOv5 训练自己的数据集,那这里可以直接沿用相同的目录结构。

推荐的结构如下:

datasets/ └── underwater_garbage/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ └── labels/ ├── train/ ├── val/ └── test/

images下放图片,labels下放同名 txt,训练时 YOLO 会根据图片路径自动查找对应标签。如果原始目录不是这个结构,比如所有图像在images/下、所有标注在labels/下且没有按 train/val/test 划分,可以用下面的 shell 快速按 8:1:1 比例划分:

# 创建目标子目录 mkdir -p images/{train,val,test} labels/{train,val,test} # 遍历 images 下所有 jpg,按随机数划分到三个子集 for f in images/*.jpg; do base=$(basename "$f" .jpg) n=$((RANDOM % 10)) if [ $n -lt 8 ]; then split=train; elif [ $n -lt 9 ]; then split=val; else split=test; fi mv "$f" "images/$split/" mv "labels/${base}.txt" "labels/$split/" done

这里用$RANDOM % 10生成 0 到 9 的随机数,小于 8 进 train,等于 8 进 val,等于 9 进 test。比例约为 8:1:1,但随机划分不能保证类别平衡,对类别不平衡的数据集后续要再统计一下。划分后需要定义 YAML:

# 数据配置文件,路径建议写绝对路径 path: /home/user/datasets/underwater_garbage train: images/train val: images/val test: images/test nc: 4 names: 0: plastic 1: net 2: bottle 3: can

path是数据根目录,train和val分别填写相对path的路径;nc要与检查脚本统计到的最大类别编号 +1 相等。names列表的下标就是标注文件里的数字类别编号,不能打乱顺序。比如原数据集的类别定义可能是0为bg、1为fish,你需要在names中保留bg的索引位置,即使模型最终不预测背景。

3.2 训练命令与关键超参数:如何兼顾精度与速度

完成目录组织后,安装最新的 ultralytics 包,然后运行:

pip install ultralytics # 使用 yolov8n 起步,epochs 设 150,patience 为 20 yolo detect train data=underwater_garbage.yaml model=yolov8n.pt \ epochs=150 imgsz=640 batch=16 patience=20 \ project=runs/underwater exp_name=exp_1

这里选用yolov8n.pt是因为水下机器人通常部署在嵌入式设备上,n 的参数量约 3.2M、计算量约 8.7 GFLOPs,中等 GPU 也能很快跑完几百轮。imgsz=640表示输入边长,如果原图像素较高且显存充足,提高到 1280 可以改善小目标检测,但训练时间接近翻倍。batch=16在 24GB 显存上没有压力,如果是 8GB 显卡就降到 8,再小的话利用参数累积模拟更大的 batch。

参数默认值说明
epochs100总训练轮数,水下数据集样本少可用150
batch16每个batch的图片数,按显存调整
patience10当验证指标连续N轮不提升时早停
lr00.01初始学习率,loss震荡时降到0.001
imgsz640输入图像边长,边做letterbox缩放
workers8数据加载进程数,Windows下设为0

很多教程不会强调patience,但对水下视频帧这种高度相关的数据,早停很有用。因为相邻帧之间相似度太高,验证集指标可能在前几轮虚高,后期才会回落。如果patience设置太小,模型会在真正收敛前停掉;我一般用 20,配合save_period=10保留中间权重,这样即使某轮指标波动,也不会丢失前期的有效权重。

训练时还要看终端里打印的参数数量。如果看到类似Model summary: 225 layers, 3157216 parameters的输出,且参数总量明显少于预训练权重说明的体量,可能是模型文件被误用成了其他格式。这个检查只需要一秒钟,但能避免浪费一整轮训练。

3.3 从训练日志中提前发现过拟合

YOLOv8 训练时会在终端同时输出train/box_loss、train/cls_loss和val/box_loss等指标。最常见的陷阱是只盯着train loss,看到它下降到很低就以为模型很好。需要把训练损失和验证损失放在同一张图上判断。训练结束后,runs/underwater/exp_1/results.csv里已经保存了每个 epoch 的所有指标,用 Python 直接画:

# 绘制训练与验证 box_loss 曲线 import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv("runs/underwater/exp_1/results.csv") print(df.columns.tolist()) # 确认列名 cols = [c for c in df.columns if "box_loss" in c] df[cols].plot(logy=True) plt.savefig("loss_curve.png", dpi=150)

先打印列名,避免不同版本的 ultralytics 命名不一致;然后用logy=True对数坐标,因为 box_loss 通常呈指数下降,普通线性坐标下前期下降速度会掩盖后期差异。如果验证损失持续上升而训练损失下降,说明过拟合。应对手段包括增大patience后的早停、把dropout=0.1调大、增加在线增强强度、或换成参数量更小的模型。

另一个常被忽略的信号是mAP50很高但mAP50-95很低。mAP50要求预测框与真实框的 IoU 超过 0.5 就算正确,而mAP50-95会在 0.5 到 0.95 之间取多个阈值,对定位精度更敏感。水下垃圾的形状不规则,如果出现这种差距,优先检查标注框是否存在外包太多背景或坐标错位,而不是盲目换模型。可以用yolo detect val在验证集上生成预测可视化,逐张查看误差大的样本。

4. 水下低照度与后向散射:用图像增强提升泛化

4.1 水下图像退化的物理模型与常见预处理选型

水下图像主要受到吸收与散射两类影响:水对不同波长的光吸收率不同,红光衰减最快,蓝绿光衰减慢,所以深水照片往往偏绿或偏蓝;悬浮颗粒造成后向散射,使图像蒙上一层雾或噪点。对比陆地上用 COCO 预训练权重,模型在海面以上学到的大量颜色和纹理线索在水下失效,这时单靠调超参数很难继续提升精度。最有效的办法是在进入 YOLO 之前做一次颜色校正和对比度增强。

常见的预处理选型包括:直方图均衡化(HE)可以对整图拉伸对比度,但当图像有局部过曝或阴影时容易破坏细节;限制对比度自适应直方图均衡(CLAHE)则在局部小块上做均衡,并用截断值限制增强幅度,更适合水下不均匀光照。还有灰度世界算法,假设全局平均色接近灰色,通过缩放 R/G/B 通道来消除色偏。下表列出各自的适用场景:

方法适用场景主要缺点
直方图均衡 (HE)整体对比度低会放大偏色
CLAHE局部光照不均需要调 clipLimit
灰度世界全局色偏大面积水体时估计不稳
UDCP浑浊水体计算量大,在线增强不现实

具体使用哪个,可以在小样本上训练两三轮看验证集趋势,不必一开始就追求复杂的恢复算法。至少我处理这份数据时,CLAHE 和灰度世界的组合在低成本场景下提升最直观。

4.2 CLAHE 离线增强:不改变标注的预处理实操

如果决定离线增强,需要注意增强只作用于图片,不修改 txt 标注。由于归一化坐标与像素分辨率无关,只要图片尺寸不变,标注就完全不需要动。下面这个脚本遍历所有训练集图片,转换为 LAB 色彩空间后只对亮度通道做 CLAHE,再转回 BGR 输出:

# 对训练图像批量做 CLAHE 增强,保留色度通道 import cv2 from pathlib import Path src_dir = Path("images/train") dst_dir = Path("images_enhanced/train") dst_dir.mkdir(parents=True, exist_ok=True) clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) for img_path in src_dir.glob("*.jpg"): bgr = cv2.imread(str(img_path)) if bgr is None: print(f"read fail: {img_path}") continue lab = cv2.cvtColor(bgr, cv2.COLOR_BGR2LAB) l, a, b = cv2.split(lab) l_enhanced = clahe.apply(l) # 只增强亮度 merged = cv2.merge((l_enhanced, a, b)) enhanced = cv2.cvtColor(merged, cv2.COLOR_LAB2BGR) cv2.imwrite(str(dst_dir / img_path.name), enhanced)

逻辑说明:createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))创建增强器,clipLimit是直方图截断上限,值越大对比度越强;tileGridSize是局部块大小。对 BGR 图像先转 LAB,是为了把亮度与颜色分离,只对亮度通道L做增强,保留a和b色度通道,避免色彩过度饱和。最后写回与源文件同名的图片。你可以在val上同样做一遍,但测试集最好保持原始状态,用于评估预处理在真实部署时是否介入。

有一个容易踩的坑:cv2.imread读不到路径时会返回None,脚本会直接异常退出,所以循环里加了if bgr is None判断。在批量处理前最好先打印任意一张图的大小,确认为(height, width, 3)。如果图片带 alpha 通道,COLOR_BGR2LAB会报错,需要先转成 BGR。

4.3 在线数据增强配置:让模型自己适应多变水体

离线增强只能覆盖一个固定颜色空间变换,实际部署时水下机器人的光源、深度、水体浊度都会变化,更稳妥的方式是利用 YOLOv8 自带的在线增强,在每次读取训练图片时动态施加随机扰动。下面这条命令在之前基础上调整了hsv_h、hsv_s、hsv_v和degrees:

# 加大亮度和饱和度扰动,模拟不同水体 yolo detect train data=underwater_garbage.yaml model=yolov8n.pt \ epochs=150 imgsz=640 batch=16 \ hsv_h=0.02 hsv_s=0.7 hsv_v=0.5 degrees=10

解释一下关键参数:hsv_h是色调扰动幅度,0.02 相当于在色环上轻微偏移,能模拟不同水色下微小的色调差异,调太大可能会改变物体原本颜色语义;hsv_s是饱和度扰动,水下图像饱和度通常偏低,设到 0.7 可以让模型见到从灰白到高饱和的渐变;hsv_v是亮度扰动,0.5 是较大范围,因为水下闪光灯会产生高亮区域,深水区域又可能存在阴影。degrees=10表示最多随机旋转 10 度,水下机器人姿态相对稳定,旋转太大会制造不合理的姿态。

YOLOv8 的mosaic增强默认开启,它把 4 张训练图拼接成一张,用于提升模型在不同背景下的泛化能力。但在水下垃圾检测中,很多目标是小尺寸垃圾,拼接后目标会被切到边界,甚至产生大量半截目标。如果训练 50 轮后box_loss下降很慢,可以尝试mosaic=0.5:

# 将 mosaic 启用概率降到一半,保留部分原始图 yolo detect train data=underwater_garbage.yaml model=yolov8n.pt \ epochs=150 imgsz=640 batch=16 mosaic=0.5

mosaic=0.5表示一半的训练样本使用马赛克增强,另一半用原始图,这样既保留拼接带来的背景多样性,也降低目标被截断的影响。跨 batch 的训练结果很难严格对比,所以每次改参数时建议单独建一个exp_name,方便后续比较。

5. 模型验证与导出:用 mAP50-95 判断是否真正可用

5.1 val 模式下的指标怎么看

训练结束后,首先执行:

# 在验证集上评估,输出 Precision/Recall/mAP yolo detect val model=runs/underwater/exp_1/weights/best.pt data=underwater_garbage.yaml

这个命令会跑一遍验证集,并在终端打印Precision、Recall、mAP50、mAP50-95。我通常把mAP50-95作为第一参考值。如果它过低而mAP50很高,说明框不够精确,可以尝试增大imgsz到 960 或 1280,或者从yolov8n换到yolov8m。不要一上来就动模型复杂度,先确认数据划分有没有问题。

5.2 导出 ONNX 并在边缘设备上推理

部署时最常见的转换格式是 ONNX:

# 导出 ONNX,opset 12 对老设备兼容性更好 yolo export model=runs/underwater/exp_1/weights/best.pt format=onnx imgsz=640 opset=12

导出的best.onnx可以用onnxruntime运行,输入是[1, 3, 640, 640]的归一化图像,输出通常两到三个 tensor,需要做非极大值抑制。这里不展开全部推理代码,但要注意预处理要和训练时一致,包括 letterbox、BGR 转 RGB、归一化到 0~1。若部署在 Jetson 上,可以用 TensorRT 进一步提速,但第一次导出先用 ONNX 验证精度对齐,避免引入新的不确定性。

5.3 一个容易被忽略的坑:zip 解压与文件配对

最后补充一个处理压缩包本身的细节。下载完水下垃圾检测数据集.zip后,先用unzip -t校验完整性:

# 测试 zip 是否完整,不实际解压 unzip -t underwater_garbage_detection.zip

输出末尾有No errors detected in compressed data之类的信息才算通过。如果提示error read zip archive或crc error,通常是下载过程中文件损坏,此时不要强行解压,否则后续训练会出现莫名奇妙的缺图问题。解压后还要清理隐藏文件:

# 删除 macOS 的元数据目录和索引文件 find . -name "__MACOSX" -type d -exec rm -rf {} + find . -name ".DS_Store" -type f -delete

这两条命令把 macOS 打包压缩时带入的元数据目录和索引文件删掉,否则find统计文件数量时会混入无关内容,训练脚本也可能因为读取了._*.txt这种 AppleDouble 文件导致标签格式解析失败。做完这些,再回到 5.1 的 val 流程,重新跑一次yolo detect val看看指标是否稳定,并对比mAP50-95有没有下降。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询