☰
航拍路面标志检测数据集:VOC与YOLO双格式实战指南
2026/9/28 23:55:29 网站建设 项目流程

简介:本资源为航拍路面直行与转弯标志检测数据集,面向从事交通标志识别、自动驾驶感知及计算机视觉研究的学生与算法工程师,可用于目标检测模型的训练、验证与对比实验。数据集同时提供Pascal VOC与YOLO两种标注格式,包含jpg图片及对应的xml、txt标注文件,标注类别共9类,覆盖left、left_back、left_right、right、straight、straight_back、straight_left、straight_left_right、straight_right等直行与转向组合标志。压缩包为7z格式,共1700个文件,其中jpg图片566张、xml标注566个、txt标注568个,整体约236.49MB,目录结构清晰,便于直接接入主流检测框架。目前已有144人学习下载。由于航拍视角下路面标志样本较难获取,该数据集经过增强处理,适合用于多类别交通标志检测的模型调优与消融实验,建议下载前先查看图片预览确认场景与标注符合需求。

1. 航拍路面标志检测数据集:566 张 9 类,VOC 与 YOLO 双格式到底怎么落地

做路面标志检测的项目,最卡脖子的往往不是模型结构,而是数据。尤其是航拍视角下的直行、转弯箭头这类地面导向标志,公开数据里能直接用的少得可怜,自己拿无人机飞一遍再逐张标,成本高到劝退。这份航拍路面直行和转弯标志检测数据集,566 张 jpg 图片,配套 566 个 Pascal VOC 格式 xml 和 566 个 YOLO 格式 txt,覆盖 left、left_back、left_right、right、straight、straight_back、straight_left、straight_left_right、straight_right 共 9 个类别,正好卡在「类别够细、量级够跑通」这个区间。它适合两类人:一类是想快速验证航拍地面标志检测 pipeline 的算法工程师,另一类是拿它做课程设计或毕设、需要现成标注又不想从零标的学生。需要提前说清楚的是,这份数据做了增强,图片偏「难找」风格,下载前务必看预览图确认分布符合预期,别下完才发现场景不对。

2. 先搞懂 VOC 与 YOLO 双格式:为什么同一批图要存两套标注

2.1 两种格式的本质差异

Pascal VOC 和 YOLO 描述的是同一件事——目标框在哪、是什么类——但坐标系和存储方式完全不同。VOC 用 xml,一个文件对应一张图,框用左上角、右下角的绝对像素坐标xmin, ymin, xmax, ymax表示,类别名直接写成字符串。YOLO 用 txt,同样一图一文件,但每行是class_id x_center y_center width height,四个值全部归一化到 0~1 之间,类别用从 0 开始的整数索引。

这个差异决定了你拿到数据后第一件事是确认「类别名到 id 的映射」。VOC 里写的是left、straight_left这种可读名字,YOLO 里只有0、1、2。如果映射表对不上,训练时模型学到的就是错位的类别,loss 能降但 mAP 一塌糊涂,这种坑我见过不止一次。

2.2 9 个类别的索引映射

这份数据的类别名带明显的组合语义,left_back、straight_left_right这种是复合导向标志。按常见约定,映射一般按字母序或标注工具导出顺序排,但不要假设,一定要自己从 xml 里抽出来核对。下面这段脚本就是干这个的:

import os import xml.etree.ElementTree as ET from collections import Counter xml_dir = "annotations" # VOC xml 所在目录 counter = Counter() for f in os.listdir(xml_dir): if not f.endswith(".xml"): continue tree = ET.parse(os.path.join(xml_dir, f)) root = tree.getroot() for obj in root.findall("object"): name = obj.find("name").text.strip() counter[name] += 1 # 按出现频次打印,顺便确认类别总数是否为 9 for name, cnt in counter.most_common(): print(f"{name}: {cnt}") print("类别总数:", len(counter))

逻辑说明:遍历所有 xml,用 ElementTree 解析,逐个object节点取name文本并计数。跑完你会得到每个类别的实例数,如果打印出 9 个类别,说明标注完整;如果少于 9,可能是某些类别在这批图里没出现,或者 xml 有损坏。参数上唯一要改的是xml_dir,指向你解压后的标注目录。

2.3 从 VOC 转 YOLO 的坐标换算

虽然数据集已经给了 YOLO txt,但你要做数据清洗、重新划分训练集,或者想加自己的增强时,还是得会自己转。核心公式就四个:

x_center = (xmin + xmax) / 2 / img_w y_center = (ymin + ymax) / 2 / img_h width = (xmax - xmin) / img_w height = (ymax - ymin) / img_h

img_w、img_h 必须从对应 jpg 读,不能写死。航拍图分辨率不统一是常态,写死尺寸会让框整体偏移。下面给一个可复用的转换脚本:

