☰
从PASCAL VOC到YOLO:飞机检测数据集解析与训练避坑指南
2026/9/28 7:49:33 网站建设 项目流程

简介:一套基于PASCAL VOC 2012定制筛选的专用飞机检测数据集,只保留aeroplane这一目标类别,面向需要训练YOLO等实时目标检测模型的开发者和算法学习者,可用于航空监控、无人机巡检、机场安检等场景中的飞机识别定位。整个压缩包共2149个文件,含716张jpg原图、717个txt标签和716个xml标签,总大小56.85MB;txt标签记录边界框坐标,xml标签补充类别与更详细的区域注解,图像与标签一一对应,解压后可直接配合Darknet等YOLO训练框架使用。已有1256人学习下载。数据集覆盖不同角度、光照和遮挡条件的真实图像,训练时可自行划分训练集与验证集,也能在此基础上做数据增强和超参数调优,帮助快速上手YOLO数据组织方式与目标检测流程。

1. 先看清这份 aeroplane_VOCtrainval2012.zip 到底值不值的下

YOLO飞机检测数据集 aeroplane_VOCtrainval2012.zip 不是那种「又大又全」的通用目标检测包,它是从 PASCAL VOC 2012 的 trainval 里专门筛出来的单类子集,一共 716 张带飞机的图片,外加每张图对应的 txt 和 xml 两种标签文件。对刚入门 YOLO 或者想快速跑通一个完整训练流程的人来说,这份资源的价值在于省掉你从二十个类别的 VOC 全集里挑图、筛框的时间——解压、划分、配好 data.yaml,就能直接进训练。它的适用场景很聚焦:单类飞机检测的训练与验证,适合正在学 YOLO 结构、想看清 label 格式怎么影响 loss,或者需要一份干净的航空器数据集做预实验的从业者。不适合想要很多类别的通用检测模型、也不适合要海量数据跑大模型的人。

2. 标签格式拆解:txt 和 xml 里各写了什么

这份数据集里每张 jpg 都有两个同名的标签文件。你在解压后看到的典型文件结构是这样的:

aeroplane_VOCtrainval2012/ ├── 2010_001426.jpg ├── 2010_001426.txt ├── 2010_001426.xml ├── 2008_008086.jpg ├── 2008_008086.txt ├── 2008_008086.xml ...

图片是原始照片,txt 是给 YOLO 训练直接用的归一化坐标,xml 是 PASCAL VOC 原始标注。两个格式记录的是同一个框,但表达方式完全不同,转换不当会在训练时埋下大坑。

2.1 txt 标签格式:一行一个目标

打开任意一个 txt 文件,每一行对应一张图中的一个飞机实例,格式是五个数字:

0 0.431603 0.288750 0.346690 0.050729

第一个数字是类别 ID,后面四个是归一化后的边界框参数。YOLO 系列从 v3 到 v8 都用这种格式,它的定义如下:

参数含义取值范围
class目标类别 ID非负整数,本数据集只有 0
x_center边界框中心点的横坐标,除以图片宽度0 到 1
y_center边界框中心点的纵坐标,除以图片高度0 到 1
width边界框宽度,除以图片宽度0 到 1
height边界框高度,除以图片高度0 到 1

注意这里的坐标全部是归一化后的比例值,不是像素坐标。YOLO 在训练时会先对图片做 resize,再用这些归一化坐标在特征图的各个尺度上做匹配,所以用比例值能让标注在不同分辨率的输入图片上保持语义一致。如果你看到某个 txt 里出现负数、大于 1 的值或宽高为 0,那这份标签基本可以判死刑,训练时轻则 loss 不降,重则直接崩。

2.2 xml 标签格式:PASCAL VOC 的原始标注长什么样

xml 文件是 VOC 原始的标注格式,记录的信息比 txt 多很多。以 2010_001426.xml 为例,核心内容是这样的:

<annotation> <filename>2010_001426.jpg</filename> <size> <width>800</width> <height>600</height> <depth>3</depth> </size> <object> <name>aeroplane</name> <pose>Left</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>345</xmin> <ymin>173</ymin> <xmax>622</xmax> <ymax>203</ymax> </bndbox> </object> </annotation>

