news 2026/9/23 18:02:08

交通灯检测数据集:XML转TXT与YOLO训练实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
交通灯检测数据集:XML转TXT与YOLO训练实战指南

简介:这是一份面向交通场景目标检测实验的交通标志与交通信号灯数据集,原创并由LabelImg手工标注,覆盖限速牌、警告牌以及红灯、绿灯、黄灯等常见类别,图片为真实路况高清照片,适合目标检测入门、模型效果对比和毕业设计使用。包内共1641个文件,包括877张高清png原图、761个xml标注文件、两个类别编号txt列表,以及一个用于格式转换的Python脚本,压缩包整体大小约217.98MB,文件组织清晰,可方便地接入YOLOv5、YOLOv8等主流检测框架。当前已有475人学习下载,数据量适中,标注信息完整,可直接作为训练集或验证集,减少自行采集与整理的时间。使用时可结合txt中的类别编号快速区分各类交通标志,也可借助自带Python脚本在xml与YOLO格式之间灵活转换,适配不同项目的输入要求,能为实验和调参节省不少前期准备工作。

1. 交通灯和标志牌共用一套数据集:双格式标注到底省了什么

做交通标志检测实验的人,十有八九在数据准备阶段先踩一个坑:网上下到的 VOC 数据是 xml 标注,YOLO 训练却只认 txt 的归一化坐标。如果你正在做红绿灯检测,会发现十字路口场景比高速路牌更难搞——信号灯在整张图里经常只有几十个像素,夜间过曝一下,人和模型都分不清那是红灯还是泛白的光斑。这个项目标题给出的数据集同时覆盖 traffic light 和 traffic sign,并且把 xml 和 txt 两种格式都备好了,正好省掉最磨人的格式转换和类别对齐环节。适合课设、毕设做目标检测实验,也适合想在小目标检测上积累经验的从业者。数据格式看得懂、坐标算得对,后面训练才不翻车。

2. 拆开 xml 和 txt 看看:两种标注格式谁该当 master

2.1 从 annotation 根节点读到 bndbox:xml 里每一层在说什么

VOC 格式的 xml 是目标检测数据集最常见的"原始凭证",它把整张图的元信息和每个目标的框都写成树状结构。拿到一个标注文件,用任意文本编辑器打开(Windows 下可以直接用记事本,VS Code 更顺手),结构大概是这样的:

<annotation> <folder>JPEGImages</folder> <filename>img_0001.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>traffic_light</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>1320</xmin> <ymin>450</ymin> <xmax>1362</xmax> <ymax>520</ymax> </bndbox> </object> </annotation>

foldder 和 filename 用于定位图片文件,size 里的 width 和 height 是转换 txt 时必读的原始尺寸,object 节点下 name 是类别名,bndbox 是左上角和右下角坐标。需要注意 truncated 表示目标是否被截断,difficult 表示该目标是否因过小、遮挡等原因难以辨认——很多训练脚本默认忽略 difficult=1 的框,转格式时最好也直接过滤。

xml 文件的优点是信息完整、人可以读懂,缺点是文件体积大,而且 YOLO 训练时根本不会去读它。它更像是标注环节的"原始存档",txt 才是喂给训练器的"菜"。保留 xml 作为 master 还有一个实际好处:后期想改类别名、合并数据集、重新抽样,都只需要对 xml 做批量操作,不需要去改已经归一化的 txt。

2.2 txt 一行的六个数字是怎么从 xml 算出来的

YOLO 的每个标注文件是纯文本,每一行代表一个目标,格式为:类别 id、归一化中心点 x、归一化中心点 y、归一化宽度 w、归一化高度 h。六个数字全部是浮点数,其中坐标和宽高都必须除以图片的原始宽高,范围在 0 到 1 之间。

import xml.etree.ElementTree as ET tree = ET.parse("img_0001.xml") root = tree.getroot() size = root.find("size") w = int(size.find("width").text) h = int(size.find("height").text) obj = root.find("object") bbox = obj.find("bndbox") xmin = int(bbox.find("xmin").text) ymin = int(bbox.find("ymin").text) xmax = int(bbox.find("xmax").text) ymax = int(bbox.find("ymax").text) x_center = (xmin + xmax) / 2 / w y_center = (ymin + ymax) / 2 / h box_w = (xmax - xmin) / w box_h = (ymax - ymin) / h print(f"0 {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}")

