简介:本资源为面向YOLO系列目标检测算法的航空卫星图像数据集,适用于遥感场景下的地物分类与目标检测任务,可支撑森林、公路、作物、河流、住宅、工业、果园、牧场及贫瘠土地等多类地物的识别研究,适合从事遥感图像分析、算法验证与模型训练的学生及开发者使用。压缩包共包含2000个文件,其中1230个xml文件与770个txt文件,分别对应VOC格式与YOLO格式的标注,包体约60.51MB,并附有data.yaml配置文件,已划分好训练与验证集,可直接用于yolov5、yolov8、yolov9、yolov7、yolov10及yolo11等算法。YOLO格式标注采用归一化中心点与宽高比例,便于直接读取训练。目前已有42人学习下载,读者可借此快速搭建遥感目标检测实验环境,省去数据采集与标注成本,并对照两种标签格式理解数据组织方式,为后续模型调参与效果评估提供可靠基础。
1. 航空卫星图像数据集落地:1366 张带标签样本能撑起哪些 YOLO 检测任务
拿到一份 1366 张带标签的航空卫星图像数据集,标注覆盖森林、公路、作物、河流、住宅、工业、果园、牧场、贫瘠土地九类地物,第一反应往往不是兴奋,而是犯嘀咕:这点量够不够训 YOLO?我的判断是,够,但前提是你把它当语义分割或场景分类的替代品来用,而不是硬套 COCO 那套密集小目标检测。航空卫星图像的成像高度决定了单像素地面分辨率通常在 0.3 到 2 米之间,同一类地物在不同图里的尺度差异极大,一块工业区可能占半张图,一条河流可能只有十几个像素宽。1366 张的体量放在自然图像里确实偏小,但遥感地物的类间差异比自然图像更依赖纹理和光谱,YOLO 的骨干网络对这类低频纹理特征本身就有不错的响应。这份数据集真正适合的人群是:做土地利用分类、遥感变化检测预研、边缘端地物识别的工程师,以及想用真实遥感数据跑通 YOLO 全流程的学习者。它不适合直接拿去打比赛刷 mAP,但足够让你把数据转换、增强、训练、导出、部署这条链路完整走一遍,并且暴露出遥感场景特有的坑。
2. 从 zip 到 YOLO 可训练格式:标签解析与目录重组
2.1 先搞清楚标签到底长什么样
拿到压缩包解压后,第一件事不是急着写训练脚本,而是把标签文件打开看几眼。遥感数据集的标签常见三种形态:一种是 PASCAL VOC 的 XML,每个目标一个 bbox;一种是掩膜 PNG,像素值对应类别;还有一种是 GeoJSON 或 shapefile,带地理坐标。标题里说的是「带标签」,没有指明格式,所以你得先做一次格式侦察。我一般会写个十几行的脚本统计标签文件的后缀分布和内容结构,这一步花五分钟,能省掉后面几小时的翻车。
import os from collections import Counter label_dir = "./labels" suffix_counter = Counter() sample_content = {} for root, _, files in os.walk(label_dir): for f in files: ext = os.path.splitext(f)[1].lower() suffix_counter[ext] += 1 # 每种后缀只留一个样本看内容 if ext not in sample_content: path = os.path.join(root, f) with open(path, "r", errors="ignore") as fh: sample_content[ext] = fh.read(300) print("后缀分布:", suffix_counter) for ext, content in sample_content.items(): print(f"\n--- {ext} 样本前300字符 ---") print(content)这段脚本做两件事:统计标签目录下所有文件的后缀分布,判断是 XML、TXT 还是 JSON;对每种后缀抽取一个样本打印前 300 个字符,让你直观看到标签的组织方式。如果输出是<annotation><object><name>forest</name><bndbox>...,那就是 VOC XML;如果是0 0.45 0.32 0.12 0.08这种五列数字,那就是 YOLO 格式的 class_id x_center y_center width height 归一化坐标;如果是{"type":"Feature","geometry":...},那就是 GeoJSON,需要额外做坐标投影转换。参数上唯一要注意的是errors="ignore",遥感标签偶尔混入非 UTF-8 编码,不加这个参数脚本会直接崩。
2.2 九类地物的类别映射与 ID 固化
确认标签格式后,下一步是把九个类别名映射成从 0 开始的整数 ID,并且写死在一个配置文件里。这件事看起来简单,但它是后面所有环节的地基。我见过太多人训练到一半发现类别 ID 对不上,或者推理时输出的类别名和训练时不一致,模型表现直接变成玄学。九类地物的英文名建议统一成:forest、road、crop、river、residential、industrial、orchard、pasture、barren。注意 road 和 river 在遥感图里都是细长条状,如果标签质量不高,这两类最容易混。
CLASS_MAP = { "forest": 0, "road": 1, "crop": 2, "river": 3, "residential": 4, "industrial": 5, "orchard": 6, "pasture": 7, "barren": 8, } # 反向映射,推理时用 ID_TO_NAME = {v: k for k, v in CLASS_MAP.items()} # 校验:确保没有重复 ID assert len(set(CLASS_MAP.values())) == len(CLASS_MAP), "类别 ID 有重复" print("类别数:", len(CLASS_MAP))这段代码的关键在于assert那行校验。类别映射一旦有重复 ID,YOLO 训练时不会报错,但模型会把两个类当成一个类学,最后 mAP 莫名其妙低一截,你还找不到原因。把CLASS_MAP单独存成一个classes.py或data.yaml里的names字段,训练、验证、推理三个环节都从这里读,不要在任何地方手写类别名。
2.3 划分训练集验证集并生成 data.yaml
1366 张图,我一般按 8:1:1 划分训练、验证、测试。遥感数据有个特殊点:如果图像是从大图上切下来的瓦片,相邻瓦片之间高度重叠,随机划分会导致验证集里的图和训练集里的图几乎一样,验证指标虚高。所以划分前要先检查文件名里有没有行列号或地理坐标,如果有,按地理区块划分而不是按文件随机划分。
import random import os import shutil random.seed(42) img_dir = "./images" lbl_dir = "./labels" out_dir = "./dataset" # 收集所有图片文件名(不含后缀) stems = [os.path.splitext(f)[0] for f in os.listdir(img_dir) if f.lower().endswith((".jpg", ".png", ".tif"))] random.shuffle(stems) n = len(stems) n_train = int(n * 0.8) n_val = int(n * 0.1) splits = { "train": stems[:n_train], "val": stems[n_train:n_train + n_val], "test": stems[n_train + n_val:], } for split, names in splits.items(): for sub in ("images", "labels"): os.makedirs(os.path.join(out_dir, split, sub), exist_ok=True) for stem in names: # 图片可能有不同后缀,遍历匹配 for ext in (".jpg", ".png", ".tif"): src_img = os.path.join(img_dir, stem + ext) if os.path.exists(src_img): shutil.copy(src_img, os.path.join(out_dir, split, "images", stem + ext)) break src_lbl = os.path.join(lbl_dir, stem + ".txt") if os.path.exists(src_lbl): shutil.copy(src_lbl, os.path.join(out_dir, split, "labels", stem + ".txt")) print({k: len(v) for k, v in splits.items()})random.seed(42)保证每次划分结果一致,方便复现。图片后缀遍历那段是因为遥感数据集经常混用 jpg 和 tif,不能假设统一后缀。划分完成后生成data.yaml:
path: ./dataset train: train/images val: val/images test: test/images nc: 9 names: ["forest", "road", "crop", "river", "residential", "industrial", "orchard", "pasture", "barren"]nc必须等于类别数,names的顺序必须和CLASS_MAP的 ID 一一对应。这个文件是 YOLO 训练的唯一数据入口,路径写相对路径时注意是相对于path字段的。
3. 遥感图像训练 YOLO 的参数选择:从输入分辨率到损失函数
3.1 输入分辨率不能照搬 640
自然图像检测默认imgsz=640,但遥感图像直接套这个值会出问题。航空卫星图像的典型尺寸是 512×512 或 1024×1024 的瓦片,如果强行缩到 640,细长的河流和公路会被插值糊掉,小目标直接消失。我的经验是:先统计训练集图像尺寸的中位数,如果中位数在 512 附近,imgsz设 512 或 640 都行;如果中位数在 1024 以上,优先设 1024,显存不够再降到 768。降分辨率带来的 mAP 损失在遥感场景里比自然图像更明显,因为地物边界本身就是弱特征。
# 统计图像尺寸分布 python -c " from PIL import Image import os, statistics sizes = [] for f in os.listdir('./dataset/train/images'): with Image.open(os.path.join('./dataset/train/images', f)) as im: sizes.append(im.size) ws = [s[0] for s in sizes] hs = [s[1] for s in sizes] print('宽中位数:', statistics.median(ws), '高中位数:', statistics.median(hs)) print('最小:', min(ws), min(hs), '最大:', max(ws), max(hs)) "如果宽高中位数差异大,说明图像不是正方形,YOLO 训练时会做 letterbox 填充,填充比例过高会浪费算力。这种情况可以考虑在数据转换阶段就把图像裁成正方形瓦片。
3.2 损失函数与 NWD 改进的适用边界
YOLO 默认的 CIoU 损失对常规 bbox 回归够用,但遥感地物里有一类特殊情况:河流、公路这种极端长宽比目标,CIoU 对角度和长宽比的惩罚不够敏感,回归容易偏。最近热词里出现的 NWD(Normalized Wasserstein Distance)改进就是针对这个问题的,它把 bbox 建模成高斯分布,用 Wasserstein 距离替代 IoU,对小目标和长条目标的回归更稳。但要注意,NWD 不是万能药,它在你这份九类地物数据上主要对 river 和 road 两类有提升,对 forest、pasture 这种大面积块状地物反而可能拖慢收敛。我的建议是先用默认 CIoU 跑一版基线,看 river 和 road 的 AP 是不是明显低于其他类,如果是,再换 NWD 做对比实验。
# 在 YOLO 的 loss.py 中替换 bbox 损失为 NWD 的核心逻辑示意 import torch def nwd_loss(pred_boxes, target_boxes, eps=1e-7): # pred_boxes / target_boxes: (N, 4) xyxy 格式 # 转成中心点+宽高 def to_cxcywh(b): cx = (b[:, 0] + b[:, 2]) / 2 cy = (b[:, 1] + b[:, 3]) / 2 w = (b[:, 2] - b[:, 0]).clamp(min=eps) h = (b[:, 3] - b[:, 1]).clamp(min=eps) return torch.stack([cx, cy, w, h], dim=1) p = to_cxcywh(pred_boxes) t = to_cxcywh(target_boxes) # 高斯分布均值与方差 mu_p, mu_t = p[:, :2], t[:, :2] var_p = (p[:, 2:] / 2) ** 2 var_t = (t[:, 2:] / 2) ** 2 # 二阶 Wasserstein 距离的平方 w2 = ((mu_p - mu_t) ** 2).sum(dim=1) + \ ((var_p.sqrt() - var_t.sqrt()) ** 2).sum(dim=1) nwd = torch.exp(-torch.sqrt(w2 + eps)) return 1 - nwd.mean()这段代码是 NWD 的简化实现,核心思路是把每个 bbox 的中心点和宽高映射成二维高斯分布的均值和方差,用两个高斯分布之间的 Wasserstein 距离衡量相似度。eps防止除零,torch.exp(-sqrt(w2))把距离转成 0 到 1 之间的相似度,最后取1 - nwd作为损失。实际集成到 YOLO 里时,需要在bbox_loss计算处替换,并且注意 NWD 的数值范围比 CIoU 小,学习率可能需要相应调低,我一般会从默认学习率乘以 0.5 开始试。
3.3 数据增强要克制几何变换
遥感图像的数据增强和自然图像有一个本质区别:自然图像里翻转、旋转、裁剪都不会改变物体的语义,但遥感图像有方向性。森林、作物、牧场这些地物对旋转不敏感,但公路和河流的走向是有地理意义的,过度旋转会让模型学到错误的朝向先验。我的配置是:水平翻转和垂直翻转可以开,90 度旋转可以开,任意角度旋转关掉,mosaic 增强开到 0.5 而不是默认的 1.0,因为 mosaic 把四张图拼在一起会破坏地物的空间连续性。
# YOLO 训练配置片段 fliplr: 0.5 flipud: 0.5 degrees: 0.0 mosaic: 0.5 mixup: 0.0 hsv_h: 0.015 hsv_s: 0.4 hsv_v: 0.3degrees: 0.0关掉任意角度旋转,mosaic: 0.5降低拼接概率,mixup: 0.0关掉混合增强,因为遥感地物的光谱特征在 mixup 后会产生不存在的类别组合。HSV 增强保留,因为不同卫星传感器的色调差异确实需要模型适应。
4. 训练过程排查:loss 不降、mAP 虚高与显存溢出
4.1 现象:训练 loss 前几个 epoch 就卡住不动
原因通常有三个:学习率太大导致梯度爆炸、类别 ID 越界、标签坐标没有归一化。遥感数据集最常见的是第三个,很多标注工具导出的坐标是像素绝对值,直接喂给 YOLO 会被当成归一化坐标,所有 bbox 都挤在图像左上角。解决方法是写个校验脚本,检查所有标签的坐标是否在 0 到 1 之间。
import os bad = [] for f in os.listdir("./dataset/train/labels"): with open(os.path.join("./dataset/train/labels", f)) as fh: for line in fh: parts = line.strip().split() if len(parts) != 5: bad.append((f, "列数不对")) continue cls_id = int(parts[0]) coords = [float(x) for x in parts[1:]] if cls_id < 0 or cls_id > 8: bad.append((f, f"类别ID越界: {cls_id}")) if any(c < 0 or c > 1 for c in coords): bad.append((f, f"坐标未归一化: {coords}")) print("问题标签数:", len(bad)) for item in bad[:10]: print(item)这个脚本检查三件事:每行是否五列、类别 ID 是否在 0 到 8 之间、坐标是否在 0 到 1 之间。跑一遍,问题标签一目了然。
4.2 现象:验证集 mAP 很高但测试集一塌糊涂
这是典型的验证集泄漏。前面提过,遥感瓦片如果来自同一张大图,相邻瓦片内容高度重叠,随机划分会让验证集和训练集共享大量相似像素。解决方法是按地理区块划分,或者用文件名里的行列号做分组划分。如果文件名里没有行列号信息,可以用图像感知哈希做聚类,把相似图像分到同一组再划分。
import imagehash from PIL import Image import os from collections import defaultdict groups = defaultdict(list) for f in os.listdir("./images"): with Image.open(os.path.join("./images", f)) as im: h = str(imagehash.phash(im, hash_size=8)) groups[h].append(f) print("哈希组数:", len(groups)) print("最大组大小:", max(len(v) for v in groups.values()))phash对图像做感知哈希,内容相似的图会落到同一个哈希值。如果最大组大小超过 20,说明数据里有大量近似重复图,必须按组划分而不是按文件划分。
4.3 现象:训练到一半 CUDA out of memory
遥感图像分辨率高,imgsz=1024时 batch size 设 16 很容易爆显存。解决路径有三条:降 batch size 到 8 或 4、开梯度累积模拟大 batch、开混合精度训练。我一般优先开混合精度,因为对 mAP 影响最小。
yolo detect train data=./data.yaml model=yolov8n.pt imgsz=1024 batch=8 amp=True epochs=100amp=True开启自动混合精度,显存占用能降 30% 到 40%。如果还爆,把batch降到 4,同时把accumulate设成 4,等效 batch size 还是 16。注意accumulate参数在部分 YOLO 版本里叫nbs,具体看版本。
4.4 现象:river 和 road 两类 AP 始终垫底
这两类地物在遥感图里都是细长条,标注边界模糊,模型很难学。除了前面说的 NWD 改进,还有一个实用技巧:在数据增强里加随机裁剪,把大图裁成小图,让细长目标在裁剪后的图里占比更大。另外检查一下标注质量,如果 river 的标注把河岸也框进去了,模型学到的就是错误边界。
# 随机裁剪增强示例 from PIL import Image import random def random_crop(img, boxes, min_size=256): w, h = img.size cw = random.randint(min_size, w) ch = random.randint(min_size, h) x0 = random.randint(0, w - cw) y0 = random.randint(0, h - ch) img_crop = img.crop((x0, y0, x0 + cw, y0 + ch)) # boxes 需要同步裁剪并过滤越界框 new_boxes = [] for b in boxes: x1, y1, x2, y2 = b x1, y1 = max(x1 - x0, 0), max(y1 - y0, 0) x2, y2 = min(x2 - x0, cw), min(y2 - y0, ch) if x2 - x1 > 4 and y2 - y1 > 4: new_boxes.append([x1, y1, x2, y2]) return img_crop, new_boxes裁剪后过滤掉宽高小于 4 像素的框,避免产生无效标注。这个增强对 river 和 road 的 AP 提升在 3 到 5 个点之间,代价是训练时间变长。
5. 导出与部署:ONNX、TensorRT 与边缘端推理的取舍
5.1 导出 ONNX 的三个必调参数
训练完的权重导出 ONNX 是部署的第一步。YOLO 的导出命令看起来简单,但遥感模型有几个参数必须显式指定,否则导出的模型在推理时坐标会偏。
yolo export model=best.pt format=onnx imgsz=1024 opset=12 simplify=True dynamic=Falseimgsz必须和训练时一致,遥感模型对分辨率敏感,训练用 1024 导出用 640 会导致精度断崖。opset=12是兼容性最好的版本,TensorRT 8 和 ONNX Runtime 都支持。simplify=True会调用 onnx-simplifier 做图优化,去掉冗余算子。dynamic=False固定输入尺寸,边缘端部署时固定尺寸比动态尺寸快 20% 以上。
5.2 TensorRT 加速的收益与代价
热词里有人问 T4 上 1080p 25 帧每秒用 TensorRT 跑 YOLO 640 分辨率能支持多少路。这个问题没有标准答案,取决于模型大小和 batch 策略。以 YOLOv8n 为例,T4 上 FP16 精度、640 分辨率、batch=1 时单帧推理约 3 到 5 毫秒,理论上能跑 200 路以上,但实际受限于视频解码和内存拷贝,能稳定跑 30 到 50 路就不错了。遥感图像分辨率更高,1024 输入下单帧约 10 到 15 毫秒,路数要相应减半。
# TensorRT 导出 yolo export model=best.pt format=engine imgsz=1024 half=True device=0half=True开启 FP16,T4 和 30 系以上显卡都支持,精度损失通常在 0.5 个 mAP 以内。device=0指定 GPU。导出的 engine 文件绑定特定显卡架构,换卡需要重新导出,这是 TensorRT 最烦人的地方。
5.3 边缘端部署的输入预处理对齐
边缘端部署最容易翻车的不是模型本身,而是预处理。训练时 YOLO 做的是 letterbox 填充,推理时如果直接用 resize 拉伸,长宽比变了,检测框会系统性偏移。遥感图像长宽比通常接近 1:1,这个问题不明显,但如果你的测试图是 1920×1080,就必须在推理代码里复现 letterbox。
import cv2 import numpy as np def letterbox(img, new_shape=1024, color=(114, 114, 114)): h, w = img.shape[:2] r = min(new_shape / h, new_shape / w) new_unpad = (int(round(w * r)), int(round(h * r))) dw = new_shape - new_unpad[0] dh = new_shape - new_unpad[1] dw /= 2 dh /= 2 img_resized = cv2.resize(img, new_unpad, interpolation=cv2.INTER_LINEAR) top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1)) left, right = int(round(dw - 0.1)), int(round(dw + 0.1)) img_padded = cv2.copyMakeBorder(img_resized, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color) return img_padded, r, (left, top) # 推理后把框映射回原图 def scale_boxes(boxes, r, pad, orig_shape): boxes[:, [0, 2]] -= pad[0] boxes[:, [1, 3]] -= pad[1] boxes /= r boxes[:, [0, 2]] = boxes[:, [0, 2]].clip(0, orig_shape[1]) boxes[:, [1, 3]] = boxes[:, [1, 3]].clip(0, orig_shape[0]) return boxesletterbox函数返回填充后的图、缩放比例和填充偏移,推理完用scale_boxes把检测框映射回原图坐标。这两个函数必须和训练时的预处理完全一致,差一个像素都可能导致框偏移。遥感图像里河流这种细目标,偏移几个像素就完全框不住了。
6. 用 1366 张图把 YOLO 遥感检测跑扎实的一个笨办法
这份数据集最大的价值不是刷多高的 mAP,而是让你把遥感检测的完整链路走通,并且暴露出自然图像里遇不到的问题。我自己的习惯是:拿到任何遥感数据集,先花半天做数据侦察,把标签格式、类别分布、图像尺寸、重复图比例这四个指标摸清楚,再动手写训练脚本。这半天投入能省掉后面至少两天的排查时间。
具体到这份九类地物数据,我建议的验证方法是:先训一个 YOLOv8n 基线,imgsz=640,epochs=50,看九类的 AP 分布。如果 forest、pasture、barren 这三类 AP 超过 0.6,说明数据质量过关;如果 river 和 road 低于 0.3,再上 NWD 改进和随机裁剪增强做对比。对比实验不要一次改多个变量,先只换损失函数,跑完看 river 和 road 的 AP 变化,再叠加裁剪增强。每次实验记录imgsz、batch、lr、损失类型、增强配置这五个参数,存成 CSV,不然跑上十组实验后你自己都记不清哪组对应哪个配置。
还有一个容易被忽略的点:推理时的置信度阈值。遥感地物的类间重叠比自然图像严重,比如果园和森林在低分辨率下纹理接近,默认conf=0.25会产生大量误检。我一般会把conf提到 0.4 到 0.5,iou保持 0.45 到 0.5,然后在验证集上画 PR 曲线找最佳截断点。这个调参过程比换模型结构带来的收益更直接。
最后说一个我踩过的坑:遥感数据集的标签里,贫瘠土地和工业区有时候标注边界是重合的,因为废弃工业用地既像工业又像贫瘠土地。这种类间模糊不是模型能解决的,需要在数据层面做一次清洗,把边界模糊的样本要么剔除,要么重新标注。我现在的习惯是训练前先跑一遍类别共现分析,看看哪些类经常出现在同一张图里且 bbox 重叠度高,这些类就是潜在的混淆源。希望帮到你。
本文还有配套的精品资源,点击获取