YOLO交通标志与红绿灯检测数据集:VOC XML转TXT格式全解析
2026/9/15 1:50:54 网站建设 项目流程

简介:面向目标检测实验与YOLO系列模型训练的交通标志/交通信号灯检测数据集,覆盖限速牌、交通警告牌、红灯、绿灯、黄灯等常见交通场景,可作为目标检测入门练习、算法对比及实际项目验证的标准数据。包内共1641个文件,包括877张高清png图片、761个xml标注文件、2个txt标签说明及1个Python辅助脚本,压缩包整体约217.98MB。图片清晰度较高,标注由LabelImg工具人工完成,并转换为YOLO所需txt格式,包含目标类别编号与四个角坐标,可直接用于训练与评估;Python脚本可辅助检查或转换标注格式,方便接入常见检测框架。目前已有474人学习下载,适合需要标准化交通标志数据集的开发者快速开展实验。

1. 从 YOLO 训练实验出发,重新理解交通标志与红绿灯数据集

做目标检测实验的人,大概率都经历过这种场景:模型结构选好了,YOLO 权重也下好了,最后卡在数据上——网上下载的 GTSRB 或 LISA 数据集格式太乱,VOC 的 xml 标注和 YOLO 要的 txt 标注对不上,红绿灯和限速牌的类别混在一起,训练时 loss 死活降不下去。这个标题里的“YOLO 交通标志 交通信号灯 红绿灯 检测数据集 xml txt格式”,指的就是一套同时包含两种标注格式的目标检测数据集,标注对象是道路上的交通标志和信号灯。它能解决的问题很具体:省去手工标注的耗时,直接拿去做 YOLOv5/YOLOv8 训练前的格式验证和迁移学习实验。

对刚入门的人,这套数据能帮忙跑通“xml 转 txt → 配好 data.yaml → 开始训练”的最小流程;对有经验的工程师,重点在于看它的类别定义是否覆盖了你需要的场景——比如黄灯算不算独立类别,倒计时数字是否单独标注,白天和夜间的图像比例如何。本文按“数据格式 → 格式转换 → 训练配置 → 质量排查”的顺序展开,每一段都给出可复现的命令和参数说明。

2. 交通标志数据集里的 VOC 格式 XML 结构,解析字段与统计脚本

2.1 VOC XML 的标注字段:从 object 到 bndbox

拿到一套带 xml 标注的交通标志数据集,第一步不是急着转格式,而是先把 xml 的标签结构读明白。标准 VOC 格式的 xml 文件结构如下,这套数据里的交通信号灯交通标志通常都是以<object>为单位标注的:

<?xml version="1.0" encoding="UTF-8"?> <annotation> <folder>traffic_light</folder> <filename>img_20241103_001.jpg</filename> <path>E:/datasets/red_light/images/img_20241103_001.jpg</path> <source> <database>Unknown</database> </source> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <segmented>0</segmented> <object> <name>red_light</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>820.5</xmin> <ymin>301.4</ymin> <xmax>877.2</xmax> <ymax>346.9</ymax> </bndbox> </object> </annotation>

这里最需要注意的是difficult字段。xml 转 txt 时,difficult=1的标注通常应该跳过,因为这类目标本身模糊、遮挡严重,YOLO 的 txt 格式里没有专门标记,直接保留反而会干扰训练收敛。truncated同理,被截断的目标如果框得不准,建议在转换时人工检查一批。filename<path>有时会有路径分隔符冲突,Windows 下是反斜杠,Linux 下是正斜杠,用脚本统计类别时不要直接拼字符串,用os.path.basename()提取纯文件名。

2.1.1 用 Python 统计 XML 中出现过的所有类别名

要判断这套数据集是否直接可用,第一件事是看类别名是否统一,比如red_lightRedLight同时存在会导致类别分裂。用下面的脚本统计所有 xml 的<name>字段:

