简介:一套面向计算机视觉目标检测任务的交通信号灯红绿灯颜色检测数据集,适合需要训练红绿灯识别模型的研究人员、算法工程师或高校课程实践使用。数据集包含19456张jpg图片,并为每张图片提供Pascal VOC格式xml与YOLO格式txt两套标注文件,类别覆盖green、red、yellow,总框数30432个,其中green框13164、red框15044、yellow框2224,可直接用于监督训练与验证。资源压缩包约895.71MB,文件总数2000个,以xml标注文件为主,附有说明txt文件,便于理解标注规则与目录结构;矩形框标注方式符合常见检测任务要求,可快速划分训练集与验证集,免去人工标注环节。已有366人学习下载,适合有明确红绿灯检测需求、希望跳过数据整理环节的开发者直接取用。
1. 交通信号灯红绿灯颜色检测:19450 张双格式数据集到底能帮你省下什么
同一个模型,白天路口跑得好好的,一进隧道就开始乱报——这是红绿灯检测项目最常见的痛点:公开数据集往往只标了信号灯的位置,没标红、黄、绿的颜色语义。这份交通信号灯红绿灯颜色检测数据集共 19450 张图、3 类颜色,同时提供 VOC 和 YOLO 两种标注格式,正好补上这个缺口。
它解决的问题很实在:省掉从零采集、标注的重复劳动,让你只需要验证标注质量,就能直接进入 YOLOv5/v8/v11 的训练流程。新手可以借它把 VOC 和 YOLO 两种格式的差异和转换原理彻底弄懂;熟手可以省下好几天数据清洗的时间,把精力花在小目标召回和颜色误判这类真正难啃的点上。
2. VOC 标注怎么落到 YOLO 训练:先搞清目录和 XML 结构
收到这份资源的第一步不是急着训练,而是把目录结构和标注内容摸清楚。红绿灯检测的数据链路里,VOC 格式负责"给人看",YOLO 格式负责"给模型吃",两者之间靠文件名一一对应。下面先讲清楚双格式目录长什么样,再给一个解析脚本,把 XML 标签变成可统计的表格。
2.1 双格式目录结构:JPEGImages 与 labels 各管什么
解压后按常规组织方式,目录结构大致是下面这样。拿到手后先对着检查一遍,缺了哪个目录,后续训练就会在哪一步翻车。
TrafficLight_Dataset/ ├── VOC/ │ ├── JPEGImages/ # 19450 张 jpg 原图 │ ├── Annotations/ # 19450 个 xml 标注,与图片同名 │ └── ImageSets/ │ └── Main/ │ ├── train.txt # 训练集文件清单 │ └── val.txt # 验证集文件清单 ├── YOLO/ │ ├── images/ │ │ ├── train/ # 按 YOLO 惯例划分的图片 │ │ └── val/ │ ├── labels/ │ │ ├── train/ # 同名的 txt 标签 │ │ └── val/ │ └── data.yaml # 类别名与路径配置 └── classes.txt # 类别清单:red, yellow, green重点看两处。第一,JPEGImages 和 Annotations 必须完全同名,一个 jpg 对应一个 xml,数量对不上说明打包时有遗漏;第二,YOLO 侧的 labels 目录里,每张图对应一个同名 txt,每行是一个目标。如果这份资源自带的 YOLO 标签已经划分好了 train/val,训练前只需要核对类别顺序,不用重新转换;如果只有 VOC 标注,就按第 3 章的脚本自己做转换。
两种格式的核心差异在坐标表达上。VOC 用的绝对像素坐标,YOLO 用归一化后的中心点坐标,这块是后面所有转换逻辑的根基,先列一个对比表记住。
| 对比项 | VOC XML | YOLO txt |
|---|---|---|
| 文件后缀 | .xml | .txt |
| 坐标形式 | xmin, ymin, xmax, ymax 绝对像素 | class_id, cx, cy, w, h 归一化 |
| 是否归一化 | 否,直接用像素值 | 是,除以图片宽高,取值 0~1 |
| 类别表达 | 字符串,如 red | 整数索引,如 0 |
| 典型工具链 | labelImg、VOC 系评测脚本 | YOLO 系训练框架 |
记住一个关键点:VOC 里 xmin/xmax 是"框住车灯"的矩形,YOLO 里的 cx/cy 是同一个矩形的中心点,w/h 是矩形宽高在图片宽高中的占比。坐标本身不变,变的只是表达方式。
2.2 把 XML 标签读进表格:一个脚本看清三类颜色分布
拿到 XML 先别急着转格式,先做一次"人口普查":每张图有哪些类别的框、框的大小分布怎么样、有没有坐标越界。这个习惯能帮你提前发现标注里的大小坑。
import glob import xml.etree.ElementTree as ET import pandas as pd rows = [] # 注意路径换成你解压后的实际目录 xml_list = sorted(glob.glob("VOC/Annotations/*.xml")) for xml_path in xml_list: tree = ET.parse(xml_path) root = tree.getroot() filename = root.findtext("filename") img_w = int(root.find("size").findtext("width")) img_h = int(root.find("size").findtext("height")) for obj in root.findall("object"): name = obj.findtext("name").strip() # red / yellow / green bndbox = obj.find("bndbox") xmin = int(float(bndbox.findtext("xmin"))) ymin = int(float(bndbox.findtext("ymin"))) xmax = int(float(bndbox.findtext("xmax"))) ymax = int(float(bndbox.findtext("ymax"))) rows.append({ "filename": filename, "class": name, "img_w": img_w, "img_h": img_h, "xmin": xmin, "ymin": ymin, "xmax": xmax, "ymax": ymax, "box_w": xmax - xmin, # 框宽像素 "box_h": ymax - ymin, # 框高像素 }) df = pd.DataFrame(rows) # 1. 每个类别的目标数量 print("=== 类别分布 ===") print(df["class"].value_counts()) # 2. 极小目标数量(宽度 < 15px 的信号灯) print("=== 小目标数量(框宽<15px) ===") print((df["box_w"] < 15).sum()) # 3. 坐标越界检查 print("=== 越界检查 ===") print((df["xmax"] > df["img_w"]).sum(), (df["ymax"] > df["img_h"]).sum())这段脚本三个输出分别回答三个问题:三类颜色是否齐全、分布是否极端不均、标注是否有越界。类别统计如果发现 red 有 5000 个框、yellow 只有 800 个框,训练时就要考虑对黄色做增强,否则模型会对黄灯视而不见。小目标数量统计则告诉你这份数据里远距离信号灯占比,直接影响训练时 imgsz 该设多少。
一个血泪经验:XML 里的类别名字段经常出现拼写变体,比如 red 和 red_light 混着标、green 标成 grean。出现这种情况时,先用df["class"].unique()打印全部类别名,确认是三类还是五类,再决定要不要做名字归一化。否则转成 YOLO 后模型会多出两个莫名其妙的类,训练出来的结果全是玄学。
3. 从 VOC 到 YOLO:转换脚本、类别映射与数据划分
如果资源里只带了 VOC 标注,或者你想按自己的类别顺序重新生成 YOLO 标签,这一步就得自己动手。整条转换链路就两步:把 XML 的绝对坐标转成归一化中心坐标,再把数据按训练/验证划分好。这个章节直接给可复制的代码和参数说明。
3.1 xml2yolo 转换脚本:坐标计算与 6 位小数约定
VOC 和 YOLO 的转换公式并不复杂:中心点 x 坐标等于(xmin + xmax) / 2再除以图片宽度,宽高分别除以图片宽高。真正的坑在类别映射顺序和坐标边界处理上,下面这个脚本把这两件事都做了。
import os import glob import xml.etree.ElementTree as ET # 类别映射表:VOC 字符串名 -> YOLO 整数 id # 注意:这个顺序必须和训练时 data.yaml 里的 names 完全一致 CLASS_MAP = {"red": 0, "yellow": 1, "green": 2} def convert_xml_to_yolo(xml_path, label_dir): """单个 xml 转成同名 txt,写入 label_dir""" tree = ET.parse(xml_path) root = tree.getroot() filename = root.findtext("filename").strip() img_w = int(root.find("size").findtext("width")) img_h = int(root.find("size").findtext("height")) txt_path = os.path.join(label_dir, filename.replace(".jpg", ".txt")) lines = [] for obj in root.findall("object"): name = obj.findtext("name").strip() if name not in CLASS_MAP: print(f"[跳过] {filename} 出现未知类别: {name}") continue bndbox = obj.find("bndbox") xmin = int(float(bndbox.findtext("xmin"))) ymin = int(float(bndbox.findtext("ymin"))) xmax = int(float(bndbox.findtext("xmax"))) ymax = int(float(bndbox.findtext("ymax"))) # 越界保护:坐标溢出图片时裁回边界,避免训练 loss 变 nan xmin = max(0, min(xmin, img_w - 1)) ymin = max(0, min(ymin, img_h - 1)) xmax = max(0, min(xmax, img_w - 1)) ymax = max(0, min(ymax, img_h - 1)) # 核心转换:中心点坐标和宽高均除以图片宽高,得到 0~1 的归一化值 cx = (xmin + xmax) / 2.0 / img_w cy = (ymin + ymax) / 2.0 / img_h bw = (xmax - xmin) / img_w bh = (ymax - ymin) / img_h # 6 位小数是 YOLO 惯例,精度足够且文件体积可控 lines.append(f"{CLASS_MAP[name]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") if lines: with open(txt_path, "w") as f: f.write("\n".join(lines) + "\n") # 批量转换 xml_files = sorted(glob.glob("VOC/Annotations/*.xml")) os.makedirs("YOLO/labels", exist_ok=True) for xml_file in xml_files: convert_xml_to_yolo(xml_file, "YOLO/labels") print(f"转换完成,共处理 {len(xml_files)} 个 xml")这段代码里有三个细节值得单独说。第一个,越界保护不是可选项:标注工具里手抖画出的框偶尔会超出图片边界,如果不在转换这步做 clip,训练读到大于 1 的归一化坐标时,轻则报 warning,重则 loss 变成 nan。第二个,类别映射表是整个链路的"单一真源",CLASS_MAP 和 data.yaml 的 names 顺序一旦不一致,模型训练的标签就是错位的,这个问题排查起来最费时间。第三个,.6f是 YOLO 训练框架的常见写法,位数太少框会抖动,位数太多没有意义。
转换完随手抽查一个 txt 文件,内容应该是这样的格式,每行五列,第一列是类别整数。
0 0.531250 0.472222 0.052083 0.083333 2 0.748800 0.351000 0.044800 0.067000第一行0表示红灯,后面依次是 cx、cy、框宽、框高,全部在 0~1 之间。如果看到某列大于 1,说明分母除错了——常见是宽度和高度写反。
3.2 训练集划分:随机种子、8:2 比例与文件清单生成
转换完成后要做的第二件事是划分训练集和验证集。19450 张图按 8:2 划分,训练集约 15560 张、验证集约 3890 张,这个量级对训练一个红绿灯检测模型是够用的。划分时最容易犯的错误是只移动了图片忘了同步标签,所以脚本里要保证图片和 txt 一一对应。
import os import glob import random import shutil random.seed(782) # 固定种子,保证每次划分结果可复现 img_files = sorted(glob.glob("YOLO/images/*.jpg")) random.shuffle(img_files) val_count = int(len(img_files) * 0.2) # 20% 做验证集 val_files = img_files[:val_count] train_files = img_files[val_count:] for split, files in [("train", train_files), ("val", val_files)]: os.makedirs(f"YOLO/images/{split}", exist_ok=True) os.makedirs(f"YOLO/labels/{split}", exist_ok=True) for img_path in files: # 文件名不含扩展名,用来定位同名的 txt stem = os.path.splitext(os.path.basename(img_path))[0] label_path = f"YOLO/labels_all/{stem}.txt" # 上一步转换出的标签目录 shutil.move(img_path, f"YOLO/images/{split}/{stem}.jpg") shutil.move(label_path, f"YOLO/labels/{split}/{stem}.txt") print(f"训练集 {len(train_files)} 张,验证集 {len(val_files)} 张")划分逻辑本身简单,但有两个习惯我建议你养成。第一,固定随机种子,否则每次跑脚本划分结果都不同,前后两次实验就没有可比性。第二,划分完之后立刻数一遍 labels 目录里的 txt 数量,跟 images 数量对上,差一个文件都要找出来。train/val 比例也可以按需调整,如果验证集太小、评估结果噪声大,可以把比例调到 0.15;如果数据里场景多样性强,保持 0.2~0.25 更稳。
如果是做交叉验证或需要精细评测,可以在划分前加一层"场景去重":先把同一路口、连续帧的图片按文件名前缀分组,再按组划分,避免同一路口的相似帧同时出现在训练集和验证集里导致指标虚高。这份数据集的图片如果有按时间序列命名的规律,这个去重步骤值得做。
4. 避坑:从标注清洗到训练翻车的五个常见问题
数据准备到这一步,最脏最累的活已经完了。但根据我自己的经历,真正让项目延期的大概率不是模型结构选型,而是下面这五类问题。每一条都按"现象 → 原因 → 解决"写清楚,你在训练阶段遇到类似状况,直接对号入座。
4.1 训练时类别数量对不上:3 类变 1 类
现象:训练日志里nc=3,但数据集里的标签文件读出来却有 5 类;或者训练过程中报 warning,提示某些 txt 里的类别 id 超出范围。
原因:VOC 转 YOLO 时用的类别映射顺序,和训练时 data.yaml 的 names 顺序不一致。最常见的是转换脚本里写死red:0, yellow:1, green:2,而 data.yaml 里把 red 放在第 2 位,导致所有标签整体错位。
解决:训练前先写一段扫描脚本,遍历所有标签 txt,用集合统计出现过的类别 id。
# 快速扫描 labels 里所有 txt 的第一列,看看实际出现哪些类别 id cat YOLO/labels/train/*.txt | awk '{print $1}' | sort -n | uniq -c输出结果应该只有 0、1、2 三类。如果出现 3、4 等超范围值,先回去核对 CLASS_MAP 和 data.yaml 的 names 顺序;如果出现 0 和 2 但没有 1,说明样本里可能没有黄灯,需要检查原始 XML 的类别名是不是拼写不一致。
4.2 归一化坐标越界:训练 loss 直接变成 nan
现象:训练刚开始几个 batch,loss 就跳成nan,或者训练能跑完但验证时画出的框全部偏到图片边缘外。
原因:VOC 标注里 bndbox 坐标超出了图片宽高范围,转换时没有做越界保护。另一个原因是转换公式里分子分母用错,比如cx = (xmin + xmax) / 2.0 / img_h,用了高度去归一化 x 坐标。
解决:转换脚本里给 xmin/xmax/ymin/ymax 加 clip,把坐标限制在0 ~ img_w-1和0 ~ img_h-1之间。训练前再用第 2.2 节的统计脚本跑一遍越界检查,确保xmax > img_w的数量是 0。如果发现越界标注,优先定位是哪几个 xml 文件,用脚本自动修正边界值,而不是整份数据重标注。
4.3 黄灯样本太少:整体 mAP 好看,per-class AP 露馅
现象:训练结束看验证结果,mAP50 有 0.85 以上,觉得模型不错;但打开 per-class AP 一看,green 和 red 都在 0.9 上下,yellow 只有 0.2。
原因:交通信号灯本身的颜色出现频率就极不均衡。红灯和绿灯持续时间长,黄灯只闪几秒,数据采集时自然就少。模型在类别不均衡下会把弱势类学得保守,相当于变相忽略了黄灯。
解决:先看统计脚本的类别分布确认失衡程度,然后对黄灯样本做针对性补充。常用手段有两个:一是对黄色样本做过采样,复制几份放进训练集;二是对黄色做轻量增强,比如小范围亮度扰动、旋转 10~15 度。评估时必须看 per-class AP,Ultralytics 训练完的results.csv里可以直接拉出每一类的 AP 曲线。
4.4 小目标消失:远距离信号灯在 640 分辨率下被压没
现象:原图上能看清的远距离红绿灯,训练时用imgsz=640缩小后只有两三个像素,模型训练完对远处信号灯几乎不检。
原因:直接 resize 把小目标的信息压没了。信号灯本身就小,远距离时在 1920x1080 原图里可能只有 20x30 像素,缩到 640 后变成 7x10 像素,CNN 下采样几次后特征就消失了。
解决:先用第 2.2 节的脚本统计框宽分布,如果大量目标小于 15 像素,直接把训练 imgsz 拉到 960 或 1280,同时开启 letterbox 而不是直接拉伸。更高阶的做法是先按 2x 切块训练,推理时再滑窗检测;但对大多数项目来说,imgsz=960 配合 YOLOv8 已经能缓解绝大部分小目标丢失问题。
4.5 颜色增强把语义搞坏:红灯被改成了橙黄灯
现象:训练时开了默认的数据增强,验证阶段红灯的误报率突然变高,模型把红灯检测成黄灯,precision 掉得明显。
原因:数据增强里的 HSV 扰动,尤其是 hue 通道的随机偏移,会把红灯的色相改到橙色区间,绿灯改到黄绿区间。信号灯检测里颜色是最核心的语义,通用增强策略在这里不适用。
解决:在 data.yaml 里显式调小增强参数,把 hue 扰动关掉或限制在极小范围。用 Ultralytics 训练时,可以在augment参数上做约束,常见的做法是hsv_h=0.0, hsv_s=0.2, hsv_v=0.2,只保留少量饱和度和亮度扰动,完全关闭色相扰动。亮度扰动其实对红绿灯有益——它能模拟白天逆光和夜晚两种光照,但一定要控制幅度。
5. 把数据集跑起来:一次验证标注质量的 YOLOv8 实验与进阶思路
前面几步做好了,训练本身反而是最省心的环节。这一章用 YOLOv8 官方仓库跑一次完整实验,顺带验证数据集质量到底行不行,再讲一个按颜色拆指标排查问题的进阶习惯。
5.1 一份最简训练配置:data.yaml 与命令行参数
先把数据集的配置文件写好。Ultralytics 系的 YOLOv5/v8/v11 都认这种 YAML 格式,路径和 names 一定要和转换脚本的 CLASS_MAP 对齐。
# data.yaml path: ./YOLO # 数据集根目录,相对路径 train: images/train # 训练集图片目录 val: images/val # 验证集图片目录 names: 0: red 1: yellow 2: green训练命令用一句话就能起:
yolo detect train data=data.yaml model=yolov8s.pt imgsz=960 epochs=80 batch=16 device=0几个参数按项目实际调整:imgsz用 960 而不是 640,是考虑到第 4.4 条的小目标问题;epochs初期验证 50 轮就够看趋势,正式训练拉到 100~120 轮;batch按显存来,16 是一个 24G 显存下的稳妥值,显存小就降到 8。如果只是想快速验证标注质量,把model换成yolov8n.pt更快,s 模型的精度更高但训练时间多一倍。
训练完成后,优先打开runs/detect/train/results.csv,看两个指标:mAP50和mAP50-95。mAP50 高说明大目标检测没问题,mAP50-95 高说明框定位准、小目标也活下来了。如果 mAP50 不错但 mAP50-95 明显低,大概率是标注框边界不够精细,或者小目标占比高、定位偏差大。
5.2 进阶:按颜色拆指标,别被整体数字骗了
整体 mAP 会掩盖类别不均衡的问题,所以我的习惯是训练完立刻按颜色分别看指标。用 Ultralytics 的 val 接口可以直接拿到每个类别的 AP:
from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") results = model.val(data="data.yaml") # 按类别打印 AP50 names = results.names for i in range(len(names)): name = names[i] ap50 = results.box.ap50[i] print(f"{name}: AP50 = {ap50:.4f}")如果 yellow 类的 AP50 明显低于另外两类,先回到第 4.3 条做样本补充或增强;如果 green 和 red 之间有交叉误判,打开验证集上的混淆矩阵确认是不是增强导致颜色漂移。这也是我摸到的一个规律:红绿灯检测项目里,绝大多数"模型不听话"的问题,根源都在数据处理而不是网络结构。
做完上面这轮验证,整个数据链路就闭环了。顺手提一个能再进一步的方向:在视频场景里做时序去抖,比如 T4 1080p 25 帧的推理环境下,对连续帧的目标框做跟踪和投票,能显著减少单帧误检。但颜色判断尽量留在单帧做,多帧平均反而会把黄灯闪烁的节奏抹掉。从那以后,我每次拿到新数据集都强制先跑完统计脚本再谈训练,确认类别分布和框质量,绝不在黑匣子上盲训。这份红绿灯数据集你也按这套流程走一遍,能省下不少返工的时间,希望帮到你。
本文还有配套的精品资源,点击获取