简介一份基于Python与OpenCV的红绿灯检测识别项目资源面向计算机视觉、智能交通及自动驾驶相关领域的学生与开发者。项目围绕信号灯检测的完整流程展开涵盖图像预处理、直方图均衡化、BGR转HSV色彩空间分离红绿区域、轮廓检测与霍夫变换定位灯型并支持结合传统机器学习或深度学习模型完成分类识别。资源包共12个文件以Python脚本为核心实现附带Markdown说明文档及多张测试图片压缩包仅343KB内容轻量且目录结构清晰。检测脚本中专门定义了信号灯状态模块示例图片覆盖圆灯、箭头灯、黄闪灯等常见形态方便读者直接运行并验证不同场景下的检测效果。目前已有4000余人学习/下载适合希望快速上手OpenCV视觉识别、理解信号灯检测工程化实现思路的读者作为实操参考。1. 红绿灯检测为什么值得先用 OpenCV 手工特征试一次如果把红绿灯检测直接交给深度学习很多人会忽略一件事在跑模型之前一个「找灯色 排除干扰」的预处理流程能省掉大半误检。实际路侧或车端场景里红绿灯检测比想象中更容易翻车——夜里对向远光灯一照红灯在画面里只剩一块模糊的色块前车刹车灯一亮整片区域都是红色候选。用 Python 和 OpenCV 做红绿灯信号灯检测最常用、也最容易跑通的路线不是一步上网络而是先用 HSV 颜色分割把灯色抠出来再用轮廓筛选、ROI 先验和多帧投票把误检压下去。这套做法只用 CPU 就能跑一帧几十毫秒适合驾驶辅助原型、车路协同 Demo也适合刚入门 OpenCV 想做一个完整图像处理项目的从业者。下面把原理、可复现代码和踩坑点一次讲清楚。2. 红绿灯识别的视觉基础HSV 颜色空间与灯板 ROI 提取2.1 为什么选 HSV 而不是 RGB颜色聚类与光照的关系红绿灯的灯头是高度饱和的人造光源颜色非常纯这给了颜色阈值方案发挥空间。但直接用 RGB 做阈值效果通常很差光线变化时RGB 三个通道会一起涨一起跌红灯可能在白天是(200, 30, 30)到了傍晚变成(120, 20, 20)你按白天调好的阈值去切傍晚就什么都切不出来。HSV 把色相H、饱和度S、明度V拆成三个独立维度其中 H 通道描述「这是什么颜色」对光照相对不敏感红绿灯这种高饱和光源在 H 通道里聚类非常紧。这里有一个非常容易踩的坑OpenCV 里的 H 范围是 0 到 179不是很多教材里写的 0 到 360。从其他语言或教程抄来的阈值直接套在cv2.inRange上会整体偏一倍这是新手经常调不出红色的首要原因。另一个坑是红色在 HSV 色环上横跨两端H 接近 0 和 H 接近 180 都是红所以红色需要两个区间再用bitwise_or合并。如果只写一个区间深红、偏紫的红灯很容易漏检。实践中最常见的做法并不是「纯手工特征」和「深度学习」二选一而是混合方案用检测网络框出灯板位置灯色判断依然交给 HSV。因为灯板定位需要处理尺度、遮挡适合交给网络而灯色判断只是「在一小块区域里比三种颜色的面积」手工特征又快又直白没必要为这个动作再跑一次分类模型。2.2 固定相机先框灯板 ROI再分割灯色红绿灯检测的误检来源绝大多数不是「没找到灯」而是「找到了太多像灯的东西」——红色尾灯、红色招牌、红色车漆都会在阈值分割后变成候选块。压误检最有效的一招不是调阈值而是缩小搜索范围。固定安装的路侧相机或车端相机信号灯在画面里的位置基本不变车端相机在一条固定车道上行驶时红绿灯永远出现在画面上半部的一定区间内。这时候直接用静态 ROI 把搜索框死比如 640x360 的画面ROI 取(0, 60, 640, 180)意思是把顶部 60 像素的天线与高楼区域切掉只搜 60 到 240 这个带状区域。这个矩形足够容纳一组垂直排列的红黄绿灯头也正好把画面下半部可能出现的刹车灯、尾灯全部挡在搜索范围外。动态 ROI 的思路也值得了解如果车在爬坡或下坡信号灯在画面里的纵向位置会上下浮动静态 ROI 可能把灯切出区域。一个简单的做法是上一帧检测到灯板候选框后下一帧在候选框周围扩大 1.5 倍作为搜索区域再配合全图每隔一段时间做一次「重新定位」。对原型项目来说先写静态 ROI 就够了等你在真实路口跑出误检再考虑动态化。2.3 探针脚本鼠标点一下看像素的真实 HSV 值调 HSV 阈值最忌凭空猜数字。我一般会先写一个探针脚本把图片显示出来鼠标点击画面任意位置直接打印该像素的 BGR 和 HSV 值。这样你能看到真实画面里红灯到底是什么 H、白天和夜里的 S、V 差多少阈值设置立刻从玄学变成查表。import cv2 img cv2.imread(traffic_light.jpg) hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) def on_click(event, x, y, flags, param): if event cv2.EVENT_LBUTTONDOWN: bgr img[y, x] hsv_px hsv[y, x] print(BGR:, bgr, HSV:, hsv_px) cv2.imshow(image, img) cv2.setMouseCallback(image, on_click) cv2.waitKey(0) cv2.destroyAllWindows()这段代码的逻辑很直接提前把整幅图转成 HSV鼠标点击时用坐标(y, x)索引图像矩阵把该点的 BGR 和 HSV 一起打印。参数说明EVENT_LBUTTONDOWN是鼠标左键事件cv2.setMouseCallback把回调函数绑定到指定窗口img[y, x]返回的是 BGR 三元组而不是 RGB打印时心里要有数。我建议你拿一张真实的路口照片分别点一下红灯灯芯、灯板黑边、旁边树叶和路面你会立刻理解为什么 H 来决定颜色、S 和 V 用来排除暗色和过曝。这个探针总共不到十行但能帮你省掉一整个下午的盲目调参。3. 用 OpenCV 实现红绿灯检测单帧代码与参数调优3.1 单帧红绿灯检测的可运行代码先给出一份单帧检测的完整实现。它做的事情是裁出 ROI、做一次高斯模糊、转 HSV、分别对红绿黄三色做阈值分割、形态学去噪、找轮廓、按面积和形状筛选、输出当前帧的灯色与候选框。import cv2 import numpy as np # 颜色阈值定义为元组OpenCV 会自动转成需要的格式 RED_1 ((0, 80, 80), (10, 255, 255)) # 红色区间一偏橙红 RED_2 ((156, 80, 80), (180, 255, 255)) # 红色区间二偏紫红 GREEN ((40, 80, 80), (85, 255, 255)) # 绿灯 YELLOW ((15, 80, 100), (35, 255, 255)) # 黄灯V 下限定高一点防止暗色误检 def mask_for(hsv, ranges): masks [cv2.inRange(hsv, lo, hi) for lo, hi in ranges] mask masks[0] for m in masks[1:]: mask cv2.bitwise_or(mask, m) return mask def pick_light(mask, min_area30): contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) best None for cnt in contours: area cv2.contourArea(cnt) if area min_area: continue x, y, w, h cv2.boundingRect(cnt) if not (0.7 w / h 1.4): # 灯头接近圆形长宽比接近 1 continue perimeter cv2.arcLength(cnt, True) if perimeter 0: continue circularity 4 * np.pi * area / (perimeter * perimeter) if circularity 0.6: # 圆度太低说明形状不像灯头 continue if best is None or area best[0]: best (area, (x, y, w, h)) return best def detect_frame(frame, roi): x, y, w, h roi crop frame[y:y h, x:x w] crop cv2.GaussianBlur(crop, (5, 5), 0) hsv cv2.cvtColor(crop, cv2.COLOR_BGR2HSV) scores {} boxes {} for name, ranges in ((red, (RED_1, RED_2)), (green, (GREEN,)), (yellow, (YELLOW,))): mask mask_for(hsv, ranges) kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) picked pick_light(mask, min_area30) if picked: scores[name] picked[0] boxes[name] picked[1] if not scores: return none, None color max(scores, keyscores.get) bx, by, bw, bh boxes[color] return color, (x bx, y by, bw, bh) # 主流程读取图片检测并画框 frame cv2.imread(traffic_light.jpg) roi (0, 60, 640, 180) # x, y, w, h按自己画面调整 color, box detect_frame(frame, roi) if box: x, y, w, h box cv2.rectangle(frame, (x, y), (x w, y h), (0, 255, 0), 2) cv2.putText(frame, color, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) cv2.imshow(result, frame) cv2.waitKey(0) cv2.destroyAllWindows()逻辑说明分成四步ROI 裁切与高斯模糊是为了去掉传感器噪点和路面细节HSV 三色分割中红色必须用两个区间合并形态学开运算把分割结果里的散点噪点消掉轮廓筛选用面积、长宽比、圆度三个条件把「像灯头」的候选挑出来。注意findContours在 OpenCV 4.x 里返回两个值在 3.x 里返回三个值代码里写contours, _ ...如果你还在用 3.x要改成_, contours, _ ...这是最常见的版本兼容报错。3.2 红绿黄三色的 HSV 阈值表下面这组阈值是我在多个路口画面上试出来的一组保守初值适合白天到傍晚背光不严重的场景。它不一定是你最终值但足够让你跑出第一个可用结果后续用标定工具修正。颜色H 下限H 上限S 下限V 下限说明红区间一0108080橙红、正红红区间二1561808080偏紫红必须与区间一合并绿40858080正绿、青绿都覆盖黄153580100V 下限高一点避免暗黄色误检S 下限和 V 下限都取了 80这个值对白天画面比较友好。如果画面里红色物体大面积误检优先把 S 下限抬到 100 以上因为红绿灯的饱和度高普通物体很少能到 100。如果夜里路面反光很强V 下限也可以抬但小心别把暗光下的红灯切没了。H 上限和下限尽量别动除非你已经明确通过探针脚本知道具体灯色偏离了标准值。3.3 三个关键筛选参数面积、宽高比与圆度轮廓筛选是整个方案里最值得花时间调的三个参数。第一个是min_area30 这个值在 640 宽的画面里大约对应一个 6x6 像素的灯芯分辨率更高的画面可以按比例放大到 60 或 100。第二个是长宽比w / h灯头是圆形实际因为运动模糊和镜头畸变会变成椭圆0.7 到 1.4 这个范围能容忍一定形变又不会把背板矩形或倒计时数字放进来。第三个是圆度计算公式是4 * pi * area / (perimeter^2)正圆的圆度为 1倒了 0.6 作为下限三角形、细长条都会被过滤掉。这三个参数组合起来的意义是红绿灯灯芯在画面里一定是「面积适中、接近圆形、比例不怪异」的块。倒计时数字和文字型 LED 通常形状细碎、长宽比夸张过不了宽高比这一关远处的小块噪点面积不够过不了面积关光线反射造成的不规则亮斑圆度差过不了圆度关。实际调参时建议先把三个参数打印出来看真实数值而不是凭感觉改这一步会让你少走很多弯路。3.4 接上视频流逐帧检测与状态输出图片跑通了接视频流只是把循环包起来的问题。这里给一个稳定度较高的视频处理框架逻辑、参数说明都在代码注释里。import cv2 cap cv2.VideoCapture(road.mp4) roi (0, 60, 640, 180) while True: ret, frame cap.read() if not ret: break color, box detect_frame(frame, roi) if box: x, y, w, h box cv2.rectangle(frame, (x, y), (x w, y h), (0, 255, 0), 2) # 把识别结果叠在画面上color 可以是 red/green/yellow/none text_color {red: (0, 0, 255), green: (0, 255, 0), yellow: (0, 255, 255)}.get(color, (255, 255, 255)) cv2.putText(frame, color, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, text_color, 2) cv2.imshow(frame, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这段代码的核心是ret, frame cap.read()ret为 False 表示视频读完了或者摄像头掉线必须立刻跳出循环否则程序会卡在空帧上。cv2.waitKey(1)的参数是等待毫秒数1 表示每次循环只等 1 毫秒控制视频播放速度的其实是视频本身的帧率不是这个参数。如果你用的是 USB 摄像头把road.mp4换成摄像头索引0即可但要注意某些摄像头初始帧率不稳定可以在detect_frame前加一个跳帧逻辑if frame_id % 2 0: continue牺牲一半帧率换取更稳定的处理节奏。在 PyCharm 里跑的时候记得把解释器指到装好 opencv-python 的虚拟环境命令行能import cv2、IDE 里却报红绝大多数是环境没选对的问题。新手最常见的另一个报错是ModuleNotFoundError: No module named cv2检查一下安装命令是不是写成了pip install opencv正确包名是opencv-python。4. 红绿灯检测避坑过曝、刹车灯与倒计时干扰的排查记录4.1 红灯变成白色灯芯LED 过曝让红色区域消失现象白天或夜间红灯检测结果里红色的掩码面积突然变得很小灯芯中央变成了白色只有外圈还带一点红色导致contourArea不够、候选框丢失。把这一帧单独存下来看红灯中心确实是一片接近白色的高光。原因LED 信号灯亮度很高相机自动曝光会把整个画面拉亮灯芯中心区域在 HSV 空间里表现为 H 值漂移、S 急剧下降色度信息被「冲白」了。颜色分割只认 S 高的点白色灯芯自然切不出来。解决方式分两步。第一步是别指望把白色灯芯也切出来只统计外圈红边就够判定「这是一个红灯」因为外圈颜色信息没有被完全冲掉。第二步是对 ROI 做一次局部对比度增强把过曝区域的色彩信息拉回来。常见做法是 CLAHE只对 LAB 空间的 L 通道做处理避免影响 H 和 Simport cv2 import numpy as np # 对 ROI 做 CLAHE 增强再转 HSV 做颜色分割 lab cv2.cvtColor(crop, cv2.COLOR_BGR2LAB) clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) lab[:, :, 0] clahe.apply(lab[:, :, 0]) crop cv2.cvtColor(lab, cv2.COLOR_LAB2BGR)clipLimit是对比度限制2.0 是常用初值越大增强越剧烈过大会把噪点一起放大tileGridSize是分块大小8x8 在 ROI 尺寸不大时足够。加了这一层之后过曝灯芯的红色外圈会重新变得饱和检测成功率明显回升。4.2 前车刹车灯和路边红色招牌被当成红灯现象画面上半部 ROI 里出现一个面积达标的红色圆块程序判断为红灯但实车一看是前车刹车灯刚好进了 ROI或者路边招牌上有个红色圆形 logo。原因刹车灯和信号灯的 HSV 特征几乎一样都是高饱和红色圆形光源单靠颜色和形状根本没法定性区分。唯一的区别是它们出现在画面里的位置——信号灯在车道上方刹车灯在车道中央高度招牌则可能在画面边缘。解决最常见做法是把 ROI 继续收窄。车端相机画面里信号灯通常位于水平中线偏上、画面中央偏左或偏右的区域把 ROI 的纵向范围从(0, 60, 640, 180)收成(0, 80, 640, 120)直接把可能包含刹车灯的中下部区域切掉。路侧相机则更简单灯板位置几乎像素级固定ROI 可以缩到刚好包住灯板的 1.2 倍。千万别小看这个「粗笨」手段在真实路口ROI 先验比任何筛选算法贡献都大。还有一个辅助手段是看候选框的亮度闪烁信号灯不会以 10Hz 以上的频率闪烁刹车灯在刹车时是持续点亮行驶中则是「亮一下、灭一下」的高频变化这个特征留给多帧投票去处理单帧阶段不纠结。4.3 绿色倒计时数字被当成绿灯现象绿灯检测经常在「绿灯」和「无灯」之间反复横跳仔细看掩码才发现绿色倒计时数字的笔画被切出来了轮廓面积和真实绿灯相当甚至更大。原因很多路口信号灯是「绿灯倒计时 灯芯」一体设计绿色的数码管倒计时和绿灯同色同源HSV 分割必然同时命中而倒计时数字笔画更长轮廓面积往往超过灯芯max(scores, keyscores.get)会把倒计时误判成主灯色。解决把筛选条件从「面积大」改成「面积适中 形状接近圆形」。倒计时数字是离散笔画整体轮廓细长宽高比往往超过 2真实绿灯是一个紧凑的圆形灯头宽高比接近 1圆度也明显更高。我给pick_light加宽高比条件之后倒计时干扰基本消除。如果路口是红黄绿三灯垂直排列你还可以加一个位置约束在 ROI 内绿灯灯头一定在中间位置或下方固定位置倒计时数字则靠近画面边缘用boundingRect的中心坐标做一次区间判断把所有中心点落在异常位置的候选直接丢弃。这一步放在轮廓筛选里按 ROI 的高度百分比写死即可不怎么费计算量。4.4 白天调好的阈值夜里换个路口就失效现象白天在某一个路口调好参数跑视频一切正常晚上换到相邻路口红色检测大面积失效绿色检测又疯狂误检HSV 阈值仿佛变成了随机数。原因两个因素叠加导致。一是相机自动曝光会随着整体画面亮度变化白天整体亮、红灯相对暗夜里整体暗、红灯相对亮同一物理颜色在不同光照下的 S 和 V 差异巨大二是不同路口信号灯的老化程度、发光颜色并不一致新灯偏冷白旧灯偏暖黄H 值会偏移 5 到 15 个单位。解决不追求一套阈值通吃改成「按场景配置化」。常见做法是准备两到三套阈值配置白天、夜晚、黄昏各一套在程序里按当前帧的平均亮度自动切换。更简单粗暴一点的做法是关掉相机的自动曝光固定曝光值让画面亮度不随环境剧烈变化颜色分割的输入就稳定下来——这是车端相机的标准做法因为自动曝光在车辆进出隧道时反而会造成两秒的亮度过冲对视觉算法是灾难。如果用的是 USB 摄像头在VideoCapture打开后尝试用cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0.25)之类接口固定曝光不同摄像头对数值定义不一样需要实测。最后提醒一句换摄像头等于换阈值同一个路口两台不同白平衡的相机拍出来的 HSV 可能差 20 个 H 值这是颜色方案的天然边界认了它用配置去兜底。5. 让红绿灯检测更可靠多帧投票与状态机判决5.1 单帧检测为什么不能直接用在驾驶辅助上单帧检测结果看着挺准但一旦接上真实视频流问题立刻暴露LED 信号灯是脉冲驱动相机曝光时间和光源闪烁频率之间存在拍频效应会随机出现某一帧灯芯特别暗甚至熄灭再加上车辆振动造成的运动模糊单帧检测在「无灯」和「绿灯」之间反复横跳是常态。还有更麻烦的——红灯切换成绿灯的瞬间如果正好丢了那几帧下游决策模块就不知道当前到底是什么颜色。所以单帧的检测结果只能叫「候选的灯色」不能叫「信号灯状态」。要在工程里用必须把时间维度加进来用多帧投票来抗闪烁用状态机来校验灯色变化是否符合红绿灯的行车逻辑。5.2 多帧投票用窗口队列压制单帧误检多帧投票的思路是保留最近 N 帧的颜色判定结果只有某个颜色在窗口内出现的比例超过阈值才把它当作当前有效结果。LED 闪烁可能让某一帧丢掉红灯但接下来两帧红灯会重新出现投票窗口能容忍这种偶然丢帧不会跟着抖动。from collections import deque, Counter class LightVoter: def __init__(self, window5, confirm_ratio0.6): self.history deque(maxlenwindow) self.confirm_ratio confirm_ratio def update(self, color): self.history.append(color) if len(self.history) self.history.maxlen: return None # 窗口未满不输出判决 counter Counter(self.history) top_color, top_count counter.most_common(1)[0] if top_count / len(self.history) self.confirm_ratio: return top_color return None # 窗口内颜色太散视为不确定逻辑说明deque(maxlenwindow)会自动丢弃最旧的一帧窗口始终保留最近 N 帧Counter统计窗口内各颜色出现次数most_common(1)取出现次数最多的颜色比例达到confirm_ratio才输出否则返回None表示当前状态不确定。参数说明window5在 30fps 下约等于 0.17 秒响应足够快confirm_ratio0.6意味着允许窗口内有 2 帧被干扰能容忍 LED 闪烁和偶发误检。想更抗干扰就把window调到 10、把confirm_ratio调到 0.7代价是状态切换会慢将近半秒——这个取舍要看下游决策模块能接受多大延迟。5.3 状态机按照红→绿→黄的合法顺序纠正识别结果投票能压住单帧抖动但压不住一类特殊的误检某个颜色连续多帧被误判比如红色广告牌连续 3 秒被当成红灯。多帧投票会全票通过照样输出红灯。这时候要靠状态机引入交通语义正常路口的信号灯必然按照「红 → 绿 → 黄 → 红」循环红灯不可能直接跳成绿灯中间必须经过黄灯部分地区还有红黄同亮的过渡状态。状态机的思路是维护一个当前状态和一张合法转移表当前是绿灯时下一帧可以接受黄灯但不能接受直接跳成红灯当前是红灯时不能直接跳成绿灯。任何不符合顺序的跳变都当作误检丢弃保持原状态。class SignalStateMachine: def __init__(self): self.state unknown self.transitions { unknown: {red, green, yellow}, red: {red, red_yellow, green}, # 红灯允许在部分区域经红黄同亮到绿 red_yellow: {red_yellow, green}, green: {green, yellow}, yellow: {yellow, red}, } def step(self, color): if color not in self.transitions.get(self.state, set()): return None # 非法跳变保持原状态 self.state color return self.state逻辑说明step接收投票后的颜色先查当前状态允许哪些下一个颜色如果传入的颜色不在合法集合里就返回None外部代码应该继续沿用上一个状态。比如当前是green投票结果突然是red状态机判定这是非法跳变不更新状态直到下一帧出现yellow才允许从green切到yellow。这个逻辑在真实视频里效果很明显原本单帧方案在红绿切换瞬间会连续误报好几个错误状态加状态机之后输出只会在合法顺序上迁移下游决策模块拿到的数据干净得多。这里有两点要注意一是状态机的初始化状态是unknown前几帧要允许任意颜色进入否则刚开始就跑飞二是部分地区的信号灯有红黄同亮的过渡相位transitions里特意加了red_yellow状态如果你测试的路口没有这个相位把它删掉也不会影响主流程。6. 红绿灯检测上线前的最后一步交互标定工具与回归验证无论代码写得多完整HSV 阈值的最终值一定是在真实画面上试出来的。我习惯先写一个交互标定工具用 Trackbar 实时调整红绿黄三色的 H、S、V 范围掩码效果和原图一起显示调到满意再把数值写进配置文件。这套工具比反复改代码跑脚本有效得多也是整个红绿灯检测项目里最值得花半小时搭建的部分。import cv2 def nothing(x): pass cv2.namedWindow(calib) cv2.createTrackbar(H_low, calib, 0, 179, nothing) cv2.createTrackbar(H_high, calib, 10, 179, nothing) cv2.createTrackbar(S_low, calib, 80, 255, nothing) cv2.createTrackbar(V_low, calib, 80, 255, nothing) img cv2.imread(traffic_light.jpg) hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) while True: h_low cv2.getTrackbarPos(H_low, calib) h_high cv2.getTrackbarPos(H_high, calib) s_low cv2.getTrackbarPos(S_low, calib) v_low cv2.getTrackbarPos(V_low, calib) mask cv2.inRange(hsv, (h_low, s_low, v_low), (h_high, 255, 255)) cv2.imshow(mask, mask) cv2.imshow(calib, img) if cv2.waitKey(30) 0xFF ord(q): break cv2.destroyAllWindows()Trackbar 回调函数必须接收一个参数nothing就是占位getTrackbarPos实时读取当前滑条位置inRange用滑条值生成掩码一边拖一边看效果。标定时一次只标一种颜色红色两个区间分开标最后把数值录进一个 JSON 或 YAML 配置文件程序启动时读取而不是写死在代码里。调完参数后要做一次回归验证录三段视频白天晴天、阴天、夜晚各一段每段 60 秒离线跑一遍记录状态机的输出序列。重点看两个指标状态切换延迟——红灯变绿灯的瞬间算法输出有没有在 0.5 秒内完成状态翻转同一颜色保持期间有没有超过 2 次的瞬间误切。第一项不达标就缩短投票窗口第二项不达标就调高confirm_ratio或收紧 ROI。这段验证做完了这个红绿灯检测方案才算真正能交付。我第一次做的时候阈值直接写死在代码里白天调好晚上就翻车后来把所有阈值和 ROI 收进配置文件画面一换先改配置再改代码少走非常多弯路。做视觉项目最后拼的往往不是算法多新颖而是参数可配置、问题可回归这几样都做到了方案才扛得住真实路口的折腾。希望帮到你。本文还有配套的精品资源点击获取