import xml.etree.ElementTree as ET import os from collections import Counter xml_dir = "./annotations/xml" cls_counter = Counter() for xml_file in os.listdir(xml_dir): if not xml_file.endswith(".xml"): continue tree = ET.parse(os.path.join(xml_dir, xml_file)) root = tree.getroot() for obj in root.findall("object"): name = obj.find("name").text.strip() cls_counter[name] += 1 print("类别统计:", dict(cls_counter)) print("总目标数:", sum(cls_counter.values()))

对比 GTSRB 那种按目录分文件夹组织的数据,这种 xml 标注方式的最大好处是每张图片可以同时含多个类别目标——比如一个十字路口画面里既有绿绿灯,也有行人过街标志和限速牌。统计逻辑上使用findall("object")而不是find("object"),因为前者会遍历全部标注目标,后者只取第一个。拿到类别列表后,下一步就是拿它和你的训练任务对齐,决定是全部保留还是合并为少数几个大类。

2.2 YOLO 的 txt 标注格式,归一化坐标的计算逻辑

当你准备用 YOLOv5 或 YOLOv8 训练时,txt 标注文件要与图片同名,内容每一行代表一个目标:class_id x_center y_center width height。其中坐标值全部相对于图片宽高做了归一化,归一化的计算公式是:

x_center = (xmin + xmax) / 2 / width y_center = (ymin + ymax) / 2 / height box_width = (xmax - xmin) / width box_height = (ymax - ymin) / height

这个计算逻辑注意点在于,VOC 的 xmin、xmax 是像素绝对值,YOLO 格式要求的是 0 到 1 之间的浮点数。文件扩展名是.txt,与图片文件放置在同一个 labels 目录下。目录结构必须严格对齐 images 和 labels,否则训练时标签无法匹配。这套交通灯数据集里如果包含夜间图像,需要留意暗光条件下部分红灯小目标是否标注了difficult=1,转换前可以对该字段做一次数量统计。

2.2.1 txt 最小值检查:发现越界框

写转换脚本之前,先做一次越界检查。有些标注框的边缘超出了图像边界,这种情况在公开数据集中多次出现,具体表现为 xmax > width 或 ymax > height。检查脚本如下:

import xml.etree.ElementTree as ET import os xml_dir = "./annotations/xml" error_list = [] for xml_file in os.listdir(xml_dir): if not xml_file.endswith(".xml"): continue tree = ET.parse(os.path.join(xml_dir, xml_file)) root = tree.getroot() size = root.find("size") img_w = float(size.find("width").text) img_h = float(size.find("height").text) for obj in root.findall("object"): box = obj.find("bndbox") xmin = float(box.find("xmin").text) ymin = float(box.find("ymin").text) xmax = float(box.find("xmax").text) ymax = float(box.find("ymax").text) if xmax > img_w or ymax > img_h or xmin < 0 or ymin < 0: error_list.append((xml_file, (xmin, ymin, xmax, ymax))) print("异常框数量:", len(error_list)) for item in error_list[:10]: print(item)

异常框在任何数据上都有可能存在,但交通信号灯场景里比较常见的是灯体反光导致标注框被拉大。碰到这类情况,推荐的处理方式不是直接删掉这一帧目标,而是把越界的 xmax 强制裁剪到img_w - 1,ymax 同理裁剪,保留该目标的其余有效像素。

3. 将 XML 标注转换为 YOLO TXT 格式的训练集,从单文件到批量

3.1 核心转换函数与类别映射表

标签转换是整个流程中投入成本最低但收益最明显的环节。直接用现成的开源工具如 labelImg 导出的 xml 做转换,常规做法是写一个 Python 函数,将单个 xml 文件的标注坐标提取后归一化再写入同名 txt。这里给出一个可独立运行的转换函数:

