ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

YOLO路标检测数据集:900张已标注图像与训练避坑指南

YOLO路标检测数据集:900张已标注图像与训练避坑指南 简介面向YOLO目标检测与路标识别任务的数据集资源包包含900张道路场景图像及对应VOC、YOLO两种格式的标注文件可直接用于模型训练与验证。资源整体设计注重工程落地参数化编程、参数便于调整代码思路清晰且注释明确适合计算机、电子信息工程、数学等专业学生完成课程设计、期末大作业或毕业设计时快速上手。压缩包共2635个文件其中png图像877张xml标注877个txt标注文件881个VOC格式方便标注可视化与格式转换YOLO格式可无缝接入YOLO系列模型训练流程压缩包大小约218.43MB目录结构简洁便于按需取用。目前已有217人学习下载能够帮助读者免去手动标注的繁琐过程将精力集中在网络配置、训练调参与结果分析上同时附有作者整理的更多数据集与仿真源码下载列表适合需要系统开展目标检测实验的初学者及进阶开发者。1. YOLO 目标检测路标数据集900 张已标注图像直接省掉一周标注时间做目标检测训练模型本身不复杂数据集才是真正耗时的地方。拿路标检测这种场景来说自己开车跑街拍图再拿 labelImg 或 X-AnyLabeling 一张张框900 张图至少两三天框偏了、类别标错还要返工。这份资源正好补上这一环900 张路标图像每张都有对应的 YOLO 格式 .txt 和 VOC 格式 .xml 标注文件解压之后就能直接当训练集用。对课程设计、期末大作业、毕业设计这些场景它把“数据准备”这个最磨人的阶段直接压缩掉对已经在跑 YOLO 工程的开发者这份数据也可以作为迁移学习的底料在已有标注基础上继续扩展类别或做场景适配。下面从数据集结构讲起把格式转换、训练参数、以及我实际踩过的坑一次说清楚。2. 路标数据集结构拆解YOLO 与 VOC 标注文件的对应关系和读取方式2.1 解压后的目录组织与命名对应规则压缩包解开之后图像命名基本是 road 开头加编号像 road51.png、road7.png、road22.png 这种。目录组织方式常见有两种一种是把 images、labels、voc 分成三个平行目录另一种是套用 JPEGImages、Annotations 这种老 VOC 布局。不管哪种核心对应规则只有一个图像文件名和标注文件名同名只是扩展名不同。road_dataset/ ├── images/ # 全部图像 │ ├── road51.png │ ├── road7.png │ └── ... ├── labels/ # YOLO 格式同名 txt │ ├── road51.txt │ ├── road7.txt │ └── ... └── voc_annotations/ # VOC 格式同名 xml ├── road51.xml ├── road7.xml └── ...拿到数据集第一件事不是直接训练而是把文件数量核对一遍。我一般是写个几十行的扫描脚本统计图片、txt、xml 三类文件数量并且找出“有图无标注”和“有标注无图”的文件名。900 张图配 900 个 txt、900 个 xml 才算完整对不上的样本先移出目录否则训练时 dataloader 会在读取到一半时突然报错。import os from collections import defaultdict def scan_dataset(root): counts defaultdict(int) base_names defaultdict(set) for dirpath, _, files in os.walk(root): for f in files: name, ext os.path.splitext(f) ext ext.lower() if ext in (.jpg, .jpeg, .png): counts[img] 1 base_names[img].add(name) elif ext .txt: counts[txt] 1 base_names[txt].add(name) elif ext .xml: counts[xml] 1 base_names[xml].add(name) return counts, base_names counts, base_names scan_dataset(road_dataset) print(counts) print(缺txt的图:, base_names[img] - base_names[txt]) print(缺xml的图:, base_names[img] - base_names[xml])这套扫描逻辑虽然简单但它能过滤掉最常见的两类数据问题一类是文件名里带了空格或者特殊字符导致标注和图像对不上另一类是同一个图像存在 .jpg 和 .png 两种副本被框架当成两张图。路标数据里的图是 png 居多的如果之后你自己补数据最好统一后缀不要混用。2.2 YOLO 标注格式逐行解读五个字段还原出像素框YOLO 的 .txt 每一行描述一个目标框五个字段顺序是 class_id、x_center、y_center、width、height。关键点在于后四个值全部是归一化坐标范围 0 到 1必须乘以图像宽高才能得到像素值。我见过不少人把 x_center 当成 xmin 直接用结果画出来的框全部偏到画面左下角。import cv2 def yolo_txt_to_boxes(txt_path, img_w, img_h): boxes [] with open(txt_path, r) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue cls_id int(parts[0]) x_c float(parts[1]) * img_w y_c float(parts[2]) * img_h box_w float(parts[3]) * img_w box_h float(parts[4]) * img_h x1 int(x_c - box_w / 2) y1 int(y_c - box_h / 2) x2 int(x_c box_w / 2) y2 int(y_c box_h / 2) boxes.append((cls_id, x1, y1, x2, y2)) return boxes img cv2.imread(road_dataset/images/road51.png) h, w img.shape[:2] for cls_id, x1, y1, x2, y2 in yolo_txt_to_boxes(road_dataset/labels/road51.txt, w, h): cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, str(cls_id), (x1, y1 - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2) cv2.imwrite(check_road51.png, img)这里把 x_c、y_c 经乘法还原成像素坐标再做半宽度的加减得到边框的左上和右下角。检测任务对边框坐标的精度要求是亚像素级别也可接受所以 int 强转没问题但要提醒一句如果后面要做的是实例分割而不是框检测浮点坐标不能丢。还一个容易被忽略的细节txt 文件里如果出现空行或行首空格split 之后长度不等于 5这段脚本会直接跳过它训练时 YOLO 也会丢弃无效行。这个数据集我检查过整体比较干净但如果你在 Windows 上编辑过标注文件换行符被改掉之后容易出现这类问题。2.3 VOC 标注 XML 结构与两类标签的换算关系VOC 格式走的是 XML 树形结构目标框坐标存放在 object 节点下的 bndbox 里而且 xmin、ymin、xmax、ymax 都是像素绝对值语义非常直观。一个典型的路标标注 XML 长这样annotation folderimages/folder filenameroad22.png/filename size width1280/width height720/height depth3/depth /size object namestop/name bndbox xmin480/xmin ymin392/ymin xmax610/xmax ymax518/ymax /bndbox /object /annotation这里最容易翻车的点是 size 节点与实际图像尺寸不一致。比如图像被缩放之后XML 里的 width/height 没同步更新后续做归一化换算时坐标全部偏移。所以在任何转换脚本里我都不建议直接信任 XML 里的 size而是用 cv2.imread 读一遍图像拿真实宽高除非你很清楚数据没有被处理过。接下来做一次最基本的校验把 XML 里的 bndbox 和图像宽高一起解析出来统计所有框是否越界。这个校验在后面的 5.5 节还会用到先把底子打好。VOC 和 YOLO 的换算关系可以归纳成一组公式x_center (xmin xmax) / 2 / img_widthy_center (ymin ymax) / 2 / img_heightbox_w (xmax - xmin) / img_widthbox_h (ymax - ymin) / img_height。下一章的转换脚本就是基于这组公式展开的。3. 把 VOC 转成 YOLO 格式转换脚本、数据集划分与路径处理3.1 类别映射与 VOC 转 YOLO 脚本数据集里同时给了 YOLO 和 VOC 两种标注按说不存在转换需求但实际项目中经常遇到一种情况你需要在原本的类别基础上做合并比如把多种限速标志合并成一个 speed_limit 类把 stop、让行这些合并成 priority 类。这时候直接用原始 txt 改会非常痛苦从 XML 重新生成一遍反而最快。这也是我建议把 XML 保留着别删的原因。先写一个类别统计脚本看这份数据里到底有哪些路标类别各自框数量是多少。import glob import xml.etree.ElementTree as ET xml_files glob.glob(road_dataset/voc_annotations/*.xml) category_counter {} for xml_path in xml_files: root ET.parse(xml_path).getroot() for obj in root.findall(object): name obj.find(name).text category_counter[name] category_counter.get(name, 0) 1 for name, count in sorted(category_counter.items(), keylambda x: x[1], reverseTrue): print(name, count)拿到类别统计之后你需要决定一件事是按原样训练还是合并类别。如果按原样class_list 的排序别乱一旦定了后面所有脚本都按住它走。如果合并在转换脚本里多加一张映射表即可。import os import glob import xml.etree.ElementTree as ET def convert_voc_to_yolo(xml_path, class_list, merge_mapNone): tree ET.parse(xml_path) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) lines [] for obj in root.findall(object): name obj.find(name).text if merge_map: name merge_map.get(name, name) if name not in class_list: continue cls_id class_list.index(name) bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) dw 1.0 / img_w dh 1.0 / img_h x_center (xmin xmax) / 2.0 * dw y_center (ymin ymax) / 2.0 * dh box_w (xmax - xmin) * dw box_h (ymax - ymin) * dh lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) return lines class_list [speed_limit, stop, yield, crosswalk, no_entry] xml_dir road_dataset/voc_annotations out_dir road_dataset/labels os.makedirs(out_dir, exist_okTrue) for xml_path in glob.glob(os.path.join(xml_dir, *.xml)): lines convert_voc_to_yolo(xml_path, class_list) txt_name os.path.splitext(os.path.basename(xml_path))[0] .txt with open(os.path.join(out_dir, txt_name), w) as f: f.write(\n.join(lines) \n)脚本里 dw、dh 是宽高的倒数相乘不只是省一次除法更重要的是避免在循环里反复创建浮点数结果。class_list 的顺序就是最终训练时 class_id 的编号改顺序等于改标注训练前一定要和 data.yaml 里的 names 对照一遍。merge_map 参数用于类别合并比如{speed_limit_30: speed_limit, speed_limit_60: speed_limit}这样能把子类别并成一个大类对样本不均衡也有缓解作用。3.2 训练集与验证集划分比例和随机种子900 张图建议按 8:2 划分也就是 720 张训练、180 张验证。划分时一定按图像划分而不是按单个标注框划分。曾经见过有人在切片层面做划分同一条路标同时出现在训练集和验证集里评估指标虚高等于白训。import random import os from glob import glob images sorted(glob(road_dataset/images/*.png)) random.seed(42) random.shuffle(images) train_imgs images[: int(len(images) * 0.8)] val_imgs images[int(len(images) * 0.8):] def write_list(path, img_list): with open(path, w) as f: for img in img_list: f.write(img.replace(\\, /) \n) write_list(train.txt, train_imgs) write_list(val.txt, val_imgs) print(ftrain: {len(train_imgs)}, val: {len(val_imgs)})这里有两个细节一是 random.seed(42) 必须写死否则每次划分随机结果不同实验无法复现二是写入时统一 replace(\, /)Windows 路径默认带反斜杠YOLO 某些版本读到混合路径会直接罢工。list 文件里每行一张图的绝对路径不要放相对路径因为训练进程的工作目录不一定等于你解压数据集的目录。3.3 组织 YAML 配置路径字段的坑数据准备好了接下来把数据集描述写进 yaml。无论是 YOLOv5 还是 YOLOv8都支持在一份 yaml 里声明根路径、训练集列表、验证集列表、类别数和类别名。# road_dataset.yaml path: D:/datasets/road_dataset train: train.txt val: val.txt nc: 5 names: [speed_limit, stop, yield, crosswalk, no_entry]这套配置我看到的问题主要集中在 path 字段上。如果你在 train/val 里写绝对路径而又同时指定了 path框架内部可能会把两者拼接成重复路径报错日志非常绕。另一个问题是 path 的结尾不要带斜杠否则拼接时会出现双斜杠某些文件系统会不当回事但模型的输出路径和日志会被污染。还有一点很少有人提yaml 文件本身别用 UTF-8 with BOM 编码保存YAML 解析器对 BOM 的处理能力参差不齐容易报出莫名其妙的 key 错误。用 VS Code 或 Notepad 转成 UTF-8 无 BOM 就行。4. 训练 YOLO 前的参数设置超参选择、类别文件与预训练权重4.1 预训练权重和模型规模怎么选900 张图属于典型的中小规模数据集直接用 COCO 预训练权重做迁移学习是成本最低、效果最稳的方案。模型规模上优先 YOLOv8s 或 YOLOv5s。理由很简单数据量摆在那里用 l/x 这种大模型训练时间长且容易过拟合泛化反而不如 s。如果你手里的显卡是 8GB 显存YOLOv8s 配 640 输入、batch 16 基本是极限如果是 16GB可以放心上 YOLOv8m。yolo detect train \ modelyolov8s.pt \ dataroad_dataset.yaml \ epochs100 \ imgsz640 \ batch16 \ workers8 \ device0model 参数指定预训练权重路径yolov8s.pt 首次使用时需要联网从官方地址拉取如果不方便下载可以手动把权重文件放到项目目录下再引用。epochs 给 100 是个保守值训练过程中可以加 early stopping或者每轮看 val loss连续 10 个 epoch 不降就停。imgsz 选 640 而不是 320是因为路标在图像中面积占比偏小低分辨率容易把细节模糊掉。workers 是数据加载线程数配 8 以上可以避免 GPU 空转。4.2 数据增强参数路标场景的特殊调整YOLOv8 默认开启 Mosaic、随机平移、缩放、颜色抖动等增强。路标检测和通用物体检测有一个明显差异路标是刚性目标不会旋转也不会变形而且文字信息对方向敏感。所以我把 degrees 直接设成 0避免模型学到歪斜的伪样本。mosaic 概率也可以从默认值调低因为 mosaic 会把四张图拼在一起小目标被进一步缩小后模型很难学到有效特征。# hyp.road.yaml 基于默认超参修改 hsv_h: 0.015 hsv_s: 0.5 hsv_v: 0.3 degrees: 0.0 mosaic: 0.5 fliplr: 0.5颜色增强保留是因为实际场景里路标会受到光照、阴影、黄昏滤镜的影响hsv 抖动可以提升模型的鲁棒性。fliplr 水平翻转取决于你的任务如果路标上有文字且文字语义关键翻转会产生“反字”这种现实中几乎不存在的样本建议关掉如果只做标志形状检测开 fliplr 问题不大。还有一个参数 scale 默认是 0.5如果训练时发现小目标路标漏检严重可以试着降到 0.3让目标缩放幅度更保守。4.3 启动训练后如何判断一切正常训练跑通不代表训练正常。启动后前 10 个迭代我就盯三个指标总 loss 是否在稳步下降box loss 和 cls loss 是否在同一个数量级GPU 利用率是否超过 70%。利用率过低通常是数据加载瓶颈调大 workers 并确认 pin_memoryTrue。如果 loss 一开始就是 nan不用怀疑第一优先查学习率第二查标注坐标越界第三查是否混入了损坏图像。日志里出现警告行不用慌但要统计一下警告次数几百条警告说明数据集质量有问题回到第 2 章的扫描脚本重查。5. 路标训练避坑指南5 个翻车现场与可复现的解决步骤5.1 txt 类别编号和 yaml 类别顺序不对齐现象训练能启动loss 也在降但验证集 mAP 很低可视化结果里模型把所有限速标志都标成 stop类别整个错位。原因txt 里的 class_id 沿用数据提供方的编号而你自己写的 yaml names 是另一套顺序甚至少写了一个类别导致全体错位。解决先用 3.1 的统计脚本列出 XML 里的全部 name定出唯一的一份 class_list。然后写一个校验脚本把 txt 里出现的 class_id 最大值和 len(yaml names) 对齐不一致就立即报错。换数据集后第一个动作就是跑这个校验。5.2 训练集路径带中文或反斜杠一启动就找不到文件现象报了 FileNotFoundError或者提示 no labels found in image list手动打开路径又确实存在。原因Windows 下路径用反斜杠YOLO 读取 train.txt 解析路径时对混合分隔符处理不稳路径含中文时部分依赖库的默认编码不是 UTF-8直接挂掉。解决数据集放纯英文目录生成的 train.txt/val.txt 统一把反斜杠替换成正斜杠并且写绝对路径。这招解决八成路径问题。如果还不行检查 yaml 里 path 尾部有没有残留斜杠。5.3 部分类别样本太少尾部类别漏检严重现象限速、stop 的检测精度不错但样本少的类别 recall 接近 0bad case 分析里基本没有它的影子。原因900 张图里不同路标分布极不均衡尾部类别占了总体框数量的 5% 都不到模型被头部类别主导。要处理前先量化按类别统计框数量再看方案类别框数量speed_limit1120crosswalk310stop260yield180no_entry42解决尾部类别只有几十张时补图是最快路径补图不方便就在训练时对这个类别做复制上采样一个框可以重复出现在多个 epoch 里。再或者按类别调整 cls loss 权重。三种方法可以组合但补图优先权重调整最后兜底。5.4 batch 调小之后 loss 变成 nan现象显存不够把 batch 从 16 改成 4训练没跑几步 loss 变 nan。原因batch 过小导致 BN 统计量不稳定学习率又没按比例缩放梯度爆炸。图像里出现全黑或全白样本也是 BN 方差归零的原因之一。解决把 learning rate 从默认 0.01 降到 0.002或者保持 batch 16 不变用梯度累积把 accumulate 调到 4等效更新步幅仍是 64。同时检查图像里有没有极端曝光样本有的话加一层归一化或直接丢弃。5.5 标注框越界触发过滤训练样本悄悄减少现象训练日志不断出现 Ignoring corrupted image and/or label 警告训练完发现参与样本少了 10% 以上。原因部分 XML 框的 xmax 大于图像宽度或 ymin 为负归一化后坐标范围超过 0 到 1被框架当作无效标注跳过。解决用脚本把越界坐标裁剪到图像范围并过滤宽高小于 1 像素的退化框。归一化公式本身不变只是在做除法前先做 clamp。6. 验证与进阶用 mAP 实测模型表现并扩展到自定义路标检测6.1 验证集评估与结果解读训练完别急着部署先用验证集做一次完整评估。对一个路标检测模型来说我自己的及格线是 mAP50 不低于 0.9mAP50-95 不低于 0.75。如果 mAP50-95 显著低于 mAP50说明框的定位精度不够也就是预测框和真值框的 IoU 很难到 0.95问题多半出在小目标上。把每个类别的 mAP50 拉一张表重点看尾部类别样本少的类别如果也能超过 0.8说明训练基本健康。from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) metrics model.val(dataroad_dataset.yaml, splitval, imgsz640) print(fmAP50: {metrics.box.map50:.4f}) print(fmAP50-95: {metrics.box.map:.4f}) for cls_id, map50 in enumerate(metrics.box.maps): print(fclass {cls_id}: {map50:.4f})如果验证指标不达标回到第 5 章排查不要一上来就换模型结构。数据标注错位、路径缺失、类别顺序错乱这些低级问题的改善空间通常比换大模型高一个量级。验证时把 IoU 阈值和 conf 阈值都调到和实际部署一致比如转 ONNX 部署时 conf 往往要调到 0.25 以上那评估也应该用 0.25 而不是默认的 0.001。6.2 扩展思路把这份数据作为迁移学习底料这份 900 张的数据集本身能直接出成果但更大的价值在它的可扩展性。你想加一个“前方施工”类别做法是在现有 best.pt 基础上继续 fine-tune而不是从零重训。数据量不大几十张新类别标注图足够把一个新类别的精度拉到可用水平因为模型已经学会“路标长什么样”新类别只需要学会区分细微差异。nc 从 5 改成 6names 增加一项把新数据放到同一目录再跑 20 到 30 个 epoch 就行。这时候原来类别和新增类别的顺序必须保持稳定new class 加到 names 尾部别插队否则之前的 5 类全部错位。如果训练环境里没有 GPU这个数据集还能反过来用直接拿公开预训练权重在这份验证集上做对比测试选出一个对路标友好的基座模型再部署到边缘设备。YOLOv8 的 export 模式导出 ONNX之后用 ONNX Runtime 或 TensorRT 加速在 RK3588 这类板子上跑实时路标识别没有问题配合摄像头做辅助驾驶原型也是常见的玩法。最后提一个我的习惯从那以后每次拿到新数据集我都会强制先走一遍“统计类别分布 → 转换格式校验 → 抽样可视化验证”这三步跑完这三步未必保证一次成功但能把 80% 以上的翻车问题拦在训练之前。这套流程看起来繁琐实际批量执行不到二十分钟收益远大于折腾排错的时间。这套 900 张的已标注数据从解压到第一次跑通训练一个晚上完全够用剩下的时间就留给调参和踩坑。希望这篇笔记能帮你在路标检测这条路上少走弯路。本文还有配套的精品资源点击获取
返回列表