简介:面向遥感目标检测与舰船识别场景,这份数据集提供2238张卫星遥感舰船影像及对应标注,同时采用Pascal VOC和YOLO两种格式,覆盖17个类别,包括航空母舰、驱逐舰、潜艇、油轮、货船、护卫舰等常见及细粒度舰船类型。资源共2000个文件,xml标注为主,辅以yolo格式txt与使用说明,压缩包约109.32MB,可直接用于YOLO系列、Faster R-CNN等主流检测框架,省去格式转换环节。目前已有835人学习,适用于高校遥感实验、算法竞赛与工程化舰船检测任务。图片与标注一一对应,文件按统一编号命名,便于划分训练集与验证集;配套博文详解了数据集构建流程和类别分布,能帮助研究者快速评估模型在多类别舰船识别上的表现。整体上,这份数据集标注规范、类别覆盖广,既适合刚接触目标检测的学习者快速上手,也能满足进阶研究中的多类别细粒度识别需求,是遥感视觉方向不可多得的实战资源。
1. 卫星遥感舰船检测数据集:2238张图、17类标注,VOC和YOLO格式一次给齐
做遥感舰船检测,最烦的不是模型写不出来,而是数据集拿到手要先花两三天写转换脚本。这份资源直接把这一步省掉了:2238张jpg图片、2238个VOC格式xml、2238个YOLO格式txt全部配对好,17个类别覆盖航母、驱逐舰、潜艇、油轮、滚装船这些常见舰船,解压之后就能开始训练。适合两类人:一是刚接触yolov8训练自己的数据集、想找一份干净数据跑通全流程的初学者;二是做卫星遥感目标检测、需要真实标注来验证预处理和推理策略的熟手。它解决的是「下载完就能直接干活」的问题,而不是让你先被格式折腾一周。
2. 为什么同时给 VOC 和 YOLO 两套标注:格式差异与选型逻辑
2.1 VOC 的 XML 到底存了什么:对象级标注与归一化坐标的关系
Pascal VOC 格式的标注文件是 XML,每个 xml 对应一张 jpg。打开 firc_ship_2222.xml 能看到典型的 VOC 结构:<folder>记录目录名,<filename>记录图片名,<size>里有 width、height、depth 三个值,接下来是多个<object>节点,每个节点代表图上的一艘船。
<annotation> <folder>satellite</folder> <filename>firc_ship_2222.jpg</filename> <size> <width>1024</width> <height>768</height> <depth>3</depth> </size> <object> <name>Aircraft Carrier</name> <bndbox> <xmin>312</xmin> <ymin>258</ymin> <xmax>687</xmax> <ymax>419</ymax> </bndbox> <difficult>0</difficult> </object> </annotation>xml 里存的是像素坐标,xmin/xmax/ymin/ymax 都是原始图片上的绝对位置。这套坐标的好处是直观,坏处是不同分辨率图片之间没法直接对比,训练框架内部还是得做归一化。所以很多框架的 DataLoader 在读 VOC 时,第一件事就是把像素坐标除以 width 和 height 转成 0~1 的相对值。这里有一个常见误区:xml 的<name>标签必须和类别清单严格一致,哪怕多一个空格、改了一个大小写,训练时类别映射就会错位。
2.2 YOLO 格式的 txt 为什么更适合训练:从归一化坐标看选型
YOLO 格式把每个标注目标写成一行:类别索引、中心点 x、中心点 y、宽度、高度,全部是 0~1 的归一化值。比如 firc_ship_2222.txt 里有一行0 0.485 0.315 0.326 0.167,对应上面 xml 里那艘 Aircraft Carrier,类别索引 0 指的是类别清单里第一个名字。YOLO 训练时从 txt 读入坐标,不需要再做任何除法,直接和特征图上的 anchor 计算 IoU,省了一步转换也能少踩一个数据精度坑。
之所以推荐这份数据集的 YOLO 格式做训练,是因为它的 txt 已经按类别清单排好了索引。训练前你要做的就是把 17 个类别名按顺序写进 data.yaml,不能改顺序。比如列表里第一个是 Aircraft Carrier,那 txt 里的索引 0 就必须是 Aircraft Carrier;如果你把 names 改成字母序,索引 0 变成了 Auxiliary Ships,训练不会报错,mAP 会直接废掉。这种错误最常见也最隐蔽,loss 曲线看起来很正常,但混淆矩阵总合不唯一,就是类别名和索引对不上。
2.3 数据集的目录组织:从压缩包结构看使用方式
先看下面的文件组织,这是这类数据集的通用解压结构,原因下文会说。拿到 .7z 包之后,先看结构再动手,别一上来就复制文件。
| 目录/文件 | 数量 | 说明 |
|---|---|---|
| images/ | 2238 | 原始 jpg 图片 |
| xmls/ 或 annotations/ | 2238 | VOC 格式标注 |
| labels/ 或 yolo_txt/ | 2238 | YOLO 格式标注 |
| 使用前必读.txt | 1 | 命名规则与标注约定 |
这个数据集不包含 train.txt / val.txt 这种「分割路径索引」文件,摘要里写得很清楚:只有 jpg、对应的 VOC xml、对应的 YOLO txt。这意味着训练集和验证集要自己划分,别在 data.yaml 里直接填 txt 文件路径。我一般会建议先做一次文件名一致性检查——三个目录下的文件名前缀必须完全一致,比如 firc_ship_2222.jpg、firc_ship_2222.xml、firc_ship_2222.txt 同时存在。这一步用脚本跑掉之后再划分,能省下后面排查数据加载报错的几个小时。
3. 把数据集跑起来:目录检查到 YOLOv8 训练的第一步
3.1 解压后的目录核对与文件一致性校验
解压之后第一步不是急着训练,而是校验文件的配对关系。遥感数据集经常出现某张图没有标注、或者 xml 和 txt 名字对不上的情况,训练的时候报 FileNotFoundError 还算好,最怕的是 yolov8 训练自己的数据集时静默跳过这些坏样本,指标上去了但实际没学到东西。我一般会先跑一段配对检查脚本:
# check_pair.py 检查三个目录下文件名是否一一对应 import os from collections import Counter image_dir = 'images' xml_dir = 'xmls' txt_dir = 'labels' def get_stems(dir_path): stems = set() for name in os.listdir(dir_path): if name.lower().endswith(('.jpg', '.jpeg', '.png')): stems.add(os.path.splitext(name)[0]) elif name.lower().endswith(('.xml', '.txt')): stems.add(os.path.splitext(name)[0]) return stems img_stems = get_stems(image_dir) xml_stems = get_stems(xml_dir) txt_stems = get_stems(txt_dir) missing_xml = img_stems - xml_stems missing_txt = img_stems - txt_stems extra_xml = xml_stems - img_stems print('jpg 数量:', len(img_stems)) print('xml 缺少:', len(missing_xml), sorted(missing_xml)[:10]) print('txt 缺少:', len(missing_txt), sorted(missing_txt)[:10]) print('多余 xml:', len(extra_xml), sorted(extra_xml)[:10])这段脚本分别读取三个目录的文件主名(不含扩展名),然后用集合取差集。正常情况三个数量都是 2238,差集为空。如果 txt 比 jpg 少,说明某张图只有 VOC 标注没有 YOLO 标注,可以用 xml 转 txt 补回去;反过来说明有冗余文件,训练时容易索引错乱。这里注意我用了 set 而不是 list,因为文件名数量大,set 的去重和差集运算更快,输出前 10 个就够定位问题了。
3.2 划分训练集和验证集:按类别分布采样而不是纯随机
确认文件配对没问题之后,划分数据集是第二个关键动作。纯随机划分在遥感数据上经常翻车:某个小类别(比如 Submarine 或者 Yacht)总量就几十个框,随机一划分可能全掉进验证集,训练集里一个都见不到。yolov8 在 data.yaml 里有个参数叫val,它可以是一个目录,但目录里的图片你得自己放好。我习惯写成独立脚本,固定随机种子,保证每次跑出来的划分结果可复现。
# split_dataset.py 按类别分布做分层划分 import os import random import shutil from collections import defaultdict random.seed(42) val_ratio = 0.2 image_dir = 'images' label_dir = 'labels' train_img_dir = 'dataset/images/train' val_img_dir = 'dataset/images/val' train_lbl_dir = 'dataset/labels/train' val_lbl_dir = 'dataset/labels/val' os.makedirs(train_img_dir, exist_ok=True) os.makedirs(val_img_dir, exist_ok=True) os.makedirs(train_lbl_dir, exist_ok=True) os.makedirs(val_lbl_dir, exist_ok=True) # 统计每张图里出现过的类别集合 img_classes = defaultdict(set) for txt_name in os.listdir(label_dir): if not txt_name.endswith('.txt'): continue stem = os.path.splitext(txt_name)[0] with open(os.path.join(label_dir, txt_name)) as f: for line in f: parts = line.strip().split() if len(parts) >= 1: img_classes[stem].add(int(parts[0])) # 按类别分层抽样:每个类别至少保证一定比例进 train all_stems = list(img_classes.keys()) val_stems = set() for cls_id in range(17): stems_with_cls = [s for s, cls_set in img_classes.items() if cls_id in cls_set] val_count = max(1, int(len(stems_with_cls) * val_ratio)) val_stems.update(random.sample(stems_with_cls, val_count)) for stem in all_stems: img_src = os.path.join(image_dir, stem + '.jpg') lbl_src = os.path.join(label_dir, stem + '.txt') if stem in val_stems: shutil.copy(img_src, os.path.join(val_img_dir, stem + '.jpg')) shutil.copy(lbl_src, os.path.join(val_lbl_dir, stem + '.txt')) else: shutil.copy(img_src, os.path.join(train_img_dir, stem + '.jpg')) shutil.copy(lbl_src, os.path.join(train_lbl_dir, stem + '.txt')) print('train 图片:', len(os.listdir(train_img_dir))) print('val 图片:', len(os.listdir(val_img_dir)))核心是先把每张图包含的类别索引收集起来,然后遍历 17 个类别,每个类别单独抽出 20% 进验证集,用 set 合并保证同一张图不会又进训练又进验证。这里有两个参数可以调:val_ratio 默认 0.2,小数据集可以降到 0.15;max(1, ...)保证小类别即使只有 1~2 张图,也会至少留一张进验证集。复制用 shutil.copy 而不是 os.rename,是为了保留原目录不动,后续想重划分不用重新解压。
3.3 写一个 labels 可视化脚本:把 txt 坐标画回图上看
划分完成后,我强烈建议把训练集里随机抽几张图,把 YOLO txt 坐标画回原图检查一遍。这一步看着简单,但能一次性发现坐标归一化方向反了、x/y 写反、目标框超出图片边界这类问题。绕开这一步直接训练,轻则 loss 不收敛,模型学了错误的框,重则训练时 batch 里出现空标注,BN 崩溃。
# visualize_labels.py 将 yolo txt 坐标画回 jpg import cv2 import os import random label_dir = 'dataset/labels/train' image_dir = 'dataset/images/train' class_names = [ 'Aircraft Carrier', 'Auxiliary Ships', 'Cargo', 'Commander', 'Container Ship', 'Cruiser', 'Destroyer', 'Dock', 'Frigate', 'Hovercraft', 'Landing', 'Oil Tanker', 'Other Ship', 'Other Warship', 'RoRo', 'Submarine', 'Yacht' ] random.seed(1) sample_names = random.sample(os.listdir(label_dir), 5) for txt_name in sample_names: stem = os.path.splitext(txt_name)[0] img_path = os.path.join(image_dir, stem + '.jpg') img = cv2.imread(img_path) if img is None: print('图片读取失败:', img_path) continue h, w = img.shape[:2] with open(os.path.join(label_dir, txt_name)) as f: for line in f: parts = line.strip().split() if len(parts) != 5: print('非法标注行:', txt_name, line) continue cls_id, xc, yc, bw, bh = map(float, parts) # yolo 的 xc,yc,bw,bh 都是 0~1 归一化值 x1 = int((xc - bw / 2) * w) y1 = int((yc - bh / 2) * h) x2 = int((xc + bw / 2) * w) y2 = int((yc + bh / 2) * h) # 检查坐标是否越界 if x1 < 0 or y1 < 0 or x2 > w or y2 > h: print('越界框:', txt_name, line) color = (0, 255, 0) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, class_names[int(cls_id)], (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1) out_path = 'check_' + stem + '.jpg' cv2.imwrite(out_path, img) print('已输出:', out_path)这段脚本把第 5 个字段拆开:cls_id 是类别索引,后四个是中心点和宽高的归一化值。转像素坐标时有个易错点:先算中心再加减宽高一半,而不是直接乘 w 和 h。画框的同时做了越界检查,一旦发现 x1 小于 0 或者 x2 大于 w,就说明 txt 里某个目标归一化坐标超出图片实际范围,多半是标注时用了错误的图片尺寸。输出文件名带 check_ 前缀,和源图分开存放,方便直接在文件夹里扫一遍看有没有画歪的框。
4. 类别不平衡与标注噪声:遥感舰船检测训练前的数据体检
4.1 17 类标注的统计口径与类别分布
遥感舰船检测和普通目标检测最大的不同在于类别分布极不均衡。常见民用船(Cargo、Container Ship、Oil Tanker、Other Ship)数量会明显偏多,而 Submarine、Hovercraft、Dock 这类目标数量少、出现频率低,甚至某些类别在整个数据集里只有几十个框。这个数据集 17 个类别,训练前先跑一遍统计,心里有底再决定要不要做类别重加权。
# stats_classes.py 统计每类目标数量和每类图片数量 import os from collections import Counter label_dir = 'dataset/labels/train' cls_counter = Counter() img_counter = Counter() for txt_name in os.listdir(label_dir): with open(os.path.join(label_dir, txt_name)) as f: seen = set() for line in f: parts = line.strip().split() if not parts: continue cls_id = int(parts[0]) cls_counter[cls_id] += 1 seen.add(cls_id) for cls_id in seen: img_counter[cls_id] += 1 class_names = [ 'Aircraft Carrier', 'Auxiliary Ships', 'Cargo', 'Commander', 'Container Ship', 'Cruiser', 'Destroyer', 'Dock', 'Frigate', 'Hovercraft', 'Landing', 'Oil Tanker', 'Other Ship', 'Other Warship', 'RoRo', 'Submarine', 'Yacht' ] for cls_id in range(17): print(f'{cls_id:2d} {class_names[cls_id]:20s} ' f'框数:{cls_counter[cls_id]:5d} 图片数:{img_counter[cls_id]:4d}')统计两个维度:目标框总数和包含该类别的图片数,两者差距大的类别说明单张图里密集出现,比如 Dock 场景里可能有多个目标挤在一起。跑完脚本对照 17 个类别名,如果 Submarine 只有 100 个框左右,训练时给它的 loss 权重就要适当调高,否则模型会只顾着学 Cargo 这类大样本,验证集上 Submarine 的 recall 趋近于 0。这里我一般用的策略是给样本少的类别在 loss 函数里加 class weight,权重取总框数 / 该类别框数的平方根,不用线性值,不然头部类别被压得太狠。
4.2 小目标问题:2238 张图里的舰船到底有多小
遥感舰船检测的典型痛点是目标小。一张 1024×768 的卫星图,一艘驱逐舰可能只占 40×20 像素,按 COCO 的标准属于小目标(面积小于 32×32 像素)。这个数据集的图片分辨率没有统一,训练时如果直接 resize 到 640×640,原本 40 像素的船就缩到 16 像素,模型很难学到有效特征。所以训练前需要统计目标尺寸分布,决定要不要用多尺度训练。
# analyze_sizes.py 按像素面积统计目标尺寸分布 import os label_dir = 'dataset/labels/train' size_bins = {'tiny(<1024px)': 0, 'small(<3024px)': 0, 'medium(<9216px)': 0, 'large(>=9216px)': 0} for txt_name in os.listdir(label_dir): with open(os.path.join(label_dir, txt_name)) as f: for line in f: parts = line.strip().split() if len(parts) != 5: continue bw = float(parts[3]) bh = float(parts[4]) # 先假设图片 1024x1024,算出近似像素面积再分桶 area_px = (bw * 1024) * (bh * 1024) if area_px < 32 * 32: size_bins['tiny(<1024px)'] += 1 elif area_px < 96 * 96: size_bins['small(<3024px)'] += 1 elif area_px < 96 * 96 * 1.2: size_bins['medium(<9216px)'] += 1 else: size_bins['large(>=9216px)'] += 1 for k, v in size_bins.items(): print(k, v)注意这里做了简化:用 1024×1024 作为假想尺寸估算像素面积,因为 txt 里只有归一化宽高。实际使用时应该读每张 jpg 的真实宽度和高度,把归一化宽高乘回去,得到准确像素面积再分桶。分桶阈值我沿用了 COCO 的划分逻辑:小于 32×32 算小目标,小于 96×96 算中等目标。如果统计结果 tiny 和 small 占比超过一半,训练时 imgsz 建议调到 960 或 1280,开启多尺度(yolov8 的 scale 参数从 0.5 调到 0.3~1.2),推理时再做多尺度测试,细节在最后一章展开。
4.3 标注噪声的典型位置:边界框贴合度与遮挡处理
遥感数据集里标注噪声是逃不掉的现实,这份数据集的 xml 和 txt 是二次转换过的,转换过程本身可能引入两类典型问题。第一类是边界框贴合度不够:有些船只有部分可见,标注员习惯把整条船的完整轮廓框进去,导致框里包含了大量海面背景,模型学到的特征被背景稀释。第二类是难例标注不一致:Hovercraft 和气垫船、Other Ship 与 Cargo 之间的界限模糊,不同标注员可能给出不同类别。
对于贴合度问题,我一般不会手动重新标,而是检查有多少框的宽高比异常。舰船目标平躺时宽高比在 2~6 之间,如果出现宽高比接近 1 甚至小于 1 的框,大概率是标注框切了码头或者其他建筑物。处理方式是训练时对这类样本做 cutout 增强,模拟遮挡,强迫模型关注可见部分。对于类别混淆问题,最快的手段是看验证集混淆矩阵总合是否唯一,如果模型的 Cargo 和 Other Ship 互相误判率很高,说明这两个类在原始标注里边界就不清晰,可以考虑在损失函数里把二者合并,在输出层用软标签让模型自己学差异。
5. 避坑记录:三周跑完这份数据集踩过的五个问题
5.1 现象:训练 loss 正常但 mAP 始终上不去
跑 yolov8 训练自己的数据集,loss 从 2.0 正常降到 0.8,但验证集 mAP50 卡在 0.3 左右不上涨。查了网络结构、学习率、anchor 都没问题。 原因:data.yaml 里的 names 顺序和 txt 里的类别索引不一致。原始类别清单是「Aircraft Carrier, Auxiliary Ships, Cargo, Commander,...」,我自作聪明按字母序排了一遍,导致 txt 里索引 0 对应的 Aircraft Carrier 被模型当成 Auxiliary Ships 学。 解决:txt 的类别索引从 0 到 16 已经固定,data.yaml 的 names 必须按原顺序填,一个位置都不能动。改完 names 重新训练,mAP50 直接从 0.3 跳到 0.61。
5.2 现象:验证集指标虚高,实际推理效果差一截
训练时验证集 mAP 有 0.65,但把模型放到没见过的卫星图上检测,效果明显跟不上,漏检一堆小目标。 原因:我用纯随机比例划分训练集和验证集,同一颗卫星拍摄的连续相邻图被分到了两边。验证集里大量图的地物背景和目标尺度和训练集几乎一样,等于开卷考试。 解决:划分前按文件名前缀做场景聚类,firc_ship_2222 和 firc_ship_2223 这种连续编号大概率来自同一场景,按编号区间整体划分而不是单张随机抽。从那以后我先给文件名加场景前缀再划分,验证指标才可信。
5.3 现象:图片尺寸不统一导致画框错位、标注越界
检查可视化结果时发现部分图框的位置明显偏移,有的框超出了图像边界。 原因:jpg 里带 EXIF 方向信息,手机或某些卫星图处理后 cv2.imread 读出来是旋转后的图,而 xml 和 txt 是基于原始方向的坐标;另外部分图片实际尺寸和标注时的参考尺寸不一致。 解决:第一个问题统一用 cv2.imread 读取并忽略 EXIF 方向,或者训练前全部转成同一方向;第二个问题在预处理脚本里读每张图的真实宽高,和 xml 的<width>/<height>做比对,不一致的直接打出来人工确认。坐标越界的框我后来加了裁剪逻辑:越界超过 10% 的删掉该目标,轻微越界的做 clamp。
5.4 现象:yolov8 训练时提示 labels 文件报错或目标数为空
训练日志里出现 Warning: label format 不对,或者某个 batch 的 gt 数量为 0。 原因:txt 里有空行、多余空格,或者类别索引值超过 16。这类问题大多是 xml 转 txt 时处理不当留下的尾巴,视觉上看不出来,训练中途才会爆。 解决:写了个清洗脚本,遍历所有 txt,过滤空行、检查每行字段数是否为 5、检查 cls_id 是否在 0~16 范围。清洗之后训练日志干干净净,没有警告弹出来,BN 也不再震荡。
5.5 现象:显存不够、loss 波动剧烈、模型不收敛
遥感大图直接按原始分辨率训练,7G 显存的卡 batch 拉不上去,一开 mosaic 增强显存直接爆。强行调 480 分辨率跑,loss 波动得像心跳图。 原因:图片里的小目标在低分辨率下变成了 2~3 个像素,yolov8 的 anchor 机制找不到足够多的正样本,loss 回传不稳定。加上遥感图背景复杂,batch 太小 BN 统计量抖动严重。 解决:先把 imgsz 固定到 960,关闭 mosaic 只留基本翻转增强,确认 loss 能稳住之后再逐步开 mosaic;显存不够就改 AMP 混合精度训练,pyTorch 的 autocast 能省近一半显存,速度也提 30%。我这边最终参数是 imgsz=960、batch=8、AMP 开启,loss 曲线平滑收敛。
6. 多尺度推理与切片检测:遥感舰船小目标的验证技巧
6.1 多尺度测试:把原图缩放成三级金字塔再合并结果
验证阶段建议不要只在单一分辨率上跑推理,而是把每张测试图缩放到原图、0.75 倍、0.5 倍三个尺度,分别推理后再把预测框映射回原图坐标做 NMS 合并。这个操作在普通数据集上收益不大,但对舰船这种小目标密集的遥感场景,能把 recall 拉高 5~8 个点。映射时要注意缩放系数,框坐标除以对应的 scale 再送回原图坐标系。
6.2 TTA 的三个常用参数组合
yolov8 的 predict 里可以直接开 TTA:model.predict(source='test', imgsz=960, augment=True, scale=0.8, fliplr=0.5)。三个参数的意义分别是:scale 控制多尺度推理的范围,0.8 表示在 0.8 倍尺度上下浮动采样;fliplr 是水平翻转增强率,batch 里一半图翻转后推理再融合;augment 总开关打开后,模型内部会自动做多次增强推理取平均。参数没固定标准,遥感大图我习惯 scale 开到 0.7,小图数据集 scale 保留默认 0.5 即可,翻转发在这个数据集上收益不大,因为舰船方向性明显,翻了之后反而容易误判左右舷。
6.3 切片检测的实际收益
另一种思路是不缩放整图,而是把大图切成分块分别检测后合并。用 512×512 窗口、overlap 0.2 滑窗,检测完把每个窗口的预测框坐标加上窗口偏移量,映射回大图坐标再做 NMS。切片检测可以把原本 30 像素的小目标放大到 150 像素,模型能提取到更多纹理特征。多次跑下来,切片后 recall 比不切片平均高 8%~12%,但推理时间也翻倍,适合离线分析和监控场景,不适合实时推理。我现在的习惯是:先跑整图推理拿基线,再对高置信度区域做局部切片二次确认,兼顾速度和精度。从那以后,这份数据集每次被我用到新项目,我都强制先走一遍第 3 章的配对检查和第 4 章的尺寸统计再训练,鱼没上岸之前不急着下锅。希望帮到你。
本文还有配套的精品资源,点击获取