简介这是一份面向交通场景目标检测实验的交通标志与交通信号灯数据集原创并由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 看看两种标注格式谁该当 master2.1 从 annotation 根节点读到 bndboxxml 里每一层在说什么VOC 格式的 xml 是目标检测数据集最常见的原始凭证它把整张图的元信息和每个目标的框都写成树状结构。拿到一个标注文件用任意文本编辑器打开Windows 下可以直接用记事本VS Code 更顺手结构大概是这样的annotation folderJPEGImages/folder filenameimg_0001.jpg/filename size width1920/width height1080/height depth3/depth /size object nametraffic_light/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin1320/xmin ymin450/ymin xmax1362/xmax ymax520/ymax /bndbox /object /annotationfoldder 和 filename 用于定位图片文件size 里的 width 和 height 是转换 txt 时必读的原始尺寸object 节点下 name 是类别名bndbox 是左上角和右下角坐标。需要注意 truncated 表示目标是否被截断difficult 表示该目标是否因过小、遮挡等原因难以辨认——很多训练脚本默认忽略 difficult1 的框转格式时最好也直接过滤。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(f0 {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 或 12803. 把 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, encodingutf-8) as f: f.write(\n.join(lines)) if __name__ __main__: xml_dir annotations txt_dir labels os.makedirs(txt_dir, exist_okTrue) 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(转换完成)这个脚本里的三个边界处理是实际使用中最容易踩坑的地方类别不在表中直接跳过避免生成错误 iddifficult 目标跳过避免把难以辨认的样本硬塞给模型越界坐标裁剪避免负宽高导致损失函数计算异常。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.yamlnames 顺序决定 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 datadatasets/traffic/traffic.yaml \ modelyolov8s.pt \ epochs100 \ imgsz640 \ batch16 \ patience20这几个参数是训练效果的基石。imgsz控制输入分辨率交通灯是小目标建议从 640 起步显存允许直接上 1280分辨率越高小目标保留的像素越多。batch受显存限制一般 16 或 32batch 太小会引入噪声太大容易过拟合。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 datadatasets/traffic/traffic.yaml \ modelyolov8s.pt \ epochs150 \ imgsz1280 \ batch8 \ multi_scaleTrue \ cos_lrTruemulti_scaleTrue会在训练过程中随机把输入缩放 0.5 到 1.5 倍相当于给模型做了数据增强小目标占比高的数据集开启后收益明显。cos_lr让学习率按余弦曲线衰减训练后期更平稳。另外可以按需调整损失权重。YOLOv8 的损失配置里box、cls、dfl 三项的比例影响模型对不同任务的侧重。交通灯类别少但框小适当调大 box 的权重让模型更在意框的回归精度from ultralytics import YOLO model YOLO(yolov8s.pt) # 调大box_loss权重, 让小目标检测的框定位更准 results model.train( datadatasets/traffic/traffic.yaml, epochs150, imgsz1280, batch8, multi_scaleTrue, cos_lrTrue, box8.0, cls0.5, dfl1.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 datadatasets/traffic/traffic.yaml \ modelruns/detect/train/weights/best.pt \ splitval \ conf0.25 \ iou0.5输出结果里会给出每个类别的 mAP0.5 和 mAP0.5:0.95。我习惯把手动切好的场景子集放到一个单独目录里每个子集单独跑一次 val然后记录成一张表。加一个conf扫描从 0.1 到 0.5 逐步提高看不同场景下置信度阈值对结果的影响曲线需要部署时直接对着这张表选阈值。子集样本数mAP0.5mAP0.5:0.95备注白天顺光5000.960.81常规表现逆光2000.780.52需要难例补充夜间3000.830.61过曝仍是瓶颈验证子集的数据划分必须独立于训练集来源否则这张表只是自我安慰。做了一轮之后你会明显感觉到模型改进的方向和预算分配都清晰了先补逆光难例再调夜间置信度门限比盲目加训练轮数有效得多。我自己吃过最大的亏就是把验证集当测试集反复调参最后换来一个只在报告里好看的模型。把夜间和逆光子集固定下来、只做最终评估每次改动模型都能有个靠谱的反馈。希望这份流程能帮你在交通标志检测上少走几步弯路。本文还有配套的精品资源点击获取