import xml.etree.ElementTree as ET import os # 类别映射字典,顺序决定 class_id,训练时保持一致 CLASS_MAP = { "red_light": 0, "green_light": 1, "yellow_light": 2, "speed_limit_30": 3, "speed_limit_50": 4, "no_entry": 5, "crosswalk": 6, "stop": 7 } def convert_xml_to_txt(xml_path, output_dir, img_width, img_height): tree = ET.parse(xml_path) root = tree.getroot() txt_lines = [] for obj in root.findall("object"): name = obj.find("name").text.strip() if name not in CLASS_MAP: print(f"跳过未映射类别: {name} 在 {os.path.basename(xml_path)}") continue difficult = int(obj.find("difficult").text) if difficult == 1: continue box = obj.find("bndbox") xmin = float(box.find("xmin").text) ymin = float(box.find("ymin").text) xmax = float(box.find("xmax").text) ymax = float(box.find("ymax").text) # 越界裁剪 xmin = max(0, min(xmin, img_width - 1)) ymin = max(0, min(ymin, img_height - 1)) xmax = max(0, min(xmax, img_width - 1)) ymax = max(0, min(ymax, img_height - 1)) if xmax - xmin < 2 or ymax - ymin < 2: print(f"过滤过小目标: {os.path.basename(xml_path)}") continue x_center = (xmin + xmax) / 2 / img_width y_center = (ymin + ymax) / 2 / img_height w = (xmax - xmin) / img_width h = (ymax - ymin) / img_height cls_id = CLASS_MAP[name] txt_lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") output_filename = os.path.basename(xml_path).replace(".xml", ".txt") output_path = os.path.join(output_dir, output_filename) with open(output_path, "w") as f: f.write("\n".join(txt_lines))

参数说明:CLASS_MAP这一字典在转换时出现概率最高的坑是类别名不一致。公开数据集里的类别命名风格多样,比如限速标志可能是speedlimit30speed_limit_30SpeedLimit30的组合。建议在转换前用第 2 章的统计脚本跑一遍所有类别名,再来建映射表。过滤宽度小于 2 像素的目标是为了防止后续 YOLO 训练时出现 NaN loss,因为极小的框在特征图上没有落点。如果原标注中已经过滤过,这一条件不会触发,保留也无害。

3.1.1 图片尺寸获取:从 XML 里读而不是从图片读

转换函数里img_widthimg_height最可靠的来源是同一个 xml 文件内的<size>节点,而不是用 PIL 或 cv2 去读图片。原因是有些数据集的图片是压缩过的缩略图,xml 里的宽高才是标注时的实际尺寸依据,按图片真实分辨率归一化会导致坐标整体偏移。实际批量处理时修改为如下调用方式:

def get_xml_size(root): size = root.find("size") return float(size.find("width").text), float(size.find("height").text) # 调用时: w, h = get_xml_size(root) convert_xml_to_txt(xml_path, output_txt_dir, w, h)

3.2 批量转换脚本:用 os.walk 处理所有子目录

交通数据集经常按场景拆成子目录,比如daytime/night/rainy/。如果只在单层目录里循环会漏掉很多文件,遍历时用os.walk一次性处理所有嵌套目录:

import xml.etree.ElementTree as ET import os annotations_root = "./datasets/annotations" output_root = "./datasets/labels" os.makedirs(output_root, exist_ok=True) CLASS_MAP = { ... } # 与上面相同 def process_all_xml(annotations_root, output_root): for dirpath, _, filenames in os.walk(annotations_root): rel_path = os.path.relpath(dirpath, annotations_root) target_out_dir = os.path.join(output_root, rel_path) os.makedirs(target_out_dir, exist_ok=True) for filename in filenames: if not filename.endswith(".xml"): continue xml_path = os.path.join(dirpath, filename) tree = ET.parse(xml_path) root = tree.getroot() img_w, img_h = get_xml_size(root) convert_xml_to_txt(xml_path, target_out_dir, img_w, img_h) process_all_xml(annotations_root, output_root)