这段代码把第一个 object 的 bndbox 转成了一行 YOLO 格式标注。核心是除以 size 里的原始宽高,而不是除以某个固定值(比如 640)。很多人转换后坐标错位,就是因为拿来算归一化的尺寸和图片实际尺寸不一致。算出来的六个数字用空格分隔,类别 id 从 0 开始计数,顺序完全由你定义的类别表决定。

2.3 收到数据集先做一次体检:类别表、尺寸分布与难例占比

新手拿到数据集就急着训练,我一般会先花十分钟做个体检,避免后面排错排到怀疑人生。体检第一项是确认类别表——这个数据集到底包含哪些类,id 顺序是什么。很多数据集只有一个 traffic_light 类,有些把 traffic_sign 也单独列出来,还有把信号灯拆成 red/green/yellow 三个类的,训练出来的模型是完全不同的东西。类别表定了就不要乱改,改一次意味着所有 txt 都得重生成。

体检第二项是看样本分布,统计哪些类别样本多、哪些少。写个小脚本遍历所有 xml,把每个类别的difficult标签和框尺寸一并算出来:

import xml.etree.ElementTree as ET import os from collections import Counter xml_dir = "annotations" cls_counter = Counter() difficult_counter = Counter() small_box = 0 total_box = 0 for xml_name in os.listdir(xml_dir): tree = ET.parse(os.path.join(xml_dir, xml_name)) for obj in tree.getroot().iter("object"): cls_counter[obj.find("name").text] += 1 if obj.find("difficult").text == "1": difficult_counter[obj.find("name").text] += 1 bbox = obj.find("bndbox") w = int(bbox.find("xmax").text) - int(bbox.find("xmin").text) h = int(bbox.find("ymax").text) - int(bbox.find("ymin").text) total_box += 1 if max(w, h) < 32: small_box += 1 print("类别样本数:", dict(cls_counter)) print("difficult样本数:", dict(difficult_counter)) print(f"小目标(最长边<32px)占比: {small_box / total_box:.1%}")
检查项说明健康标准
类别分布每个类别在 train/val 中都有足够样本最少类别不低于 200 张
difficult 占比过小/遮挡目标过多会拉低 mAP建议低于 10%
小目标占比最长边小于 32 像素的框占比高于 30% 需针对性调参
图片尺寸混用大图和小图会导致训练不稳定建议统一 resize 到 640 或 1280

3. 把 xml 批量转成 txt:归一化坐标转换脚本与三个边界处理

3.1 不写代码的转换路径:labelimg 打开即转,但只适合小批

labelimg 是打标工具里最常用的一款,它能直接打开 xml 目录,也支持把 VOC 格式导出成 YOLO txt 格式。操作路径是:打开 labelimg 后用"打开目录"指向图片文件夹,在"PascalVOC"模式下加载对应 xml,再切换成 YOLO 模式保存,工具会按当前选择的类别表重新生成 txt。这个流程对单张或少量图片可行,但批量转换时效率很低,而且 labelimg 的类别列表需要你手动维护,顺序稍有不一致就会导致 id 错位。

所以我的实践是:labelimg 只用来做人工复核和补标,批量转换一律写脚本。补标的工作流是——先用现有 xml 训练一版模型,把模型预测结果导成伪标签,再用 labelimg 打开人工修正,修正完重新生成 txt。这套闭环能大幅减少全手工打标的时间。

3.2 写一个批量转换脚本:CLASSES 顺序、原图尺寸、越界裁剪是关键

批量转换脚本的核心逻辑和前面示例一致,但要多处理三个边界情况:类别不在表里怎么办、框超出图片边界怎么办、无效框怎么过滤。下面是可以直接改路径就用的版本:

