☰
交通标志红绿灯检测:XML转TXT格式与YOLO训练避坑指南
2026/10/5 2:55:53 网站建设 项目流程

简介:面向目标检测实验与交通场景识别的学习者,这份原创数据集提供交通标志与交通信号灯样本,涵盖限速牌、警告牌及红绿灯等典型目标,可直接用于YOLO系列模型的训练与验证。资源共1641个文件,压缩包约217.98MB,主体为877张高清PNG图片、761个XML标注文件,同时附2个TXT类别及坐标说明、1个Python脚本;XML保留LabelImg标注原始结果,TXT则转换为YOLO所需的四角坐标与类别编号,格式衔接顺畅。图片覆盖多种道路环境,清晰度较高,标注边界框贴合目标,既适合初学者练习数据预处理、格式转换和模型调参,也便于进阶者开展迁移学习或对比不同检测算法。自带转换脚本可省去重复标注时间,整体目录结构清晰,便于按需取用。目前已有475人学习下载,可作为交通标志检测课程设计、毕业设计或入门实验的实用数据支撑。

1. 交通标志与红绿灯检测数据集:先看清里面装的是什么

做交通标志和红绿灯检测实验,最容易翻车的往往不是模型结构,而是数据集本身。市面上这类 YOLO 目标检测数据集,一部分把标注存成 txt 的 YOLO 格式,另一批历史项目还保留着 xml 的 VOC 标注,标题里的“交通标志 交通信号灯 红绿灯 检测数据集 xml txt格式”指的就是这种双格式资源——既留了 xml 里的完整标注信息,又能转成 YOLO 直接训练。它的价值是帮你省掉最贵的标注时间,但同时也埋了两个坑:xml 转 txt 不是换个后缀就能用,红绿灯与交通标志的类别编号必须和训练配置严格对齐。我按拿到数据之后的顺序,把格式解析、转换脚本、训练参数和排错经验从头理顺,适合正在做目标检测实验的学生,以及要跑智慧交通项目的工程朋友参考。

2. 先读懂标签体系:xml 与 txt 双格式标注的字段差异

拿到这类数据集,别急着训练,花半小时搞清楚 xml 和 txt 分别装了什么,后面能省一整天的排错时间。两种格式表达的是同一批目标框,只是坐标系和存放方式不同。xml 是 VOC 格式的完整记录,txt 是 YOLO 直接读取的压缩版。只有把两者之间的换算关系摸清楚,后面写转换脚本才不会被莫名其妙的坐标带偏。

2.1 VOC 的 xml 标注:八个字段,缺一个就丢一组目标

一个标准 VOC 格式的 xml 文件里,我平时只关心下表中这几个字段。folder、filename、source 这类元信息对训练没有直接影响,但 filename 常被用来和图片配对,不要忽略。

字段内容训练中的作用
folder / filename图片所在目录与文件名与图片配对的依据
size / width图片宽度(像素)归一化计算的分母之一
size / height图片高度(像素)归一化计算的分母之二
size / depth通道数,通常为 3一般用不到
object / name目标类别名决定 txt 里的 class_id
object / truncated目标是否在图像边缘被截断0 或 1,通常保留
object / difficult目标是否属于难例为 1 时建议转换时剔除
object / bndboxxmin ymin xmax ymax像素绝对坐标,转换唯一数据源

真正决定坐标的是 size 和 object 下的内容。size 里的 width 和 height 是转换时的分母,一旦这里的数据和实际图片宽高不一致,转出来的坐标全部偏移。object 里一个目标一个块,name 是类别名,bndbox 是四个像素坐标。一张图里有多个目标就存在多个 object 块,解析时要用 findall("object") 而不是 find("object"),否则只拿到第一个框——这是转换脚本里最常见的隐形 bug。

2.2 YOLO 的 txt 标注:归一化坐标为什么全是小数

YOLO 的 txt 每行一个目标,五个数字用空格分开:class_id x_center y_center width height。class_id 是整数,四个坐标都是 0 到 1 的小数。遇到这种全小数格式,新手容易以为坐标写错了,其实这就是 YOLO 的标准写法。

txt 字段计算方式说明
class_idCLASS_NAMES.index(name)类别名转成从 0 开始的编号
x_center((xmin + xmax) / 2) / img_w目标中心点横向位置,归一化
y_center((ymin + ymax) / 2) / img_h目标中心点纵向位置,归一化
width(xmax - xmin) / img_w目标宽度占比
height(ymax - ymin) / img_h目标高度占比

