
简介面向遥感目标检测研究者与算法工程师这份数据集提供2238幅卫星舰船影像及对应的Pascal VOC与YOLO格式标注覆盖航母、驱逐舰、潜艇、集装箱船、油轮等17个常见舰船类别。VOC与YOLO双格式可直接适配Faster R-CNN、YOLO系列等主流检测框架省去标注格式转换环节便于直接开展模型训练与验证。压缩包大小约109.32MB内部以VOC格式的XML标注文件为主体1999个并配有说明文档文件总量约2000个下载和整理都较为轻量。说明文档对标注格式和类别信息做了必要提示可帮助快速上手类别划分贴近真实遥感侦察场景对舰船细粒度识别、目标检测竞赛和课程实验均有实用价值。目前已有835人学习下载适合需要标准舰船检测数据集的开发者和研究人员。1. 拿到一份 2238 张的遥感舰船数据集先别急着开训卫星遥感图像的舰船检测听起来是个专门领域其实落到工程上就是一件事把港口、近海、岛屿周边那些在光学影像里呈“条状”“梭形”的目标用检测框从背景里捞出来。这个数据集压缩包体积不大2238 张图、17 个类别、VOC 和 YOLO 两种标注格式都齐了属于“规模适中、格式友好”的入门级遥感检测资源。对刚接触遥感目标检测的工程师来说它解决了最磨人的数据准备问题——不用自己对着图像画框解压之后就能喂给 YOLO 系列模型跑一轮训练。但要提醒一句这个数据量级放在舰船检测里并不算大单类别的有效样本可能只有几十到几百个真正决定你训练效果的不是“有没有数据”而是拿到数据后的一整套校验、划分和训练策略。这篇文章不打算讲空洞的“遥感AI前景”而是从拿到压缩包后的第一步开始把格式转换、数据验签、训练配置、踩坑排查和提升精度的思路完整拆开。2. VOC 与 YOLO 双格式的构成先搞清楚包里到底是什么2.1 VOC 格式的目录结构与 XML 标注字段解压 7z 之后你会看到一个典型的 VOC 风格目录树。常见做法是保留JPEGImages原始图片、AnnotationsVOC 格式的 XML 标注、ImageSets/Main训练/验证划分的 txt 索引这三个核心目录。如果作者还同时给出了 YOLO 格式的标签目录那通常是并列的labels文件夹每张图对应一个同名的 txt 文件内容是一行一行的归一化坐标。VOC 的 XML 标注长这样字段含义很直接annotation folderJPEGImages/folder filenameship_00123.jpg/filename size width1280/width height720/height depth3/depth /size object namecargo/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin256/xmin ymin180/ymin xmax410/xmax ymax240/ymax /bndbox /object /annotation这里bndbox里的是绝对像素坐标name对应类别名。这份 XML 是整个数据集最接近“原始标注”的形态后续如果要做旋转框检测、实例分割或者格式迁移都得从它重新出发。所以我的建议是哪怕你只打算用 YOLO 格式训练也别把 Annotations 删掉。2.2 YOLO 格式的 txt 标签每行一个目标五个数字YOLO 格式的标签没有 XML 那么复杂每张图对应一个 txt每行描述一个目标实例3 0.486328 0.312500 0.078906 0.052083 7 0.721094 0.554167 0.037891 0.025000五个数字依次是类别 id从 0 开始、归一化后的中心点 x、中心点 y、宽度 w、高度 h。归一化指的是用绝对坐标除以图片的宽和高所以这些值都在 [0, 1] 区间内。如果这个包里的 txt 是由 VOC XML 自动转换的你需要警惕一件事坐标四舍五入到小数点后几位。按常见精度6 位小数其实够用但如果转换代码写得粗糙可能出现宽度为 0 的非法框——这个问题后面避坑章节单独说。2.3 两种格式并存的原因不同训练框架的输入习惯不同作为工程交付数据集同时提供 VOC 和 YOLO 格式本质上是降低使用门槛。Ultralytics YOLO 系列、YOLOv5 原版训练脚本、MMYOLO 这些工具读的是 txt 标签而需要用到torchvision自带的VOCDetectionAPI、或者想跑 MMDetection 的某些检测器时直接读 VOC XML 会更顺畅。把转换脚本留好以后自己扩充数据时就能复用这条链路。# voc_to_yolo.py import xml.etree.ElementTree as ET import os def voc_to_yolo(xml_path, out_txt_path, class_list): 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 not in class_list: continue # 跳过未登记类别 cls_id class_list.index(name) bnd obj.find(bndbox) xmin float(bnd.find(xmin).text) ymin float(bnd.find(ymin).text) xmax float(bnd.find(xmax).text) ymax float(bnd.find(ymax).text) # 计算中心点和宽高注意宽高除以图片尺寸 cx (xmin xmax) / 2.0 / img_w cy (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) with open(out_txt_path, w) as f: f.write(\n.join(lines))这段脚本的核心逻辑是把绝对像素坐标换算成归一化坐标。注意我用cx (xmin xmax) / 2.0 / img_w而不是(xmin xmax) / 2 / img_w浮点除法和整数除法的区别在 Python 3 里不明显但刻意写成浮点可以避免脚本被移到 Python 2 环境时翻车。另外class_list必须和训练用的类别清单顺序一致否则类别 id 会对不上。3. 数据验签三步走训练前的家底盘点3.1 统计图片分辨率与 EXIF 信息拿到数据集后的第一件事不是配训练环境而是把数据集的“家底”摸清楚。我一般会写一个脚本统计图片的高宽分布、是否有 EXIF 旋转信息、是否有损坏图片。遥感图像数据集里最常见的坑是图片实际分辨率不是 640x640 的整数倍模型输入的 reszie 比例一旦没对齐小目标就会被压没。# inspect_dataset.py from PIL import Image import os, collections img_dir JPEGImages size_counter collections.Counter() sizes [] for fn in os.listdir(img_dir): if not fn.endswith((.jpg, .jpeg, .png)): continue p os.path.join(img_dir, fn) try: with Image.open(p) as im: w, h im.size sizes.append((w, h)) size_counter[(w, h)] 1 # 读取 EXIF orientation存在时防止后续训练被自动旋转 exif im.getexif() if 274 in exif: print(f{fn}: EXIF orientation {exif[274]}) except Exception as e: print(f{fn}: 损坏或无法读取 - {e}) print(Top 10 分辨率:, size_counter.most_common(10))这段脚本会告诉你两件事图片是否有多个来源混入的分辨率比如 1280x720 和 1024x1024 混在一起以及有没有损坏文件。EXIF orientation 这类信息如果不检查训练时图像可能被隐性旋转导致标签坐标错位——虽然现代深度学习框架的 dataloader 大多会处理但自己先看到才安心。遥感舰船这种目标对分辨率特别敏感一张 1280 宽的图上可能存在只有 40x20 像素的小船输入网络时如果被缩放过目标信息可能直接丢失。3.2 标签坐标合法性与类别分布核对第二步是核对标签坐标是否越界、是否出现负值或超宽高同时统计类别分布。这个脚本直接读 YOLO 格式 txt如果谁给你一份标签文件这个校验必须跑在前面。# check_labels.py import os label_dir labels img_w, img_h 1280, 720 # 先用统一直观分辨率占位实际应从图片信息读取 class_counter {} bad_lines [] for fn in os.listdir(label_dir): if not fn.endswith(.txt): continue with open(os.path.join(label_dir, fn), r) as f: for line in f: parts line.strip().split() if len(parts) ! 5: bad_lines.append((fn, 字段数不为 5)) # 字段数异常 continue cls, cx, cy, w, h parts cx, cy, w, h map(float, (cx, cy, w, h)) # 坐标合法性检查 if w 0 or h 0: bad_lines.append((fn, 宽高小于等于零)) if cx - w/2 0 or cx w/2 1.0001: bad_lines.append((fn, x 方向越界)) if cy - h/2 0 or cy h/2 1.0001: bad_lines.append((fn, y 方向越界)) class_counter[int(cls)] class_counter.get(int(cls), 0) 1 print(类别分布:, dict(sorted(class_counter.items()))) print(异常行数:, len(bad_lines)) for fn, reason in bad_lines[:20]: print(f {fn}: {reason})这段代码虽然逻辑简单但能拦住大量“训练到一半 loss 变成 nan”的悲剧。越界坐标会让损失函数计算时出现 log 0或者让 anchor 匹配到根本不在图内的目标直接拉垮收敛。如果你的包里有多个不同分辨率图片那么这里的归一化坐标是按各自图片尺寸计算的检查时不能用一个固定尺寸应该逐张读取图片尺寸再对应检查——我上面用占位参数是为了说明思路实际工程里必须从 JPEGImages 里读每张图的高宽。3.3 训练集 / 验证集划分的合理性判断遥感数据集的划分有一个很容易被忽略的问题附近海域的舰船分布通常是成簇的同一片港口里可能拍到了几十艘船如果这些图分别出现在训练集和验证集里模型等于提前见过了答案。常见的错误做法是完全随机划分正确做法是按场景块划分——同一张场景延伸出来的裁剪块必须归到同一边。# split_by_scene.py import os, random, shutil # 假设文件名有场景前缀如 portA_001.jpg img_files os.listdir(JPEGImages) scene_groups {} for fn in img_files: scene_id fn.split(_)[0] # 按文件名前缀判定场景 scene_groups.setdefault(scene_id, []).append(fn) train_imgs, val_imgs [], [] for scene_id, imgs in scene_groups.items(): random.shuffle(imgs) split int(len(imgs) * 0.8) # 每个场景内部再按 8:2 切 train_imgs.extend(imgs[:split]) val_imgs.extend(imgs[split:]) print(ftrain{len(train_imgs)}, val{len(val_imgs)})这里的关键在于“场景前缀”来源于文件名约定。如果这个数据集的命名本身就是流水号比如 0001.jpg 到 2238.jpg那就没法按场景划分退而求其次的做法是按经纬度或拍摄时间划分——但文件名没有这些信息时只能依靠现有的ImageSets/Main给定划分或者按聚类算法对图像特征聚类后分桶。总之随机划分对遥感场景来说基本就是给自己埋雷。4. 用 YOLOv8 训练自己的舰船检测模型最小可跑通配置4.1 数据目录组织与 YAML 配置拿到数据之后最先要做的是按 YOLO 训练工具的习惯重新组织目录。常见做法是建一个主目录内部按images/train、images/val、labels/train、labels/val分好。YOLOv8 的YOLODataset在读数据时会自动找同级labels目录所以图片和标签的目录层级必须严格对应。# ship.yaml path: /data/ship_dataset # 数据集根目录绝对路径 train: images/train # 相对 path 下的训练图片目录 val: images/val # 相对 path 下的验证图片目录 nc: 17 # 类别数 names: 0: aircraft_carrier 1: amphibious_ship 2: cargo 3: destroyer 4: fishing_boat 5: frigate 6: hovercraft 7: merchant_ship 8: patrol_boat 9: rescue_boat 10: sailboat 11: submarine 12: tanker 13: tug 14: warship 15: yacht 16: oil_platformnames的排列顺序必须和标签 txt 里的类别 id 一一对应这是 YOLO 系训练最容易出错的点。如果作者的类别清单和你 YAML 里的不一致——比如他把cargo放在 id2而你漏看了一个类别——那么训练不会报错但模型会把货船学成了巡逻艇验证集 mAP 还会诡异地高因为 datset 内部是一致的只有在实际使用时暴露问题。所以拿到数据集后建议先看一遍类别清单文件或者自己用脚本把类别 id 和名称打出来确认。4.2 模型选型与训练命令舰船检测有一个绕不开的难点小目标密集排列。同一幅遥感图像里可能停着十几艘并排的船每艘船在 640x640 的输入下只有 20-50 像素宽。针对这种情况模型初始权重选择上我一般会用yolov8s而不是yolov8n理由是 nano 的 backbone 太浅特征图上的语义信息不够支撑细粒度分类——比如货轮和油轮在缩小的特征图上几乎无法区分。如果显存允许yolov8m的收益会更明显。yolo detect train \ dataship.yaml \ modelyolov8s.pt \ imgsz1280 \ batch16 \ epochs100 \ patience20 \ projectship_experiment \ namerun_ship_v1imgsz1280是这个任务的胜负手。如果按默认的 640 训练舰船目标平均只有 30 像素左右在 640x640 输入下可能缩小到 10 像素级别检测器基本看不到细节。1280会让显存占用翻倍所以要配合batch16来平衡。如果你的 GPU 只有 8GB 显存一个务实方案是先用imgsz640跑通流程确认能收敛后再用更大尺度微调——不建议一上来就追求最优参数先让流程跑通才是第一优先级。4.3 训练过程中的关键监控指标训练时不要只盯 loss 曲线更重要的两个指标是mAP50和mAP50-95。遥感舰船检测里 mAP50 往往虚高因为框定位精度要求没那么严格真正反映定位质量的是 mAP50-95它对边界框的精准度更敏感。如果训练到 50 轮后 mAP50-95 还在缓慢爬升说明模型还在学习更精细的定位此时早停反而会损失性能。另一个需要盯的是验证集的分类混淆矩阵。舰船类别里最容易混淆的是货船cargo和油轮tanker它们在光学影像里轮廓接近都是矩形船体加舰桥。如果训练日志里这两类的 recall 都偏低就要考虑用类别权重或复制粘贴增强来优先处理尾部类别。5. 避坑指南遥感舰船检测数据集的五个经典翻车现场5.1 解压后图片分辨率被统一缩放现象训练前统计图片尺寸发现所有图片都是 640x640而 XML 里写的高宽是 1280x720或者图片源文件本身是 3000x2000被压缩包作者统一调整过。原因数据集作者为了缩小压缩包体积常见做法是对图片做了等比缩放或中心裁剪。这样会导致标签 txt 里的归一化坐标虽然看起来合法但实际目标的像素面积变小可辨识细节丢失。解决用上一节inspect_dataset.py先统计真实图片尺寸。如果发现图片被统一缩放检查一下目标最小尺寸——如果缩小后最小目标还大于 15x15 像素问题不大如果小于 10 像素建议放弃这张图或在增强阶段使用拼图策略把多个裁剪块拼成大图再训练。5.2 类别名里有空格txt 解析错位现象训练启动后日志里出现IndexError: list index out of range或class id 超出 names 范围。原因VOC XML 里的name如果写成passenger ship或oil platform转换到 YOLO 格式时如果没做空格替换标签行的第二个字段会被误判为类别 id。解决写脚本把类别清单里的空格统一替换成下划线同时做一层额外保险# fix_class_names.py import os # 读类别清单 with open(classes.txt) as f: classes [line.strip() for line in f] # 展示所有类别名人工确认后再替换 print(classes) # 替换文本 for i, name in enumerate(classes): if in name or - in name: classes[i] name.replace( , _).replace(-, _) with open(classes_fixed.txt, w) as f: f.write(\n.join(classes))替换后记得同步修改 YAML 里的names和所有标签 txt 的第一列 id。这个步骤虽然机械但漏掉一个空格模型就会把一类目标整体学歪。5.3 标签坐标越界归一化坐标超出图片范围现象训练正常loss 下降但测出来的检测框全部偏到图像边缘或者某些目标完全检测不到。原因YOLO 格式生成时用了中心点加宽高的表示如果原始 VOC 标注里的xmax略大于图片宽度标注软件常见问题转换脚本没做钳制就会产生cx w/2 1的非法标签。解决直接过滤宽高小于 3 像素的实例这类小目标在多数网络里都是噪声留着只会干扰 anchor 匹配。同时对越界坐标做钳制到 [0,1] 区间def clamp_label(cls, cx, cy, w, h): # 先把中心点和宽高钳制在合法范围 if w 0 or h 0 or w 1 or h 1: return None x1 max(0, cx - w/2) y1 max(0, cy - h/2) x2 min(1, cx w/2) y2 min(1, cy h/2) if x2 - x1 0.01 or y2 - y1 0.01: return None # 钳制后太小丢弃 new_cx (x1 x2) / 2 new_cy (y1 y2) / 2 new_w x2 - x1 new_h y2 - y1 return cls, new_cx, new_cy, new_w, new_h这里的0.01阈值对应输入 1280 分辨率下的约 12 像素低于这个值的目标本身就没有足够特征。5.4 训练集和验证集来自同一场景的裁剪块现象训练完成时 mAP50 达到 0.93但一上真实港口图像立刻漏检严重val 集指标惨不忍睹。原因数据集的原始大图可能被切分成多个小 patch同一个港口码头的不同裁块随机分到了 train 和 val。模型在训练时见过同一片港区相当于开卷考试。解决这种问题靠代码比较难自动识别除非文件名有明确前缀最落地的方案是观察验证集 loss 是否明显高于训练集——如果 loss 差距大划分基本出了问题。重新按场景块划分确保同一来源的裁块全部归入训练或验证一侧不要交叉。5.5 类别不均衡导致尾部类别永远学不会现象训练结束后前几个大类的 AP 都在 0.85 以上而最后 3 个类别的 AP 只有 0.2-0.4并且无论怎么增加训练轮次都上不去。原因遥感舰船数据集的类别分布天然长尾比如渔船可能有 800 个实例而潜艇可能只有 50 个实例。模型在每轮训练中见到潜艇的样本次数太少。解决两个常用方案。一是给尾部类别增加采样权重具体在 YOLO 里表现为队列采样或类别权重参数二是用复制粘贴增强Copy-Paste Augmentation把尾部类别的目标抠出来随机叠加到同一批图片的其他位置。# oversample_tail_classes.py import random, shutil, os # 统计类别分布 class_counts {} for fn in os.listdir(labels): with open(os.path.join(labels, fn)) as f: for line in f: cls int(line.split()[0]) class_counts[cls] class_counts.get(cls, 0) 1 # 设定一个阈值低于该值的类别图片整体复制一份加入训练集 threshold 200 for fn in os.listdir(labels): with open(os.path.join(labels, fn)) as f: lines f.readlines() need_dup any(int(l.split()[0]) threshold for l in lines) if need_dup: base fn[:-4] shutil.copy(fimages/{base}.jpg, fimages_extra/{base}_dup.jpg) shutil.copy(flabels/{base}.txt, flabels_extra/{base}_dup.txt)直接把图片复制一份加入训练集会让模型每轮迭代都多看到这些稀有类别。这种简单过采样虽然不如 SMOTE 精致但在检测任务里往往最有效因为上下文是真实的不像插值合成可能产生伪影。6. 更进一步的精度提升旋转框检测与类别细化如果你已经把 VOCYOLO 双格式的数据集在 YOLOv8 上训通并且发现轴对齐的矩形框在密集排列的舰船上表现欠佳——比如相邻两艘船挨得很近水平框把两艘船框在一起——那下一步值得投入的方向是旋转框检测。遥感舰船很多是长条形且方向任意轴对齐框的回归损失会被“朝向”这个维度拖累。MMRotate 或者 YOLOv8-OBB 都支持这类训练你需要从 VOC XML 里读取四个角点坐标而不是 bbox 的矩形信息数据集本身如果只提供水平框标注那么需要先做一个“最小外接旋转框”的转换。另一个值得尝试的精度提升点是类别合并。17 个类别里有些类别之间的视觉差异非常小比如军舰warship和驱逐舰destroyer在低分辨率卫星图上几乎无法区分。如果实际业务只关心“商船/渔船/军舰”三大类可以在训练前用脚本把同类别的 id 映射合并这会显著提升每个大类的正样本数量mAP 往往能提升 3-5 个点。# merge_classes.py import os # 定义合并映射原类别 id - 新类别 id merge_map { 3: 0, # destroyer - warship 4: 1, # fishing_boat 保留 14: 0, # warship - warship 2: 2, # cargo 保留 12: 3, # tanker 保留 } def rewrite_label(label_path, new_label_path, merge_map): with open(label_path) as f: lines f.readlines() with open(new_label_path, w) as f: for line in lines: parts line.strip().split() cls int(parts[0]) if cls not in merge_map: continue # 类别直接丢弃如果不需要 new_cls merge_map[cls] f.write(f{new_cls} { .join(parts[1:])}\n)对 17 类数据集来说合并之后通常能压到 8-10 类训练时的收敛速度和 mAP 都会有可感知的提升。不要觉得这是“作弊”——业务落地时类别粗粒度意味着更高的可解释性和更低的误报率毕竟遥感影像里“渔船”和“小型巡逻艇”的边界本来就是模糊的。我现在的习惯是拿到任何一个新数据集先写统计脚本跑出类别分布、图片尺寸、label 合法性三张报表再决定用什么训练策略。盲开训练就是拿自己的时间和 GPU 电费赌运气数据校验做扎实的工程翻车概率会小很多。希望这篇笔记能让你少踩几个坑也希望你手里的模型在港口和近海场景里跑得稳。本文还有配套的精品资源点击获取