xml 里的 bndbox 给出的是绝对像素坐标,同时附带目标类别名、是否截断、是否难例等额外信息。YOLO 训练时不会直接用 xml,而是需要先把 bndbox 转成 2.1 里的归一化五元组。常见的做法是写一个 Python 转换脚本,读 xml、算中心点坐标、再做归一化写入 txt。

# xml_to_yolo.py import xml.etree.ElementTree as ET import os def xml_to_txt(xml_path, txt_path): # 解析PASCAL VOC格式的xml 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.iter('object'): name = obj.find('name').text # 在本数据集中只保留 aeroplane 类别 if name != 'aeroplane': continue # 关键点:单类数据集的类别ID固定写0 class_id = 0 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) # 计算中心点和宽高 x_center = (xmin + xmax) / 2 / img_w y_center = (ymin + ymax) / 2 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h # 组装成YOLO格式的txt行 lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") with open(txt_path, 'w') as f: f.write('\n'.join(lines)) # 批量转换 xml_dir = 'aeroplane_VOCtrainval2012' for f in os.listdir(xml_dir): if f.endswith('.xml'): xml_path = os.path.join(xml_dir, f) txt_path = os.path.join(xml_dir, f.replace('.xml', '.txt')) xml_to_txt(xml_path, txt_path)

这段脚本里有几处需要说明。class_id = 0 是强制写死的,不是从 xml 里读的,因为 aeroplane 在 PASCAL VOC 全集类别顺序里排第二类(person 是第 0 类,aeroplane 是第 1 类),但这份数据集只保留飞机一个类别,data.yaml 里只有一个名字叫 aeroplane 的类,所以 YOLO 的 class 索引必须从 0 开始。x_center 和 y_center 是中心像素坐标除以图片宽高,全部变成 0~1 区间的小数。归一化时如果你图片尺寸读错了,比如把 height 当成 width,生成的框就会被拉成竖条或扁条,训练时 loss 虽然能降但检测框完全不对。

2.3 图片分辨率与场景特征:直接影响训练参数

VOC2012 的图片不是统一尺寸的,有的 800×600,有的 1024×768。训练时模型会把输入 resize 成一个固定正方形,通常是 640×640。图片里的飞机有停在停机坪上的、有低空飞行的、有远处带仰角的,部分图里目标很小,只有几十个像素。

# 统计图片尺寸分布 python -c " from PIL import Image import os sizes = set() for f in os.listdir('aeroplane_VOCtrainval2012'): if f.endswith('.jpg'): img = Image.open(os.path.join('aeroplane_VOCtrainval2012', f)) sizes.add(img.size) print(sorted(sizes))

看输出你会得到一张尺寸列表,比如 [(500, 375), (800, 600), (1024, 768)]。这意味着 imgsz 参数不能设太小,否则小尺寸原图被压缩后飞机细节全丢。分辨率跨度大也说明训练时的数据增强里应该打开 mosaic 和 random_perspective,让模型对不同缩放尺度更鲁棒。

3. 整理目录结构:数据划分与 data.yaml 配置

拿到解压后的图片和标签,不能直接丢给 YOLO 训练。Ultralytics YOLO 默认的目录结构要求图片和标签分开放,并且训练集、验证集要单独建目录。这一章直接给出可复制的目录搭建脚本。

3.1 为什么要按 images 和 labels 分目录

YOLO 训练时通过图片路径推断标签路径,默认规则是找同目录下的同名 txt 文件,但如果图片和标签混在一起,文件多了之后很容易错配。标准的目录结构是:

datasets/ └── aeroplane/ ├── images/ │ ├── train/ │ │ ├── 2010_001426.jpg │ │ └── ... │ └── val/ │ ├── 2008_008086.jpg │ └── ... ├── labels/ │ ├── train/ │ │ ├── 2010_001426.txt │ │ └── ... │ └── val/ │ ├── 2008_008086.txt │ └── ... └── data.yaml