import xml.etree.ElementTree as ET import os # 类别表顺序即 YOLO class_id 顺序,不要随意调整 CLASSES = ["traffic_light", "traffic_sign"] def xml_to_txt(xml_path, txt_path): tree = ET.parse(xml_path) root = tree.getroot() size = root.find("size") w = int(size.find("width").text) h = int(size.find("height").text) lines = [] for obj in root.iter("object"): name = obj.find("name").text # 边界1: 跳过不在类别表中的目标 if name not in CLASSES: continue # 边界2: 跳过 difficult 目标,避免干扰训练 if obj.find("difficult") is not None and obj.find("difficult").text == "1": continue bbox = obj.find("bndbox") xmin = float(bbox.find("xmin").text) ymin = float(bbox.find("ymin").text) xmax = float(bbox.find("xmax").text) ymax = float(bbox.find("ymax").text) # 边界3: 越界坐标裁剪到图片范围内 xmin = max(0, min(xmin, w)) xmax = max(0, min(xmax, w)) ymin = max(0, min(ymin, h)) ymax = max(0, min(ymax, h)) # 剔除裁剪后无效的框 if xmax <= xmin or ymax <= ymin: continue x_center = (xmin + xmax) / 2 / w y_center = (ymin + ymax) / 2 / h box_w = (xmax - xmin) / w box_h = (ymax - ymin) / h class_id = CLASSES.index(name) lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}") with open(txt_path, "w", encoding="utf-8") as f: f.write("\n".join(lines)) if __name__ == "__main__": xml_dir = "annotations" txt_dir = "labels" os.makedirs(txt_dir, exist_ok=True) for xml_name in os.listdir(xml_dir): if not xml_name.endswith(".xml"): continue txt_name = os.path.splitext(xml_name)[0] + ".txt" xml_to_txt(os.path.join(xml_dir, xml_name), os.path.join(txt_dir, txt_name)) print("转换完成")

这个脚本里的三个边界处理是实际使用中最容易踩坑的地方:类别不在表中直接跳过,避免生成错误 id;difficult 目标跳过,避免把难以辨认的样本硬塞给模型;越界坐标裁剪,避免负宽高导致损失函数计算异常。CLASSES列表的顺序必须和你后面训练时 dataset.yaml 里的 names 顺序完全一致,这是整个流程里最需要记住的一点。

3.3 转完之后怎么验:三个抽检习惯杜绝坐标错位

转换脚本跑完,不要急着训练,先花三分钟做抽检。第一个抽检是比对图片和 txt 的数量,一张图片必须对应一个 txt,多出来或缺少的都是问题。第二个抽检是打开几个 txt 看数值范围,所有坐标和宽高都应该在 0 到 1 之间,出现大于 1 或负数说明图片尺寸读错了。

第三个抽检是可视化。用 OpenCV 在图上画出 txt 里的框,人工看位置是否和实际目标吻合:

import cv2 # 把txt标注画回图上,确认转换没出错 img = cv2.imread("img_0001.jpg") h, w = img.shape[:2] with open("img_0001.txt") as f: for line in f: cid, 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) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, str(int(cid)), (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imwrite("check.jpg", img)

抽检时重点看两类情况:框是否整体偏移(多半是归一化时用了错误的分母),以及框是否只剩一条线(多半是宽或高被算成了 0)。抽检通过之后再进入训练阶段,能省下大量排错时间。

4. 训练自己的红绿灯检测模型:目录结构、环境与参数调法

4.1 数据目录和 dataset.yaml:names 顺序决定 class_id

训练之前先把数据目录组织好,Ultralytics YOLO 系列的目录约定是 images 和 labels 分开放,train 和 val 分开。我习惯的目录结构长这样:

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

对应的 traffic.yaml 文件:

path: datasets/traffic train: images/train val: images/val names: 0: traffic_light 1: traffic_sign

这里有个很容易被忽略的细节:names里的顺序必须和转换脚本里的CLASSES一致。训练时程序按行号读取 txt 的第一个数字作为类别 id,再拿这个 id 去查 names 列表。如果转换脚本里 traffic_light 是 0,而 yaml 里 0 对应 traffic_sign,训练不会报错,但模型学的东西完全错位,验证结果会非常诡异。

4.2 训练命令里的关键参数:imgsz、batch、epochs 与多尺度

数据组织好后,用 Ultralytics 的命令行就能开始训练。以 YOLOv8 为例,最小可用的训练命令是:

yolo train data=datasets/traffic/traffic.yaml \ model=yolov8s.pt \ epochs=100 \ imgsz=640 \ batch=16 \ patience=20

这几个参数是训练效果的基石。imgsz控制输入分辨率,交通灯是小目标,建议从 640 起步,显存允许直接上 1280,分辨率越高小目标保留的像素越多。batch受显存限制,一般 16 或 32,batch 太小会引入噪声,太大容易过拟合。epochs给到 100 到 200 之间,配合patience早停——连续 20 轮验证集指标不涨就自动停。

训练日志里除了总的 loss,还会分别显示 box_loss、cls_loss 和 dfl_loss。交通灯检测经常出现 box_loss 降不下去的情况,原因就是小目标的框稍有偏移,IoU 变化就非常大,损失函数波动剧烈。遇到这种情况,优先考虑加大imgsz,而不是盲目加训练轮数。

