简介:基于YOLOv5与OpenCV构建的道路红绿灯识别检测系统,面向计算机视觉入门及进阶学习者,可完成红灯、绿灯、黄灯、交通灯四类目标的精准检测识别。资源包含完整训练与推理源码、预训练模型权重、评估指标曲线及使用说明,覆盖从环境配置到模型评估的完整流程,适合作为目标检测实战项目参考。压缩包共79个文件,以py源码、pyc编译文件、yaml配置、pt模型文件为主,辅以jpg/png结果图像、sh脚本、txt说明等,整体大小约41.68MB,结构清晰,便于按需取用。训练过程迭代200次,附带loss下降曲线、Recall召回率、precision精确度及mAP等评估图表,模型拟合较好,可直接用于道路场景下的红绿灯识别实验。目前已有1487人学习下载,其中红绿灯数据集可另行获取,遇到使用问题也可与作者沟通交流。
1. yolov5+opencv 道路红绿灯识别:这套组合的真实分工与适用场景
去年做一个路侧红绿灯识别项目,摄像头装在路口杆件上,正对对向车道,最近的一排灯只有15米,最远的掉头灯在50米外。夜间对面车灯一照,灯体直接过曝成一片白斑,纯yolo模型十帧能漏八帧。后来把方案改成基于yolov5+opencv实现道路红绿灯识别检测:yolov5负责在整帧中定位灯组并给出粗分类,OpenCV负责对检测框做颜色验证、灯圈形状校验和连续帧时序投票。这套组合适合正在做辅助驾驶、车路协同或交通监控的工程师和学生项目,读完这篇笔记,你可以从数据集准备一路走到带评估指标曲线的可部署系统,同时避开夜间过曝和多车道串扰这两个现场最常见的坑。
2. 为什么是yolov5+opencv而不是端到端模型:选型逻辑与场景失效分析
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,移动场景必须yolo+opencv混合;yolov5是当前综合成本最低的检测器选择。模型尺寸优先试yolov5s,显存紧张再换yolov5n,精度不够再上yolov5m。
2.3 红绿灯为什么总让检测模型翻车:小目标、过曝和颜色失真
先说小目标。距离30米时,一个标准红绿灯灯体在1080P画面里大约只有30x30像素,占据整帧面积的千分之一。yolov5在640x640推理尺寸下,这个目标经过三次下采样后特征图上的响应非常弱,漏检率天然偏高。解决方向有两个:一是把推理尺寸提到1280,代价是速度几乎减半;二是训练时用mosaic增强和复制粘贴增强让模型见惯小目标。具体怎么调后面章节细说。
过曝是红绿灯场景独有的难题。夜间红灯亮起时,CMOS传感器为了兼顾周围暗部会拉长曝光时间,结果灯体中心变成纯白色,只有边缘一圈是红色。模型训练数据里如果缺少这种过曝样本,推理时就会把红灯认成白灯或者直接漏掉。处理办法不是去改模型,而是在OpenCV验证阶段同时接受“纯红”和“白心红边”两种模式,对白色中心区域周围做环形颜色采样。
颜色失真是另一个隐蔽问题。不同厂家摄像头的白平衡策略差异很大,同一个红灯在A品牌摄像头下是正红色,在B品牌下偏橙,在C品牌下偏紫。如果HSV阈值只按标准红色标定,换个摄像头立刻失效。后面第5章会专门讲怎么处理,这里先提一句:阈值要留余量,并且要做多摄像头标定。
3. 训练自己的红绿灯数据集:从采集标注到yolov5超参数调优
3.1 数据集怎么来:公开数据集打底,自采数据补场景
红绿灯检测不像行人检测那样有海量公开数据可用,但也不是完全没有。业界常用的公开数据集有BSTLD(Bosch 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 32,8GB建议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%以上epoch),box_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和S,V只做辅助约束,对亮度变化不敏感,这是红绿灯场景必须用HSV的根本原因。
以OpenCV默认的H范围(0到180)为基准,我给一组经过实际项目验证的阈值参数:
| 颜色 | H范围 | S范围 | V范围 | 备注 |
|---|---|---|---|---|
| 红 | 0-10 或 156-180 | 80-255 | 80-255 | 红色在H上跨越0度两端 |
| 绿 | 35-85 | 60-255 | 60-255 | 下界放宽避免暗绿丢失 |
| 黄 | 15-34 | 80-255 | 80-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_area=20): # roi_mask: 二值mask,灯体发光区域为白色 contours, _ = cv2.findContours( roi_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) if not contours: return False c = max(contours, key=cv2.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 OpenCV+YOLO跑通的完整推理代码
把上述逻辑整合成一段可运行的推理代码。这里用torch.hub加载训练好的权重,OpenCV负责视频读取和颜色验证。
import cv2 import numpy as np import torch # 加载训练好的模型权重,conf和iou按场景调 model = torch.hub.load('ultralytics/yolov5', 'custom', path='runs/traffic_light/exp/weights/best.pt', force_reload=False) 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, f"cls{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 -y;CentOS上执行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),不满足S>80的阈值条件。模型分类是对的,但OpenCV验证这一层把正确结果拦掉了。
解决:颜色验证时对红色增加一个“高亮模式”——如果框内存在大量高亮像素(V>200, S<30),则检查这些高亮像素周围一圈是否有红色像素。实现上可以只对高亮区域的边界做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 用ONNX+OpenCV做推理加速部署
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, 5+nc],需按yolov5格式解析后处理 return outonnxruntime在树莓派5上的推理速度,yolov5s 640输入大约能跑到15到20FPS,比torch直接推理快接近一倍。如果要压到实时30FPS,就得走TensorRT(NVIDIA平台)或OpenVINO(Intel平台),这两个推理框架对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验证、部署加速的流程,是当前性价比最高的路线。希望帮到你。
本文还有配套的精品资源,点击获取