图片在 images/train/xxx.jpg,标签在 labels/train/xxx.txt,两者文件名完全相同,只有扩展名不一样。validation 同理。这样设计的好处是你只需要在 data.yaml 里分别指定 train 和 val 的图片目录,YOLO 会自动去找对应的 labels 目录。

3.2 数据划分脚本:随机分配加完整性校验

我一般不会手动拖文件,而是写一个划分脚本,一次性完成创建目录、打乱、拷贝、校验四件事。

# split_data.py import os import random import shutil src_dir = 'aeroplane_VOCtrainval2012' dst_dir = 'datasets/aeroplane' train_ratio = 0.8 random.seed(42) # 收集所有图片文件 images = [f for f in os.listdir(src_dir) if f.endswith('.jpg')] random.shuffle(images) split_idx = int(len(images) * train_ratio) train_files = images[:split_idx] val_files = images[split_idx:] # 创建目标目录 for split in ['train', 'val']: os.makedirs(os.path.join(dst_dir, 'images', split), exist_ok=True) os.makedirs(os.path.join(dst_dir, 'labels', split), exist_ok=True) def copy_paired(split_files, split_name): missing_txt = [] for img_name in split_files: base = os.path.splitext(img_name)[0] src_img = os.path.join(src_dir, img_name) src_txt = os.path.join(src_dir, base + '.txt') # 关键校验:标签文件必须存在 if not os.path.exists(src_txt): missing_txt.append(base) continue dst_img = os.path.join(dst_dir, 'images', split_name, img_name) dst_txt = os.path.join(dst_dir, 'labels', split_name, base + '.txt') shutil.copy2(src_img, dst_img) shutil.copy2(src_txt, dst_txt) return missing_txt print('train missing txt:', copy_paired(train_files, 'train')) print('val missing txt:', copy_paired(val_files, 'val')) print('train count:', len(train_files), 'val count:', len(val_files))

脚本里最关键的是 copy_paired 函数里的 exists 判断。训练集里如果混入一张没有 txt 的图,YOLO 会跳过它并打印 warning,但如果缺失比例过高,模型能学到的样本变少,直观表现就是 mAP 上不去。random.seed(42) 保证每次运行得到相同划分,便于复现你自己的训练结果。train_ratio 设成 0.8 在 716 张图的总量下大约能得到 572 张训练、144 张验证,对单类任务足够。

3.3 写 data.yaml:路径和类别 ID 别搞错

完成目录划分后,在 datasets/aeroplane/ 下新建 data.yaml:

# data.yaml path: /absolute/path/to/datasets/aeroplane train: images/train val: images/val names: 0: aeroplane

path 建议写绝对路径,因为 YOLO 在运行时会以当前工作目录为基准解析相对路径,如果你在项目根目录跑训练,写相对路径很容易踩坑。names 的索引必须和 txt 里的第一个数字对应,这份数据集里全是 0,所以 names 只有 0 这一个键。类别名 aeroplane 仅用于显示,不影响训练效果,但你将来想改成 airplane 也完全可以,只要保持索引 0 不变。

4. 用 YOLOv8 训练飞机检测模型

目录准备好了,接下来进入训练环节。这一章讲模型选型、训练命令和训练日志的判读方法,按这套参数配置可以直接跑通,不需要额外调参。

4.1 模型选型:为什么从 yolov8n 开始

716 张图属于小规模数据集,模型选太大容易过拟合,选太小的又怕精度不够用。Ultralytics YOLOv8 系列里 yolov8n 是最轻量的版本,参数量大约 320 万,适合在单卡小显存环境下快速迭代。有人会问怎么不用 yolov5s,其实两者都能做,但 yolo 命令在 v8 里默认集成了更多数据增强策略,对 VOC 这种小数据集更友好。如果你用的也是 ultralytics 框架,首次执行训练命令时会自动下载 yolov8n.pt 预训练权重,不需要手动去网页找。网络条件一般的情况下,可以先把 pt 文件放到项目根目录,命令行指定 model 参数时用本地文件名,避免运行时报下载超时。

模型参数量适用场景
yolov8n约 320 万小数据集、边缘设备、快速验证
yolov8s约 1110 万精度要求更高、显存充足
yolov8m约 2590 万大模型,本数据集不建议

