ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Trash-ICRA19数据集YOLO格式转换与训练全流程指南

Trash-ICRA19数据集YOLO格式转换与训练全流程指南 简介本资源为面向计算机、电子信息工程等专业学生及算法初学者的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_okTrue) 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 fTrash-ICRA19/images/{img_name} lbl_path fTrash-ICRA19/labels/{os.path.splitext(img_name)[0]}.txt out_path fverify_{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_okTrue) os.makedirs(lbl_out, exist_okTrue) 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显存不够就调小但不要小于 8subdivisions每批分几次前向4 或 8batch/subdivisions 是实际每次送入的样本数learning_rate初始学习率0.001太大容易震荡太小收敛慢max_batches最大迭代次数类别数×2000不少于 60001144 张图6000 次左右够用steps学习率衰减节点max_batches 的 80% 和 90%例如 4800, 5400scales衰减系数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 21 | 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):.*mAP0.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(mAP0.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 曲线都过一遍确认没有类别错位和标注异常之后再开始正式训练。这个习惯帮我省掉了至少三次推倒重来的时间。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

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