简介这是一套基于Python与OpenCV实现的红绿灯识别系统源代码面向计算机视觉初学者、课程设计或自动驾驶入门开发者帮助理解颜色空间转换、阈值分割与形状分析在交通灯识别中的落地方式。压缩包共10个文件约23KB包含5个py脚本主程序、GUI界面与多个识别版本、3个json配置、1个png预览图及gitignore结构轻量便于快速运行与二次修改。系统提供图形界面支持滤镜选择与色彩阈值调整可适应不同光照与摄像头视角并输出实时识别结果。已有651人学习下载。读者可从中获得完整的图像预处理、HSV色彩过滤、红绿灯状态判定与GUI交互实现思路同时接触光照变化、实时性与错误处理等工程考量适合作为计算机视觉项目练手与功能扩展的参考模板。1. 红绿灯识别系统从一段路口视频到能跑起来的 GUI手头有一段行车记录仪拍的路口视频想让它自动判断当前是红灯还是绿灯这件事听起来像是个深度学习项目实际上用 python-opencv 的传统图像处理思路就能做出一个能演示、能交互的版本。这套红绿灯识别系统源代码的核心逻辑并不复杂把图像从 BGR 转到 HSV 空间按颜色阈值把红、黄、绿三个区域抠出来再做形态学去噪和轮廓筛选最后根据亮起的灯位判断当前信号。配上 GUI 界面之后你可以直接拖入图片或视频实时看到识别结果和置信度。它适合两类人一类是刚学完 opencv 图像处理、想找一个完整项目练手的入门者另一类是需要快速搭一个交通灯智能识别演示原型、验证算法可行性的工程师。整套方案不依赖 GPU普通笔记本就能跑重点在于参数调优和工程细节。2. 颜色阈值分割HSV 空间为什么比 RGB 更适合红绿灯2.1 从 BGR 到 HSV 的转换逻辑与参数含义opencv 读进来的图像默认是 BGR 三通道直接在这个空间里做颜色判断会非常痛苦因为光照一变R、G、B 三个值同时漂移你很难画出一条固定的分界线。HSV 把颜色拆成色调Hue、饱和度Saturation、明度Value三个维度红绿灯的颜色差异主要体现在色调上光照强弱主要影响明度这样就能把颜色判断和亮度变化解耦开。转换本身只需要一行import cv2 import numpy as np # 读取图像opencv 默认按 BGR 通道顺序读入 frame cv2.imread(traffic_light.jpg) # 转换到 HSV 色彩空间后续所有颜色判断都在这个空间做 hsv cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)这里有个容易翻车的点opencv 的 H 通道取值范围是 0 到 179不是常见的 0 到 360。这是因为 8 位图像里 H 被压缩了一半红色在标准色环上跨 0 度在 opencv 里就变成了 H 接近 0 和接近 179 两段。所以红色阈值必须写两组不能只写一组。2.2 红黄绿三色的阈值范围与掩膜生成下面是我一般会用的初始阈值实际项目里要根据摄像头白平衡和路灯色温微调# 红色在 HSV 中跨越 0 度需要两段阈值合并 lower_red1 np.array([0, 100, 100]) upper_red1 np.array([10, 255, 255]) lower_red2 np.array([160, 100, 100]) upper_red2 np.array([179, 255, 255]) # 黄色范围较窄集中在 20 到 35 之间 lower_yellow np.array([20, 100, 100]) upper_yellow np.array([35, 255, 255]) # 绿色范围相对宽但要注意避开青色干扰 lower_green np.array([40, 80, 80]) upper_green np.array([90, 255, 255]) # 生成掩膜每个颜色得到一张二值图 mask_red1 cv2.inRange(hsv, lower_red1, upper_red1) mask_red2 cv2.inRange(hsv, lower_red2, upper_red2) mask_red cv2.bitwise_or(mask_red1, mask_red2) mask_yellow cv2.inRange(hsv, lower_yellow, upper_yellow) mask_green cv2.inRange(hsv, lower_green, upper_green)参数说明每个 np.array 里的三个值依次是 H、S、V 的下限或上限。S 和 V 的下限设成 100 和 100 是为了过滤掉灰白色和暗色区域避免把白色车身或灰色路面误判成灯。如果发现夜间识别不到可以把 V 的下限降到 50 左右如果白天误检多把 S 的下限提到 120 以上。2.3 形态学处理去掉噪点、补上空洞掩膜出来之后通常会有很多小噪点灯的中心也可能因为反光出现空洞。这时候用开运算去噪、闭运算补洞# 定义一个 5x5 的椭圆结构元素比矩形更贴合圆形灯体 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) # 开运算先腐蚀后膨胀去掉孤立噪点 mask_red cv2.morphologyEx(mask_red, cv2.MORPH_OPEN, kernel) # 闭运算先膨胀后腐蚀填补灯体内部空洞 mask_red cv2.morphologyEx(mask_red, cv2.MORPH_CLOSE, kernel) # 对黄色和绿色做同样处理 mask_yellow cv2.morphologyEx(mask_yellow, cv2.MORPH_OPEN, kernel) mask_yellow cv2.morphologyEx(mask_yellow, cv2.MORPH_CLOSE, kernel) mask_green cv2.morphologyEx(mask_green, cv2.MORPH_OPEN, kernel) mask_green cv2.morphologyEx(mask_green, cv2.MORPH_CLOSE, kernel)kernel 大小是个经验值。5x5 适合 640x480 到 1280x720 的分辨率如果图像更大比如 1920x1080建议用 7x7 或 9x9。kernel 太小去不掉噪点太大会把远处的小灯也一起吃掉。这一步做完掩膜上基本只剩下几个干净的色块。3. 轮廓筛选与灯位判断怎么从一堆色块里找到真正的信号灯3.1 轮廓提取与面积、宽高比过滤掩膜上可能同时存在多个色块比如红色尾灯、红色广告牌、红色衣服。需要靠几何特征把真正的信号灯挑出来def find_candidates(mask, min_area50, max_area5000): 从掩膜中提取候选轮廓按面积和宽高比过滤 contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) candidates [] for cnt in contours: area cv2.contourArea(cnt) # 面积太小是噪点太大可能是大面积红色物体 if area min_area or area max_area: continue x, y, w, h cv2.boundingRect(cnt) aspect_ratio w / float(h) # 信号灯近似圆形或略扁宽高比在 0.5 到 2.0 之间 if aspect_ratio 0.5 or aspect_ratio 2.0: continue # 填充率轮廓面积与外接矩形面积之比圆形约 0.78 fill_ratio area / (w * h) if fill_ratio 0.5: continue candidates.append((x, y, w, h, area)) return candidatesmin_area 和 max_area 需要根据实际画面里灯的大小来定。如果摄像头离路口远灯在画面里只有十几个像素min_area 要降到 20 左右。fill_ratio 这个指标很实用它能过滤掉细长条形的红色物体比如红色栏杆或红色车身条纹。3.2 用位置关系排除干扰灯位排列的几何约束真正的红绿灯三个灯是垂直或水平排列的而且同一时刻通常只有一个灯亮。利用这个先验知识可以大幅降低误判def group_traffic_lights(red_cands, yellow_cands, green_cands): 将三个颜色的候选框按位置聚类判断是否属于同一组信号灯 all_cands [] for c in red_cands: all_cands.append((red, c)) for c in yellow_cands: all_cands.append((yellow, c)) for c in green_cands: all_cands.append((green, c)) # 按 x 坐标排序水平排列的信号灯 x 依次增大 all_cands.sort(keylambda item: item[1][0]) groups [] used [False] * len(all_cands) for i in range(len(all_cands)): if used[i]: continue group [all_cands[i]] used[i] True for j in range(i 1, len(all_cands)): if used[j]: continue # 两个候选框中心点距离小于阈值认为是同一组 xi, yi, wi, hi, _ all_cands[i][1] xj, yj, wj, hj, _ all_cands[j][1] cx1, cy1 xi wi / 2, yi hi / 2 cx2, cy2 xj wj / 2, yj hj / 2 dist ((cx1 - cx2) ** 2 (cy1 - cy2) ** 2) ** 0.5 if dist max(wi, hi) * 3: group.append(all_cands[j]) used[j] True groups.append(group) return groups这个聚类逻辑的核心是同一组信号灯的候选框中心距离不会太远大概是灯体尺寸的 2 到 3 倍。如果画面里同时出现多个路口的灯这个分组能把它们分开后续每组独立判断。3.3 亮灯判定与置信度输出一组信号灯里哪个颜色面积最大、填充率最高就判定为当前亮灯def judge_signal(group): 输入一组候选框输出当前信号灯颜色和置信度 if not group: return unknown, 0.0 # 按面积排序取最大的作为主候选 group.sort(keylambda item: item[1][4], reverseTrue) color, (x, y, w, h, area) group[0] # 置信度用填充率表示越接近 1 越像完整灯体 fill_ratio area / (w * h) confidence min(fill_ratio, 1.0) # 如果同组里有多个颜色面积接近说明判断不可靠 if len(group) 1: second_area group[1][1][4] if second_area area * 0.7: return uncertain, confidence * 0.5 return color, confidence置信度低于 0.5 的时候界面上应该显示“不确定”而不是硬给一个结果。实际路口视频里黄灯和红灯切换的瞬间经常出现两个颜色同时可见的情况这时候输出 uncertain 比输出错误答案更合理。4. GUI 界面搭建用 tkinter 把识别流程串起来4.1 界面布局与控件选择python 自带的 tkinter 足够做这个演示界面不需要额外装 PyQt。布局上分三块顶部是文件选择按钮和模式切换中间是图像显示区底部是识别结果和参数调节滑块。import tkinter as tk from tkinter import filedialog, ttk from PIL import Image, ImageTk class TrafficLightGUI: def __init__(self, root): self.root root self.root.title(红绿灯识别系统) self.root.geometry(900x700) # 顶部控制栏 self.top_frame tk.Frame(root) self.top_frame.pack(filltk.X, padx10, pady5) self.btn_image tk.Button(self.top_frame, text选择图片, commandself.load_image) self.btn_image.pack(sidetk.LEFT, padx5) self.btn_video tk.Button(self.top_frame, text选择视频, commandself.load_video) self.btn_video.pack(sidetk.LEFT, padx5) self.btn_stop tk.Button(self.top_frame, text停止, commandself.stop_video, statetk.DISABLED) self.btn_stop.pack(sidetk.LEFT, padx5) # 图像显示区 self.canvas tk.Canvas(root, width860, height500, bggray) self.canvas.pack(padx10, pady5) # 底部结果栏 self.bottom_frame tk.Frame(root) self.bottom_frame.pack(filltk.X, padx10, pady5) self.result_label tk.Label(self.bottom_frame, text等待输入, font(Arial, 16)) self.result_label.pack(sidetk.LEFT) self.confidence_label tk.Label(self.bottom_frame, text置信度: --, font(Arial, 12)) self.confidence_label.pack(sidetk.RIGHT) self.video_capture None self.is_playing False这里用 PIL 的 ImageTk 来显示 opencv 图像因为 tkinter 原生不支持直接显示 numpy 数组。转换的时候要注意 BGR 转 RGB否则颜色会偏。4.2 图片识别流程与结果回显图片模式的流程很直接读图、识别、画框、显示。def load_image(self): path filedialog.askopenfilename( filetypes[(Image files, *.jpg *.jpeg *.png *.bmp)]) if not path: return frame cv2.imread(path) if frame is None: self.result_label.config(text图像读取失败) return result_frame, color, confidence self.process_frame(frame) self.show_frame(result_frame) self.result_label.config(textf识别结果: {color}) self.confidence_label.config(textf置信度: {confidence:.2f}) def process_frame(self, frame): 完整的识别流程返回画了框的图像和识别结果 hsv cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) # 生成三个颜色的掩膜 mask_red self.get_red_mask(hsv) mask_yellow cv2.inRange(hsv, lower_yellow, upper_yellow) mask_green cv2.inRange(hsv, lower_green, upper_green) # 形态学处理 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) for mask in [mask_red, mask_yellow, mask_green]: cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, dstmask) cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, dstmask) # 提取候选并分组 red_cands find_candidates(mask_red) yellow_cands find_candidates(mask_yellow) green_cands find_candidates(mask_green) groups group_traffic_lights(red_cands, yellow_cands, green_cands) # 取面积最大的一组作为主信号灯 if groups: groups.sort(keylambda g: sum(c[1][4] for c in g), reverseTrue) color, confidence judge_signal(groups[0]) # 在图上画出所有候选框 for c in groups[0]: x, y, w, h, _ c[1] cv2.rectangle(frame, (x, y), (x w, y h), (0, 255, 0), 2) else: color, confidence unknown, 0.0 return frame, color, confidenceprocess_frame 是整个系统的核心它把前面几章讲的步骤串在一起。注意这里对每个掩膜都做了开闭运算实际跑的时候如果发现视频卡顿可以把形态学操作去掉靠轮廓筛选来兜底。4.3 视频模式与帧循环处理视频模式需要在主线程之外跑一个循环否则界面会卡死。tkinter 的 after 方法可以做到不阻塞界面def load_video(self): path filedialog.askopenfilename( filetypes[(Video files, *.mp4 *.avi *.mov)]) if not path: return self.video_capture cv2.VideoCapture(path) if not self.video_capture.isOpened(): self.result_label.config(text视频打开失败) return self.is_playing True self.btn_stop.config(statetk.NORMAL) self.update_video() def update_video(self): if not self.is_playing or self.video_capture is None: return ret, frame self.video_capture.read() if not ret: self.stop_video() return # 缩小分辨率加快处理速度 frame cv2.resize(frame, (640, 360)) result_frame, color, confidence self.process_frame(frame) self.show_frame(result_frame) self.result_label.config(textf识别结果: {color}) self.confidence_label.config(textf置信度: {confidence:.2f}) # 每 30 毫秒处理下一帧约 33fps self.root.after(30, self.update_video) def stop_video(self): self.is_playing False if self.video_capture: self.video_capture.release() self.video_capture None self.btn_stop.config(statetk.DISABLED)视频处理时把分辨率降到 640x360 是个实用技巧识别精度损失不大但速度能提升三到四倍。如果原始视频是 1080p直接处理会非常卡。5. 避坑与排查红绿灯识别里最容易翻车的五个地方5.1 红色总是识别不到或者识别成两段现象画面里红灯明明很亮但掩膜上红色区域是断开的或者干脆没有。原因opencv 的 H 通道红色跨越 0 和 179 两端只写一组阈值只能覆盖一半。另外如果摄像头白平衡偏冷红色会往品红方向偏H 值可能跑到 170 以上。解决红色阈值必须写两组并用 bitwise_or 合并。如果还是断把 H 的上限从 10 放宽到 15下限从 160 降到 150。用 cv2.cvtColor 转成 HSV 后可以先把 H 通道单独取出来看直方图确认红灯的 H 值到底落在哪个区间。5.2 白天正常、晚上全黑或者全是噪点现象白天跑得好好的一到夜间视频要么什么都识别不到要么满屏都是红色噪点。原因夜间路灯和车灯造成大面积高亮区域S 和 V 的分布跟白天完全不同。固定的 S、V 下限在夜间会把很多暗红色物体也框进来。解决加一个亮度判断分支。先算整帧的平均 V 值如果低于 60判定为夜间模式把 S 下限降到 60V 下限降到 40同时把 min_area 提高到 100 以上过滤掉小噪点。如果高于 180判定为强光模式把 S 下限提到 150避免白色高光被误判。5.3 视频处理越来越卡最后界面无响应现象视频刚开始还能跑几十帧之后越来越慢最后整个窗口卡死。原因update_video 里每帧都创建新的 numpy 数组和 PIL Image 对象python 的垃圾回收跟不上内存持续增长。另外如果 process_frame 里做了太多形态学操作单帧耗时超过 30 毫秒after 队列会堆积。解决把 kernel 的创建提到循环外面不要每帧都 getStructuringElement。PIL Image 对象用完手动 del 并调用 gc.collect() 不现实更好的做法是复用同一个 PhotoImage 对象用 paste 更新内容。如果还是卡把形态学操作去掉只保留轮廓面积过滤。5.4 黄灯和红灯同时亮起时判断混乱现象黄灯亮的时候系统有时候报红色有时候报黄色切换瞬间结果跳来跳去。原因黄灯的 H 范围是 20 到 35红灯的上限是 10中间有 10 个单位的间隔。但实际摄像头里黄灯边缘会有红色渐变导致同一盏灯同时产生红色和黄色候选框。解决在 judge_signal 里加一个优先级规则如果同一组里同时有红色和黄色候选且黄色面积大于红色的 60%判定为黄色。另外在时间维度上做平滑连续 3 帧结果一致才更新界面显示避免单帧误判导致结果闪烁。5.5 绿色把路边的树和青色广告牌也框进来现象绿灯识别没问题但画面边缘的绿色植物或青色招牌经常被误判成绿灯。原因绿色的 H 范围 40 到 90 太宽把青色和部分黄绿色都包进来了。另外植物的纹理复杂轮廓填充率可能刚好落在阈值边缘。解决把绿色的 H 上限从 90 降到 75下限从 40 提到 45。同时提高 fill_ratio 的下限到 0.6因为真正的绿灯是均匀发光的圆盘填充率接近 0.78而树叶的填充率通常低于 0.5。如果还有误检加一个位置约束只保留画面下半部分的绿色候选因为信号灯通常不会出现在画面顶部。6. 进阶技巧用帧间差分和 ROI 把识别速度再提一倍前面讲的方案是逐帧全图处理在 640x360 分辨率下大概能跑到 15 到 20 帧。如果想让它在树莓派或者老笔记本上也能流畅跑有两个技巧值得试。第一个是 ROI 裁剪。信号灯在画面里的位置通常比较固定尤其是固定摄像头拍路口。你可以先手动框一个感兴趣区域后续只在这个区域里做颜色分割。ROI 一般取画面中间偏上的位置宽度占全图的三分之一到一半。这样处理面积直接降到原来的四分之一速度提升非常明显。def crop_roi(frame, roi_ratio(0.3, 0.1, 0.7, 0.6)): 按比例裁剪 ROI默认取画面中间偏上区域 h, w frame.shape[:2] x1 int(w * roi_ratio[0]) y1 int(h * roi_ratio[1]) x2 int(w * roi_ratio[2]) y2 int(h * roi_ratio[3]) return frame[y1:y2, x1:x2], (x1, y1)roi_ratio 四个值依次是左、上、右、下的比例。这个参数需要根据你的摄像头安装位置调没有万能值。调的时候先把 ROI 框画出来看一眼确保信号灯始终在框内。第二个是帧间差分。视频里相邻两帧的背景几乎不变只有信号灯切换的时候对应区域才有明显变化。可以先算当前帧和上一帧的差值只在差值大的区域做颜色识别。这样大部分帧只需要做一次减法耗时可以忽略。class FrameDiffDetector: def __init__(self, threshold25, min_changed_ratio0.01): self.prev_gray None self.threshold threshold self.min_changed_ratio min_changed_ratio def need_process(self, frame): 判断当前帧是否需要做完整识别 gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) gray cv2.GaussianBlur(gray, (5, 5), 0) if self.prev_gray is None: self.prev_gray gray return True diff cv2.absdiff(self.prev_gray, gray) _, thresh cv2.threshold(diff, self.threshold, 255, cv2.THRESH_BINARY) changed_ratio np.count_nonzero(thresh) / thresh.size self.prev_gray gray # 变化面积超过阈值才做完整识别 return changed_ratio self.min_changed_ratiothreshold 控制像素差多少算变化25 是个比较稳的值太低会把噪点当变化太高会漏掉缓慢的灯色切换。min_changed_ratio 控制变化面积占比0.01 表示画面 1% 的像素变了才触发识别。实际用的时候信号灯切换那一两帧会触发识别其余帧直接复用上一次结果整体帧率能翻倍。这两个技巧叠加使用的时候要注意顺序先做 ROI 裁剪再做帧间差分最后在 ROI 内做颜色分割。ROI 裁剪减少处理面积帧间差分减少处理频次两者不冲突。我自己调这套系统的时候最大的教训是不要一上来就追求全自动。先把单张图片的阈值调稳再跑视频最后加 GUI。很多人反过来界面先搭好结果识别效果一塌糊涂调参的时候连个稳定的测试基准都没有。另外 HSV 阈值没有一套参数能通吃所有摄像头换一个设备就要重新标定这个心理预期要有。希望帮到你。本文还有配套的精品资源点击获取