4.2 训练命令与关键参数说明

从项目根目录执行:

yolo detect train \ data=datasets/aeroplane/data.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ patience=20 \ pretrained=True

各参数的作用如下表:

参数作用建议值
data指向 data.yaml 的路径绝对路径最稳
model模型结构或预训练权重首次用 yolov8n.pt
epochs训练轮数100 起步,看验证集 mAP 是否继续涨
imgsz训练输入分辨率640 兼顾速度和精度
batch每批图片数按显存调整,6G 显存建议 8
patience早停阈值,验证 mAP 连续多少轮不涨就停止20
pretrained是否加载 COCO 预训练权重True

显存不够时的表现是执行到一半报 CUDA out of memory。我一般先跑 batch=8 试一轮,看显存占用率,再决定要不要往上加。patience=20 的意思是验证集 mAP 连续 20 轮没有提升就提前终止,能省不少时间,因为你不知道 100 轮里哪一轮是真正的拐点。

4.3 训练日志怎么看:loss 曲线和 mAP

训练开始时输出面板会长这样:

Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size 1/100 2.1G 1.482 1.135 1.208 25 640 2/100 2.1G 1.201 0.9875 1.032 18 640

判断训练是否健康的几个观察点。box_loss 是边界框回归损失,持续下降说明模型在学框的位置;cls_loss 是分类损失,单类数据集里它应该降得很快,因为只有两类(有飞机和没飞机),相当于二分类;dfl_loss 是分布焦点损失,负责框的边界精细度,波动正常,但整体趋势得向下。如果 box_loss 在第 20 轮以后还在明显下降,说明 100 轮不够,可以加 epochs。训练结束后在 runs/detect/train/ 下会生成 confusion_matrix.png、results.png 等文件,results.png 里能看到每个 epoch 的 mAP50 和 mAP50-95。VOC 时代的传统指标是 mAP50,即 IoU 阈值取 0.5 的平均精度,评估这份数据集看 mAP50 即可。mAP50-95 是多个 IoU 阈值的平均,数值通常比 mAP50 低不少,不要拿它当主线指标。

5. 避坑指南与常见问题:亲手踩过的五个数据坑

从拿到原始 zip 到训练结束,最容易翻车的不是模型结构,而是数据和标签层面的各种小毛病。以下五个问题是我在实测过程中遇到过的典型场景,按「现象 → 原因 → 解决」的方式记录。

5.1 训练时提示 no labels 或图片标签对不上

现象:训练启动时打印大量 WARNING,或者执行到一半提示 no labels found,loss 一直不降。

原因:解压后的文件名大小写不一致,或者部分图片本身缺标签文件。VOC 原始文件名全是小写 jpg,但有人手动改名时会把扩展名改成大写 JPG,YOLO 匹配标签只认 .txt 不认扩展名大小写关系,找不到同名 txt 就跳过这张图。

解决:在划分脚本里加入 exists 检查(见 3.2 的 copy_paired),把缺失标签的图片单独输出到一个目录自己检查。我后来还养成了个习惯:解压完先跑一个全量检查脚本统计图片数和标签数,数值对得上再往下走。

5.2 class ID 写错导致类别混乱

现象:训练时 loss 下降正常,但验证集 mAP 很低,可视化检测结果发现所有框都偏向图片某一边缘。

原因:aeroplane 在 PASCAL VOC 全局类别列表里是第 1 类(person 是第 0 类),如果转换脚本直接用了 VOC 的全局索引,txt 里的 class 就变成 1,但 data.yaml 里只有一个 0 类,于是所有带 1 的标签在训练时被当成无效框,模型学不到正确信息。

解决:单类数据集的 txt 里 class 一律写 0。写转换脚本时直接硬编码 class_id = 0,不要从 xml 里读取 name 映射,这是最稳妥的做法。

5.3 训练中期 bn 崩溃、loss 变成 NaN

现象:某个 epoch 时 loss 突然变 NaN,或者报 RuntimeError: CUDA error: device-side assert triggered。