# 训练参数速查表 # imgsz: 640起步, 显存够用就1280, 小目标收益最明显 # batch: 16-32, 显存不足先降batch别降imgsz # epochs: 100-200, 配合早停看验证集指标 # patience: 10-20, 防止过拟合和算力浪费

4.3 小目标专项:红绿灯占图比例低时该怎么调 yolo 损失配置

交通灯检测的核心难点是小目标。一个 1920x1080 的十字路口画面里,信号灯区域往往只有 30x30 像素,占整图面积不足 0.03%。直接用默认参数训练,模型会把注意力全放在大面积的车身和路面上。

常见做法是开启多尺度训练,让模型在不同输入尺寸下学习目标特征,增强对小目标的鲁棒性:

yolo train data=datasets/traffic/traffic.yaml \ model=yolov8s.pt \ epochs=150 \ imgsz=1280 \ batch=8 \ multi_scale=True \ cos_lr=True

multi_scale=True会在训练过程中随机把输入缩放 0.5 到 1.5 倍,相当于给模型做了数据增强,小目标占比高的数据集开启后收益明显。cos_lr让学习率按余弦曲线衰减,训练后期更平稳。

另外可以按需调整损失权重。YOLOv8 的损失配置里,box、cls、dfl 三项的比例影响模型对不同任务的侧重。交通灯类别少但框小,适当调大 box 的权重,让模型更在意框的回归精度:

from ultralytics import YOLO model = YOLO("yolov8s.pt") # 调大box_loss权重, 让小目标检测的框定位更准 results = model.train( data="datasets/traffic/traffic.yaml", epochs=150, imgsz=1280, batch=8, multi_scale=True, cos_lr=True, box=8.0, cls=0.5, dfl=1.5, )

5. 交通标志检测训练避坑清单:五个最容易翻车的现场

5.1 验证集 mAP 虚高,部署后却乱检

现象:训练日志里验证集 mAP 达到 0.9 以上,可把模型接到实际视频流里,画面里没有红绿灯的地方也在乱框。

原因:数据划分方式出了问题。如果 train 和 val 是随机划分的,同一段连续视频帧会同时出现在两个集合里,模型相当于"背过"了答案。这在交通场景数据里特别常见,因为采集时都是连续录像截帧,相邻帧高度相似。

解决:按采集来源或场景文件夹划分数据集,而不是按单张图片随机切。比如把 5 条不同路口的视频各抽出一部分做 val,保证验证集场景和训练集不重叠。划分完检查一下 val 里图片的文件名前缀是否和 train 有明显交集。

5.2 红灯在逆光下全漏检,白天数据里根本看不出来

现象:测试集整体 mAP 正常,但把模型拿到傍晚逆光的十字路口,红灯几乎全部漏检,偶尔还把白色路灯错认成信号灯。

原因:信号灯本身是自发光目标,逆光时镜头产生光晕,红灯区域过曝成白色或橙色,颜色特征被破坏。而数据集里正常天气、正光条件下的样本占绝大多数,模型从没学会"过曝的红灯也是红灯"。

解决:训练时把白平衡、曝光、HSV 里的饱和度做随机扰动,模拟不同光照条件。关键做法是单独在逆光时段采集几百张难例,手工标注后加入训练集。这类难例样本比增加整体数据量更值钱,我一般会单独建一个 hard_examples 目录,在训练配置里加大这类图片的采样权重。

5.3 class_id 对不上,训练能跑但完全错位

现象:训练跑完,推理时模型把 traffic_sign 全识别成 traffic_light,有时甚至把两个类完全反着输出。

原因:转换脚本里的 CLASSES 列表顺序和 dataset.yaml 里的 names 顺序不一致。txt 里写的是"第几个类",它本身不存类别名,全靠两个文件对齐。手动维护容易出现这种错位。

解决:不要手写 yaml 里的 names,写个小脚本从 CLASSES 自动生成,或者从参数文件里读。我习惯用同一个配置源管理类别表,转换脚本读它,data.yaml 也从它生成,从根上杜绝顺序不一致的可能。

5.4 调置信度门限调出玄学,框时有时无

现象:同一条视频,置信度阈值调到 0.2 时红灯旁边全是噪声框,调到 0.5 时又突然一个都不出。

原因:置信度门限本质是在查准率和查全率之间找平衡点,而这个平衡点在不同场景下是变化的。白天光线好时模型给真实目标的置信度普遍集中在 0.3 到 0.6 之间,逆光和夜间则普遍偏低。固定一个门限无法适配所有时段。

