简介:YOLO舰船检测数据集是一份从PASCAL VOC 2012中筛选出所有包含“boat”类别的图像构成的专用训练集,面向目标检测开发者、算法研究人员以及海事智能视觉落地项目。压缩包解压后共1648个文件,内含549张JPEG船舶图像、549个VOC格式XML标注文件、550个YOLO格式TXT标注文件,整体大小约56.31MB。TXT标注按YOLO规范记录目标框中心点、宽高与类别ID,XML标注则遵循PASCAL VOC标准描述框坐标与标签,两种格式可分别供YOLO系列和Faster R-CNN、SSD等框架使用。由于数据集仅保留“boat”一个类别,模型训练目标集中,适合做舰船检测、海上交通监控等场景的专项优化,也可通过翻转、裁剪、色彩扰动等数据增强手段扩大样本量,从而提升召回率与定位精度。已有2399人学习/浏览,借助该数据可快速搭建从数据解析、模型训练到效果评估的完整流程,对照图片与标注理解目标检测原理,进而支撑模型调优与部署验证。
1. 为什么 549 张 boat 图是最磨人的一类检测任务
海洋监控摄像头画面里,船往往只有十几个像素,浪花、倒影、岸边浮木都是“疑似船”。用 COCO 预训练权重直接推,大船能框住,但错检率居高不下。单类舰船检测的难点不在识别,而在定位和背景抑制。boat_VOCtrainval2012 这个数据集把 PASCAL VOC 2012 里 boat 类别抽出来,筛出 549 张带完整 bndbox 标注的 JPEG 图像,同时给出 VOC XML 和 YOLO txt 两套标注。对正在练手 YOLO 目标检测流程、想在真实背景下训练自己的船舶检测器的工程师和数据科学同学,它是很好的单类检测样板。小样本、单类别、强背景干扰,这三个属性放一起,比直接拿 COCO 大模型微调更能暴露数据工程问题。要验证自己的数据链路是否扎实,先把目录结构摸清,把 XML 转成 YOLO 格式并验证坐标,再用标准脚本把数据喂进 YOLOv8。
2. 从 VOC XML 到 YOLO txt:格式转换是训练前的分水岭
拿到 boat_VOCtrainval2012.zip,解压后就要开始破译标注结构。它的图像文件名沿用 VOC 原始编号,例如 2011_000238.jpg 属于 VOC 2012 的 trainval 集,2008_005863.jpg 属于更早的 VOC 年份。文件名与图像内容无直接关系,后续做数据集划分时按序号采样即可,不要根据文件名猜测场景。
2.1 解压后先看目录结构和 XML 关键节点
常见做法是直接 unzip 解压到本地,然后快速观察目录布局。
unzip boat_VOCtrainval2012.zip -d boat_dataset find boat_dataset -maxdepth 2 -type d | sort输出一般会看到三个核心目录,对应关系如下表:
| 目录 | 内容 | 体量 |
|---|---|---|
| JPEGImages | 船舶原始图片,JPEG 有损压缩 | 549 张 |
| Annotations | VOC XML 格式标注 | 与图片一一对应 |
| ImageSets/Main | 按 trainval/test 划分的清单文件 | 1~2 个 txt |
JPEG 是数字图像存储里常用的有损压缩格式,单张缩略图约几十到两百 KB。这里数量只有 549,整个压缩包通常不超过 300 MB,训练和拷贝成本都很低。标注文件是理解数据集的关键,直接看一张 XML 的内部结构:
head -60 boat_dataset/Annotations/2008_002610.xml一个典型 VOC 对象标注包含 filename、size 和 object 三部分。object 里有 name(类别名 boat)、pose、truncated、difficult,以及 bndbox 的 xmin、ymin、xmax、ymax。训练时真正读取的是 name 和 bndbox 四个整数坐标。truncated 和 difficult 表示目标被截断或难以辨认,YOLO 训练一般直接忽略,但做竞赛评测时 difficult 对象不参与 AP 计算,转换脚本里最好保留解析逻辑,方便后续统计。
提示:JPEGImages 文件名不能有中文和空格。如果解压后发现路径带空格,训练时 Dataloader 拼接路径不会对空格转义,概率性报 FileNotFoundError。这种坑在数据集来自不同渠道时很常见,拿到先统一改名。
2.2 YOLO 归一化坐标格式与换算原理
VOC XML 记录的是以图像左上角为原点的像素坐标,YOLO 则需要归一化的中心点坐标和宽高,即摘要里说的「类ID 中心x 中心y 宽 高」。换算公式如下:
yolo_x_center = ((xmin + xmax) / 2) / image_width
yolo_y_center = ((ymin + ymax) / 2) / image_height
yolo_width = (xmax - xmin) / image_width
yolo_height = (ymax - ymin) / image_height
所有输出值都在 0~1 之间。YOLO 内部把 feature map 每个格子对应到整图坐标,归一化让不同分辨率图像可以共用一套 anchor 和网络参数。如果直接喂绝对像素值,网络会认为 1920×1080 的船和 640×360 的船是两类目标。这里有个容易忽略的细节:增强模块(Mosaic、随机缩放)在送入网络前会做 letterbox,原图宽高比改变后,标注坐标要跟着 affine 变换走,YOLOv8 的自带 dataloader 会处理这一步;自己造数据时常见的错误是把 XML 里的 xmin、ymin、xmax、ymax 原样写进 txt,导致训练时边界框扭曲。
2.3 完整转换脚本:解析、归一化、落盘
下面给出兼容 boat_VOC 目录结构的转换脚本,输出到 labels 子目录:
import xml.etree.ElementTree as ET import os voc_root = "boat_dataset" labels_dir = os.path.join(voc_root, "labels") os.makedirs(labels_dir, exist_ok=True) def voc_to_yolo(xml_path, out_path): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find("size/width").text) img_h = int(root.find("size/height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text if name != "boat": continue bbox = obj.find("bndbox") xmin = float(bbox.find("xmin").text) ymin = float(bbox.find("ymin").text) xmax = float(bbox.find("xmax").text) ymax = float(bbox.find("ymax").text) x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h # 单类别 boat 的 class_id 固定为 0 lines.append(f"0 {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") with open(out_path, "w") as f: f.write("\n".join(lines) + "\n") for xml_name in os.listdir(os.path.join(voc_root, "Annotations")): if not xml_name.endswith(".xml"): continue stem = xml_name[:-4] xml_path = os.path.join(voc_root, "Annotations", xml_name) out_path = os.path.join(labels_dir, stem + ".txt") voc_to_yolo(xml_path, out_path)代码逻辑:先取 size/width 和 size/height 做归一化分母,再遍历 object 节点解析 bndbox。class_id 写死为 0,因为该数据集只有一个类别。后续如果要合并 COCO 船类子集,建议把 name 到 id 的映射抽成字典,方便扩展。这段脚本也适用于 KITTI 标注转 YOLO 的思路,KITTI 的 bbox 字段顺序同样是 xmin、ymin、xmax、ymax,只是 XML 标签名不同,把 find 路径改掉即可。运行后抽检 2008_002610 的标注内容,坐标值应为 0~1 的小数;如果出现一整行 0.0 或 1.0,多半是边界框贴边,需要回头检查 XML 的 bndbox 是否越界。
3. 把 boat 数据组装成 YOLOv5/v8 训练目录并跑通首轮
格式转换只完成数据准备的一半,接下来要组织目录结构和配置 data.yaml。YOLOv5 和 YOLOv8 都要求 images 和 labels 分目录存放,图片名与标签名一致,只差后缀。训练脚本会按图片文件名自动找同名 .txt。
组织好的标准结构如下:
boat_yolo/ ├── images/ │ ├── train/ # 例如 439 张 │ └── val/ # 例如 110 张 ├── labels/ │ ├── train/ │ └── val/ └── boat.yaml3.1 目录重组与 train/val 划分的比例决策
用 shell 命令把上一章生成的 labels 和 JPEGImages 归位:
cd boat_dataset mkdir -p ../boat_yolo/images/train ../boat_yolo/images/val ../boat_yolo/labels/train ../boat_yolo/labels/val mv JPEGImages/*.jpg ../boat_yolo/images/train/ 2>/dev/null549 张图规模小,val 留 100~120 张比较稳,否则验证集太少,mAP 波动会非常大。比例一般取 train:val = 8:2 或 7:3。这里的 trainval 原始划分与 VOC 官方 train/val 交集问题要特别注意:VOC 的 trainval 是 train 和 val 的总和,boat_VOCtrainval2012 本身是一个整体,如果直接从 ImageSets/Main 里拿现成清单,可能把验证图和训练图混在一起,造成数据泄漏,训练时 mAP 虚高。
更严谨的做法是用固定随机种子做一次划分并保存清单:
import os import random random.seed(0) imgs = sorted(os.listdir("boat_dataset/JPEGImages")) random.shuffle(imgs) val_count = int(len(imgs) * 0.2) val_set = set(imgs[:val_count]) for img in imgs: stem = img[:-4] src_img = f"boat_dataset/JPEGImages/{img}" src_lbl = f"boat_dataset/labels/{stem}.txt" if img in val_set: os.makedirs("boat_yolo/images/val", exist_ok=True) os.makedirs("boat_yolo/labels/val", exist_ok=True) os.replace(src_img, f"boat_yolo/images/val/{img}") os.replace(src_lbl, f"boat_yolo/labels/val/{stem}.txt") else: os.makedirs("boat_yolo/images/train", exist_ok=True) os.makedirs("boat_yolo/labels/train", exist_ok=True) os.replace(src_img, f"boat_yolo/images/train/{img}") os.replace(src_lbl, f"boat_yolo/labels/train/{stem}.txt")固定 random.seed(0) 后打乱,可以保证每次执行得到相同划分。val_count 按 20% 计算,因为原图存在漏标注,保留更多验证样本能暴露标注质量问题。如果后续换用 COCO 船类子集扩充,建议把划分脚本的参数化,不要手动复制图片。
3.2 data.yaml 配置与 AMD 显卡运行注意事项
boat.yaml 最少只需要三行路径加一个类别列表:
path: ../boat_yolo train: images/train val: images/val names: 0: boatpath 建议写相对路径,或者写绝对路径时用正斜杠。YOLOv8 对 Windows 绝对路径中的反斜杠处理不如相对路径稳,这个坑常见于从 Windows 拷贝到 Linux 服务器训练的场景。names 的 key 必须从 0 开始,单类别数据集才不会报 num_class 不匹配错误。
如果是在 AMD 显卡上跑 YOLO,需要用 ROCm 编译的 torch 环境,否则 import torch 后 device 检测不到 cuda,训练退回 CPU。安装 ultralytics 前先确认 torch 能打印出 ROCm enabled,再决定是否拉预编译包。目前 ultralytics 官方 wheels 对 ROCm 支持仍然停留在社区构建版本,直接 pip install ultralytics 后强制 device=0 会看到 RuntimeError: Attempting to deserialize object on a CUDA device,这是环境问题,不是代码问题。
3.3 用 YOLOv8 跑通第一轮训练
安装依赖后直接启动训练:
pip install -U ultralytics yolo detect train data=boat.yaml model=yolov8n.pt epochs=100 imgsz=640 batch=16 device=0关键参数建议如下:
| 参数 | 建议值 | 说明 |
|---|---|---|
| model | yolov8n.pt / yolov8s.pt | 数据量小从 n 起步,先跑通再做 s/l 对比 |
| imgsz | 640 或 1280 | 船目标小则 1280 能提升小目标召回,显存约翻 3 倍 |
| batch | 16 或 32 | 显存不够时减半,549 张图 batch 32 对精度提升有限 |
| epochs | 100~300 | 单类目标 100 轮足够判断过拟合方向 |
| device | 0 或 cpu | 无 GPU 时加 device=cpu,训练时间会拉长 10 倍以上 |
第一轮训练时建议加plots=True保存 confusion matrix 和预测样例。训练结束后运行验证命令:
yolo detect val data=boat.yaml model=runs/detect/train/weights/best.pt只跑通训练不跑验证,就看不到 mAP 的完整视图,模型好坏无从判断。如果验证时报 label 不匹配错误,检查 labels 目录下是否有空 txt 文件,空标注文件会导致 dataloader 解析失败。
4. YOLO 损失函数与单类舰船 mAP 的判读方法
训练跑起来以后,问题从“怎么传数据”变成“怎么判断模型在变好”。YOLOv5 和 YOLOv8 的损失组合不一样,直接决定日志里该看哪些字段。
4.1 不同版本损失函数组成与单类场景差异
YOLOv5 损失由三部分组成:box_loss 用 CIoU,obj_loss 用 BCEWithLogits,cls_loss 也用 BCEWithLogits。YOLOv8 是 anchor-free 结构,box 分支用 Distribution Focal Loss 加 CIoU,类别分支仍用 BCE。单类 boat 数据集的差异集中在 cls_loss 上:只有一个类别,BCE 只输出一个 logit,负样本数量远大于正样本,obj_loss 比 cls_loss 更容易主导总 loss。
训练日志字段的含义要分清:
epoch 50 box_loss: 0.62 cls_loss: 0.03 dfl_loss: 1.02 val box_loss: 0.71 val cls_loss: 0.05 mAP50: 0.83 mAP50-95: 0.41box_loss 下降说明边界框回归在收敛;cls_loss 是类别置信度损失,单类下数值通常较小,如果它突然增大,先怀疑数据集中混入了非 boat 背景图;dfl_loss 是 YOLOv8 独有的分布损失,负责预测框质量分布,平稳波动不需要过度干预。如果第 30 轮 mAP50 就超过 0.9,不要高兴太早,多数是 val 划分太小导致的偶然偏高,重点看 val loss 是否同步下降。
4.2 mAP 计算口径与 results.csv 诊断
mAP50 和 mAP50-95 是练手小模型最常看的两个指标。mAP50 是 IoU 阈值为 0.5 时的平均精度,衡量“框是否大致框住”;mAP50-95 是 0.5 到 0.95 区间按步长 0.05 计算的平均值,对定位精度要求更高,数值通常只有 mAP50 的一半。单类数据集只有 boat 一个类别,mAP 就等于这个类别的 AP。如果 mAP50 明显高于 0.8,但 mAP50-95 低于 0.3,说明模型漏检少但框偏了,优先调 anchor 或提高 imgsz。
用 results.csv 画出曲线做定位诊断:
import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv("runs/detect/train/results.csv") df.columns = [c.strip() for c in df.columns] plt.figure(figsize=(10, 4)) plt.plot(df["epoch"], df["train/box_loss"], label="train box") plt.plot(df["epoch"], df["val/box_loss"], label="val box") plt.legend() plt.savefig("loss_curve.png")代码不引入额外依赖,pandas 和 matplotlib 在训练环境里一般都有。看到 train/box_loss 继续下降而 val/box_loss 开始上扬,就是典型的过拟合信号。549 张小样本大概在 60~100 轮之间出现,此时应停止训练改用早停机制,或者切换更强的数据增强,而不是继续加 epochs。
5. 小样本舰船检测的增强补偿与锚框调整
5.1 按海上场景裁剪增强参数
549 张图决定模型必然依赖增强正则化。YOLOv8 默认增强参数对通用目标有效,但海上场景需要专门调整。参考配置如下:
hsv_h: 0.015 degrees: 10.0 translate: 0.1 scale: 0.4 fliplr: 0.5 flipud: 0.0 mosaic: 1.0 mixup: 0.0 copy_paste: 0.0海天线会让上下翻转产生大量不合理样本,flipud 直接关掉;fliplr 保留是因为左右航行方向不影响语义。scale 设 0.4 的代价是目标缩小后更难学,如果原图里船普遍小于 32×32 像素,把 scale 降到 0.2 更稳。mosaic 保留为 1.0,它把四张图拼一起训练,能提升模型对被遮挡船只的鲁棒性。mixup 和 copy_paste 在单类小样本下容易出现重复标签叠加,建议先从 0 起步,观察 mAP50-95 是否停在 0.5 以下再决定是否打开。
5.2 用预测脚本检查锚框尺寸适配度
训练完成后检查锚框是否适配本数据集中小目标:
from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") results = model.predict( source="boat_yolo/images/val/2008_002610.jpg", conf=0.25, iou=0.45, ) boxes = results[0].boxes.xywh.cpu().numpy() for b in boxes: print(f"w={b[2]:.0f} h={b[3]:.0f}")conf 和 iou 阈值控制输出框数量。对比同一图像 labels 目录下的 txt,如果输出框普遍比标签小一半以上,说明默认锚框是按 COCO 目标比例初始化的,对舰船这种长宽比变化大的目标适配不足。此时可运行yolo detect tune data=boat.yaml model=runs/detect/train/weights/best.pt重新搜索锚框超参,或者直接把 imgsz 提到 1280,让网络在小目标的 feature map 上保留更多空间特征。锚框调整对 mAP50-95 的影响通常比增加训练轮数更直接,这也是单类小样本数据集上最值得先做的优化方向。
本文还有配套的精品资源,点击获取