ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

红绿灯识别数据集YOLO训练全流程与避坑指南

红绿灯识别数据集YOLO训练全流程与避坑指南 简介红绿灯识别数据集是面向YOLO等目标检测模型训练用的交通信号灯标注资料可帮助开发者省去自行采集和标注图像的环节适用于自动驾驶感知、智能交通监控等场景。压缩包共2000个文件约644.58MB其中1998个XML文件采用PASCAL VOC格式记录图像文件名、红绿灯类别红灯、绿灯、黄灯及边界框左上角与右下角坐标另有1个TXT文件汇总标签信息1个Python脚本用于查看自定义数据集的标签类别及数量。已有303人学习/下载适合具备一定YOLO使用经验、正在准备红绿灯识别训练数据的开发者。借助这套资源可直接整理成YOLO所需的标注格式快速开展模型训练配套脚本有助于核对类别分布避免数据不平衡为车路协同、信号灯状态识别等项目提供可落地的数据基础。1. 红绿灯识别数据集最容易“看着简单、跑起来翻车”的 YOLO 训练资源做自动驾驶感知的同行应该都清楚红绿灯识别数据集是 YOLO 训练里最容易“看着简单、跑起来翻车”的资源之一。类别只有红黄绿目标形态固定很多人以为标注几批图就能拿下高 mAP可真到验证时就露馅远处路口的灯在画面里只有二三十个像素夜间过曝成一团光晕逆光时整个灯牌白成一片无论城市路口还是高速公路收费站实际情况都比想象复杂得多。这份红绿灯识别数据集正是冲着这些难样本去的图片按 YOLO txt 格式标注坐标解压后可以直接划分目录、配置 data.yaml 喂给 YOLO 系列训练。适合做辅助驾驶感知、交通信号监控以及信号灯识别的课程设计或毕设任务的开发者。2. 数据集结构拆解下载后第一件事不是训练是检查标签2.1 目录组织images 与 labels 的对应关系先看目录结构。绝大多数标注过的目标检测数据集解压出来都是这样一个基本形态dataset/ ├── images/ │ ├── train/ # 训练图片jpg 或 png │ ├── val/ # 验证图片 │ └── test/ # 测试图片部分数据集没有这一项 ├── labels/ │ ├── train/ # 与 images/train 同名的 txt 标签 │ ├── val/ │ └── test/ ├── classes.txt # 类别列表一行一个类别名 └── data.yaml # YOLO 用的数据集配置部分数据集自带images 和 labels 严格同名一张 a.jpg 对应 labels 里一个 a.txt。YOLO 训练按文件名前缀匹配图片与标签如果 txt 和 jpg 对不上训练日志会刷出大量 “WARNING: no labels found in xxx.jpg”。这时候基本可以断定目录配对出了问题先别急着改网络结构。拿到这份数据集后我的习惯是先做一轮“空标签检查”分别统计 labels 下每个目录里的 txt 数量再与 images 下的图片数做差。这个差经常不为零原因是标注者跳过了某些“灯不清晰、无法判断颜色”的图这些图留在 images 里但没有标签。YOLO 遇到无标签图片不会报错但会把整张图当纯背景学如果这类图占比超过 10%模型的召回率会明显被拖低。常见做法是写个十几行的小脚本把没有对应标签的图片单独挪到 ignore/ 目录不让它参与训练循环。有一点容易忽略如果解压后找不到 classes.txt可以打开任意一个 txt 标签看第一列数字的最大值再对照 README 或图片内容判断类别数。数字最大是 2就是三类0、1、2最大是 3就是四类。这个数字直接决定后面 data.yaml 里 nc 怎么填填错了训练不报错但类别名会整体错位。2.2 标签格式解析YOLO txt 里那五个数字YOLO 的 txt 标签每行代表一个目标固定五个值空格分隔0 0.566666 0.488888 0.018518 0.029629对应关系是class_id、x_center、y_center、width、height。后面四个全部除以图片宽高做了归一化取值范围理论上在 0 到 1 之间。这行数据的意思是类别 0假设是 red目标中心在图片横向 56.6%、纵向 48.9% 的位置宽占 1.85%、高占 2.96%。这里有个容易翻车的细节前两个值是中心点坐标不是左上角坐标。如果你自己写可视化脚本画框时需要先算出左上角x1 x_center - width/2、y1 y_center - height/2直接用中心点去画框会整体偏移。别问我怎么知道的我第一次画标签时所有红绿灯的框都骑在灯杆上。用可视化工具把标注画回图上你会发现框比想象小得多——红绿灯在正常行车记录仪画面里就是小目标一个框可能只有三四十个像素。这为后面的训练埋下重要伏笔检测头下采样倍数一高小目标的特征就丢得差不多了。下载后我还会跑一遍标签合法性检查脚本。# check_labels.py from pathlib import Path def check(label_dir, img_dir): labels sorted(Path(label_dir).glob(*.txt)) imgs {p.stem for p in Path(img_dir).glob(*.[jj][pp][gg])} bad [] for lb in labels: if lb.stem not in imgs: bad.append((lb.name, 无对应图片)) continue for line in lb.read_text().strip().splitlines(): parts line.split() if len(parts) ! 5: bad.append((lb.name, 字段数不为5, line)) continue cls int(parts[0]) vals [float(v) for v in parts[1:]] if cls 0: bad.append((lb.name, 类别id为负, line)) if any(v 0 or v 1 for v in vals): bad.append((lb.name, 坐标超界, line)) print(f共检查 {len(labels)} 个标签异常 {len(bad)} 个) for item in bad[:10]: print(item) check(labels/train, images/train)这个脚本做三件事先查图片与标签是否同名对应再查字段数是否为 5最后确认类别 id 和四个坐标值都在合法区间。跑完如果出现“坐标超界”的报错先修复这批数据再训练。我在实际项目里遇到过标签中混入 width1.8 的情况单个异常框会让训练初期的 loss 曲线一直压不下去而且排查半天很难想到问题出在数据上。2.3 类别分布统计先防类别不平衡红绿灯任务常见的类别定义有三种粒度按颜色分red/green/yellow、按颜色加方向分red_left/green_straight 等、按灯组整体框分。打开 classes.txt 看一眼就知道这份数据集用的是哪种粒度。如果只有三类说明按颜色分够用如果有七八类说明按“颜色方向”分功能更细但每类样本量被摊薄训练难度更大。不管哪种粒度训练前都要统计每个类别的目标数量而不是只看图片总数。# count_classes.py from collections import Counter from pathlib import Path def count(label_dir): counter Counter() for lb in Path(label_dir).glob(*.txt): for line in lb.read_text().strip().splitlines(): parts line.split() counter[parts[0]] 1 for cls, num in counter.most_common(): print(f类别 {cls}: {num} 个目标) return counter count(labels/train) count(labels/val)统计结果里最该警惕两种分布。一是 red/green/yellow 数量差距超过 3 倍比如 red 有 8000 个、green 只有 2000 个模型会天然偏向红灯绿灯召回率大概率不及格对策是训练时给少量类别更高的 loss 权重或对少量类别做过采样。二是 val 的类别分布与 train 差异过大这会导致 mAP 忽高忽低你改一个参数后性能看似提升实际只是验证集挑了几张好认的图。3. 把数据集跑进 YOLO目录划分、data.yaml 与训练参数3.1 目录划分图片和标签必须同步移动用这份数据集跑 YOLO 训练第一步是把目录整理成框架认识的样子。有些下载包已经预置了 images/train、labels/val 这类标准结构那可以直接跳过这一节。但更多时候压缩包只有平铺的 images 目录和 labels 目录需要自己划分。我的习惯是按 8:1:1 切训练集 80%验证集 10%测试集 10%。划分时必须保证图片和标签成对移动。一个经典翻车操作复制了图片但忘了对应的 txtYOLO 在训练时会把缺标签的图当纯背景处理。用一段小脚本可以避免这个坑# split_dataset.sh python - EOF from pathlib import Path import random, shutil random.seed(42) src_img Path(images) src_lbl Path(labels) out Path(yolo_dataset) for split in [train, val, test]: for sub in [images, labels]: (out / split / sub).mkdir(parentsTrue, exist_okTrue) files list(src_img.glob(*.jpg)) random.shuffle(files) n_train int(len(files) * 0.8) n_val int(len(files) * 0.9) for i, img in enumerate(files): split train if i n_train else val if i n_val else test lbl src_lbl / (img.stem .txt) if not lbl.exists(): continue shutil.copy(img, out / split / images / img.name) shutil.copy(lbl, out / split / labels / lbl.name) if i % 500 0: print(f{split}: {img.name}) EOF脚本用random.seed(42)固定划分顺序保证多次运行结果一致方便复现。比例按 8:1:1 是目标检测的常用起点如果数据集总量不大可以把 val 降到 10% 以内给训练多留样本。lbl.exists()判断用于跳过无标签图片它不会报错但你需要数一下最终落进三个 split 的图片总数确认和原始数据量差异不大。落盘后的目录结构是yolo_dataset/ ├── train/ │ ├── images/ │ └── labels/ ├── val/ │ ├── images/ │ └── labels/ └── test/ ├── images/ └── labels/3.2 data.yaml 与训练命令参数怎么设定目录整理好写一个 data.yaml 指向它。YOLOv5、YOLOv8 都能用同一套结构。yaml 里至少给出训练集路径、验证集路径和类别信息。类别列表务必打开 classes.txt 确认后再填别想当然写 red/green/yellow——实际可能是 red/green/yellow/off 四类写错了训练不会报错但类别名会错位可视化时特别别扭。# data.yaml path: /home/me/yolo_dataset train: train/images val: val/images test: test/images nc: 3 names: [red, green, yellow]训练命令用 YOLOv8 的入口格式YOLOv5 用户换成 train.py 的对应参数写法即可yolo train datadata.yaml \ modelyolov8s.pt \ imgsz640 \ batch16 \ epochs100 \ patience20 \ projectruns/ \ nametraffic_light这里每个参数都有讲究列个表方便照抄参数推荐值说明modelyolov8s.pt红绿灯是小目标n 模型 backbone 太浅特征提取能力不够s 是性价比折中显存 12G 以上可以上 mimgsz640 起步显存够用直接 960红绿灯目标太小960 比 640 在小目标 mAP 上能提 3 到 5 个点batch8G 显存配 1616G 配 32红绿灯样本量小不必强行大 batchepochs100收敛快一般五六十轮就停了patience20连续 20 轮 val mAP 不涨就提前停省时间freeze10数据量小于 5000 个目标时冻结前 10 层效果更好见第 5 章训练过程中重点盯两个输出一是 train/loss 曲线是否在初期剧烈振荡二是每轮结束打印的 mAP50 与 mAP50-95。如果 mAP50 能上 90这份数据集的标注质量基本合格。3.3 val 与 test 的差异别用同一批图自欺欺人很多红绿灯数据集只有 train 和 val 两个目录val 里的样本还常常与 train 来自同一段行车记录视频的不同帧时间间隔只有一两秒。这会导致一个假象模型在 val 上 mAP 很高一到真实的新路口就露馅因为 val 里那些帧的画面背景、光照、角度与 train 太像了模型“背”下来的是背景而不是学会判断灯的颜色。我的处理方式是如果数据集没有 test 目录就从 train 里按时间间隔挑一批出来单独拼成 test 集间隔至少 10 秒以上最好来自不同路段。具体到这份数据集可以按图片文件名里的时间戳或序号来筛比如每 30 帧抽 1 帧。宁可 test 集张数少一点也要保证它与 train 的重复场景足够少这样最后出来的评估结果才有参考价值而不是自欺欺人的数字。提示划分时注意保持目录结构完整test 也要有配套的 images 和 labels否则评估脚本会报路径错误。4. 红绿灯训练避坑指南四个高频问题的现象、原因与解决4.1 背景误检刹车灯和红色广告牌被当成了红灯现象模型在验证视频里频繁框出前方车辆的刹车灯和路边红色招牌置信度还不低有时候超过 0.7。原因训练样本里红灯类别混入了太多“红色发光物”。标注时如果标准不严格画面里所有红色圆形物体都被标成 red模型学到的其实是“颜色发光”特征而不是“位于路口、高度固定、灯组轮廓”这些结构特征。解决第一步检查 labels 里 red 类的标注框把框画回图上看有没有明显标错的第二步给训练集补充一批“无红绿灯的路口负样本”这些图片不需要任何标注框放进 images 但 labels 留空YOLO 会把它当纯背景参与训练。我一般会从原始数据里把不含 red 标签的图片挑出来单独建一个 negative/ 目录再并入 data.yaml 的 train 路径让模型看到更多“有红灯但不是红绿灯”的场景。4.2 夜间过曝、光晕让灯“变大”导致漏检现象白天 mAP 正常一到夜间视频红灯变成了比实际大好几倍的光晕模型反而框不出来或者只框出光晕的一部分。原因标注框按灯芯实际大小画但夜间灯芯加上光学衍射画面里呈现的尺寸远大于灯芯本身。训练时模型看到的是“小框配大光斑”特征对不上推理时它想找小框可小框的边界在光晕中根本不存在。解决对夜间图片重新标注“光晕灯芯”的整体区域框可以比白天大 30% 到 50%让模型学到“亮斑整体就是灯”。训练时把 imgsz 从 640 提到 960给网络更多像素去分辨光晕边界。数据增强方面HSV 的饱和度随机变化对夜间帮助不大反而要加一点高斯模糊增强模拟雨夜对焦不准的情况。4.3 小目标漏检下采样倍数把灯“吃”掉了现象远距离路口的红绿灯完全不检测近距离正常。检查输入尺寸后发现一个 20x20 像素的灯经过下采样在检测头倒数第二层特征图只占不到 1 个像素。原因YOLOv8 的 head 有 P3/P4/P5 三层输出小目标主要靠 P3 层8 倍下采样。原始行车记录仪分辨率 1920x1080 缩放到 640x640 后一个原本 50x50 的灯芯只剩 16x16 像素落到 P3 上只剩 2x2模型能从这么少的像素里提取的信息极其有限。解决直接把训练分辨率拉到 960x960P3 上的目标尺寸立刻翻一倍。如果显存不够第二个选择是开启多尺度训练让模型适应不同输入尺寸第三个选择是换用带 P2 层的检测头把更高分辨率的浅层特征并入 neck。前两个方案更实际改结构会引入额外参数量和训练时间。4.4 标签越界、归一化坐标大于 1现象运行 check_labels.py 时发现 width 或 height 大于 1或者坐标出现负值训练过程 loss 忽高忽低val mAP 在 0.3 附近徘徊。原因标注工具导出 YOLO 格式时坐标转换出错最常见的是标注工具用像素坐标导出脚本忘了除以图片宽高另一种是图片做过裁剪裁剪后标签没同步更新导致框指向了图片外。解决写一个清洗脚本对异常框做“钳制”而不是直接删保留大部分有效信息# clean_labels.py from pathlib import Path def clamp(label_dir): for lb in Path(label_dir).glob(*.txt): lines [] for line in lb.read_text().strip().splitlines(): parts line.split() cls parts[0] x, y, w, h [float(v) for v in parts[1:]] # 中心点钳制到 [0,1] x max(0.0, min(1.0, x)) y max(0.0, min(1.0, y)) # 宽高钳制到 [0,2]保留比例合法性 w max(0.0, min(2.0, w)) h max(0.0, min(2.0, h)) lines.append(f{cls} {x:.6f} {y:.6f} {w:.6f} {h:.6f}) lb.write_text(\n.join(lines) \n) clamp(labels/train) clamp(labels/val)钳制原理坐标超界通常只是个别值算错把中心点压回图片范围、宽高封顶到 2 倍图片能保住大部分有效标注。钳制完再跑一次 check_labels.py确认异常归零再训练。注意钳制只适用于“轻微越界”如果中心点跑到 5.7 这种离谱位置说明标签生成的源头有问题得回到标注数据重新导出别依赖钳制来兜底。5. 进阶调优迁移学习、针对性增强与混淆矩阵分析5.1 迁移学习用 COCO 预训练权重还是从头训红绿灯数据集即便有上万张图片相对场景的多样性也还是不够随机初始化训练很难收敛到高精度。我的常规做法是加载 yolov8s.pt 这种预训练权重把它当作对图像纹理和物体边界的“预认知”再在红绿灯数据上微调。原因很简单预训练权重虽然没专门学过红绿灯细节但已经学会了区分“发光物”和“普通物体”的通用特征这些特征迁移到红绿灯任务上很值钱。微调时有个相对省心的经验前 10 轮把 backbone 参数冻结只训练 head等 loss 降到接近收敛再解冻全部层用小学习率微调。YOLOv8 用 freeze 参数控制yolo train datadata.yaml \ modelyolov8s.pt \ imgsz960 \ freeze10 \ epochs100 \ lr00.005freeze10 表示冻结前 10 层参数配合 lr00.005 的小学习率能规避数据集小、随机初始化导致的大梯度破坏预训练特征的问题。如果数据集够大一万张以上也可以不冻结直接训两三百轮下来效果不会差太多。5.2 数据增强针对红绿灯的雨雾、逆光与模糊YOLO 内置的增强已经覆盖了翻转、旋转、HSV 抖动、mosaic。对红绿灯这种户外固定目标有两项增强比其他任务更重要亮度对比度随机化和模糊。原因很直接红绿灯最难识别的场景就是逆光整体画面偏灰白和雨夜光晕加运动模糊。内置 HSV 增强能调饱和度和明度但力度不太够。我一般会额外做一次离线增强生成一批“模拟恶劣天气”的样本混进训练集。用 albumentations 可以直接实现# augment_dark.py import albumentations as A import cv2 transform A.Compose([ A.RandomBrightnessContrast( brightness_limit(-0.4, 0.3), contrast_limit(-0.3, 0.3), p0.7 ), A.GaussianBlur(blur_limit(3, 7), p0.4), A.RandomFog(fog_coef_lambda(0.1, 0.5), p0.3), ]) image cv2.imread(some_frame.jpg) aug transform(imageimage) cv2.imwrite(aug_dark.jpg, aug[image])brightness_limit 拉低是模拟黄昏和夜间低照度RandomFog 是模拟雨雾对光线的散射GaussianBlur 是模拟雨天运动模糊。这份增强只对训练集做验证集保持原始数据否则验证指标会失真。增强力度要控制亮度压得太低、连人都看不清时模型会去学不存在的特征mAP 反而掉了。我通常会抽几张增强后的图片肉眼确认红灯不能增强成黄色否则等于人为制造混淆样本。5.3 混淆矩阵红灯和绿灯互相打架怎么找原因训练结束后runs/train/exp/ 目录下会生成 confusion_matrix.png这是排查分类问题的第一份材料。红绿灯任务最怕看到的现象red 的真实样本被预测成 green或者反过来。出现这种情况先别急着改模型结构回头看样本本身。红绿混淆最常见的原因是颜色空间不一致标注时用的图片色温基准和训练时不一致同一盏红灯在不同图片里显示为橙色或粉色。解决方法是把数据增强的色调抖动幅度调小。YOLO 内置 HSV 增强里色调项默认约 0.015红绿混淆严重时把它降到 0.005防止数据增强把红灯“摇”成绿灯。如果混淆集中在 yellow 上那属于正常现象——黄灯与红灯、绿灯在色相上相邻边界本身模糊这时考虑合并类别或统一按灯组整体框标注。loss 曲线也要看但不能只看训练 loss。重点看 val 的 box_loss 和 cls_loss 在最后 20 轮是否分离如果 box_loss 还在降而 cls_loss 已平说明模型能框住灯的位置但在分类上遇到瓶颈反过来 box_loss 平、cls_loss 还在降说明定位到位问题出在分类粒度。两个 loss 的收敛状态指向完全不同的下一步调整方向这个区别值得花时间想清楚。6. 用连续视频验证模型mAP 高不等于能用单张图片的 mAP 是静态指标红绿灯识别与别的目标检测不太一样——下游逻辑要看同一盏灯在连续帧里的状态切换是否稳定。mAP 95 的模型可能在真实视频里出现红灯在 30 帧内跳成黄灯再变回红灯的情况这种抖动接入后续控制逻辑后果不用多说。我的验证方法是准备一段没参与训练的路口视频用训练好的权重跑推理观察每盏灯的类别是否连续稳定。命令很简单yolo predict modelruns/train/exp/weights/best.pt \ sourcetest_intersection.mp4 \ conf0.25 \ saveTrueconf 别拉到 0.5。红绿灯小目标在远距离时置信度普遍只有 0.3 到 0.4阈值设高了会大量丢帧看着稳定实际是漏检。0.25 是个相对平衡的点宁可多出几个候选框把稳定性判断留给后续处理。验证时盯两类现象。一是同一盏灯在 10 帧内从 red 跳到 green检查这几帧的图片多数情况是镜头逆光导致灯色失真回到第 5 章的增强策略去补样本。二是目标框位置在相邻帧间剧烈跳动框的中心点一帧在灯芯上、下一帧偏到灯柱上说明定位置信度不足把输入分辨率再拉高一档。更严谨一点可以写个小脚本对相邻帧检测框做 IOU 关联超过 0.5 视为同一目标然后统计该目标的类别序列看有没有反复横跳# check_stability.py # 对同一目标的连续帧检测结果统计类别变化次数 def check_stability(class_ids, max_jumps2): changes sum(1 for i in range(1, len(class_ids)) if class_ids[i] ! class_ids[i - 1]) return changes max_jumps这个方法把“看起来稳定”变成“可量化的稳定”。我自己的习惯是任何红绿灯模型交付前都强制跑一遍“三连验证”静态测试集 mAP 一次、夜间视频一次、逆光路口实拍一次。没有经过连续帧验证的模型我不会拿去做任何控制相关的东西。那以后每轮训练完都重复这个流程看起来多花了二十分钟但省掉了大量“到了现场才发现模型不可用”的返工时间。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进