简介这是一份面向计算机视觉研究与工程应用的无人机高空拍摄路面车辆目标检测数据集适合需要训练和验证车辆检测模型的开发者。数据集内图片与标注一一对应图片共6778张每张均配有Pascal VOC格式的XML标注及YOLO格式的TXT标注标注类别统一为vehicle总框数达89122个标注密度高可支撑复杂场景下的多目标检测训练标注工具采用labelImg矩形框规则清晰可直接用于模型训练。压缩包共2000个文件其中以1999个XML标注文件为主整体大小263.02MB。数据集源自无人机高空视角能帮助研究多尺度目标、视角变形等真实问题适用于交通流量统计、异常车辆监测等场景同时标注文件按图片一一命名便于数据划分与预处理。目前已有598人学习下载对有标注需求的车辆检测项目具有直接参考价值。1. 无人机高空拍摄路面车辆数据集6770张双格式样本小目标模型的第一道练兵场做目标检测的人应该都有过这种经历模型在公开数据集上刷分漂亮一换到无人机俯视场景就集体翻车。地面视角的车辆样本和天空视角完全是两个世界——车顶取代了车脸目标在画面里只占几十个像素密集排列的车辆还会互相遮挡。这份「无人机高空拍摄路面车辆数据集6770张VOCYOLO格式.zip」就是把俯视场景缺失的这块拼图补上。它对做智慧交通、安防巡检、车流统计的工程师尤其有用也适合刚学YOLO训练流程的新手拿来找手感。更难得的是它同时给了VOC和YOLO两种标注格式省去了格式转换的麻烦解压之后看一眼目录结构就能直接开工。我会从数据集本身的结构分析讲起然后给出格式转换、训练配置、踩坑排查的完整路径最后聊几个把精度往上拉的进阶手段。这篇文章里出现的脚本和参数都是我自己在类似数据集上反复调过的你可以直接复制去改。2. 拆开压缩包看门道VOC与YOLO标注格式的结构差异与解析2.1 两种标注格式的本质区别XML的绝对坐标与TXT的归一化坐标拿到压缩包解压之后第一件事不是急着训练而是搞清楚目录里到底有什么。常见的组织方式是根目录下分Annotations、JPEGImages、ImageSets和labels四个文件夹分别对应VOC的XML标注文件、原始图片、数据集划分文件和YOLO格式的TXT标注文件。VOC格式用XML记录每个目标的类别和边界框坐标坐标值是像素绝对值YOLO格式则是每张图对应一个同名TXT每行写类别 x_center y_center width height四种数值全部归一化到0到1之间。两种格式各有各的用法场景VOC是很多检测框架的通用中间格式像MMDetection、Detectron2都原生支持解析它YOLO格式则是Ultralytics系训练器的默认输入。你手里的这份数据集同时给了两种意味着你不需要做VOC到YOLO的坐标换算但反过来你得理解两种格式的坐标表示差异否则后续自己做数据清洗、合并数据集时会吃大亏。XML文件里通常是这种结构annotation folderJPEGImages/folder filename000001.jpg/filename size width1920/width height1080/height depth3/depth /size object namecar/name bndbox xmin320/xmin ymin240/ymin xmax380/xmax ymax280/ymax /bndbox /object /annotationbndbox里的四个值就是边界框左上角和右下角的像素坐标配合size里的图像宽高就能算出一个目标在画面中的实际尺寸。我建议你拿到数据后先写个脚本批量统计所有XML里目标的像素宽高分布这能让你对这个数据集的“目标尺度”心里有数。2.2 用Python快速盘点数据集的类别分布与目标尺度很多人在训练前不看数据分布直接跑训练脚本最后发现某些类别AP特别低才回头看。我习惯先把数据集的底细摸清楚写一个统计脚本跑一遍输出每个类别的目标数量、平均宽高、以及目标面积占图像面积的比例分布。这一步花不了几分钟但对后续判断训练效果能省下大量排查时间。import xml.etree.ElementTree as ET import os import glob from collections import defaultdict annotation_dir Annotations stats defaultdict(lambda: {count: 0, widths: [], heights: [], areas: []}) for xml_file in glob.glob(os.path.join(annotation_dir, *.xml)): tree ET.parse(xml_file) root tree.getroot() img_width int(root.find(size/width).text) img_height int(root.find(size/height).text) img_area img_width * img_height for obj in root.findall(object): name obj.find(name).text bbox obj.find(bndbox) xmin int(bbox.find(xmin).text) ymin int(bbox.find(ymin).text) xmax int(bbox.find(xmax).text) ymax int(bbox.find(ymax).text) w xmax - xmin h ymax - ymin stats[name][count] 1 stats[name][widths].append(w) stats[name][heights].append(h) stats[name][areas].append((w * h) / img_area) for cls, s in stats.items(): print(f类别: {cls}, 目标数: {s[count]}, f平均宽: {sum(s[widths])/len(s[widths]):.1f}, f平均高: {sum(s[heights])/len(s[heights]):.1f}) print(f 面积占比中位数: {sorted(s[areas])[len(s[areas])//2]*100:.3f}%)这段代码做的事情很直接遍历所有XML提取每个目标的类别和坐标换算成宽高和面积占比。逻辑上要注意XML里所有值都是字符串必须先转int再运算size/width这种斜杠路径是ET的简化写法等价于逐级find。跑完这个脚本以后重点看两个数平均宽高如果低于32像素说明这是典型的小目标数据集面积占比中位数如果低于0.5%意味着绝大多数目标在整幅图里只占很小一块。这两个数字决定了后续训练时的输入分辨率、推理策略都要围绕“小目标”来做针对性调整。比如这类数据用YOLOv8默认的640输入尺寸跑效果往往会比地面视角数据集差一截原因就是目标太小下采样到特征图后可能只剩下几个像素。2.3 ImageSets文件夹里的划分文件训练/验证/测试集怎么切分VOC格式的惯例是把数据划分放在ImageSets/Main目录下常见的文件名是train.txt、val.txt、trainval.txt。这些文件里每行一个图片文件名不带扩展名告诉训练框架哪些图用来训练、哪些用来验证。这份数据集如果已经在压缩包里给好了划分文件用YOLO训练时直接按名字索引就行如果没给或者你想重新划分就得自己动手切。我一般会按8:1:1的比例切分但这里有一个细节需要注意车辆检测数据集中同一场景连续帧的图片相似度很高如果随机划分很可能训练集和验证集中出现高度相似的画面导致验证指标虚高。更好的做法是先把文件名按前缀或拍摄批次分组再以组为单位划分保证时间上相邻的帧都落在同一侧。群里的图片本质上是视频抽帧得到的这个坑就很常见了。除了VOC自带的划分文件YOLO训练时通常直接用数据集配置文件里的train和val路径指向两个存放图片的文件夹。如果你用的训练框架是Ultralytics的YOLOv8它会根据目录下所有图片自动建索引不再需要手动维护TXT列表。也就是说把图片按训练集/验证集分别复制到两个目录里比维护一个TXT划分文件更省事。3. 把VOC翻译成YOLO的TXT坐标换算脚本与四个关键细节3.1 为什么还需要转换脚本数据集里常混入自定义类别名这份数据集号称VOCYOLO双格式但我处理过不少同类资源发现一个经验法则压缩包里的YOLO标注有时只涵盖了部分类别。比如VOC XML里标了car、truck、bus、motorbike四个类别而YOLO格式的TXT里可能只转换了前三个。原因很可能是转换脚本本身用了固定的类别列表或者标注人员在后期补充了新类别但没有重新生成TXT。所以稳妥的做法是自己写一个转换脚本把Annotations里的XML全部转成YOLO格式的TXT对比压缩包里自带的labels文件夹以你自己生成的为准。这一步看似多余却能保证类别索引的完全可控尤其是你后续想往数据集里加新类别、或者合并其他数据集时类别ID的映射表必须掌握在自己手里。VOC转YOLO的核心公式只有一行x_center (xmin xmax) / 2 / img_widthy_center (ymin ymax) / 2 / img_heightwidth (xmax - xmin) / img_widthheight (ymax - ymin) / img_height。3.2 完整转换脚本类别表、归一化与防溢出import xml.etree.ElementTree as ET import os import glob # 类别列表顺序决定了YOLO标签里的ID一定要和训练配置保持一致 classes [car, truck, bus, motorbike] def convert_voc_to_yolo(xml_file, output_dir): tree ET.parse(xml_file) root tree.getroot() img_width int(root.find(size/width).text) img_height int(root.find(size/height).text) txt_name os.path.splitext(os.path.basename(xml_file))[0] .txt txt_path os.path.join(output_dir, txt_name) lines [] for obj in root.findall(object): cls_name obj.find(name).text if cls_name not in classes: continue cls_id classes.index(cls_name) bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) # 坐标换算中心点坐标和宽高全部归一化到[0, 1] x_center (xmin xmax) / 2.0 / img_width y_center (ymin ymax) / 2.0 / img_height w (xmax - xmin) / img_width h (ymax - ymin) / img_height # 防溢出个别标注框越界会导致归一化数值大于1或小于0 x_center max(0.0, min(1.0, x_center)) y_center max(0.0, min(1.0, y_center)) w max(0.0, min(1.0, w)) h max(0.0, min(1.0, h)) lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(txt_path, w) as f: f.write(\n.join(lines)) os.makedirs(labels, exist_okTrue) for xml_file in glob.glob(Annotations/*.xml): convert_voc_to_yolo(xml_file, labels)这个脚本里有两个容易忽略的参数细节。第一个是类别列表的顺序一旦确定了就不能随意调整因为TXT文件里只存类别ID不存类别名ID对应的含义完全由classes列表的顺序决定。训练阶段配置文件的类别列表必须和这里完全一致否则会出现“模型学会了但是预测结果全对不上号”的玄学问题。第二个是防溢出处理XML里偶尔会有标注框稍微超出图像边界的情况不裁剪的话归一化后可能出现负值或大于1的值训练时有些版本会直接报错。3.3 转换后的验证标注可视化这一步不能省转换脚本跑完看到生成了几千个TXT文件不代表万事大吉。我见过最典型的翻车案例是类别ID错位——XML里标注的bus在转换后变成了YOLO的car原因就是转换脚本里的类别列表和XML标注里的类别名匹配错了。要发现这种问题唯一可靠的办法是可视化检查。import cv2 import os def visualize_yolo_annotation(img_path, txt_path, classes): img cv2.imread(img_path) h, w img.shape[:2] with open(txt_path, r) as f: for line in f.readlines(): parts line.strip().split() cls_id, x_center, y_center, box_w, box_h parts cls_id int(cls_id) x_center float(x_center) * w y_center float(y_center) * h box_w float(box_w) * w box_h float(box_h) * h x1 int(x_center - box_w / 2) y1 int(y_center - box_h / 2) x2 int(x_center box_w / 2) y2 int(y_center box_h / 2) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, classes[cls_id], (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) return img img_path JPEGImages/000001.jpg txt_path labels/000001.txt result visualize_yolo_annotation(img_path, txt_path, classes) cv2.imwrite(check.jpg, result)这段代码把TXT里的归一化坐标还原成像素坐标然后把边界框和类别画到原图上。重点检查两类问题边界框是否贴合车辆轮廓、类别标签和框内物体是否吻合。我从经验上说抽10到20张图检查就够了分布在不同场景里看不要全看同一批图。如果发现某一张图的所有框都偏移了固定距离大概率是XML里坐标单位的问题如果只有个别框偏通常是标注本身的误差。4. 用YOLOv8把这份数据跑成模型训练配置与评估指标4.1 数据集配置文件与目录组织少走弯路的摆放方式训练之前先把数据集整理成YOLO工具链习惯的目录结构。常见做法是建立images/train、images/val、labels/train、labels/val四个目录图片和标注文件分别按训练/验证放好文件名一一对应。Ultralytics的YOLOv8会自动把同一路径下的.jpg和同名.txt匹配起来不需要额外指定标注文件位置。然后写一个data.yaml配置文件这是整个训练流程里最容易被改错的文件之一# data.yaml path: /your/absolute/path/to/dataset train: images/train val: images/val nc: 4 names: [car, truck, bus, motorbike]path字段建议写绝对路径写相对路径的话训练时的工作目录一变就会报找不到图片nc是类别数量必须和names列表长度一致names的顺序必须和转换脚本里classes的顺序一模一样。这三个字段任何一个出错训练都能跑起来但验证或推理时就会出现错乱这也是很多人说YOLO训练“看着没问题结果全错”的头号原因。4.2 训练命令与关键超参数高空小目标场景下的取舍逻辑数据准备好了就进入训练环节。我推荐先跑一个短周期的实验验证数据没问题再跑长周期拿最终模型。短周期用50轮长周期用150到200轮输入尺寸直接从1280起步或者用640加切片推理。yolo detect train datadata.yaml \ modelyolov8n.pt \ imgsz1280 \ epochs50 \ batch16 \ device0 \ workers8 \ patience20几个参数的选择理由imgsz1280是高空小目标数据集的关键设置。用640训练一个32x32像素的目标在输入图上占约5%面积下采样到40x40的特征图后只剩几个像素特征几乎全丢提到1280后同样目标在下采样后的特征图里能保留更多细节。代价是显存和训练时间明显上涨如果你的显卡只有8GB显存可以把batch降到8甚至4。yolov8n.pt是nano版本参数最少、跑得最快适合先验证数据链路验证通过后再换成yolov8s.pt或yolov8m.pt冲精度。patience20表示连续20轮验证集指标不提升就早停防止无效训练占用资源。训练跑完以后不要急着收工先看验证集结果里的两个指标mAP50和mAP50-95。mAP50是IoU阈值0.5下的平均精度反映目标大概位置对不对mAP50-95是0.5到0.95多个阈值下的平均精度对边界框的精确度要求更高。对于高空小目标数据集mAP50-95通常会明显低于普通数据集因为小目标的框稍微偏几个像素IoU就掉下去了。如果mAP50能到80以上但mAP50-95只有40多说明检测到的目标位置基本对但边界框不准可以试试调整NMS参数或者在loss里增加对边界框回归的权重。训练结束后YOLO会在runs/detect/train/目录下生成权重文件、预测样本图和各类曲线。记得去看results.png里训练损失和验证损失的曲线如果训练损失持续下降但验证损失在某个epoch后开始反弹说明过拟合了需要加强数据增强或者增加dropout。4.3 推理验证用训练好的权重跑新图而不是跑训练集模型训练完第一步是在验证集上推理目的是评估泛化能力。这一条很重要很多新手习惯拿训练集里的图片做效果演示画出来的框特别漂亮但那是模型“背答案”不能代表真实水平。yolo detect predict modelruns/detect/train/weights/best.pt \ sourceimages/val/ \ imgsz1280 \ conf0.25 \ iou0.45 \ save_txtTrue这里source指向验证集图片目录conf0.25是置信度阈值低于这个值的检测结果会被过滤掉iou是NMS的IoU阈值值越小对重叠框的抑制越强。这两个参数直接影响可视化结果里框的多少和稀疏程度如果你发现输出图上框太多太密调高conf到0.4框太少漏检明显调低到0.1。实际部署时这两个值还需要再重新调训练和验证阶段只需要一个大概合理的观察窗口。推理完成后对照runs/detect/predict里的标注图重点关注两类错误一种是同一辆车被画了两个框NMS没压住另一种是两辆紧挨着的车被合并成一个框。前者说明iou阈值偏高了后者说明偏低根据实际表现做微调。5. 高空小目标训练避坑漏检、类别失衡与标注噪声的排查记录5.1 漏检集中在小尺寸目标imgsz与切片推理的取舍现象模型在验证集上mAP50有75%但单独统计后发现所有漏检的目标都是面积占比小于0.2%的小车大车和公交车几乎全部命中。原因这是高空俯视数据集的典型问题。目标在1280分辨率输入下仍然只占不到30x30像素经过模型的主干网络多次下采样后小目标的特征在深层特征图里已经非常微弱。YOLOv8的检测头在三个尺度上做预测最小尺度对应P3层8倍下采样目标如果小于16x16像素在P3层只剩2x2个像素点几乎没有语义信息。解决两个方向。第一把imgsz提到1536或1600但这会占用大量显存且训练时间翻倍我一般只在目标特别小时用。第二个方案是推理阶段做切片推理SAHI把原图切成1280x1280的小块分别检测再合并结果训练仍然用1280推理用切片。这个方案对显存占用基本没影响实际效果提升非常明显。后文会在进阶部分给出具体参数。5.2 类别严重失衡私家车占比过高导致卡车AP低现象训练完总mAP不错但results.csv里truck类的AP只有car类的一半甚至更低。原因这类无人机数据集大多来自城市道路采集车辆分布天然是长尾的。统计一下数量就知道了car可能有五六万目标truck可能只有几千。模型在训练时对出现频率高的类别过拟合对低频类别的特征学习不充分。解决最直接的做法是在训练配置里打开类别平衡采样。Ultralytics的YOLOv8不支持内置的类别权重参数我一般用两个替代方案一是把低频类别的图片复制几份扩充进训练集注意是整图复制不要只复制标注让模型多见到几次二是训练时用class_weight自定义loss权重不过YOLOv8的py接口里需要自己写回调对新手不太友好。最省事的是调整验证指标观察方式单独计算每个类别的AP而不是只看平均mAP然后针对掉队类别决定是否补数据。如果truck只有几百个目标别硬调参找别的公开数据集补充这个类别更划算。5.3 标注框不贴合目标密集排列车辆的标注偏差现象可视化检查时发现部分图中紧挨着的两辆车被标成了一个框或者框的边缘明显超出了车体轮廓。原因制作数据集的标注员在密集场景下难以精确框选每个目标或者原始标注经过了某种自动标注工具的粗标后人工只做了抽查。无人机俯视视角下车辆排列紧密车顶颜色相近时边界模糊人眼都容易看走眼。解决使用清洗脚本基于几何规则先筛一批可疑标注出来宽高比异常的比如车居然标成了3:1的长条、面积极端大的占了整张图30%以上、或者与周围框重叠率超过0.8的。筛出来之后人工再过一遍。这个方法并不完美但能防住那些明显破坏训练的脏标注。import os import glob def find_abnormal_boxes(txt_dir, img_w1920, img_h1080, min_area0.2, max_ratio4.0): suspicious [] for txt_file in glob.glob(os.path.join(txt_dir, *.txt)): with open(txt_file, r) as f: for line in f: parts line.strip().split() if len(parts) ! 5: suspicious.append((txt_file, 格式异常)) continue _, _, _, w, h map(float, parts) area w * h ratio max(w, h) / max(min(w, h), 1e-6) if w * img_w 10 or h * img_h 10: # 小于10像素的目标 suspicious.append((txt_file, 目标过小)) if area min_area: # 面积占图超过20% suspicious.append((txt_file, 目标过大)) if ratio max_ratio: # 宽高比超过4可能是错误标注 suspicious.append((txt_file, 长宽比异常)) return suspicious suspicious_files find_abnormal_boxes(labels) for fname, reason in suspicious_files[:30]: print(f{fname}: {reason})这个脚本的判定逻辑很简单但很实用。小于10像素的目标在1280分辨率下基本不可能被检测出来这类标注留在训练集里只会让模型学“算了不检测了”面积占图20%以上的目标大概率是把多个车标成了一个框需要手工拆分。跑完脚本后把筛出来的文件单独放一个目录用可视化脚本逐张确认再决定改还是删。5.4 数据划分不合理高相似帧跨训练集与验证集现象训练时损失正常下降验证集的mAP却出现大幅波动且最终验证精度明显高于人工抽检的预期。原因很多数据集的图像是按视频帧顺序编号的比如前100张是同一段路的连续画面后100张是另一段路的。如果随机切分训练集和验证集同一场景的画面会同时出现在两侧模型在训练时已经“见过”验证集里几乎相同的画面验证分数虚高真实场景泛化能力远低于指标。解决用文件名前缀做分组再划分。比如文件按scene01_0001.jpg这种命名就以前缀scene01为最小单位先把所有同前缀的图片聚到一组再按组切分。切分完成后可以验证一下从验证集中取一张图在训练集中找它的直方图相似度最高的图如果相似度超过0.9说明划分仍然有问题。实际做的时候用文件名前缀分组基本能解决95%的问题。6. 提升精度的进阶路线切片推理与公开数据融合先说切片推理SAHI。这是一个不需要重新训练就能显著拉升小目标检出率的方案原理很简单推理时不把整张1920x1080的图缩成1280输入而是先把原图切成多个1280x1280的重叠切片每个切片单独送进模型检测最后把检测框合并回原图坐标。这样小目标在被切片放大后像素面积变大模型更容易识别。具体操作上切片重叠率设20%太高会重复检测太低会让目标跨切片被截断。合并时NMS的IoU阈值我一般设0.3比单图推理低一些因为同一个目标可能被相邻切片重复检测到抑制要更强。需要安装sahi库命令行大概长这样sahi predict --model_type yolov8 --model_path runs/detect/train/weights/best.pt \ --source images/val/ --slice_width 1280 --slice_height 1280 \ --overlap_ratio 0.2 --postprocess_class_agnostic False我自己的使用感受是SAHI对小目标AP的提升幅度通常在3到8个百分点具体看原始目标有多小。第二个进阶方向是融合公开数据来扩充训练集尤其是补足稀有类别。这份数据主要集中在城市道路车辆如果你想让它识别工地场景的渣土车或者高速路的大货车现有类别可能不够用。我做过一次类似的融合把VisDrone数据集中相关类别抽出来格式转换成YOLO的TXT然后和这份数据合并。融合的难点不在格式而在类别映射——两个数据集的car定义可能有细微差别VisDrone的car可能包含面包车而这份数据里的面包车可能被标成了truck。合并前先用上面的统计脚本跑一遍两个数据集的类别分布对着看名称相同的类别在目标尺寸分布上有没有明显差异有差异就要决定到底按哪个标准合并。从我的经验来说宁可少要几个类别也别硬合并——类别语义混乱对模型精度的影响比数据量不足更严重。最后想说一个工作习惯每次训练前把数据集统计、训练命令、超参数写进一个实验记录文件里不要靠记忆。我做高空车辆检测时踩过的坑已经够多了——换了一版数据忘记同步类别列表、训练到一半发现XML里有脏数据、推理时conf调太低导致画满屏的框——这些如果当时有记录至少能少折腾半天。目标检测这个方向数据和配置的每一个细节都可能在最终结果上被放大把它们管理好比追求一个更复杂的模型更能稳定出活。希望这篇笔记能帮你在无人机车辆检测这条路上少走几个弯路祝训练顺利。本文还有配套的精品资源点击获取