
简介这是一份基于YOLOv3与DarkNet-53的道路红绿灯检测识别综合项目实践报告面向深度学习初学者、计算机视觉学习者及智能驾驶相关研究人员。报告围绕小目标检测难点展开完整呈现了从实验调研、环境配置、开源交通标志数据集预处理到模型训练、参数调优、评估与应用的全过程并具体说明了输入416×416、3类目标、200个epochs等关键参数。内容还包含FPN多尺度融合策略分析、单目标与多目标红绿灯检测效果展示以及动态场景适用性讨论。压缩包仅1个doc文档大小438KB适合作为课程设计、综合实践项目或毕业设计的参考模板。目前已有648人学习下载对希望快速了解YOLOv3在交通场景落地流程的读者具有直接参考价值。1. 为什么“红路灯”检测总被抄作业却总在一落地就翻车“红路灯”这三个字其实是很多项目的报告里默认会写错的名字正确叫法是红绿灯、交通信号灯。基于yolov3的道路红绿灯检测识别实验报告在自动驾驶、辅助驾驶、车路协同项目里几乎是入门必做的一个方向。问题在于网上能找到的成熟源码和实验报告很多照着跑一遍也能出图、能画框、能报个mAP但一旦把模型放到真实的十字路口、阴雨天、夜间、逆光场景里漏检和误检会突然变多很多人这时候才开始怀疑数据集、怀疑Anchor、怀疑Darknet是不是有问题。这个方向真正值得做的事不是“用yolov3跑通一个demo”而是搞明白红绿灯检测和其他目标检测任务在数据设计上的本质差异目标小、状态多、颜色语义强、时序依赖高而yolov3作为一套老但清晰、可控性强、部署成本低的检测框架特别适合用来说明这些问题并落成一套可复现的流程。这篇笔记适合正在做课程实验、毕设或小型路侧项目的工程师目标是让读者能从一个只有标注图片和一台本地GPU的环境出发把数据整理、训练、调参、离线评估、实路验证整个链路做扎实顺带绕开那些最常坑人的隐性bug。2. yolov3信号灯检测的原理和选型理由为什么用Darknet-53这个骨架而不是直接上Transformer2.1 信号灯检测对网络结构的三个硬性要求先把需求搞清楚。红绿灯检测不是“在一张图上找到目标”这么简单它对网络有三个硬性要求第一小目标敏感度要高路口画面里一个灯通常只占整张图的几十个像素尤其是远距离的灯头比例可能小于0.5%第二对颜色信息要敏感红灯绿灯黄灯的区别几乎完全靠HSV空间的色相来区分如果特征的通道注意力机制太强很容易把颜色细节压掉第三推理速度要能跑在边缘设备上车端或路侧盒子的算力往往很有限不能一上来就上多阶段检测器。yolov3的骨架Darknet-53由一系列残差块组成在ImageNet上做过预训练。它没有像ResNet那样在最后靠全局平均池化直接输出分类结果而是保留了不同分辨率的特征层用于多尺度预测。这对信号灯这种“大背景下的小目标”特别重要高分辨率的浅层特征保留空间信息和边缘信息而深层的语义特征提供类别判断能力二者通过类似FPN的路径拼接在一起。相比之下yolov5的CSP结构确实更快但yolov3代码更透明、超参数更少在实验报告里更便于逐层分析和画特征图。需要注意的是红绿灯并不只是一个“有就是有”的存在。一个完整的信号灯集合包含灯头位置、灯色状态、箭头方向三个语义维度很多实验只拿“红绿黄”当三类做这实际上是丢掉了方向灯的维度。后面在数据标注部分会专门讨论这个问题这里先记住一个结论如果只按颜色分三类去做检测遇到左转箭头灯或黄闪灯时基本必炸。2.2 多尺度预测与Anchor设计的取舍yolov3在三个尺度上做预测分别对应输入尺寸的32倍、16倍和8倍下采样特征图。以416乘416的输入为例三个输出层的特征图分辨率是13乘13、26乘26、52乘52。小目标主要由52乘52那层负责因为每个网格对应的原图区域只有8乘8像素足够捕捉小灯头。但很多时候大家直接用COCO预训练的anchor这对红绿灯来说并不合理COCO里小目标占比高但形状比例和信号灯的矩形轮廓差异很大。我在实际项目中更常用K-means在训练集上重新聚类anchor。常见做法是对标注框做归一化然后用IoU作为距离度量做聚类。要注意的是yolov3的anchor是宽高比例不包含坐标信息。聚类得到9组anchor后按面积从小到大排序分配给三个尺度层小尺度层对应大目标大尺度层对应小目标。不要把这个顺序搞反否则训练会异常慢且mAP极低。# 用darknet自带的kmeans脚本重新聚类anchor python scripts/reorg_anchors.py -dataset cfg/traffic_light.data -num_clusters 9 -out_file cfg/anchors.txt这个脚本并不存在于darknet官方仓库常见做法是自己写一个基于numpy的kmeans聚类输入是标签文件里所有的宽高值。聚类后得到的9个anchor要手动填进yolov3的cfg文件中每个yolo层前的3个filters个数也对应要改filters 3 * (classes 5)如果类别数是4那filters就是27。这里最容易翻车的地方是改最后一个卷积层时把filters算错或者改完yolo层却忘了改它前面的conv层。2.3 为什么不建议直接上yolov5或RT-DETR做实验yolov5、yolov8乃至RT-DETR在小目标和精度上的表现确实优于v3但是它们不适合“做实验报告”这个场景。原因不在精度而在可解释性v3的每个输出层在做什么、loss怎么算、mAP怎么统计都很容易从代码里读出来而且训练所需的显存更小一张1080Ti甚至2060就能完成v5的autoanchor、EMA、mixup这些结构在消融实验里很难拆干净论文里的对比表格会很难写。如果目标是工程落地v3的推理权重也只有不到250MBTensorRT转换后FP16能压到120MB左右在Jetson Xavier NX上能跑到30ms以内。当然如果项目对检测率的要求大于对可解释性的要求直接上yolov5s是合理的选择。但本标题既然写的是yolov3那我们就以v3为主干把整个链路做严谨最后再给出一个结论v3在红绿灯场景的mAP大约能到60到75之间真正限制上限的往往不是网络而是数据里小目标的标注质量和类别定义。3. 数据是红绿灯检测的地基从COCO筛选到自建小目标数据集的完整流程3.1 先分清两类可用数据源开源数据集与自采数据红绿灯检测的公开数据集主要有三个来源COCO的traffic light类别、Bosch Small Traffic Lights Dataset简称BSTLD以及某国内自动驾驶公司的路测数据集。COCO里traffic light的标注有接近2000张图小目标占比高但标注框存在大量“连体灯”问题一个框把红黄绿三个灯头同时框住这作为检测任务还算可接受但作为分类任务就有麻烦。BSTLD是专门针对小目标信号灯的数据集在德国道路上采集图像分辨率1280乘720很多灯头小于20像素非常适合用来检验模型的小目标能力。最稳的数据路线是“公开集预训练自采数据微调”。常见做法是用脚本把COCO里的traffic light类单独筛出来转成YOLO格式再混合BSTLD一起做预训练最后加入自己在路口采集的数据做微调。自采数据不需要太多500到1000张覆盖不同距离、天气、时段的图片就已经能把模型拉回本地路口的分布区间。采集时最好用1080p以上的摄像头固定机位和移动机位各取一部分不要全从视频里抽帧否则相邻帧的相似度过高会造成训练集冗余。# 从COCO JSON里筛选traffic_light类并输出YOLO格式标签 import json, os from pathlib import Path coco_path instances_train2017.json out_dir Path(yolo_traffic_light) out_dir.mkdir(exist_okTrue) img_dir Path(train2017) with open(coco_path, r) as f: coco json.load(f) # 先找到traffic_light的category_id cat_id None for cat in coco[categories]: if cat[name] traffic light: cat_id cat[id] # 按image_id聚合annotations img_anns {} for ann in coco[annotations]: if ann[category_id] ! cat_id: continue img_anns.setdefault(ann[image_id], []).append(ann) # 写YOLO标签文件 for img_info in coco[images]: img_id img_info[id] if img_id not in img_anns: continue w, h img_info[width], img_info[height] txt [] for ann in img_anns[img_id]: x, y, bw, bh ann[bbox] # COCO的bbox是[x,y,w,h] cx (x bw / 2) / w cy (y bh / 2) / h nw bw / w nh bh / h txt.append(f0 {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}) label_file out_dir / (img_info[file_name].replace(.jpg, .txt)) label_file.write_text(\n.join(txt))这里要特别提醒一个经典问题COCO的bbox坐标是左上角加宽高而YOLO格式是中心点加宽高转换时中间的除法和归一化一步都不能错。另外筛选后图片文件的复制不要漏掉没有标注的图片最好是写一个脚本遍历images没有对应标签就跳过拷贝而不是只拷贝有标注的否则训练时数据路径会报错。写完转换脚本后要抽查图片和标签的对应关系。一个快速检查方法是写一个OpenCV脚本把标注框画回原图上用肉眼检查一组不同场景的图片确认框的边界没有明显偏移、没有整框标到灯杆上。3.2 类别体系设计按“灯色方向”组合定类而不是只定红绿黄这是实验报告里最容易被忽视但最影响上限的决策点。信号灯检测的类别设计有两种常见做法第一种是只分red、green、yellow三类把方向灯和圆盘灯归为一类第二种是分red_left、red_straight、green_left、green_straight、yellow等组合类。第二种对下游决策系统更友好因为下游系统关心的不仅是“有没有红灯”而是“哪个方向能走”。我一般会定义这样一组类别red_circle, green_circle, red_left, green_left, red_right, green_right, yellow, off一共8类。这里“off”代表熄灭状态很多城市夜间会关掉一部分方向的信号灯如果不加这一类模型会把熄灭的灯头强行归类成红或黄导致下游逻辑混乱。加了“off”类之后检测到的灯头但无法判断状态的情况也有了明确出口。类别设计的决策会直接影响训练样本数量。如果只有一个2000张的公开集硬分8类的结果就是每类只有一两百个实例小目标检测基本学不出来。所以务实策略是首先用COCO和BSTLD训练出一个只分红绿黄三类的基座模型然后另外收集方向灯图片做数据扩充最后用“冻结骨干只训练头部”的方式做一次针对方向灯的微调。这种方式既保证了基础检测率又把方向语义逐步注入。3.3 增强策略Mosaic、HSV扰动与几何变换的合理组合红绿灯检测的增强策略和通用目标检测不完全一样。通用的随机裁剪、缩放确实能提升泛化性但信号灯是强颜色依赖目标如果HSV扰动幅度太大红灯会被扰动成橙色或粉色模型反而学不到“红色”这个稳定的语义。所以颜色增强要保守H通道的扰动范围控制在正负5度以内S和V在正负20%左右且H的扰动不适合用在夜间图片上。Mosaic增强对红绿灯是有效的因为它把四张图拼在一起迫使模型学会在更小的目标尺度上检测这正是小目标场景需要的。同时随机缩放比例里多加几个小于0.5的档位进一步模拟远距离灯头效果。注意Mosaic增强在训练末期应该关闭否则模型持续看到拼接后的异常上下文在真实全图上反而可能掉点。几何增强里的随机翻转需要特别小心如果训练集里没有专门补充左右方向灯的对称样本盲目做水平翻转会让左转箭头灯和右转箭头灯的语义发生冲突。常见做法是把翻转增强关掉或者在数据集里手动扩充反转后的标注副本保证两个方向的箭头数量基本均衡。4. yolov3训练实操从零开始跑通红绿灯检测的最小命令集合4.1 准备文件结构与预训练权重训练前的目录结构必须保持简单清晰避免路径错误浪费一整天。常见做法是建立一个项目根目录下面放darknet源码、数据集文件夹和输出文件夹三块。darknet的编译依赖OpenCV和CUDA建议直接用官方Makefile编译而不是用cmake因为cmake分支在新版darknet里维护较弱。# 编译darknetCUDAOpenCV版本 cd darknet sed -i s/GPU0/GPU1/ Makefile sed -i s/CUDNN0/CUDNN1/ Makefile sed -i s/OPENCV0/OPENCV1/ Makefile make -j8编译成功后会生成一个darknet二进制文件。接着需要准备三个文件一个.data文件指明训练集、验证集的路径和类别数一个.names文件写类别名一个.cfg文件定义网络结构。很多实验报告会让人直接改cfg里的classes和filters但忽略掉.data和.names会导致训练时读不到标签类名。预训练权重建议从Darknet官网下载darknet53.conv.74这是基于ImageNet分类预训练的骨干权重不含检测头。使用预训练权重能显著减少训练时间尤其在小数据集上效果明显。下载后把它放在项目根目录下准备开始训练。4.2 修改cfg文件和训练启动参数以yolov3.cfg为模板把里面的classes改成你实际的类别数同时把每个yolo层前面的filters改为3乘以类别数加5。类别数如果是3类filters为24如果是8类filters为39。对红绿灯这类小目标任务推荐把输入分辨率设为608乘608而不是416乘416这会直接增加小目标的像素数。显存不够时也可以通过减小subdivisions来缓解。# 训练命令使用darknet53.conv.74预训练权重 ./darknet detector train \ cfg/traffic_light.data \ cfg/yolov3_traffic_light.cfg \ darknet53.conv.74 \ -gpus 0,1 \ -dont_show \ -map训练命令里有几个关键参数需要解读。-dont_show表示关闭实时可视化窗口在远程服务器上很有用。-map会在每个epoch结束后在验证集上计算mAP并输出到终端这是监控训练效果最重要的指标loss只能用来判断是否发散不能用来判断模型好坏。-gpus后跟多个GPU编号用逗号分隔单卡训练时不加这个参数或只写一个序号。训练过程中的典型表现是前100个iteration的loss值在几十甚至上百是正常的因为随机初始化的检测头输出值很大如果loss一直不下降先检查learning_rate是否过大yolov3默认的初始学习率是0.001如果batch size改大了而学习率没降容易出现震荡。红绿灯场景的epoch一般不需要跑太长30000次iteration以内基本就能收敛前提是数据量在几千张这个量级。4.3 训练过程中的关键日志解读loss、avg loss与mAPdarknet每100个iteration会打印一次loss和avg loss。avg loss是平滑后的值更值得关注。如果avg loss在前500个iteration内从几十降到十以内说明网络在正常学习。这时需要重点观察mAP曲线的变化mAP从0开始爬升是正常的但如果到了20000次iteration mAP还不超过10%几乎可以确定是数据标签有问题或anchor分配有问题。另一个细节是iteration和epoch的关系。一个epoch等于完整跑完一遍训练集iteration是batch的数量。如果训练集有5000张图batch为64subdivisions为8那么一个epoch大约需要5000除以64约78次iteration。训练日志里会显示当前iteration对应的epoch位置确保在判断“训练是否偏了”时参考的是epoch而不是iteration。最后在训练结束时备份backup目录下生成的yolov3_traffic_light_final.weights这个就是最终模型。5. 红绿灯检测常见问题排查从数据集翻车到夜间失效的五个血泪记录5.1 训练loss持续不降甚至上涨现象loss在几百个iteration里完全不下降avg loss还在往上爬。原因最常见的是学习率设置偏大。很多人把yolov3.cfg原版的learning_rate0.001直接套用在自己很小的数据集上但小数据集的梯度方差大过大的学习率会让loss在陡峭区域反复横跳。另一个常见原因是类别数改了但filters没改导致输出维度不匹配实际训练时网络结构报错但被darknet的容错机制忽略了。解决先把learning_rate调到0.0005或0.0003同时把burn_in从默认的1000改成2000让前2000个iteration用更小的速率预热。确认cfg文件中所有yolo层的classes都改了所有yolo层前面的filters都等于3乘以(classes加5)。改完后重新训练前2000个iteration的loss应当稳定下降到原始值的50%左右。5.2 小目标灯头几乎全部漏检但大目标灯头检测正常现象近距离的灯头能检测出来距离超过30米的灯头在测试图片上一个都检不到。原因输入分辨率太小或者直接把416乘416的分辨率用在包含大量远距离小目标的测试图上。另外如果anchor聚类时没有把较小的anchor包含进来小目标就没有合适的先验框。解决把训练和测试的输入分辨率都调整为608乘608。重新运行kmeans聚类时把聚类中心数设为9后检查最小的三个anchor如果宽度或高度小于3个像素说明聚类结果不适合小目标场景需要增加采样或重新缩放标注框。还可以在增强中提高随机缩放的占比例让小目标样本占总样本的比例达到20%以上。5.3 夜间图片大量误检路灯被识别成红灯现象白天测试效果尚可夜间图片里路灯、刹车灯、霓虹灯被大量识别成红色信号灯。原因夜间场景在训练集里占比太少模型没有见过灯头周围是暗色背景的样子把“红色圆形发光体”当成了充分特征。实际上真实信号灯在夜间有明显的灯头外壳和黑边框而刹车灯和路灯没有。解决在训练集中补充至少30%的夜间图片并配合暗度增强。OpenCV里可以用cv2.convertScaleAbs把亮度整体降低也可以直接用darknet自带的曝光扰动参数在cfg文件的data_augmentation部分设置jitter和hue的合理值。注意夜间增强的重点是保持灯头的色彩饱和度和外壳边缘纹理不要把整张图压得太暗导致灯头自身也看不清。5.4 红灯绿灯互混特别是远处的小尺寸灯头现象验证集里red的mAP有80%但green的mAP只有40%或者对同一组图反复测试两次分类结果不一致。原因绿色信号灯在远处因为镜头色差或雾霾呈现出蓝绿色或灰白色模型只能靠低层纹理的微妙差异来区分而yolov3对低层特征的分类权重往往不足。另一个原因是绿色类别在数据集中数量少于红色类别数据不平衡导致分类边界偏移。解决先统计训练集中红绿两类的实例数量如果绿色不足用数据集扩充工具生成绿色的变换版本。另外可以在分类头后面加一个小型的颜色校正分支用平均值池化提取灯头区域的颜色直方图辅助分类决策。这种方法不算复杂但对红绿灯这种强颜色语义目标往往有奇效。5.5 在固定路口的测试效果不错一到新城市就失灵现象在采集数据的城市测试mAP稳定但在另一个城市的同风格路口测试误检和漏检明显增多。原因不同城市的信号灯款式、排列方式、灯头大小差异很大有的城市用竖排三灯有的用横排还有的带数字倒计时面板这些变化都会影响检测。训练集只覆盖了一种固定样式模型过拟合到这种样式上。解决在数据准备阶段就有意识地加入多城市的公开数据集或者通过风格迁移增强来模拟不同灯头样式。更实际的方案是收集至少三个不同城市来源的路口图片混合训练并把倒计时面板出现的情况单独检查一下看模型是否误把数字区域当成了灯头。验证时不要只在采集地验证用另一城市的少量图片做跨域验证这是衡量模型泛化能力最直接的方法。6. 把检测结果变成可用的信号灯状态输出序列决策与置信度校准检测模型输出的是一堆带类别和置信度的框但下游系统真正需要的是“当前路口是否允许直行、左转、右转”这样的结构化决策。这个过程需要两步后处理第一步是把单帧检测结果按时间序列做投票和滤波解决单帧误检闪烁的问题第二步是把信号灯状态与路口几何信息关联输出车道级通行指令。单帧置信度往往不够稳定一个红灯在连续30帧里可能有一两帧突然置信度降到0.3直接被过滤掉。常见做法是维护一个长度为10帧的滑动窗口每个候选目标框按位置做IoU关联然后对类别进行多数投票。如果投票结果里绿灯占多数哪怕当前帧检测缺失也仍然输出绿灯状态。这个机制能有效提升时序稳定性在实验报告里也是可以量化对比的指标一般能把帧级准确率从92%提升到97%以上。置信度校准是另一个值得做的优化。yolov3输出的置信度并不是真实概率尤其是在小目标上高置信度的框都集中在近距离灯头远距离灯头的置信度天然偏低。一个实用的校准方法是等频分箱把验证集上所有检测框的置信度区间分成10份统计每一份中真实正例的比例然后用这个比例替代原始置信度输出。这样下游逻辑就不需要针对不同相机距离设置多个置信度阈值了。实现上只需要几十行Python代码建议直接在实验结果里加入校准前后的对比数据。最后讨论一下部署与落地的边界。用TensorRT对yolov3做FP16加速后在Jetson NX上的推理时间约为25毫秒足够支撑25帧每秒的实时检测。但要注意TensorRT对自定义anchor和yolo层有兼容性问题需要手动编写yolo层插件或改用自带yolo解析的版本。我的经验是先在darknet上做离线验证产品化时再转TensorRT不要在训练阶段就直接用TensorRT调试否则迭代效率太低。少走弯路的做法是在保存最终权重之前把验证集的结果可视化一遍随机挑几组白天、夜间、逆光、雨天的图片确认没有明显的系统性问题再进入部署环节这一步能省掉后面几乎所有的排查时间。希望这篇笔记中关于数据处理、类别设计、训练调参和排查思路的内容能帮到你让红绿灯检测这个看似简单的小项目真正经得住真实路口的考验。本文还有配套的精品资源点击获取