import os import xml.etree.ElementTree as ET from PIL import Image classes = ["left", "left_back", "left_right", "right", "straight", "straight_back", "straight_left", "straight_left_right", "straight_right"] class_to_id = {c: i for i, c in enumerate(classes)} xml_dir = "annotations" img_dir = "images" out_dir = "labels" os.makedirs(out_dir, exist_ok=True) for f in os.listdir(xml_dir): if not f.endswith(".xml"): continue stem = os.path.splitext(f)[0] img_path = os.path.join(img_dir, stem + ".jpg") if not os.path.exists(img_path): print("缺图,跳过:", stem) continue w, h = Image.open(img_path).size tree = ET.parse(os.path.join(xml_dir, f)) lines = [] for obj in tree.getroot().findall("object"): name = obj.find("name").text.strip() if name not in class_to_id: continue bnd = obj.find("bndbox") xmin = float(bnd.find("xmin").text) ymin = float(bnd.find("ymin").text) xmax = float(bnd.find("xmax").text) ymax = float(bnd.find("ymax").text) # 裁剪越界框,航拍标注偶尔会超出图像边界 xmin, xmax = max(0, xmin), min(w, xmax) ymin, ymax = max(0, ymin), min(h, ymax) xc = (xmin + xmax) / 2 / w yc = (ymin + ymax) / 2 / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h lines.append(f"{class_to_id[name]} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}") with open(os.path.join(out_dir, stem + ".txt"), "w") as fp: fp.write("\n".join(lines))

逻辑说明:先建类别到 id 的字典,保证顺序固定;读图拿真实宽高;解析每个框做归一化;越界坐标用max/min裁到图像范围内,避免出现负值或大于 1 的归一化坐标——YOLO 训练时遇到这种值会直接报错或静默丢弃。参数上classes列表的顺序就是最终 class_id,务必和你训练配置里的names一致。

3. 用这份数据跑通 YOLO 训练:目录结构、配置与启动

3.1 目录组织与 data.yaml

YOLO 系列(v5/v8/v11 都类似)对目录结构有约定,最省事的方式是:

dataset/ images/ train/ val/ labels/ train/ val/ data.yaml

图片和标签同名不同后缀,放在对应 split 下。data.yaml是训练的入口配置:

path: /abs/path/to/dataset train: images/train val: images/val nc: 9 names: 0: left 1: left_back 2: left_right 3: right 4: straight 5: straight_back 6: straight_left 7: straight_left_right 8: straight_right

nc必须等于 9,names的索引必须和 txt 里的 class_id 严格对应。这里最容易翻车的是path用了相对路径又没搞清相对谁,建议直接写绝对路径。

3.2 划分训练集与验证集

566 张不算多,验证集比例别超过 15%,否则训练样本更少。按 8.5:1.5 划,验证集约 85 张。划分脚本:

import os, random, shutil random.seed(42) # 固定种子,保证可复现 img_dir = "images" lbl_dir = "labels" out = "dataset" ratio = 0.15 stems = [os.path.splitext(f)[0] for f in os.listdir(img_dir) if f.endswith(".jpg")] random.shuffle(stems) n_val = int(len(stems) * ratio) val_stems = set(stems[:n_val]) for split in ["train", "val"]: os.makedirs(f"{out}/images/{split}", exist_ok=True) os.makedirs(f"{out}/labels/{split}", exist_ok=True) for stem in stems: split = "val" if stem in val_stems else "train" shutil.copy(f"{img_dir}/{stem}.jpg", f"{out}/images/{split}/{stem}.jpg") shutil.copy(f"{lbl_dir}/{stem}.txt", f"{out}/labels/{split}/{stem}.txt")

逻辑说明:固定random.seed(42)让每次划分结果一致,方便复现实验;先 shuffle 再切,避免原始文件按类别聚集导致验证集分布偏。参数ratio控制验证集比例,数据量小的时候可以降到 0.1。

3.3 启动训练与关键参数

以 YOLOv8 为例,一条命令就能跑:

yolo detect train \ data=dataset/data.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=20 \ project=runs/train \ name=road_sign

参数说明:model选 nano 版是因为 566 张图撑不起大模型,容易过拟合;imgsz=640是航拍小目标的常见折中,标志在图中占比不大,再小会丢细节;batch=16按显存调,显存不够就降到 8;patience=20表示 20 轮没提升就早停,小数据集上很实用。训练完看runs/train/road_sign下的results.csv和混淆矩阵,重点看straight_left_right这种复合类别的召回,它最容易和straight_left混。

4. 避坑与排查:这份数据集最容易翻车的五个地方

4.1 现象:训练 loss 正常但 mAP 接近 0