归一化的价值在于不依赖原始分辨率,图片缩放后直接送进网络,模型不需要关心输入尺寸。但这也意味着坐标是否准确完全取决于 xml 里 size 字段。举个例子,某张 1280x720 的图里有个红绿灯,bndbox 为 (168, 320, 190, 342),x_center 算出来是 179 / 1280 = 0.1398,y_center 是 331 / 720 = 0.4597。转换后 txt 里对应这一行就是“1 0.139844 0.459722 0.017188 0.030556”,后面你拿这个结果去画框,必须能叠回原图。

2.3 类别编号是双向对齐:红绿灯和交通标志最容易错位的地方

类别体系没统一是这类数据集翻车的第一来源。同一个数据集里,xml 可能叫 green、traffic_light_green,也可能直接叫 light_green,交通标志那边更乱,有 speed_limit_30、speed_limit_40 这种按具体限速拆开的,也有直接写 sign 的。拿到手第一步,先把 xml 里所有 name 去重打印一遍,看实际有多少类,再决定拆还是合。

我一般会建议按颜色区分红绿灯,按类型合并交通标志:

编号类别名说明
0green_light绿灯,含左转、直行箭头灯
1red_light红灯
2yellow_light黄灯,样本少时注意过采样
3speed_limit限速标志,合并各级限速
4warning警告标志,合并各类警告
5no_entry禁令标志,禁止驶入类

转换脚本里的 class_names 顺序、data.yaml 里的 names 顺序、模型推理时输出的索引,三处必须完全一致。任何一边换了顺序,训练时 loss 照常下降,但 mAP 永远是零。后面排错章节会反复强调这一点,因为这是实验里最容易看走眼的地方。

3. 把 xml 转成 YOLO 的 txt:一份能直接跑的脚本与目录组织

格式看懂以后,接下来就是动手把 xml 批量转成 txt。这类数据集下载下来经常是 images 和 xmls 混在一起放,常见做法是先按 train/val 两套目录摆整齐,再写脚本转换。我一般不在原目录里改,而是新建 images、xmls、labels 三种子目录,图片和标注一一对应,后面训练器找文件也省心。

3.1 先按 images、xmls、labels 三房分离组织目录

无论你从哪个渠道拿到这套数据,我都建议先整理成下面这个结构再动脚本:

dataset/ ├── images/ # 原始图片 │ ├── train/ │ └── val/ ├── xmls/ # 与 images 同名的 VOC 标注 │ ├── train/ │ └── val/ ├── labels/ # 转换后的 YOLO txt 输出目录 │ ├── train/ │ └── val/ └── data.yaml # 训练时用的数据集配置

这个结构的核心逻辑是“三房分离”:图片归图片,原始 xml 归 xml,转换产物归 labels。labels 目录保持与 images 相同的子目录名和文件名,训练器才能自动找到同名 txt。很多人在这一步图省事,把 labels 全部平铺在一个目录里,结果 train 和 val 的标注互相覆盖,训练时图片匹配到错误的标签文件,指标一塌糊涂。

3.2 xml 转 txt 脚本:四个关键点决定转换是否干净

转换脚本不复杂,但有几个细节必须处理好。下面这份是我在多个数据集上调过的版本,可以直接拿去改路径用:

# -*- coding: utf-8 -*- import os import xml.etree.ElementTree as ET # 类别顺序决定 txt 第一列的数字,必须与后续 data.yaml 完全一致 CLASS_NAMES = [ "green_light", "red_light", "yellow_light", "speed_limit", "warning", "no_entry", ] def xml2txt(xml_path, out_path): tree = ET.parse(xml_path) root = tree.getroot() # 归一化分母必须用 xml 里的 size,不能用 cv2 读图后的尺寸 size = root.find("size") img_w = int(size.find("width").text) img_h = int(size.find("height").text) with open(out_path, "w", encoding="utf-8") as f: for obj in root.findall("object"): # findall 不是 find name = obj.find("name").text.strip() if name not in CLASS_NAMES: continue # 不在类别表里的直接跳过 class_id = CLASS_NAMES.index(name) difficult = obj.find("difficult") if difficult is not None and difficult.text == "1": continue # 难例剔除,避免污染训练 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) x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h box_w = (xmax - xmin) / img_w box_h = (ymax - ymin) / img_h if not (0 <= x_center <= 1 and 0 <= y_center <= 1): print("warning: out of range", xml_path, class_id) f.write(f"{class_id} {x_center:.6f} {y_center:.6f} " f"{box_w:.6f} {box_h:.6f}\n") if __name__ == "__main__": xml_dir = "dataset/xmls/train" out_dir = "dataset/labels/train" os.makedirs(out_dir, exist_ok=True) for fname in os.listdir(xml_dir): if fname.endswith(".xml"): xml2txt( os.path.join(xml_dir, fname), os.path.join(out_dir, fname.replace(".xml", ".txt")), )

