资讯动态

飞机卫星图目标检测:VOC转YOLO与YOLOv8训练全流程解析

发布时间:2026/10/7 6:38:34 来源:尧图企业网站定制
简介一套面向人工智能目标检测任务的卫星图像飞机识别数据集专为在遥感图中定位飞机而设计适合初学者及研究者用于单类别目标检测训练与算法验证。压缩包内含约两千个文件其中一千张高分辨率彩色JPG图像与一千个配套XML标注文件各占一半另附说明文本整体大小约二百九十二兆。标注文件提供飞机边界框真实坐标是监督学习的关键卫星背景常带阴影、遮挡可有效提升模型泛化难度与实用性。数据集可直接配合SSD、YOLO、Faster R-CNN等主流检测框架使用训练时还可结合训练集、验证集与测试集划分及数据增强策略。资源已有六百三十四人学习下载适合需要积累遥感目标检测实战样本、优化飞机识别精度的AI开发者和竞赛选手。1. 飞机卫星图目标检测数据集这份1000张图到底能帮你练什么做目标检测最怕的不是模型选错而是手里没一份能反映真实难度的数据。普通街景图到处都有但遥感场景——飞机停在停机坪、跑道边白色蒙皮和水泥地面混在一起阴影把机翼切成几段——很多人就卡住了。这份人工智能目标检测数据集飞机卫星图3就是专门解决卡点的1000张1024x1024高分辨率卫星彩图只有一类目标“飞机”每张图带一个XML标注文件里面是飞机边界框的坐标。对于想入门遥感目标检测或者打算用YOLOv8训练自己的数据集的人来说它比从LabelImg开始标注友好得多。更重要的是它保留了真实遥感图的“难”阴影、反射、部分遮挡正好用来检验模型是真正学到了飞机的几何结构还是只会认绿色背景。2. 先拆数据集XML结构、目录形态和单类别标注的完整链路2.1 目录结构与annotations文件名批次、图像尺寸和标注关系下载解压后第一件事不是训练而是把数据集的“脾气”摸清。这份资源的核心构成是三部分jpg图片、annotations文件夹、info.txt。图片文件名形如airplane-003-0595.jpg前三位是批次或场景号后面是图像序列号比如打开目录你能看到这样一组文件airplane-003-0595.jpg airplane-003-0588.jpg airplane-003-0584.jpg airplane-003-0596.jpg airplane-003-0694.jpg airplane-003-0685.jpg airplane-003-0686.jpg airplane-003-0701.jpg airplane-003-0704.jpg airplane-003-0585.jpg这些名字里“003”大概率代表采集批次同一批次的图可能在同一个机场、相近的时刻拍出来。info.txt一般会写明来源、分辨率、类别数量annotations里是与每张图片同名的XML文件。图片尺寸固定1024x1024对遥感目标检测来说这是个很合适的输入尺寸单架飞机在图中能保留几十到一两百像素恰好踩在“中小目标”的检测门槛上。XML采用的是VOC格式根节点是annotation里面有size和多个object。每个object包含类别名name、是否截断truncated、是否难例difficult以及bndbox坐标。这个数据集只有一类所以大部分标注的name都是airplane但脚本逻辑最好按多类别写方便以后扩展到车辆、船只等目标。工程上我建议先不做任何训练而是把整个数据集的“体检表”打出来每张图标注了几个框、框的宽度和高度分布、宽高比分布。这一步能帮你判断该用Anchor-based模型还是Anchor-free模型。YOLO系列对通用目标友好但遇到大量小目标需要额外调整这在后面章节会展开。2.2 用Python解析XML格式校验、边界框统计与异常检测先用标准库xml.etree.ElementTree扫描所有XML做一次格式校验和基础统计。脚本不需要第三方依赖跑起来很快。import xml.etree.ElementTree as ET from pathlib import Path ann_dir Path(annotations) image_count 0 total_objects 0 class_set set() bbox_sizes [] difficult_count 0 for xml_file in sorted(ann_dir.glob(*.xml)): tree ET.parse(xml_file) root tree.getroot() image_count 1 size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) for obj in root.findall(object): name obj.find(name).text class_set.add(name) total_objects 1 difficult obj.find(difficult) if difficult is not None and int(difficult.text) 1: difficult_count 1 bndbox obj.find(bndbox) if bndbox is None: print(f[warn] {xml_file.name} 的 object 没有 bndbox) continue xmin int(bndbox.find(xmin).text) ymin int(bndbox.find(ymin).text) xmax int(bndbox.find(xmax).text) ymax int(bndbox.find(ymax).text) box_w xmax - xmin box_h ymax - ymin bbox_sizes.append((box_w, box_h, img_w, img_h)) print(fimage count: {image_count}) print(ftotal objects: {total_objects}) print(fclasses: {class_set}) print(fdifficult objects: {difficult_count}) widths [b[0] for b in bbox_sizes] heights [b[1] for b in bbox_sizes] print(fbbox width p50/p90: {sorted(widths)[len(widths)//2]}/{sorted(widths)[int(len(widths)*0.9)]}) print(fbbox height p50/p90: {sorted(heights)[len(heights)//2]}/{sorted(heights)[int(len(heights)*0.9)]})这段代码做了几件事统计XML文件数量遍历每个object收集类别名读取bndbox坐标并统计框宽高分布。difficult字段在VOC评估中有特殊含义如果它超过5%转换标签时就要考虑是否过滤。脚本里还加了一个警告如果某个object没有bndbox打印出来。因为有些标注工具会导出polygon代替bndbox这种文件需要单独处理否则转换后会变成空标签。跑完统计后你会得到类似“image count: 1000, total objects: 1500, classes: {airplane}, difficult objects: 0”的输出。如果平均框宽在80到160像素之间说明飞机相对整图属于中小目标。YOLO默认的640训练尺寸会把它们缩得更小后面训练时imgsz必须保持1024甚至1280。另外要注意坐标越界。很多标注工具会把框画到图片边缘外解析时做一次max/min裁剪否则归一化后框宽高会变成负值训练时直接报错。这个问题在真实数据集中很常见我在后面避坑章节会专门提到。3. 把VOC格式转成YOLO格式转换脚本与训练集划分3.1 为什么必须转格式归一化坐标和目录结构差异YOLO训练需要的标签不是XML而是与图片同名的txt文件。每行格式是class x_center y_center width height例如0 0.512345 0.678901 0.123456 0.234567。其中x_center是框中心点的x坐标除以图像宽度得到0到1之间的浮点数width是框宽除以图像宽。这种归一化坐标的好处是模型不依赖输入图片的绝对尺寸任意分辨率都能计算损失。而VOC格式给的是xmin, ymin, xmax, ymax绝对像素坐标必须转换。YOLOv8的数据组织方式和YOLOv5保持一致期望的目录结构是data/airplane/ ├── train/ │ ├── images/ # 训练图片 │ └── labels/ # 训练标签(txt) └── val/ ├── images/ # 验证图片 └── labels/ # 验证标签(txt)图片集和标签集保持平行文件名必须一一对应。airplane-003-0595.jpg的标签就是airplane-003-0595.txt。YOLO训练时会自动在images旁边找labels目录如果找不到对应txt会跳过这张图并在日志里提示但不会中断训练。我见过有人划分后只拷了图片结果训练集看起来有1000张实际只有600张参与训练因为另外400张没有标签。所以划分后一定要检查两边文件数量。3.2 转换脚本读取XML、归一化、写入同名txt下面这个脚本把annotations目录下所有XML转换成YOLO格式的txt放到labels目录。代码里加了坐标裁剪和越界保护适应真实标注的脏数据。import xml.etree.ElementTree as ET from pathlib import Path def convert_voc_to_yolo(xml_path: Path, out_dir: Path, class_map: dict) - bool: 将单个VOC XML转换为YOLO txt返回是否写入成功 tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_map: print(f[skip] {xml_path.name} 包含未映射类别: {name}) continue bndbox obj.find(bndbox) if bndbox is None: print(f[warn] {xml_path.name} 缺少 bndbox跳过该目标) continue xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) # 防御性裁剪防止坐标超出图像边界 xmin max(0.0, min(xmin, img_w - 1)) xmax max(xmin 1.0, min(xmax, img_w)) ymin max(0.0, min(ymin, img_h - 1)) ymax max(ymin 1.0, min(ymax, img_h)) x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h class_id class_map[name] lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) if not lines: return False out_dir.mkdir(parentsTrue, exist_okTrue) txt_path out_dir / (xml_path.stem .txt) txt_path.write_text(\n.join(lines), encodingutf-8) return True # 使用示例 ann_dir Path(annotations) out_dir Path(labels) class_map {airplane: 0} # 只有一类编号从0开始 converted 0 for xml_file in sorted(ann_dir.glob(*.xml)): if convert_voc_to_yolo(xml_file, out_dir, class_map): converted 1 print(fconverted: {converted}, total xml: {len(list(ann_dir.glob(*.xml)))})脚本里的class_map是关键它决定类别到编号的映射。这个数据集只有airplane所以映射只有一项。如果以后补充了车辆、船只只需要扩展class_map并重跑。坐标归一化时注意x_center必须是(xmin xmax) / 2再除以img_w不能先除以2再相加。输出保留6位小数精度足够文件体积也不会太大。xmax max(xmin 1.0, min(xmax, img_w))这一行是防止出现xmax xmin的非法框。我在真实数据里见过xmin比xmax还大的情况多半是标注工具坐标写反了。如果不加这行归一化后宽度是负数YOLO训练会报“invalid box”错误。3.3 按比例划分训练集和验证集固定种子与同步拷贝转换完成后需要把图片和txt标签划分到训练集和验证集。常见比例是9:1数据量少时可以给训练集多一些。为了可复现固定random.seed(42)。下面脚本按文件列表随机打乱再把图片和对应标签同步拷贝到新目录。import random import shutil from pathlib import Path image_dir Path(images) label_dir Path(labels) train_img_dir Path(data/airplane/train/images) train_lab_dir Path(data/airplane/train/labels) val_img_dir Path(data/airplane/val/images) val_lab_dir Path(data/airplane/val/labels) for d in [train_img_dir, train_lab_dir, val_img_dir, val_lab_dir]: d.mkdir(parentsTrue, exist_okTrue) images sorted(image_dir.glob(*.jpg)) random.seed(42) random.shuffle(images) total len(images) train_count int(total * 0.9) train_images images[:train_count] val_images images[train_count:] def copy_pair(img_path: Path, img_dst_dir: Path, lab_dst_dir: Path): shutil.copy2(img_path, img_dst_dir / img_path.name) label_file label_dir / (img_path.stem .txt) if label_file.exists(): shutil.copy2(label_file, lab_dst_dir / label_file.name) else: print(f[warn] {img_path.name} 没有对应txt标签) for img in train_images: copy_pair(img, train_img_dir, train_lab_dir) for img in val_images: copy_pair(img, val_img_dir, val_lab_dir) print(ftrain: {len(train_images)}, val: {len(val_images)}) print(ftrain labels: {len(list(train_lab_dir.glob(*.txt)))}, val labels: {len(list(val_lab_dir.glob(*.txt)))})这个脚本里有几个隐藏细节。图片和标签必须在同一轮循环里一起拷不能先拷图片再拷标签否则中间出错会导致两边数量不一致。最后打印train labels数量时如果和图片数不一致说明有缺失标签此时要去labels目录检查。我一般还会统计一下训练集和验证集里airplane框的总数确保比例接近9:1而不是因为某几张图框特别多导致验证集框比例失衡。随机划分对这份数据集有一个隐患文件名中的003批次可能代表同一机位、同一光照条件。如果同一批次的图一部分进训练集、一部分进验证集验证mAP会虚高因为模型在训练时见过类似的背景和飞机姿态。更严谨的做法是分组划分先按文件名前缀分组再把整个组分到训练或验证。这个技巧在第5章会展开初学者先用随机划分跑通流程即可。4. YOLOv8训练实操配置data.yaml、训练参数与结果验证4.1 编写data.yaml路径、类别与单类别注意事项YOLOv8用data.yaml描述数据集位置和类别。这个数据集只有一类yaml内容很简洁path: data/airplane train: train/images val: val/images test: test/images names: 0: airplanepath是数据集根目录train和val是相对根目录的图片目录路径。YOLOv8会自动在train/images旁边找train/labels所以目录名不能改错。如果只有训练集和验证集test可以省略但写上也无妨。names从0开始单类别就写0: airplane。注意类别名用英文避免编码问题中文类别名在部分版本下会报错。这里有一个常见误解train路径里不要写绝对路径也不要在path之外再写一层data/airplane。我见过有人写成train: data/airplane/train/images导致YOLO在data/airplane/data/airplane/train/images里找图直接报错。配置文件写好后可以用yolo checklist或直接打印yaml内容确认一次。4.2 训练命令与关键参数imgsz、batch、epochs怎么设在命令行执行yolo detect train dataairplane.yaml modelyolov8n.pt imgsz1024 epochs100 batch8 device0 projectruns nameairplane_1024参数含义如下modelyolov8n.pt使用官方预训练权重初始化。1000张图训yolov8n不会太慢而且能利用COCO上预训练的通用特征。如果数据量更大、显卡更强可以换yolov8s.pt。imgsz1024和原始图像尺寸一致。这是性能提升的关键详见第5章。epochs100单类别100轮基本够用。训练完看results.csv里val/box_loss还在下降就继续训到150或200。batch8显存不够就减小batch不要降低imgsz。8G显存跑1024分辨率8 batch可能会爆改batch4或2。device0指定GPU。没有GPU写devicecpu但1024输入的CPU训练非常慢建议用云端GPU或先用640跑通测试。如果是新手我强烈建议先用yolov8n和epochs20跑一遍确认数据路径、标签文件都没问题再回来跑长训练。这样半小时内能见到结果而不是等几小时后才发现训练集数量不对。训练过程中可以打开runs/airplane_1024/results.csv看实时曲线重点看train/box_loss是否下降、val/mAP50(B)是否上升。4.3 验证与单图推理跑通一次完整推理训练结束后runs/airplane_1024/weights/best.pt保存了验证集上指标最好的权重。用它对验证集做评估yolo detect val modelruns/airplane_1024/weights/best.pt dataairplane.yaml imgsz1024输出里会包含每个类别的precision、recall、mAP50、mAP50-95。对单类别的遥感检测mAP50更重要。mAP50-95因为涉及IOU 0.5到0.95的平均小目标框稍微偏一点分数就掉得厉害所以偏低很正常不必执着于把它拉高。然后挑一张验证图做可视化推理yolo detect predict modelruns/airplane_1024/weights/best.pt sourcedata/airplane/val/images/airplane-003-0595.jpg imgsz1024 conf0.25 saveTruesource可以是单张图、文件夹或视频。conf0.25是置信度阈值低于此值的框不显示。saveTrue会把结果图存到runs/detect/predict。建议把conf降到0.1再看一次你会看到更多低置信度框——这些框里混着被阴影遮挡的飞机和误检的地面车辆。别急着下结论先分清楚哪部分是需要修复的漏检哪部分是需要负样本抑制的误检。5. 避坑指南卫星图飞机检测的五个典型翻车现象以下现象按我实际训练中遇到的频率排序。每一个都按“现象 → 原因 → 解决”的结构说明方便你照着排查。5.1 现象一模型把亮色车辆误检为飞机现象训练完成后模型在验证图上对白色面包车、公交车车顶框出高置信度甚至比真飞机的置信度还高。原因这个数据集只有飞机正样本没有“无飞机”的负样本。模型在特征空间里发现“灰白色矩形亮斑”和飞机很像于是把视觉相似的车辆也当成了飞机。这是目标检测在单一类别小数据集上最常见的表现模型学会了找“亮色矩形物体”而不是理解飞机的机翼、机身结构。解决收集一些不包含飞机的卫星图作为负样本放到训练目录中并给它们生成空标签文件。YOLO支持空标签图片训练时会把它们作为背景参与损失计算。我一般保证负样本数量大概是正样本的三分之一。另一个办法是降低conf阈值但后处理只能缓解误检比例不能根治。只有让模型见过“没有飞机的背景长什么样”它才会学会区分。5.2 现象二标注框太紧训练时上下文不足现象模型输出的边界框总比飞机实际轮廓小一圈机头或机翼被截断看起来像“框没框全”。原因VOC的bndbox是紧贴目标的最小外接矩形但飞机有翼展、阴影和轮子紧框会让模型只看到飞机本体看不到周围的地面背景。模型在判断边界时缺少上下文倾向于把预测框收缩到置信度最高的核心区域。解决转换脚本里对框做适度外扩。常见做法是把xmin, ymin向外扩10%xmax, ymax也向外扩10%扩完后再做边界裁剪。比如框宽100像素外扩10像素。这个操作能让模型看到飞机周围的水泥地或草地帮助它分清“飞机边界”而不是“白色像素边界”。实测对mAP50有1到3个点的提升。但别扩太多否则一个框可能把旁边的飞机也包进去NMS阶段就会出错。5.3 现象三默认640尺寸训练小飞机大量漏检现象用imgsz640训练验证集上很多小飞机完全没被检出来尤其是一架停在浅色水泥地面上的白色飞机人和背景几乎融为一体。原因原始图片是1024x1024缩放到640后原本100x100像素的飞机变成62x62像素。YOLO主干网络有32倍下采样62像素的物体在最终特征图上只占约2x2个格子信息几乎全部丢失。解决训练时imgsz必须是1024最好设置成1280。显存不够时减小batch而不是减小分辨率。另外在大图上训练时可以用切片推理SAHI辅助把大图切成多个小块分别检测再合并这个思路在下一章详细说。如果训练后模型在640上性能尚可说明你的目标不算太小但换到遥感场景时不要抱有侥幸心理。5.4 现象四阴影里的飞机召回率很低现象凡是机翼有一半落在阴影里的飞机模型只框住亮出的部分甚至整架漏检。夜间或强阴影下召回率掉到30%以下。原因训练数据中的飞机大多是光照均匀的模型学到的是“亮色蒙皮机翼形状”的组合。阴影破坏了飞机的局部亮度模型对“暗色飞机”没有见过置信度自然降低。解决在训练参数里加大颜色扰动。YOLO内置的hsv_h, hsv_s, hsv_v参数默认是0.015, 0.7, 0.4可以改成hsv_h0.05, hsv_s0.9, hsv_v0.6让模型在训练时见到更多亮度变化的样本。另外可以加入随机gamma增强但别太猛否则会把飞机蒙皮变成花的。若你发现增强后训练loss抖动厉害说明幅度过大回调一些。5.5 现象五随机划分导致同一场景的飞机同时出现在训练和验证集现象训练时mAP50曲线很漂亮最高到0.95但把模型放到另一批机场图片上测试mAP直接掉到0.6。原因文件名的003批次很可能代表同一场景。同一机位、同一光照条件下拍出的多张图背景几乎一样。随机划分时几张相似的图可能分别进了训练集和验证集模型相当于“背过题”了验证集虚高。解决按文件名前缀分组划分。具体做法是先把所有图片按airplane-003这样的前缀分组得到若干组再把整组随机分配到train或val。这样训练集和验证集有更独立的场景指标才可信。代码实现也不难在划分之前先根据img.name.split(-)[1]构造分组字典再对组做random.shuffle。之后的训练流程不变但你的模型泛化能力会扎实很多。6. 进阶操作切片推理、TTA增强与自定义训练策略6.1 用切片-融合策略检测大尺寸遥感图前面讲的imgsz1024只适用于这份数据集本身的尺寸。实际业务里一张遥感图动辄5000x5000像素直接缩放成1024会让飞机只剩几个像素。常用解决方案是滑窗切片把大图切成多个1024x1024的小块保留20%重叠对每个切片推理再把检测框坐标映射回原图最后做一次NMS合并重叠框。下面是一段可直接用的切片推理代码使用Ultralytics的Python接口import cv2 import numpy as np from ultralytics import YOLO model YOLO(runs/airplane_1024/weights/best.pt) def simple_nms(boxes, scores, iou_thr0.4): x1 boxes[:, 0]; y1 boxes[:, 1] x2 boxes[:, 2]; y2 boxes[:, 3] areas (x2 - x1) * (y2 - y1) order scores.argsort()[::-1] keep [] while order.size 0: i order[0] keep.append(i) xx1 np.maximum(x1[i], x1[order[1:]]) yy1 np.maximum(y1[i], y1[order[1:]]) xx2 np.minimum(x2[i], x2[order[1:]]) yy2 np.minimum(y2[i], y2[order[1:]]) inter np.maximum(0.0, xx2 - xx1) * np.maximum(0.0, yy2 - yy1) iou inter / (areas[i] areas[order[1:]] - inter) inds np.where(iou iou_thr)[0] order order[inds 1] return keep def slice_infer(image_path, slice_size1024, overlap0.2, conf0.2): img cv2.imread(image_path) h, w img.shape[:2] stride int(slice_size * (1 - overlap)) boxes, scores [], [] for y in range(0, h, stride): for x in range(0, w, stride): crop img[y:y slice_size, x:x slice_size] result model.predict(crop, imgszslice_size, confconf, verboseFalse)[0] for box in result.boxes: x1, y1, x2, y2 box.xyxy[0].tolist() boxes.append([x x1, y y1, x x2, y y2]) scores.append(box.conf.item()) boxes np.array(boxes) scores np.array(scores) keep simple_nms(boxes, scores, iou_thr0.4) return boxes[keep], scores[keep]这段代码的overlap0.2意味着切片之间重叠20%避免飞机正好被切成两半时两边的框各剩半架。stride int(slice_size * 0.8)所以每个切片只移动80%的位置。推理后坐标要加上切片左上角的偏移量x, y映射回原图。最后那个NMS实现是入门级的实际生产可以直接用cv2.dnn.NMSBoxes或torchvision.ops.nms替代。切片会增加推理时间但对大图检测几乎是必须的。6.2 用TTA增强验证结果YOLO自带ttaTrue参数推理时会对图像做水平翻转、垂直翻转和旋转把多次推理结果合并能小幅提升召回率尤其是对阴影和遮挡。命令如下yolo detect predict modelruns/airplane_1024/weights/best.pt sourcebig_test.png imgsz1024 ttaTrue conf0.1 saveTruettaTrue会显著增加推理时间大约是8倍。如果你只是做离线分析或比赛值得开。如果模型要部署到线上实时推理建议关掉TTA只保留单尺度推理。6.3 一个复盘习惯把失败样本沉淀为分析集我从这个数据集里学到最重要的不是mAP数字而是一个工作习惯每次训练后把验证集上被漏检、误检的图单独拷到一个hard_examples目录定期回看。你会发现漏检集中在某几种停机位朝向或者某一类阴影角度然后针对性地调整增强参数或者补充切片样本。从那以后我每次拿到新的遥感数据集都会强制走一遍固定流程按批次分组划分、用1024训练、切片推理验证、失败样本复盘。这套流程走完模型才敢放到真实业务场景里用。这份飞机卫星图数据集适合当你跑通上述流程的起点。下载后按第3章和第4章的脚本走一遍你就能把“只会调YOLO接口”变成“能处理遥感场景的检测工程师”。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