保留原始目录结构是关键操作。这样 labels 目录下自动生成与 annotations 相同的子目录层级,后续做train.txtval.txt文件列表选择时,直接用os.listdir读取images/trainlabels/train两个目录做同步筛选,避免文件名对不齐的问题。转换完成后,通过统计 txt 文件行数来验证每个 txt 是否生成了内容,用下面这一行命令查看所有空文件:

grep -rL ".*" ./datasets/labels --include="*.txt"

输出结果为空的文件需要检查源 xml 是否含有未映射类别。常见情况是 xml 里有一个<name>traffic_sign</name>这种泛化类别,被跳过导致整个 txt 为空。此时需要补齐CLASS_MAP,或者增加一个默认 id 映射到misc类。YOLO 训练对类别数不敏感,但类别定义要前后一致。

3.3 转换后的自检方案:可视化标注结果

格式转换看似简单,实际运行后最好还是人工验证一批。自检的方式是使用 OpenCV 读图片,画矩形框,把框的坐标还原回像素并叠加类别名称。代码如下:

import cv2 def draw_yolo_box(image_path, txt_path, class_names, output_path): img = cv2.imread(image_path) h, w = img.shape[:2] with open(txt_path) as f: for line in f: cls_id, xc, yc, bw, bh = map(float, line.split()) x1 = int((xc - bw / 2) * w) y1 = int((yc - bh / 2) * h) x2 = int((xc + bw / 2) * w) y2 = int((yc + bh / 2) * h) cls_name = class_names[int(cls_id)] cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, cls_name, (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) cv2.imwrite(output_path, img)

用这个可视化方法挑选 20 张不同场景下的图片输出到./debug目录,人工浏览一轮。重点检查的是:红绿灯是否被标注到全灯体还是只标灯芯、远处的小尺寸交通标志是否被漏标、出租车顶上的小标志有没有被框进去等。这里发现的标注质量问题,比后续调模型参数对 mAP 的提升更大。

4. 使用转换后的 TXT 数据集跑通 YOLOv8 目标检测实验

4.1 目录结构组织与 data.yaml 配置

转换完成后,把数据集按 YOLO 的标准目录结构重新组织。推荐的布局如下:

datasets/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── traffic_sign.yaml

这里有一个常见的目录映射问题:YOLOv8 训练时默认是在images/下找train/,然后在相同父目录下找labels/。如果你的数据集原本是 images 和 labels 分开放,需要一份镜像同构的目录结构。用ln -s或移动文件都可行,但要前后一致。接下来创建traffic_sign.yaml

# 数据集路径,建议使用绝对路径,避免训练时相对路径问题 path: /home/user/datasets train: images/train val: images/val # 类别名称,顺序必须与 txt 标注中的 class_id 一致 names: 0: red_light 1: green_light 2: yellow_light 3: speed_limit_30 4: speed_limit_50 5: no_entry 6: crosswalk 7: stop

names列表顺序如果与转换时CLASS_MAP不一致,训练出的模型输出类别名就会错乱。这里强调一点——path字段写绝对路径,避免在chdir后路径解析到错误位置。训练脚本如果使用相对路径,可能在多机或不同目录下运行时报AssertionError: train labels not found,多数指向的其实是 yaml 根路径配置错误而非文件缺失。

4.2 训练命令与关键超参数设置

用 YOLOv8 训练的最小命令是:

yolo detect train data=/home/user/datasets/traffic_sign.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ device=0

参数明细说明:

  • model=yolov8n.pt:预训练权重,n是 nano 版本,显存占用小,适合在 4GB 左右的显卡上训练。如果你的标注框大多是小目标,可以换成yolov8s.pt,但训练时间会明显增加。
  • imgsz=640:训练时输入尺寸。红绿灯数据里很多样例是 1920x1080 的高清图像,640 的resize 会把远处的小目标缩小到不到 10 个像素,可以考虑改imgsz=1280,但显存需求约为原来的 4 倍。
  • batch=16:根据显存调整。如果CUDA out of memory,逐次减半到 8 或 4。
  • device=0:指定单卡训练。没有 N 显可用时改成device=cpu,训练速度会显著下降。

