简介:这是一份面向计算机视觉研究与工程应用的无人机高空拍摄路面车辆目标检测数据集,适合需要训练和验证车辆检测模型的开发者。数据集内图片与标注一一对应,图片共6778张,每张均配有Pascal VOC格式的XML标注及YOLO格式的TXT标注,标注类别统一为vehicle,总框数达89122个,标注密度高,可支撑复杂场景下的多目标检测训练;标注工具采用labelImg,矩形框规则清晰,可直接用于模型训练。压缩包共2000个文件,其中以1999个XML标注文件为主,整体大小263.02MB。数据集源自无人机高空视角,能帮助研究多尺度目标、视角变形等真实问题,适用于交通流量统计、异常车辆监测等场景,同时标注文件按图片一一命名,便于数据划分与预处理。目前已有598人学习下载,对有标注需求的车辆检测项目具有直接参考价值。
1. 无人机高空拍摄路面车辆数据集:6770张双格式样本,小目标模型的第一道练兵场
做目标检测的人应该都有过这种经历:模型在公开数据集上刷分漂亮,一换到无人机俯视场景就集体翻车。地面视角的车辆样本和天空视角完全是两个世界——车顶取代了车脸,目标在画面里只占几十个像素,密集排列的车辆还会互相遮挡。这份「无人机高空拍摄路面车辆数据集6770张VOC+YOLO格式.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> <folder>JPEGImages</folder> <filename>000001.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>car</name> <bndbox> <xmin>320</xmin> <ymin>240</ymin> <xmax>380</xmax> <ymax>280</ymax> </bndbox> </object> </annotation><bndbox>里的四个值就是边界框左上角和右下角的像素坐标,配合<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 为什么还需要转换脚本:数据集里常混入自定义类别名
这份数据集号称VOC+YOLO双格式,但我处理过不少同类资源,发现一个经验法则:压缩包里的YOLO标注有时只涵盖了部分类别。比如VOC XML里标了car、truck、bus、motorbike四个类别,而YOLO格式的TXT里可能只转换了前三个。原因很可能是转换脚本本身用了固定的类别列表,或者标注人员在后期补充了新类别但没有重新生成TXT。
所以稳妥的做法是自己写一个转换脚本,把Annotations里的XML全部转成YOLO格式的TXT,对比压缩包里自带的labels文件夹,以你自己生成的为准。这一步看似多余,却能保证类别索引的完全可控,尤其是你后续想往数据集里加新类别、或者合并其他数据集时,类别ID的映射表必须掌握在自己手里。
VOC转YOLO的核心公式只有一行:x_center = (xmin + xmax) / 2 / img_width,y_center = (ymin + ymax) / 2 / img_height,width = (xmax - xmin) / img_width,height = (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_ok=True) 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 data=data.yaml \ model=yolov8n.pt \ imgsz=1280 \ epochs=50 \ batch=16 \ device=0 \ workers=8 \ patience=20几个参数的选择理由:imgsz=1280是高空小目标数据集的关键设置。用640训练,一个32x32像素的目标在输入图上占约5%面积,下采样到40x40的特征图后只剩几个像素,特征几乎全丢;提到1280后同样目标在下采样后的特征图里能保留更多细节。代价是显存和训练时间明显上涨,如果你的显卡只有8GB显存,可以把batch降到8甚至4。yolov8n.pt是nano版本,参数最少、跑得最快,适合先验证数据链路;验证通过后再换成yolov8s.pt或yolov8m.pt冲精度。patience=20表示连续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 model=runs/detect/train/weights/best.pt \ source=images/val/ \ imgsz=1280 \ conf=0.25 \ iou=0.45 \ save_txt=True这里source指向验证集图片目录,conf=0.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_w=1920, img_h=1080, min_area=0.2, max_ratio=4.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调太低导致画满屏的框——这些如果当时有记录,至少能少折腾半天。目标检测这个方向,数据和配置的每一个细节都可能在最终结果上被放大,把它们管理好,比追求一个更复杂的模型更能稳定出活。希望这篇笔记能帮你在无人机车辆检测这条路上少走几个弯路,祝训练顺利。
本文还有配套的精品资源,点击获取