航拍路面标志检测数据集:VOC与YOLO双格式实战指南

发布时间:2026/9/28 23:56:11
航拍路面标志检测数据集:VOC与YOLO双格式实战指南 简介本资源为航拍路面直行与转弯标志检测数据集面向从事交通标志识别、自动驾驶感知及计算机视觉研究的学生与算法工程师可用于目标检测模型的训练、验证与对比实验。数据集同时提供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_himg_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_okTrue) 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.yamlYOLO 系列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_rightnc必须等于 9names的索引必须和 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_okTrue) os.makedirs(f{out}/labels/{split}, exist_okTrue) 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 \ datadataset/data.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ patience20 \ projectruns/train \ nameroad_sign参数说明model选 nano 版是因为 566 张图撑不起大模型容易过拟合imgsz640是航拍小目标的常见折中标志在图中占比不大再小会丢细节batch16按显存调显存不够就降到 8patience20表示 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(datadataset/data.yaml, splitval) # 逐类打印 AP0.5重点盯稀有类 for i, name in enumerate(metrics.names.values()): print(f{name}: mAP50{metrics.box.ap50[i]:.4f})逻辑说明metrics.box.ap50是按类别索引排的数组metrics.names是 id 到名字的映射两者对齐就能逐类看。参数上splitval指定评估集想更严格可以加conf0.25过滤低置信预测。还有一个容易被忽略的点航拍图的拍摄高度、光照、地面材质差异很大这份数据做了增强泛化性比纯原始图好但也意味着你在自己场景上微调时学习率要调小比如lr00.001否则容易把预训练学到的通用特征冲掉。我吃过这个亏直接拿默认 lr 在自有数据上微调结果模型对原数据的性能掉了一大截后来每次微调都强制先用小 lr 跑 10 轮 warmup 看曲线再决定要不要放大。从那以后我每次拿到新数据集都强制先跑一遍类别统计和标注一致性检查再动手训练。这份 566 张 9 类的航拍路面标志数据量不大但类别设计有代表性拿来练手或做基线都合适下载前记得对照预览图确认场景。希望帮到你。本文还有配套的精品资源点击获取