飞机检测数据集实战:VOC转YOLO格式与训练避坑指南
简介这份飞机数据集面向计算机视觉初学者与目标检测开发者用于训练和验证单类别飞机识别模型可服务于遥感图像分析、航空器检测等场景。资源共2986张jpg图片每张均配有对应的Pascal VOC格式xml标注与YOLO格式txt标注标注类别仅airplane一类累计标注框5129个全部由labelImg工具以矩形框方式完成标注准确合理。压缩包为7z格式约379.91MB内含2000个文件其中1999个xml标注文件与1个说明txt图片与标注一一对应方便直接接入YOLO或VOC训练流程。目前已有737人学习下载适合需要快速获取单类别检测数据、验证模型训练流程或进行迁移学习实验的读者可省去自行采集与标注的时间成本。1. 飞机数据集2986张VOCYOLO格式从拿到手到跑通第一条检测结果手上拿到一个标注好的飞机数据集2986张图同时给了VOC和YOLO两套格式这件事本身就说明它大概率是从Pascal VOC体系里转出来的或者作者为了兼容不同训练框架特意做了双份标注。飞机检测在目标检测里算是个经典但不算简单的场景目标尺度跨度大停机坪上的飞机可能只占几十个像素跑道上的大飞机又能撑满半张图背景干扰强航站楼、廊桥、地面标线、甚至云层阴影都容易被误检再加上不同光照、不同拍摄角度模型很容易在某个子集上翻车。这个数据集能解决的核心问题就是让你不用从零标注就能直接进入模型训练和调参环节适合想快速验证YOLO系列算法、做飞机检测demo、或者拿它当目标检测入门练手的人。2986张这个量级不算大单卡消费级显卡就能跑但也不算小足够看出过拟合和泛化之间的差别。接下来我会按实际落地顺序把格式转换、环境配置、训练参数、验证排查这几个环节拆开讲中间会提到VOC和YOLO两种格式在目录结构和标注文件上的本质差异以及为什么很多人拿到双格式数据集反而更容易踩坑。2. 先搞懂VOC和YOLO格式到底差在哪目录结构、坐标表示与类别映射2.1 VOC的XML结构一张图一个文件坐标是绝对像素值Pascal VOC格式的核心是Annotations目录下每张图对应一个XML文件文件名和图片名一致只是扩展名不同。XML里记录了图片尺寸、目标类别和边界框坐标。边界框用xmin、ymin、xmax、ymax四个值表示单位是像素原点是左上角。一个XML里可以有多个object节点每个节点对应一个目标。这种格式的好处是可读性强用任何文本编辑器都能看坏处是解析慢训练时如果每次读图都去解析XMLIO开销会很明显。常见做法是先把XML转成统一的中间格式比如COCO的JSON或者YOLO的TXT再喂给训练框架。annotation folderimages/folder filenameplane_0001.jpg/filename size width1024/width height768/height depth3/depth /size object nameplane/name bndbox xmin212/xmin ymin145/ymin xmax489/xmax ymax376/ymax /bndbox /object /annotation上面这个XML里图片是1024×768飞机框的左上角在(212,145)右下角在(489,376)。注意VOC的坐标是1-based还是0-based在不同工具里处理不一致有些标注工具从1开始计数有些从0开始转换时如果不统一框会整体偏移一个像素小目标上这个偏移可能直接让IoU掉几个点。2.2 YOLO的TXT结构一行一个目标坐标是归一化中心点加宽高YOLO格式要求每张图对应一个TXT文件TXT里每一行代表一个目标格式是类别索引 中心点x 中心点y 宽度 高度。所有数值都归一化到0到1之间也就是除以图片的宽和高。类别索引从0开始对应一个固定的类别列表通常放在classes.txt或者data.yaml里。这种格式解析快直接按行读就行但可读性差脱离图片尺寸就看不懂框在哪。# YOLO格式TXT示例假设类别0是plane 0 0.342 0.339 0.270 0.301这行表示类别0中心点在图片宽度34.2%的位置高度33.9%的位置框宽占图片宽度的27%框高占图片高度的30.1%。换算回像素中心点大约是(350,260)宽约276高约231和上面VOC的框基本对得上。实际转换时浮点精度会带来微小误差但通常不影响训练。2.3 双格式数据集常见的目录组织与类别映射陷阱拿到“VOCYOLO格式”的数据集常见的目录结构有两种一种是分开放VOC一套目录YOLO一套目录图片可能共用也可能各存一份另一种是混在一起Annotations放XMLlabels放TXTimages放图片再用两个不同的data配置文件分别指向。不管哪种第一件事是确认类别列表是否一致。VOC的XML里写的是类别名YOLO的TXT里写的是类别索引如果classes.txt的顺序和XML里类别名出现的顺序不一致训练出来的模型会把飞机认成别的类。我一般会先跑一个脚本统计所有XML里出现过的类别名再和YOLO的classes.txt逐行对比确认索引映射关系。import os import xml.etree.ElementTree as ET from collections import Counter voc_dir Annotations yolo_classes classes.txt # 统计VOC里所有类别名 voc_classes Counter() for xml_file in os.listdir(voc_dir): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(voc_dir, xml_file)) for obj in tree.findall(object): name obj.find(name).text.strip() voc_classes[name] 1 # 读取YOLO类别列表 with open(yolo_classes, r) as f: yolo_list [line.strip() for line in f if line.strip()] print(VOC类别统计:, voc_classes) print(YOLO类别列表:, yolo_list) # 检查是否一致 voc_set set(voc_classes.keys()) yolo_set set(yolo_list) if voc_set ! yolo_set: print(类别不一致VOC独有:, voc_set - yolo_set) print(YOLO独有:, yolo_set - voc_set) else: print(类别集合一致继续检查顺序映射)这段脚本先遍历所有XML用Counter统计每个类别名出现的次数同时读取YOLO的classes.txt。如果两个集合不一致说明数据集本身有问题需要先修正如果集合一致但顺序不同就要根据YOLO的索引顺序重新生成TXT不能直接用XML里的类别名去查索引。参数上voc_dir指向Annotations目录yolo_classes指向类别文件运行后看输出即可。这一步花不了几分钟但能避免后面训练完发现类别全错的血泪教训。3. 把VOC转成YOLO可用的TXT转换脚本、坐标归一化与四个边界坑3.1 转换脚本的核心逻辑与完整代码转换的核心就三步读XML拿到图片宽高和每个object的框把绝对坐标转成归一化的中心点加宽高按类别名查索引写入TXT。下面这个脚本可以直接抄作业处理单目录下的所有XML输出到labels目录。import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, classes, output_dir): 将单个VOC XML转换为YOLO TXT xml_path: XML文件路径 classes: 类别名列表索引即类别索引 output_dir: 输出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.strip() if name not in classes: print(f警告: {xml_path} 中类别 {name} 不在类别列表中跳过) continue cls_id classes.index(name) bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) # 计算归一化中心点和宽高 x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h width (xmax - xmin) / img_w height (ymax - ymin) / img_h # 裁剪到0-1范围防止标注越界 x_center max(0.0, min(1.0, x_center)) y_center max(0.0, min(1.0, y_center)) width max(0.0, min(1.0, width)) height max(0.0, min(1.0, height)) lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}) # 写入TXT文件名和XML一致扩展名换成txt base_name os.path.splitext(os.path.basename(xml_path))[0] out_path os.path.join(output_dir, base_name .txt) with open(out_path, w) as f: f.write(\n.join(lines)) # 使用示例 classes [plane] # 根据实际类别修改 xml_dir Annotations out_dir labels os.makedirs(out_dir, exist_okTrue) for xml_file in os.listdir(xml_dir): if xml_file.endswith(.xml): voc_to_yolo(os.path.join(xml_dir, xml_file), classes, out_dir)脚本里对坐标做了裁剪把归一化后的值限制在0到1之间。这一步很多人会忽略但实际标注里经常出现框超出图片边界的情况比如xmax大于图片宽度不裁剪的话YOLO训练时可能报错或者产生异常梯度。类别索引直接用classes.index(name)获取所以classes列表的顺序必须和训练时data.yaml里的names顺序完全一致。输出文件名保持和XML同名只是扩展名换成txt这样图片、XML、TXT三者的对应关系一目了然。3.2 四个边界坑越界框、空TXT、文件名不匹配、类别名大小写第一个坑是越界框。上面脚本虽然做了裁剪但裁剪后框的宽高可能变成0这种无效框应该直接跳过而不是写入TXT。可以在计算完宽高后加一个判断如果width或height小于一个极小值比如1e-6就continue。第二个坑是空TXT。有些图片可能没有标注任何目标对应的XML里没有object节点转换后TXT是空文件。YOLO训练时遇到空TXT会当作负样本这本身没问题但如果你的数据集里负样本比例过高模型会偏向于预测背景。建议统计一下空TXT的数量如果超过总图片数的10%就要考虑是否保留这些图片。第三个坑是文件名不匹配。图片是jpgXML是xmlTXT是txt三者的基础名必须完全一致。实际数据里经常出现图片叫plane_001.jpgXML叫plane_1.xml这种不一致会导致训练时找不到标注。转换前先用脚本检查一遍文件名对应关系。import os img_dir images xml_dir Annotations txt_dir labels img_names set(os.path.splitext(f)[0] for f in os.listdir(img_dir) if f.endswith(.jpg)) xml_names set(os.path.splitext(f)[0] for f in os.listdir(xml_dir) if f.endswith(.xml)) txt_names set(os.path.splitext(f)[0] for f in os.listdir(txt_dir) if f.endswith(.txt)) print(图片无XML:, img_names - xml_names) print(XML无图片:, xml_names - img_names) print(TXT无图片:, txt_names - img_names)第四个坑是类别名大小写。VOC里可能写的是PlaneYOLO的classes.txt里写的是plane直接index查不到。统一转成小写再比较或者在classes列表里同时保留大小写变体。我一般会在转换前把所有类别名统一成小写避免这种玄学问题。4. 在PyCharm里配YOLO训练环境从创建虚拟环境到跑通第一个epoch4.1 创建虚拟环境与安装依赖PyCharm里配YOLO环境第一步是建虚拟环境。打开PyCharmFile - New Project选择Pure PythonLocation里指定项目目录Python Interpreter选New environmentLocation默认在项目下.venvBase interpreter选你系统里的Python 3.8到3.10之间的版本。YOLOv5和YOLOv8对Python版本要求不完全一样v5支持3.7以上v8建议3.8以上但3.11在某些依赖上会有兼容问题稳妥起见用3.9或3.10。建好环境后打开TerminalPyCharm会自动激活虚拟环境。安装PyTorch去PyTorch官网查对应CUDA版本的安装命令比如CUDA 11.8用pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118如果只用CPU训练去掉index-url直接装默认版本。然后装YOLO框架以ultralytics为例pip install ultralyticsultralytics会自动装numpy、opencv-python、pillow等依赖。装完后在PyCharm的Python Console里跑import torch print(torch.__version__) print(torch.cuda.is_available())如果cuda.is_available()返回False检查显卡驱动和CUDA版本是否匹配。这一步翻车最多很多人装完发现用的是CPU在跑训练速度差几十倍。4.2 准备data.yaml与目录结构YOLO训练需要一个data.yaml文件指定训练集、验证集路径和类别信息。假设目录结构如下dataset/ images/ train/ val/ labels/ train/ val/ data.yamldata.yaml内容path: /home/user/dataset train: images/train val: images/val nc: 1 names: [plane]path是数据集根目录train和val是相对path的路径nc是类别数names是类别名列表。注意names的顺序必须和TXT里的类别索引一致。如果之前转换时classes列表是[plane]这里names也必须是[plane]。训练集和验证集的划分比例常见是8:2或9:12986张图按8:2分训练集约2388张验证集约598张。划分时用随机种子固定保证每次划分一致。import os import random import shutil random.seed(42) img_dir images/all train_img_dir images/train val_img_dir images/val train_lbl_dir labels/train val_lbl_dir labels/val os.makedirs(train_img_dir, exist_okTrue) os.makedirs(val_img_dir, exist_okTrue) os.makedirs(train_lbl_dir, exist_okTrue) os.makedirs(val_lbl_dir, exist_okTrue) all_imgs [f for f in os.listdir(img_dir) if f.endswith(.jpg)] random.shuffle(all_imgs) split int(len(all_imgs) * 0.8) for i, img_name in enumerate(all_imgs): base os.path.splitext(img_name)[0] src_img os.path.join(img_dir, img_name) src_lbl os.path.join(labels/all, base .txt) if i split: shutil.copy(src_img, os.path.join(train_img_dir, img_name)) shutil.copy(src_lbl, os.path.join(train_lbl_dir, base .txt)) else: shutil.copy(src_img, os.path.join(val_img_dir, img_name)) shutil.copy(src_lbl, os.path.join(val_lbl_dir, base .txt))这段脚本用固定种子42打乱所有图片前80%复制到train后20%复制到val同时把对应的TXT也复制过去。注意图片和TXT必须同步划分不能只分图片不分标注。4.3 启动训练与关键参数解读在PyCharm的Terminal里运行yolo detect train datadataset/data.yaml modelyolov8n.pt epochs100 imgsz640 batch16这行命令用YOLOv8n预训练权重训练100轮输入尺寸640批次大小16。参数含义data指向data.yamlmodel指定预训练权重epochs是训练轮数imgsz是输入图片缩放尺寸batch是每批图片数。如果显存不够把batch降到8或4如果训练集小epochs可以设到200但要注意过拟合。训练过程中看loss曲线box_loss和cls_loss应该整体下降如果震荡剧烈检查学习率默认lr00.01小数据集可以降到0.001。训练完在runs/detect/train/weights/下会生成best.pt和last.ptbest.pt是验证集上表现最好的权重用来做推理。5. 训练完别急着上线飞机检测的排查清单与五个常见翻车点5.1 验证集指标怎么看mAP、precision、recall的取舍训练日志里会输出mAP50、mAP50-95、precision、recall。mAP50是IoU阈值0.5时的平均精度mAP50-95是IoU从0.5到0.95每隔0.05取平均后者更严格。飞机检测里如果小目标多mAP50-95通常比mAP50低不少这是正常的。precision高recall低说明模型保守只检确信度高的recall高precision低说明模型激进误检多。实际部署时根据场景取舍如果漏检代价大调低置信度阈值提高recall如果误检代价大调高阈值提高precision。5.2 五个翻车点现象、原因与解决翻车点一训练loss正常但验证mAP极低。现象是训练集loss降到很低验证集mAP一直在0.1左右。原因通常是data.yaml里names顺序和TXT类别索引不一致模型学的是错误的类别映射。解决方法是重新检查classes.txt和data.yaml的names确保逐行对应。翻车点二小飞机全漏检。现象是停机坪上的小飞机一个都检不出来大飞机正常。原因是输入尺寸640下小目标经过下采样后特征几乎消失。解决方法是提高imgsz到1024或1280或者在数据增强里开启mosaic和copy-paste增加小目标样本。翻车点三航站楼被误检成飞机。现象是背景里的建筑物、廊桥被框成飞机。原因是负样本不足模型没学会区分飞机和相似形状的背景。解决方法是加入含航站楼但不含飞机的负样本图片或者在训练时提高背景类的权重。翻车点四训练到一半loss突然变nan。现象是前几十轮正常突然loss变成nan。原因通常是学习率过高或者某个批次的标注框宽高为0。解决方法是降低lr0并在数据加载时过滤掉宽高小于1e-6的框。翻车点五推理时框位置整体偏移。现象是推理出来的框比实际飞机偏左或偏上。原因是VOC转YOLO时坐标原点处理不一致有些工具用1-based坐标转换时没减1。解决方法是检查转换脚本确保xmin、ymin、xmax、ymax都是0-based或者统一加1再转。6. 把2986张飞机数据集用出更高上限从单模型到多模态验证的进阶思路6.1 用ONNX导出做跨平台验证训练完的best.pt可以导出成ONNX方便在别的推理引擎里跑也能用来验证模型是否过拟合了某个框架。导出命令yolo export modelruns/detect/train/weights/best.pt formatonnx imgsz640导出后在Python里用onnxruntime加载跑一张测试图对比ONNX和PyTorch的输出是否一致。如果差异很大检查导出时的opset版本和输入尺寸。ONNX的好处是可以在没有PyTorch环境的机器上跑比如一些边缘设备。6.2 用多模态做二次校验飞机检测有个特殊之处很多误检来自形状相似的背景比如航站楼的弧形屋顶。单纯靠视觉特征很难完全区分这时候可以引入多模态做二次校验。常见做法是先用YOLO出候选框再把候选框区域裁剪出来送进一个图像分类模型或者CLIP类的多模态模型判断这个区域是不是飞机。这样虽然增加了推理时间但能显著降低误检率。具体实现上可以用YOLO的置信度做第一层过滤置信度低于0.5的直接丢弃0.5到0.8之间的送多模态校验高于0.8的直接输出。这个阈值需要根据实际数据调我一般会在验证集上画precision-recall曲线找F1最大的点作为阈值。6.3 一个具体技巧用验证集错误分析反推标注问题训练完别只看mAP把验证集里所有误检和漏检的图挑出来逐张看。我习惯用脚本把预测结果画在图上保存到一个目录然后快速过一遍。经常发现的问题包括标注框只框了飞机机身没框机翼导致模型学到的飞机不完整同一架飞机被标了多个框模型学到重复检测有些图片里飞机被遮挡严重标注却给了完整框模型学到的是不存在的边界。这些标注问题不修正再怎么调参也上不去。修正标注后重新训练mAP通常能涨3到5个点。这个习惯我保持了几年每次拿到新数据集都先做一轮错误分析比盲目加数据增强有效得多。希望帮到你。本文还有配套的精品资源点击获取