逻辑说明:脚本按目录遍历所有 xml,对每个 object 块提取类别和 bndbox,加总取中点后除以图片宽高,得到归一化坐标。输出文件的命名规则是把 xml 后缀换成 txt,保证和图片同名。

参数说明:CLASS_NAMES 的顺序一旦定下来就不要改,它直接决定 txt 第一列的数字,也决定之后 data.yaml 里的 names 顺序。xml 的 size 字段必须和真实图片一致,如果图片被 resize 过而 xml 没更新,转出来的框会整体偏移。difficult 为 1 的难例建议剔除,否则模型会把难例当成噪声硬学。程序里加了范围告警,如果坐标超出 0 到 1,会打印警告,这时要回到 xml 检查 bndbox 是否标错。

3.3 转换后的可视化校验:把框画回图上,十行代码见真章

转换完不要直接开训练,先抽几十张图把框画回去看一眼。这一步能同时验证两件事:归一化坐标换算是否正确,类别索引是否对得上。我常用下面这个脚本:

# verify.py import cv2 def draw_boxes(image_path, label_path, class_names): img = cv2.imread(image_path) h, w = img.shape[:2] with open(label_path) as f: for line in f: parts = line.strip().split() if len(parts) != 5: continue class_id = int(parts[0]) xc, yc, bw, bh = map(float, parts[1:]) # 把归一化坐标换算回像素,注意是中心点加减宽高的一半 x1 = int((xc - bw / 2) * w) y1 = int((yc - bh / 2) * h) x2 = int((xc + bw / 2) * w) y2 = int((yc + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, class_names[class_id], (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) cv2.imwrite(image_path.replace(".jpg", "_check.jpg"), img)

在校验图上如果发现框全部跑到左上角,或者整体偏移,多半是 xml 的 size 和实际图片尺寸不一致;如果某个框明显框错位置但其他框正常,则是那一个目标的 bndbox 标注问题;如果类别名和框对不上,就是 CLASS_NAMES 的顺序错位,回到脚本里改顺序,而不是改训练配置。这个检查习惯能帮你过滤掉大部分低级错误。

4. 用这套数据训练 YOLO:data.yaml 与训练参数的设置顺序

格式转对了,训练本身反而简单。在 ultralytics 框架下,数据集、模型、训练参数三样对齐就能跑起来。这里有一个容易被忽略的事实:这类数据集的问题从来不在损失函数,也不在模型结构,而在于数据和配置是否一致。

4.1 data.yaml:path、nc、names 三处与转换脚本对齐

data.yaml 是训练时读入的第一个文件,写错一处就会导致类别错位。我习惯把路径写成相对路径,避免换机器以后找不到目录。

# dataset/data.yaml train: dataset/images/train # 训练集图片目录 val: dataset/images/val # 验证集图片目录 nc: 6 names: 0: green_light 1: red_light 2: yellow_light 3: speed_limit 4: warning 5: no_entry

train 和 val 指向图片目录,训练器会自动去找同名的 labels 目录,所以 images 与 labels 的目录名、文件名必须完全一致。nc 必须等于转换脚本里 CLASS_NAMES 的长度,names 的顺序必须和 CLASS_NAMES 一字不差。训练器实际输出的类别索引就是 txt 里的第一列数字,任何换位都会导致整体错位。建议转换脚本跑完后,随便打开一个 txt 文件,看第一列数字对应的类别名,再回来核对 yaml。

4.2 训练命令与三类参数:imgsz、batch、mosaic 这样调

训练命令本身很直接,但参数不能全用默认值。红绿灯和交通标志属于中小目标,默认配置是为 COCO 80 类准备的,直接套用会吃亏。

