简介面向智能车、自动驾驶及计算机视觉学习场景的红绿灯图像分类数据集提供red与green两类信号灯图片按训练集、验证集分目录保存每类样本独立存放训练集共16000张、验证集共4000张可直接用ImageFolder读取也可作为yolov5分类数据集使用无需额外清洗与标注。数据包为zip压缩包大小约360MB共2000个文件其中1998个jpeg图片构成主体另有1个json类别字典记录两类标签映射1个可视化py脚本可随机选取4张图片自动展示并保存方便快速核查数据质量。目前已有335人学习适合需要快速获得标准交通信号灯分类数据的项目可用于模型训练、算法验证、竞赛准备等场景省去采集整理时间。整个目录结构清晰json字典与图片类别严格对应便于二次扩展和迁移到其他分类框架尤其适合作为yolov5分类任务的直接输入。1. 图像分类数据集这张牌在智能赛道上比模型架构更决定生死做智能赛车道的红绿灯识别时新手往往一上来就调模型、换骨干网络折腾两周后loss还是下不去。我接手过好几个类似的模拟项目最后定位到的根因几乎都一样不是模型不行而是图像分类数据集本身没立住。这个标题里的核心词是“智能赛车道 红绿灯图像分类 训练集/验证集”它要解决的不是“怎么把ResNet调得更深”而是“如何造一份类别干净、划分可靠、能扛住真实赛道光影干扰的数据集”。一句话说清楚它的价值有了结构良好的训练集和验证集哪怕你只用最普通的图像分类模型也能在赛道上稳定输出红黄绿三色判断反过来数据集里混入了转灯中间帧、过曝灯盘、错误标注再好的模型也会在关键时刻翻车。这篇笔记按我自己的落地习惯来写先说数据集怎么采集与清洗再讲训练集和验证集怎么划分才不泄露接着把最常见的坑一条条排掉最后给出可直接抄走的PyTorch训练脚本和验证技巧。2. 采集与清洗智能赛道红绿灯图像分类数据集的第一步不是打标签而是定类别边界2.1 类别体系怎么定三分类必须扩成四分类很多人在一开始就把任务定为红灯、绿灯、黄灯三类这是第一个大坑。真实赛道场景里相机视野内经常出现“没有灯”的画面比如刚过弯道、灯在视野外、或者灯被杆件遮挡。如果你的图像分类数据集里全是带灯的图模型在推理时就会把任意背景强行判断成某一种灯色这在高实时性要求的智能车控制里非常危险。所以我的习惯是把类别体系定为四类red、green、yellow、none。其中none表示“画面中没有明确的红绿灯灯盘”它既可以是空背景也可以是遮挡后的背景。你可以用下面的表格来对齐标注口径类别名判定标准典型误标来源red 红灯灯盘点亮红色亮度明显超过灯盘边框红灯转绿灯瞬间的暗红残影被标成红色green 绿灯灯盘点亮绿色色相清晰可辨逆光下绿灯发白被标成noneyellow 黄灯灯盘点亮黄色/橙色红灯与绿灯在同一画面中过渡闪烁none 无灯画面中无完整可辨识灯盘灯盘占像素太少导致看不清边界定义一定要写进团队约定里否则不同人标出来的标签一致性会很差。对于智能赛车道这类目标明确的项目我建议由同一个人完成全量标注或者至少由两个人做交叉抽检把“不确定的图片单独挑出来人工复审”。2.2 数据来源怎么搭实拍、仿真渲染、公开红绿灯图像分类数据集三路并进常见做法是用三路数据来拼一路是赛道实拍用赛道同款摄像头在比赛场地的不同位置录制视频然后抽帧一路是仿真渲染用3D仿真环境摆不同角度、不同距离、不同光照下的红绿灯模型还有一路是公开的红绿灯图像数据但用之前必须看它的相机视角和场景是否与赛道接近。实拍抽帧时有一个关键参数是抽帧间隔我一般用每秒抽2到5帧而不是连续全部抽出来。理由很简单连续帧之间的画面高度相似会让训练集里出现大量“近重复”图片导致验证集指标虚高。你可以在抽帧时做一个简单的去重逻辑计算相邻帧的像素差差值低于阈值的丢弃。# 用 ffmpeg 按固定间隔抽帧并过滤掉变化过小的帧 ffmpeg -i input.mp4 -vf fps5,selectgt(scene,0.12) -vsync vfr frame_%04d.jpg这个命令里的参数含义fps5表示每秒最多保留5帧selectgt(scene,0.12)表示只有当前帧与上一帧的场景变化分数大于0.12时才保留数值越大保留的帧越少。针对智能赛车道这种灯盘和背景都比较固定的场景0.10到0.15之间是合理区间太大会漏掉亮度刚变化的灯色状态太小会留下大量重复帧。2.3 图像预处理统一尺寸、裁剪ROI、校正白平衡采集回来的原始图片分辨率往往从720p到4K不等而图像分类模型通常吃固定输入所以要先统一。对红绿灯分类来说最忌讳的是把整张大图上所有内容直接缩小到224x224因为灯盘在画面中占比很小缩小后细节完全丢失。我一般先用目标检测框或者手动标注框把灯盘的ROI区域标出来再按ROI中心落一个正方形裁剪最后resize到224x224或256x256。如果你使用的是公开红绿灯图像分类数据集要先用脚本检查它是否已经做了类似裁剪如果它给的是整图你需要自己在训练前加一个中心裁剪逻辑。# 用 PIL 按标注框做正方形 ROI 裁剪 from PIL import Image import os def crop_roi(image_path, box, output_size(224, 224)): img Image.open(image_path) x1, y1, x2, y2 box cx, cy (x1 x2) // 2, (y1 y2) // 2 side max(x2 - x1, y2 - y1) * 1.2 # 1.2 倍余量避免灯盘贴边 half int(side // 2) left max(0, cx - half) top max(0, cy - half) right min(img.width, cx half) bottom min(img.height, cy half) cropped img.crop((left, top, right, bottom)) return cropped.resize(output_size, Image.BILINEAR)这段代码里两个参数值得注意1.2的余量系数是按我的经验定的太小会让灯盘充满整张图导致模型过拟合到“满图都是灯”的分布太大会重新引入背景噪声Image.BILINEAR是缩放插值方式对灯盘这种小目标双线性比最近邻平滑得多能保留边缘过渡。清洗阶段还要做一次白平衡抽检。智能赛道的光照变化剧烈阴天、强逆光、人工补光灯都会让灯盘颜色偏移。我通常的做法是抽取每个类别各20张图人工查看色相分布如果发现某种灯色明显偏色就在训练时加入色调抖动增强而不是在预处理阶段强行校正因为赛道实拍的偏色本身就是测试环境的一部分。3. 训练集与验证集划分分层抽样是底线按场景分组才是关键3.1 为什么随机划分在赛道场景里不靠谱很多图像分类数据集的构建教程都会告诉你按7比3或8比2随机划分训练集和验证集。这个做法在通用图像分类上没问题但在智能赛道红绿灯场景里会带来严重的“数据泄露”。问题出在采集过程天然具备时间连续性。如果你先用视频抽帧采集了1000张图其中前500张是晴天上午拍的后500张是傍晚拍的随机划分很可能让同一段视频里的相邻帧同时出现在训练集和验证集里。验证集看起来准确率很高但一到正式赛道的实际场景就露馅因为模型真正学到的是“这个时间段、这个背景下的灯”而不是“红绿灯本质的视觉特征”。干净的划分方式是按采集批次或场景分组把同一个视频片段、同一个仿真场景、同一时间段的图片看作一个整体要么全部进训练集要么全部进验证集。这样的图像分类数据集才具有可靠的泛化性评估意义。# 按场景分组划分训练集和验证集 import os import shutil import random random.seed(42) data_root raw_images scene_groups {} for item in os.listdir(data_root): scene_id item.split(_)[0] # 文件名前缀代表场景ID scene_groups.setdefault(scene_id, []).append(item) train_items, val_items [], [] for scene_id, items in scene_groups.items(): random.shuffle(items) split_point int(len(items) * 0.8) train_items.extend(items[:split_point]) val_items.extend(items[split_point:])这段代码的核心设计是scene_id的提取方式。文件名前缀可以带时间、地点、采集批次例如scene01_frame_0001.jpg。按前缀分组后同一个场景的视频帧不会同时落到两个集合里验证集才有可信度。random.seed(42)保证每次重跑划分结果一致split_point则是每个场景单独按80%取训练集避免某些场景样本少却整组进入验证集。3.2 ImageFolder目录结构一张图一个文件搞定数据加载训练集和验证集划分完成后推荐把目录组织成PyTorch的ImageFolder格式这样不需要自己写复杂的Dataset类。目录结构如下注意类别名必须是英文小写避免中文路径带来的编码问题。dataset/ ├── train/ │ ├── red/ │ ├── green/ │ ├── yellow/ │ └── none/ └── val/ ├── red/ ├── green/ ├── yellow/ └── none/把图片落进这个目录结构后建议立刻做一个自动化校验脚本。我吃过这个亏某次数据迁移时移动了一部分图片目录里多了一个隐藏文件夹训练集的图片总数和标注记录对不上训练出来的模型在验证集上指标正常、实际识别却乱套。# 校验训练集和验证集的图片数量与扩展名 import os from collections import Counter root dataset for split in [train, val]: for class_name in [red, green, yellow, none]: class_dir os.path.join(root, split, class_name) if not os.path.isdir(class_dir): print(fwarning: {class_dir} 不存在) continue exts Counter( os.path.splitext(f)[1].lower() for f in os.listdir(class_dir) ) total sum(exts.values()) print(f{split}/{class_name}: {total} 张, 扩展名分布 {dict(exts)})这段脚本的价值在于把隐患在训练前暴露出来。如果发现某个类别只有几十张或者出现了非jpg后缀的图片就要回到上一步处理而不是等训练跑到一半才发现数据加载报错。对智能赛车道这种追求可靠性的应用数据清洗的自动化脚本比模型代码更值得投入。3.3 类别分布要检查平衡是默认值不平衡要主动处理划分完训练集和验证集后还要统计每个类别的图片数量。理想情况下红灯、绿灯的样本量应该接近黄灯样本可以适当少一些none样本不应超过总数的40%否则模型会把太多注意力花在背景下判断上。如果发现某个类别严重不足正规做法是采集更多原始数据而不是用复制粘贴的方式把现有图片多拷几份。复制带来的过拟合会让验证集准确率看起来更高实际效果却更差。如果实在无法补充数据可以用HSV空间的色调、饱和度扰动做增强比如把红灯的色相在±8度内轻微偏移模拟不同灯珠亮度下的红色变化。4. 智能车赛道红绿灯图像分类数据集避坑五个最常踩的坑和对应处理方案4.1 验证集准确率高、赛道实测翻车现象训练集和验证集上准确率都能到98%以上一到真正的赛道环境就频繁误判尤其在不同背景、不同光照下表现极不稳定。原因这是典型的数据泄露。随机划分让同一个视频片段里的相邻帧同时进了训练集和验证集模型在验证集上看到的“新图片”其实和训练图片高度相似本质上是记住了背景而不是灯色。解决回到第3章按场景分组重新划分。判断标准很简单验证集中的任意一张图如果在训练集中能找到采样时间间隔小于1秒的图片就说明划分不够干净。对没有视频元数据的公开红绿灯图像分类数据集则按拍摄地点或文件夹作为分组依据。4.2 黄灯死活学不好预测结果经常和红灯混淆现象黄灯类别的召回率明显低于红灯和绿灯推理时偶尔把黄灯判成红灯。原因黄灯样本数量太少。赛道场景中黄灯持续时间短抽帧能抓到的画面天然少于红灯和绿灯。另外黄灯与红灯在暗光环境下色相接近标注一致性也容易出问题。解决先统计类别数量确认不均衡程度再做两步处理。第一步在训练时按类别反比设置采样权重第二步针对黄灯单独做颜色抖动增强模拟不同亮度下的黄橙色变化。更激进的做法是把“红灯转绿灯”过程中的暗红残影帧人工剔除避免给模型提供错误中间态。4.3 灯盘在整图里太小resize之后变成一团糊现象模型训练正常收敛但预测时对远距离灯盘的判断非常随机经常把距离稍远的灯判成none。原因原始图像被直接resize到224x224而灯盘在720p画面里可能只占几十个像素缩到224后灯的区域只剩2到3像素人眼都认不出来模型自然更认不出来。解决按第2章的做法先做ROI裁剪再resize。如果只想快速验证效果可以在Dataset里临时加一个CenterCrop先裁掉四周背景再缩放。注意不要裁得太狠把灯盘裁掉一半反而更糟裁剪框中心应该对应灯盘中心。4.4 验证集里有“灯”却被标成了none现象训练完成后随机抽查验证集里的none类别图片发现里面有一小部分其实是清晰可见的红绿灯。原因标注阶段图快把模糊帧、远距离小目标全部丢进none导致none类别内部语义混乱。模型学到了“小灯没灯”推理时对真实远处灯盘的判断也会被带偏。解决标注规范里明确规定“能看清灯色就标对应颜色看不清灯色才标none”。对历史标注数据做二次复核把none里混入的清晰灯盘图捞出来重新归类。这个工作量很大但比起训练后排查语义错乱更值得提前投入。4.5 白天效果好、傍晚和强逆光下失效现象验证集里白天图占多数模型对阴天、傍晚、强逆光场景的适应性很差黄灯和红灯在低照度下分不清。原因采集时段集中在白天训练集的光照分布太窄。赛道实际比赛时间可能横跨上午到下午甚至室内外光线切换。解决采集阶段刻意覆盖多个光照条件训练阶段使用更强的颜色增强例如随机调整亮度±40%、饱和度±30%并小概率叠加灰度化手段促使模型不要过度依赖颜色绝对值。我一般用torchvision.transforms.ColorJitter实现但要注意增强强度不要大到自己都认不出灯色。5. 用PyTorch在本地跑通最小训练方案从图像分类数据集到可用模型5.1 为什么选ResNet18而不是更大更花哨的模型智能赛车道的推理设备通常是车载的低功耗计算板卡算力有限。ResNet18只有约1100万参数在224x224输入下的单帧推理延迟低而且TorchVision官方提供了在ImageNet上的预训练权重用预训练权重做迁移学习即使你的红绿灯图像分类数据集只有两三千张也能训出可用模型。这不是说更深的模型不能用而是从务实角度出发ResNet18是“最快能验证数据集质量”的选择。如果数据量足够大、又有较多的算力后续可以平滑切换到ResNet34或MobileNetV3做对比实验但里程碑验证阶段我一般固定在ResNet18上。5.2 最小可复现训练脚本训练、验证、保存最优权重一次写完下面这个脚本可以直接复制到项目里跑通注释已经标出关键参数的含义。import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets, transforms, models # 数据增强与归一化 train_transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.8, 1.0)), transforms.ColorJitter(brightness0.3, contrast0.3, saturation0.3, hue0.05), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) val_transform transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) train_dataset datasets.ImageFolder(dataset/train, transformtrain_transform) val_dataset datasets.ImageFolder(dataset/val, transformval_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4) model models.resnet18(weightsmodels.ResNet18_Weights.DEFAULT) model.fc nn.Linear(model.fc.in_features, 4) # 4 对应 red/green/yellow/none criterion nn.CrossEntropyLoss(label_smoothing0.05) optimizer torch.optim.AdamW(model.parameters(), lr3e-4, weight_decay1e-4) best_acc 0.0 for epoch in range(30): model.train() total_loss, correct, total 0, 0, 0 for images, labels in train_loader: optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() * images.size(0) _, preds torch.max(outputs, 1) correct (preds labels).sum().item() total labels.size(0) train_acc correct / total model.eval() val_correct, val_total 0, 0 with torch.no_grad(): for images, labels in val_loader: outputs model(images) _, preds torch.max(outputs, 1) val_correct (preds labels).sum().item() val_total labels.size(0) val_acc val_correct / val_total print(fepoch {epoch1}: train_acc{train_acc:.4f}, val_acc{val_acc:.4f}) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_traffic_light.pt) print(fbest val_acc: {best_acc:.4f})几个参数值得说明batch_size32是低速推理板卡上的常见选择显存不足时降到16lr3e-4是AdamW在迁移学习里的安全起点太大会让预训练权重被冲坏太小则收敛慢label_smoothing0.05为了缓解标注噪声带来的过度自信这对红绿灯图像分类数据集中少量误标样本很有用scale(0.8, 1.0)的随机裁剪用来模拟灯盘在画面中大小不一的情况。5.3 训练完别急着部署先看混淆矩阵再看可分性训练好模型后仅看准确率是不够的。你要在验证集上画出混淆矩阵重点看yellow被错分到red的比例。这个比例超过5%说明黄灯的类别可分性不足问题大概率出在数据集采集或标注上而不是模型。这时候回看第4章的处理方案优先去查黄灯的样本质量和数量而不是盲目换更大的模型。混淆矩阵的绘制代码很短核心是用sklearn.metrics.confusion_matrix统计真实标签和预测标签的对应关系再把结果打印成表格。如果你的验证集里某个类别的准确率明显低于平均值就应该把该类别的误判图片整理出来人工确认是标注错了还是灯色本身太接近。6. 验证集使用的最后一公里时序投票与难例复盘在真实的智能赛车道项目中模型推理不是孤立的单帧判断而是一条连续的决策流。基于图像的瞬时分类结果直接控制车速和转向会在灯色切换边界处产生抖动。常见的做法是加一个长度为5到7帧的时序投票器取最近N帧预测结果的众数作为最终输出。这个技巧能把单帧偶发误判过滤掉代价是引入100到200毫秒的决策延迟在低速小车场景下完全可以接受。时序投票还有一个隐形好处它能让红灯转绿灯的瞬间决策更平滑。黄灯出现在红灯和绿灯之间模型在单帧上已经能把大部分边界帧判成yellow叠加上一轮投票后不会出现红灯绿灯来回跳的情况。实现时只需要一个固定长度的环形缓存代码量很小但对赛道成绩的提升非常直接。验证集在此时还有一个用法难例复盘。每次做完一轮实验我会把验证集里所有预测错误的图片导出到一个文件夹里按“真实类别”排序然后逐张看原因记录是过曝、遮挡、还是相似色相干扰。这个习惯帮我发现过两次严重问题一次是某个批次的图片在采集时摄像头的自动白平衡出了偏差所有灯色都偏蓝另一次是none类别里出现了大量的绿色地垫反光模型学到了“绿色背景绿灯”。这两次问题靠loss曲线根本看不出来只有逐张过图才能发现。提前在图像分类数据集里多花一天做校验至少能省下后期一周的调参时间。我最初做红绿灯分类时拿到训练集和验证集就直接开训验证集准确率不错就以为万事大吉结果现场实测让整个项目返工。自那以后我的固定动作是先跑数据校验脚本再画类别分布图最后按场景分组划分训练只是最后一步验证而已。希望这些踩坑经验能帮你少走一段弯路也祝你的智能车在赛道上稳稳刹住、果断通行。本文还有配套的精品资源点击获取