资讯详情

基于yolov3的道路红绿灯检测识别实验:从数据集到anchor重聚类与颜色校验

📅 2026/10/7 12:02:41 | 华诺云谱 👁 阅读
基于yolov3的道路红绿灯检测识别实验:从数据集到anchor重聚类与颜色校验
简介这份实验报告围绕基于Yolov3与DarkNet-53的道路红绿灯及路标检测识别展开面向智能驾驶、计算机视觉方向的学生与研究者适合作为目标检测课程设计、综合项目实践或入门小目标检测的参考范例。资源包内含1个doc文档约438KB完整记录了从实验调研、环境准备、数据预处理、模型训练到效果展示与问题解决的全过程并给出epochs、batch size、输入尺寸416×416、3个类别等关键参数设置。报告重点分析了YOLOv3借助多尺度特征图融合提升小目标检测精度的思路展示了单目标与多目标红绿灯场景下的识别效果并总结了实时性与精度优化经验。目前已有649人学习下载可为读者撰写实验报告、复现YOLOv3交通标志检测流程、理解FPN式上采样融合策略提供直接参考。1. 从一次路口误检说起yolov3 道路红绿灯检测到底难在哪城市路口跑 yolov3 做红绿灯检测最容易翻车的不是模型本身而是数据分布和推理后处理。我见过太多实验报告把 mAP 刷到 0.9 就收工结果一上车测试白天正常、傍晚全灭、夜间把车尾灯认成红灯。原因很朴素红绿灯在整幅图里占比极小COCO 预训练权重对「小目标 强逆光 运动模糊」几乎没有先验直接 fine-tune 很容易过拟合到某个路口的固定形态。这篇笔记围绕「基于 yolov3 道路红绿灯检测识别实验报告」这个题目把一套能复现的实验流程拆开讲数据集怎么组织、anchor 怎么重聚类、训练参数怎么设、推理阶段怎么做颜色判定、评估指标怎么算才不骗自己。适合正在做课程设计、毕设或工程预研的读者也适合已经跑通 demo 但精度上不去的同学。整套方案在单张 8G 显存的卡上就能跑不依赖任何闭源工具。2. 数据集与标注红绿灯检测的第一道分水岭2.1 为什么通用数据集不够用公开的交通灯数据集如 Bosch Small Traffic Lights、LISA、S2TLD各有侧重Bosch 分辨率高但场景单一LISA 偏北美灯型S2TLD 覆盖国内路口但标注粒度不一。直接混用会出现类别定义冲突——有的把「红黄绿」当三类有的把「灯 箭头」拆成十几类。我的做法是统一成 4 类red、green、yellow、off含未点亮和箭头灯这样既覆盖实际需求又避免类别过多导致小样本类崩掉。如果自己采集建议按「路口 × 时段 × 天气」三维度分层。每个路口至少覆盖早高峰、正午、傍晚、夜间四个时段每个时段不少于 200 帧。夜间样本尤其重要因为红绿灯在夜间是自发光目标和白天反射光的特征差异极大训练集里夜间占比低于 20% 时模型夜间召回率通常掉 30% 以上。2.2 标注规范与 VOC 转 YOLO 格式标注用 LabelImg 即可但有几个细节必须统一框只框灯体发光面不含灯壳和支架被遮挡超过 50% 的目标标为 difficult训练时忽略黄灯和红灯同时亮过渡态时按当前实际点亮颜色标。标完导出 VOC 格式再转成 YOLO 需要的 txt。import xml.etree.ElementTree as ET import os # 类别映射顺序必须和训练时的 names 文件一致 classes [red, green, yellow, off] def convert_voc_to_yolo(xml_path, img_w, img_h, out_path): tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.iter(object): cls_name obj.find(name).text if cls_name not in classes: continue cls_id classes.index(cls_name) bbox obj.find(bndbox) # VOC 坐标是 1-based转 YOLO 需要减 1 x1 float(bbox.find(xmin).text) - 1 y1 float(bbox.find(ymin).text) - 1 x2 float(bbox.find(xmax).text) - 1 y2 float(bbox.find(ymax).text) - 1 # 归一化为中心点 宽高 cx (x1 x2) / 2.0 / img_w cy (y1 y2) / 2.0 / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h lines.append(f{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) with open(out_path, w) as f: f.write(\n.join(lines))这段脚本的关键点是坐标减 1 和归一化。VOC 的 xmin 从 1 开始YOLO 从 0 开始漏掉减 1 会让所有框整体偏移一个像素小目标上这个误差很致命。另外归一化必须用图像真实宽高不能用网络输入尺寸否则 resize 后框会错位。转换完建议随机抽 20 张用脚本把框画回图上肉眼检查我一般会写个可视化脚本比看 loss 曲线更早发现问题。2.3 数据增强的取舍红绿灯检测的增强不能照搬通用策略。随机裁剪要慎用因为灯体本来就小裁掉一半就废了mosaic 增强对 yolov3 有效但四张图拼接后小目标密度过高容易让模型学到「到处都是灯」的虚假先验。我一般用这几项HSV 色调扰动±10、亮度对比度扰动±20%、随机水平翻转、轻微旋转±5°。夜间样本额外加高斯噪声模拟传感器噪点。翻转要注意红绿灯左右翻转后颜色语义不变可以放心用但如果是箭头灯翻转会改变指向这类样本要单独标记不参与翻转。3. 网络结构与 anchor 重聚类让 yolov3 适配小目标3.1 yolov3 的三个检测尺度够不够yolov3 在三个尺度上做检测分别是 13×13、26×26、52×52以 416 输入为例。红绿灯在 1920×1080 原图里通常只有 20×60 像素缩到 416 后只剩 4×13 像素落在 52×52 特征图上也只有 8×26勉强够用但召回率不高。如果显存允许把输入提到 608 甚至 832小目标召回会明显改善。我实测 416 输入时红灯召回约 0.78608 输入能到 0.89代价是推理耗时增加约 1.8 倍。另一个思路是砍掉 13×13 分支。红绿灯不会占满整幅图大目标分支基本是浪费算力去掉后模型更轻且不影响精度。这个改动在自定义训练时很实用但要注意修改 cfg 里的 yolo 层和对应的 route 连接改错会导致维度不匹配。3.2 用 k-means 重新聚类 anchorCOCO 的 9 个 anchor 是为通用目标设计的直接拿来检测红绿灯IoU 分布会很难看。必须用自己的标注框重新聚类。做法是把所有训练框的宽高提取出来跑 k-meansk9距离用 1-IoU。import numpy as np def iou(box, clusters): # box: (w, h), clusters: (k, 2) x np.minimum(clusters[:, 0], box[0]) y np.minimum(clusters[:, 1], box[1]) inter x * y area_box box[0] * box[1] area_cluster clusters[:, 0] * clusters[:, 1] return inter / (area_box area_cluster - inter) def kmeans_anchors(boxes, k9, max_iter100): np.random.seed(42) clusters boxes[np.random.choice(len(boxes), k, replaceFalse)] for _ in range(max_iter): distances 1 - np.array([iou(b, clusters) for b in boxes]) labels distances.argmin(axis1) new_clusters np.array([ boxes[labels i].mean(axis0) if np.any(labels i) else clusters[i] for i in range(k) ]) if np.allclose(new_clusters, clusters): break clusters new_clusters # 按面积排序小 anchor 放前面 return clusters[np.argsort(clusters[:, 0] * clusters[:, 1])]聚类前要把框的宽高归一化到 0-1除以图像宽高聚类后再乘以网络输入尺寸得到最终 anchor。我一般会跑 3 次不同随机种子取平均避免陷入局部最优。聚类完把 anchor 按面积从小到大写进 cfg 的三个 yolo 层小 anchor 给 52×52 分支大 anchor 给 13×13 分支。这一步做对mAP 通常能涨 3-5 个点比调学习率管用。3.3 修改 cfg 的关键参数以 darknet 框架为例需要改这几处width/height改成 608、anchors换成聚类结果、classes改成 4、每个 yolo 层前的filters改成(classes 5) * 3 27。filters 算错是最常见的翻车点报错信息通常是维度不匹配但新手容易忽略。另外random1开启多尺度训练对红绿灯这种尺度变化大的目标有帮助但会拖慢训练显存紧张时可以关掉。4. 训练、推理与颜色判定把检测框变成信号4.1 训练参数与收敛判断迁移学习从 darknet53.conv.74 开始前 2000 步冻结骨干只训检测头学习率 1e-3之后解冻全网络学习率降到 1e-4用 step 策略在 8000 和 12000 步各降 10 倍。batch 设 168G 卡用 608 输入可能只能到 8subdivisions 设 16 或 32。训练时盯三个指标avg loss 是否稳定下降、各分支的 recall 是否均衡、验证集 mAP 是否过拟合。如果训练 loss 降但验证 mAP 停滞多半是过拟合加增强或加 dropout。一个血泪经验红绿灯数据集里 green 类样本通常远多于 yellow因为黄灯只亮 3 秒。类别不平衡会让模型偏向 green。解决办法是在 loss 里给 yellow 加权或者对 yellow 样本做过采样。我一般把 yellow 的采样权重设成 green 的 3 倍效果比改 loss 函数直接。4.2 推理后处理与颜色二次校验yolov3 输出的是类别概率但红绿灯有个特殊问题同一个灯体在不同光照下颜色会漂移模型可能把暗红灯判成 off。我的做法是在检测框内做一次 HSV 颜色校验用检测结果和颜色判定做投票。import cv2 import numpy as np def refine_color(frame, box, cls_id, classes): x1, y1, x2, y2 map(int, box) roi frame[y1:y2, x1:x2] if roi.size 0: return cls_id hsv cv2.cvtColor(roi, cv2.COLOR_BGR2HSV) # 红色有两段 hue mask_red cv2.inRange(hsv, (0, 80, 80), (10, 255, 255)) | \ cv2.inRange(hsv, (170, 80, 80), (180, 255, 255)) mask_green cv2.inRange(hsv, (40, 80, 80), (85, 255, 255)) mask_yellow cv2.inRange(hsv, (15, 80, 80), (35, 255, 255)) counts [mask_red.sum(), mask_green.sum(), mask_yellow.sum()] color_map {0: red, 1: green, 2: yellow} # 如果颜色投票和模型类别冲突且颜色置信度高以颜色为准 if max(counts) 0.3 * roi.shape[0] * roi.shape[1] * 255: color_id counts.index(max(counts)) if classes[color_id] ! classes[cls_id]: return color_id return cls_id这段逻辑的核心是模型负责定位颜色负责分类。HSV 阈值要根据实际摄像头调白天和夜间用不同阈值组。注意红色 hue 跨 0 和 180 两段漏掉任何一段都会导致红灯漏检。投票阈值 0.3 是经验值太低会误判太高不起作用建议在验证集上扫一遍。4.3 评估指标怎么算才不骗自己mAP 是基础但红绿灯检测更该看的是「信号级」指标每个灯组的颜色是否正确、切换时刻是否及时。我一般额外统计三个数颜色准确率检测框内颜色判定正确的比例、误报率把非灯目标判成灯的比例、切换延迟真实切换时刻到模型输出切换的帧数差。切换延迟超过 3 帧约 100ms在实际控制里就不可接受了。这些指标比 mAP 更能反映落地效果写实验报告时建议一起放。5. 避坑与排查那些让实验报告翻车的细节5.1 现象训练 loss 正常但验证 mAP 极低原因通常是标注格式错误或类别映射不一致。VOC 转 YOLO 时如果类别顺序和 names 文件对不上模型学到的类别全是错的。解决方法是转换后随机抽图可视化确认框和类别都对。另一个可能是图像路径里有中文或空格darknet 读取失败但不报错直接跳过样本导致训练集实际为空。5.2 现象夜间推理几乎全黑检测不到灯原因是训练集夜间样本太少或者夜间样本的亮度增强过度导致模型学到虚假特征。解决方法是单独统计夜间验证集的召回率如果低于 0.5就往训练集里补夜间样本而不是调推理阈值。另外夜间推理时可以对输入做一次 gamma 校正提亮暗部但要注意别把噪声也放大。5.3 现象同一路口不同摄像头精度差异巨大原因是摄像头安装角度和曝光参数不同导致灯体在图像里的形态差异大。解决方法是训练时加入透视变换增强模拟不同安装角度推理时如果换摄像头先用少量新摄像头样本做一次微调哪怕只有 100 张也能明显改善。我一般会留一个「在线微调」的开关部署后遇到新场景就快速适配。5.4 现象黄灯几乎检测不到原因是黄灯样本少且持续时间短模型欠拟合。解决方法除了过采样还可以把黄灯和红灯的过渡态单独标一类让模型学到「红黄同亮」的模式。另外推理时如果连续几帧检测到红灯后突然出现黄灯可以结合时序做平滑减少漏检。5.5 现象推理速度达不到实时原因是输入分辨率太高或模型分支太多。解决方法是先降到 416 测试精度损失如果可接受就用 416或者砍掉 13×13 分支。另外 darknet 默认用 CPU 做 NMS可以改成 GPU 版 NMS能省 10-20ms。如果还不行考虑用 TensorRT 加速但要注意 INT8 量化对小目标精度有影响需要校准。6. 进阶技巧用时序投票把单帧检测变成稳定信号单帧检测最大的问题是抖动同一盏灯在连续帧里可能红绿交替直接输出会导致下游逻辑崩溃。我的做法是维护一个长度为 5 的滑动窗口对每个灯组用检测框的 IoU 做关联统计颜色投票只有窗口内超过 60% 的帧一致才输出该颜色否则保持上一帧状态。这个逻辑用 20 行代码就能实现但能把信号抖动率降低 80% 以上。from collections import deque, Counter class LightTracker: def __init__(self, window5, threshold0.6): self.window window self.threshold threshold self.tracks {} # track_id - deque of colors def update(self, track_id, color): if track_id not in self.tracks: self.tracks[track_id] deque(maxlenself.window) self.tracks[track_id].append(color) counter Counter(self.tracks[track_id]) top_color, count counter.most_common(1)[0] # 投票比例超过阈值才切换否则维持上一稳定状态 if count / len(self.tracks[track_id]) self.threshold: return top_color return None # 表示保持上一帧track_id 可以用简单的 IoU 匹配当前帧检测框和上一帧所有框算 IoU取最大且超过 0.3 的作为同一目标否则新建 track。窗口长度 5 是延迟和稳定性的折中窗口越长越稳但切换延迟越大。如果下游对延迟敏感可以改成加权投票越新的帧权重越高。验证这套方案是否有效不能只看 mAP要录一段真实路口视频统计「信号切换时刻和模型输出时刻的帧差」。我一般会人工标 20 次切换作为 ground truth然后算平均延迟和误切换次数。这套评估方法比任何离线指标都更能说明问题。最后说个习惯每次改完 anchor 或输入尺寸我都会先跑 500 步看 loss 曲线是否正常再决定要不要继续训。这个习惯帮我省了无数次通宵。红绿灯检测没有银弹数据质量、anchor 适配、后处理时序逻辑三块都做扎实才能从 demo 走到能用。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