简介:这套道路机器人交通标志识别数据集采用VOC标注格式,面向自动驾驶、机器人导航及计算机视觉学习者,覆盖交通灯、马路、左右转、黄线、人行道、机器人等路面导航标志,可用于目标检测、实例分割或语义分类等任务的训练与评估。包内共816个文件,其中407个xml标注文件与407个jpg原图一一对应,xml包含目标类别与边界框坐标,jpg为真实道路视频抽帧画面,另附2个txt说明文件便于理解标注规范,压缩包仅5.75MB,轻量易上手。数据集来源于连续行车场景,标注内容贴近实际环境,能有效检验模型对光照变化、道路标线及交通设施的感知能力,学习者既可直接拆分训练集与验证集,也可替换检测类别进行迁移学习。目前已有836人学习下载,对需要补充路面导航训练样本的研究者或开发者而言,是一份实用且标注规范的入门级数据集。
1. 道路机器人的路面导航标志识别:为什么说VOC格式是这条路的起点
一台园区巡检机器人或配送机器人,要在有行人、有自行车、有同行机器人的路面上正常走,它必须回答几个问题:前方红灯能不能过,路面黄线要不要跟着走,路口有没有人行道和左右转箭头,迎面过来的到底是人还是另一台机器人。这些问题都落在一个共同的技术底座上——目标检测。而目标检测的训练数据,最透明、最不挑工具的数据格式,就是VOC格式。我不打算在这里展开一堆概念,先讲清楚最常用的落地路径:从采集路面图像、按VOC格式标注,到转YOLO训练,再到踩坑排查。适合谁看?手里有路面视觉数据、正准备自己标数据集做识别的工程师,或者想评估这个方向能不能投入的团队。
2. 路面标志识别的VOC数据集怎么做:目录约定与类别定义
2.1 VOC格式的目录约定,和一张标注XML的核心字段
VOC格式本身不是算法,它是存放检测数据集的通用约定。常见做法是放在一个VOCdevkit目录树下:
VOCdevkit/ └── VOC2007/ ├── JPEGImages/ # 原始图像 ├── Annotations/ # 每张图像对应一个XML标注 └── ImageSets/ └── Main/ # train.txt / val.txt / trainval.txt每个XML的内容才是关键。以一张1080p路面图像为例,它的标注文件核心字段长这样:
<annotation> <folder>JPEGImages</folder> <filename>road_clip_001_0001.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>traffic_light_green</name> <bndbox> <xmin>1420</xmin> <ymin>260</ymin> <xmax>1480</xmax> <ymax>420</ymax> </bndbox> </object> <object> <name>yellow_line</name> <bndbox> <xmin>200</xmin> <ymin>850</ymin> <xmax>1700</xmax> <ymax>880</ymax> </bndbox> </object> </annotation>size里的width和height必须和实际图像一致,很多转换脚本会用这两个值做归一化,填错后期会出现大量负坐标。object字段里的name是目标类别名,bndbox写矩形框的左上角和右下角。VOC格式相当直白:一张图一个XML,每个目标一个object,没有多余层级。这也是为什么几乎所有检测框架都留了VOC转换入口,把VOC当中间层是成本最低的选择。
提示:ImageSets/Main下还要有train.txt、val.txt和trainval.txt,每行一个不带.jpg后缀的文件名。后续转YOLO时的数据划分从这些txt里读,不是从Annotations目录里扫。
2.2 路面标志的类别定义:从交通灯到机器人的一套可执行方案
标题里点名了六类目标:交通灯、马路、左右转、黄线、人行道、机器人。落到VOC格式时,这个清单不能直接当成类别名,要做几轮拆解。我的做法是先列一张类别决策表,所有标注员、训练脚本、评估报告都用同一份:
| 目标对象 | 推荐类别名 | 标注意见 |
|---|---|---|
| 交通灯 | traffic_light_red / traffic_light_green / traffic_light_yellow | 按灯色拆类,导航决策依赖颜色,本质上是轻量的语义识别 |
| 马路 | road_area(前方可行驶区域) | 整条马路不适合bbox,只标车前近处有效区域,或改用分割 |
| 左右转 | turn_left / turn_right / turn_straight | 以机器人行驶方向为基准,不能按图里看到的视角定 |
| 黄线 | yellow_line | 避免一条线标成超长框,按固定长度分块 |
| 人行道 | crosswalk | 斑马线区域分段标注,别把路口大片路面圈进来 |
| 机器人 | robot | 其他道路机器人,动态目标,单列一个类 |
交通灯按灯色拆类这里多说一句。有的项目图省事只标一个traffic_light类,训练完才发现模型知道哪里有灯,但不知道灯是什么颜色,下游还要再接分类器去抠图识别,多一环就多一个误差源。路面机器人要的最终信号是“能不能过路口”,所以按红、绿、黄三色拆类,模型一步到位输出灯色,导航决策直接可消费。
马路这个类别是VOC格式的一个边界问题。马路在图像里通常是一整块或长条形,用检测框框出来会带进大量背景像素,模型学不到稳定特征。我一般两条路选一条:要么只标“前方可行驶区域”,约定好近处截断线;要么把车道边界、路沿单独作为line类处理,跟黄线一个思路。后者更贴近导航的实际需要。
黄线的标法直接影响模型能不能用。一条黄线从画面底部一直延伸到远处,整体标一个bbox,模型要么学成“画面下方正中有一大片黄”,要么把远处碎线全漏掉。常见做法是每隔1到1.5米截一段,每段一个小框,段与段允许少量重叠,模型学会的是局部线特征,而不是一个宽高比异常的大框。
机器人这一类的边界要定义清楚。园区混行场景里,行人和机器人的外观差异远距离时并不明显。如果训练数据里经常出现行人,建议额外加一个person类,否则模型为了凑robot的AP把行人框进来,演示时很漂亮,真跑起来就是安全事故。
2.3 图像采集阶段就要为VOC留好余量
很多项目在采集环节图省事,用一段白天的视频就开工,结果夜间、逆光、雨天的性能全是窟窿。路面标志和车牌识别这类任务一样,模型能学到多少与采集覆盖面强相关。我一般要求采集时覆盖三个变量:时段(白天、黄昏、夜晚)、天气(晴、阴、雨后反光)、视角(平视为主,允许轻微俯仰)。分辨率尽量不低于1920x1080,远处转向箭头和交通灯只有十几个像素,分辨率降到640,标注框小到下采样后几乎消失。
采集回来的原始视频,不要抽帧后全部保留。先做一次初筛:运动模糊、过曝、被遮挡超过一半的帧删掉或标记成difficult。可以用OpenCV对每帧做亮度直方图统计,按白天、黄昏、夜间粗略分桶,避免训练集里夜间样本只有几个却不自知。注意VOC的difficult字段在转YOLO时通常会被丢掉,所以确定某张图不适合训练时,直接不进入train.txt比标记difficult更可靠。
3. 从图片到VOC再转YOLO:标注工具、场景划分与转换脚本
3.1 标注工具怎么选:CVAT批量标,LabelImg精修
VOC标注工具有不少,我的选择是CVAT做初标,LabelImg做精修。CVAT可以本地部署,对视频帧序列支持track模式,给第一帧框好,后面几十帧自动插值,路面连续场景能省掉一大半重复劳动。LabelImg是老牌工具,界面轻量、保存加载都很快,适合对CVAT自动插值出现的漂移框做逐帧修正。
不管用哪个工具,导出时都要固定选择VOC格式。CVAT导出格式里选Pascal VOC,它会自动生成JPEGImages、Annotations和ImageSets/Main结构。有个细节,CVAT导出VOC时类别名在object里按创建顺序编号,但那个编号只是标注顺序,不代表类别ID顺序,后面转YOLO时必须以自己维护的classes.txt为准,不能直接信导出文件里的隐含顺序。
3.2 数据划分:按场景分,别按文件随机分
路面图像是从连续视频抽帧出来的,相邻帧几乎一模一样。直接对整个文件夹做随机划分,同一个路口的同一段视频帧会同时出现在train和val里,模型等于先看答案再考试,mAP虚高,跑到新路线上立刻现原形。正确做法是按场景分组:同一视频片段、同一个路口的图像必须分到一起,然后以场景为单位划分训练集和验证集。
下面这个脚本从文件名解析出片段ID,按片段ID分配数据:
import os import random from collections import defaultdict trainval_dir = "VOCdevkit/VOC2007/ImageSets/Main" def parse_clip_id(filename): # 约定文件名格式: clip_id_frame.jpg,例如 road_07_0001.jpg -> road_07 parts = filename.split("_") return "_".join(parts[:-1]) images = [f for f in os.listdir("VOCdevkit/VOC2007/JPEGImages") if f.endswith(".jpg")] clips = defaultdict(list) for img in images: clips[parse_clip_id(img)].append(img) clip_ids = list(clips.keys()) random.shuffle(clip_ids) split = int(len(clip_ids) * 0.8) train_clip_ids = set(clip_ids[:split]) val_clip_ids = set(clip_ids[split:]) def write_set(path, filenames): with open(path, "w") as f: f.writelines(name.replace(".jpg", "") + "\n" for name in sorted(filenames)) train_files = [img for cid in train_clip_ids for img in clips[cid]] val_files = [img for cid in val_clip_ids for img in clips[cid]] write_set(os.path.join(trainval_dir, "trainval.txt"), train_files) write_set(os.path.join(trainval_dir, "train.txt"), train_files) write_set(os.path.join(trainval_dir, "val.txt"), val_files)parse_clip_id取文件名里去掉最后一段帧号的部分。如果命名不带片段ID,采集时就按“场景文件夹/视频名/帧号”组织,或者让CVAT用task id做前缀。trainval.txt是完整集合,train.txt和val.txt是训练、验证子集,YOLO转换时主要读后两个。
提示:划分比例按场景数来,不要按图像数来。如果只有10个场景视频,分8/2没问题;如果某个场景特别长,验证集图片占比会偏高,这是正常的,关键是确保验证集里没有训练场景的镜头。
3.3 VOC转YOLO:转换脚本与四个边界处理
VOC格式不能直接送进YOLO训练。YOLO要的是每张图一个txt文件,每行表示一个目标:class_id cx cy w h,四个值都归一化到0到1。转换脚本的核心是把XML里的bndbox转成中心点加宽高表示:
import os import xml.etree.ElementTree as ET CLASSES = [ "traffic_light_red", "traffic_light_green", "traffic_light_yellow", "road_area", "turn_left", "turn_right", "turn_straight", "yellow_line", "crosswalk", "robot", "person" ] def convert_voc_xml_to_yolo(xml_path, out_dir, img_width, img_height): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.iter("object"): name = obj.findtext("name").strip() if name not in CLASSES: continue bndbox = obj.find("bndbox") xmin = float(bndbox.findtext("xmin")) ymin = float(bndbox.findtext("ymin")) xmax = float(bndbox.findtext("xmax")) ymax = float(bndbox.findtext("ymax")) # 越界裁剪,防止负坐标和超宽高进入训练 xmin = max(0.0, min(xmin, img_width - 1)) xmax = max(0.0, min(xmax, img_width - 1)) ymin = max(0.0, min(ymin, img_height - 1)) ymax = max(0.0, min(ymax, img_height - 1)) if xmax - xmin < 1 or ymax - ymin < 1: continue cx = (xmin + xmax) / 2.0 / img_width cy = (ymin + ymax) / 2.0 / img_height w = (xmax - xmin) / img_width h = (ymax - ymin) / img_height class_id = CLASSES.index(name) lines.append(f"{class_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") out_path = os.path.join(out_dir, os.path.basename(xml_path).replace(".xml", ".txt")) with open(out_path, "w") as f: f.write("\n".join(lines))这段脚本处理了四个容易出问题的点。第一,越界裁剪:标注员手滑拖出画面边缘很常见,不裁剪会产出负坐标或归一化宽度超过1的值,训练时轻则警告重则loss变NaN。第二,过滤掉宽或高小于一个像素的无效框。第三,class_id用的是CLASSES列表索引顺序,这个顺序必须和训练时data yaml里的names顺序完全一致。第四,w和h是归一化值,模型输出也是这个单位,不要手工加像素单位。
转换完别急着训练,先抽几张图做可视化验证。把txt读回来画在原图上,确认框位置和类别对应正确。这一步只花几分钟,能拦住一大半标注问题。
4. 用YOLO在VOC转出的数据集上跑通路面识别:训练命令与必调参数
4.1 数据配置和最小训练命令
YOLO系列对VOC的支持很成熟。YOLOv5自带voc.yaml模板,YOLOv8以上版本需要自己写一个data yaml。以路面箭头、远处交通灯这类密集小目标场景为主,我用的配置模板是这样:
path: /data/road_signs train: images/train val: images/val names: 0: traffic_light_red 1: traffic_light_green 2: traffic_light_yellow 3: road_area 4: turn_left 5: turn_right 6: turn_straight 7: yellow_line 8: crosswalk 9: robot 10: personpath写数据集根目录,train和val是相对路径。YOLO训练时从图像同名txt读标注,所以images/train目录下每张jpg必须有一个同名txt,对应上一章转换脚本的产物。目录结构通常是:
/data/road_signs/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── road_signs.yaml训练命令我一般从YOLOv8起步,硬件不够再降级到YOLOv5:
yolo detect train \ data=road_signs.yaml \ model=yolov8s.pt \ epochs=200 \ imgsz=1280 \ batch=16 \ patience=30 \ project=road_model \ name=exp_rgb参数逻辑不是随便填的。model选了yolov8s而不是n,路面标志里箭头和黄线都是小目标,n的参数量在这种细粒度任务上容易欠拟合,s是性价比平衡点。imgsz设1280而不是默认640,因为很多输入图像是1080p,箭头宽度经常只有十几个像素,640下采样后特征几乎消失。batch=16对应显存限制,如果只有单张12G卡,imgsz=1280时batch降到8,或设成auto让程序自动按显存算。epochs设200配patience=30,验证指标30轮不涨就提前停,能省不少时间。
4.2 路面识别的参数要克制:增强别过猛,推理阈值要重调
路面标志识别和车牌识别有共性:目标细长、远距离、小尺寸。通用目标检测的部分默认参数在这里反而不合适。YOLO默认开启的mosaic增强会把四张图拼在一起再随机裁剪,对正常路面图而言,拼图会把黄线截断成几段,模型学到的线特征变了形。我的做法是把mosaic降到0.5,或直接关掉。
其他增强按路面场景调:hsv_h和hsv_s对交通灯颜色识别影响不大,可保持默认甚至加大一点;flip_lr要谨慎,左右转箭头水平翻转后类别语义完全反转,如果数据集开了flip,标签必须同步翻转,否则模型会学到“箭头方向无所谓”。scale增强建议在0.5到1.5之间,过大的缩放会把远处小目标缩成噪声。
锚框方面,YOLOv5之后的自动锚框会按数据集重新计算。如果训练日志里看到anchors完全没动,大概率是数据集太小或目标尺寸分布单一。黄线和人行道的框纵横比往往异常大,可以单独统计所有框的平均宽高比,必要时手动指定锚框。
推理阶段的conf_thres和iou_thres也要单独说。路面场景里黄线分段多、目标之间距离近,默认0.45的iou_thres很容易把相邻两段黄线框合并成一个,线段连续性表现很差。我一般把iou_thres降到0.35,conf_thres设0.25起步,夜间场景再降低一点。
4.3 验证阶段:mAP不是唯一标准,视觉识别效果要看混淆矩阵和失败图
训练结束后,val目录里会生成混淆矩阵和PR曲线。路面识别的视觉识别效果,我建议重点关注三件事。第一,交通灯红黄绿三类之间的混淆,黄色和红色在过曝场景下边界模糊,混淆矩阵里红黄互相串,说明采集数据里两者亮度重叠太多。第二,robot和person的混淆,这是安全等级最高的错误,宁可召回低,也不要让模型把行人框成robot。第三,yellow_line在远距离的表现,对应PR曲线里yellow_line的召回率,明显低于其他类,大概率是标注分段不规范。
把所有val里置信度最低的50张图拉出来看一遍,比盯着整体mAP有用得多。很多时候mAP上了0.85,翻图才发现漏检全集中在逆光和夜间场景。失败样本的价值在于指导补采数据,而不是继续调参。调参多少带点玄学成分,建议每次只改一个变量,多变量同时改动后出了问题根本定位不到原因。
5. 路面识别避坑记录:从VOC标注到训练的五个高频翻车点
5.1 标签名不统一导致类别错乱
现象:训练启动时报错class not found,或者classes清单里出现两个相似名字,模型的类别数比预期多。
原因:标注阶段多个人协作,有人写traffic_light_green,有人写Green,还有人的标注文件里名字带空格。VOC格式没有机制约束标签名,转换脚本遇到未登记的name往往直接跳过或当成新类。
解决:标注前统一发一份类别清单,CVAT里设置label列表让标注员只能选;转换前加一道扫描校验,把Annotations下所有XML的name字段收集统计,和CLASSES列表比对,多一个少一个都中止转换。用shell命令就能做:
grep "<name>" VOCdevkit/VOC2007/Annotations/*.xml | sed 's/.*<name>\(.*\)<\/name>.*/\1/' | sort | uniq -c输出里有任何不在类别清单里的词,先回头修标注,别指望转换脚本过滤,过滤掉的框等于白标。
5.2 bbox越界让loss直接变NaN
现象:训练前几个epoch正常,中途某一步loss突然变成NaN,后面回不来。
原因:标注框有一部分伸出图像边界,转换脚本没裁剪时生成了cx或w大于1的值,模型回归预测的目标异常,梯度爆炸。
解决:转换脚本里加裁剪逻辑,对应第3章的写法。训练前再跑一个检查,读取每个txt,坐标小于0或大于1就报文件名。还有一种隐蔽情况:XML里写的是1920x1080,实际图像是1600x900,size字段和图像不一致,归一化坐标全偏,同样会炸,图像统一后务必全量检查size字段。
5.3 黄线分段不统一,召回率上不去
现象:验证集里黄线检测断断续续,近处粗线段有框,远处细线段全部漏检。
原因:标注员A把一条黄线标成一个大框,标注员B每2米切一段,模型对“黄线”的尺寸分布学得非常混乱,同一张图同一类目标宽高比从1:1到1:20都有,检测头很难兼顾。
解决:标注规范写明“黄线标注单元长度1到1.5米,相邻框重叠不超过20%”,同时把黄线类别的标注框宽度统计打印出来,超过设定阈值就重新审查。这属于标注质量管控问题,调anchor只能缓解,根治还得回到规范上。
5.4 左右转箭头方向标反
现象:val的混淆矩阵里turn_left和turn_right互相串,数值还特别均匀。
原因:标注员以“自己在图里看到的箭头朝向”为判断依据,没有统一基准方向。路面箭头在图像里有时斜着、有时从远处看是折断的,不同人对“朝左”的理解可能完全相反。
解决:规范里明确“以机器人行驶方向为基准,箭头朝机器人左侧为turn_left”,标注页面上贴正例图。标注完成后抽20%的框做二次复检,重点看转向类。方向识别错了导航决策直接反着走,比漏检更危险。
5.5 白天训练顺,夜间一测就翻车
现象:白天数据集上mAP超过0.8,夜间场景里交通灯漏检、路面标志基本看不见。
原因:训练图像全是上午拍摄,交通灯的亮度、周围环境光、灯罩反光形态与夜间完全不同,模型学到的是白天光照下的颜色和形状组合。
解决:采集阶段把夜间、黄昏独立成子集,训练配置里用光度类增强模拟光照变化,或者夜间单独训练一个模型做场景切换。我倾向于在数据集层面解决,增强模拟出的夜间形态和真实夜间还是有差距。先把夜间子集标好加进训练集,再考虑增强参数。
6. 进阶:用VOC的干净结构搭一个持续迭代的标注闭环
当你有了几百张VOC标注、模型能跑出可用精度时,最值得做的不是堆epochs,而是把整个流程转成一个“模型出初标、人工只改错、增量进训练”的闭环。我的做法是:让已训练模型跑一遍新增的未标注片段,用高置信度预测直接生成VOC格式的待确认XML,导入CVAT后人工只修正错框和漏框,确认后再走一次转换脚本进入下一轮训练。VOC格式的价值在这里就体现出来了——它足够简单,标注工具、转换脚本、训练框架都能直接吃,不需要为某个私有格式写适配。
一个小技巧:在转换脚本里保留difficult字段的读取逻辑,把模型预测置信度在0.3到0.5之间、人工还没确认的框写成difficult=1单独存放。增量训练时这些框参与损失计算,但不会因为人工漏改一个框就拉偏模型,人工确认后再去掉difficult正式进入训练集。这不算什么半监督高深方案,但配合VOC字段做起来很顺手,几乎不需要额外代码。
另一个习惯是每轮训练后把失败样本按类别归档,比如单独建night_failures文件夹,放模型在夜间漏检的帧,下一轮训练前抽出来作为必训样本放回训练集。这比随机加数据更稳,能保证每个已知薄弱点都被覆盖。我自己栽过的跟头是早期把精力全放在调mAP上,忘记留时间为失败样本做回归验证,结果每次精度提升只在旧场景里成立,新场景一测就露馅。后来改成“每轮训练完强制看50张失败图并归档”,模型的可用性反而比mAP数字涨得更快。
路面导航标志识别这个方向,VOC格式只是一块垫脚石,真正决定上限的是数据覆盖度、标注一致性和迭代闭环这三件事。把这三件做好,模型水平和维护成本都会舒服很多。希望帮到你。
本文还有配套的精品资源,点击获取