简介:面向无人机场景车辆检测的实战数据集,适合从事目标检测算法训练、智能交通与航拍视觉分析的开发者与研究者使用。数据集包含1000张真实场景高质量图片,覆盖城市道路行驶车辆、道边停车、停车场、小区车辆以及车辆遮挡、严重遮挡等多种复杂情形,类别划分为轿车car、货车van和巴士bus三类,可用于无人机车辆检测项目或作为通用检测数据集的场景补充。资源包为1个PDF文件,约2MB,内含数据集基本情况介绍与获取方式,标注采用labelimg完成,质量较高,并提供VOC(xml)、COCO(json)、YOLO(txt)三种主流格式,可直接投入YOLO等算法训练。随附YOLO11一键训练脚本,支持GPU(GPUs)、CPU及Mac芯片多平台方案,并给出博主训练结果日志供参考。目前已有261人学习,便于快速验证模型效果并复现训练流程。
1. 无人机视角下的车辆检测:1000 张图、三种标签格式与 YOLO11 一键训练到底怎么落地
拿到「无人机场景-目标检测-车辆检测数据集-1000张图-+对应VOC/COCO/YOLO三种格式标签+支持GPU(GPUs)/CPU/Mac三平台YOLO11一键训练脚本」这个标题,多数人的第一反应是:数据集我有了,脚本我也能写,但为什么跑出来的 mAP 总比论文低一截?问题往往不在模型,而在数据标签的坐标系转换和训练入口的环境判断。无人机航拍视角下的车辆目标有几个鲜明特征:尺度小、密集、遮挡严重、背景随高度剧烈变化。1000 张图不算多,但如果标签格式对齐、训练脚本能在 GPU、CPU、Mac 三端自动切换设备,这套流程完全可以在单机跑出一个可用的车辆检测基线。这篇内容面向已经会用 Python 和 PyTorch、但还没把无人机车辆检测完整跑通的人,从标签格式差异讲到 YOLO11 一键脚本的设备判断逻辑,再到实际训练时最容易翻车的几个参数。
2. 三种标签格式的坐标系差异与转换脚本
2.1 VOC、COCO、YOLO 的坐标定义到底差在哪
VOC 格式的标注文件是 XML,每个目标用<bndbox>记录xmin、ymin、xmax、ymax,这四个值是绝对像素坐标,原点在图像左上角。COCO 格式用 JSON,bbox字段是[x, y, width, height],同样是绝对像素,但注意它是左上角坐标加宽高,不是右下角。YOLO 格式用.txt,每行class_id x_center y_center width height,全部是归一化到 0 到 1 之间的相对值,中心点坐标加宽高。
这三种格式在无人机车辆检测里最容易出问题的地方是:VOC 转 YOLO 时忘记做归一化,或者归一化时除了图像宽高而不是标注范围。另一个高频错误是 COCO 的bbox宽高在转换时被误当成右下角坐标。我一般会在转换脚本里加一步断言,检查转换后的中心点是否落在 0 到 1 之间,宽高是否为正且不超过 1。
2.2 用 Python 把 VOC 批量转成 YOLO 和 COCO
下面这个脚本处理 VOC 的 XML 目录,输出 YOLO 的.txt和 COCO 的单个 JSON。假设图像是.jpg,XML 和图像在同一目录下按文件名对应。
import os import json import xml.etree.ElementTree as ET from PIL import Image # 类别映射,无人机车辆检测通常只有 car / truck / bus 等 CLASS_MAP = {"car": 0, "truck": 1, "bus": 2, "van": 3} def voc_to_yolo_and_coco(xml_dir, img_dir, out_yolo_dir, out_coco_json): os.makedirs(out_yolo_dir, exist_ok=True) coco_images = [] coco_annotations = [] ann_id = 1 for xml_file in sorted(os.listdir(xml_dir)): if not xml_file.endswith(".xml"): continue tree = ET.parse(os.path.join(xml_dir, xml_file)) root = tree.getroot() filename = root.find("filename").text img_path = os.path.join(img_dir, filename) with Image.open(img_path) as im: w, h = im.size coco_images.append({"id": len(coco_images) + 1, "file_name": filename, "width": w, "height": h}) yolo_lines = [] for obj in root.findall("object"): cls_name = obj.find("name").text if cls_name not in CLASS_MAP: continue cls_id = CLASS_MAP[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) # 裁剪到图像边界,无人机图像边缘常有截断目标 xmin = max(0, min(xmin, w - 1)) ymin = max(0, min(ymin, h - 1)) xmax = max(0, min(xmax, w - 1)) ymax = max(0, min(ymax, h - 1)) xc = (xmin + xmax) / 2.0 / w yc = (ymin + ymax) / 2.0 / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h # 过滤掉宽高为 0 的无效框 if bw <= 0 or bh <= 0: continue yolo_lines.append(f"{cls_id} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}") coco_annotations.append({ "id": ann_id, "image_id": len(coco_images), "category_id": cls_id, "bbox": [xmin, ymin, xmax - xmin, ymax - ymin], "area": (xmax - xmin) * (ymax - ymin), "iscrowd": 0 }) ann_id += 1 txt_name = os.path.splitext(filename)[0] + ".txt" with open(os.path.join(out_yolo_dir, txt_name), "w") as f: f.write("\n".join(yolo_lines)) coco_out = { "images": coco_images, "annotations": coco_annotations, "categories": [{"id": v, "name": k} for k, v in CLASS_MAP.items()] } with open(out_coco_json, "w") as f: json.dump(coco_out, f, indent=2) # 调用示例 voc_to_yolo_and_coco( xml_dir="./drone_vehicle/Annotations", img_dir="./drone_vehicle/JPEGImages", out_yolo_dir="./drone_vehicle/labels_yolo", out_coco_json="./drone_vehicle/annotations_coco.json" )这段代码的关键逻辑有三处。第一,CLASS_MAP必须和后续 YOLO11 训练时的data.yaml里的names顺序完全一致,否则类别索引会错位。第二,裁剪到图像边界这一步在无人机图像里不能省,因为航拍目标经常被图像边缘截断,VOC 标注里可能出现xmax等于图像宽度甚至超出 1 像素的情况。第三,过滤宽高为 0 的框,这类框在 COCO 评估里会直接报错,在 YOLO 训练里会产生 NaN 损失。
参数方面,xc、yc、bw、bh保留 6 位小数足够,YOLO11 官方实现内部会再做一次浮点解析,精度损失可以忽略。COCO JSON 里的area字段用于评估时的面积分组,如果只做训练可以不算,但建议保留,方便后续用 pycocotools 做验证。
2.3 转换后必须做的三项校验
转换脚本跑完不代表数据就能用。我一般会做三个检查。第一,随机抽 20 张图,用 OpenCV 把 YOLO 格式的框画回图像上,肉眼确认框的位置和大小没有整体偏移。第二,统计每个类别的实例数量,如果某个类别少于 50 个实例,训练时大概率欠拟合,需要考虑合并类别或补充数据。第三,检查图像宽高比分布,无人机图像如果同时存在 4:3 和 16:9 两种比例,YOLO11 默认的imgsz=640会做 letterbox 填充,小目标在填充后可能进一步缩小,这时候要考虑把imgsz提到 960 或 1280。
import cv2 import numpy as np def visualize_yolo_label(img_path, label_path, class_names): img = cv2.imread(img_path) h, w = img.shape[:2] with open(label_path) as f: for line in f: parts = line.strip().split() if len(parts) != 5: continue cls_id, xc, yc, bw, bh = map(float, parts) x1 = int((xc - bw / 2) * w) y1 = int((yc - bh / 2) * h) x2 = int((xc + bw / 2) * w) y2 = int((yc + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, class_names[int(cls_id)], (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) cv2.imwrite("check_vis.jpg", img) visualize_yolo_label( "./drone_vehicle/JPEGImages/000001.jpg", "./drone_vehicle/labels_yolo/000001.txt", ["car", "truck", "bus", "van"] )这个可视化脚本只依赖 OpenCV,跑一张图就能看出转换是否正确。如果框整体偏左上或偏右下,多半是归一化时用错了宽高,或者 VOC 的xmax被误当成了宽。
3. YOLO11 一键训练脚本:GPU、CPU、Mac 三端设备判断与参数配置
3.1 为什么需要一键脚本而不是直接调 ultralytics 命令
YOLO11 的官方训练入口是yolo detect train或 Python 里的YOLO("yolo11n.pt").train(...)。但在实际项目里,训练环境往往不统一:实验室服务器有 NVIDIA GPU,个人笔记本是 Mac 的 MPS,还有同事只有 CPU。如果每次训练都手动改device参数,很容易出现「在 GPU 上跑通、在 Mac 上报错」的情况。一键脚本的核心价值不是省几条命令,而是把设备判断、路径检查、数据配置校验和训练参数默认值封装成一个可复用的入口。
我一般会把脚本分成三层:环境探测层、数据校验层、训练启动层。环境探测层负责判断当前机器有没有 CUDA、有没有 MPS、都没有就回退到 CPU。数据校验层检查data.yaml里的路径是否存在、类别数是否和标签里的最大 class_id 一致。训练启动层根据设备类型调整batch、workers和amp参数。
3.2 设备探测与训练参数自动适配
下面这个脚本用torch探测设备,然后根据设备类型设置不同的默认参数。注意 Mac 的 MPS 后端对某些算子支持不完整,YOLO11 在 MPS 上训练时需要把amp关掉,否则可能出现梯度为 NaN。
import os import sys import yaml import torch from ultralytics import YOLO def detect_device(): if torch.cuda.is_available(): return "cuda", torch.cuda.device_count() if hasattr(torch.backends, "mps") and torch.backends.mps.is_available(): return "mps", 1 return "cpu", 1 def build_train_args(device, gpu_count): args = { "data": "./drone_vehicle/data.yaml", "epochs": 100, "imgsz": 960, "patience": 20, "save": True, "pretrained": True, "optimizer": "AdamW", "lr0": 0.001, "lrf": 0.01, "cos_lr": True, "close_mosaic": 10, "val": True, "plots": True, } if device == "cuda": args["device"] = list(range(gpu_count)) if gpu_count > 1 else 0 args["batch"] = 16 * max(1, gpu_count) args["workers"] = 8 args["amp"] = True elif device == "mps": args["device"] = "mps" args["batch"] = 8 args["workers"] = 4 args["amp"] = False # MPS 上 AMP 不稳定 else: args["device"] = "cpu" args["batch"] = 4 args["workers"] = 2 args["amp"] = False return args def check_data_yaml(yaml_path): with open(yaml_path) as f: cfg = yaml.safe_load(f) for key in ["train", "val", "names"]: if key not in cfg: raise ValueError(f"data.yaml 缺少字段: {key}") train_path = cfg["train"] if not os.path.exists(train_path): raise FileNotFoundError(f"训练集路径不存在: {train_path}") return cfg def main(): device, gpu_count = detect_device() print(f"检测到设备: {device}, GPU 数量: {gpu_count}") cfg = check_data_yaml("./drone_vehicle/data.yaml") print(f"类别: {cfg['names']}") args = build_train_args(device, gpu_count) model = YOLO("yolo11s.pt") model.train(**args) if __name__ == "__main__": main()设备探测的逻辑很直接:torch.cuda.is_available()为真就用 CUDA,否则检查 MPS,最后回退 CPU。参数适配里几个关键点值得展开。imgsz设成 960 而不是默认的 640,是因为无人机车辆目标普遍偏小,640 分辨率下很多车辆只有十几个像素,YOLO11 的 P3 特征图经过 8 倍下采样后几乎无法保留有效信息。batch在单卡 CUDA 上给 16,MPS 给 8,CPU 给 4,这个数值不是绝对的,取决于显存或内存大小,如果训练时出现 OOM,优先降batch而不是降imgsz。amp在 CUDA 上开启可以省显存、提速,但在 MPS 和 CPU 上必须关掉,这是血泪经验,开着 AMP 在 Mac 上跑几十个 iteration 后 loss 直接变 NaN。
close_mosaic设成 10 表示最后 10 个 epoch 关闭 mosaic 增强。Mosaic 对小目标检测有帮助,但训练末期关闭可以让模型更适应真实分布,这个技巧在 YOLOv5 时代就有,YOLO11 依然适用。
3.3 data.yaml 的写法与类别数对齐
YOLO11 的data.yaml结构很简单,但无人机车辆检测里最容易错的是names的顺序和nc的对应关系。下面是一个四类车辆检测的示例。
path: ./drone_vehicle train: images/train val: images/val test: images/test nc: 4 names: 0: car 1: truck 2: bus 3: van注意train和val写的是相对路径,相对于path字段。如果直接写绝对路径也可以,但换机器时容易失效。nc必须等于names的条目数,且names的键必须从 0 开始连续。如果 VOC 转换时CLASS_MAP里 car 是 0、truck 是 1,这里就必须一致。我见过有人把names写成列表["car", "truck", "bus", "van"],YOLO11 也能解析,但键值对形式更不容易出错。
数据集划分方面,1000 张图建议按 7:2:1 分 train、val、test。如果某些类别的实例数太少,可以考虑分层抽样,保证每个 split 里都有所有类别。划分脚本可以用sklearn.model_selection.train_test_split对文件名列表做一次随机划分,然后按划分结果把图像和标签分别复制到对应目录。
3.4 训练启动与日志观察
脚本跑起来后,控制台会输出每个 epoch 的 box_loss、cls_loss、dfl_loss 和 mAP50、mAP50-95。无人机车辆检测里,前 10 个 epoch 的 box_loss 下降速度是关键指标。如果 box_loss 在前 5 个 epoch 几乎不降,大概率是学习率太低或者标签有问题。我一般会把lr0从 0.001 提到 0.01 试一次,如果 loss 震荡严重再降回来。
训练日志里还有一个容易被忽略的字段是instances,表示当前 batch 里的目标数量。如果这个值长期低于 5,说明数据集中小目标占比过高,或者imgsz太小导致部分目标在数据加载时被过滤掉了。YOLO11 默认会过滤掉宽或高小于 2 像素的框,如果无人机图像里车辆只有 3 到 4 个像素,这个过滤阈值需要调整,但更根本的解决办法是提高输入分辨率。
# 如果不想用 Python 脚本,也可以直接用命令行,但设备参数需要手动指定 yolo detect train data=./drone_vehicle/data.yaml model=yolo11s.pt epochs=100 imgsz=960 batch=16 device=0 amp=True命令行方式适合快速验证,但跨平台时还是建议用前面的 Python 脚本,因为设备判断逻辑已经封装好了。
4. 无人机车辆检测训练中的避坑与排查
4.1 现象:训练 loss 正常下降但 mAP 始终为 0
原因通常有两个。第一,验证集的标签路径在data.yaml里写错了,YOLO11 加载不到验证标签,评估时把所有预测都当成假阳性。第二,类别索引错位,比如训练标签里 car 是 0,但data.yaml里 car 是 1,模型学到的类别和评估时的类别对不上。解决办法是先用第 2 章的可视化脚本检查验证集标签,再打印data.yaml的names和标签文件里的最大 class_id 做对比。
4.2 现象:Mac 上训练几个 epoch 后 loss 变成 NaN
这是 MPS 后端加 AMP 的经典翻车场景。MPS 对torch.cuda.amp的替代实现不完整,某些算子在半精度下会溢出。解决办法是在build_train_args里对 MPS 设备强制amp=False,同时把lr0降到 0.0005 左右。如果关掉 AMP 后仍然 NaN,检查数据里有没有宽高为 0 的框,这类框在计算 DFL loss 时会产生无穷大。
4.3 现象:GPU 显存足够但训练速度极慢
常见原因是workers设得太小,数据加载成了瓶颈。在 CUDA 设备上,workers建议设为 CPU 核心数的 1 到 2 倍,但不要超过 16,否则进程调度开销反而拖慢速度。另一个原因是图像尺寸太大,imgsz=1280比imgsz=640的计算量大约翻两番,如果 GPU 是入门级,优先保证batch能跑满,再考虑提imgsz。
4.4 现象:验证集 mAP 比训练集低很多
无人机车辆检测里,如果训练集和验证集的拍摄高度、光照条件差异大,过拟合会非常明显。解决办法是在data.yaml里把val指向和训练集同分布的图像,或者用mosaic、mixup、copy_paste等增强手段提高泛化。YOLO11 默认开启 mosaic,但mixup和copy_paste需要手动在train_args里加,注意copy_paste对实例分割任务更有效,纯检测任务收益有限。
4.5 现象:CPU 训练时内存爆满
CPU 训练没有显存限制,但内存容易被数据加载和模型副本吃满。batch=4、workers=2是保守配置,如果内存仍然不够,把imgsz降到 640,或者用yolo11n.pt而不是yolo11s.pt。另外,CPU 训练时amp必须关掉,半精度在 CPU 上不仅不加速,还会因为类型转换增加内存开销。
5. 从 1000 张图到可用模型:小目标增强与验证技巧
1000 张图在无人机车辆检测里属于小样本,直接训 YOLO11s 很容易过拟合。我一般会做两件事:一是用imgsz=960配合close_mosaic=10,让模型在训练末期看到更多真实尺度的小目标;二是在验证阶段用conf=0.001而不是默认的 0.25,因为小目标的置信度普遍偏低,默认阈值会漏掉大量正确预测。下面这个验证脚本可以输出不同conf阈值下的 mAP 曲线,帮助判断模型的实际可用区间。
from ultralytics import YOLO model = YOLO("./runs/detect/train/weights/best.pt") for conf in [0.001, 0.01, 0.05, 0.1, 0.25]: metrics = model.val(data="./drone_vehicle/data.yaml", conf=conf, iou=0.5) print(f"conf={conf}, mAP50={metrics.box.map50:.4f}, mAP50-95={metrics.box.map:.4f}")跑完这个循环,如果conf=0.001时 mAP50 比conf=0.25高 10 个点以上,说明模型对小目标的置信度校准不好,可以考虑在训练时加label_smoothing或者用focal_loss替代默认的 BCE loss。YOLO11 的损失函数在ultralytics/utils/loss.py里,改起来需要一定代码量,更简单的做法是增加小目标在训练集中的比例,或者用copy_paste把车辆实例复制到更多背景上。
另一个实用技巧是测试时增强(TTA)。YOLO11 的val接口支持augment=True,会对每张图做多尺度翻转推理再合并结果,对小目标检测通常有 1 到 3 个点的 mAP 提升,代价是推理时间翻倍。如果部署环境对延迟不敏感,这个开关值得打开。
metrics = model.val(data="./drone_vehicle/data.yaml", augment=True, conf=0.01) print(metrics.box.map50)最后说一个我踩过的坑:无人机图像里的车辆方向任意,YOLO11 默认的水平框在密集场景下重叠严重,NMS 后容易漏检。如果数据里车辆朝向一致(比如高速公路场景),可以尝试旋转框检测,但 YOLO11 官方对旋转框的支持不如水平框成熟,需要改数据格式和损失函数。对于 1000 张图的规模,我建议先把水平框做到 mAP50 在 0.7 以上,再考虑是否值得投入旋转框。希望帮到你。
本文还有配套的精品资源,点击获取