
简介面向无人机目标检测与视觉感知任务的数据集基于一万张原始航拍图片制作统一标注为无人机类别并按训练、验证、测试三个子集划分数量分别为三千一百五十五、三千九百零六、两千九百三十九模型实测正确识别率可达百分之九十五点七。压缩包约七百三十五兆包含两千个PASICAL VOC XML标注文件坐标框信息完整便于后续转换为YOLO、COCO JSON等常用格式。数据划分与标签字段完整可直接用于目标检测模型训练、算法效果对比、航拍目标识别演示等场景。目前已有二百三十七人学习对需要搭建无人机感知演示项目、完成毕业设计或开展科研试验的开发者是一份现成可用的标注数据基础可显著减少数据准备时间。1. 10000张PASCAL VOC XML无人机数据集先看清“95.7%”是怎么来的做无人机目标检测的人拿到“10000张原始无人机图片、PASCAL VOC XML格式标注、识别率95.7%”这套数据时第一反应通常是兴奋第二反应才是冷静这个95.7%到底是准确率、召回率还是mAP实测场景能不能复现这批数据的价值不在标注格式本身而在“原始”两个字——采集后没做过筛选大量背景、遮挡、模糊、反光都留在里面比清洗过的数据集更接近真实部署环境。它适合正在做电力巡检、违建识别、车辆检测、目标计数但又缺一套带规范标注的航拍数据来冷启动的工程团队。如果你指望把XML直接扔进训练脚本就能拿到95.7%我劝你先花半小时看完第2章的标注结构。2. 拆开10000张无人机数据集JPEGImages、Annotations、ImageSets 各管什么2.1 三件套目录一张原图对应一个 XML别再把标注文件搞混PASCAL VOC 格式的数据集目录结构几乎固定这套一万张的无人机数据也不例外。拿到压缩包后先不要急着训练第一步是确认目录是否完整├── JPEGImages/ # 10000张jpg原始航拍图 │ ├── 000001.jpg │ ├── 000002.jpg │ └── ... ├── Annotations/ # 10000个同名xml标注文件 │ ├── 000001.xml │ ├── 000002.xml │ └── ... └── ImageSets/ └── Main/ ├── train.txt # 训练集图片列表不带扩展名 ├── val.txt # 验证集图片列表 ├── trainval.txt # 训练验证合并列表 └── test.txt # 测试集列表可能没有这里的核心规则是JPEGImages 里的000001.jpg对应 Annotations 里的000001.xml文件名一一对应扩展名不同。ImageSets/Main 下的 txt 文件每一行是图片名不带.jpg后缀。很多人第一步就栽在这里——用glob.glob(*.jpg)遍历图片后想当然地拼出xml_path img_path.replace(.jpg, .xml)结果在 Windows 与 Linux 混用环境下因为文件大小写不一致或者路径分隔符问题导致大量 XML 找不到。这批数据是“原始”采集所以图片往往不止 1920x1080 一种分辨率无人机不同飞行高度、不同云台角度拍出来的图宽高比差异很大。VOC 格式里每张图片的尺寸是写进 XML 内部的不是靠代码读图片再猜的。因此任何解析脚本都必须以 XML 里的 size 字段作为坐标归一化的基准不要用cv2.imread()返回的实际宽高去替换它除非你能确认二者完全一致。2.2 读透一个标注文件 到 的逐层含义打开任意一个 XML 标注文件结构大概是这样的annotation folderJPEGImages/folder filename000001.jpg/filename pathD:/uav_dataset/JPEGImages/000001.jpg/path source databaseUAV Dataset/database /source size width1920/width height1080/height depth3/depth /size segmented0/segmented object namecar/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin100/xmin ymin200/ymin xmax180/xmax ymax260/ymax /bndbox /object /annotationsize字段里的 width 和 height 是这张图片的原始宽高所有坐标都必须在这个坐标系下解释。object节点是重复出现的——一张图里有几个目标就有几个object。name是类别名bndbox里是矩形框的左上角xmin, ymin和右下角xmax, ymax坐标单位是像素不是百分比。三个容易被忽略的字段truncated表示目标是否在图像边缘被截断difficult表示目标是否存在严重遮挡或极小、极不清晰。PASCAL VOC 官方的评估规则默认不算入 difficult 目标但如果你在自己训练时不做过滤difficult 目标也会参与 loss 计算最终表现为验证集 mAP 和“官方宣称的95.7%”对不上。pose字段在无人机俯视视角下基本没有意义它本身是 PASCAL VOC 为行人、车辆侧视图设计的。解析时还有一个隐蔽点VOC 的 bndbox 坐标理论上是整数但部分标注工具会输出浮点数或者出现 xmin xmax 这种退化框。不要默认数据就是干净的解析脚本里必须加一层坐标合法性校验。3. 用 Python 正确解析 VOC XML从 xml.etree 到可视化验证3.1 选择解析工具为什么我不用正则而是用 ElementTreeVOC XML 的层级相对固定有人图省事直接用正则去提取 bndbox 里的数字。这种做法在标注工具版本单一、文件从未被其他软件改动时能跑通但只要 XML 里多了一个属性或者xmin /xmin之间有换行和缩进正则就崩给你看。XML 是结构化数据正确做法是用解析器按节点读取Python 标准库里xml.etree.ElementTree完全够用零第三方依赖不会引入 lxml 的编译安装问题。打开和编辑这些 XML 文件时同样不建议用 IDE 的自动格式化功能。IDEA 或 VS Code 的 XML 格式化会自动重排缩进、甚至改写空行导致 git diff 时每一行都显示为改动后续做标注版本对比时全是噪音。我一般用 VS Code 安装 XML Tools 插件看树形结构或者直接用 Notepad 打开只做局部修改不做全文件格式化。3.2 一段能直接跑的解析脚本读坐标、读类别、统计目标数写一个通用的解析函数作为后续转 YOLO、可视化、数据清洗的统一入口import os import glob import xml.etree.ElementTree as ET def parse_voc_xml(xml_path): 解析 PASCAL VOC 单张标注文件 返回: {image_path:..., width:..., height:..., objects:[...]} tree ET.parse(xml_path) # 文件损坏时这里会抛 ParseError root tree.getroot() base_dir os.path.dirname(xml_path) filename root.findtext(filename) image_path os.path.join(base_dir, filename) size root.find(size) width int(size.findtext(width)) height int(size.findtext(height)) objects [] for obj in root.findall(object): # 遍历所有目标框 name obj.findtext(name).strip() bndbox obj.find(bndbox) xmin float(bndbox.findtext(xmin)) ymin float(bndbox.findtext(ymin)) xmax float(bndbox.findtext(xmax)) ymax float(bndbox.findtext(ymax)) # 坐标合法性校验框不能退化或超出图像范围 if xmin xmax or ymin ymax: print(f[warning] 退化框 {xml_path}: {name} [{xmin}, {ymin}, {xmax}, {ymax}]) continue if xmin 0 or ymin 0 or xmax width or ymax height: print(f[warning] 越界框 {xml_path}: {name} [{xmin}, {ymin}, {xmax}, {ymax}]) objects.append({ name: name, bbox: [xmin, ymin, xmax, ymax], truncated: int(obj.findtext(truncated) or 0), difficult: int(obj.findtext(difficult) or 0) }) return { image_path: image_path, width: width, height: height, objects: objects } xml_dir Annotations stats {} for xml_file in sorted(glob.glob(os.path.join(xml_dir, *.xml))): parsed parse_voc_xml(xml_file) for obj in parsed[objects]: stats[obj[name]] stats.get(obj[name], 0) 1 print(类别统计:, stats)逻辑说明先用ET.parse把整个 XML 载入内存并构建 ElementTree再通过getroot()拿到根节点。root.find(size)只匹配直接子节点VOC 的 size 是 annotation 直接子节点所以没问题但 xmin 是 bndbox 的子节点不是 object 的直接子节点不能直接obj.findtext(xmin)必须先obj.find(bndbox)再往下找。参数说明int(obj.findtext(truncated) or 0)这行的目的是防空值——有些标注工具不会写 truncated 字段findtext返回 None直接 int() 会报错用or 0兜底。float()而不是int()是因为少部分工具会输出小数点坐标全程浮点处理更稳妥。脚本最后的警告信息不要直接忽略退化框和越界框的比例是这个数据集质量的第一个体检指标。3.3 可视化验证把框画回原图这一步不能省解析脚本返回的数字再漂亮也不如眼睛看得见。Visualization 是验证标注与图像是否对齐的最后一关尤其无人机俯拍图里目标往往又小又多坐标差几个像素在数字上看不出来画到图上立刻现形import cv2 import random def draw_boxes(image_path, parsed, output_path): img cv2.imread(image_path) if img is None: print(f[error] 图片读取失败: {image_path}) return img_h, img_w img.shape[:2] if img_w ! parsed[width] or img_h ! parsed[height]: print(f[warning] XML尺寸与图片不符 {image_path}: fxml{parsed[width]}x{parsed[height]}, img{img_w}x{img_h}) for obj in parsed[objects]: xmin, ymin, xmax, ymax [int(v) for v in obj[bbox]] color (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), color, 2) label obj[name] if obj[difficult]: label _diff cv2.putText(img, label, (xmin, max(ymin - 8, 0)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) cv2.imwrite(output_path, img) # 抽查前20个文件 xml_files sorted(glob.glob(Annotations/*.xml))[:20] for xml_file in xml_files: parsed parse_voc_xml(xml_file) out_path vis_ os.path.basename(parsed[image_path]) draw_boxes(parsed[image_path], parsed, out_path)逻辑说明cv2.imread读出 BGR 图像shape返回的是 H×W×C所以与 XML 里的 width 比较时要写成img_w ! parsed[width]不要写反。画框时cv2.rectangle的坐标参数是左上角和右下角与 VOC 的 bndbox 定义完全一致直接贴过去即可。参数说明random.randint(0, 255)给每个类别随机生成颜色只是为了肉眼区分不同目标框不是固定色板max(ymin - 8, 0)是为了防止目标在图像顶部时文字画出边界OpenCV 的 putText 坐标如果为负会导致崩溃。抽查数量建议覆盖 train、val、test 三个集合而不是只挑前几个文件名——顺序往往与难易程度无关。4. 把 VOC XML 转成 YOLO 训练格式目录组织与关键参数设定4.1 为什么最终还是要转 YOLO txt免去运行时反复解析 XML训练时直接对 XML 做在线解析可行但没必要。YOLO 系列训练管线默认读取class_id x_center y_center width height的纯文本格式每行一个目标运行时解析 XML 需要在 DataLoader 里频繁调用xml.etree每 epoch 都重读一遍白白浪费 IO还会把 XML 解析失败的异常带到训练循环里让排查变得困难。更关键的是 YOLO 需要归一化坐标而 VOC 是绝对像素坐标逐样本换算不如在预处理阶段一次性完成。因此实际操作上分为两步先把整个 Annotations 目录转成同等数量的 txt 文件再按 train.txt / val.txt 的名单把图片和 txt 分别复制到images/train、images/val、labels/train、labels/val。注意是“复制”而不是“移动”原始 VOC 目录要保留后续可视化、复盘、改标注还要用。4.2 VOC 转 YOLO 的坐标归一化公式和一条命令脚本归一化公式只有四条x_center ((xmin xmax) / 2) / widthy_center ((ymin ymax) / 2) / heightw (xmax - xmin) / widthh (ymax - ymin) / heightimport os import glob import xml.etree.ElementTree as ET # 按实际数据集的类别顺序填写顺序就是训练时的 class_id CLASS_NAMES [car, person, building, tree] def voc_xml_to_yolo_txt(xml_path, out_dir): tree ET.parse(xml_path) root tree.getroot() width int(root.find(size).findtext(width)) height int(root.find(size).findtext(height)) base os.path.splitext(os.path.basename(xml_path))[0] txt_path os.path.join(out_dir, base .txt) with open(txt_path, w) as f: for obj in root.findall(object): name obj.findtext(name).strip() if name not in CLASS_NAMES: print(f[skip] 不在类别清单中: {name} {xml_path}) continue cls_id CLASS_NAMES.index(name) bndbox obj.find(bndbox) xmin float(bndbox.findtext(xmin)) ymin float(bndbox.findtext(ymin)) xmax float(bndbox.findtext(xmax)) ymax float(bndbox.findtext(ymax)) x_center ((xmin xmax) / 2) / width y_center ((ymin ymax) / 2) / height w (xmax - xmin) / width h (ymax - ymin) / height # 防止归一化后出现负数或超过1的值 x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) w min(max(w, 0.0), 1.0) h min(max(h, 0.0), 1.0) f.write(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}\n) os.makedirs(labels_all, exist_okTrue) for xml_file in glob.glob(Annotations/*.xml): voc_xml_to_yolo_txt(xml_file, labels_all)逻辑说明输出的 txt 每行对应一个目标框第一列是类别 id后四列是归一化后的中心点坐标和宽高。f{x_center:.6f}保留 6 位小数足够——在 1920 宽度的图上6 位小数的精度达到 0.00002 像素量级完全不会因为精度丢失导致框偏移。参数说明CLASS_NAMES的顺序决定了 class_id一旦训练开始就不要改动否则已生成的 txt 全部作废。类别清单的排序不是按字母而是按你训练时 data.yaml 里 names 的顺序。越界框与退化框在这个阶段要处理掉但不能在循环里直接break否则一张图里一个坏框会丢掉整张图所有有效目标正确做法是continue跳过坏框保留同一张图里的其他合法框。4.3 训练前必调的三个参数imgsz、batch、epochs 的取舍转好格式后在 YOLOv8 或 YOLOv11 里训练。一份最小可用的 data.yaml 长这样path: /data/uav_dataset train: images/train val: images/val nc: 4 names: [car, person, building, tree]训练命令yolo detect train datadata.yaml modelyolov8s.pt epochs100 imgsz640 batch16关键参数按优先级说imgsz640是默认值但无人机俯视场景目标往往只有 20×20 像素640 下小目标会缩到 10×10 以下特征几乎丢失。如果有显存余量我一般先试imgsz1024mAP 通常能涨 2~4 个点代价是训练时间翻倍、显存占用明显上升。imgsz 和 batch 是此消彼长的关系二者乘积约等于单卡显存上限。batch16在单卡 24GB 显存、imgsz640 时比较稳batch 太小比如 4会让训练后期 loss 波动大BatchNorm 统计不稳定batch 太大则单 epoch 时间过长不划算。如果显存不够优先降 imgsz 而不是降 batch。epochs100对 10000 张图来说基本够模型收敛。判断标准不是 train_loss而是 val_loss 是否进入平台期。100 epoch 后 val_loss 还在稳定下降可以继续训练已经震荡或回升就是过拟合早停比盲目加 epoch 更有效。训练时的确认点观察前几个 epoch 的 loss 曲线是否正常下降同时打开训练日志里的标注预览图确认 Mosaic 增强后的样本没有出现标注框与图像错位。如果训练集图片里有大量带 EXIF 旋转信息的 JPG转 YOLO 时 OpenCV 读图会自动忽略 EXIF 旋转而原图在查看器里是旋转过的这会导致框看起来歪掉——这是无人机相机常见问题后续会单独讲。5. 无人机标注数据集的 5 个避坑点从坐标越界到图片损坏5.1 现象训练到一半报 XML ParseError又定位不到坏文件训练循环里偶发报错xml.etree.ElementTree.ParseError: not well-formed (invalid token)但报错信息没有明确指出是哪个文件。逐个人工检查 XML 不可能10000 个文件靠肉眼翻不现实。原因最常见是标注工具在输出时把写成了裸符号比如类别名出现RDXML 规范里必须写作amp;解析器遇到裸就会报错。第二个常见原因是有人在 Excel 或 WPS 里打开 XML 后保存导致编码或转义字符被改写。解决写一段全量扫描脚本遍历 Annotations 目录用ET.parse逐个解析捕获ParseError并打印文件路径。扫描通过后再跑训练而不是等训练随机崩溃。这类问题在“原始数据集”里尤其常见——发布者自己可能也没用解析器跑过完整校验只看了几个样例。5.2 现象mAP 整体 0.9无人机小目标却几乎全漏模型在大目标上表现极好平均 mAP 很高但实际跑无人机航线时车辆、行人这种 20×20 像素的小目标几乎检测不到。回看验证集发现拉高 mAP 的是少数几个大尺寸建筑目标。原因mAP 是所有类别、所有尺寸目标的平均值大目标框面积大、IOU 容易达标会在统计上掩盖小目标召回率低的问题。另一个原因是 Mosaic 增强在 imgsz640 时会把目标缩小 4 倍原本 20×20 的小目标缩成 5×5直接变成噪声。解决不要只看整体 mAP用yolo detect val输出的 per-class AP 表逐个看重点看车辆、行人这类小目标类别的 AP50。训练时把 Mosaic 的 scale 参数调低或对小目标类别做离线过采样把包含小目标的图片复制 2~3 份加入训练集。如果数据里的小目标类别本身框数就少单独给它们加 oversample 权重比调全局增强参数更直接。5.3 现象train.txt 与 val.txt 出现同图评估分数虚高训练完的 mAP 高得离谱接近 99%部署时表现和训练指标完全对不上。检查发现 train.txt 和 val.txt 中有大量重复图片。原因这个数据集的 ImageSets/Main 是发布时按顺序划分的比如前 8000 张为 train后 2000 张为 val。如果原始图片按航迹连续采集同一目标可能出现在相邻好几张图片里切分时又没有做序列去重导致训练集与验证集存在高相似度甚至同一目标的多个视角评估指标自然好看但换一条新航线就现原形。解决训练前写个脚本对 train.txt 和 val.txt 做交集校验打印重复项。更严格的方案是按拍摄时间或航迹 ID 做分组划分保证同一架次的数据只出现在一个集合。如果数据集没有提供这些元信息退而求其次的做法是检查相邻序号图片的感知哈希相似度把相似度大于阈值的图片从验证集中剔除。5.4 现象类别框数差 100 倍模型只学大目标统计 10000 张图的框数分布后发现车辆类有 8 万多个框船只类只有 300 多个框模型对船只目标的 AP 接近 0。这不是网络结构的问题是类别不均衡的问题。原因目标检测的 loss 默认按 batch 内所有框平均计算多数类别主导了梯度方向少数类别的框数量太少对 loss 的贡献微乎其微训练几个 epoch 后网络直接忽略它。解决先按框数统计做可视化柱状图框数少于 2000 的类别就要单独处理。最简单的是离线复制粘贴增强——从原图中把少数类目标抠出来贴到没有该目标的纯净背景图上同时生成对应的 YOLO txt 标注把类别框数拉到与多数类同量级。注意粘贴时目标不能出边界缩放不能超过原目标尺寸的 30%否则模型学到的不是目标特征而是粘贴痕迹。另一个办法是在 loss 层用类别权重YOLOv8 的cls参数可以配合自定义权重调整但工程上最稳的还是数据层面先均衡。5.5 现象jpg 损坏或 XML 缺失让训练中断且没法续训训练到第 30 个 epoch突然报FileNotFoundError: xxx.jpg或者cv2.imread返回 NoneDataLoader 直接崩溃。重新启动训练时又从零开始前面的 30 个 epoch 白跑。原因数据集传输或解压过程中部分图片损坏jpg 文件头还在但数据不完整OpenCV 读到一半返回 None。也有可能是图片名在 Windows 解压时被截断或自动改名导致 XML 里记录的 filename 与实际文件对不上。解决训练前做一次完整性校验脚本遍历所有 txt 标注确认对应图片存在且cv2.imread不为 None同时检查图片尺寸与 XML size 字段是否一致。校验通过后再训练。训练时要开启 checkpoint 保存YOLO 默认每 50 epoch 存一次可以手动改成save_period10脚本崩溃后从最近的 checkpoint 用resumeTrue续训不要每次都从头跑。6. 复现 95.7% 之前设好验证边界mAP50 自测与小目标增强技巧6.1 自测命令用 val.txt 跑一次 mAP50 与 mAP50-95拿到这套数据后先用官方划分好的 val.txt 做一次基准测试yolo detect val modelruns/detect/train/weights/best.pt datadata.yaml splitval这里要看两个指标mAP50和mAP50-95。mAP50 是 IoU 阈值 0.5 下的平均精度框大概框住目标就算对mAP50-95 是把 IoU 阈值从 0.5 每隔 0.05 提升到 0.95再取平均对框的精准度要求高得多。标题里的“95.7%”绝大多数情况下指的是 mAP50这个数字很漂亮但放到 mAP50-95 上通常只有 60%~75%二者不是同一个量级。验证时还要看results.csv里每一类的 AP尤其关注车辆、行人这类小目标类别如果某一类的 AP50 低于 50%整体 mAP 再高也不能直接上线。6.2 进阶针对无人机小目标的三项增强无人机场景和普通地面视角不一样目标小、密度高、背景复杂常规增强策略要按参数针对性调整增强项默认值无人机场景建议原因Mosaic scale1.00.3~0.5防止小目标被缩到 5×5 像素以下MixUp 比例0.2~0.40.1~0.2MixUp 会让小目标更模糊降低召回随机裁剪无RandomCrop 后 resize 回 imgsz等效放大小目标提升 AP50Mosaic 和 MixUp 都是从 YOLO 训练管线里直接调的增强参数。Mosaic 拼四张图并随机缩放无人机小目标本来只有 20 像素再被 Mosaic 缩小 4 倍就真的没了把 scale 调低到 0.3 能保证小目标在拼接后仍有 10 像素以上。随机裁剪不是 YOLO 自带的增强要自己在数据集层面做——对包含小目标的图片做中心区域裁剪并放大模拟无人机降低高度后的视角对提升小目标 AP 非常有效。我自己的习惯是训练前先跑一遍数据校验脚本再跑一次完整解析和可视化抽查最后才动手训练——从标注入手的项目80% 的坑都出在数据本身而不是模型。复现 95.7% 之前先确认自己的验证划分和评估口径跟它的定义一致否则这个数字只能当宣传值看。希望帮到你。本文还有配套的精品资源点击获取