yolo detect train \ data=dataset/data.yaml \ model=yolov8s.pt \ imgsz=640 \ batch=16 \ epochs=100 \ workers=4 \ device=0

imgsz 默认 640,对远处的红绿灯来说偏小。一个小目标在 1080p 原图里只有 24x24 像素,缩到 640 以后变成十几个像素,特征基本消失。显存允许的情况下,我会把 imgsz 提到 960,batch 相应降到 8。显卡只有 6G 显存的话,坚持 imgsz=640、batch=8 更稳妥。epochs 对这类单场景数据集不必太多,100 轮足够,训练时盯住 val loss,连续 20 轮不降就可以停。

mosaic 增强要单独说。yolo 默认 mosaic=1.0,对常规目标没问题,但红绿灯这类小目标在 mosaic 拼接时会被切到边缘甚至直接丢掉。我一般前 2/3 训练轮数保持 mosaic=0.5,后 1/3 设成 0,让模型在最后阶段学习完整目标。至于损失函数,红绿灯数据集常见问题不是 loss 不收敛,而是类别错位导致 loss 正常下降、mAP 全零,别在损失函数上花太多时间排查。类别不平衡更直接的解法是把数量少的类别对应图片复制几份进训练目录,同时复制同名标签,比调任何损失权重都见效快。

4.3 划分 train/val:按场景切,别随机切

随机划分在目标检测数据集上是最大的隐患。同一路口的连续视频帧背景几乎一样,随机切会让大量相似帧同时出现在训练集和验证集,val mAP 虚高到 0.9 以上,一到真实场景立即露馅。正确做法是按采集场景或时间划分,保证验证集与训练集来自不同时段或不同路口。

我一般这样操作:假如数据是按视频片段组织的,先按视频编号分组,每个视频的前 80% 帧进 train,后 20% 帧进 val,然后同步移动图片和 xml。

# 按场景目录 scene_01 拆分 mkdir -p dataset/images/train/scene_01 mkdir -p dataset/xmls/train/scene_01 mv dataset/raw/scene_01/*.jpg dataset/images/train/scene_01/ mv dataset/raw/scene_01/*.xml dataset/xmls/train/scene_01/

注意 mv 命令里图片和 xml 必须同步操作,否则图片进了 train 而对应 xml 还留在 raw 目录,转换脚本会漏掉标注。验证集不需要很大,占总量的 5% 到 10% 就够,但必须异源,这样的验证结果才有参考价值。

5. 数据预处理与训练避坑:五个让模型翻车的常见原因

前面流程走完,剩下的就是排错。这一章我把实际踩过的坑按“现象 → 原因 → 解决”列出来,每条都是真实项目里见过的,不是理论推断。

5.1 xml 解析报错:跑到一半就中断

现象:转换脚本执行到某个 xml 文件时抛出 ParseError,程序直接退出,前面的转换结果也没写完整。

原因:xml 文件不是 UTF-8 编码,常见的是 GBK 格式保存;还有可能是下载过程中文件被截断,标签闭合标签缺失。数据集批量打包时这种坏文件几乎一定存在。

解决:脚本里对单个文件做 try/except 容错,解析失败时打印文件路径,跳过继续处理其他文件。对编码问题,先把 xml 用 UTF-8 重新保存再转;对真正损坏的文件,删掉 xml 的同时记住删掉对应图片,否则训练时图片配对找不到标签。

try: tree = ET.parse(xml_path) except ET.ParseError: print("bad xml:", xml_path) continue

5.2 训练正常但 mAP 全零:类别编号错位

现象:训练能正常跑完,loss 也在下降,但验证结果的每个类别 mAP 都是 0,或者一类的 mAP 高到异常而其他类全零。

原因:txt 第一列的类别编号和 yaml names 的顺序没对上。转换脚本里 CLASS_NAMES 把 green_light 放在 0,yaml 里却把 red_light 放在 0,模型学到的特征和标签全错位了。这种错误用肉眼很难发现,因为 loss 曲线看起来完全正常。

解决:取训练集里一个 txt 文件,打印第一列数字,对照 yaml 的 names 逐个核对。另一个更直观的办法是用校验脚本画框,如果框的位置全对但类别名乱跳,就是这里错位了。记住这个顺序:转换脚本 CLASS_NAMES、data.yaml names、模型 output index,三处必须同序。

5.3 验证集 mAP 虚高到 0.9:同源帧污染

