
简介基于YOLOv3的道路红绿灯检测识别实验报告文档包面向智能驾驶、计算机视觉方向的学生与开发者提供从环境搭建、数据准备、模型训练到评估应用的全流程实验记录。压缩包仅1个doc文件大小438KB内容包含项目申请书、任务书、结课鉴定表及完整实验报告便于对照学习。已有648人学习下载。报告详细展示了基于DarkNet-53的YOLOv3模型在416×416输入下对红绿灯和路标三类目标的识别效果包含200个epochs、batch 64等训练参数以及FPN多尺度融合、小目标检测精度提升与实时性问题的解决思路。针对实验中的典型难点如小目标漏检、模型调优方向等均给出具体方法适合作为高校综合项目实践、深度学习中目标检测方向课程设计的参考范本。1. yolov3道路红绿灯检测这篇实验报告到底要回答什么我见过不少团队把红绿灯检测当成一个常规目标检测任务来跑丢一个 YOLO 训练脚本上去loss 降到 1 以内就说“能用了”。实际一上路晴天上午十点效果还行一到傍晚逆光或者夜间路灯和红灯同时亮的时候误检和漏检一起冒出来。这篇实验报告要讲清楚的正是“基于 yolov3 道路红路灯检测识别”这套方案怎么从数据准备一路走到模型评估哪些参数决定了它能不能落地。它是给做车载感知、交通信号识别或者自动驾驶视觉模块的工程师看的也适合研究生做课题时抄作业。核心结论是先处理数据再调模型yolov3 在红绿灯这种小目标场景下能到可用级别但必须针对信号灯特性做定制调整。2. 红绿灯数据准备标注格式、类不平衡与训练集划分2.1 数据源选择别只盯着公开数据集红绿灯检测的第一步不是搭模型而是先搞清楚数据集里到底有什么。公开数据集里Bosch 的小目标数据集和国内一些交通信号数据集都带红绿灯标注但它们的拍摄视角和国内路口安装的红绿灯不完全一致。Bosch 的数据是欧洲风格信号灯横置和竖置都有但国内路口常见的是竖置灯组加倒计时数字如果你直接用公开数据训练在本地路口测试时经常会出现“灯找到了但红绿反了”或者“倒计时数字被当成灯”的问题。我一般会把公开数据和自采数据混着用。自采数据不用多三五个路口、早中晚各跑一趟手机或行车记录仪拍就行重点是覆盖几种典型情况直行灯、左转灯、箭头灯、圆形灯、带倒计时的灯组。数据量上每类灯至少有 1000 个实例框绿灯可以略微少一些但红灯一定要多因为红灯漏检的后果比绿灯漏检严重得多。更关键的是背景多样性同一个路口的三月与七月树叶密度完全不同阴影位置也不同这些差异都会影响泛化。2.2 用 Labellmg 做 VOC 标注后再转 YOLO 格式yolov3 训练需要的是每张图对应一个 txt 文件每行是“类别ID x_center y_center width height”坐标全部归一化到 0 到 1 之间。如果你已经用 Labellmg 标注了 VOC 格式的 XML不需要重新标一遍写个小脚本转换就行。import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, txt_path, class_list): tree ET.parse(xml_path) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) with open(txt_path, w) as f: for obj in root.findall(object): name obj.find(name).text if name not in class_list: continue class_id class_list.index(name) box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) x_center (xmin xmax) / 2 / img_w y_center (ymin ymax) / 2 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h f.write(f{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}\n) if __name__ __main__: classes [red, green, yellow, red_left, green_left] xml_dir annotations txt_dir labels os.makedirs(txt_dir, exist_okTrue) for xml_file in os.listdir(xml_dir): if not xml_file.endswith(.xml): continue voc_to_yolo( os.path.join(xml_dir, xml_file), os.path.join(txt_dir, xml_file.replace(.xml, .txt)), classes ) print(f转换完成共处理 {len(os.listdir(xml_dir))} 个 XML 文件)这段逻辑说明一下类别的顺序由 class_list 决定训练时 cfg 文件里的 classes 数量必须和这个 list 长度一致。坐标归一化是 yolov3 的硬性要求如果不归一化宽高比不同的图片会导致 anchor 匹配失效。还有个不起眼但很关键的细节是 XML 里“size”字段可能有不同的嵌套结构Labellmg 和 Labelbox 这类工具导出的格式略有差异跑之前先打印一条出来核对 root 结构免得索引报错。参数说明class_list 里的类别名称必须和 XML 中的 name 完全一致连大小写都不能差。坐标系是用整张图的宽高做分母所以对图片尺寸没有固定要求但后面训练时 cfg 里的 width 和 height 会强制 resize 到 416x416这里归一化就是为那个 resize 做的准备。2.3 类不平衡与背景难例采样策略和 IOU 过滤红绿灯数据有个天然问题红灯和黄灯的数量往往远少于绿灯而指向性箭头灯比圆形灯少一个数量级。yolov3 默认把所有类别的 loss 平等对待如果某类样本太少它的梯度贡献会被大类别淹没。常见处理办法是给少样本类别加一个采样权重在训练时对每张图的类别分布做一次统计样本少的类别所在图片以更高概率被选入每个 batch。还有一种做法是在 loss 里给类别权重加权但我更推荐先做采样平衡因为改变 loss 权重容易让模型在小样本类别上过拟合出现“只看颜色不看形状”的问题。背景难例也要格外注意。路边广告牌上的红色圆形 logo、刹车灯、甚至夕阳反光都会在训练初期被模型当成红灯。解决方式是在数据集里混入一批“无目标”图片也就是那些包含红色圆形物但并不是红绿灯的图。这类图标注成空 txt 文件放入训练集yolov3 会从中学习“这些红色区域不该有框”。我习惯把难例图占比控制在总数据量的 20% 左右太少不起作用太多会压制真正的正样本。2.4 训练集、验证集与测试集怎么切分红绿灯检测的评估一定要按路口切分不能按图片随机切分。原因是同一个路口的连续帧非常相似如果随机切验证集里会出现与训练集几乎相同的画面mAP 虚高得离谱真正上路才知道泛化不行。我按视频片段切采集时给每个路口一个编号按编号排序后每隔 10 个路口取 2 个做验证、2 个做测试剩下的做训练。这样可以确保验证集和测试集中的路口完全没在训练中出现过。切分完毕后统计一下每个集合中各类别的框数量确认分布大致一致。如果验证集里没有黄灯样本你调参时根本无法发现黄灯类的漏检问题。红绿灯任务的类别不平衡比一般目标检测更严重切分前先看一眼分布比训练完再补救省事得多。3. yolov3 模型结构选型Darknet-53、Anchor 与损失函数怎么配合3.1 为什么选 yolov3 而不是 yolov5 或 SSD做红绿灯检测很多人会直接问“这都哪年了为什么不用 yolov5”。但实验报告的定位是稳定复现不是刷榜。yolov3 的工程成熟度很高Darknet 框架配置简单训练中对显存要求低而且每个层的输出都可以直接可视化排查问题比 yolov5 的封装结构直观得多。SSD 在红绿灯这种小目标上的表现不如 yolov3因为 SSD 的默认框设置偏大对 30x30 像素以下的信号灯基本无能为力。yolov3 的多尺度预测天然适合红绿灯。它在三个尺度上做检测分别是 13x13、26x26、52x52 的特征图。红绿灯在 1080p 行车记录仪画面里通常只有 20 到 50 像素高对应到 416x416 的输入图上就是 8 到 20 像素左右需要靠 52x52 那一层来检测。当然52x52 也带来了误检风险因为它感受野小更容易被局部颜色欺骗这就是后面要讲 anchor 与后处理过滤的原因。3.2 Anchor 尺寸对红绿灯小目标的影响yolov3 的 anchor 是聚类出来的不同数据集聚类结果完全不同。如果你直接用 COCO 的 anchor比如 10x13、16x30、33x23 那组对红绿灯这种长宽比接近 1:1 或 1:2 的小物体会非常不匹配。我做过一次实验COCO anchor 训练 100 轮后红绿灯类别的 recall 只有 0.6 左右聚类后能到 0.8 以上。# 在 Darknet 目录下执行kmeans 聚类生成适合当前数据集的 anchor ./darknet detector calc_anchors data/traffic_light.data \ -num_of_clusters 9 -width 416 -height 416 -show参数说明num_of_clusters 设为 9 是因为 yolov3 每个尺度 3 个 anchor共 9 个。width 和 height 必须与训练 cfg 保持一致否则聚类出的 anchor 是在不同分辨率下的迁移到训练时会失去意义。show 参数会打印每个 cluster 的样本数量如果发现某个 cluster 只有个位数样本说明这个尺寸分布不合理可以考虑减少 cluster 数或补充对应尺寸的样本。聚类完成后把生成的 9 个 anchor 按面积从小到大排序前三个分配给 52x52 层中间三个给 26x26 层最大的三个给 13x13 层。这里有个细节Darknet 的 cfg 中每个 yolo 层的 anchors 必须按“从小到大”排列很多人在这里排反了导致小目标全部匹配到感受野最大的层训练起来感觉 loss 降了但检测效果极差。3.3 Darknet-53 的骨干网络在红绿灯任务上的表现Darknet-53 是 53 层卷积网络没有全连接层靠残差连接加深网络。红绿灯这种颜色特征显著而纹理特征弱的目标其实用不了那么深的网络来提取纹理但深网络对“区分红色圆形物是信号灯还是刹车灯”这类容易混淆的情况还是有帮助的。刹车灯和信号灯在颜色上几乎一样区分它们靠的是位置上下文比如信号灯通常在画面上部或固定在杆上刹车灯在中下部。这些上下文信息需要更大的感受野Darknet-53 的深卷积层在这里起了作用。在实际训练中我倾向于冻结骨干网络的前 20 层只微调后面层。因为前几层学到的边缘和颜色特征非常通用用公开数据预训练已经足够了重新训练反而容易在数据集较小时过拟合。具体做法是修改 cfg 中的 stopbackward 参数在想要冻结的层之后加上 stopbackward1训练脚本在反向传播时会停在那一层。这个参数在做红绿灯这类小型数据集时尤其好用能显著减少训练时间。3.4 损失函数三个部分对红绿灯训练的直接影响yolov3 的 loss 包含三块坐标损失、置信度损失和类别损失。红绿灯场景里坐标损失的权重可以适当调低因为信号灯框的精确到像素的边界对判断“红灯还是绿灯”的意义不大只要中心点落在灯面上分类对了就够了。但置信度损失的权重需要调高因为红绿灯误检的根源就是模型预测出了太多不必要的框。loss 权重在 cfg 的 yolo 层里设置三个参数是 loss_scale、coord_scale 和 conf_scale。我常用的一组设置是 coord_scale1.0、conf_scale1.5、cls_scale1.0这组值在保持召回率的同时能把误检框压掉不少。需要注意调高 conf_scale 会让训练 loss 数值升高这是正常的不要看到 loss 变高就调回去要观察的是最终 mAP 和每类的 recall 是否改善。4. 训练实操从命令行到训练日志一遍跑通4.1 Darknet 训练红绿灯模型的最小命令假设你已经准备好了标注数据、anchor 聚类结果和类别文件训练命令本身并不复杂。下面是最小可用的命令组合我会跟着解释每个输入文件是干什么的。# 下载 Darknet 并编译打开 GPU 支持后执行训练 git clone https://github.com/pjreddie/darknet cd darknet # 修改 Makefile 后编译GPU1, CUDNN1, OPENMP1 make # 开始训练weight 参数指定预训练权重 ./darknet detector train data/traffic_light.data \ cfg/yolov3-traffic.cfg \ darknet53.conv.74 \ -gpus 0,1 21 | tee train.log这段命令中data/traffic_light.data 是一个文本文件内容格式固定包括类别数、训练集图片列表路径、验证集图片列表路径和 backup 目录路径。cfg/yolov3-traffic.cfg 是模型结构文件你需要把原有 cfg 中的 filters 和 classes 改成自己的类别数。darknet53.conv.74 是 Darknet-53 在 ImageNet 上的预训练权重只包含卷积层加载后能加速收敛。tee train.log 的作用是把训练输出同时保存到文件里方便后面回溯 loss 变化趋势。训练过程会用类似迭代轮数的方式输出日志每 100 轮打印一次平均 loss、IOU 和 recall 的统计数值。第一次训练时把 batch 设成 64、subdivisions 设成 8这样每次前向传播实际使用 8 张图对显存要求友好很多。如果显存只有 8G 左右subdivisions 设成 16 也是可以的代价是训练速度变慢。4.2 日志里的那些指标怎么看训练日志是判断“模型是否在往正确方向走”的唯一窗口。Darknet 输出格式大概是这样的v3 (iou loss, Normalizer: (iou: 0.75, obj: 1.00, cls: 1.00) Region 82 Avg IOU: 0.67, Class: 0.83, Obj: 0.52, NoObj: 0.02, .5R: 0.71, .75R: 0.45, count: 18这里最重要的几个值是 Avg IOU、.5R 和 NoObj。Avg IOU 表示预测框与标注框的平均交并比红绿灯这类小目标在训练初期能到 0.6 以上就算正常如果一直在 0.4 徘徊大概率是 anchor 没配对好或者学习率出了问题。.5R 表示 IoU 阈值 0.5 下的召回率小目标能到 0.7 以上就值得继续训练如果 30 轮后还在 0.5 以下建议停下来检查数据标注框是否漏标了。NoObj 是模型把背景预测为目标框的置信度损失这个值应当小如果在训练 50 轮后仍然大于 0.1说明你的模型在背景区域持续给出高置信度预测。解决办法是检查训练集里是否大量图片的背景里存在相近颜色的物体或者把 conf_scale 调高强行压低背景预测。4.3 学习率与数据增强红绿灯场景的常用设置红绿灯数据量通常不大学习率设置得太高容易震荡甚至发散。我常用的初始学习率是 0.001配合多步长衰减策略在迭代到最大轮数的 80% 和 90% 时分别降为原来的 0.1 倍。Darknet 的 cfg 中可以在训练区段加 steps 和 scales 参数来实现。# 假设最大训练轮次为 10000 learning_rate0.001 policysteps steps8000,9000 scales0.1,0.1参数说明steps 必须按顺序递增scales 表示对应 steps 之后学习率乘以的倍数。这里用 0.1 意味着在 8000 轮时学习率从 0.001 降到 0.0001在 9000 轮时再降到 0.00001。红绿灯任务的训练轮次不宜太长我见过不少人把 10000 轮跑满结果验证集 mAP 在 6000 轮之后开始缓慢下降这就是过拟合的典型表现。如果你观察到这种趋势把训练轮次砍到 6000 到 7000 轮反而效果更好。数据增强方面yolov3 内置了对随机裁剪、翻转、饱和度和曝光的更改。对于红绿灯饱和度扰动特别重要。因为不同厂家生产的红绿灯颜色略有差异LED 灯珠的色温不同同是红灯有的偏橙有的偏玫红饱和度扰动能让模型学习到“红色”的范围而不是某个精确的 RGB 值。但要注意hue 扰动值不要设得太大否则红色和黄色会混在一起模型分不清红灯和黄灯。4.4 从训练到验证map 计算命令与结果解读训练完成后用验证集跑一次 mAP 评估命令如下./darknet detector map data/traffic_light.data \ cfg/yolov3-traffic.cfg \ backup/yolov3-traffic_final.weights \ -thresh 0.25 -iou_thresh 0.5参数说明-thresh 是置信度阈值评估时设成 0.25 是行业常用值可以反映出模型在低置信度下的表现。如果这里 mAP 明显低于你在测试时手动看的效果多半是验证集里存在训练阶段没见过的困难场景。红绿灯检测的评估还会输出每个类别的 AP我重点看红类的 AP它低于 0.8 我不会把模型交给车上用。5. 避坑yolov3 红绿灯检测的 5 条踩坑记录5.1 红灯绿灯类别混叠根因是颜色范围没对齐现象训练时 loss 正常测试时把红灯识别成绿灯或者绿色箭头灯和圆形红灯混在一起。最初的几个版本里这个问题严重到 P 值虚高但实际没法用。原因不同地区信号灯的色温和形状差异比想象的大。我用的数据集里红灯偏橙红验证集里偏玫红模型学到的是“橙红”的特征空间见到玫红就归类到相近的绿色类别上。另一个原因是标注本身存在误差有些标注员把“红色倒计时数字”标成红灯把“绿色倒计时数字”标成绿灯倒计时数字和信号灯颜色一致但没有灯组语义模型无法区分。解决先从标注质量入手制定规则排除倒计时数字和行人过街红灯。然后让训练集和验证集覆盖更大的色域用色彩抖动增强配合饱和度随机扰动重新训练。最后在类别定义里增加一个“unknown_red_circle”类收集广告牌、刹车灯等常见干扰物标注进去让模型学会抑制它们而不是强行归类到红绿之间。5.2 小目标漏检52x52 层形同虚设现象车距 30 米以上时信号灯在画面中只有 10 到 15 像素高模型完全漏检。检查日志发现 13x13 和 26x26 层的召回率很高但 52x52 层的 .5R 极低。原因yolov3 的 52x52 层用于小目标检测但如果你在训练时将输入分辨率固定为 416x416信号灯经过多次下采样后只剩很小的一块特征区域。聚类的 anchor 虽然关注了小尺寸但是 52x52 层的特征图分辨率仍然不够。解决把训练分辨率提升到 608x608 甚至 736x736同时生成 anchor 时也要用相同的分辨率重新聚类。如果显存不够把 batch 从 64 降到 32 并用 subdivisions 补偿。提升分辨率后52x52 层的特征可以覆盖到 20 像素以上的信号灯小目标漏检问题明显改善。5.3 夜间与逆光场景模型突然“瞎了”现象白天路测 mAP 有 0.85夜间测试直接掉到 0.4红灯和路灯混在一起分不清楚。逆光时灯的颜色全部偏暗模型甚至识别为“未知物体”。原因数据集中夜间样本严重不足。公开数据集大多在白天拍摄夜间的灯珠有光晕晕染现象模型看到的非圆形红色区域与白天的灯差异大路灯的暖光也容易触发误检。解决补采夜间数据重点是雨后地面反光、路灯与红灯同时出现在一个画面中的情况。训练时将曝光扰动调大让模型学习到“暗背景下亮灯”的特征组合。夜间我有一个技巧是单独跑一次预标注先用白天模型给夜间数据打框再人工修正这能大幅缩短夜间数据标注工时。5.4 loss 降不下去anchor 聚类结果导致训练波动现象训练 2000 轮后 loss 还在 4 到 6 之间徘徊从中看不出收敛趋势。loss 偶尔会突然跳高一次然后回落。原因anchor 聚类时我直接把 COCO 的 anchor 填进了 cfg没有重新聚类。小目标与大目标的梯度方向不一致导致模型在优化目标上左右互搏。偶尔跳高是因为 batch 里出现了极端样本之前没有难例抑制机制。解决按 3.2 节方法用 calc_anchors 重新聚类。同时在 cfg 里开启 random1多尺度训练配合足量难例。最后把 loss 的移动平均窗口调大我用 python 脚本每 100 轮做一次均值统计过滤掉单次异常值能看到真实的收敛趋势。5.5 前向推理时目标框偏到灯杆上现象yolov3 在测试图片上给出的红绿灯框明显偏移中心点落在灯杆或悬臂上如果按框的中心点去关联灯组状态逻辑就乱了。原因训练时 loss 坐标权重太大模型为了减小坐标误差把框“缩”到了目标中心附近。对红绿灯这种小目标宽高误差在 5 像素以内都可接受但中心点必须准。实际上我从日志看出坐标 loss 占比超过 40% 就会挤压分类 loss 的学习。解决把 coord_scale 适当调低至 0.8 或者 0.9让模型把更多梯度方向放在置信度和分类上同时保证中心点回归准确。如果你在可视化中看到大量框的中心点在灯杆上而灯体边缘也可以手动在损失函数里对中心点做额外的加权但我通常只在后处理中加一个二次匹配步骤框的中心距离灯体越近置信度权重越高。6. 从 mAP 到车载部署前的最后一步置信度阈值与延迟验证yolov3 训练完成后别急着接上车。车载环境对误检的容忍度极低一个误检框如果触发了刹车逻辑后果比漏检还严重。我的习惯是把置信度阈值从 0.25 提到 0.6然后分别在白天、夜间、雨雾等场景做一次前向推理测试统计每帧的平均耗时。Darknet 的 C API 可以直接在嵌入式设备上调用也可以用 TensorRT 做加速把 FP32 换 FP16 后推理耗时能减少一半。验证时记录两个指标一是每类和每个场景的精确率与召回率。二是框的中心点与灯组的距离误差确保输出的坐标可以直接用于信号灯状态判断。如果发现夜间误检依然存在我会在模型输出后加一个基于位置的逻辑过滤比如忽略画面下方 20% 区域内的红色目标框这个区域不太可能出现红绿灯。用 yolov3 做红绿灯检测的项目真正成熟的标志不是 mAP 数字而是它在没见过的路口的全天候表现。这类模型在一次上车实测后往往会暴露出一个共同的训练盲区车辆转弯时的视角变化。直线行驶时信号灯总是垂直于画面转弯时灯组出现明显的旋转和透视形变yolov3 对这些样本的鲁棒性不好。建议在数据增强阶段加入小幅旋转正负 5 度以内让模型学到灯的轮廓而不是特定朝向。这条经验是我自己踩坑后沉淀下来的用上了之后模型在环岛和匝道场景的表现明显稳定。希望这个方向的经验能帮到你尤其是那些刚把 yolov3 跑通但不知道如何让它真正“上路”的工程师。模型训练的终点永远是实车验证那一刻。本文还有配套的精品资源点击获取