
简介本资源为面向无人机视觉研究与目标检测开发者的无人机数据集适用于无人机目标检测、跟踪、地图构建等场景的模型训练与算法验证尤其适合需要PASCAL VOC XML格式标注数据的中高级深度学习实践者。压缩包共2000个文件以XML标注文件为主对应10000张原始无人机图片整体约735MB标注内容可直接用于YOLO、COCO json及PASCAL VOC xml等多种主流框架的转换与训练。数据集按train、validation、test划分分别为3155张、3906张和2939张标签聚焦drone类别结构清晰便于直接接入训练流程。目前已有237人学习下载正确识别率可达95.7%可为无人机感知能力研究、目标检测模型调优及导航控制算法实验提供扎实的数据支撑帮助读者快速复现高精度检测结果并开展对比实验。1. 一万张无人机图片的数据集到底能拿来做什么手里有一万张原始无人机图片标注格式是 PASCAL VOC XML官方口径说正确识别率能到 95.7%。这个数字放在技术群里第一反应通常是「真的假的」第二反应是「我拿它训什么」。我一开始也这么想直到自己拿一批航拍图跑了一遍检测流程才发现这类数据集真正的价值不在那个百分比而在于它把「数据采集」这个最耗人力的环节替你省掉了。无人机视角的图像和普通地面拍摄完全是两回事。目标小、背景杂、光照变化剧烈同一类目标在 50 米高度和 120 米高度拍出来像素占比能差一个数量级。一万张这个量级意味着你有足够的样本去覆盖高度、角度、天气的组合而不是靠几十张图硬调参。适合谁用做航拍目标检测的算法工程师、需要快速验证模型结构的在校研究者、以及想拿真实数据练手检测全流程的开发者。下面我按「先搞清楚数据长什么样再跑通训练最后处理坑」的顺序讲。2. 先看清 VOC XML 里到底存了什么2.1 VOC XML 的字段结构和无人机场景的对应关系PASCAL VOC 格式的标注文件本质是一个 XML核心信息全在object节点里。一个典型的无人机图片标注长这样annotation folderdrone_images/folder filenameDJI_0042.jpg/filename size width4000/width height3000/height depth3/depth /size object namevehicle/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin1520/xmin ymin980/ymin xmax1685/xmax ymax1102/ymax /bndbox /object /annotationsize里的宽高是原图尺寸bndbox的四个坐标是绝对像素值不是归一化值。这一点和 YOLO 的 txt 格式有本质区别转换时必须先读原图尺寸再做除法。truncated表示目标是否被图像边缘截断difficult表示目标是否难以识别——这两个字段在无人机数据里出现频率比常规数据集高得多因为航拍图里目标经常只露出一半或者被树冠遮挡。我一般拿到数据集先做一件事统计每个类别的实例数量和 bndbox 的宽高分布。无人机图片里小目标占比往往超过 60%如果 bndbox 宽度中位数低于 40 像素那模型的主干网络和输入分辨率就得重新考虑。2.2 用脚本做一次数据体检在训练之前跑一个统计脚本能省掉后面大量返工。下面这段代码遍历所有 XML输出类别分布和框尺寸分布import os import xml.etree.ElementTree as ET import numpy as np from collections import defaultdict def audit_voc_dataset(anno_dir): 统计VOC数据集的类别分布和边界框尺寸 class_count defaultdict(int) box_widths [] box_heights [] missing_size 0 for xml_file in os.listdir(anno_dir): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(anno_dir, xml_file)) root tree.getroot() # 读取原图尺寸用于后续归一化校验 size_node root.find(size) if size_node is None: missing_size 1 continue img_w int(size_node.find(width).text) img_h int(size_node.find(height).text) for obj in root.findall(object): name obj.find(name).text class_count[name] 1 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) w xmax - xmin h ymax - ymin box_widths.append(w) box_heights.append(h) # 越界检查坐标超出原图范围说明标注有问题 if xmax img_w or ymax img_h or xmin 0 or ymin 0: print(f[越界] {xml_file}: ({xmin},{ymin},{xmax},{ymax}) vs {img_w}x{img_h}) print(类别分布:, dict(class_count)) print(f框宽度 中位数{np.median(box_widths):.1f} 均值{np.mean(box_widths):.1f}) print(f框高度 中位数{np.median(box_heights):.1f} 均值{np.mean(box_heights):.1f}) print(f小目标占比(宽40px): {sum(1 for w in box_widths if w 40)/len(box_widths)*100:.1f}%) print(f缺失size字段的文件数: {missing_size}) audit_voc_dataset(./annotations)这段脚本做了三件事统计类别分布、计算框尺寸的中位数和均值、检查坐标越界。越界检查特别重要VOC 标注是人工画的一万张图里出现几十个越界框很正常如果不提前清理训练时数据增强的裁剪操作会直接报错或者产生无效样本。小目标占比这个指标决定了你后面要不要用 mosaic 增强、要不要把输入分辨率拉到 1280 以上。2.3 划分训练集时别用随机划分常规做法是 8:2 或 7:3 随机划分但无人机数据有个坑同一架次拍摄的图片高度相似随机划分会导致训练集和验证集里出现几乎一样的图验证指标虚高。我一般按拍摄架次或者按时间戳分组划分保证验证集里的场景在训练集里没出现过。如果数据里没有架次信息可以用图片的 EXIF 或者文件名前缀做粗略分组。具体操作上先按文件名前缀聚类再把整个簇分到训练集或验证集而不是逐张随机。这样验证集上的 mAP 才是真实泛化能力而不是记忆能力。3. 从 VOC 转到 YOLO 格式转换脚本和四个边界坑3.1 转换的核心逻辑YOLO 格式要求每行是class_id x_center y_center width height全部归一化到 0-1。转换时最容易翻车的地方是坐标归一化的基准——必须用原图宽高不能用网络输入尺寸。下面这个脚本处理了类别映射、坐标归一化和越界裁剪import os import xml.etree.ElementTree as ET def voc_to_yolo(anno_dir, output_dir, class_list): anno_dir: VOC XML 所在目录 output_dir: YOLO txt 输出目录 class_list: 类别名列表顺序即 class_id os.makedirs(output_dir, exist_okTrue) class_to_id {name: i for i, name in enumerate(class_list)} for xml_file in os.listdir(anno_dir): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(anno_dir, xml_file)) root tree.getroot() size_node root.find(size) img_w int(size_node.find(width).text) img_h int(size_node.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_to_id: continue # 跳过不在目标类别表里的标注 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) # 裁剪到图像范围内防止越界坐标产生负值或超过1 xmin max(0, min(xmin, img_w)) xmax max(0, min(xmax, img_w)) ymin max(0, min(ymin, img_h)) ymax max(0, min(ymax, img_h)) # 过滤掉宽高为0的无效框 if xmax xmin or ymax ymin: continue 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 lines.append(f{class_to_id[name]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) txt_name os.path.splitext(xml_file)[0] .txt with open(os.path.join(output_dir, txt_name), w) as f: f.write(\n.join(lines)) voc_to_yolo(./annotations, ./labels, [vehicle, person, building])关键参数说明class_list的顺序决定了 class_id必须和训练时的 data.yaml 完全一致顺序错了模型学出来的类别就是乱的。坐标保留 6 位小数足够YOLO 内部会再处理。裁剪到图像范围这一步不能省我见过标注员把框画到图像外面去的不裁剪的话归一化后会出现大于 1 的值训练时 loss 直接变 NaN。3.2 四个必须检查的边界情况第一个坑是图片和标注文件名不匹配。VOC 数据集里经常出现 XML 存在但对应图片缺失或者反过来。转换完必须做一次交叉校验用集合差集找出孤儿文件。第二个坑是类别名大小写不一致。同一个类别在不同 XML 里写成Vehicle和vehicle如果不做统一会被当成两个类。转换前先跑一遍set(name)把所有类别名收集起来看一眼。第三个坑是difficult字段的处理。VOC 标准里difficult1的样本在评估时应该被忽略但 YOLO 格式没有这个字段。我的做法是训练时保留这些样本增加数据多样性但评估时单独记录它们的表现避免被拉低指标。第四个坑是图片格式。无人机原始图可能是 RAW 或者大尺寸 JPEG训练前统一转成 RGB 的 JPEG 或 PNG同时记录转换后的尺寸。如果原图是 4000x3000直接训练显存吃不消通常缩放到 640 或 1280但缩放后小目标可能只剩几个像素这时候要考虑切图训练。4. 训练配置让 95.7% 这个数字在你手里复现4.1 输入分辨率和 batch size 的取舍95.7% 这个识别率大概率是在特定输入分辨率下测出来的。无人机小目标多输入分辨率直接决定小目标能不能被检测到。我做过对比同一批数据640 输入下小目标召回率比 1280 低了将近 20 个百分点。但 1280 的显存占用是 640 的四倍左右batch size 得往下压。常见做法是先用 640 跑通流程确认数据管道没问题再切到 1280 做正式训练。如果显存不够用梯度累积模拟大 batch。下面是一个典型的训练配置# data.yaml path: ./drone_dataset train: images/train val: images/val nc: 3 names: [vehicle, person, building]# 训练命令以常见检测框架为例 python train.py \ --data data.yaml \ --img 1280 \ --batch 8 \ --epochs 150 \ --weights pretrained.pt \ --cache ram \ --rect--img 1280是输入分辨率--batch 8在 1280 下对显存要求较高不够就降到 4 并开启梯度累积。--cache ram把图片缓存到内存一万张图如果都是小尺寸内存够的话能显著加速。--rect开启矩形训练减少 padding对长宽比差异大的航拍图有帮助。4.2 数据增强里哪些该开哪些该关无人机数据的增强策略和常规数据集不一样。水平翻转可以开垂直翻转要谨慎——航拍图里天空和地面的位置是固定的垂直翻转会产生不自然的样本。Mosaic 增强对小目标检测帮助很大它把四张图拼成一张变相增加了小目标的上下文。但 mosaic 会让目标尺寸进一步缩小如果原图小目标已经很小mosaic 后可能只剩一两个像素反而有害。我的经验是小目标占比超过 60% 时mosaic 的概率从默认的 1.0 降到 0.5同时把缩放范围收窄。HSV 增强可以开航拍图光照变化大色调和饱和度扰动能提升鲁棒性。旋转增强要小心航拍图的目标朝向本身就有随机性过度旋转可能让模型学到错误的朝向先验。4.3 验证指标怎么看才不被 95.7% 忽悠mAP0.5 到 95.7% 是有可能的但要看在什么条件下。如果验证集和训练集来自同一架次、同一高度这个数字参考价值有限。我一般会额外看三个指标小目标面积小于 32x32的单独召回率、不同高度分层的 mAP、以及混淆矩阵里类间混淆的情况。具体操作上在验证脚本里按 bndbox 面积把样本分成 small/medium/large 三组分别算 AP。如果 small 组的 AP 比 large 组低 30 个点以上说明模型对小目标还有明显短板这时候该考虑切图训练或者换更强的特征金字塔结构而不是继续调学习率。5. 避坑与排查训练不收敛、指标虚高、显存爆炸5.1 现象loss 从第一轮就是 NaN原因通常是标注里有非法值。VOC 转 YOLO 时如果没做越界裁剪归一化坐标可能出现负数或大于 1某些框架的损失函数对这类值没有保护直接产生 NaN。另一个可能是类别 id 超出了nc定义的范围。解决转换脚本里强制裁剪坐标到 [0, 1]并且在训练前跑一遍校验脚本检查所有 txt 文件里的坐标范围和 class_id 最大值。发现异常文件直接打印文件名人工确认后修正或剔除。5.2 现象训练集 loss 正常下降验证集 mAP 一直不涨最常见的原因是训练集和验证集分布不一致。如果随机划分验证集里可能有大量训练集没见过的场景模型泛化不过来。反过来如果按架次划分后验证集 mAP 很低说明模型确实没学到通用特征。排查步骤先可视化验证集的预测结果看是漏检多还是误检多。漏检多说明召回不够检查输入分辨率和小目标增强误检多说明背景干扰大检查负样本比例和置信度阈值。另一个隐蔽原因是类别不平衡某个类别样本极少模型直接偏向多数类。这时候用类别加权损失或者对少数类做过采样。5.3 现象训练到一半显存突然爆了如果用的是动态 batch 或者可变尺寸输入某张特别大的图可能撑爆显存。无人机原始图尺寸不统一有的 4000x3000有的 5472x3648如果 dataloader 没做统一缩放就会出这个问题。解决在 dataloader 里强制 resize 到固定尺寸或者用--rect配合最大边长限制。另外检查--cache ram是否把原图全加载进内存了一万张 4000x3000 的图缓存起来需要几十 GB 内存内存不够会触发 swap表现像是卡死。5.4 现象转换后的标签和图片对不上VOC 的 XML 里filename字段和实际文件名不一致的情况很常见尤其是数据集经过多次拷贝和重命名之后。如果转换脚本用 XML 里的 filename 去匹配图片就会找不到文件。解决转换时以 XML 文件名去掉扩展名为准去匹配图片而不是用filename字段。转换完做一次完整性校验确保每个 txt 都有对应的图片每个图片都有对应的 txt。5.5 现象95.7% 的指标在自己数据上复现不出来先确认三件事输入分辨率是否一致、验证集划分方式是否一致、评估代码的 IoU 阈值是否一致。这三个任何一个不同指标都能差出十几个点。另外如果原数据集的 95.7% 是在包含训练集的验证集上测的那这个数字本身就没有参考意义。我一般只信在自己划分的、场景不重叠的验证集上跑出来的数字。6. 把一万张图用出两万张的效果切图训练与难例挖掘一万张无人机图片如果直接缩到 640 训练小目标信息损失严重。我后来固定用的一套做法是切图训练把每张原图切成 2x2 或 3x3 的子图每个子图单独作为训练样本标注框按切分位置重新计算。这样一万张图能变成四万到九万张训练样本小目标的像素占比大幅提升。切图的关键参数是重叠率。不重叠切分会导致目标被切断我一般设 20% 重叠保证边界处的目标至少完整出现在一张子图里。切图后要重新做一次数据体检确认没有产生宽高为 0 的无效框。def split_image_with_overlap(img, boxes, tile_size1280, overlap0.2): img: numpy array, HWC boxes: list of [xmin, ymin, xmax, ymax, class_id] tile_size: 子图边长 overlap: 重叠比例 h, w img.shape[:2] stride int(tile_size * (1 - overlap)) tiles [] for y in range(0, h, stride): for x in range(0, w, stride): x2 min(x tile_size, w) y2 min(y tile_size, h) x1 max(0, x2 - tile_size) y1 max(0, y2 - tile_size) tile img[y1:y2, x1:x2] tile_boxes [] for box in boxes: bx1, by1, bx2, by2, cls box # 计算框在子图中的新坐标 nx1 max(bx1, x1) - x1 ny1 max(by1, y1) - y1 nx2 min(bx2, x2) - x1 ny2 min(by2, y2) - y1 # 保留有效框且要求框在子图内面积占比超过30% if nx2 nx1 and ny2 ny1: inter (nx2 - nx1) * (ny2 - ny1) orig (bx2 - bx1) * (by2 - by1) if inter / orig 0.3: tile_boxes.append([nx1, ny1, nx2, ny2, cls]) tiles.append((tile, tile_boxes)) return tiles这段代码的核心逻辑是按 stride 滑动窗口切图每个框重新计算在子图中的坐标只保留在子图内面积占比超过 30% 的框。占比阈值可以调设太低会引入大量残缺目标设太高会丢失边界目标。20% 重叠配合 30% 面积阈值是我试过比较平衡的组合。切图训练的代价是推理时也要切图并且要把子图的检测结果映射回原图坐标再做 NMS。这一步容易出重复框因为重叠区域的目标会被多个子图检测到。NMS 的 IoU 阈值要比常规低一些我一般用 0.4 而不是默认的 0.5。另一件事是难例挖掘。训练完第一轮后把验证集上漏检和误检的样本挑出来人工确认标注是否有问题然后把确认过的难例加入训练集再训一轮。一万张图里通常能挑出几百张难例对指标的提升比盲目加数据明显得多。最后说个习惯我每次拿到新数据集第一件事永远是跑数据体检脚本而不是直接开训。标注里的问题在训练前发现改一行代码就能解决训练后发现可能要重跑好几天。这个习惯帮我省下的时间比任何调参技巧都多。希望帮到你。本文还有配套的精品资源点击获取