解决:把门限当作场景化参数来调节。白天用 0.3 到 0.4,夜间和逆光降到 0.15 到 0.2。更稳的做法是引入帧间稳定性过滤——相邻几帧里同一个位置连续出现才输出结果,单独一帧闪现的框直接丢弃。调门限不要靠感觉,直接跑一次验证集,画出置信度和 mAP 的曲线,选曲线拐点附近的值。

5.5 xml 转 txt 后坐标出界,训练时 anchor 匹配直接崩

现象:训练日志里 loss 从第一轮开始就剧烈震荡,偶尔出现 nan。检查发现部分 txt 里的坐标大于 1 或为负数。

原因:转换脚本用了固定尺寸做归一化分母,而不是读取 xml 里 size 节点的实际宽高。比如把图片放到了统一尺寸再标注,转换时却按原图尺寸算,或者反过来。坐标出界后模型算 IoU 时匹配不到有效 anchor,损失计算也就失去意义。

解决:回归转换脚本,确认归一化分母来自 xml 里的 width 和 height。转换完写一个检查脚本,统计所有 txt 里数值的 min 和 max,出现小于 0 或大于 1 的立即停下来排查来源。这是防翻车最直接的一道保险。

6. 用夜间与逆光子集验证模型水平:一张评估表看清边界

模型训练完,我建议不要只看总的 mAP,而是把验证集按场景切成白天、夜间、逆光三个子集分别评估。交通灯场景的白天样本好认,真正决定模型能不能用的是夜间和逆光表现。评估命令非常简单:

yolo val data=datasets/traffic/traffic.yaml \ model=runs/detect/train/weights/best.pt \ split=val \ conf=0.25 \ iou=0.5

输出结果里会给出每个类别的 mAP@0.5 和 mAP@0.5:0.95。我习惯把手动切好的场景子集放到一个单独目录里,每个子集单独跑一次 val,然后记录成一张表。加一个conf扫描,从 0.1 到 0.5 逐步提高,看不同场景下置信度阈值对结果的影响曲线,需要部署时直接对着这张表选阈值。

子集样本数mAP@0.5mAP@0.5:0.95备注
白天顺光5000.960.81常规表现
逆光2000.780.52需要难例补充
夜间3000.830.61过曝仍是瓶颈

验证子集的数据划分必须独立于训练集来源,否则这张表只是自我安慰。做了一轮之后你会明显感觉到,模型改进的方向和预算分配都清晰了:先补逆光难例,再调夜间置信度门限,比盲目加训练轮数有效得多。我自己吃过最大的亏,就是把验证集当测试集反复调参,最后换来一个只在报告里好看的模型。把夜间和逆光子集固定下来、只做最终评估,每次改动模型都能有个靠谱的反馈。希望这份流程能帮你在交通标志检测上少走几步弯路。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 17:56:23

STM32开源项目三件套实测:代码、原理图与仿真的完整上手指南

从收藏夹吃灰到真正跑通&#xff0c;我花了两个晚上把一套网上开源的STM32项目完整过了一遍。这套项目就是很多初学者硬盘里都有的江科大STM32&#xff0c;代码、原理图、仿真三件套配得很齐。网上讨论这套资源的帖子很多&#xff0c;但大多数停留在"视频讲得好"&quo…

作者头像 李华
网站建设 2026/9/23 17:51:03

超声腹部多器官分割实战:从数据预处理到模型训练避坑指南

简介&#xff1a;超声腹部多器官图像分割数据集面向医学影像分析、深度学习与计算机辅助诊断研究者&#xff0c;覆盖肝脏、肾脏、胆囊、脾脏、胰腺、血管及肾上腺等主要腹部结构&#xff0c;适合多器官分割模型的训练、验证与算法对比。包内共1855个文件&#xff0c;主体为1853…

作者头像 李华
网站建设 2026/9/23 17:44:47

《君子之交》深度书评:人物、阅读顺序与txt合集整理指南

从来没有哪本小说&#xff0c;让我在读完txt全集之后&#xff0c;把手机扣在桌上发了十分钟呆。《君子之交》做到了。它连着一个续篇&#xff0c;还带一组番外&#xff0c;合在一起像一坛埋了很多年的酒&#xff0c;入口不烈&#xff0c;后劲却大得离谱。我后来又把文件里的“正…

作者头像 李华