简介基于YOLOv5与OpenCV构建的道路红绿灯识别检测系统面向计算机视觉入门及进阶学习者可完成红灯、绿灯、黄灯、交通灯四类目标的精准检测识别。资源包含完整训练与推理源码、预训练模型权重、评估指标曲线及使用说明覆盖从环境配置到模型评估的完整流程适合作为目标检测实战项目参考。压缩包共79个文件以py源码、pyc编译文件、yaml配置、pt模型文件为主辅以jpg/png结果图像、sh脚本、txt说明等整体大小约41.68MB结构清晰便于按需取用。训练过程迭代200次附带loss下降曲线、Recall召回率、precision精确度及mAP等评估图表模型拟合较好可直接用于道路场景下的红绿灯识别实验。目前已有1487人学习下载其中红绿灯数据集可另行获取遇到使用问题也可与作者沟通交流。1. yolov5opencv 道路红绿灯识别这套组合的真实分工与适用场景去年做一个路侧红绿灯识别项目摄像头装在路口杆件上正对对向车道最近的一排灯只有15米最远的掉头灯在50米外。夜间对面车灯一照灯体直接过曝成一片白斑纯yolo模型十帧能漏八帧。后来把方案改成基于yolov5opencv实现道路红绿灯识别检测yolov5负责在整帧中定位灯组并给出粗分类OpenCV负责对检测框做颜色验证、灯圈形状校验和连续帧时序投票。这套组合适合正在做辅助驾驶、车路协同或交通监控的工程师和学生项目读完这篇笔记你可以从数据集准备一路走到带评估指标曲线的可部署系统同时避开夜间过曝和多车道串扰这两个现场最常见的坑。2. 为什么是yolov5opencv而不是端到端模型选型逻辑与场景失效分析2.1 YOLO管检测、OpenCV管验证两个组件各自扛哪一段红绿灯识别和通用目标检测最大的区别在于灯本身是一个“主动发光体”它在图像里的外观受曝光、白平衡、环境光影响极大。同一个红灯白天是深红色圆斑夜间是高亮过曝的白心红边雨夜可能是带光晕的一团。如果指望yolov5一个模型把所有外观都学进权重里训练数据得覆盖全时段全天气成本极高而且遇到没见过的眩光情况照样翻车。常见做法是让两个组件分工。yolov5只做“定位”和“粗分类”输出每个灯组的包围框和类别red/green/yellow这一阶段不追求像素级精确框稍微偏一点没关系。OpenCV拿到框之后做三件事第一把框内图像转到HSV色彩空间用颜色阈值统计发光像素占比第二用轮廓提取和圆形度计算验证框内确实有一个圆形灯圈排除把红色尾灯、红色广告牌、倒计时数字误判成红灯的情况第三按时间轴做连续帧投票只有连续若干帧都是同一状态才切换输出避免单帧误检导致信号跳变。这种“粗检测细验证”的分层设计好处是每一层都简单、可解释、可单独调参。模型漏检时OpenCV无从验证但至少不会因为颜色误判产生额外虚警模型误检时OpenCV的颜色和形状验证可以拦截掉大部分假阳性。整个系统出了问题你能明确知道是yolo没找到还是opencv验证没通过而不是面对一个端到端黑匣子无从下手。2.2 选yolov5而不是yolov8或纯OpenCV复现成本与部署边界的取舍很多人在yolov5和yolov8之间犹豫。我的选择逻辑很简单红绿灯项目要的是“快速出活 易于部署 踩坑有据可查”。yolov5在GitHub上的issue和教程存量最大从训练到导出ONNX再到TensorRT部署每一步都有成熟的方案yolov8的ultralytics包更新快但API变动也快半年前的部署代码可能因为接口调整直接跑不起来。对于红绿灯这种相对固定的视觉任务yolov5s的精度已经完全够用没必要追新。纯OpenCV方案我也试过。固定角度、固定位置的杆件摄像头下用颜色阈值形态学处理确实能识别红绿灯响应还特别快。但一旦摄像头装在公交车或巡检车上视角随车身颠簸和转向变化灯组大小和位置在画面里剧烈跳动纯CV的ROI和阈值全失效。这种场景必须用深度学习做检测再用OpenCV做后处理。反过来yolov5单独硬扛也不行它把倒计时数字、红色刹车灯和真正的红灯混为一谈时你很难通过改模型解决——因为问题不在定位而在语义边界。选型结论用一句话说固定摄像头可以只上OpenCV移动场景必须yoloopencv混合yolov5是当前综合成本最低的检测器选择。模型尺寸优先试yolov5s显存紧张再换yolov5n精度不够再上yolov5m。2.3 红绿灯为什么总让检测模型翻车小目标、过曝和颜色失真先说小目标。距离30米时一个标准红绿灯灯体在1080P画面里大约只有30x30像素占据整帧面积的千分之一。yolov5在640x640推理尺寸下这个目标经过三次下采样后特征图上的响应非常弱漏检率天然偏高。解决方向有两个一是把推理尺寸提到1280代价是速度几乎减半二是训练时用mosaic增强和复制粘贴增强让模型见惯小目标。具体怎么调后面章节细说。过曝是红绿灯场景独有的难题。夜间红灯亮起时CMOS传感器为了兼顾周围暗部会拉长曝光时间结果灯体中心变成纯白色只有边缘一圈是红色。模型训练数据里如果缺少这种过曝样本推理时就会把红灯认成白灯或者直接漏掉。处理办法不是去改模型而是在OpenCV验证阶段同时接受“纯红”和“白心红边”两种模式对白色中心区域周围做环形颜色采样。颜色失真是另一个隐蔽问题。不同厂家摄像头的白平衡策略差异很大同一个红灯在A品牌摄像头下是正红色在B品牌下偏橙在C品牌下偏紫。如果HSV阈值只按标准红色标定换个摄像头立刻失效。后面第5章会专门讲怎么处理这里先提一句阈值要留余量并且要做多摄像头标定。3. 训练自己的红绿灯数据集从采集标注到yolov5超参数调优3.1 数据集怎么来公开数据集打底自采数据补场景红绿灯检测不像行人检测那样有海量公开数据可用但也不是完全没有。业界常用的公开数据集有BSTLDBosch Small Traffic Light Dataset和S2TLD前者是欧洲道路场景后者包含中国和美国的交叉口画面。这些数据集适合做预训练和算法验证但直接拿到现场用通常是不够的——你的摄像头安装高度、角度、路口类型和公开数据差异很大模型泛化一定打折扣。自己采集数据才是关键。常见做法是拿行车记录仪或者开发板摄像头在固定路口的杆件上连续录制一周覆盖白天、黄昏、夜晚、雨天、逆光五个场景。采集时注意把不同时段的视频分开存放方便后续按场景划分训练集和验证集。录完的视频用抽帧脚本每隔3到5秒抽一帧每帧里至少包含一个清晰的灯组这样能攒出几千张有效图片。标注方面不建议用LabelImg一张张画框效率太低。用OpenCV写一个半自动标注脚本先跑一遍yolov5预训练模型生成候选框人工只修正错框和补漏框速度能快三倍以上。标注类别不要分太细就三类red、green、yellow。箭头灯左转、直行、右转如果现场需要区分单独加red_left、green_straight等类别但注意每个类别至少要有300个实例否则小样本类别会把整体mAP拖下来。3.2 VOC/COCO转YOLO格式标注转换脚本与bbox越界处理如果你的标注工具导出的是VOC XML或者COCO JSON而yolov5训练需要YOLO txt格式每行一个目标格式为“类别 x_center y_center width height”全部归一化到0到1这一步必须写脚本转换。网上有很多现成脚本但大部分没有处理bbox越界和宽高为0这两个边界情况我改造过一版核心逻辑如下import xml.etree.ElementTree as ET import os def voc_to_yolo(xml_path, out_dir, class_map): tree ET.parse(xml_path) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) lines [] for obj in root.iter(object): cls obj.find(name).text if cls not in class_map: continue box obj.find(bndbox) x1 float(box.find(xmin).text) y1 float(box.find(ymin).text) x2 float(box.find(xmax).text) y2 float(box.find(ymax).text) # 越界裁剪标注框偶尔会超出图像边界 x1 max(0, min(x1, img_w - 1)) y1 max(0, min(y1, img_h - 1)) x2 max(1, min(x2, img_w - 1)) y2 max(1, min(y2, img_h - 1)) if x2 - x1 2 or y2 - y1 2: continue xc ((x1 x2) / 2) / img_w yc ((y1 y2) / 2) / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h # 归一化后做一次范围保护防止浮点误差越过[0,1] xc min(max(xc, 0.0), 1.0) yc min(max(yc, 0.0), 1.0) w min(max(w, 0.0), 1.0) h min(max(h, 0.0), 1.0) lines.append(f{class_map[cls]} {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}) if lines: out_name os.path.splitext(os.path.basename(xml_path))[0] .txt with open(os.path.join(out_dir, out_name), w) as f: f.write(\n.join(lines)) class_map {red: 0, green: 1, yellow: 2} voc_to_yolo(annotations/00001.xml, labels, class_map)这段脚本的要点在于越界处理。标注工具偶尔会生成xmax大于图像宽度的框如果直接归一化训练时yolov5会报“Box does not fit within image bounds”警告严重时训练直接中断。裁剪后再过滤掉过小框宽或高小于2像素是为了防止归一化后宽高接近0导致loss变成NaN。另外注意x2和y2保底为1避免出现宽或高为0的空框。转换完成后把图片和txt标签按yolov5的标准目录结构放好images/train、images/val、labels/train、labels/val四个目录一一对应。最后写一个data yaml文件指向训练集和验证集路径并声明类别数和类别名。# traffic_light.yaml train: data/traffic_light/images/train val: data/traffic_light/images/val nc: 3 names: [red, green, yellow]这个yaml在训练命令里用--data参数指定。注意train和val路径建议用绝对路径yolov5对相对路径的解析偶尔会因为当前工作目录不同而找不到文件这是很多人训练时遇到的第一个“环境玄学”。3.3 yolov5超参数设置img、batch、lr与数据增强的取舍yolov5训练红绿灯模型超参数不需要大改但有几个值必须按场景调。推理尺寸--img我建议用640起步如果验证集上小目标漏检严重再提到1280对比测试。注意训练尺寸和推理尺寸不需要完全一致可以训练用640、推理用1280只是推理耗时翻倍现场要提前评估。batch size取决于显存。以yolov5s为例12GB显存的卡可以跑batch 328GB建议batch 16。batch太小小于8会导致BN层统计不稳定训练早期loss波动特别大。学习率直接用默认的hyp.scratch-low.yaml就行红绿灯不是那种特殊到要魔改学习率的任务。真正值得动的是数据增强参数我一般会把mosaic设为1.0默认开启把小目标的复制粘贴增强打开让模型在训练时多看到小尺寸灯体。数据增强里有一个参数要关掉或者调小hsv_h、hsv_s、hsv_v这三个颜色抖动参数。红绿灯分类极度依赖颜色信息颜色抖动太强会让红色样本在增强后变成橙色甚至紫色干扰模型学习“红色红灯”这个本质特征。默认hyp里的hsv_h是0.015、hsv_s是0.7建议把hsv_s降到0.3左右hsv_h保持0.01以下。这个调整的收益在夜间样本多的数据集上非常明显。3.4 训练命令与loss监控怎么判断模型在正常收敛数据准备好后训练命令非常简单。如果你用的是官方yolov5仓库ultralytics/yolov5在项目根目录执行conda activate yolov5 cd yolov5 python train.py \ --data traffic_light.yaml \ --weights yolov5s.pt \ --img 640 \ --epochs 100 \ --batch-size 16 \ --device 0 \ --hyp data/hyps/hyp.scratch-low.yaml \ --project runs/traffic_light第一行先激活你创建好的conda环境环境里需要装好pytorch、torchvision和opencv-python。--weights用yolov5s.pt表示基于COCO预训练权重做迁移学习不要去下载一个随机初始化的权重从头训练红绿灯数据集没那么大从头训很容易欠拟合。训练开始后怎么判断模型在正常收敛看两个东西。第一是终端里打印的box_loss和cls_loss正常情况下两者都应该是逐步下降的曲线如果有反弹但不超过峰值两倍也没事。第二是看runs/traffic_light/exp/results.png里自动生成的loss曲线图训练到后半段60%以上epochbox_loss应该稳定在一个小范围内震荡而不是持续上升。如果loss一直在降但验证集mAP不涨大概率是过拟合了把epochs减半或者加大数据增强。训练结束后best.pt和last.pt会存在runs/traffic_light/exp/weights/下。best.pt是验证集mAP最高的权重部署时用这个。last.pt是最后一轮的权重一般用不上但可以在best.pt丢失时救急。4. OpenCV后处理与实时信号输出把检测框变成可信的灯态4.1 推理管线四步过滤、裁剪、HSV验证、时序投票模型训练好之后只是第一步真正决定现场能不能用的是OpenCV后处理。我把推理管线拆成四步每一层都在消除上一层的错误输出。第一步是置信度过滤。yolov5会输出所有置信度大于阈值的框红绿灯场景建议把conf阈值设在0.35到0.5之间。阈值太低会混入大量背景误检阈值太高夜间小目标会漏检0.4是一个大多数场景下平衡得比较好的起点。还要限制每帧最多输出20个框防止画面里路灯、车灯、广告牌全被当成灯组。第二步是类别与位置过滤。红绿灯只可能出现在画面的上半部分把整帧的下三分之一直接裁掉不检测能砍掉一大半红色刹车灯的干扰。这个ROI限定的收益非常大几乎零成本。第三步是HSV颜色验证。对每个检测框内的图像区域转到HSV空间按类别对应的颜色范围做阈值分割统计颜色像素占比。占比超过某个阈值才认定灯确实是这个颜色。这一步能把“框对了但颜色分错”的误检拦下来。第四步是时序投票。单帧判断不可靠用一个长度5到10帧的滑动窗口只有窗口内同一状态出现次数超过60%才切换输出信号。这样即使某一帧因为逆光或遮挡判断失败输出信号也不会抖动。4.2 HSV颜色阈值的参数表为什么RGB在这里不可靠在RGB空间里判断颜色遇到的最大问题是亮度耦合。同一个红色在白天和夜晚的RGB值差别巨大你用(200, 0, 0)近似红色白天成立夜晚灯体过曝后RGB变(255, 255, 255)就完全失效了。HSV把色调(H)、饱和度(S)、亮度(V)分开判断颜色只用H和SV只做辅助约束对亮度变化不敏感这是红绿灯场景必须用HSV的根本原因。以OpenCV默认的H范围0到180为基准我给一组经过实际项目验证的阈值参数颜色H范围S范围V范围备注红0-10 或 156-18080-25580-255红色在H上跨越0度两端绿35-8560-25560-255下界放宽避免暗绿丢失黄15-3480-25580-255橙色和黄灯共享此区间红色要特别处理在HSV色环上红色位于0度和180度交界处所以需要两个区间做“或”操作。绿和黄之间、黄和红之间都有过渡地带实际标定时要对着你摄像头的实拍画面微调S下界——S下界定太低会把灰色路灯误判成绿色定太高夜间红色会丢失。V下界80是为了过滤掉纯黑背景夜间暗光下如果灯体亮度不足可以下调到60甚至50。4.3 灯圈圆形度验证与黄灯闪烁状态机HSV颜色验证通过之后还有一个高风险误判源红色车尾灯、红色刹车灯、红色广告牌它们的HSV特征和红灯几乎一样。要区分它们靠形状。真实红绿灯灯圈是接近正圆的车尾灯通常是矩形或条带状。用OpenCV轮廓分析算圆形度能把非圆目标剔除掉。import cv2 import numpy as np def is_circle_like(roi_mask, min_area20): # roi_mask: 二值mask灯体发光区域为白色 contours, _ cv2.findContours( roi_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) if not contours: return False c max(contours, keycv2.contourArea) area cv2.contourArea(c) perimeter cv2.arcLength(c, True) if area min_area or perimeter 0: return False # 圆形度 4 * pi * 面积 / 周长^2正圆为1 circularity 4 * np.pi * area / (perimeter * perimeter) return circularity 0.65圆形度大于0.65是一个经验值。正圆的圆形度是1.0真实灯圈因为过曝、光晕和边缘锯齿一般落在0.7到0.9之间。低于0.65的物体大概率是矩形尾灯或条状灯带。min_area设20像素是为了过滤掉远距离小目标在mask上的孤立噪点。如果目标确实很小比如只有15x15像素mask面积会很小圆形度波动大可以适当下调min_area。黄灯闪烁是红绿灯识别里最容易被忽略的时序问题。黄灯在闪烁时单帧上看起来就是黄色灯亮和普通黄灯没有区别。如果不做时序处理系统会在“黄灯亮-黄灯灭-黄灯亮”之间反复跳变。处理思路是增加一个状态机检测到黄灯亮时不立即输出黄灯状态而是记录黄灯亮的连续帧数如果黄灯在短时间内比如2秒内亮灭交替超过3次判定为闪烁输出“黄灯闪烁”状态而非“黄灯常亮”。闪烁状态下车辆通行规则不同这个输出对下游决策模块很重要。4.4 OpenCVYOLO跑通的完整推理代码把上述逻辑整合成一段可运行的推理代码。这里用torch.hub加载训练好的权重OpenCV负责视频读取和颜色验证。import cv2 import numpy as np import torch # 加载训练好的模型权重conf和iou按场景调 model torch.hub.load(ultralytics/yolov5, custom, pathruns/traffic_light/exp/weights/best.pt, force_reloadFalse) model.conf 0.4 model.iou 0.45 model.max_det 20 def check_light_color(crop_bgr, cls_id): 按类别做HSV颜色验证返回颜色像素占比 hsv cv2.cvtColor(crop_bgr, cv2.COLOR_BGR2HSV) if cls_id 0: # red mask cv2.inRange(hsv, (0, 80, 80), (10, 255, 255)) \ cv2.inRange(hsv, (156, 80, 80), (180, 255, 255)) elif cls_id 1: # green mask cv2.inRange(hsv, (35, 60, 60), (85, 255, 255)) else: # yellow mask cv2.inRange(hsv, (15, 80, 80), (34, 255, 255)) if crop_bgr.size 0: return False ratio cv2.countNonZero(mask) / (crop_bgr.shape[0] * crop_bgr.shape[1]) return ratio 0.12 # 从视频文件读取也可以用摄像头VideoCapture(0) cap cv2.VideoCapture(test_night.mp4) while True: ret, frame cap.read() if not ret: break # 只对上半部分做检测排除车尾灯干扰 roi frame[:int(frame.shape[0] * 0.7), :] results model(roi) for *xyxy, conf, cls in results.xyxy[0].cpu().numpy(): x1, y1, x2, y2 map(int, xyxy) crop roi[y1:y2, x1:x2] if crop.size 0: continue if not check_light_color(crop, int(cls)): continue # 通过颜色验证的框才画出来 cv2.rectangle(roi, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(roi, fcls{int(cls)} {conf:.2f}, (x1, y1 - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow(traffic_light, roi) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()代码里需要注意的点裁剪框crop是从roi上切出来的所以画框也要画在roi上否则显示时位置错位。check_light_color返回的ratio阈值0.12表示发光像素占框内面积12%以上才认定为真实灯体。这个值是我在不同路口测出来的如果你的摄像头分辨率高、框框得紧ratio可以调到0.15如果框框得松、包含背景多0.08更合适。阈值太低会把颜色相近的路灯误判成灯组太高会让夜间过曝的灯因为中心变白而丢失。5. 红绿灯识别系统的五类踩坑现象、原因与排查顺序5.1 ModuleNotFoundError: No module named cv2现象训练或推理脚本第一行import cv2就报错提示No module named cv2。这是最常见的环境问题尤其在用conda创建新环境后。原因conda新环境里没有安装opencv-python包。也有一种情况是装了opencv-python但import时报libGL.so.1错误那是因为系统缺libgl1库不是Python层的问题。解决先执行pip install opencv-python装完后测试python -c import cv2; print(cv2.__version__)。如果报libGL.so.1在Ubuntu/Debian上执行apt install libgl1 -yCentOS上执行yum install libglvnd-glx。顺带提醒不要同时装opencv-python和opencv-contrib-python两个包会互相冲突覆盖文件。只需要基础功能就装opencv-python需要SIFT等contrib功能再换装opencv-contrib-python。5.2 夜间过曝导致HSV阈值失效现象夜间红灯在画面里变成白心红边的圆斑颜色像素占比低于0.12本来应该识别出来的红灯被过滤掉或者更糟过曝严重的红灯整体变白HSV验证直接判定为“非红”。原因灯体中心过曝后红色像素被高光冲成白色HSV里S值降到很低接近0不满足S80的阈值条件。模型分类是对的但OpenCV验证这一层把正确结果拦掉了。解决颜色验证时对红色增加一个“高亮模式”——如果框内存在大量高亮像素V200, S30则检查这些高亮像素周围一圈是否有红色像素。实现上可以只对高亮区域的边界做5像素宽的环形采样统计红色占比。这个环形采样逻辑用OpenCV的dilate和subtract就能实现不要为了这种场景去引入大模型。另一个办法是降低V和S的下限但会引入更多误检得不偿失。5.3 多车道灯组串扰与误检现象路口有多组红绿灯分别控制不同方向车道。模型经常把对向车道的左转灯识别成当前车道的直行灯导致输出信号和实际路权不符。原因yolov5只检测灯的存在和位置不理解“哪个灯属于哪条车道”。当画面里同时出现多组灯时检测框可能跨越两个灯组或者把相邻灯组的部分像素圈进同一个框。解决分两步。第一步用ROI限定每个灯组出现的画面区域——在标定阶段把画面中每个灯组的位置固定成独立ROI检测结果必须落在对应ROI内才被接受第二步检测框的y坐标中心点落在哪个ROI就归属那个灯组避免交叉匹配。这个方法要求摄像头位置固定对车载场景不适用车动了ROI要跟着动一般用车道线检测结果动态调整ROI的y范围。5.4 摄像头白平衡导致颜色偏移现象同一套HSV阈值在A摄像头上正常识别换到B摄像头后红灯检测率骤降绿色和黄色互相混淆。原因不同摄像头的自动白平衡算法差异很大。有些摄像头会把红色场景整体向橙色偏移有些在黄昏时把绿色压暗成墨绿。HSV的H分量对色调偏移很敏感H偏10度就能让红色从区间里滑出去。解决给你的系统增加一个“摄像头标定模式”。部署后让现场人员对着标准色卡或者固定红色物体拍一段视频程序计算该摄像头下红色和绿色的H分布中心自动把HSV阈值整体平移。这是最通用的做法。应急情况下把H区间放宽到红0-15和150-180绿30-90但放宽后误检率会明显上升不是长久之计。注意训练数据里多收集不同摄像头的图片让模型本身对色偏不敏感也是治本的办法。5.5 小目标漏检与anchor尺寸不匹配现象验证集mAP看着还行但视频里30米外的灯组几乎全漏近距离灯组正常。原因yolov5的anchor是自适应计算的默认针对COCO目标分布。红绿灯普遍偏小默认anchor在小尺寸目标上覆盖不足小目标的检测头P3层没有分配到足够的正样本。解决训练时让yolov5自动重新聚类anchor。在train.py中加入--multi-scale参数并在数据增强里开启复制粘贴增强让模型看到更多小目标。如果数据集中小目标占比不高可以先对所有标注框做一次统计宽高小于32像素的框占多少比例。占比低于20%时建议在训练时对图片做随机裁剪放大把小目标放大后再喂给模型。推理端不要用640直接提到1280代价是帧率降一半但在路侧固定摄像头场景通常可以接受。6. 评估指标曲线怎么读以及部署前的三个验证技巧6.1 mAP与PR曲线哪些指标值得现场关注yolov5训练完会在结果目录里生成results.png、PR_curve.png、confusion_matrix.png等评估图。results.png里最核心的是mAP_0.5和mAP_0.5:0.95两条曲线。mAP_0.5是IoU阈值0.5下的平均精度模型“框得差不多就算对”适合衡量整体检测能力mAP_0.5:0.95是IoU从0.5到0.95的平均值对框的定位精度要求更高。红绿灯场景mAP_0.5达到0.9以上是及格线mAP_0.5:0.95能到0.6就够用因为下游的OpenCV验证阶段并不依赖精确定位的框框大致把灯圈住就行。PR_curve.png里每条曲线看P查准率和R查全率的权衡。红绿灯项目优先保证查全率——漏掉一个红灯可能直接导致车辆闯红灯后果远严重于把刹车灯误判成红灯。所以部署时conf阈值要往低调看到P下降一点但R保持高位是可以接受的。混淆矩阵重点看red和green是否互相串如果红色大量被预测成绿色基本可以判断是训练数据里两类样本的亮度或色偏分布差异过大需要补数据而不是调阈值。6.2 用ONNXOpenCV做推理加速部署torch.hub加载模型方便但不适合生产部署每次启动都要初始化torch环境树莓派5这类低算力设备上跑640推理还勉强跑1280就很吃力。常见做法是导出ONNX用onnxruntime推理。导出命令cd yolov5 python export.py --weights runs/traffic_light/exp/weights/best.pt --include onnx --opset 12导出后用一段简短的onnxruntime推理代码import cv2 import numpy as np import onnxruntime as ort sess ort.InferenceSession(best.onnx, providers[CPUExecutionProvider]) input_name sess.get_inputs()[0].name def infer_frame(frame_bgr): img cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB) img cv2.resize(img, (640, 640)) / 255.0 blob img.transpose(2, 0, 1)[None].astype(np.float32) out sess.run(None, {input_name: blob})[0] # out shape: [1, 25200, 5nc]需按yolov5格式解析后处理 return outonnxruntime在树莓派5上的推理速度yolov5s 640输入大约能跑到15到20FPS比torch直接推理快接近一倍。如果要压到实时30FPS就得走TensorRTNVIDIA平台或OpenVINOIntel平台这两个推理框架对yolov5都有官方支持导出步骤和ONNX类似但涉及到层融合和FP16量化这里不展开。6.3 现场验证三板斧视频回放、ROI限定、计时统计部署前必须做三轮验证。第一轮用录制的视频回放测试不要直接用摄像头实时调试——视频可以反复回放同一帧定位问题效率高得多。准备白天的视频、黄昏的视频、夜间的视频各一段每段至少5分钟统计每一帧的检测结果和漏检帧数算出真实场景下的漏检率和误检率。第二轮做ROI限定验证。在测试视频里手动标注几个灯组的固定位置然后对比加了ROI和没加ROI的误检情况。这一步能量化排除对向车道干扰的收益如果加了ROI后误检率下降了50%以上说明ROI方案值得保留如果下降不明显说明干扰源不在ROI之外查HSV阈值方向。第三轮是计时统计。在推理代码里用cv2.getTickCount记录从读取帧到输出灯态的总耗时统计500帧的平均延迟。红绿灯识别场景延迟200ms以内可以接受超过300ms就要考虑换推理框架或降输入分辨率。延迟超标的瓶颈通常不在模型而在OpenCV预处理和HSV验证的逐像素操作检查一下是不是在cvtColor和inRange上花了太多时间。我自己习惯把这三轮验证写成三个独立脚本每次改完模型参数就全跑一遍把结果记录在一个markdown表格里。红绿灯识别是一个“看起来简单、部署全是坑”的方向灯光过曝、颜色偏移、小目标漏检这些坑都只能靠反复验证打磨掉没有捷径。这套数据的准备、训练、OpenCV验证、部署加速的流程是当前性价比最高的路线。希望帮到你。本文还有配套的精品资源点击获取