现象:训练结束后 val mAP 高达 0.9,看起来效果非常好,但把模型拿到现场视频上跑,漏检严重。

原因:train/val 用随机方式划分,同一个视频片段里的相邻帧同时出现在训练集和验证集。模型相当于背下了这段路的背景,验证时看到“熟悉”的帧就得高分,遇到没见过的路口立刻失效。这是我第一次做目标检测实验时踩过的最深的坑,也是做这类数据集最容易看走眼的地方。

解决:放弃随机划分,按采集时间或场景目录划分。验证集可以小,但必须和训练集来自不同场景。划分后检查一遍两个集合里有没有文件名前缀相同的图片,有就说明同源帧还没清干净。

5.4 红绿灯小目标漏检:mosaic 把目标切没了

现象:整体 mAP 尚可,但远处的小红绿灯几乎全漏,近处的却能检测到。

原因:小目标在图像里只占几十像素,imgsz=640 输入下进一步缩小,再叠加 mosaic 拼接,目标可能在拼接边界被截断一半。模型在训练时大量看到残缺目标,自然学不会完整的红绿灯特征。

解决:把 imgsz 提到 960 并相应降 batch;mosaic 增强在训练后 1/3 轮数关掉;把包含小目标的图片在训练目录里多复制几份,对应标签同步复制,增加这类样本的曝光度。还有一个土办法:把包含远距离红绿灯的图,在预处理时只做轻微缩放,不要用大的 scale 增强。

5.5 txt 里出现 NaN 或超界坐标:size 字段与图片不一致

现象:转换完成后扫一遍 txt,发现某些行出现 nan,或者 width、height 算出来大于 1。

原因:xml 的 size 字段和实际图片宽高不一致,常见于图片被批量压缩后 xml 没同步更新。bndbox 本身也可能标错,比如 xmax 比图片宽度还大。

解决:转换脚本里加范围断言,x_center、y_center、box_w、box_h 任一超出 0 到 1 就打印文件名。批量扫描已有的 txt,把含 nan 或超界数值的行挑出来定位到具体 xml,手工修正 size 或 bndbox。修正时以实际图片的宽高为准,不要看 xml 里写的。

for txt in label_files: for line in open(txt): parts = line.strip().split() if "nan" in line or float(parts[3]) > 1: print(txt, line)

6. 验证模型的两条捷径:按类 mAP 和 ONNX 导出实测

训练完别只盯着整体 mAP 看。整体 0.9 不代表红绿灯好用,按类别拆开才能暴露真实短板。我每次都会把验证结果按类打印,重点关注 yellow_light 和 no_entry 这类样本少的类别,如果它们的 mAP 明显低于 green_light,最常见的原因是样本不足,而不是模型结构问题。这里给一个我自己常用的验收标准作参考。

验证维度指标建议参考值
检测精度mAP500.85 以上
小目标召回AR 小目标红绿灯单独看,低于 0.7 要补样本
类别平衡per-class mAP各类之间差距小于 0.1
推理速度单张推理耗时1080p 下 30ms 以内

第二条捷径是导出 ONNX 做实测。训练完的 best.pt 在验证集上再好看也不如实拍视频有说服力,导出成 ONNX 后用 onnxruntime 跑单张推理,能同时验证模型在非训练环境下的数值精度和速度。

yolo export model=runs/detect/train/weights/best.pt format=onnx imgsz=640
import onnxruntime as ort import cv2 sess = ort.InferenceSession("best.onnx") img = cv2.imread("test.jpg") # 预处理:resize 到 imgsz、归一化、通道转 CHW,按原训练时的预处理顺序 # 推理后取 output 的 boxes 和 scores,按置信度阈值过滤 # 把归一化坐标换算回原图尺寸后画框

导出后如果发现输出结果和 PyTorch 推理不一致,先看预处理是否完全复刻训练流程,尤其是归一化除 255 和颜色通道顺序。如果部署环境对速度还有更高要求,常见路线是在 ONNX 基础上再做引擎优化,压榨剩余性能,这一步的收益比换更重的模型结构来得直接。我第一次拿这类数据集做实验时,就吃过随机划分的亏,当时看着 val mAP 高得离谱,拿到现场视频被打脸。后来老实按视频前 80% 切,数值才诚实起来。这类数据集的价值在标注质量,不在新模型结构,把数据管好、把验证做严格,效果远比折腾模型快。希望帮到你。

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

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

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

立即咨询