简介:面向yolo系列算法目标检测任务的航空卫星图像数据集,涵盖森林、公路、作物、河流、住宅、工业、果园、牧场及贫瘠土地等多类地物目标,共1366张带标签图像。资源已按训练、验证、测试划分完成,并附带data.yaml配置文件,可直接用于yolov5、yolov7、yolov8、yolov9、yolov10、yolo11等主流版本训练与验证。包内同时提供YOLO格式与VOC格式两种标注文件:前者的txt文件逐行记录类别索引与归一化后的中心点坐标、宽高信息,后者的xml文件按Pascal VOC结构保存目标位置,便于不同工具链切换使用。全部文件共2000个,其中xml标注1230个、txt标注770个,压缩包大小约60.51MB,结构紧凑,下载后即可展开训练。适合目标检测初学者快速上手,也方便遥感领域研究者在多类别地物识别中直接复用标注数据;目前已有42人学习下载,可用于地物分类、农业监测、区域规划等应用研究。
1. 1366张带标签航空卫星图,YOLO训练的第一步不是写训练代码
用YOLO做航空卫星图像的目标检测,很多人第一反应是拿到数据后立刻解压、改个yaml、敲训练命令。但遥感图与自然图像差异很大:1366张带标签的航空卫星数据,类别覆盖森林、公路、作物、河流、住宅、工业、果园、牧场和贫瘠土地,这9类地物在图像上的形态完全不同,不是简单套用COCO预训练模型就能直接收敛的。常见的问题包括mAP虚高但实际漏检严重、后几类样本太少导致模型直接学不到、长条形地物如公路和河流被框切成多段。所以这篇直接把数据准备、格式转换、训练参数、评估排查和遥感特有的调优手段按一条线走通,适合已经把YOLO基本流程跑通、但不熟悉遥感数据的人。
2. 从zip到YOLO训练集:目录结构、标注格式与类别检查
拿到zip包,先别急着解压训练。先扫目录,确认标注格式,再做可视化核对。遥感数据集的标注规范差别巨大,有些是从公开遥感数据集转化的VOC格式,有些是Labelme的JSON输出,还有些是别人处理好的YOLO txt。三种格式的后续处理路径完全不同,第一步是花十分钟看清楚,而不是直接假设它是标准YOLO格式。
2.1 解压后先扫清目录结构与标注类型
我一般在Linux环境下先用find命令把目录结构拉出来,同时统计文件后缀:
mkdir -p ~/aerial_dataset cd ~/aerial_dataset unzip ~/downloads/yolo算法-航空卫星图像数据集-1366张图像带标签-森林-公路-作物-河流-住宅的-工业-果园-牧场-贫瘠的土地.zip find . -maxdepth 2 -type d | sort find . -maxdepth 2 -type f | sed 's/.*\.//' | sort | uniq -c第一行是解压,第二行看目录层级,第三行统计文件后缀。-maxdepth 2把扫描范围控制在两层以内,避免嵌套太深时输出刷屏;uniq -c直接给出jpg、txt、xml、json各自的数量,一眼就能判断标注格式。如果发现还有嵌套的压缩包,先一并解压出来再继续。
接着抽一个标注文件看内容。YOLO格式的txt每行是class_id x_center y_center width height,全部是相对图像的归一化坐标;VOC格式的xml则包含<object><name>和<bndbox>节点;Labelme的json里有shapes和imageWidth/imageHeight。三种格式决定了后续要不要写转换脚本。
find . -name "*.txt" | head -3 cat "$(find . -name '*.txt' | head -1)"如果txt文件是空的,说明要么是背景图,要么是标注遗漏。对于1366张图像的中小规模数据集,空标签图是否保留要看你的任务:想检测所有地物就删掉,想做包含背景的分类就保留。我倾向于保留并单独建一个backgrounds目录,后面训练时可以作为负样本加入。
2.2 抽样可视化标签,逐类核对标注质量
格式确认后不要直接统计数量,而是先做可视化。标注文件与图像是否对齐、框是否框在目标上、有没有整张图只标了一个点的情况,这些问题只有人眼看过才能发现。我一般每类抽5张图,把YOLO标签画出来检查:
import cv2 img_path = "images/0.jpg" label_path = "labels/0.txt" class_names = ["forest", "highway", "crop", "river", "residential", "industrial", "orchard", "pasture", "barren_land"] img = cv2.imread(img_path) h, w = img.shape[:2] with open(label_path) as fp: for line in fp: parts = line.strip().split() if len(parts) < 5: continue cid, xc, yc, bw, bh = map(float, parts) x1 = int((xc - bw / 2) * w) y1 = int((yc - bh / 2) * h) x2 = int((xc + bw / 2) * w) y2 = int((yc + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, class_names[int(cid)], (x1, max(0, y1 - 6)), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) cv2.imwrite("check_0.jpg", img)这段代码把归一化坐标乘回原图宽高,画出矩形框和类别名。注意YOLO里xc, yc是中心点坐标,绘制时要换算成左上角与右下角;putText里的max(0, y1 - 6)防止类别名画到图像外。如果发现某些图标注框偏移半个身位,说明标注质量不稳定,后续训练时要看着办,要么手动修正,要么在loss权重上做处理。
可视化要重点看三件事:长条形目标是不是被一个巨大框包住、密集小目标是不是只标了部分、类别名称有没有和实际地物对不上。这些在mAP里很难暴露,但在图上非常直观。
2.3 统计类别分布,先看类别ID映射是否连续
接下来统计每个类别出现的实例数。遥感地物的共性是类别极不平衡:森林、作物这类大块连片地物通常标注数量多,河流可能只有几十条,牧场和贫瘠土地面积大但实例少。这个统计直接决定后面训练时的采样策略和增强策略。
import os from collections import Counter label_dir = "labels" counter = Counter() img_covered = Counter() for f in os.listdir(label_dir): if not f.endswith(".txt"): continue cls_in_img = set() with open(os.path.join(label_dir, f)) as fp: for line in fp: parts = line.strip().split() if len(parts) < 5: continue cid = int(parts[0]) counter[cid] += 1 cls_in_img.add(cid) for c in cls_in_img: img_covered[c] += 1 for cid in sorted(counter.keys()): print(f"class {cid}: instances={counter[cid]}, images={img_covered[cid]}")counter统计实例总数,img_covered统计每个类别至少出现一次的图像数。这两个数字分开看很重要:某个类实例数多但只集中在少数几张图里,和分散在几百张图里,训练行为完全不同。
结合标题中的9类地物,类别映射关系大致如下:
| class_id | 类别 | 常见形态 | 对YOLO的挑战 |
|---|---|---|---|
| 0 | 森林 | 大块连片 | 框内包含大量非目标区域 |
| 1 | 公路 | 长条弯曲 | 宽高比极端,单框横跨多段 |
| 2 | 作物 | 条块纹理 | 与果园、牧场边界模糊 |
| 3 | 河流 | 细长蜿蜒 | 一个框无法覆盖连续体 |
| 4 | 住宅 | 密集小方块 | 小目标聚集,漏检率高 |
| 5 | 工业 | 大尺度规则建筑 | 与住宅容易混淆 |
| 6 | 果园 | 均匀纹理区 | 与森林、作物边界交叠 |
| 7 | 牧场 | 大片草地 | 与贫瘠土地难以区分 |
| 8 | 贫瘠的土地 | 低纹理裸土 | 语义界线本身不清晰 |
这张表后面会反复用到。所有类别ID必须从0开始连续编号,如果原始数据里类别跳号或顺序不一致,要先做一个重映射字典再写训练配置。
2.4 VOC/JSON转YOLO txt的坐标换算与过滤规则
如果标注是VOC格式,转换成YOLO txt是绕不开的一步。VOC里的坐标是像素绝对值,YOLO需要归一化,换算公式是:
x_center = ((xmin + xmax) / 2) / img_width y_center = ((ymin + ymax) / 2) / img_height width = (xmax - xmin) / img_width height = (ymax - ymin) / img_height对应脚本:
import xml.etree.ElementTree as ET class_mapping = { "forest": 0, "highway": 1, "crop": 2, "river": 3, "residential": 4, "industrial": 5, "orchard": 6, "pasture": 7, "barren_land": 8 } def voc_to_yolo(xml_path, img_w, img_h): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.findall("object"): name = obj.find("name").text cid = class_mapping.get(name) if cid is None: continue box = obj.find("bndbox") xmin = float(box.find("xmin").text) ymin = float(box.find("ymin").text) xmax = float(box.find("xmax").text) ymax = float(box.find("ymax").text) if xmax <= xmin or ymax <= ymin: continue xc = (xmin + xmax) / 2 / img_w yc = (ymin + ymax) / 2 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{cid} {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}") return lines脚本里先过滤掉宽高非正的框,防止某些标注工具导出时出现坐标颠倒。转换后建议再跑一遍可视化,确认ID没有因为类别名拼写不同而掉进cid is None分支。为什么非要在转换后确认一遍,因为遥感标注里的类别名经常带有空格、下划线或中文,比如barren land和barren_land会被解析成两个键,导致这一类别实例数直接归零。
3. 基于这份1366张遥感数据训练YOLO:配置文件、数据划分与超参
数据准备好了,才轮到训练。这里要强调一个前提:1366张图、9类地物,这个规模在遥感任务里属于中小型数据集,选模型、设超参的思路和用几万张自然图像训练完全不一样。模型选大了会过拟合,数据增强开猛了会破坏长条形目标,patience设太短又会错过晚来的收敛点。
3.1 模型选型:YOLOv8s起步,YOLO11留给对比实验
对这种规模的数据集,起步模型建议用YOLOv8s。为什么不选n?n的参数量小、训练快,但遥感小目标的语义特征提取能力偏弱;m或l在这个数据量下容易把训练时间拉长且收益不稳定。YOLO11比v8在主干和训练策略上有改动,但1366张图不足以把这部分优势发挥出来,可以先跑通v8s的baseline,再拿v8m做对比就够了。
3.2 写data.yaml并做按类别分布的数据划分
ultralytics的训练入口是yolo detect train,数据侧只需要一个yaml文件:
path: /data/aerial train: images/train val: images/val names: 0: forest 1: highway 2: crop 3: river 4: residential 5: industrial 6: orchard 7: pasture 8: barren_landpath是数据集根目录,train和val是相对路径,names必须与2.3节的class_id一一对应。注意类别名里不要带空格,barren land写成barren_land,否则后面评估指标打印时排版会乱。
数据划分不能简单random shuffle。遥感数据经常出现同一区域的多张图,随机切分会让验证集和训练集包含同一片地物的不同视角,导致mAP虚高。更稳妥的做法是先按文件名前缀或图像编号分组,再把组切分。以纯随机划分为例:
import random, shutil from pathlib import Path root = Path("/data/aerial") all_imgs = sorted((root / "images/all").glob("*.jpg")) random.seed(42) random.shuffle(all_imgs) split_idx = int(len(all_imgs) * 0.8) for split, imgs in [("train", all_imgs[:split_idx]), ("val", all_imgs[split_idx:])]: out_img_dir = root / "images" / split out_lbl_dir = root / "labels" / split out_img_dir.mkdir(parents=True, exist_ok=True) out_lbl_dir.mkdir(parents=True, exist_ok=True) for img in imgs: label = root / "labels/all" / (img.stem + ".txt") if not label.exists(): continue shutil.copy(img, out_img_dir / img.name) shutil.copy(label, out_lbl_dir / label.name)random.seed(42)固定随机种子,保证每次划分结果一致。if not label.exists()跳过没有对应标签的图像,避免空标签进入训练。更细化的做法是按2.3节统计出的类别分布做分层抽样,保证河流、牧场、贫瘠土地这些少样本类别在验证集里也有出现,否则验证集的mAP会非常不稳定。
3.3 训练命令与核心超参
baseline训练命令:
cd /data/aerial yolo detect train \ data=aerial.yaml \ model=yolov8s.pt \ epochs=100 \ imgsz=640 \ batch=8 \ patience=20 \ mosaic=0.8 \ fliplr=0.5 \ scale=0.3model=yolov8s.pt是COCO预训练权重,遥感图和自然图像差异大,但底层的边缘、纹理特征可以迁移,1366张图的情况下从零训练收敛慢且容易陷入局部最优。patience=20表示验证集mAP连续20个epoch不增长就早停,遥感数据集小,过拟合来得快,没必要死等100个epoch。mosaic=0.8是YOLOv8默认的mosaic增强概率,对遥感数据要适当下调。mosaic把4张图拼成一张,大面积的森林、作物会被切得支离破碎,长条形公路和河流在拼接边界处语义断裂,模型容易学到错误的上下文。scale=0.3控制随机缩放的幅度,遥感地物尺寸相对稳定,不需要像自然图像那样做大幅缩放。
各项建议参数如下:
| 参数 | 默认值 | 建议值 | 原因 |
|---|---|---|---|
| imgsz | 640 | 640起步,显存充裕可试960 | 小目标依赖分辨率,但显存占用随分辨率平方上升 |
| batch | 16 | 8~16 | 受显存限制,batch太大会让BN统计不稳定 |
| epochs | 100 | 100 | 数据量小,再长就过拟合 |
| patience | 50 | 20 | 遥感数据收敛窗口短,早停更实际 |
| mosaic | 1.0 | 0.8 | 降低大面积地物被切碎的概率 |
| fliplr | 0.5 | 0.5 | 遥感图左右镜像语义保持不变 |
| hsv_h | 0.015 | 0.005 | 地物颜色有领域一致性,过大的色彩扰动会让模型学错 |
3.4 类别不平衡的应对:复制少样本图而不是调loss权重
直接调loss权重在YOLO里不容易生效,因为YOLO训练是按图像采样的,不是按实例采样。更直观的做法是让少样本类别对应的图像在训练集里多出现几次。操作上把包含河流、贫瘠土地的图像路径复制到训练列表中,配合mosaic增强,相当于提高了这些图像的采样概率。复制后图的总量增加了但模型输入尺寸不变,训练代价增加有限。
代价是要小心过拟合,复制不是无限复制。我一般会把最少的类别复制到接近中位类别图像数的1.5倍就停,同时注意验证集不要因为复制而被污染。另一个辅助手段是把少样本类别的实例区域裁剪出来,通过Copy-Paste增强粘贴到不含该类别的图像上,这个在遥感地物上效果比通用目标明显,因为地物纹理与原图背景融合度天然较高。
4. 训练完先别信mAP:混淆矩阵、按类指标与遥感漏检排查
训练结束后,很多人直接看终端打印的总mAP就收工。遥感数据上这远远不够。9类地物形态差异大,总mAP会被森林、作物这些好分的类别拉高,河流、贫瘠土地的低分被掩盖。要按类检查,结合混淆矩阵判断到底哪些地物在互相打架。
4.1 从results.png与训练日志确认收敛状态
训练完成后进runs/detect/train目录,先看results.png。这张图包含box_loss、cls_loss、dfl_loss,以及验证集mAP50和mAP50-95。注意区分两类现象:如果cls_loss一直在降但val mAP不涨,通常是验证集和训练集分布不一致,或者验证集样本太少导致指标抖动;如果box_loss降到0.8左右就下不去,常见原因是标注框本身有偏差,模型在学习一个不稳定的回归目标。
遥感场景里值得额外留意的现象是mAP50高但mAP50-95低。这说明模型能把目标大致框出来,但框的边界不够准。小目标在640分辨率下可能只占十几个像素,IOU稍微偏移一点mAP50-95就掉得厉害。
4.2 跑validation并直接读混淆矩阵
用下面命令验证:
yolo detect val \ model=runs/detect/train/weights/best.pt \ data=aerial.yaml跑完后runs/detect/val目录下会生成confusion_matrix.png。这张图比mAP更直接。森林和果园往往整片混淆,牧场和贫瘠土地互相误判,河流经常被漏检到背景里。如果看到某两类横向纵向都有明显响应,先别急着调模型,回去看这两类的标注是否真的可分。牧场和贫瘠土地在视觉上确实没有明确边界,标的人可能自己都拿不准,这种情况模型学不好不是模型的错。
4.3 用低置信度预测区分漏检和误检
验证集上漏检怎么排查?把置信度阈值调低重新预测,看模型到底给不给框。如果给了框但置信度低,说明模型学到了特征,只是打分不够高;如果低置信度下依然完全没有框,才是真的漏学。
from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") results = model.predict( source="images/val", conf=0.05, imgsz=640, save=True, project="debug_vis" )conf=0.05把阈值压得很低,目的是把模型的所有预测都释放出来。逐个看保存下来的预测图,重点看三类现象:低置信度框是否刚好压在目标上,判断是置信度校准问题还是特征学习不足;同一目标是否出现多个互相重叠的半截框,说明NMS参数或目标形态有问题;长条形河流是否被切成几段框,这是单框检测长条目标的固有缺陷。
4.4 按类看AP,找到真正拖后腿的类别
验证集命令行输出的末尾会打印每一类的P、R和mAP:
Class Images Instances Box(P R mAP50 all 273 ... ... ... ... forest 273 ... ... ... ... highway 273 ... ... ... ...重点看R(召回率)低的类别。召回低意味着有大量目标没被检测出来,在遥感地物里通常对应三类原因:目标太小、训练样本太少、目标形态长宽比极端。河流和公路召回落典型,原因是单个框很难覆盖整条连续地物,训练时框的宽高比分布和自然图像差异太大。此时如果只是想提升这类指标,可以在训练参数里开启rect=True,按宽高比分组batch,减少padding带来的无效计算:
yolo detect train \ data=aerial.yaml \ model=yolov8s.pt \ epochs=30 \ rect=Truerect=True会按图像的宽高比排序并分桶,同一batch内的图pad到相近尺寸,长条形遥感图的特征保留更好。注意它要求cache=True或允许预处理阶段重排数据,部分旧版本下与mosaic增强不兼容。
5. 遥感场景的YOLO调优手段:瓦片切分、多尺度推理与ONNX导出
遥感图像本身通常很大,1366张图如果原图是几千像素见方,全局resize到640会让小目标缩小到几乎不可见。解决这个问题最有效的手段是瓦片切分。
5.1 瓦片切分与标签同步偏移
把原始大图切成1024×1024的tile,切分时保留重叠区域,避免目标恰好落在切缝上。切图的同时必须把标签坐标同步偏移,否则标注位置全部错位。以下函数处理标注偏移与过滤:
def filter_tile_boxes(boxes, x0, y0, tile_w, tile_h, min_cover=0.6): keep = [] for x1, y1, x2, y2 in boxes: inter_w = max(0, min(x2, x0 + tile_w) - max(x1, x0)) inter_h = max(0, min(y2, y0 + tile_h) - max(y1, y0)) inter_area = inter_w * inter_h box_area = (x2 - x1) * (y2 - y1) if box_area <= 0: continue if inter_area / box_area >= min_cover: nx1 = max(x1, x0) - x0 ny1 = max(y1, y0) - y0 nx2 = min(x2, x0 + tile_w) - x0 ny2 = min(y2, y0 + tile_h) - y0 keep.append((nx1, ny1, nx2, ny2)) return keepmin_cover=0.6表示只有超过60%面积落在tile内的目标才保留,避免大量半截框污染训练。推理时每个tile独立预测,最后把所有框的坐标加上对应tile的偏移量,再做一次全局NMS合并重叠区域。重叠区建议设10%~20%,太少会让切缝两边的检测不稳定。
5.2 多尺度与TTA
训练完模型后,用原始图像直接推理往往不是最佳方案。ultralytics的predict支持多尺度测试时增强:
yolo detect predict \ model=runs/detect/train/weights/best.pt \ source=test_images \ imgsz=960 \ tta=True \ conf=0.15tta=True会对输入做翻转和尺度变换后融合预测框,对小目标召回有明显收益,但推理时间会增加到原来的3倍左右。放在batch推理服务前要评估延迟预算。单独设imgsz=960而不开TTA是一个性价比更高的中间档位。
5.3 导出ONNX并固定输入尺寸
模型验证没问题后一般要导出部署。先用ONNX导出:
yolo export \ model=runs/detect/train/weights/best.pt \ format=onnx \ dynamic=True \ opset=12如果部署端是固定尺寸输入,dynamic=True建议关闭,固定比如640或960,动态尺寸在TensorRT和OpenVINO上的兼容性开销不值得。导出后用onnxruntime验证一遍:
import onnxruntime as ort import numpy as np session = ort.InferenceSession("best.onnx", providers=["CPUExecutionProvider"]) input_name = session.get_inputs()[0].name img = np.random.randn(1, 3, 640, 640).astype(np.float32) outputs = session.run(None, {input_name: img}) for o in outputs: print(o.shape)如果输出shape与预期一致,再把瓦片切分的tile推理、坐标偏移、NMS合并这三段串起来,跑一遍完整测试图,确认切到1024或960尺寸下漏检情况最轻。对这份1366张带标签的遥感数据,把瓦片大小设成tile_size=1024, overlap=128,配合imgsz=960的TTA推理,一般能同时兼顾小目标召回率和漏检边界切碎的问题。
本文还有配套的精品资源,点击获取