遥感小目标数据集构建实战:潜艇卫星图检测全流程

发布时间:2026/10/2 14:05:44
遥感小目标数据集构建实战:潜艇卫星图检测全流程
简介面向人工智能目标检测研究与视觉模型训练这份潜艇卫星图数据集提供了可直接上手的监督学习素材。共2001个文件内含1000张1024×1024的jpg卫星影像与1000个配套的xml标注文件另有1个txt说明文档标注信息以边界框形式记录潜艇位置覆盖识别与定位双重任务省去手动标注成本适合直接投入YOLO、Faster R-CNN、Mask R-CNN等主流框架进行训练、验证和测试。整体压缩包约345.83MB目录按images、annotations、info.txt分层组织便于批量读取与解析对搭建自动化检测流程十分友好。目前已有1686人学习下载常被用于海洋监视、航道安全、水下目标识别等课题或实战项目。在有限样本下配合迁移学习与数据增强还能进一步检验并提升模型对不同光照、海况和云层干扰场景的鲁棒性。1. 潜艇卫星图目标检测的数据集到底难在哪讲个真实场景你在遥感影像里找一艘潜艇它可能只有二三十个像素和周围的海面杂波、尾迹、云影混在一起。用自然图像的目标检测数据集思路去做第一轮验证就会翻车——模型要么什么都检不出来要么把浪花当成目标。潜艇卫星图数据集之所以难不是难在“标注”本身而是难在“目标存在性”的前提判定卫星拍到的到底是潜艇、水面舰还是一段不规则波浪所以做这个方向的数据集本质是在做一套“从强噪声背景里抠弱信号”的工程。这篇笔记要讲的就是一条从原始卫星影像到可训练数据集的完整落地路径包括数据来源、切片策略、标注规范、小目标增强以及我在反复重做数据集时踩过的五个坑。适合正在做遥感目标检测、或者准备用 YOLO 类模型训练小众目标的朋友新手能按步骤复现熟手可以直接跳到第 5 章看参数边界。2. 先分清任务边界潜艇卫星图数据集的特殊性在哪里2.1 卫星图像目标检测与常规数据集的三点本质差异常规目标检测数据集比如 COCO、VOC目标通常占图像面积的 5% 到 30%背景干净类别之间形态差异大。卫星图目标检测完全不是这个逻辑。第一点是目标尺度极小一艘常规潜艇长度约 70 米到 120 米在 0.5 米分辨率的卫星影像里约占据 140 到 240 个像素看起来只是一条“细长的深色条带”到了 10 米分辨率的 Sentinel-2 影像里它就只剩下 7 到 12 个像素基本是一个点。第二点是大背景重复纹理海面有波浪、有耀斑、有云影、有船只尾迹这些纹理的灰度分布和潜艇在水面时呈现的深色条带非常相似检测器很容易把纹理误判为目标。第三点是时间与光照不一致不同卫星过境时间不同太阳高度角不同同一条潜艇在不同影像里的灰度对比度可能相差 3 倍以上。这三条差异直接决定了数据集设计策略——不能用普通目标检测的思维方式去选图、切图和标注。普通数据集要求目标清晰可辨而卫星图数据集要解决的是“如何在高噪声背景中保持目标的语义完整性”。所以第一步不是急着找图而是先想清楚你手里的影像分辨率能支撑多大目标的检测如果你用的是 10 米分辨率影像却希望检测出单艘潜艇那在物理上就不成立数据集做得再精细模型也不可能学到像素里不存在的信息。2.2 潜艇目标的成像特征与可分性分析这是整个数据集建设中最值得花时间琢磨的环节。潜艇在卫星图像里通常以三种形态出现水面航行状态、半潜状态、水下潜航状态。水面航行状态的潜艇可见的是细长的艇体轮廓在光学影像中呈现深灰色或深黑色条带周围有艇艏波和尾迹尾迹区域灰度与海面背景差异明显。半潜状态的潜艇只露出指挥台围壳在影像里形成一个短小的深色矩形大小约 10×20 像素这是最容易和小型渔船混淆的形态。水下潜航状态的潜艇在光学影像中几乎不可见只能通过尾流在特定阳光角度下的海面散射特征来间接识别尾流呈“V”形亮带或暗带宽度约 20 到 50 米长度可达数百米。可分性分析的核心是回答一个问题目标和背景之间是否存在稳定的特征差异以光学影像为例潜艇艇体与海面背景的灰度差通常在 20 到 60 灰度值之间8 位影像而波浪纹理的灰度变化幅度也在 15 到 40 之间。这意味着纯灰度特征不是可分性指标。真正有效的是“几何形状 上下文关系”潜艇在水面状态时长宽比大于 3:1且伴随尾迹港口停泊的潜艇周围一定有码头设施与防波提。所以标注时要刻意保留这些上下文信息而不是像普通数据集那样把目标裁到紧致包围框。我在实际标注时习惯保留目标周围 2 到 3 倍的目标区域作为上下文让模型能学到“目标 环境”的组合特征。2.3 光学影像与 SAR 影像两条技术路线的数据差异标注之前先做模态选型。光学卫星影像如 Sentinel-2、WorldView 系列的分辨率可以做到亚米级肉眼可读性好适合用来建立直观认知但受云层和光照影响严重——多云海域经常几周都拿不到一张干净影像。SAR合成孔径雷达影像如 Sentinel-1、高分三号则不受云层影响可以全天时全天候采集而且对水面目标的雷达回波特征明显。但 SAR 图像是单通道灰度图相干斑噪声极其强烈肉眼识别难度大标注者的认知负担显著增加。两种模态的数据集制作策略完全不同。光学影像的关键词是“筛选”先按云覆盖量筛再按太阳高度角筛最后按目标形态筛清洗工作占掉 70% 的时间。SAR 影像的关键词是“降噪与去斑”需要做多视处理或者使用去斑滤波器如 Lee 滤波器、Frost 滤波器然后再送入标注流程。如果条件允许建立双模态数据集的价值很大——光学影像负责高精度定位SAR 影像负责全天候监测两个模态在推理阶段做决策级融合可以显著降低漏检率。但要注意双模态数据集的标注一致性维护成本极高一对同一个目标的包围框在光学和 SAR 图像里可能差出 10 个像素以上这需要在标注规范里提前定义锚定规则通常以目标的几何中心为锚点。3. 自建潜艇卫星图数据集从原始影像到可训练样本的完整流程3.1 公开影像源与获取工具配置获取原始影像是第一步。常见的公开影像源有 Sentinel-1SAR10 米分辨率、Sentinel-2光学10 米分辨率、Landsat 系列光学15 米分辨率以及国内的高分系列部分公开数据。对于潜艇检测这个任务Sentinel-1 和 Sentinel-2 的组合已经是成本最低且覆盖度最高的方案。下载 Sentinel 数据通常使用 Copernicus Open Access Hub 或者 AWS 上的 Sentinel 数据存储桶。用 Python 的sentinelhub库可以脚本化下载配置起来更快。# 使用 sentinelhub 批量下载指定区域的 Sentinel-2 影像 from sentinelhub import SentinelHubRequest, SentinelHubDownloadClient, BBox, CRS, DataCollection # 设置 API 凭证从 Sentinel Hub 账号中获取 config SHConfig() config.sh_client_id your_client_id config.sh_client_secret your_client_secret # 定义研究区域这里设置为中心经纬度与 3000 米边长 bbox BBox([115.8, 20.5, 116.1, 20.8], crsCRS.WGS84) # 设定时间范围目标是得到不同季度、不同光照条件下的样本 requests [] for time_interval in [(2024-03-01, 2024-03-15), (2024-06-01, 2024-06-15)]: request SentinelHubRequest( evalscript //VERSION3 function evaluatePixel(sample) { // 使用真正的彩色合成波段RB04, GB03, BB02 return [sample.B04, sample.B03, sample.B02]; } , input_data[SentinelHubRequest.input_data(DataCollection.SENTINEL2_L1C, time_intervaltime_interval)], responses[SentinelHubRequest.output_response(default, png)], bboxbbox, size(512, 512), configconfig ) requests.append(request) # 批量执行下载请求 response SentinelHubDownloadClient(requests).download()这段代码的逻辑不复杂但参数设置有几个关键点需要解释。BBox定义了取图区域坐标顺序是 [西经, 南纬, 东经, 北纬]写错顺序会直接导致下载区域偏移到海里。size(512, 512)设定输出影像的像素尺寸这里不是原图分辨率而是 Sentinel Hub 服务端按你要的区域范围插值重采样后的尺寸所以要算好3000 米边长 512 像素对应约 5.8 米分辨率比原始 10 米分辨率略高一些。evalscript是波段合成脚本这里用的是真彩色合成如果你要训练灰度图直接输出sample.B04单波段即可。下载前建议先用一个小的size比如 64×64测试 API 凭证是否有效否则很容易因为凭证权限问题在批量下载中段浪费大把时间。3.2 大影像切片策略子图划分的尺寸与重叠率设计从卫星平台下载的是整景影像一景 Sentinel-2 影像覆盖范围约 10980×10980 像素不能直接拿去训练。一是计算资源承受不了这么大的输入张量二是目标在这种超大影像里占比太小检测器学不到有效特征。必须做切片。切片策略直接决定数据集质量常见的做法是按固定窗口切图窗口尺寸一般选 512×512 或 1024×1024重叠率 10% 到 20%。import cv2 import numpy as np from tqdm import tqdm def sliding_window_crop(image, window_size512, overlap0.15): 大影像滑动窗口切片 :param image: HxWxC 的卫星影像 :param window_size: 切片边长 :param overlap: 相邻切片重叠比例 h, w image.shape[:2] step int(window_size * (1 - overlap)) crops [] # 按行列步进滑动注意边界补齐 for y in range(0, h - window_size 1, step): for x in range(0, w - window_size 1, step): crop image[y:y window_size, x:x window_size] crops.append((x, y, crop)) # 处理右边界与下边界的残差部分 if w % step 0: for y in range(0, h - window_size 1, step): x w - window_size crops.append((x, y, image[y:y window_size, x:x window_size])) if h % step 0: for x in range(0, w - window_size 1, step): y h - window_size crops.append((x, y, image[y:y window_size, x:x window_size])) return crops # 读取整景影像并做切片 full_image cv2.imread(sentinel2_scene.png, cv2.IMREAD_COLOR) tiles sliding_window_crop(full_image, window_size512, overlap0.2) print(f生成切片数: {len(tiles)})这里的重叠率参数值得细抠。对普通目标检测数据集重叠率低一点没关系因为目标在图像中的占比大不会因为切图被截断。但卫星小目标不一样目标本身可能只有 20 像素如果它恰好落在切片边缘包围框会被截断一半标注和训练都会出问题。重叠率 15% 到 20% 能有效减少这种“目标被切破”的概率但代价是数据集冗余增大同一目标出现在多个切片中训练时相当于对其过采样。处理方案有两个一是切片时记录坐标偏移在标注时把全局坐标换算成局部坐标二是在训练阶段容忍目标重复出现靠数据增强的随机性去弱化过拟合风险。3.3 标注工具选型与格式转换脚本切片完成后进入标注环节。遥感目标检测常用的标注工具有 LabelImg、CVAT、Label Studio。我一般推 CVAT 做卫星影像标注理由很直接它支持大图缩放浏览512×512 切片在界面里可以无损放大到看到单个像素而 LabelImg 对超大图像的缩放流畅度不如 CVAT。另外 CVAT 支持多人协作、任务分派、标签分歧检查一个数据集几百张切片时单人标注容易疲劳多人协同能提高效率。标注完成后导出格式一般是 COCO JSON 或者 CVAT XML但训练 YOLO 系模型需要 TXT 格式的归一化坐标标注。格式转换脚本要自己写下面是标准转换逻辑。import json import os from pathlib import Path def coco_to_yolo(coco_json_path, output_dir): 将 COCO 格式标注转换为 YOLO 格式标注 COCO 的坐标是像素绝对坐标YOLO 需要归一化的 cx, cy, w, h with open(coco_json_path, r, encodingutf-8) as f: coco_data json.load(f) # 建立图像ID与文件名的映射 img_id_to_name {img[id]: img[file_name] for img in coco_data[images]} # 分离目标的类别 ID category_id_to_label {cat[id]: idx for idx, cat in enumerate(coco_data[categories])} # 按图像分组所有标注对象 annotations_by_img {} for ann in coco_data[annotations]: img_id ann[image_id] annotations_by_img.setdefault(img_id, []).append(ann) for img_id, anns in annotations_by_img.items(): file_name img_id_to_name[img_id] # 读取图像尺寸用于归一化 img_info next(item for item in coco_data[images] if item[id] img_id) img_w, img_h img_info[width], img_info[height] # 输出 txt 文件名与图像同名 txt_path Path(output_dir) / (Path(file_name).stem .txt) lines [] for ann in anns: # COCO bbox 格式为 [x, y, width, height] x, y, w, h ann[bbox] # 转换为 YOLO 的归一化中心点格式 cx (x w / 2) / img_w cy (y h / 2) / img_h nw w / img_w nh h / img_h # 钳制越界坐标到 [0,1] 区间避免训练时读入非法值 cx min(max(cx, 0.0), 1.0) cy min(max(cy, 0.0), 1.0) nw min(max(nw, 0.0), 1.0) nh min(max(nh, 0.0), 1.0) label category_id_to_label[ann[category_id]] lines.append(f{label} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}) with open(txt_path, w, encodingutf-8) as f: f.write(\n.join(lines))这个转换脚本的逻辑本身不复杂但边界处理容易疏忽。钳制坐标到 [0,1]这一步看起来是保底操作实际上是一个双刃剑——如果一张切片里目标的包围框有一半在图像外钳制后的坐标会变成一条贴近图像边界的细线模型会学到错误的形状特征。所以更好的做法是在转换阶段就过滤掉那些w或h小于原始尺寸 20% 的标注框。另外category_id_to_label的映射有个隐藏陷阱CVAT 导出的 COCO JSON 中类别 ID 往往不是从 0 开始的直接拿去做索引会导致类别错位。接上enumerate重新编号是最稳妥的方案。4. 增强策略与数据划分小目标检测的数据集成败点4.1 针对小目标的增强马赛克增强与尺度扰动数据集不是“切完了 标注好了”就能直接训练。卫星影像里的目标尺度小直接输入 YOLO 系列模型时默认输入尺寸一般是 640×640你的 512×512 切片缩放过去目标可能只剩十几个像素。模型在特征提取层经过多次下采样后尾部的特征图上一个目标可能连一个响应点都占不满。所以增强策略的设计重点不是随便做个随机翻转、亮度扰动而是“保持甚至放大目标在有效特征图上的响应面积”。# data.yaml 文件内容yolov8 训练时的数据集配置 path: ./datasets/submarine_dataset train: images/train val: images/val names: 0: submarine 1: ship# 使用 ultralytics YOLOv8 训练开启马赛克增强与大尺度扰动 yolo train \ modelyolov8n.pt \ datadatasets/submarine_dataset/data.yaml \ imgsz640 \ epochs100 \ batch8 \ mosaic1.0 \ scale0.9 \ hsv_h0.01 \ hsv_s0.5 \ hsv_v0.4 \ degrees90 \ fliplr0.5这里重点说mosaic1.0和scale0.9两个参数。马赛克增强把四张训练图拼成一张会让拼接图中目标的数量变为原来的四倍这对小目标检测尤其有效——因为模型在一张图上能同时看到多个不同背景下的目标迫使它学到目标的固有特征而不是背景残差。scale0.9表示尺度抖动范围是 0.1 到 1.0 倍这意味着在部分样本里目标会被进一步缩小而在另一部分样本里会被放大。实际经验是对小目标数据集这个参数设置到 0.8 到 0.9 之间会有稳定收益但超过 0.95 之后模型容易把“模糊的微小暗块”当成目标误检率快速上升。4.2 数据集划分按“场景独立性”而不是“随机划分”这是做卫星目标检测数据集时最容易犯的分布式错误。按随机比例把切片分到训练集和验证集看起来没问题实际上泄漏严重——同一个海域、同一天的相邻切片背景完全相似如果一部分进了训练集另一部分进了验证集验证分数就会虚高。等模型部署到一片新的海域性能立刻崩掉。正确做法是按场景分组划分。具体流程是先给所有切片打上来源场景标签如“某港口 2024-03”“某海峡 2024-06”然后以场景组为单位分配数据。以下代码演示了按场景分组划分的过程。from sklearn.model_selection import GroupShuffleSplit # 假设有一个 DataFrame每行对应一张切片 import pandas as pd df pd.read_csv(tiles_metadata.csv) # scene_id 代表切片所属场景组 X df[tile_path] y df[class_label] groups df[scene_id] # 用 GroupShuffleSplit 确保同一个场景不会同时出现在 train 和 val 中 gss GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, val_idx next(gss.split(X, y, groups)) print(训练集样本量:, len(train_idx), 验证集样本量:, len(val_idx))GroupShuffleSplit的原理是按groups做切分保证同一组内的所有样本要么都在训练集要么都在验证集。这个做法直接断绝了背景泄漏问题。划分比例上建议训练集与验证集按 8:2但如果数据总量少于 2000 张切片可以考虑 7:3同时用 5 折交叉验证来评估稳定性。还有一个容易忽视的点卫星影像切片之间存在“时间相关性”——同一场景不同日期的切片可能因为季节光照变化而呈现完全不同的灰度分布这恰恰是模型泛化能力需要的多样性所以划分时应该尽量让每个场景组内包含不同日期的样本而不是把所有春季样本划到验证集里。4.3 类别平衡与正样本问题潜艇目标数据集绕不开的正样本稀缺问题。假设你有 5000 张切片其中只有 600 张是含潜艇目标的正样本剩下的全是纯海面背景。直接训练的话模型会认为输出“无目标”是最安全的预测以此获得极低的损失。解决思路有两个方向一是过采样正样本让正样本在训练中的一个 epoch 内出现的频率翻倍二是引入负样本挖矿策略。我的常见做法是先用经过预训练的 YOLO 模型跑一遍所有负样本切片找出置信度高于 0.3 的“假阳性切片”把这些挑出来加入训练集作为难负样本。这个策略可以显著压低误检率效果是单纯加旋转增强的两到三倍。# 负样本挖矿脚本的概念示例 import torch from ultralytics import YOLO # 加载一个已经训练过的模型 model YOLO(runs/detect/train/weights/best.pt) false_positive_tiles [] # 遍历所有负样本切片 for tile_path in negative_tile_list: results model.predict(tile_path, conf0.3, verboseFalse) # 如果模型在纯背景上输出了目标框说明该切片对模型有干扰需要加入训练集 if len(results[0].boxes) 0: false_positive_tiles.append(tile_path) print(难负样本数量:, len(false_positive_tiles)) # 把这些切片移动到训练集数据目录中这个脚本的执行时机有些讲究。第一轮训练先用简单的数据增强跑个短周期比如 30 epoch得到初始模型第二轮训练前执行挖矿把难负样本加入训练集再训练完整周期。经过一轮挖矿后模型对海面杂波的鲁棒性会明显提升验证集的误检率通常能下降 20% 以上。挖矿迭代可以做两轮第三轮之后收益就变很小了。5. 避坑记录潜艇卫星图数据集最常见的问题与对策5.1 现象训练损失一直下降但验证集精确率始终为零出现过不止一次的情况是模型在训练集上表现越来越好到了验证集上却什么都检不出来。后来排查发现原因出在标注坐标的参考系错位。CVAT 导出 COCO 格式时如果用“原始图像坐标”标注的切片是 512×512但转换脚本里读到的img_w和img_h是 640×640归一化坐标就被整体缩放了一截目标框位置和实际目标完全对不上。解决方式在格式转换脚本里加上坐标上限自检——统计所有标注框中心点的横纵坐标范围如果离 0.5 的均值偏差过大说明参考系换算有误。这个自检步骤五秒钟就能跑完但每次都能拦下一批错误标注。5.2 现象验证集 mAP 虚高部署到新地区表现骤降这是典型的“划分泄漏”导致的问题。随机划分训练集和验证集时来自同一景大影像的相邻切片被分开模型在训练时已经“见过了”验证区域的海面背景纹理。解决做法就是前面提到过的GroupShuffleSplit按场景划分。除此之外建议保留一个更极端的“跳出区”验证集从一张完全独立于训练场景的影像里切出约 10% 的切片全程不参与训练优化只用于最终模型评估。这个跳出区验证集的分数才是接近实际部署性能的参考值。5.3 现象潜艇和货船互相误检类别混淆严重这个问题的根源在于标注规范不够明确。在低分辨率卫星影像里一艘万吨级货船在码头附近停泊时轮廓与潜艇的细长形态十分相似标注者很容易把两类目标标注混。解决方式是建立按目标长度与几何形态的标注决策树——潜艇的长宽比通常在 5:1 到 9:1 之间而货船在 3:1 到 5:1 之间潜艇舰艏形状是圆钝的货船的舰艏是尖峭的。把这些规则写进标注规范里同时让两个标注者独立标注同一批切片计算两次标注的类别一致性低于 85% 的切片剔除重标。5.4 现象训练时 loss 直接爆掉数值变成 NaN先前我在跑 YOLO 训练时遇到过 NaN 损失后来定位到是标注数据中存在空标注文件——某一张切片没有目标但其对应的 txt 文件是空文件这本身没问题但如果另一个文件里存在只有类别标签没有坐标的数据行读入时坐标为空损失计算就成了 NaN。解决方式是训练前用脚本扫描所有标注文件校验每行的字段数是否为 5且坐标值都在 [0,1] 区间内。另外要注意所有输入图像要先检查是否损坏可以用cv2.imread后断言返回的数组不是None。5.5 现象模型把云的边缘识别为潜艇这是遥感目标检测特有的问题。云层边缘与海面交界处有明显的灰度梯度变化特征上接近“细长条带”模型容易误检。我观察到的现象是误检框都是紧贴着云边缘延伸的方向和目标的真实形态差异很大。解决方式有两种一是在切片清洗阶段把云覆盖率超过 30% 的切片直接丢弃减少模型接触此类干扰的机会二是增加一个“背景类别”的实际标注类别——把云、浪花、船只尾迹等典型干扰物标注成独立类别让模型学习到“这些东西不是潜艇”。前者省事后者训练效果好但标注成本翻倍。对精度要求高的场景我建议两条腿走路。6. 数据集的最后一关用有效特征响应去验证数据质量正式训练前有一个很划算的验证技巧用预训练模型跑特征图可视化确认模型在目标位置确实产生了有效的特征响应。这一步 30 分钟就能完成但可以避免训练了十几个小时后才发现数据集设计方向的根本问题。具体做法是加载一个在遥感数据上预训练好的目标检测模型对标注过的验证集切片进行推理然后使用 Grad-CAM 类方法可视化目标所在的特征热力图。如果热力图的峰值中心与标注框的中心接近说明该切片的目标形态特征是清晰的、可学习的如果热力响应散落在背景纹理上说明这张切片的标注本身就存在歧义。按此方法把响应差的目标切片全部找出来重新检查标注是否准确。有一个更直接的数据集检查指标计算每张切片中目标包围框像素与切片总像素的占比。如果这个比值小于 0.001建议先把该切片去除或裁剪到更小的包围区域因为模型很难从低于 0.1% 像素占比的目标里学到有效信息。满幅 512×512 的切片里一个 20×40 的目标占比约 0.3%已经是边缘情况。低于这个值即使训练能看到目标学到的大部分“特征”都来自背景。说到这回顾我做这个数据集的经验最大的教训是不要急着标注。先把任务边界想清楚把数据源的分辨率与目标尺度的匹配关系摆到桌面上再去谈标注工具和格式转换。卫星图像目标检测的数据集建设90% 的时间应该花在理解“目标在这个图像里长什么样”剩下的 10% 才是执行。对住你的分辨率测底限对住你的切片策略保目标完整度对住你的场景划分防泄漏这套方法论能让你在潜艇卫星图这条路上少走至少两轮弯路。希望帮到你。本文还有配套的精品资源点击获取