ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

无人机检测数据集实战:YOLO与VOC标注格式转换及训练避坑指南

无人机检测数据集实战:YOLO与VOC标注格式转换及训练避坑指南 简介一套面向空中无人机检测任务的旋翼无人机UAV数据集包含7000多张已标注图片覆盖多种旋翼无人机形态统一采用“drone”单类别标注可直接用于YOLO、SSD、Faster R-CNN等目标检测算法的训练与评估也适合高校计算机视觉课程、无人机巡检项目作为实验数据。压缩包共22675个文件其中7558张jpg原图对应7558个xml标签与7559个txt标签两种标注格式可无缝切换以适配不同检测框架整体体积876.78MB文件按图片序号整齐命名便于快速筛选与批量处理。目前已有1111人学习下载从标注解析、数据划分到模型训练均可据此展开能显著节省数据采集与格式转换时间使研究者集中精力进行检测精度调优与算法对比同时对无人机避障、低空目标识别等应用也有直接参考价值。1. 空中无人机检测数据集7000张旋翼UAV图片能解决什么问题做空中无人机检测的人最容易翻车的地方不是模型选型而是数据集的坑。这份无人机检测数据集一共7000多张标注好的旋翼无人机UAV图片类别只有drone一类视觉目标非常明确凡是画面里出现的旋翼无人机都要用一个框圈出来。和很多开源数据集不一样的是它同时给了txt和xml两种标签格式意味着YOLO系、mmdet系、甚至自己写数据加载器的方案都能直接吃进去不用再从零做格式搬移。适合正在跑目标检测算法、做无人机视觉感知、或者准备用无人机巡航数据做训练的从业者。2. 数据集结构与标注格式txt和xml并存先搞懂再动手拿到压缩包先别急着解压就开训。这个数据集的图片命名像是drone_2_595.jpg同一张图会对应一个.txt和一个.xml。txt是YOLO系训练直接读的归一化坐标xml是Pascal VOC风格给需要bbox标签树的框架用。两个文件存在时间久了容易有一方缺失、坐标越界、类别名大小写不一致的问题我建议先把两套格式的读取逻辑都写出来做一次全量校验再进训练流程。2.1 两份标注各自的读取方式YOLO txt和Pascal VOC xmlYOLO的txt每一行是一组标注格式固定为class x_center y_center width height坐标全部相对于图片宽高做了归一化范围0到1。读取时务必把类别索引和四维浮点分开解析不要在按空格切分时踩到尾部换行符。import os from PIL import Image def read_yolo_txt(txt_path, img_path): with open(txt_path, r) as f: lines f.readlines() w, h Image.open(img_path).size boxes [] for line in lines: parts line.strip().split() cls int(parts[0]) cx, cy, bw, bh map(float, parts[1:]) # 归一化坐标转像素坐标方便画框和评估 x1 int((cx - bw / 2) * w) y1 int((cy - bh / 2) * h) x2 int((cx bw / 2) * w) y2 int((cy bh / 2) * h) boxes.append((cls, x1, y1, x2, y2)) return boxes这里cls直接取整是因为类别索引从0开始。这个数据集只有drone一个类训练时这个值恒为0但如果后续你把其他飞行物并入数据类别索引就不能丢了。Pixel坐标换算后可能超出图片边界比如标注框边缘落在图片外读取时最好顺手做一次clamp否则后面画框或计算IoU容易报错。XML这边用Python自带的xml.etree就行不需要引入额外依赖。VOC XML的关键字段就三个filename、size里的宽高、object里的bndbox。import xml.etree.ElementTree as ET def read_voc_xml(xml_path): root ET.parse(xml_path).getroot() boxes [] for obj in root.iter(object): name obj.find(name).text bndbox obj.find(bndbox) x1 int(bndbox.find(xmin).text) y1 int(bndbox.find(ymin).text) x2 int(bndbox.find(xmax).text) y2 int(bndbox.find(ymax).text) boxes.append((name, x1, y1, x2, y2)) return boxes注意root.iter(object)是遍历所有object节点避免极少数嵌套结构导致漏读。我一般会额外检查difficult字段因为VOC里difficult1的框表示目标极小或者遮挡严重很多框架训练时会自动忽略。不同数据集对这个字段的处理不一致建议先统计一下有多少difficult框再决定是保留还是当噪声剔除。2.2 用脚本把标注信息可视化验证框位置和类别名类别名对不对、框跟不跟得上机体光看数字无法判断。把标注画回原图是第一步也是后面所有调参的基础。import cv2 import os def draw_boxes(img_path, boxes, class_namesNone): img cv2.imread(img_path) for box in boxes: cls, x1, y1, x2, y2 box[:5] label class_names[cls] if class_names else str(cls) color (0, 255, 0) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, label, (x1, y1 - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2) cv2.imwrite(check_ os.path.basename(img_path), img)class_names参数传[drone]即可显示可读类名。实际做可视化时别一张张看图我习惯把同一批抽样的结果拼成一张网格图一眼扫过去就能发现某个框偏移、漏标、或者类别名写成Drone导致大小写不一致。这个阶段发现的问题比训练完再回头查快得多。2.3 文件命名规律与标注完整性核对drone_2_595.jpg这种命名drone是场景前缀2可能是拍摄批次595是帧序号。即使这个数据集里文件本身是完整的你后续自己扩充数据时也建议沿用类似命名至少保留一个能区分拍摄批次的字段否则后面做按批次划分训练集会很难受。标签完整性问题不能只用眼睛扫。写一个脚本遍历所有图片检查同名txt和xml是否存在同时检查txt是否为空文件。空标注文件不算错误但混入训练集后会额外产生无目标的负样本影响训练稳定性。文件内容典型用途.jpg原始图像模型输入.txtYOLO归一化坐标每行一个框YOLO系列直接读取.xmlVOC格式的filename、size、objectmmdet、PaddleDetection等框架import os img_dir images missing [] for f in os.listdir(img_dir): if f.endswith(.jpg): stem os.path.splitext(f)[0] txt_path os.path.join(labels, stem .txt) xml_path os.path.join(annotations, stem .xml) if not os.path.exists(txt_path) or not os.path.exists(xml_path): missing.append(f) print(missing annotation:, len(missing))这段脚本把缺失情况打印出来方便你确认文件数量。7000多张图如果缺失比例超过1%建议先联系数据来源补全不要自行删图硬凑否则类别分布会悄然改变。3. 把数据集跑进目标检测训练流程格式统一与训练集划分这一章解决的是从原始数据集到可直接训练之间的最后一公里。很多人的做法是解压后直接改yaml路径就开训结果训练中途报坐标越界或者验证集mAP虚高得离谱。这里的关键在于格式统一和划分策略。3.1 为什么先统一标签格式YOLO txt与XML标注的转换逻辑同一份标注给两套格式本意是兼容不同框架但两套格式各自维护容易出偏差。比如有人手动改过xml里的框坐标txt没有同步更新。所以最稳妥的做法是选一个格式作为基准再统一导出另一份。常见做法是以txt为基准因为YOLO和大部分现代检测框架最终都吃txt。如果要从VOC XML生成txt核心是坐标映射VOC的xmin、ymin、xmax、ymax是像素坐标YOLO需要的是归一化中心点坐标和宽高。import xml.etree.ElementTree as ET import os def voc_to_yolo(xml_path, out_dir, class_map): root ET.parse(xml_path).getroot() filename root.find(filename).text 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 if name not in class_map: print(f[warn] unknown class: {name}) continue box obj.find(bndbox) x1 float(box.find(xmin).text) y1 float(box.find(ymin).text) x2 float(box.find(xmax).text) y2 float(box.find(ymax).text) # 像素坐标转归一化中心坐标 cx (x1 x2) / 2 / img_w cy (y1 y2) / 2 / img_h bw (x2 - x1) / img_w bh (y2 - y1) / img_h lines.append(f{class_map[name]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) stem os.path.splitext(os.path.basename(xml_path))[0] with open(os.path.join(out_dir, stem .txt), w) as f: f.write(\n.join(lines))class_map传入{drone: 0}即可写6位浮点足够训练时对精度不敏感。转换过程中如果发现x2 - x1为负说明标注框坐标乱序比如xmin大于xmax这种标注要单独拉出来人工核对不能让它静默进入训练集。还有一类坑是XML里size的宽高和图片实际尺寸不一致通常是裁剪或缩放过的图没有同步更新XML转换出来所有框都会偏移。3.2 训练集/验证集划分随机切分不够按批次切分更稳目标检测训练一般把数据按8:2或9:1划分train和val。很多人的第一反应是随机打乱。随机划分对普通图像数据集基本够用但无人机影像有个特殊性——同一批航拍视频抽帧出来的图之间高度相似。如果同一段飞行的相邻帧被同时分进train和val验证指标会虚高真正部署时碰到新场景反而掉点。import os import random import shutil def split_dataset(src_img, src_label, dst_root, train_ratio0.8, seed0): names [f[:-4] for f in os.listdir(src_img) if f.endswith(.jpg)] random.seed(seed) random.shuffle(names) train_n int(len(names) * train_ratio) for split_name, subset in ((train, names[:train_n]), (val, names[train_n:])): img_out os.path.join(dst_root, split_name, images) label_out os.path.join(dst_root, split_name, labels) os.makedirs(img_out, exist_okTrue) os.makedirs(label_out, exist_okTrue) for name in subset: shutil.copy(os.path.join(src_img, name .jpg), os.path.join(img_out, name .jpg)) shutil.copy(os.path.join(src_label, name .txt), os.path.join(label_out, name .txt))seed参数固定随机序列方便复现。随机划分适合先跑通流程验证代码和训练链路没问题。但模型调参阶段我一般会再用按批次划分的方式对比一次先按文件名里的批次字段分组按组切分保证同一批次的图全部落在train或val。这样得到的mAP更接近真实场景的泛化能力。如果要做更严格的时间序列验证还可以按帧间隔抽验证集——每连续100帧取5帧做val其余训练。这种切法对巡检类无人机数据尤其重要因为连续帧之间背景重叠度太高随机划分很容易让模型记住背景而非无人机本身的特征。4. 避坑标好无人机的图为什么训出来还是翻车这个数据集类别单一、标注齐全看起来非常简单但恰恰是这种干净的数据集最容易让人放松警惕。我在实际跑训过程中遇到过几类高频问题每一条都是真实翻过车的按现象、原因、解决的顺序写出来。4.1 误检模型把树枝、远处建筑、甚至鸟当成drone现象训练完在测试视频里跑画面上标出一堆绿框不少框落在树冠边缘、电线杆顶、飞鸟身上。验证集mAP明明不低实际使用却完全不能用。原因这个数据集里无人机目标并不总是占据画面主体很多图里机体很小背景占比超过90%。模型学到的不一定是旋翼机体特征而是天空/背景交界处有一团深色纹理。特别是广角镜头下远处的鸟和无人机在视觉上几乎无法区分。解决先做数据统计看目标框占图片面积的比例分布。如果大量目标框只有几十个像素优先考虑提高输入分辨率或开启mosaic增强。更实际的做法是收集一批负样本——纯天空、纯地面、有飞鸟但无无人机的图——加入训练集。负样本加入后模型会把不像无人机的区域置信度压下去误检会明显减少。4.2 小目标漏检远程无人机的框只有十几个像素默认锚框吃不下现象近距离无人机检测一切正常距离稍远直接漏检。单张图片里目标明明存在标签也画出来了但推理时置信度极低。原因YOLO系列默认锚框针对COCO数据集的常见目标尺寸设计对微小目标不友好。十几像素的框经过多次下采样后到检测头阶段只剩一两个特征点特征信息已经丢得差不多了。解决把训练输入尺寸从640提高到1280小目标特征能保留更多。代价是训练时间明显变长显存占用翻倍。如果显存紧张选择带P2小目标检测层的模型结构比单纯拉高分辨率更划算。另外mosaic增强可以让模型经常看到被缩小到很小的目标这类增强对无人机检测几乎必开。4.3 验证集指标虚高同一段飞行轨迹被同时分进train和val现象验证mAP达到0.97模型看似完美。换一段新拍摄的无人机视频来测试mAP直接掉到0.5以下。原因这个问题在随机划分时尤其隐蔽。同一批次拍摄的连续帧背景几乎一样无人机飞行姿态也接近模型在train里见过几乎同款画面再到val里去测相当于开卷考试。解决不做随机划分按文件名前缀或拍摄批次划分。训练前先查看有哪些批次前缀把同一前缀下的帧全部放进同一个集合。用这种划分方式重新评估得到的mAP才是真实水平。4.4 标签格式混用txt和xml坐标单位不一致转换完框全部偏移现象用脚本把xml统一转txt后训练loss异常偏高可视化发现很多框贴在目标边缘甚至一半在画面外。原因txt和xml两套标注不是同步更新的某些图xml里是像素坐标直接填进去的YOLO解析时当成归一化坐标强制除以图片宽高框自然错位。解决转换前先写一个合法性校验检查坐标范围是否在合理区间。归一化坐标的宽高必须小于1像素坐标的xmax不能超过图片宽度。暴露出不一致的标注后以与图片实际尺寸匹配的那份为准重新生成另一份格式。def validate_txt_boxes(txt_path, img_w, img_h): with open(txt_path) as f: for line in f: parts line.strip().split() if len(parts) ! 5: print(finvalid line: {txt_path}: {line}) continue _, cx, cy, bw, bh map(float, parts) # 归一化坐标合法范围是0到1越界就是格式错误 if cx 0 or cx 1 or cy 0 or cy 1: print(fcenter out of range: {txt_path}) if bw 1 or bh 1: print(fsize out of range: {txt_path})这个脚本只花几秒钟能避免后面整轮训练白跑。我在处理多来源数据集时会把txt和xml先分别统计一遍坐标分布两者差异超过阈值的图直接隔离出来人工检查。5. 进阶从样本统计到NMS阈值一个更稳的无人机检测调优路径模型训练完成只是第一步真正决定无人机检测能不能落地的是两个数字验证集上目标框的尺寸分布和推理时NMS阈值的选择。先跑一个统计脚本看看标注框的宽度和高度分布情况。import cv2 import numpy as np from glob import glob stats [] for txt in glob(val/labels/*.txt): img_path txt.replace(labels, images)[:-4] .jpg h, w cv2.imread(img_path).shape[:2] with open(txt) as f: for line in f: _, cx, cy, bw, bh line.split() stats.append((float(bw) * w, float(bh) * h)) arr np.array(stats) print(box width p50/p90:, np.percentile(arr[:, 0], [50, 90])) print(box height p50/p90:, np.percentile(arr[:, 1], [50, 90]))p50和p90这两个数字决定了很多训练参数。如果p50框宽在40像素左右640输入下小目标特征已经很弱建议上1280分辨率或换带P2层的模型。如果p90框不到200像素说明几乎没有大目标不需要为锚框尺寸留太多余量反而应该把注意力放在低置信度区间的召回上。NMS阈值方面我习惯按场景分两套配置。高精度巡检场景IoU阈值设0.5置信度阈值0.35以上优先保证框的位置准确实时跟踪场景IoU阈值降到0.45置信度阈值提高到0.5宁可漏检也不要误检因为跟踪器可以靠连续帧互补但误检会直接带偏轨迹。真正让我彻底改掉拿过来就训习惯的是有一次数据集中混入了一批夜间红外图。模型训练时loss降得很漂亮实测时白天场景正常、夜间场景全瞎。从那时起我每次拿到新数据集都会先跑一遍统计脚本看一眼尺寸分布、批次划分、坐标合法性这三项再决定训练参数。这个习惯帮我省掉的重复训练时间比花在调模型上的时间还多。希望帮到你。本文还有配套的精品资源点击获取
返回列表