简介:一份卫星遥感舰船检测数据集,面向目标检测算法开发、遥感影像智能解译等应用场景,也适合计算机视觉初学者作为多类别目标检测练习数据。数据涵盖17个舰船类别,包括航空母舰、驱逐舰、潜艇、货船、油轮等典型目标,类别划分细致,基于两千余张遥感影像构建,并提供Pascal VOC与YOLO两种主流标注格式,可直接接入YOLO、Faster R-CNN、SSD等常见检测框架训练。资源包采用7z压缩格式,解压后总大小约109.32MB,共2000个文件,文件以VOC格式的XML标注为主,另附YOLO标注支持及使用说明,目录清晰、便于批量读取与格式转换,也方便按需筛选与二次开发。该数据集已吸引834人学习下载,特别适合需要多类舰船标注样本的科研人员、算法工程师和竞赛团队使用,能够显著节省数据采集与人工标注时间,为高精度舰船识别模型提供可靠的数据基础,也可用于教学演示与开源项目复现,覆盖多尺度目标检测场景。
1. 卫星遥感舰船检测数据集:VOC+YOLO双格式、2238张、17类,直接绕开标注转换这一步
拿到这包“卫星遥感舰船检测数据集”,第一反应是确认它能不能直接喂给YOLO。解压之后是三组文件:2238张jpg、2238个VOC风格的xml、2238个YOLO风格的txt,类别17类,覆盖航母、驱逐舰、护卫舰、油轮,还包括Dock、Landing这类港口设施。这份资源的价值不在图片多,而在双格式并存——xml负责可追溯、可修正,txt负责直接训练,两边是同一套标注内容,等于给你留了后悔药。适合三类人:做遥感目标检测的研究生、拿公开数据验证YOLOv8流程的工程新手、以及需要快速迭代舰船类目标的算法团队。接下来把从解压到跑通训练前的全部准备过程拆开,每个环节都给能直接复制的代码和参数说明。
2. 双格式的对应逻辑:xml的绝对坐标与txt的归一化坐标差在哪
很多人拿到双格式数据集,会以为两份标注是冗余,随便用哪份都行。实际操作下来并不是:VOC格式的xml是可编辑、可回读的“源文件”,YOLO格式的txt是简化后的“派生文件”。一旦需要增删类别、修正错框,改完xml重新派生txt,比直接改txt容易得多。这也是我劝你先别删xml的原因——后续任何一个改标注的需求,都会绕回xml。
2.1 两种标注的字段含义
VOC的xml用绝对像素坐标描述目标,核心字段是bndbox节点里的xmin、ymin、xmax、ymax,以及object节点下的name。YOLO的txt每一行代表一个目标,格式是class_index cx cy w h,四个坐标值全部归一化到0~1之间,class_index是类别索引,从0开始编号。两边字段的对位关系如下。
| xml字段 | txt字段 | 含义 | 示例值 |
|---|---|---|---|
| name | class_index | 类别名与类别索引的映射 | Aircraft Carrier -> 0 |
| xmin, xmax | cx, w | 左右边界换算为中心点和宽度 | xmin=120, xmax=240 -> cx=0.14 |
| ymin, ymax | cy, h | 上下边界换算为中心点和高度 | ymin=80, ymax=160 -> cy=0.13 |
| 原图宽高 | 无 | 归一化时的分母 | 1280x720 |
这套对应关系里,最容易被忽略的是宽高归一化的分母必须来自当前图片实际尺寸,而不是xml里size节点的旧值。xml是人工编辑过的,size节点可能在多次压缩、裁剪后被改过一次,而标签却仍由旧尺寸派生,最终结果就是txt里的比例尺整体偏移,模型训练时损失曲线正常,但推理框全部偏高或偏左。我在另一份遥感数据上遇到过这个问题,排查半天才发现是xml里的宽度比实际图窄了400像素。
2.2 从bndbox换算到YOLO标签的验证脚本
双格式数据集的另一个好处是可以互查。下面这段脚本把xml里的绝对坐标换算成YOLO归一化坐标,再与已有的txt逐行对比,专门用来确认“两份标注是不是真的同一套内容”。
import xml.etree.ElementTree as ET import os import cv2 def xml_to_yolo(xml_path, img_path): # 宽高必须从真实图片读取,不用xml里的size字段 img = cv2.imread(img_path) h, w = img.shape[:2] root = ET.parse(xml_path).getroot() converted = [] for obj in root.findall("object"): cls_name = obj.findtext("name") bnd = obj.find("bndbox") xmin = float(bnd.findtext("xmin")) ymin = float(bnd.findtext("ymin")) xmax = float(bnd.findtext("xmax")) ymax = float(bnd.findtext("ymax")) # 中心点坐标换算 cx = ((xmin + xmax) / 2.0) / w cy = ((ymin + ymax) / 2.0) / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h converted.append((cls_name, cx, cy, bw, bh)) return converted # 使用示例:对任意一张图做换算 for xml_path in ["Annotations/firc_ship_18.xml"]: img_path = xml_path.replace("Annotations", "images").replace(".xml", ".jpg") print(xml_to_yolo(xml_path, img_path))逻辑说明:脚本用cv2.imread读取原始图像拿到真实宽高,再对每个object节点取出bndbox的四个边界值,中心点cx用左右边界相加除以2再除以宽,宽度bw用右边界减去左边界再除以宽,高度同理。输出是一个包含类别名和四个归一化值的元组列表。
参数说明:w和h的单位是像素,这里用浮点除法,Python 3里不会丢精度;round到6位是YOLO训练脚本的常见习惯,实际保留7~8位也行,但不要改成整数,否则框与框之间的微小差异会丢失,密集排列的集装箱船容易被合并成一个大框。对比txt时,把输出里的类别名替换成对应索引,再与txt每一行做逐值对比,误差超过1e-4就说明转换链路有偏差。
2.3 最容易翻车的类别编号问题
YOLO格式的txt里没有类别名,只有索引0~16。这个索引对应的类名由训练端data.yaml里的names列表决定,而不是由xml里的类名字典序决定。也就是说,同一行0 0.5 0.5 0.1 0.1,在names顺序为Aircraft Carrier在前时是航母,顺序一换就变成了其他船。
常见做法是分别打开一份单类目标的firc_ship_*.xml和对应的firc_ship_*.txt,确认xml里的类名与txt首列索引指向的类名一致。尤其要注意Other Ship和Other Warship这类语义相近的类,它们做标注时很容易被标混,如果训练集里这两类标签本身错位,模型就会长期无法区分货船与军舰。我一般会把这17个类名按资源给定的顺序先写进data.yaml,再做一次全量交叉验证,而不是直接拿未知顺序的txt开跑。
3. 解压后先验证三件事:文件完整性、坐标一致性、类别分布
跳过校验直接训练是大多数检测项目翻车的起点。这份数据集的目录结构很规整,但只要缺一张图或漏一个txt,YOLO训练时对应的图像就会被静默跳过,最终AP曲线看起来正常,实际参与训练的样本却少了一批。所以拿到压缩包的第一天,我先花半小时跑下面的三项检查。
3.1 完整性:jpg、xml、txt三套文件名必须一一对应
文件命名规律是firc_ship_编号,三种格式共用同一个编号。先在bash里统计总量。
find images -name "*.jpg" | wc -l find Annotations -name "*.xml" | wc -l find labels -name "*.txt" | wc -l三个数字都应该是2238。数量一致不代表文件一一对应,还得比对去掉后缀之后的文件名集合。
ls Annotations/*.xml | sed 's/.*\///;s/\.xml$//' | sort > xml_ids.txt ls images/*.jpg | sed 's/.*\///;s/\.jpg$//' | sort > img_ids.txt comm -3 xml_ids.txt img_ids.txtcomm -3输出两个集合的差异行,正常情况不打印任何内容。sed两步分别是去掉路径前缀、去掉.xml和.jpg后缀,sort保证两边排序一致。若出现多余的txt文件,比如某个firc_ship_*.txt额外残留导出的标签副本,YOLO训练不报错,但验证时统计框数与xml对不上,这一类问题靠第3.2节的交叉验证才能抓出来。
3.2 坐标一致性:xml与txt的框数是否完全相等
文件完整只说明“三份文件都在”,不说明“标注内容一致”。下面这段脚本逐一统计每张图的xml对象数与txt行数,不一致就把图名打出来。
import glob import os import xml.etree.ElementTree as ET def count_xml_objects(xml_path): root = ET.parse(xml_path).getroot() return len(root.findall("object")) def count_txt_lines(txt_path): if not os.path.exists(txt_path): return 0 lines = [ln for ln in open(txt_path) if len(ln.split()) >= 5] return len(lines) for img_path in glob.glob("images/*.jpg"): base = os.path.basename(img_path)[:-4] xml_path = f"Annotations/{base}.xml" txt_path = f"labels/{base}.txt" n_xml = count_xml_objects(xml_path) n_txt = count_txt_lines(txt_path) if n_xml != n_txt: print(f"不一致: {base} xml={n_xml} txt={n_txt}")逻辑说明:count_xml_objects统计object节点个数,count_txt_lines只统计至少5列的行,避免把空行或注释行也算进去。循环遍历所有jpg,按前缀拼接出xml和txt路径,数量不相等就输出。
参数说明:>= 5这层过滤很关键。有些标注工具会在txt末尾写一行类别颜色配置,这行数据不足5列,不该视为标签;反过来如果txt每一行都恰好5列但首列数字超出0~16范围,说明类别索引配置错了,这属于第5章要讲的问题。我见过最隐蔽的情况是某一类停靠岸边的舰船全部没框,xml里没有对应object,txt行数自然比正常图少,人工看不出,脚本一跑就暴露了。
3.3 类别分布:先摸清17类的底子再决定训练策略
只看总量不看分布,类别不平衡会在训练中期突然暴雷。这步统计两类数据:图片级类别覆盖数(某类出现在多少张图里)和标注框级类别计数(某类总共有多少框)。两者含义不同,前者决定模型能否见到该类样本,后者决定模型能否充分学习该类特征。
from collections import Counter import glob import xml.etree.ElementTree as ET img_cls = Counter() box_cls = Counter() for xml_path in glob.glob("Annotations/*.xml"): root = ET.parse(xml_path).getroot() names = [obj.findtext("name") for obj in root.findall("object")] img_cls.update(set(names)) # 图片级:这张图里出现过的类 box_cls.update(names) # 框级:每个类累计多少框 print("图片覆盖数:", dict(img_cls)) print("框数量:", dict(box_cls))逻辑说明:img_cls.update(set(names))先去重再计数,回答“有多少张图包含航母”;box_cls.update(names)不去重,回答“航母这个类总共多少标注框”。跑完这步基本能预测后续训练结果——样本量垫底的类,AP一般也不太好看。
参数说明:如果某类框数量是两位数的量级,比如常见数据集里Aircraft Carrier、Submarine这类样本天然就少,那第5.2节的过采样和增强方案就要优先照顾它们;如果跑出来Oil Tanker、Cargo这类商船数量远高于军舰,模型会自然偏向商船,验证时Cruiser、Frigate的AP就会被压得很低。此处的统计结果同时决定第4章的划分方式和第5章的增强比例,值得在看训练曲线之前先打印出来。
4. 从裸标注到可训练状态:划分train/val、写data.yaml、定YOLOv8超参
这份数据集的txt里没有分割路径,也就是说官方没替你做train/val划分,拿data.yaml指到images目录训练会把验证集混进训练集。实际做法是自己划分并组织目录,这一章把从裸标注到可训练状态的完整链路跑一遍。
4.1 划分策略:固定随机种子,而不是按文件名硬切
文件命名与拍摄场景相关,按文件名头尾切分会让某些舰船类型只出现在训练集或验证集。应按随机种子打乱后均匀切出训练、验证、测试三部分,比例用0.8/0.1/0.1,测试集留着做最终评估,不参与训练和调参。
import glob import random random.seed(42) # 种子固定,确保每次划分结果一致 imgs = sorted(glob.glob("images/*.jpg")) random.shuffle(imgs) n = len(imgs) n_train = int(n * 0.8) n_val = int(n * 0.9) train_imgs = imgs[:n_train] val_imgs = imgs[n_train:n_val] test_imgs = imgs[n_val:] for name, subset in [("train", train_imgs), ("val", val_imgs), ("test", test_imgs)]: with open(f"{name}.txt", "w") as f: for p in subset: f.write(p + "\n")逻辑说明:random.seed(42)固定随机源,保证多次运行生成的划分一致,后续调参时能对比“同一批验证集”上的结果。sorted()先排序再打乱,避免不同平台文件顺序差异干扰随机性。划分结果写入三个txt列表文件。
参数说明:比例参数0.8和0.9分别对应训练集1780张、验证集224张、测试集224张左右。如果某个小样本类恰好在测试集里出现次数过多,验证集看不到该类样本,逐类AP会失真,此时应改用分层抽样,按类别分布做``train_test_split的stratify`划分。常见做法是先用普通随机划分跑通流程,确认类别底线后再决定是否上分层。
4.2 目录组织与data.yaml的写法
YOLOv8按目录结构对应标签,不读取路径列表txt。因此必须把图片和标签按同样方式移动,组织成images/train、images/val、images/test与labels/train、labels/val、labels/test。
while read p; do f=$(basename "$p") mv "images/$f" "images/train/" mv "labels/${f%.jpg}.txt" "labels/train/" done < train.txt${f%.jpg}去掉jpg后缀,拼接出txt文件名再移动。val和test两个列表重复执行同样命令。执行完检查labels/train下的txt数量与images/train一致,否则说明某个标签缺失。
data.yaml按下面的结构写:
path: /data/ship train: images/train val: images/val test: images/test nc: 17 names: 0: Aircraft Carrier 1: Auxiliary Ships 2: Cargo 3: Commander 4: Container Ship 5: Cruiser 6: Destroyer 7: Dock 8: Frigate 9: Hovercraft 10: Landing 11: Oil Tanker 12: Other Ship 13: Other Warship 14: RoRo 15: Submarine 16: Yacht三个高频错误:一是path路径不对,YOLO会把train字段相对path拼接,路径写错会直接报文件不存在;二是nc与names数量不一致,YOLO训练时按names数量决定输出头,数量对不上loss计算就会错位;三是names顺序与txt首列索引顺序不一致,这种不报错但模型输出完全错乱。解决第三类问题最快的方法就是第2.3节说的单类图验证法。
4.3 YOLOv8超参初调:分辨率优先、batch按显存收缩
卫星遥感舰船目标在原始影像里往往只占几十个像素,输入分辨率直接决定小目标还能不能保留特征。先用一次小规模训练确定baseline,再逐步加量。
yolo detect train \ data=ship.yaml \ model=yolov8s.pt \ imgsz=1280 \ batch=8 \ epochs=100 \ amp=True \ workers=4imgsz=1280是遥感舰船检测的关键起点。640输入下,一艘只有30像素宽的目标经过5次下采样后只占约1个像素,特征图里基本消失;1280输入让它至少保有2~3个像素的响应。batch受显存约束,8G显存跑yolov8s加1280输入很紧张,可以降级到yolov8n;显存不足时依次考虑调小batch、换成小模型、最后才把imgsz降到1024。
参数说明:amp=True表示混合精度训练,多数环境能省20%~30%显存,但旧驱动或特定显卡下偶尔出现loss为NaN的情况,一旦遇到就关掉amp再试。workers=4是数据加载进程数,Windows上建议改成0或2,避免多进程与训练主进程冲突。第一次跑不要直接上yolov8x,先用最小模型验证数据链路,通了再换大模型。
5. 避坑实录:小目标漏检、类别样本失衡与非船类标注边界
这一章是数据集落地时最容易翻车的几处,每一条都对应实际训练里的真实事故。按现象、原因、解决三段记录,踩过的坑能少走两圈冤枉路。
5.1 小目标漏检:舰船在影像里太小,下采样后特征消失
现象:训练loss正常下降,但val的mAP@0.5在0.5上下徘徊,放大检测结果图发现大量船只根本没被框出来,尤其远离港口的海面目标,整片区域没有任何预测框。
原因:舰船目标天然属于小目标范畴,原图中可能只有20x40像素。输入尺寸设为640时,经过YOLOv8的stride 32下采样,目标在深层特征图上只剩不到2个像素,分类头拿到的语义信息几乎为零。这不是模型不努力,而是输入分辨率决定了它看不到。
解决:先按第4.3节把imgsz提到1280,再看小目标是否改善。还不够时,推理端用滑窗切图配合预测框合并,把原图切成若干1280x1280的块,每块独立预测,再把框坐标映射回原图,重叠区域的重复框用NMS合并。这个方案比单纯放大整图更省显存,缺点是推理时间增加,需要按场景取舍。
5.2 类别样本失衡:小样本类别被商船类压制
现象:训练完成后查看逐类AP,Oil Tanker和Cargo的AP能到0.8以上,而Submarine、Commander这类AP只有0.2甚至更低。整体mAP看着还行,一拆开就露馅。
原因:数据集里商船类目样本占据大头,模型学习时对高频类拟合得更充分;低频类本就样本少,加上随机划分时如果小样本类恰好没怎么进训练集,模型等于没见过这类目标。
解决:第一步回看第3.3节的分布统计,确定哪些类框数少于300;第二步对这些类做过采样,把小样本类图片在训练集中复制2~3份,或者对同一张图做旋转、翻转、亮度扰动后作为多张图参与训练;第三步如果类别之间特征差异不直观可以用复制粘贴增强,把小样本目标贴到海面背景图里,注意贴图时目标尺寸要缩放得和原图一致,否则模型会把特殊分辨率当作特征。
5.3 Dock与Landing的语义边界:这些类不是“船”
现象:训练时把Dock当成一个普通目标类别,后续业务里输出一堆码头框;或者把Landing与Aircraft Carrier混在一起,验证时互相拉低AP。
原因:这是标注语义设计问题,不是模型问题。Dock的本意是港口泊位或船坞设施,Landing在军事术语里可理解为登陆舰,也可以指登陆作业区域。数据集的类别体系把“非船设施”和“半船概念”纳入了检测范围,让模型既学船又学港口结构,类间边界模糊。
解决:做任务前先明确检测目标。如果最终场景是海上舰船识别,直接把Dock和Landing从训练数据中移除,或合并到Other类;如果场景是港口监控,需要同时识别舰船和泊位,则保留。Commander这类指挥舰外观与普通军舰接近,样本数量少时优先合并到Other Warship,等样本量上来再拆开。
5.4 训练初期loss异常:先查标签再调参
现象:首个epoch训练到几十步时loss突然变为NaN,或者前几个epoch loss一直下降但第10个epoch后验证AP为0。多数人的第一反应是调学习率,实际上问题出在标签。
原因:NaN最常来自两点,一是txt里存在归一化越界值,比如某行坐标大于1或为负数,AMP混合精度下计算结果溢出;二是某张图对应的标签框宽或高为0,损失函数里出现除零。验证AP为0则往往是类别索引与data.yaml的names顺序不对齐,模型输出的头顺序和标签索引对不上。
解决:遇到NaN先关amp再试一次,不是amp就直接跑第3.2节的坐标交叉脚本,把越界行过滤出来;宽高为0的框直接删除,或者回xml修正后重新导出txt。验证AP为0时,拉出第2.3节的单类图检查法,逐类确认索引。从那以后,我每次换数据集做的第一件事都是完整跑一遍第3章的三个脚本,再决定要不要调参。
6. 进阶用法:难例挖掘与逐类AP评估的闭环
6.1 难例挖掘:把val预测结果回贴到原图上
模型训练完成后不要只看指标,要把预测结果可视化地回贴到验证集图片上。先让模型输出验证集检测结果。
yolo detect val \ model=runs/detect/train/weights/best.pt \ data=ship.yaml \ imgsz=1280 \ save_txt=True \ save_conf=Truesave_txt=True把预测框写成txt,save_conf=True附带置信度。逐张观察预测图时,重点不是看框得多准,而是看两类错误:置信度低于0.4却确实是舰船目标的漏检框,以及置信度很高但框在码头建筑或陆地上的误检框。把这些图单独收集起来,与第3.3节的类别分布对照,能直观看出是模型能力问题还是数据覆盖问题。
6.2 逐类AP才是验收标准
训练日志里最后打印的mAP是整体平均,掩盖了类间差异。YOLOv8的验证输出会列出每一类的AP50与AP50-95,逐类检查才能定位究竟是哪一类拖了后腿。判断规则很简单:AP50低于0.5的类别,优先补数据和增强;AP50高但AP50-95低的类别,优先调输入分辨率和锚框,这类说明框基本位置对,但边界不精细。从那以后我每次训练完都会强制把逐类AP表打出来看一眼,确认改动确实提升了目标类别,而不是整体均值涨了小样本类别却在跌。这套流程下来,后面再换新数据集时,踩坑次数一次比一次少,希望帮到你。
本文还有配套的精品资源,点击获取