简介:这份船只检测数据集面向计算机视觉研究者、目标检测开发者及航海安全相关项目团队,用于训练和优化船只识别与定位模型。资源同时提供VOC与YOLO两种主流标注格式:VOC以XML文件记录每艘船的边界框与类别信息,YOLO则以TXT文件给出中心点坐标、宽高和类别ID,方便直接接入不同检测框架。压缩包共25683个文件,包含8561张jpg图像、8561个txt标注和8561个xml标注,整体约863.6MB,图片覆盖正面与侧面等多种角度,有助于提升模型对光照、视角和遮挡变化的泛化能力。目前已有3892人学习下载。读者可基于该数据开展YOLOv3、YOLOv4或Faster R-CNN等模型的迁移学习,结合IoU与mAP评估检测效果,并应用于海洋监控、港口管理等场景。
1. 船只检测数据集:VOC 与 YOLO 双格式到底怎么选、怎么转、怎么训
做水面目标检测的同行多半遇到过这种局面:模型在 COCO 上跑得挺欢,一换到港口、航道、近海监控画面就集体翻车,船体反光、尾迹、岸线干扰、小目标密集,全是坑。船只检测数据集之所以被反复搜索,核心原因就一个——通用数据集里船这一类样本太少,且标注粒度和实际业务对不上。而「包含 VOC 和 YOLO 两种格式」这句话,恰恰是这类数据集最实用的地方:VOC 的 XML 适合做数据审查、格式转换和跨框架迁移,YOLO 的 txt 能直接喂给 ultralytics 系训练脚本,省掉一轮转换。这篇不聊虚的,按「拿到数据集先干什么 → 两种格式怎么互转 → 训练参数怎么设 → 哪些坑必踩」的顺序讲透,新手能照着跑通,熟手能对着参数和边界做取舍。
2. 先搞懂 VOC 与 YOLO 两种标注格式的差异与选型逻辑
2.1 VOC XML 的结构与它为什么适合做「中间格式」
Pascal VOC 格式的标注是一个图像对应一个 XML 文件,核心节点是<object>,里面包含<name>(类别名)、<bndbox>(xmin、ymin、xmax、ymax,绝对像素坐标)。船只检测里常见的类别命名有 boat、ship、vessel 几种,不同来源的数据集不统一,这是后面训练翻车的常见起点。
VOC 的优势在于可读性和信息冗余度。XML 里能塞进<difficult>、<truncated>、<pose>这些字段,做数据清洗时你可以按 truncated 比例过滤掉被岸线切掉一半的船,也可以按 bndbox 面积筛掉误标的浪花。我一般把 VOC 当作数据集的「母版」:所有审查、去重、类别合并都在这一层做完,再转成 YOLO 去训练。反过来,如果你只拿到 YOLO txt,想回溯某张图为什么被标成这样,就得自己写脚本反解,麻烦得多。
一个典型的船只 VOC 标注长这样:
<annotation> <filename>ship_00231.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>ship</name> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>842</xmin> <ymin>317</ymin> <xmax>1105</xmax> <ymax>498</ymax> </bndbox> </object> </annotation>坐标是绝对像素值,这一点在转换时最容易出错——YOLO 要的是归一化后的中心点加宽高,两者差一个除法,除错了模型照样能训,但框会整体偏移,loss 降不下去还找不到原因。
2.2 YOLO txt 的结构与归一化坐标的坑
YOLO 格式每张图对应一个同名 txt,每行一个目标:class_id x_center y_center width height,后四个值都是相对图像宽高的归一化值,范围 0~1。class_id 从 0 开始,和你在 data.yaml 里写的 names 顺序严格对应。
这里有个高频翻车点:VOC 里类别是字符串,YOLO 里是整数,转换时必须维护一张固定的映射表。我见过有人先转了一批数据把 ship 映射成 0,后来又补了一批把 boat 映射成 0,合并训练时模型学出来的类别语义直接乱掉,mAP 掉一大截还以为是数据量不够。正确做法是把类别映射写死在配置里,转换脚本读同一份配置。
# classes.txt 或 data.yaml 里的 names 顺序,必须全局唯一且固定 CLASS_MAP = {"ship": 0, "boat": 1, "vessel": 2}归一化还有个边界问题:xmax 等于图像宽度时,x_center + width/2 会等于 1.0,某些老版本训练脚本对 1.0 边界处理不严,会报越界或裁掉半个框。稳妥做法是转换后统一 clip 到 [0, 1],并留一个极小 epsilon。
2.3 两种格式的选型对照
| 维度 | VOC XML | YOLO txt |
|---|---|---|
| 坐标形式 | 绝对像素 xmin/ymin/xmax/ymax | 归一化中心点+宽高 |
| 类别表示 | 字符串 name | 整数 class_id |
| 附加字段 | truncated/difficult/pose | 无 |
| 直接可用框架 | MMDetection、Detectron2、部分 TF 系 | ultralytics YOLO 全系 |
| 适合场景 | 数据审查、清洗、跨框架迁移 | 直接训练、快速迭代 |
选型结论很直接:数据集同时给了两种格式,就用 VOC 做审查和存档,用 YOLO 做训练输入。不要试图只留一种,VOC 丢了以后想换框架会痛苦,YOLO 丢了每次训练都要现转。
3. 从 VOC 到 YOLO:转换脚本、参数与验证方法
3.1 转换脚本的完整实现
下面这个脚本是我常用的版本,处理了类别映射、坐标 clip、空标注跳过三个关键点。假设目录结构是VOC/Annotations/*.xml和VOC/JPEGImages/*.jpg。
import os import xml.etree.ElementTree as ET CLASS_MAP = {"ship": 0, "boat": 1, "vessel": 2} EPS = 1e-6 def voc_to_yolo(xml_dir, img_dir, out_dir): os.makedirs(out_dir, exist_ok=True) skipped = 0 for xml_file in os.listdir(xml_dir): if not xml_file.endswith(".xml"): continue tree = ET.parse(os.path.join(xml_dir, xml_file)) root = tree.getroot() size = root.find("size") w = int(size.find("width").text) h = int(size.find("height").text) lines = [] for obj in root.iter("object"): name = obj.find("name").text.strip() if name not in CLASS_MAP: continue # 未登记类别直接丢弃,避免 class_id 错位 cls_id = CLASS_MAP[name] 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) # 归一化并 clip,防止边界越界 xc = min(max((xmin + xmax) / 2.0 / w, 0.0), 1.0) yc = min(max((ymin + ymax) / 2.0 / h, 0.0), 1.0) bw = min(max((xmax - xmin) / w, EPS), 1.0) bh = min(max((ymax - ymin) / h, EPS), 1.0) lines.append(f"{cls_id} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}") if not lines: skipped += 1 continue # 无有效目标的图不生成空 txt,避免训练器报错 txt_name = os.path.splitext(xml_file)[0] + ".txt" with open(os.path.join(out_dir, txt_name), "w") as f: f.write("\n".join(lines)) print(f"done, skipped {skipped} empty-label images") voc_to_yolo("VOC/Annotations", "VOC/JPEGImages", "labels/train")逻辑说明:先读图像宽高拿到归一化分母,再逐 object 取类别和框。类别不在 CLASS_MAP 里的直接跳过而不是报错,是为了兼容数据集里偶尔混入的杂类。坐标 clip 到 [0,1] 解决边界越界,宽高加 EPS 防止出现 0 宽高的退化框。空标注图不生成 txt,因为 ultralytics 训练时遇到没有对应 label 的图会警告甚至跳过,不如在转换阶段就处理干净。
参数说明:CLASS_MAP 必须和后续 data.yaml 的 names 完全一致;EPS 取 1e-6 足够小不影响精度又能避免除零;输出目录按 train/val 分开,转换时分别调用。
3.2 转换后的三重验证
转完不验证等于没转。我一般做三层检查:
第一层,数量对齐。图片数、txt 数、原始 xml 数三者关系要清楚,空标注图会导致 txt 数少于图片数,这是正常的,但差值要能解释。
第二层,可视化抽检。随机抽 20 张把 YOLO 框画回原图,肉眼看框是否贴合船体。这一步能抓出坐标顺序写反(x 和 y 搞混)这种脚本 bug。
import cv2 def draw_yolo(img_path, label_path, names): img = cv2.imread(img_path) h, w = img.shape[:2] with open(label_path) as f: for line in f: c, xc, yc, bw, bh = line.split() xc, yc, bw, bh = map(float, (xc, yc, bw, bh)) x1 = int((xc - bw / 2) * w) y1 = int((yc - bh / 2) * h) x2 = int((xc + bw / 2) * w) y2 = int((yc + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, names[int(c)], (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite("check.jpg", img)第三层,统计分布。把每张图的框数量、框面积占比、宽高比统计出来,船只检测里宽高比通常偏扁长(船长大于船宽),如果出现大量接近正方形的框,多半是标注质量问题或把浪花标进去了。
3.3 data.yaml 的写法与路径陷阱
ultralytics 系训练靠 data.yaml 组织数据,路径写错是最常见的「模型不收敛」假象来源。
path: /data/ship_dataset train: images/train val: images/val names: 0: ship 1: boat 2: vessel注意 path 是数据集根目录,train/val 是相对 path 的图片目录,labels 目录默认由 ultralytics 按同名规则去推——它会把你 train 路径里的images替换成labels去找 txt。所以目录结构必须是images/train配labels/train,命名不一致它找不到标签,训练时全部当背景,loss 看着在降但模型啥也没学到。这个坑我踩过,排查了半天才发现是目录名对不上。
4. 用这套数据集训练船只检测模型的参数设置与排错
4.1 输入分辨率与小目标船只的取舍
船只检测的难点之一是小目标:远距离的船在 1080p 画面里可能只有二三十像素宽。输入分辨率直接决定小目标能不能被检出。YOLO 默认 640,对近景大船够用,对远景小船经常漏。我的经验是把 imgsz 提到 960 或 1280,mAP 在小目标上通常有明显提升,代价是显存和推理耗时上升。
如果业务是实时监控,得算一笔账:1080p 25 帧的视频流,用 TensorRT 加速的 YOLO 在 640 分辨率下单卡能撑的路数,和 1280 分辨率下差好几倍。分辨率、路数、精度三者不可能同时拉满,先明确业务能接受的最低召回,再倒推分辨率。
4.2 关键训练参数与含义
yolo detect train \ data=data.yaml \ model=yolov8n.pt \ imgsz=960 \ epochs=200 \ batch=16 \ lr0=0.01 \ lrf=0.01 \ mosaic=1.0 \ close_mosaic=20 \ patience=50参数说明:imgsz 按上面说的权衡;batch 受显存限制,显存不够就降 batch 同时按比例降 lr0,别只降 batch 不调学习率;mosaic 是 YOLO 的招牌增强,把四张图拼一张,对小目标友好,但船只检测里拼接会造出水面不连续的假场景,如果发现模型对水面纹理过拟合,可以把 mosaic 调低到 0.5;close_mosaic 在最后若干 epoch 关掉 mosaic,让模型在真实分布上收尾,这个参数别省。
4.3 训练不收敛时的排查顺序
遇到 loss 不降或 mAP 卡住,按这个顺序查:先看 data.yaml 路径和类别数对不对,再看标注可视化有没有框错位,然后确认类别映射有没有错位,最后才怀疑模型和超参。我统计过自己踩的坑,八成问题出在前两步,跟模型结构没关系。船只检测还有个特有情况:如果数据集里大量图片是同一片水域、同一时段拍的,验证集和训练集分布太近,mAP 虚高,换个港口就崩,这时候要做的是按场景划分 train/val,而不是调参。
5. 船只检测数据集避坑:5 个血泪踩坑记录
坑一:类别名不统一导致 class_id 错位。现象是训练能跑、loss 能降,但推理时船被识别成 boat 或反之,混淆矩阵一塌糊涂。原因是不同批次数据里 ship 和 boat 混用,转换时映射表没统一。解决办法是转换前先统计所有 XML 里出现过的 name,人工确认合并规则,写死一份 CLASS_MAP,所有批次共用。
坑二:归一化坐标越界导致框偏移。现象是可视化时部分框整体偏出图像或缩成一条线。原因是 xmax 等于图像宽度时归一化结果等于 1.0,某些处理环节对边界不友好。解决办法是转换时统一 clip 到 [0,1],宽高加极小 epsilon。
坑三:images 和 labels 目录命名不匹配。现象是训练启动正常但 mAP 始终接近 0,模型像在学背景。原因是 ultralytics 按 images→labels 的规则找标签,目录名不一致就找不到。解决办法是严格保持images/train对labels/train的镜像结构,转完用脚本核对两边文件名集合是否一致。
坑四:空标注图未处理。现象是训练日志里大量 warning,或者某些图被反复跳过。原因是原数据集里有不含任何目标的图,转换时生成了空 txt。解决办法是转换阶段就跳过空标注图,或在 data.yaml 层面确认训练器对空标签的处理策略。
坑五:验证集与训练集同源导致指标虚高。现象是本地验证 mAP 很高,一上真实场景就崩。原因是数据集的 train/val 是按图片随机切的,同一段视频的相邻帧被分到两边,模型其实见过近似画面。解决办法是按视频源或拍摄场景划分 train/val,保证验证集里的场景训练时没见过。
6. 进阶:把 VOC 母版用成可持续迭代的数据资产
数据集不是训完一次就扔的消耗品。我现在的习惯是永远保留 VOC 母版,YOLO 格式按需生成,因为业务迭代时你会不断往里加新数据、改类别、修标注,母版是唯一能承载这些变更的形态。
具体做法是给数据集建一个版本管理流程:每次新增数据先转成 VOC,跑一遍统计脚本看类别分布和框尺寸分布有没有漂移,确认没问题再合并进母版,然后一次性重新生成 YOLO 格式和 data.yaml。这样训练用的永远是最新快照,而母版始终可追溯。
验证数据集质量有个我常用的技巧:算每个类别的框面积中位数和宽高比中位数,画成分布图。船只检测里,如果某批新数据的宽高比中位数突然从 3.5 掉到 1.2,基本可以断定标注风格变了或者混进了错误标注,这时候别急着训,先回去查标注。这个检查比看 mAP 更早发现问题,省下的返工时间很可观。
还有个容易忽略的点:VOC 里的 difficult 字段。很多公开数据集把它当摆设,但在船只检测里它有用——被岸线遮挡、被浪花干扰的船可以标 difficult=1,训练时可以选择性忽略这些样本,避免模型去拟合本身就模糊的目标。我一般会在数据审查阶段把 difficult 比例统计出来,超过 15% 就说明这批数据采集质量有问题,该考虑重新采而不是硬训。
最后说个习惯:每次转换和训练前,先跑一个五分钟的数据体检脚本,输出图片数、标注数、类别分布、框尺寸分布、空标注比例。这五分钟能挡掉后面几小时的无效训练。数据集这活儿,脏活累活都在前期,前期做扎实了,模型那边反而省心。希望帮到你。
本文还有配套的精品资源,点击获取