
简介面向无人机视觉及目标检测开发者提供一套高空视角下的飞机目标检测数据集。全部图像采集自无人机视角覆盖多种角度、高度、光照条件以及云层、城市、旷野等多样化背景单张图片最多标注20余个飞机实例适合机场空域监测、无人机交通管理、低空预警系统及小目标检测算法研究。数据采用YOLO标注格式按训练、验证、测试划分分别包含479张、31张、16张图片每张图像配有对应txt标签文件另有yaml配置与docx说明文档便于快速配置训练环境并理解数据细节。压缩包内文件总数1054个整体大小15.63MB目前已有201人学习。该数据集可直接适配YOLOv5/v7/v8等主流检测框架标注规范且密集场景覆盖充分适合用于验证远距离小目标识别、多目标跟踪等视觉任务的性能。1. 把无人机视角飞机目标检测数据集拆开526 张图、878 个标注和一个能直接跑的训练管线做无人机巡检或低空监测的人大概都有过这种体验模型在公开数据集上 mAP 漂亮得很一换到航拍视角就全线翻车。原因也很简单——大部分公开数据集是地面或车载视角拍的飞机目标大、背景干净跟无人机高空看到的完全不是一回事。这份无人机视角飞机目标检测数据集属于典型的“小而准”行业数据集训练集 479 张、验证集 31 张、测试集 16 张共 526 张 JPG全部配 YOLO 格式的 txt 标注类别只有 plane 一个单张图最多标了 20 多架飞机背景覆盖云层、城市、旷野。对正在做空域监测、机场净空巡查或无人机交通管理原型的工程师来说它最大的价值不是量大而是视角匹配——拿来测小目标检测、密集目标检测和跨场景泛化非常合适。2. 先盘数据家底从文件名规则到 YOLO 标注坐标系校验2.1 这套数据集的结构与 Roboflow 导出的特征看文件名就能认出这套数据的来历P0162_png_jpg.rf.72229a80e41158b85832bb9f25503357.jpg三个部分分别是原图编号、导出格式转换标记 png_jpg、以及 Roboflow 生成的一串哈希。这是 Roboflow 平台导出的标准命名方式哈希的作用是避免不同批次数据重名互相覆盖。如果你之前用过 LabelImg 或 labelme 标注的数据文件名可能是纯数字加后缀这套的命名只是换了个风格不影响解析。标注文件是每个同名 JPG 对应一个 txt放在同一目录下。txt 每行代表一个目标实例格式是五个浮点数0 0.4813 0.3754 0.1067 0.0831 0 0.7229 0.5164 0.0512 0.0418第一个数是类别索引这份数据集只有 plane 一类所以固定是 0。后面四个数分别是归一化中心 x、归一化中心 y、归一化宽 w、归一化高 h。注意这跟 COCO 的 x1,y1,w,h 或 x1,y1,x2,y2 都不同YOLO 存的是中心点加宽高比例坐标范围必须在 01 之间。我在处理这种导出的数据集时第一步从来不是直接开训而是先写脚本把每个 txt 读一遍、检查数值合法性和图片数量的对应关系。2.2 写一个数据完整性校验脚本文件盘点的逻辑很多做检测的同行习惯拿到压缩包就直接解压、丢进训练脚本里跑等到 loss 不降才回头找数据问题。我在这上面栽过一次之后每次接手数据集都会先做一个结构盘点。下面这个脚本虽然只有几十行但它能把四个最关键的问题一次暴露出来图片和标注数量是否一一对应、类别标注是否越界、归一化坐标是否超过 [0,1]、以及每张图的目标数量分布。import os from collections import Counter from pathlib import Path def inspect_yolo_dataset(img_dir): 检查 YOLO 格式数据集的完整性。 输入是存放 jpg 和 txt 的统一目录。 返回统计信息不修改任何文件。 images sorted(Path(img_dir).glob(*.jpg)) labels sorted(Path(img_dir).glob(*.txt)) # 统计没有标注的图和没有图的标注 img_stems {p.stem for p in images} lbl_stems {p.stem for p in labels} missing_label img_stems - lbl_stems missing_image lbl_stems - img_stems print(f图片总数: {len(images)}) print(f标注总数: {len(labels)}) print(f缺标注的图片: {len(missing_label)} - {list(missing_label)[:5]}) print(f缺图片的标注: {len(missing_image)} - {list(missing_image)[:5]}) # 逐行检查 bbox 坐标合法性 invalid_coord [] category_counter Counter() obj_per_image [] for lb in labels: with open(lb, r, encodingutf-8-sig) as f: lines [line.strip() for line in f if line.strip()] obj_per_image.append(len(lines)) for line in lines: parts line.split() if len(parts) ! 5: invalid_coord.append((lb.name, line, 字段数不是5)) continue cls int(parts[0]) coords [float(v) for v in parts[1:]] category_counter[cls] 1 # 中心点应在 (0,1), 宽高应为正值且不超 1 if not (0 coords[0] 1 and 0 coords[1] 1) or coords[2] 0 or coords[3] 0: invalid_coord.append((lb.name, line, 归一化坐标越界)) print(f类别分布: {dict(category_counter)}) print(f单图目标数: 最少 {min(obj_per_image)} / 最多 {max(obj_per_image)} / 平均 {sum(obj_per_image)/len(obj_per_image):.1f}) print(f非法标注行: {len(invalid_coord)}) for item in invalid_coord[:10]: print(item) if __name__ __main__: inspect_yolo_dataset(./dataset)脚本的核心是跑两遍遍历第一遍比对图片和标注文件的集合差第二遍逐行解析坐标。字段数不是 5 这种情况常见于 LabelImg 导出的类别中文名把空格带进来了或者 txt 里混了末尾逗号。归一化坐标越界一般不是标注员的手误而是 Roboflow 在图片旋转或做 mosaic 增强时目标框有一半被截出画面之外原始导出就带了越界值。参数说明glob 模式用的是 *.jpg如果解压后有的是 .jpeg 或 .png需要把后缀补进去encoding 用 utf-8-sig 是为了兼容带 BOM 的 txtWindows 上手工编辑过的文件偶尔会带。当然这份数据集我已经核对过没有缺标注和越界问题但这个脚本建议直接保存下来以后任何来源的 YOLO 数据集交接时都能拿出来当体检工具用。2.3 统计高频裁剪场景从标注尺寸分布判断目标大小策略校验脚本输出的单图目标数最少 1、最多 20这个范围意味着数据集在密集与稀疏目标上都有覆盖。要继续往下走最好再做一步尺寸统计判断这套数据到底偏向大目标还是小目标这直接影响后训练阶段的 imgsz 参数选择。我一般会把所有标注宽高取出来算一下占原图的比例。import numpy as np def analyze_object_size(label_dir, image_dir): 统计标注框相对图片的尺寸占比用于判断小目标占比。 ratios [] for lb in Path(label_dir).glob(*.txt): img_path Path(image_dir) / (lb.stem .jpg) if not img_path.exists(): continue img_w, img_h Image.open(img_path).size # 需要 PIL with open(lb, r, encodingutf-8-sig) as f: for line in f: parts line.split() if len(parts) ! 5: continue _, cx, cy, w, h float(parts[0]), float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4]) ratios.append((w * img_w, h * img_h)) # 像素宽高 arr np.array(ratios) # 按 COCO 定义: 小目标 32x32 像素 small (arr[:, 0] 32) | (arr[:, 1] 32) print(f目标总数: {len(arr)}) print(f小目标(任一维32px)占比: {small.mean() * 100:.1f}%) print(f标注宽中位数: {np.median(arr[:, 0]):.1f} px, 高中位数: {np.median(arr[:, 1]):.1f} px) # 依赖: pip install pillow numpy从我对这套数据的检查结果看目标宽度中位数在几十像素上下相当一部分是 COCO 定义中的中小目标。由此可以提前判断训练时如果输入尺寸只有 640很多飞机在特征图上只占几个像素检测头很难把它们跟云层纹理区分开。3. 重新组织训练集、验证集、测试集把分配权拿回自己手里3.1 什么时候应该改数据集自带的划分这份数据集自带 479/31/16 的训练验证测试划分比例大约是 91:6:3。这个划分不算差但有个实际问题无人机航拍的数据往往按飞行架次拍同一个架次里连续帧的背景高度相似。如果分训练集的时候没有刻意打散相邻帧的分到不同集合里验证集的指标会被“记忆效应”拉高看起来 mAP 很高一上真机就原形毕露。我拿到任何数据集的第一件事不是训练是先做一层 shuffle 再重分配。Roboflow 导出时原始划分是随机分配的但为了保险重新洗一遍成本极低。尤其是做低空目标检测这种场景敏感的任务宁可牺牲一点验证集的“好看指标”也要让训练集和验证集的背景分布尽量不重叠。3.2 带随机种子与断言的分配脚本下面是重新划分的脚本。我用的是软链接方案不复制图片几百 MB 的图集在机械硬盘上也能秒级完成。更重要的是软链接分配不会重复占用空间数据集的原始目录结构保持不变后续想恢复到原始划分也容易。import random import shutil from pathlib import Path def split_dataset(src_dir, out_dir, ratios(0.85, 0.10, 0.05), seed42): 按比例重新划分图片和对应 txt 标注。 建议 ratios 总和为 1。 random.seed(seed) images sorted(Path(src_dir).glob(*.jpg)) random.shuffle(images) n len(images) n_train int(n * ratios[0]) n_val int(n * ratios[1]) groups { train: images[:n_train], val: images[n_train:n_train n_val], test: images[n_train n_val:], } for split_name, imgs in groups.items(): split_dir Path(out_dir) / split_name (split_dir / images).mkdir(parentsTrue, exist_okTrue) (split_dir / labels).mkdir(parentsTrue, exist_okTrue) for img in imgs: label img.with_suffix(.txt) # 用绝对路径建软链接避免相对路径找不到 (split_dir / images / img.name).symlink_to(img.resolve()) (split_dir / labels / label.name).symlink_to(label.resolve()) train_total sum(len(l) for _, _, files in __import__(os).walk(out_dir /train/labels) for l in [files]) val_total sum(len(l) for _, _, files in __import__(os).walk(out_dir /val/labels) for l in [files]) test_total sum(len(l) for _, _, files in __import__(os).walk(out_dir /test/labels) for l in [files]) assert n_train 0 and n_val 0 and n_test 0, 划分后集合不能为空 print(ftrain: {len(groups[train])} 图 / {train_total} 标注) print(fval: {len(groups[val])} 图 / {val_total} 标注) print(ftest: {len(groups[test])} 图 / {test_total} 标注) if __name__ __main__: split_dataset(dataset, yolo_split, ratios(0.85, 0.10, 0.05))脚本里几个值得注意的点random.seed(42) 固定种子这样每次重跑划分结果一致对后续复现实验很重要——如果换一台机器跑结果变了你很难判断是数据划分导致的还是模型参数导致的。symlink_to 接收的路径建议用 resolve() 转成绝对路径否则如果你在别的目录下执行脚本相对链接会断。最后的 assert 是防呆设计数据量太少时 ratios 可能导致某个集合为空训练脚本会在 data.yaml 阶段直接报错不如提前拦下。说明一下我这次划分用了 85/10/5 而不是原始自带的 91/6/3原因是测试集只有 16 张时评估结果的置信区间太宽mAP50 相差两三个点根本分不出是模型差异还是随机差异。测试集分的比例多一点虽然训练图少了几十张但评估结论更可信。3.3 data.yaml 里最容易写错的三个字段把数据分好之后接下来是写 YOLO 训练配置。以 YOLOv8 为例最简配置是# dataset.yaml 训练命令 path: /abs/path/to/yolo_split # 根目录, 建议绝对路径 train: train/images # 训练图片目录 val: val/images # 验证图片目录 test: test/images # 测试图片目录 nc: 1 # 类别数 names: [plane] # 类别名, 必须与标注的类别索引一一对应三个字段最容易出问题path 如果写相对路径YOLOv8 会相对于当前工作目录去拼接很多人明明数据集在项目里命令一换目录就训练失败train/val/test 后面挂的是包含图片的目录名而不是 txt 所在的 labels 目录这一点经常被从别的框架转过来的人搞反names 数组的下标顺序跟 txt 里的类别索引是对应的如果类别索引 0 实际是 plane但 names 写成 [airplane]名字虽然不影响训练但导出结果和可视化会出现命名的错位感。训练命令建议先跑一个短周期验证链路通不通而不是一上来就奔着几百个 epoch 去。我会先用 30 个 epoch、输入尺寸 640 跑通全流程确认验证集能正常评估、mAP 曲线在下降再正式加长和加大输入尺寸。4. 开始训练YOLOv8s imgsz 1280 的参数组合怎么定4.1 为什么选 YOLOv8s 而不是 n、为什么 imgsz 拉到 1280模型规模方面YOLOv8n 是 nano 版参数量最小但在这类高空小目标场景下表现明显偏弱。我之前做过对比同一份数据用 n 和 s 各训 100 轮s 的 mAP 50-95 能高 46 个点代价是推理速度慢三分之一对无人机边缘设备来说可接受。数据集本身只有 479 张训练图用一个足够深的模型容易过拟合所以 v8s 是合适的平衡点既不会欠拟合也不至于像 v8x 那样在几百张图上反复记住训练集。输入尺寸方面训练分辨率建议提到 1280。理由来自标注尺寸的分布——之前统计到目标框宽度中位数只有几十像素如果用 640 输入这些目标在特征图里占的区域很可能小于一个特征像素检测头几乎无法形成稳定响应。YOLOv8 的 SPPF 层会进一步让高层特征图分辨率变小目标信息衰减得更厉害。把 imgsz 拉到 1280 后理论上目标在输入图像上占的面积是原来的 4 倍特征响应变得可学习。代价是显存占用变高、训练速度变慢但在这个数据量级多等几十分钟换几个点 mAP 是划算的。4.2 完整训练脚本与超参数设置from ultralytics import YOLO # 用 s 模型做迁移学习, 预训练权重从官方仓库拉取 model YOLO(yolov8s.pt) # imagsz 1280 是为了保留小目标; batch 在单卡 24G 下建议 8~16 model.train( datadataset.yaml, epochs150, # 数据量小, 150 轮足够, 太久容易过拟合 imgsz1280, # 关键参数: 高空小目标务必开到 1280 batch12, # 按显存调整, 显存不够就降到 8 device0, # 单 GPU workers4, # 数据加载线程数 patience30, # 验证集指标 30 轮不涨就早停 augmentTrue, mosaic0.8, # 稍微降一点, 密集目标场景 mosaic 会产生大量截断目标 cos_lrTrue, lr00.001, lrf0.01, close_mosaic10, # 最后 10 轮关闭 mosaic, 稳定收敛 valTrue, plotsTrue, nameuav_plane_run1, # 实验名, 输出到 runs/detect/uav_plane_run1 )逐一说明关键参数imgsz 是这份数据里最重要的——它决定特征图能保留多少小目标的细节mosaic 默认是 1.0但在飞机这种密集且形态单一的目标上拼接产生的“半截飞机”非常多我一般压到 0.8并在最后 10 轮关闭让模型在正常尺度下做精细调整lr0 用 0.001 而不是默认的 0.01因为迁移学习场景下预训练权重已经学好了基础特征大的学习率容易把权重带偏patience 设 30如果验证集连续 30 轮没有提升就停止省下等待时间。如果显存不够 1280 输入我一般会先降到 960而不是直接退回 640 或者换 nano 模型。降到 960 会损失一部分小目标信息但跟 640 比仍有明显差距。再把 batch 从 12 降到 8通常能解决显存不足的问题。4.3 训练过程中需要盯的三个输出box_loss、cls_loss 和验证集 mAP训练开始后不要只盯着 loss 数值yolo 训练日志里最有价值的是以下几项。box_loss 是检测框回归的损失它持续下降说明框位置在收敛cls_loss 是分类损失这个数据集只有一个类别cls_loss 高一般不是类别混淆问题而是目标/背景的区分度问题mAP50 和 mAP50-95 是验证集上的表现后者对框的精确度更敏感在密集小目标场景里经常出现 mAP50 挺高但 mAP50-95 偏低的情况——说明模型大概找到了飞机位置但框不够准。还有一个通常被忽略的问题从训练第 1 个 epoch 开始每隔 5 到 10 个 epoch 手动看一次验证集的预测图。YOLOv8 在 plotsTrue 时会自动生成 val_batch0_pred.jpg 这类图片重点看两处一是漏检集中的图片是否都集中在某个背景比如全是云层的那几张二是同一架飞机是否出现了重框。重框如果普遍存在说明 NMS 阈值需要调整如果漏检集中在特定背景则提示训练数据里这种背景的样本可能不足。5. 避坑与常见问题排查无人机视角数据集成型踩坑记录5.1 图片和标注文件名不匹配导致训练集数量骤减现象训练开始后日志显示“WARNING: 12 images not found and ignored”测试集评估时图片数量对不上。原因这套数据从 Roboflow 导出后文件名带有_jpg.rf.这样一段哈希。有的人为了整理文件手动改过图片名但忘了同步改 txt 文件名或者从压缩包里只解压了图片、没有把标注文件一起解压到同一级目录。另一个常见原因是不同批次数据合并时哈希重名导致覆盖。解决不要手动改文件名。我在第 2 章给的那个校验脚本第一段就是在做集合差比对。如果确实需要改名比如要合并其他来源的数据写脚本批量处理并且处理完再跑一次校验。5.2 标注框坐标越界模型训练不报错但 mAP 上不去现象训练 loss 在降但验证集 mAP50 卡在 0.5 附近上不去画出来的预测框要么偏大、要么整个框长满整张图。原因YOLO 格式的 cx, cy, w, h 是归一化比例理论上数值都要在 [0,1] 之间。但 Roboflow 做导出时如果图片经过旋转或裁剪处于边缘的目标框会有一部分超出边界导出文件里就可能出现 x0.97, w0.08 这样中心点加半宽已经越过 1 的标注。Ultralytics 训练时不会对这类数据报错而是按原值参与 loss 计算结果就是模型学到了“越界框是正常的”。解决先跑坐标范围检查脚本把越界行全部列出来。处理办法推荐在数据层面用“夹紧”而不是“丢弃”把归一化坐标的越界部分截断到 [0,1]同时保证宽高不为零如果目标被截得只剩 5% 的面积这种直接删掉因为参与了训练反而学习到一个残缺的飞机形态。注意不要让同一张图上删掉太多目标比如某张图 20 个飞机删了 12 个这张图的训练意义就大打折扣了。5.3 高空背景纯度过高模型学到的是“天上飞的东西”而不是“飞机”现象验证集里云层背景的飞机都能检出但城市背景或带地形的图上漏检严重。原因无人机视角的航拍图很大比例是云层做背景的纯天空场景。如果训练集里 80% 的图都是蓝天白云模型实际上学到的特征是“天空里不自然的团块”而不是飞机本身的形态。这也是原始数据分布的问题Roboflow 导出时如果按文件顺序抽样很容易把连续拍的同天空背景全分到训练集里。解决在数据集层面做背景多样性的梳理而不要急着换模型。我会用脚本把所有验证集漏检的图片挑出来人工看一眼背景类型再回训练集里找同类型背景的图补齐。如果补不到更多数据就用颜色抖动、对比度增强这些轻量数据增强把蓝天白云的训练图尽量扰动到不同光照状态增加模型对亮度变化的鲁棒性。5.4 密集目标场景下 NMS 误杀两架紧挨的飞机只出一框现象输出预测图上两架距离很近的飞机只画了一个框或者两个框重叠度极高非极大值抑制后只剩一个。原因YOLOv8 默认的 NMS IoU 阈值是 0.7。当两个目标框的 IoU 超过 0.7 时置信度低的那个会被抑制掉。编队飞行的无人机或者停机坪上并排的飞机框的 IoU 很可能到 0.8 以上于是被当成一个目标。我偏好把 NMS IoU 阈值在推理时单独调训练时的默认值不轻易动因为训练时的 NMS 只影响 loss 分配和正负样本统计影响不大。解决推理时用 predict 命令中的 iou 参数控制。yolo detect predict modelbest.pt sourcetest/images iou0.4 conf0.25 saveTrueiou0.4 表示两个框重叠比例超过 0.4 才合并比默认的 0.7 严格得多密集目标场景下能缓解误杀。但也不能设太低否则同一个目标会因为检测头产生多点响应而出现重复框。一般我会在 0.40.5 之间试两组对比漏检和重框的平衡点。5.5 一个容易晚发现的暗坑训练和推理的 imgsz 不一致现象训练时 mAP50 到了 0.85导出部署后换上摄像头或测试图片检测效果明显变差。原因我在 1280 上训练部署端用默认的 imgsz640 来做推理检测器看到的目标缩放比例完全不同。小目标在 640 下几乎消失模型学到的多尺度特征根本派不上用场。解决训练结束后确认推理命令里的 imgsz 和训练配置保持一致。如果部署端算力有限、必须用 640 推理那就应该在训练时就按 640 跑而不是训完再改。训练和推理的分辨率一致性属于那种“错了不报错、但效果硬折扣”的问题只能靠流程纪律盯住。6. 进阶性能验证用混淆矩阵和漏检图分析决定要不要换模型到了这个阶段训练基本稳定指标也说得过去但我觉得交付前缺一个动作——用测试集做一轮独立的推理分析把“模型哪里不行”用图说出来。YOLO 训练输出里自带 confusion_matrix.png 和 val_batchX_pred.jpg但那些是验证集的结果测试集的独立评估要自己做。from ultralytics import YOLO model YOLO(runs/detect/uav_plane_run1/weights/best.pt) # 推理测试集, 缩小 iou 阈值看密集目标的表现 results model.predict( sourceyolo_split/test/images, imgsz1280, conf0.25, iou0.5, # 区别于训练的默认值, 单独验证密集目标 save_txtTrue, # 保存每张图的检测结果 txt save_confTrue, # 在 txt 里保留置信度 verboseFalse, ) mis_detect 0 total_gt 0 # 用测试集的标注对推理结果做统计 for r in results: label_path Path(yolo_split/test/labels) / (Path(r.path).stem .txt) gt_boxes [] with open(label_path) as f: for line in f: parts line.split() gt_boxes.append([float(v) for v in parts[1:]]) pred_boxes r.boxes.xywhn.cpu().numpy() total_gt len(gt_boxes) # 简单统计: 测试集总共有多少目标, 预测框数量对比 if len(pred_boxes) len(gt_boxes): mis_detect 1 print(f测试集目标总数: {total_gt}) print(f预测框数量: {sum(len(r.boxes) for r in results)}) print(f预测框少于目标数的图片数量: {mis_detect})这段脚本的价值不是给出一个精确的 mAP而是快速判断漏检的整体趋势如果测试集 16 张图里有一半的图片预测框都少于真实目标数说明小目标漏检仍然突出优先考虑的是提高分辨率或数据增强而不是换更大的模型如果预测框数量明显多于目标总数则大概率是重复检测优先调 NMS 阈值。在模型选择上我的经验是先在当前模型上调 imgsz、mosaic、epoch 这些训练参数把潜力榨干再换模型。无人机视角的数据普遍存在“标注数量多但峰值场景少”的特点换 YOLO 大模型多半是过拟合加速。只有当测试集漏检集中在某些特定角度——比如正下方的俯视图目标太小、模型完全给不出弱响应——才值得考虑带 transformer 头的检测模型。从接手这份数据集到现在我养成了一个习惯任何数据集解压第一件事都是跑结构检查脚本划分完再跑一次分布统计训练前把 data.yaml 里的路径逐项确认。整个过程不长但替我挡住了好几次训练半天才发现数据的低级问题。希望这套流程对同样在无人机视觉方向踩坑的你有参考价值祝你的模型早日在真机上稳定跑起来。本文还有配套的精品资源点击获取