原因:YOLO txt 里的 class_id 和data.yaml的names顺序对不上。数据集给的 txt 是按它自己的顺序生成的,你如果按字母序重排了names,映射就错位了。

解决:用第 2.2 节的脚本从 xml 抽出真实类别顺序,或者直接读一个 txt 看最大 id 是不是 8,再对照names逐条核对。别嫌麻烦,这一步省下的时间远小于重训一轮。

4.2 现象:报错「negative coordinates」或框跑到图外

原因:VOC 原始标注里有框超出图像边界,转换时没裁剪,归一化后出现负值或大于 1。

解决:转换脚本里加max(0, xmin)、min(w, xmax)这类裁剪,如 2.3 节所示。已经转好的 txt 可以写个校验脚本扫一遍,把越界行删掉或修正。

4.3 现象:验证集指标虚高,实际推理一塌糊涂

原因:数据做了增强,同一张原图可能衍生出多张相似图,随机划分时相似图同时进了训练和验证,造成数据泄漏。

解决:如果文件名或内容能看出增强来源,按「原图分组」划分,同一组的衍生图只进一个 split。实在分不清,就把验证集比例调小并人工抽查几张,确认没有肉眼几乎一样的图跨集。

4.4 现象:某些类别一个框都检不出

原因:9 个类别里left_back、straight_left_right这类复合标志本身样本就少,566 张里可能只有个位数实例,模型学不动。

解决:先统计每类实例数(2.2 节脚本),对少于 20 个实例的类别,要么针对性补充数据,要么在训练时用类别权重、focal loss 缓解。别指望靠调 epoch 解决样本不平衡。

4.5 现象:图片能读但标注文件缺失或为空

原因:解压不完整,或者增强过程中个别图没配上标注。

解决:写个一致性检查,遍历 images 下每个 jpg,确认同名 txt 存在且非空。空 txt 在 YOLO 里表示「无目标」,如果本应有目标就是漏标,会拉低召回。

import os img_dir, lbl_dir = "dataset/images/train", "dataset/labels/train" for f in os.listdir(img_dir): stem = os.path.splitext(f)[0] lbl = os.path.join(lbl_dir, stem + ".txt") if not os.path.exists(lbl): print("缺标注:", stem) elif os.path.getsize(lbl) == 0: print("空标注:", stem)

5. 进阶:把 9 类复合标志拆成属性组合,提升小样本类别表现

跑通基线之后,真正影响这份数据上限的是复合类别的样本稀疏问题。straight_left_right这种三向标志,本质是「直行 + 左 + 右」三个属性的组合,硬当成一个独立类别去学,样本少、类间相似度高,模型很难收敛。我一般会做一步属性拆解:把 9 个类别映射成「方向属性」的多标签表示,比如straight_left→{straight:1, left:1, right:0, back:0},然后用多标签分类头或者把属性当辅助任务联合训练。

具体做法有两种。轻量的一种是在数据层面做标签平滑式的软标签,把复合类别的 one-hot 换成属性向量,改分类 loss 为 BCE。重的一种是双头结构,检测头照常出框,分类头拆成主类别 + 属性两个分支,属性分支用 4 个二分类 sigmoid。后者改动大,但小样本类别召回通常能涨几个点。

验证这套改动有没有用,别只看整体 mAP,要单独拉出straight_left_right、left_back这些稀有类的 AP 和召回对比。我习惯在val上跑完预测后,用下面这段把每个类别的指标单独打出来:

from ultralytics import YOLO model = YOLO("runs/train/road_sign/weights/best.pt") metrics = model.val(data="dataset/data.yaml", split="val") # 逐类打印 AP@0.5,重点盯稀有类 for i, name in enumerate(metrics.names.values()): print(f"{name}: mAP50={metrics.box.ap50[i]:.4f}")

逻辑说明:metrics.box.ap50是按类别索引排的数组,metrics.names是 id 到名字的映射,两者对齐就能逐类看。参数上split="val"指定评估集,想更严格可以加conf=0.25过滤低置信预测。

还有一个容易被忽略的点:航拍图的拍摄高度、光照、地面材质差异很大,这份数据做了增强,泛化性比纯原始图好,但也意味着你在自己场景上微调时,学习率要调小(比如lr0=0.001),否则容易把预训练学到的通用特征冲掉。我吃过这个亏,直接拿默认 lr 在自有数据上微调,结果模型对原数据的性能掉了一大截,后来每次微调都强制先用小 lr 跑 10 轮 warmup 看曲线再决定要不要放大。

从那以后我每次拿到新数据集,都强制先跑一遍类别统计和标注一致性检查,再动手训练。这份 566 张 9 类的航拍路面标志数据,量不大但类别设计有代表性,拿来练手或做基线都合适,下载前记得对照预览图确认场景。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询