简介:这份交通信号灯数据集面向计算机视觉与智能交通领域开发者,提供红、绿、黄三色信号灯的原始图像样本,适用于目标检测模型训练、自动驾驶感知验证、城市交通视频分析等场景。压缩包共2000个文件,其中1995张jpg图片构成核心图像库,覆盖不同角度、光线与路口环境下的信号灯样本;3个json文件提供COCO格式的目标标注,标注框对应红、绿、黄灯位置,另有2个txt文件用于类别列表或训练/验证划分说明,整体容量223.94MB。已有594人学习/浏览该资源。使用者可直接基于COCO标注接入主流检测框架,如YOLO、MMDetection等,免去手动标注成本;图片命名以traffic-light前缀统一编号,便于按批次管理、数据增强与样本筛选。该数据集适合作为算法实验、毕业设计或智能交通项目前期数据支撑,也可帮助初学者快速上手目标检测全流程。
1. 交通信号灯数据集与 COCO 标注:为什么目标检测的起点往往是一份 JSON
如果你正在做车路协同、自动驾驶感知或者路口视频分析,大概率会先下载一份“交通信号灯数据集,可识别红绿黄三种颜色并使用coco格式标记.zip”。解压后里面通常就是路口图片、一个标注 JSON 和类别说明,图里有红灯、黄灯、绿灯三种目标,每个目标都给了矩形框和颜色类别。COCO 格式的价值在于,它不是把类别写在文件名里,而是用结构化的 JSON 把图片、标注、类别串成一张关系表,主流检测训练框架读取后可以直接算损失、出评测,不用你手工解析。这篇笔记按“读标注、跑训练、做体检、排问题”的顺序展开,适合刚拿到数据集的初学者,也适合在信号灯小目标上反复漏检的工程师参考。
2. 读懂 COCO 格式的信号灯标注:从 json 字段到三色类别划分
2.1 一张图里的三段结构:images、annotations、categories 各管什么
COCO 格式的标注文件是一个大 JSON,最外层的键有info、images、annotations、categories四部分。images数组里每条记录代表一张图,常见字段是id、file_name、width、height;annotations数组里每条记录代表一个目标,字段是id、image_id、category_id、bbox、area,有时带segmentation;categories数组把类别 ID 映射到类别名。这三段不是平级的三份数据,而是通过id和image_id串起来的:图片是父表,目标是子表,类别是字典。信号灯数据集里,一张图往往有多个灯组,一个灯组里可能左右排列着红、黄、绿三颗灯珠,这种父表子表结构比 VOC 的 XML 更贴近真实路口。
import json with open("annotations/instances_train.json", "r", encoding="utf-8") as f: coco = json.load(f) img = coco["images"][0] ann = coco["annotations"][0] print("image 字段:", img) print("annotation 字段:", ann) img_id_to_file = {item["id"]: item["file_name"] for item in coco["images"]} dup_ids = len(coco["images"]) - len(img_id_to_file) print("重复 image id 数量:", dup_ids)用字典推导把 image id 映射到文件名,顺手检测重复 ID。有些标注工具导出的包,images里同一张图被写了两遍,训练时不一定会报错,但会重复计算 loss,让训练曲线变平滑,验证却偏低。另一点要注意file_name是相对于data_prefix的路径,如果数据集把图片放在train2017/下,file_name通常写成train2017/xxx.jpg,训练配置里就不能再加一次文件夹名,否则路径会变成train2017/train2017/xxx.jpg。
bbox是[x, y, width, height],原点在图像左上角;area一般等于width * height,但在带segmentation的多边形标注里,更严格的定义是多边形面积。信号灯通常没有复杂轮廓,矩形框足够,所以大部分包只提供 bbox 不提供掩膜,这对检测任务没有影响。
2.2 红黄绿在 categories 里怎么编号:类别 ID 稳定比名称更重要
类别 ID 决定了训练时 loss 是怎么分配颜色的。先写一个统计脚本,确认包里到底有几个类别、每类多少框,再决定要不要调整。
import json from collections import Counter with open("annotations/instances_train.json", "r", encoding="utf-8") as f: coco = json.load(f) id_to_name = {c["id"]: c["name"] for c in coco["categories"]} print("categories:", id_to_name) counts = Counter() for ann in coco["annotations"]: counts[id_to_name.get(ann["category_id"], "unknown")] += 1 print("类别统计:", dict(counts))这段逻辑很简单:遍历所有标注,按category_id翻译成名字再计数。输出里如果出现unknown,说明标注里写了 categories 之外的 ID,这是不合规包最常见的错误之一。类别名本身不重要,重要的是category_id在训练配置、模型输出、评测脚本三处保持一致。如果你自己合并过两个数据集,A 包红灯 ID=1,B 包红灯 ID=0,合并后的 JSON 必须统一,我习惯用{"red": 1, "yellow": 2, "green": 3}这样的规则重新映射,而不是依赖原始文档里的文字描述。
颜色名称也会有坑:有人标注成red_light、green_light、yellow_light,有人标注成红色、绿灯;同一份 JSON 里出现两种写法,模型不会报错,但会把它们当成两个类别,导致黄灯被拆成“黄灯”和“yellow”两类,各自的样本量都不够。拿到包的第一天就统一类别词表,能省掉后面所有调试时间。
2.3 信号灯是典型小目标:bbox 与 area 的换算关系会直接改变评估结果
COCO 官方评测把目标按面积分三档:小目标面积小于32*32=1024像素,中目标小于96*96=9216像素,以上算大目标。这个阈值不是拍脑袋定的,它直接影响mAP_small、mAP_medium的划分。信号灯很有趣:在 1920x1080 的图片里,一颗 40x40 像素的灯面积是 1600 像素,按阈值属于中目标;但如果把图片缩放到 640x640,灯变成约 18x18 像素,面积不到 400 像素,又变成了小目标。
import numpy as np areas = [ann["area"] for ann in coco["annotations"] if ann["area"] > 0] if areas: print("面积分位(px):", np.percentile(areas, [10, 50, 90]).round(1))用分位数看目标尺寸分布比看平均值可靠。如果 50 分位面积只有 500 像素,说明大量灯珠在图里非常小,训练时应该用原图或高分辨率输入,千万不要为了省显存把图缩到 512。如果面积分布里冒出好几个面积几万像素的框,多半是标注把整个信号灯箱体框进去了,而不是单颗灯珠,这类样本会让模型学到“灯箱就是灯”,部署时反而在远距离漏检。
| COCO 档位 | 面积阈值 | 信号灯常见表现 |
|---|---|---|
| small | < 1024 px | 远距离路口灯,占大头 |
| medium | < 9216 px | 近景灯组,模型最容易学习 |
| large | >= 9216 px | 灯箱特写,通常不是单灯 |
这个表和 bbox 换算关系,决定了你后面看 mAP 时应该关注哪一档。如果模型mAP_medium高、mAP_small低,这是信号灯任务的常态,不是模型坏了。
3. 用 MMDetection 跑通这份 COCO 信号灯数据集:最小配置与三个必调参数
3.1 为什么优先选 MMDetection,而不是自己写训练循环
拿到 COCO 格式的数据,最省事的路线是直接上 MMDetection。它把数据加载、模型、训练策略全部配置化,改几行就能训练,而且评测输出直接包含mAP_small、mAP_medium这些档位指标。这套生态里不止有检测,像 mmrotate 训练 DOTA 旋转框数据集、MMSegmentation 做道路分割,用的都是同一套数据接口,你在这里踩过的 COCO 坑,换到旋转框、语义分割任务依然适用。如果之后想用 YOLOv8 训练自己的数据集做部署,也是先把 COCO 转成 YOLO txt 再喂进去,不建议直接从 COCO 绕开检测框架手写训练循环。
Detectron2 也支持 COCO,但配置风格和社区资源这两年明显不如 MMDetection 密集,信号灯这种小目标任务需要频繁调img_scale和增强,MMDetection 的配置覆盖更直接。我的习惯是先用 MMDetection 把基线跑出来,确认红黄绿三色可分,再谈部署优化;否则直接上部署框架,黄灯翻车时很难分清是数据问题还是模型问题。
3.2 最小配置:改 metainfo、数据根目录与 annotation 路径
先把数据整理成标准的 COCO2017 风格目录,训练集和验证集分开:
mkdir -p data/traffic_light/train2017 mkdir -p data/traffic_light/val2017 mkdir -p data/traffic_light/annotations然后写一份基于 Faster R-CNN 的配置。下面这份配置只改数据部分,模型结构保持 COCO 预训练权重初始化:
_base_ = "../faster_rcnn/faster-rcnn_r50_fpn_1x_coco.py" data_root = "data/traffic_light/" metainfo = dict( classes=("red", "yellow", "green"), ) train_dataloader = dict( batch_size=4, num_workers=4, dataset=dict( data_root=data_root, metainfo=metainfo, ann_file="annotations/instances_train.json", data_prefix=dict(img="train2017/"), ), ) val_dataloader = dict( dataset=dict( data_root=data_root, metainfo=metainfo, ann_file="annotations/instances_val.json", data_prefix=dict(img="val2017/"), ), ) val_evaluator = dict( ann_file=data_root + "annotations/instances_val.json", ) default_hooks = dict( checkpoint=dict(interval=3, save_best="coco/bbox_mAP"), ) train_cfg = dict(max_epochs=40)这段配置做了三件事:用metainfo覆盖类别名称;把训练和验证的数据路径指到正确位置;把验证集标注文件单独传给val_evaluator。data_prefix只写train2017/,因为file_name本身可能已经包含train2017/前缀,如果两边重复,路径会拼接错误。训练命令:
python tools/train.py configs/traffic_light/faster-rcnn_r50_fpn_1x_tl.py --work-dir work_dirs/traffic_light如果你的 JSON 没有单独的验证集,可以用tools/misc/split_coco.py从训练集里按比例切出一份,保持类别的比例一致,尤其要保证黄灯在验证集里也存在。
3.3 三个必调参数:img_scale、batch_size、eval 间隔
第一个参数是img_scale。信号灯是小目标,全图缩放会让灯珠变得更小,所以分辨率不能一味求低。默认(1333, 800)是 COCO 预训练常见的输入,但如果你手上的原图是 1920x1080,建议测试推理时用(1920, 1080),或者至少在验证时开多尺度。代价是显存上升,因此下一步调 batch。
第二个参数是batch_size。交通灯数据集通常只有几千张,BN 层的统计量需要足够多样本才稳定。按显存调节:
| 显存 | batch_size | 备注 |
|---|---|---|
| 6GB | 2 | 配梯度累积凑到 16 |
| 11GB | 4 | 常见起步值 |
| 24GB | 8 | 可以开更强的增强 |
第三个参数是eval间隔。上面的配置已经把checkpoint.interval改为 3,save_best设为coco/bbox_mAP。默认 12 epoch 存一次权重对信号灯数据集太稀疏,黄灯曲线要到很晚才出现,你等于在盲跑。每 3 个 epoch 验证一次,能提前看到mAP_small是否在涨,也方便判断是否要提前停车。
这三个参数之外,初始学习率lr=0.001对 batch_size=16 是合理的;如果你的总 batch 小于 16,学习率要相应下调到 0.0005 左右,否则训练前期 loss 不降是常态。
4. 训练前先做数据体检:COCO 标注质量与信号灯增强策略
4.1 检查空洞、重复与越界:一个一跑就懂的小脚本
数据集的坑多半不在模型,而在标注。首要查的是空洞:图里有但没有任何目标。信号灯数据里经常混入无灯路口、隧道进出帧、天色全黑的无效图,如果这类图占了三成,模型会把“没有灯”学成“不输出”,验证集因为空图不参与计算而毫无察觉。
import json with open("annotations/instances_train.json", "r", encoding="utf-8") as f: coco = json.load(f) img_ids = {img["id"] for img in coco["images"]} ann_img_ids = {ann["image_id"] for ann in coco["annotations"]} empty_ids = img_ids - ann_img_ids print(f"无标注图片数: {len(empty_ids)}") for img in coco["images"]: for ann in coco["annotations"]: if ann["image_id"] != img["id"]: continue x, y, w, h = ann["bbox"] if x < 0 or y < 0 or x + w > img["width"] or y + h > img["height"]: print(f"越界框: {img['file_name']}, bbox={ann['bbox']}")脚本用 set 差集找空图,再遍历所有图片和标注判断 bbox 是否越界。越界框常见于标注工具的坐标系错乱,从 PASCAL VOC 转 COCO 时xmax和width含义混用,右边多算一个像素。MMDetection 训练时会忽略部分越界样本,但导出的模型会在图像边缘漏检,训练前修掉最省事。
还需要查重复标注 ID。annotation的id是全局唯一键,如果两份数据拼接时没重新编号,训练时可能出现同一个目标被当成两个目标的情况,loss 计算不平滑。
4.2 模糊、过曝与夜间样本:该删的要删,该留的要多留
信号灯本身的颜色特征很强,但真实路口的车灯炫光、雨夜反光、逆光,都会让灯珠在照片里接近白色。这类图片不是噪声,而是部署时一定会遇到的情况。我的处理方式是:模糊到人眼都判断不了颜色的先删,过曝但位置可辨的保留,并在增强阶段加入颜色扰动。
可以写一个脚本,统计每张图标注框内的亮度均值和模糊程度,输出成 CSV 让人工抽样检查:
import cv2 import json import numpy as np with open("annotations/instances_train.json", "r", encoding="utf-8") as f: coco = json.load(f) img_id_to_info = {img["id"]: img for img in coco["images"]} rows = [] for ann in coco["annotations"]: img = img_id_to_info[ann["image_id"]] path = "data/traffic_light/" + img["file_name"] image = cv2.imread(path) if image is None: continue x, y, w, h = [int(v) for v in ann["bbox"]] crop = image[max(y,0):y+h, max(x,0):x+w] gray = cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY) laplacian = cv2.Laplacian(gray, cv2.CV_64F).var() rows.append((path, laplacian, float(gray.mean()), ann["category_id"])) print("模糊度低于 50 的样本数:", sum(1 for r in rows if r[1] < 50)) print("亮度高于 240 的框数:", sum(1 for r in rows if r[2] > 240))Laplacian方差低于 50 通常意味着画面模糊,高于 240 的亮度均值说明框内几乎纯白。这些数字不是绝对标准,但可以帮你从几千张图里快速筛出重点要看的几十张。纯白灯珠框对红灯影响小,对黄灯影响大,因为黄灯褪色后和红灯、白灯高度相似。
4.3 针对小目标的增强:mosaic、多尺度与复制粘贴,哪个对灯真有效
信号灯不建议一上来就把 Mosaic 开到最大。Mosaic 把四张图拼在一起,整体缩小目标尺寸,对本来就小的灯珠不友好。如果你的图片是 1080p,我更推荐先调多尺度随机缩放,在 0.5~1.5 倍之间随机改变输入尺寸,模拟相机安装角度的远近变化。
train_pipeline = [ dict(type="LoadImageFromFile"), dict(type="LoadAnnotations", with_bbox=True), dict( type="RandomResize", scale=(1333, 800), ratio_range=(0.5, 1.5), keep_ratio=True, ), dict(type="RandomFlip", prob=0.5), dict(type="PackDetInputs"), ]这里的ratio_range=(0.5, 1.5)表示每次随机把图片缩放到原图的 0.5 到 1.5 倍。对信号灯来说,0.5 倍会大幅缩小灯珠,因此我不建议把下限放到 0.3。RandomFlip需要注意:如果数据里包含了“红灯在左、绿灯在右”的箭头灯,随机翻转会让灯组左右关系混乱,箭头灯数据集一般关掉翻转或只在单方向做。
对黄灯这种样本不足的类别,复制粘贴增强比过采样更有效。把黄灯灯珠从原图抠出,随机贴到没有灯的背景上,注意避开已有灯组区域,重新计算 bbox。灯的语义很小、边缘清晰,粘贴后不违和,这是信号灯任务里少有的、可以直接放心用的 copy-paste 场景。
5. 信号灯 COCO 数据集的避坑清单:五个真实现象与排查方法
下面这五条是实际项目中翻车频率最高的,排序也基本是按出现次数来的。
5.1 红绿都准,黄灯永远召不回
现象:验证集上红灯 mAP 0.85,绿灯 0.82,黄灯只有 0.1。 原因:黄灯样本太少,且黄灯在远距离和红灯亮度接近;训练时 loss 被红灯主导。 解决:先看第 2 章的类别统计,若黄灯占比低于 5%,先做复制粘贴增强把黄灯数量拉到红灯的一半;再把黄灯类别的分类 loss 权重调高,在loss_cls的class_weight里给黄灯更高权重;如果权重拉满仍无效,说明原始标注里黄灯本身质量差,需要回看标注框。
5.2 验证 mAP 高,实车或路口视频却疯狂漏检
现象:COCO eval 的 bbox_mAP 超过 0.7,但把一段路口视频逐帧喂进去,远距离灯一个都检不出来。 原因:COCO eval 的结果是高分辨率图片上的指标,视频帧往往被播放器或采集卡缩放过,灯进一步变小;另外视频帧的过曝和运动模糊在训练集里比例不足。 解决:用原图分辨率做测试,检查img_scale是否小于原图;评测时单独打印mAP_small,如果它只有 0.2,问题主要在小目标策略;同时在训练集里加入 2 倍降采样样本,让模型提前见过更小的灯。
5.3 COCO eval 全为 0:类别 ID 从 1 开始还是从 0 开始
现象:训练 loss 正常下降,每个 epoch 都正常保存权重,但验证输出里 bbox_mAP 一直是 0。 原因:数据集的 categories ID 可能是0,1,2,而框架默认类别从 1 开始;或者验证集 categories 与ann_file不匹配,导致 gt 全部匹配失败。 解决:先打印model.dataset_meta或coco.cats看实际读到的 ID;再把metainfo的类别顺序与 JSON 里categories的 ID 对齐。一个稳定做法是把categories写成[{"id": 1, "name": "red"}, {"id": 2, "name": "yellow"}, {"id": 3, "name": "green"}],训练配置里classes顺序保持一致,不要依赖名称去猜。
5.4 箭头灯和圆盘灯混标,绿灯类别内部打架
现象:绿灯 mAP 不低,但输出里箭头右转灯和圆盘灯的置信度经常抖动,时高时低。 原因:标注时没有区分灯组类型,同一类别里既有圆盘灯、箭头灯,还有倒计时数字区域,模型不知道“到底该学哪种”。 解决:如果业务只需要红黄绿三种颜色,先按灯组拆分标注文件,把箭头灯单独留作困难样本;如果下游业务需要方向,就把 categories 扩成red_left、red_straight、green_left、green_straight再训练。这一步必须在标注阶段决定,后面改类别代价很高。
5.5 过曝把黄灯拍成白色,颜色特征失效
现象:白天夜间都正常,傍晚逆光和雨夜,黄灯被识别成红灯或直接漏掉。 原因:标注框内平均亮度极高,RGB 三通道都接近 255,黄色特征完全丢失。 解决:在增强里加入亮度偏移和模拟过曝,把正常黄灯图的部分区域拉成白色,再让模型同时学习“灯珠位置”和“颜色类别”。如果数据集本身包含过曝样本,不要删,把它们单独归到验证集,用来观测真实场景下的鲁棒性。
6. 进阶:用红黄绿互斥规则做后处理,顺便给数据集补上验证基线
拿到一份“红黄绿 + COCO”的数据集,除了训练,还有一个常被忽略的用法:用它验证路口灯组的互斥性质。正常交通信号灯组在同一时刻只会亮一种颜色,红、黄、绿三者互相排斥。如果模型同时输出“红灯 0.6”和“绿灯 0.5”,大概率是模型问题,也可能标注里混了黄灯闪烁的中间帧。把互斥约束加进后处理,能直接砍掉一部分明显错误的输出,这个方向也和你后续要做信号灯控制算法(比如强化学习做配时)天然衔接,因为感知输出必须符合灯组逻辑。
一个简单的做法是:把检测框按位置聚类成灯组,每个灯组内部选最高置信度的颜色作为最终结果,再用逻辑判断过滤掉“同一灯组同时出现红绿”的输出。
def same_group(box_a, box_b, dist_thr=80): ax, ay, aw, ah = box_a bx, by, bw, bh = box_b # 两个框中心距离足够近,视为同一灯组 cx = (ax + aw / 2) - (bx + bw / 2) cy = (ay + ah / 2) - (by + bh / 2) return abs(cx) < dist_thr and abs(cy) < dist_thr def filter_conflict(dets): dets = sorted(dets, key=lambda d: d["score"], reverse=True) keep = [] for det in dets: conflict = False for k in keep: if same_group(det["bbox"], k["bbox"]) and det["category_id"] != k["category_id"]: conflict = True break if not conflict: keep.append(det) return keep这段代码按置信度从高到低保留检测框,如果新框和已保留框距离很近且颜色不同,就认为是一次红绿互斥冲突,直接丢弃。dist_thr=80是经验值,需要根据图片里灯组的像素间距调整;它并不完美,但足以在部署阶段把红色和绿色同时出现的明显误检压下去。跑完验证集后,分别记录加规则前后的 mAP,只要提升超过 1 个点,规则就值得固化成部署代码。
往里再走一步,你可以用这份 COCO 标注做坏灯筛查:如果模型的绿灯高置信度输出持续一整段视频,而同灯组的红灯从不输出,很可能是灯组损坏,也可能是标注本身就是错的。这类应用和自动驾驶感知是反过来的,它需要的是稳定输出,而不是追逐极限 mAP。做到这一步,一份数据集的边际价值就远远不止训练模型了。我自己每次拿到新数据集,都会先跑一遍体检脚本再训练,这个习惯已经帮我少走了很多弯路,希望帮到你。
本文还有配套的精品资源,点击获取