简介:本资源为面向计算机、电子信息工程等专业学生及算法初学者的YOLO目标检测实战数据集,基于Trash-ICRA19海洋垃圾检测场景构建,可直接用于课程设计、期末大作业与毕业设计。压缩包共2288个文件,包含1144张jpg图像与1144个同名xml标注文件,图像与标注一一对应,覆盖bio、obj等多类海洋目标,整体约26.21MB,解压后即可投入训练,省去自行采集与标注的时间成本。资源配套代码采用参数化编程,参数修改方便,思路清晰且注释详细,便于理解YOLO训练与推理流程。目前已有259人学习下载,读者可借此快速搭建海洋目标检测实验环境,掌握数据加载、标注解析、模型训练与评估的完整链路,并在此基础上替换参数或扩展类别,完成从数据到模型的闭环实践。
1. 从一批水下图像说起:Trash-ICRA19 数据集到底能干什么
如果你正在做海洋垃圾检测相关的课程设计或毕业设计,大概率会遇到一个很现实的问题:公开的海洋垃圾数据集要么标注格式不统一,要么类别定义模糊,要么图像质量参差不齐,拿到手之后光是清洗和格式转换就要耗掉大半时间。Trash-ICRA19 这个数据集就是冲着这个痛点来的——它源自水下机器人在海底拍摄的真实场景,专门标注了海洋环境中的垃圾目标,而这份资源把它整理成了 YOLO 目标检测可以直接吃的格式:1144 张图像,每张都配好了对应的标注文件,解压就能用。
这份资源适合三类人:一是做计算机视觉课程设计、需要快速跑通一个完整检测流程的学生;二是想验证自己改进的 YOLO 结构在真实水下场景下表现如何的开发者;三是需要一个标注规范、类别清晰的海洋检测数据集来做对比实验的研究者。它解决的核心问题不是“教你什么是目标检测”,而是“让你跳过数据准备阶段,直接进入模型训练和调参环节”。下面我会从数据集的类别结构、格式转换、训练配置到实际踩坑,把这份资源拆开讲清楚。
2. 拆开压缩包:类别定义、标注格式与目录结构
2.1 类别体系与图像来源
Trash-ICRA19 的类别划分并不复杂,但需要先搞清楚它的标注逻辑。数据集中的目标主要分为几类:海洋生物(如鱼类、贝类等)、水下机器人相关物体、以及各类垃圾(塑料制品、金属罐、橡胶制品等)。从文件名可以看出来,bio开头的图像通常包含生物目标,obj开头的则更多是物体和垃圾。这种命名方式不是随意的,它反映了原始采集时的分类逻辑。
图像分辨率并不统一,这是因为原始数据来自不同型号的水下拍摄设备。常见做法是在训练前统一缩放到 640×640 或 416×416,但要注意:直接拉伸会导致目标变形,尤其是细长型的垃圾目标。我一般会先用 letterbox 方式做等比缩放加灰边填充,这样既不破坏长宽比,又能保证输入尺寸一致。
标注文件是 XML 格式,遵循 PASCAL VOC 的标准结构。每个 XML 文件里包含图像尺寸、目标类别名称和边界框坐标(xmin、ymin、xmax、ymax)。这个格式本身没问题,但 YOLO 训练需要的是归一化后的中心点坐标和宽高,所以格式转换是绕不开的一步。
2.2 目录组织与文件对应关系
解压后的目录结构通常是这样的:
Trash-ICRA19/ ├── images/ │ ├── obj0871_frame0000120.jpg │ ├── bio0003_frame0000185.jpg │ └── ... ├── annotations/ │ ├── obj0871_frame0000120.xml │ ├── bio0003_frame0000185.xml │ └── ... └── classes.txtclasses.txt里列出了所有类别名称,每行一个。这个文件很关键,因为后续生成 YOLO 格式的.txt标签时,需要根据它来确定类别索引。如果这个文件缺失或者类别顺序和 XML 里的名称对不上,训练时就会出现类别错乱——模型把塑料瓶识别成鱼,loss 还降得挺开心,这种翻车场景我见过不止一次。
注意:在开始任何转换之前,先确认
images和annotations里的文件名是一一对应的。有些压缩包在传输过程中会丢失部分文件,导致图像和标注对不上。用一条简单的 shell 命令就能检查:
# 检查图像和标注文件是否一一对应 cd Trash-ICRA19 diff <(ls images/ | sed 's/\.jpg$//' | sort) <(ls annotations/ | sed 's/\.xml$//' | sort)如果输出为空,说明完全对应;如果有输出,那些就是缺失或多余的文件,需要先处理掉再往下走。这个步骤花不了两分钟,但能省掉后面几个小时的排查时间。
3. 从 XML 到 YOLO 标签:转换脚本与参数细节
3.1 转换脚本的完整实现
把 PASCAL VOC 格式的 XML 转成 YOLO 需要的.txt格式,核心逻辑就三步:读 XML、提取边界框、归一化坐标。但实际写起来有几个细节容易出错,我把完整脚本贴出来,然后逐段说明。
import os import xml.etree.ElementTree as ET # 类别列表,顺序必须和 classes.txt 一致 CLASSES = ['bio', 'obj', 'trash'] # 根据实际 classes.txt 修改 def convert_bbox(size, box): """ 将 VOC 格式的 (xmin, ymin, xmax, ymax) 转换为 YOLO 格式 返回归一化后的 (x_center, y_center, width, height) """ dw = 1.0 / size[0] dh = 1.0 / size[1] x_center = (box[0] + box[1]) / 2.0 y_center = (box[2] + box[3]) / 2.0 w = box[1] - box[0] h = box[3] - box[2] return (x_center * dw, y_center * dh, w * dw, h * dh) def convert_annotation(xml_path, output_dir): """将单个 XML 文件转换为 YOLO 格式的 txt 文件""" tree = ET.parse(xml_path) root = tree.getroot() size = root.find('size') w = int(size.find('width').text) h = int(size.find('height').text) # 用图像文件名作为输出文件名 image_name = os.path.splitext(os.path.basename(xml_path))[0] out_path = os.path.join(output_dir, image_name + '.txt') with open(out_path, 'w') as out_file: for obj in root.iter('object'): cls_name = obj.find('name').text if cls_name not in CLASSES: continue # 跳过不在类别列表中的目标 cls_id = CLASSES.index(cls_name) xmlbox = obj.find('bndbox') b = (float(xmlbox.find('xmin').text), float(xmlbox.find('xmax').text), float(xmlbox.find('ymin').text), float(xmlbox.find('ymax').text)) bb = convert_bbox((w, h), b) out_file.write(f"{cls_id} {' '.join([str(round(x, 6)) for x in bb])}\n") # 批量转换 if __name__ == '__main__': xml_dir = 'Trash-ICRA19/annotations' out_dir = 'Trash-ICRA19/labels' os.makedirs(out_dir, exist_ok=True) for xml_file in os.listdir(xml_dir): if xml_file.endswith('.xml'): convert_annotation(os.path.join(xml_dir, xml_file), out_dir) print(f"转换完成,共处理 {len(os.listdir(out_dir))} 个文件")这段代码里有两个地方需要根据实际情况调整。第一是CLASSES列表,必须和classes.txt里的顺序完全一致,否则类别索引会错位。第二是convert_bbox函数里的坐标顺序——VOC 的 XML 里是xmin, ymin, xmax, ymax,但我在代码里传参时写成了(xmin, xmax, ymin, ymax),这是为了配合后面convert_bbox里的计算逻辑。如果你自己改代码,一定要注意这个顺序,搞反了框会飞到图像外面去。
3.2 转换后的验证方法
转换完成后不能直接拿去训练,得先验证一下标签文件是否正确。最直观的方法是可视化:把 YOLO 格式的标签画回图像上,看看框的位置和大小对不对。
import cv2 import os def visualize_label(image_path, label_path, output_path): """将 YOLO 格式的标签画到图像上,用于验证转换是否正确""" img = cv2.imread(image_path) h, w = img.shape[:2] with open(label_path, 'r') as f: for line in f.readlines(): parts = line.strip().split() cls_id = int(parts[0]) x_center, y_center, bw, bh = map(float, parts[1:]) # 反归一化,得到像素坐标 x1 = int((x_center - bw / 2) * w) y1 = int((y_center - bh / 2) * h) x2 = int((x_center + bw / 2) * w) y2 = int((y_center + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, str(cls_id), (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) cv2.imwrite(output_path, img) # 随机抽几张验证 test_images = ['obj0871_frame0000120.jpg', 'bio0003_frame0000185.jpg'] for img_name in test_images: img_path = f'Trash-ICRA19/images/{img_name}' lbl_path = f'Trash-ICRA19/labels/{os.path.splitext(img_name)[0]}.txt' out_path = f'verify_{img_name}' visualize_label(img_path, lbl_path, out_path)跑完这段代码,打开生成的verify_*.jpg文件,如果框能准确套住目标,说明转换没问题。如果框的位置明显偏移或者大小不对,大概率是归一化时用错了图像尺寸——XML 里记录的宽高和实际图像文件的宽高可能不一致,这种情况在数据集里并不罕见,需要以实际读取到的图像尺寸为准。
3.3 数据集划分与配置文件
YOLO 训练需要把数据分成训练集、验证集和测试集。常见的比例是 7:2:1 或 8:1:1。我一般会写个脚本自动划分,同时生成 YOLO 需要的.data配置文件和.names文件。
import os import random import shutil def split_dataset(image_dir, label_dir, output_dir, ratios=(0.7, 0.2, 0.1)): """按比例划分数据集,并生成 YOLO 训练所需的配置文件""" images = [f for f in os.listdir(image_dir) if f.endswith('.jpg')] random.shuffle(images) n = len(images) n_train = int(n * ratios[0]) n_val = int(n * ratios[1]) splits = { 'train': images[:n_train], 'val': images[n_train:n_train + n_val], 'test': images[n_train + n_val:] } for split_name, file_list in splits.items(): img_out = os.path.join(output_dir, split_name, 'images') lbl_out = os.path.join(output_dir, split_name, 'labels') os.makedirs(img_out, exist_ok=True) os.makedirs(lbl_out, exist_ok=True) for img_file in file_list: shutil.copy(os.path.join(image_dir, img_file), img_out) lbl_file = os.path.splitext(img_file)[0] + '.txt' shutil.copy(os.path.join(label_dir, lbl_file), lbl_out) # 生成 train.txt 和 val.txt with open(os.path.join(output_dir, 'train.txt'), 'w') as f: for img in splits['train']: f.write(os.path.join(output_dir, 'train', 'images', img) + '\n') with open(os.path.join(output_dir, 'val.txt'), 'w') as f: for img in splits['val']: f.write(os.path.join(output_dir, 'val', 'images', img) + '\n') print(f"训练集 {len(splits['train'])} 张,验证集 {len(splits['val'])} 张,测试集 {len(splits['test'])} 张") split_dataset('Trash-ICRA19/images', 'Trash-ICRA19/labels', 'Trash-ICRA19/split')划分完成后,还需要一个.data文件告诉 YOLO 去哪里找数据和类别信息:
classes = 3 train = Trash-ICRA19/split/train.txt valid = Trash-ICRA19/split/val.txt names = Trash-ICRA19/classes.names backup = backup/classes.names就是每行一个类别名称的文本文件。这两个文件放在 YOLO 的cfg目录下或者项目根目录都行,只要路径写对就可以。
4. 训练配置与参数调优:从能跑到跑好
4.1 锚框调整与输入尺寸选择
YOLO 的默认锚框是基于 COCO 数据集聚类得到的,直接拿来检测海洋垃圾效果不一定好。Trash-ICRA19 里的目标尺寸分布和 COCO 差异很大——水下拍摄的垃圾目标往往偏小,而且长宽比更极端。我一般会用 k-means 重新聚类一遍锚框。
# 使用 darknet 自带的聚类工具重新计算锚框 ./darknet detector calc_anchors Trash-ICRA19/trash.data -num_of_clusters 9 -width 416 -height 416跑完之后会输出 9 组锚框的宽高值,把它们替换到 YOLO 配置文件里对应的anchors行。这一步对召回率的提升比较明显,尤其是小目标垃圾的检测。
输入尺寸方面,416×416 是速度和精度的折中点。如果显卡显存够(8GB 以上),可以上到 608×608,小目标的检测效果会更好。但要注意,输入尺寸越大,训练时间越长,而且如果数据集本身图像分辨率不高,强行放大反而会引入噪声。
4.2 关键训练参数的含义与设置
YOLO 的配置文件里参数很多,但真正影响训练效果的就这么几个:
| 参数 | 含义 | 建议值 | 说明 |
|---|---|---|---|
| batch | 每批样本数 | 16 或 32 | 显存不够就调小,但不要小于 8 |
| subdivisions | 每批分几次前向 | 4 或 8 | batch/subdivisions 是实际每次送入的样本数 |
| learning_rate | 初始学习率 | 0.001 | 太大容易震荡,太小收敛慢 |
| max_batches | 最大迭代次数 | 类别数×2000,不少于 6000 | 1144 张图,6000 次左右够用 |
| steps | 学习率衰减节点 | max_batches 的 80% 和 90% | 例如 4800, 5400 |
| scales | 衰减系数 | 0.1, 0.1 | 配合 steps 使用 |
这些参数不是拍脑袋定的。max_batches的经验公式是类别数乘以 2000,但最少不低于 6000 次,否则模型还没收敛就停了。steps设在 80% 和 90% 的位置,让学习率在训练后期降下来,帮助模型稳定到局部最优。
训练命令本身很简单:
./darknet detector train Trash-ICRA19/trash.data cfg/yolov4-trash.cfg yolov4.conv.137 -map-map参数会定期计算 mAP,方便观察模型的实际检测效果。如果不加这个参数,只能看 loss 曲线,而 loss 低不代表检测效果好——这是新手最容易踩的坑之一。
4.3 训练过程中的监控与中断处理
训练开始后,控制台会不断输出 loss 值和 mAP。重点关注两个信号:一是 loss 是否在稳定下降,二是 mAP 是否在上升。如果 loss 降到某个值后开始震荡甚至反弹,说明学习率可能偏大,或者数据集里有标注错误的样本。
训练中断是常有的事,可能是断电、显存溢出或者手动停止。YOLO 会定期保存权重文件到backup目录,恢复训练时用最后保存的权重作为预训练权重即可:
./darknet detector train Trash-ICRA19/trash.data cfg/yolov4-trash.cfg backup/yolov4-trash_last.weights -map注意这里用的是_last.weights而不是_best.weights。_best是验证集上表现最好的权重,适合最终推理;_last是最后一次迭代的权重,适合继续训练。搞混了会导致训练从头开始或者效果倒退。
5. 避坑指南:标注、训练与推理中的五个典型问题
5.1 类别索引错位导致模型“指鹿为马”
现象:训练 loss 正常下降,但推理时模型把鱼识别成垃圾,或者把垃圾识别成生物,置信度还不低。
原因:classes.txt里的类别顺序和转换脚本里CLASSES列表的顺序不一致。比如classes.txt里第一行是trash,但脚本里CLASSES = ['bio', 'obj', 'trash'],这样trash的索引就变成了 2 而不是 0,标签文件里的类别 ID 全部错位。
解决:转换之前先打开classes.txt确认顺序,把CLASSES列表改成完全一致的顺序。如果已经转换完了,不用重新转,写个脚本把标签文件里的第一列数字按映射关系改一遍就行。
5.2 图像与标注尺寸不匹配导致框偏移
现象:可视化验证时发现框的位置整体偏移,或者框的大小明显不对。
原因:XML 文件里记录的width和height与实际图像文件的尺寸不一致。这种情况通常是因为图像在某个环节被缩放或裁剪过,但 XML 没有同步更新。
解决:在转换脚本里不要用 XML 里的尺寸,直接用cv2.imread读取实际图像的宽高。多花几毫秒读一次图像,能避免后面几个小时的排查。
5.3 小目标漏检严重
现象:训练 mAP 看起来还行,但推理时远处的、小的垃圾目标基本检测不到。
原因:YOLO 的默认锚框尺寸偏大,而 Trash-ICRA19 里小目标占比不低。另外,如果输入尺寸设得太小(比如 320×320),小目标经过下采样后特征几乎消失。
解决:用 k-means 重新聚类锚框,把输入尺寸提高到 608×608,或者在网络结构里增加一个更小的检测尺度。如果显存不够,至少把锚框调小。
5.4 训练 loss 震荡不收敛
现象:loss 曲线上下剧烈波动,没有明显的下降趋势。
原因:学习率太大,或者 batch size 太小导致梯度估计不稳定。也有可能是数据集里存在标注框超出图像边界的样本。
解决:先把学习率降到 0.0001 试试,如果还震荡就检查标注文件里有没有坐标超出 [0,1] 范围的异常值。写个脚本扫一遍所有标签文件,把越界的框裁剪到边界内。
5.5 推理速度慢于预期
现象:模型在测试集上 mAP 不错,但推理一张图要几百毫秒,达不到实时要求。
原因:输入尺寸太大,或者模型本身参数量过多。也有可能是推理时没有启用 GPU 加速。
解决:如果追求速度,把输入尺寸降到 416×416,或者换用 YOLO-tiny 系列。推理时确认darknet编译时开启了 CUDA 和 cuDNN,否则默认走 CPU,速度差几十倍。
6. 进阶技巧:用 mAP 曲线判断模型是否过拟合
训练到后期,很多人只看最终的 mAP 值,但那个数字本身说明不了太多问题。真正有用的是 mAP 随迭代次数的变化曲线。我一般会在训练命令里加上-map参数,然后把控制台输出重定向到日志文件,训练结束后用脚本把 mAP 曲线画出来。
# 训练时保存日志 ./darknet detector train Trash-ICRA19/trash.data cfg/yolov4-trash.cfg yolov4.conv.137 -map 2>&1 | tee train.log # 提取日志中的 mAP 数据并绘图 python3 -c " import re import matplotlib.pyplot as plt iterations = [] maps = [] with open('train.log', 'r') as f: for line in f: match = re.search(r'(\d+):.*mAP@0.50 = ([\d.]+)', line) if match: iterations.append(int(match.group(1))) maps.append(float(match.group(2))) plt.plot(iterations, maps) plt.xlabel('Iterations') plt.ylabel('mAP@0.50') plt.title('Trash-ICRA19 Training mAP Curve') plt.grid(True) plt.savefig('map_curve.png') print(f'共提取 {len(iterations)} 个数据点') "拿到曲线之后怎么看?如果 mAP 在某个点之后不再上升,甚至开始下降,而训练 loss 还在继续降,这就是典型的过拟合信号。这时候应该用_best.weights而不是_last.weights做推理,或者提前停止训练。如果 mAP 曲线一直震荡,说明学习率还是偏大,需要进一步调小。
另一个实用技巧是看不同类别的 AP 分布。YOLO 训练日志里会分别输出每个类别的 AP,如果某个类别的 AP 明显低于其他类别,说明这个类别的样本可能太少或者标注质量有问题。Trash-ICRA19 里bio类别的样本数量通常比trash少,如果bio的 AP 偏低,可以考虑对这类样本做过采样,或者在 loss 函数里给这类样本更高的权重。
从那以后我每次拿到新数据集,都会先跑一遍完整的转换和验证流程,把可视化结果和 mAP 曲线都过一遍,确认没有类别错位和标注异常之后再开始正式训练。这个习惯帮我省掉了至少三次推倒重来的时间。希望帮到你。
本文还有配套的精品资源,点击获取