训练时关注两个指标:box_losscls_loss。如果box_loss在 30 轮后仍然不下滑,多数原因是数据集里存在大量目标框中心位置抖动,检查标注的可视化输出结果。红绿灯这种细长型目标(宽度不到高度的一半),其长宽比对损失函数影响大,YOLOv8 默认的 anchor 设计能处理这种情况,但不要在两三个 epoch 后就停止训练。

4.3 训练与转换接口最常见的 3 个报错

第一个高频报错是标注文件为空。YOLO 训练日志显示某个 epoch 的总损失直接为 0,或者收敛 loss 极小,排除数据路径问题后检查 labels 目录下是否存在0 0.5 0.5 0 0这类全零行。第二个是类别数不匹配,报错内容类似Number of classes in model (80) does not match the dataset (8),原因是加载了yolov8n.pt这个 COCO 预训练权重。如果训练时names类别数不等于 80,模型会自动替换最后一层分类头,此时报错不太常见。反而容易出错的是自己改过的权重文件没有配好类别数。

第三个值得注意的问题是验证时 mAP@50 虚高而 mAP@50-95 很低,这通常表示模型只抓到了大目标,红绿灯这种小目标没有学到。常规处理是在数据增强上做调整:训练时默认开启的 mosaic 增强会把小目标裁掉一部分,关闭它对小目标更友好:

yolo detect train data=... model=yolov8n.pt \ epochs=100 imgsz=640 \ batch=16 device=0 \ mosaic=0.0 scale=0.3

mosaic=0.0关闭马赛克拼接,scale=0.3降低随机缩放的幅度,让小目标保持原有尺寸。这一参数组合在红绿灯场景下比默认数据增强的收敛效果更好,尤其是目标占比不足 1% 的画面。

5. 交通标志数据集里被忽略的关键一环:验证 set 的采样偏差问题

训练集、验证集的划分方式对最终模型效果的影响,在红绿灯这类类别不平衡的数据集上体现得尤为明显。常见错误做法是随机采样做 train/val 划分,但这会导致极端天气、夜晚、逆光等关键场景不均衡地落在训练集或验证集中。如果最终实验报告里的精度比预期低,先检查 val 集合是否恰好包含了一大批白天强光照下小而模糊的限速标志。

推荐的划分策略是按场景分桶而不是按文件随机分。将数据按采集时间或目录描述分为daytimenightrainy三组,每组内部再按 8:2 划分,组成 train 和 val。这样验证结果对场景有更好的代表性。

在训练完成后使用model.val()时,注意查看每个类别的混淆矩阵。对交通标志的识别来说,容易混淆的是speed_limit_30speed_limit_50——它们的形状相同,只有数字不同,在远距离小目标时 tensor 特征极其接近。此时检查数据集中数字区域的标注是否精确到数字边框内,如果标注框覆盖了整个圆牌,模型很难学习数字区域的细节差异。标注规范里数字区域单独建类或缩小 bndbox 到数字区域,会带来明显的精度收益。

另一个容易被忽略的问题是多标签重叠:同一个十字路口画面里,红灯和倒计时数字可能被标注成两个重叠框。YOLO 训练本身允许重叠框同时存在,但在 NMS 后处理时两个类别框会互相抑制,导致倒计时数字类别永远检测不到。处理这种重叠的策略是去掉倒计时数字类别,只保留信号灯状态类别;如果数据集规范是两者都必须标注,可以让倒计时数字标注只在倒计时区域完整可见时出现,并将它的置信度阈值调低,典型值设为 0.2。

数据层面的工作做到这里,训练效率和模型精度基本都能对齐预期。输出验证结果的常用做法是对 val 集做批量预测,并把预测结果与 GT 画在同一张图上,按类别统计漏检目标,找出具体是哪一种交通标志、哪一个光照条件贡献了主要错误。

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

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

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

立即咨询