无人机目标检测数据集:VOC/YOLO/COCO格式转换与YOLOv8训练全攻略
简介面向无人机视觉研究与AI应用开发者一套完整的无人机图片数据集已整理上线适合目标检测、目标跟踪、地图构建等深度学习任务。资源包含10000张原始无人机图片标注类别为drone支持PASCAL VOC XML、YOLO、COCO JSON三种常见格式能够适配不同深度学习框架的数据接口。ZIP压缩包内共2000个XML标注文件整体约735MB数据已划分train3155张、validation3906张、test2939张三部分目录结构清晰方便按照训练、验证、测试流程直接调用。通过分析和处理这批数据可研究无人机的飞行特性、感知能力与导航控制算法进而提升无人机的性能与可靠性基于该数据训练的检测模型正确识别率可达95.7%能有效减少数据采集和人工标注所需时间。目前已有237人学习下载适合计算机视觉入门者及无人机方向研究者直接使用清晰的划分和多种标注格式也便于横向对比不同模型的训练效果降低复现实验的额外成本。1. 无人机数据集10000 张原图与三种标注格式直接喂给 YOLO 的目标检测训练集做反无人机目标检测的时候很多人第一反应是找现成的无人机数据集结果要么是国外公开集只有两三百张要么是视频抽帧之后标注混乱根本喂不动模型。这份无人机数据集一共 10000 张原始图片全部是单类别 drone 标注同时给出 PASCAL VOC XML、YOLO txt、COCO JSON 三套格式train 3155 张、validation 3906 张、test 2939 张已经按目录分好标称识别率 95.7%。适合目标检测入门、反无人机监控和低空安防项目预研也适合拿来对比 YOLOv8 这类模型在无人机小目标上的收敛速度。XML 是标准 VOC 结构每条 object 都带 bndbox拿到手就能解析不需要额外清洗三套格式里 YOLO txt 直接喂给 Ultralytics 训练COCO JSON 给 MMDetection 用省去自己攒数据集的半个月时间。2. XML 解析与格式转换PASCAL VOC、YOLO、COCO JSON 三套标注怎么切换拿到手第一步不是急着训练而是确认标注格式和你选的训练框架能不能对上。这份数据里的 XML 文件名形如3423_jpg.rf.b0ce580...xmlrf 加 hash 后缀是标注平台导出时留下的特征说明数据链路是「原始图片 → 平台标注 → 三格式导出」。PASCAL VOC 是三套格式里信息最完整的YOLO txt 是训练最常用的COCO JSON 是 MMDetection、Detectron2 这些框架的首选。我的习惯是先写一个解析器把 VOC XML 读成结构化字典再从这个字典往 YOLO 和 COCO 转让三套格式共享同一个数据源避免改一处漏一处。转换前先做两个数学校验XML 文件数量应该等于图片数量三个目录图片数加起来要等于 10000。这份数据 3155 3906 2939 10000能对上。如果对不上多半是导出时漏了标签或图片被重复拷贝这种问题在转换脚本跑之前就要查清楚否则后面所有统计都白做。2.1 PASCAL VOC XML 解析从声明到 bndbox 的字段语义XML 文件本质是纯文本想人工看标注对不对用 VS Code 或 Notepad 直接打开就行想批量改必须写脚本按字段改不要用文本编辑器手改几百个文件。每个 XML 第一行是?xml version1.0 encodingutf-8?这一行声明了编码后面解析报错有一半概率和它有关。根节点是annotation下面挂着 filename、size 和一组 object每个 object 里有 name 和 bndboxbndbox 里是 xmin、ymin、xmax、ymax 四个坐标size 节点里的 width、height 是图片原始宽高转 YOLO 归一化时必须拿它当分母不能拿训练输入尺寸算。节点路径字段含义这份数据集里的典型值/annotation/filename图片文件名3423_jpg.rf.b0ce58.jpg/annotation/size/width图片宽度像素640 或更大/annotation/size/height图片高度像素640 或更大/annotation/object/name类别名drone/annotation/object/bndbox目标框xmin / ymin / xmax / ymax用 Python 标准库的 xml.etree.ElementTree 就能解析不需要装 lxml只做格式转换和清洗的话ElementTree 足够快也不引入额外依赖import xml.etree.ElementTree as ET def parse_voc_xml(xml_path: str): 把单个 VOC XML 解析成结构化字典 tree ET.parse(xml_path) root tree.getroot() record { filename: root.find(filename).text, width: int(root.find(size/width).text), height: int(root.find(size/height).text), objects: [], # 一张图可能同时出现多个 drone } for obj in root.findall(object): name obj.find(name).text box obj.find(bndbox) # 用 int(float(...)) 而不是 int(...)部分导出工具会把坐标写成 12.0 record[objects].append({ name: name, xmin: int(float(box.find(xmin).text)), ymin: int(float(box.find(ymin).text)), xmax: int(float(box.find(xmax).text)), ymax: int(float(box.find(ymax).text)), }) return record解析细节里有三个小坑。size/width 这种多层路径可以直接写在 find() 里ElementTree 支持用斜杠表示子节点路径bndbox 坐标用 int(float(...)) 双保险因为有的标注工具导出小数坐标直接 int() 会抛 ValueError一张 640x640 的图可能标了多个目标所以 objects 设计成列表而不是单元素。解析后建议顺手把 filename、宽高、框数量打印出来扫一遍空标注的 XML、宽高为 0 的 XML 在这一步就能暴露不用等到训练时报错。2.2 YOLO txt 与 COCO JSON 的转换坐标归一化与 id 映射YOLO txt 是每行一个目标的五元组类别 id、归一化中心点 x、归一化中心点 y、归一化宽、归一化高。注意它存的是中心点和宽高不是 VOC 的左上右下所有值都归一化到 0~1跟图片实际分辨率解耦。COCO JSON 反过来bbox 用的是绝对像素[x, y, w, h]还要求 area 和 iscrowd 字段。三个格式的差异集中在坐标表示和 id 管理上转换脚本里最容易错的就是这两处。格式文件组织坐标表示类别 id 规则常见消费方PASCAL VOC XML每图一个 .xmlxmin/ymin/xmax/ymax 像素字符串类别名LabelImg、自写训练YOLO txt每图一个 .txt归一化中心点 宽高0 起顺序编号Ultralytics YOLOCOCO JSON单一 .json[x, y, w, h] 像素字典中 id 从 1 起MMDetection、Detectron2VOC 转 YOLO 是最常用的路径单类别数据集里 CLASS_NAMES 只有一个元素但建议还是维护成列表后续加类别时不会改坏脚本import os CLASS_NAMES [drone] # 按训练顺序维护加新类别时保持稳定 def voc_to_yolo(xml_path: str, out_dir: str, xml_root: str None): record parse_voc_xml(xml_path) w, h record[width], record[height] lines [] for obj in record[objects]: cls_id CLASS_NAMES.index(obj[name]) x_center (obj[xmin] obj[xmax]) / 2 / w y_center (obj[ymin] obj[ymax]) / 2 / h box_w (obj[xmax] - obj[xmin]) / w box_h (obj[ymax] - obj[ymin]) / h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) base os.path.splitext(record[filename])[0] with open(os.path.join(out_dir, base .txt), w, encodingutf-8) as f: f.write(\n.join(lines))归一化的关键是分母必须用 XML 里 size 的原始宽高如果图片训练前做 resizeYOLO 的 dataloader 会按同一比例缩放标签所以这一步不用提前缩。但如果某个框的 xmax 已经大于原图宽度先做裁剪再归一化否则转出来的中心点会偏移。批量跑的时候加一个 main 入口遍历 annotions 目录即可if __name__ __main__: import glob for xml_path in sorted(glob.glob(annotations/*.xml)): voc_to_yolo(xml_path, labels/)VOC 转 COCO 的差异在 id 管理。COCO 要求 images、annotations、categories 三段每个 annotation 必须挂 image_id 和 category_idcategory 的 id 从 1 开始一张图多个目标时 annotation id 要自增不能复用。这部分逻辑不难但字段多、容易漏import json def voc_to_coco(xml_paths, out_json_path): images, annotations [], [] current_ann_id 1 for img_id, xml_path in enumerate(xml_paths, start1): rec parse_voc_xml(xml_path) images.append({ id: img_id, file_name: rec[filename], width: rec[width], height: rec[height], }) for obj in rec[objects]: x obj[xmin] y obj[ymin] bw obj[xmax] - obj[xmin] bh obj[ymax] - obj[ymin] annotations.append({ id: current_ann_id, image_id: img_id, category_id: 1, bbox: [x, y, bw, bh], area: bw * bh, iscrowd: 0, }) current_ann_id 1 coco { images: images, annotations: annotations, categories: [{id: 1, name: drone}], } with open(out_json_path, w, encodingutf-8) as f: json.dump(coco, f, ensure_asciiFalse)提示这段代码踩过的坑是 iscrowd 忘了写。MMDetection 读数据时缺了 iscrowd 会在 filter 阶段报 KeyError而且错误信息只带 image_id不带文件路径定位很费劲。生成 JSON 后立刻 json.load 回来校验images 长度等于 xml 数量每个 annotation 的 image_id 都能在 images 里找到category_id 不超过 categories 长度。3. 用 YOLOv8 复现 95.7%目录组织、训练命令与验证指标解读数据转换完下一步是把 YOLO txt 和图片按框架要求的目录结构摆好。Ultralytics YOLOv8 的数据集契约是每个集合一个目录目录下分 images 和 labels 两个子目录同名图片和标签用相同 basename 配对data.yaml 里给出路径即可。这个契约不满足训练过程会静默跳过缺失标签的图片最终 mAP 低得莫名其妙。3.1 目录组织与 data.yaml先解决路径不一致问题推荐目录结构如下validation 目录名不要图省事改成 val除非你同时改 yaml 里的指向datasets/drone/ ├── data.yaml ├── train/ │ ├── images/ # 3155 张 │ └── labels/ # 3155 个 .txt ├── validation/ │ ├── images/ # 3906 张 │ └── labels/ └── test/ ├── images/ # 2939 张 └── labels/data.yaml 这样写path: ./datasets/drone # 相对 yaml 文件所在目录的根路径 train: train/images val: validation/images test: test/images nc: 1 names: - dronepath 是基准路径train/val/test 都相对于它写Ultralytics 用 val 关键字指向验证集test 是可选的只有显式指定 splittest 时才用nc 必须和 CLASS_NAMES 长度一致names 的顺序就是类别 id 的映射顺序。这份数据集的验证目录名是 validation 而不是 valyaml 里必须写 validation写成 val 会直接提示数据集为空。配好 yaml 后先做一次配对检查不要直接开训import glob imgs {p.split(/)[-1][:-4] for p in glob.glob(datasets/drone/train/images/*)} lbls {p.split(/)[-1][:-4] for p in glob.glob(datasets/drone/train/labels/*)} print(只有图片无标签:, len(imgs - lbls)) print(只有标签无图片:, len(lbls - imgs))这段代码用 basename 去掉扩展名后做集合差。只有标签无图片的 txt 会让 YOLO 在训练时找不到对应图像只有图片无标签的图会被当成纯背景单类别数据集里这两种错误都会把 mAP 拉低。注意[:-4]假定扩展名是 .jpg/.png/.txt如果混入 .jpeg切片长度要改成 5。3.2 训练命令与超参选择s/m 模型的取舍与 epochs 设定单类别无人机检测我一般从 yolov8s 起步而不是 yolov8n。n 更快但小目标召回率差一截10000 张的数据量喂 s 模型刚好m 模型如果没有 24G 以上显存容易在 640 输入下跑不动。具体命令yolo detect train \ datadatasets/drone/data.yaml \ modelyolov8s.pt \ epochs80 \ patience20 \ imgsz640 \ batch16 \ lr00.01 \ optimizerauto \ projectruns/drone \ nameexp1参数选择逻辑model 用 yolov8s.pt 是拿 COCO 的预训练权重做迁移收敛速度比随机初始化快很多epochs 设 80配合 patience20 早停val mAP50 连续 20 轮不涨就停避免无效训练batch 取决于显存16G 显存跑 s 模型 640 输入没问题8G 就降到 8优先保证不 OOMlr0 用 0.01 是配合 optimizerauto 的常见做法模型越大 lr0 越小。训练时盯着metrics/mAP50(B)这一列看不要只看 loss——单类别的 box loss 下降不代表检测框位置真的对了。3.3 验证指标解读mAP50、mAP50-95 与 95.7% 的含义训练结束后 runs/drone/exp1 下会生成 weights/best.pt 和 last.ptbest 是 val mAP50 最高的权重不是最后一个 epoch。用 best 跑验证yolo detect val \ modelruns/drone/exp1/weights/best.pt \ datadatasets/drone/data.yamlUltralytics 会输出 precision、recall、mAP50、mAP50-95 四个核心数同时把逐 epoch 曲线写进 results.csv。标称的 95.7% 在数据集描述里一般指 mAP50也就是 IoU 阈值 0.5 时的平均精度不是 top-1 准确率更不是 F1。单类别数据集里 precision 和 recall 的差异更直观precision 高说明误检少recall 高说明漏检少95.7% 对应的 precision/recall 组合才是部署时真正关心的东西。读 results.csv 验证复现结果import pandas as pd df pd.read_csv(runs/drone/exp1/results.csv) last df.iloc[-1] # 最后一个 epoch 的行 print(mAP50:, round(last[metrics/mAP50(B)], 4)) print(precision:, round(last[metrics/precision(B)], 4)) print(recall:, round(last[metrics/recall(B)], 4))注意mAP 是模型在所有置信度阈值下 precision-recall 曲线下面积的平均所以验证阶段改 conf 不会改变 mAP 数值只会影响实际推理时的误报率。如果复现值和标称差超过 2 个点先检查 val 路径是否指到了 test 目录再检查 imgsz 是否一致mAP50 对输入分辨率敏感640 和 960 的结果能差 3~5 个点。4. 避坑指南XML 解析失败、坐标越界与划分异常这一章是血泪经验。数据集的 XML 是机器导出的机器导出意味着格式统一但也意味着坑藏在细节里编码、文件名、坐标边界、划分比例每一个都能让训练结果悄悄变差。下面五条按出现频率排前两条在转换阶段就会爆后三条要训练完才看得见。4.1 XML 解析报 not well-formed现象ET.parse(xml_path) 直接抛 xml.etree.ElementTree.ParseError报错只有一行 syntax error看不出是哪个文件。原因导出工具写 XML 时文件头带了 UTF-8 BOM部分解析器把 BOM 当普通字符处理少数 XML 里 filename 含或中文空格没转义直接违反 XML 语法。解决按 bytes 读取并忽略非法字节后再 ET.fromstring单个文件坏了就跳过并记录不要中断整批转换import xml.etree.ElementTree as ET def safe_parse(xml_path: str): with open(xml_path, rb) as f: raw f.read().decode(utf-8, errorsignore) try: return ET.fromstring(raw) except ET.ParseError as e: print(f解析失败已跳过: {xml_path} - {e}) return Noneerrorsignore 会悄悄丢掉非法字节代价是如果非法字节出现在坐标值里解析出来的数字可能偏小所以记录跳过列表比硬解析更安全转换完拿图片数量去对上一步的数学校验。4.2 bndbox 坐标越界或为 0现象转 YOLO 后训练时日志出现 WARNING: skipping label with box out of bounds或者 mAP50 异常低但 loss 曲线很漂亮。原因标注工具允许把标注框拖出图片边界导出时坐标没裁剪还有的图在标注后被统一 resize 过XML 还记录着旧尺寸。解决归一化前按原图宽高做 clamp宽或高小于 1 个像素的框直接丢弃def clamp_box(xmin, ymin, xmax, ymax, w, h): xmin max(0, min(xmin, w - 1)) ymin max(0, min(ymin, h - 1)) xmax max(xmin 1, min(xmax, w - 1)) ymax max(ymin 1, min(ymax, h - 1)) return xmin, ymin, xmax, ymax这里用w - 1而不是w因为像素坐标范围是 0 到 width-1xmax 等于 width 时框的右边界已经落在图像外面。clamp 之后如果 xmax xmin 或 ymax ymin这个框就是退化框直接过滤掉。4.3 图片文件名与 XML 的 filename 对不上现象把 XML 里的 filename 拼到图片目录下发现文件不存在批量配对大量失败。原因XML 里记录的是带 hash 后缀的导出名比如3423_jpg.rf.b0ce58.jpg而实际图片可能是下载后自己重命名成3423.jpg的版本。解决先按 basename 去后缀匹配匹配不上的再从 XML 读 filename 批量给图片重命名让图片迁就 XMLimport glob, os, shutil import xml.etree.ElementTree as ET img_dir, xml_dir images, annotations for xml_path in glob.glob(os.path.join(xml_dir, *.xml)): root ET.parse(xml_path).getroot() fname root.find(filename).text if os.path.exists(os.path.join(img_dir, fname)): continue base fname.split(_jpg)[0] # 去掉 _jpg.rf.xxx 后缀保留原始编号 for ext in (jpg, jpeg, png): cand os.path.join(img_dir, f{base}.{ext}) if os.path.exists(cand): shutil.copy(cand, os.path.join(img_dir, fname)) break这个方案保证 XML 里的 filename 与实际文件最终一致之后再按目录划分。split(_jpg)[0]是启发式处理遇到原始文件名里本身带_jpg的要多看几条日志确认没误伤。4.4 验证集比训练集还大划分比例的反直觉现象这份数据 train 只有 3155 张validation 却有 3906 张和常规「训练集最大」的直觉相反。原因导出时的默认随机划分没有按来源控制比例素材本身分布也不均匀落到哪个目录纯看运气不是刻意设计。解决如果训练曲线里 val mAP 长期高于 train 的拟合趋势说明数据分布有偏。我一般会把 train 和 validation 合并后按 8:1:1 重新洗牌划分而不是硬用导出比例test 保持不动用来做最终的独立测试。4.5 连续帧重复导致的数据泄漏现象val 的 mAP50 到 0.95换到 test 或新采集的视频上掉到 0.85差距大得离谱。原因数据源头大概率是视频抽帧相邻帧内容高度相似train 和 validation 各分到了同一段画面的前后帧模型在验证时等于见过答案。解决划分前先对图片做 perceptual hash 去重或者按文件名中的序列号分桶每 10 帧取 1 帧代表再分配。做完这一步val 和 test 的差距才会收敛到真实水平不做的话95.7% 只是自欺欺人的数字。5. 识别率复现与坏例分析mAP 怎么算、FP/FN 怎么追标称 95.7% 能不能在自己机器上复现出来取决于三件事用的哪个权重、跑哪个集合、imgsz 是多少。这一章按复现路径走一遍顺便把单类别检测的坏例分析讲清楚。5.1 用 results.csv 复现标称识别率先看训练时自动生成的 results.csv最后一个 epoch 的 metrics/mAP50(B) 是 val 集上的复现值。如果想在 test 集上再验证一次Ultralytics 的 val 命令支持指定 splityolo detect val \ modelruns/drone/exp1/weights/best.pt \ datadatasets/drone/data.yaml \ splittestsplittest 的前提是 data.yaml 里配了 test 字段这份数据集配了所以能直接跑。复现时如果数值对不上优先查 imgsz 和权重选择同一个权重在不同输入尺寸下的 mAP50 差异明显这也是很多人「复现不出标称值」的第一大原因。5.2 混淆矩阵与坏例单类别的 FP/FN 怎么定位val 跑完会在 runs/drone/exp1 下生成 confusion_matrix.png。单类别检测看这张图只关心两个区域真实无人机被分成背景也就是漏检背景被分成无人机也就是误检。漏检通常是目标太小或严重遮挡误检通常是树枝、塔吊、飞鸟这类外观和无人机轮廓接近的东西。把测试图片批量预测出来人工扫一遍yolo predict \ modelruns/drone/exp1/weights/best.pt \ sourcedatasets/drone/test/images \ conf0.25 \ saveTrue \ save_txtTruepredict 生成目录里的 txt 记录了每张图的预测框、类别和置信度配合原图看能快速统计出 FP 集中在什么场景是天空背景的云层还是地面建筑的塔吊。这一步看起来笨但比盯着 loss 曲线瞎调参有效得多。5.3 独立盲测别让测试集变成验证集test 集如果反复用来调阈值它就变成了第二验证集最终指标被「测」得虚高。我的做法是从 test 集里抽出 100 张没参与过任何调试的图用 LabelImg 重新标一遍只标 drone存成独立的 blind_set 目录最后跑一次yolo detect val \ modelruns/drone/exp1/weights/best.pt \ datablind_set.yaml这 100 张只在最后跑一次跑完就锁起来不再碰。标称 95.7% 是数据集的起点盲测数字才是敢写进交付报告的指标两者差距如果超过 5 个点说明数据划分里还有泄漏回头检查 4.5 那一节。6. 部署前的小目标增强切片推理与 TTA 把指标搬上实拍画面训练集里很多无人机占据的画面比例很小这在反无人机场景里很常见——实拍画面中目标可能只有 20x20 像素。640 输入下小目标经过多次下采样后特征几乎消失所以部署到实拍大图前我一般会做切片推理把 1920x1080 原图切成 640x640 的块重叠 20%逐块推理后再把框映射回原图坐标最后做全局 NMS 合并。import numpy as np def slide_crop(img, tile640, overlap0.2): 大图切 tile返回 (x, y, patch) 列表 h, w img.shape[:2] step int(tile * (1 - overlap)) patches [] for y in range(0, h - tile 1, step): for x in range(0, w - tile 1, step): patches.append((x, y, img[y:y tile, x:x tile])) return patches图边缘不够一个 tile 时直接丢弃别补零补零会让边缘出现假目标。推理完把每个框加上 tile 的 (x, y) 偏移再用置信度排序做 NMSIoU 阈值 0.5。如果 GPU 有余量predict 时加--augment开 TTA多尺度和翻转推理能再提 1~2 个点召回代价是推理时间翻倍只在离线分析里用。切片推理对 640 输入训练出来的模型提升非常稳定这是我用过最值的部署技巧。从那以后每次拿无人机数据集我都会强制走一遍「XML 解析 → YOLO 转换 → 目录校验 → 训练 → 测试集盲测 → 切片推理验证」这条链路确认数据没有泄漏、指标真实再谈往项目里集成。这套数据集的 XML 标注和划分好的 train/validation/test 目录都在下载包里把 data.yaml 的 path 改成自己机器上的绝对路径就能复现 95.7%。希望帮到你。本文还有配套的精品资源点击获取