原因:标签里有坐标越界的框。比如某个 xml 里 xmax 比 xmin 还小,或者某些手工标注的框宽高算出来是负数、坐标为 1.5 之类的越界值。归一化坐标必须严格落在 0~1 区间内,宽高必须大于 0。

解决:训练前写一个数据校验脚本扫描所有 txt:

# validate_labels.py import os label_dir = 'datasets/aeroplane/labels' bad_files = [] for split in ['train', 'val']: for f in os.listdir(os.path.join(label_dir, split)): path = os.path.join(label_dir, split, f) with open(path) as fh: for line in fh: parts = line.strip().split() if len(parts) != 5: bad_files.append(path) break x_c, y_c, w, h = map(float, parts[1:]) if x_c < 0 or x_c > 1 or y_c < 0 or y_c > 1 or w <= 0 or h <= 0: bad_files.append(path) break print('failed files:', bad_files)

运行后如果有输出,就逐个检查对应图片,修正或删除异常框。老版本 ultralytics 在遇到越界标签时不会预处理过滤,直接进 CUDA 算子导致 crash,所以越早扫出来越好。

5.4 验证 mAP 高但实际检测不行

现象:VOC 测试集上 mAP50 很高,但拿手机拍的飞机照片或无人机俯视图来测,完全检测不到。

原因:VOC2012 中的 aeroplane 图片场景集中在中低空视角、目标主体比较完整,训练样本里缺少小目标、遮挡、逆光和密集停机场景。模型学到的特征偏向 VOC 场景。

解决:把 conf 阈值调低,比如 predict 时 conf=0.15,并且单独留一撮业务相关的图片做真实测试集,不能只用 VOC 里的 val 图评估模型生产可用性。数据增强里的 mosaic、copy_paste 能缓解一部分,但治本方法是补充自己场景的数据。

5.5 解压后目录嵌套多了一层

现象:data.yaml 里 path 指向解压目录,结果训练时找不到任何图片。

原因:压缩包在 Windows 上用默认解压工具解压时,经常生成 aeroplane_VOCtrainval2012/aeroplane_VOCtrainval2012/xxx.jpg 这样的嵌套结构。

解决:解压后先养成习惯看一层目录结构,确认 jpg 在最外层。路径配好后再跑yolo detect train,如果还是报图片不存在,打开 data.yaml 用绝对路径重新写一遍 path,不要用相对路径猜。

6. 模型验证与进阶:从跑通到能落地

6.1 对验证结果做一次置信度阈值调优

训练完成后,runs/detect/train/weights/ 下会有 best.pt 和 last.pt。best.pt 是根据验证集 mAP 选出来的最优权重,推理时默认用它:

yolo predict \ model=runs/detect/train/weights/best.pt \ source=datasets/aeroplane/images/val \ conf=0.25 \ iou=0.45

conf 是置信度阈值,iou 是 NMS 的 IoU 阈值。conf 设高会漏掉小目标,设低会多出误检框,实际做法是先跑一遍 val 看混淆矩阵,找到「飞机被分成背景」和「背景被分成飞机」的交叉点,再决定把 conf 调到 0.15 还是 0.4。无人机或高空俯视的小飞机通常得调到 0.15 左右才不漏检,iou 同时降到 0.3 防止密集飞机框被合并吞掉。

6.2 导出 ONNX 和复用训练流程

如果要把模型部署到 Jetson 或服务端,导出成 ONNX 比直接跑 PyTorch 模型更快:

yolo export model=runs/detect/train/weights/best.pt format=onnx dynamic=True

导出后再用 ONNX Runtime 推理,延迟能压到几十毫秒级。另外这套从 xml 转 txt、划分数据集、写 data.yaml、训练、看混淆矩阵的流程可以完整复用到你自己的单类项目,只要把源数据换成自己的标注格式,class_id 写 0 这一个关键点不变。我第一次拿这个数据集练手时没做标签校验,训练到一半突然 device-side assert triggered,翻遍训练日志才发现是有个 txt 里宽高算成了负数。从那以后我每转一次数据都强制走一遍越界扫描和文件名配对检查,训练前花五分钟,省下排查一整天的时间。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询