news 2026/10/10 21:00:53

OpenCV红绿灯检测实战:HSV颜色分割与轮廓筛选的完整方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
OpenCV红绿灯检测实战:HSV颜色分割与轮廓筛选的完整方案

简介:Python与OpenCV实现红绿灯检测与识别的入门项目,面向计算机视觉初学者及智能交通、自动驾驶相关开发者,解决信号灯颜色识别与状态判断问题。项目采用传统视觉方案,将图像预处理、BGR转HSV颜色空间、阈值分割、轮廓提取等关键步骤浓缩在精简脚本中,并附中文说明文档,便于读者快速理解基于颜色分割的红绿灯识别流程,也可作为深度学习方案的基线对比。预处理中的灰度化、高斯滤波、直方图均衡化等步骤,均有直观的代码呈现。压缩包共12个文件,包含可运行的检测主程序、负责灯态判断的辅助模块、覆盖圆形灯、箭头灯、闪烁黄灯等典型场景的6张PNG/JPG样例图,以及Markdown说明文档;整体仅343KB,轻量便携。其中多张样例图可直接用于验证代码效果,readme中对代码结构、运行方式和参数调优做了说明,方便快速上手。代码步骤清晰,HSV颜色范围与阈值可自定义修改,适合课程设计、毕业设计二次开发,也可按需扩展为实时视频流检测版本。当前已有4039人学习下载,是低成本实验红绿灯识别、掌握OpenCV颜色空间处理与目标判别的实用资料;代码量精简、环境依赖少,适合快速跑通实验。

1. 红绿灯检测用 OpenCV 做,比想象中更依赖前期调参

用 Python 和 OpenCV 做红绿灯检测,本质上不是“能不能识别”的问题,而是“在什么光照、什么角度、什么画质下还能稳定识别”的问题。很多初学者拿到一张清晰的白天红灯图,跑个颜色阈值就出结果,兴奋三分钟,换上夜间逆光或者雨天场景立刻翻车。这个项目的核心难点不在 OpenCV 的 API 调用,而在你对 HSV 颜色空间的理解、对轮廓特征的筛选逻辑,以及对真实路口场景里各种干扰的预判。

这篇文章写给正在做课程设计、毕业设计,或者想用视觉方案做交通场景小项目的开发者。我会从环境搭建开始,讲清楚红绿灯检测里颜色分割和形状校验的原理,给出一套可以跑的完整代码,再把我踩过的坑按“现象 → 原因 → 解决”拆开讲。你跟着走一遍,应该能做出一个在固定机位下识别红绿灯还比较稳的原型。

2. 先把环境跑通:OpenCV 的安装与摄像头取流是第一个隐形门槛

2.1 安装 OpenCV 的常见做法和版本选择

红绿灯检测只需要用到 OpenCV 的基础图像处理模块,不需要 contrib 扩展包里的 SIFT、 SURF 那些东西,所以安装标准版opencv-python就够。这里的关键是版本选择。我一般推荐装 4.x 的最新稳定版,比如 4.8 或 4.9,因为 3.x 系列在部分新电脑和 Python 3.10+ 环境下容易遇到编译兼容问题。

安装命令很简单:

pip install opencv-python pip install numpy

如果你在 Ubuntu 上开发,可能会遇到系统自带的 Python 被externally-managed-environment限制导致 pip 装不进去的情况,常见做法是建虚拟环境:

python3 -m venv venv source venv/bin/activate pip install opencv-python numpy

Windows 用户反而简单,直接pip install就行。装完之后验证是否成功的命令是:

import cv2 print(cv2.__version__)

如果输出了类似4.9.0的版本号,说明安装正常。这里给一个重点提醒:OpenCV 的版本号和你代码里cv2.findContours的返回值个数直接相关。你如果搜到老教程,里面写contours, hierarchy = cv2.findContours(...)是 OpenCV 3.x 和 4.x 的写法;如果是 2.x 的教程,会多一个返回值,直接照抄会报错。

2.2 读取图片和摄像头的两种数据源写法

红绿灯检测项目的数据源无非两种:要么读视频文件或图片做离线测试,要么接摄像头做实时的。先看通用的读取代码:

import cv2 # 方式一:读图片 frame = cv2.imread("traffic_light.jpg") if frame is None: print("图片读取失败,检查路径和文件名") exit() # 方式二:读摄像头 cap = cv2.VideoCapture(0) # 0 代表默认摄像头 if not cap.isOpened(): print("摄像头打开失败,检查驱动或换个索引值") exit() while True: ret, frame = cap.read() if not ret: break # 这里放检测逻辑,frame 就是每一帧的 BGR 图像 cv2.imshow("camera", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()

这套代码的逻辑很清楚:cv2.imread返回None表示读取失败,常见原因是中文路径,后面避坑章节会细说;VideoCapture(0)的参数如果不止一个摄像头,可能要改成1或2。实时循环里waitKey(1)是必须的,否则 OpenCV 的窗口会卡死。

实际做项目时,我建议先把离线图片跑通再上摄像头,因为摄像头的帧率不稳定,排查问题会更难。你可以在视频文件上测试,把VideoCapture(0)换成VideoCapture("test_video.mp4"),效果一样但可以逐帧分析。

3. 从 BGR 到 HSV:红绿灯颜色分割的原理与调参逻辑

3.1 为什么用 HSV 而不是直接用 RGB 做颜色判断

刚接触 OpenCV 的人最容易犯的错是直接用 BGR 三通道的值判断颜色:看到(0, 0, 255)就认为是红色。这在纯色合成图里成立,但在真实摄像头画面里完全不可用,因为光照变化会让同一个红灯的 RGB 值从(0, 0, 255)漂移到(30, 40, 200),你没法用固定阈值覆盖。

HSV 颜色空间把颜色的本质属性拆成了三个独立维度:H(色调)、S(饱和度)、V(亮度)。色调 H 决定了这是红色、绿色还是黄色,不受光照亮暗影响;饱和度 S 表示颜色的鲜艳程度;亮度 V 表示明暗。所以分割红色区域时,只需要锁定 H 在红色范围,S 和 V 给一个合理的下限,就能把红灯从画面里抠出来。

再看一下 OpenCV 里 BGR 转 HSV 的调用方式:

import cv2 import numpy as np frame = cv2.imread("traffic_light.jpg") hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)

转换后的 hsv 图像同样是三通道,但第一通道是 H 值,范围是 0 到 179,不是 0 到 360。很多从其他语言转过来的开发者会在这一步翻车,因为 OpenCV 的 H 范围被压缩了一半,你拿网上标准的 0 到 360 范围去设阈值,会什么都检测不到。

3.2 红绿灯三色阈值范围的设定方法与可视化验证

红绿灯识别的核心就是三组 HSV 范围。下面这个表格是我在白天正常光照下反复测试过的初始值,适合作为起点,但不建议直接用于所有场景:

颜色H 范围S 范围V 范围说明
红色0-10 和 160-17980-25580-255红色在 HSV 空间跨越 0 度边界
黄色11-2580-25580-255黄灯饱和度一般较高
绿色35-8580-25550-255绿灯 V 下限放宽,因为绿灯偏暗

注意红色有两段范围,这是 HSV 色环的特性:红色正好位于 0 度位置,所以它在数值上被拆成 0 附近和 179 附近两段,需要用cv2.inRange分别生成掩码再合并。这段逻辑的代码实现如下:

import cv2 import numpy as np frame = cv2.imread("traffic_light.jpg") hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) # 红色范围:OpenCV 的 H 范围是 0-179 lower_red1 = np.array([0, 80, 80]) upper_red1 = np.array([10, 255, 255]) lower_red2 = np.array([160, 80, 80]) upper_red2 = np.array([179, 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) cv2.imshow("mask_red", mask_red) cv2.waitKey(0) cv2.destroyAllWindows()

这段代码的核心有两个:cv2.inRange会生成一张二值图,像素值在阈值范围内的变成白色(255),范围外的变成黑色(0);cv2.bitwise_or把两段红色范围合并成一张完整的红色掩码。

这里的参数不是死的。你试夜间图的时候,V 的下限往往会从 80 降到 40,否则红灯在暗光下会被过滤掉;但如果白天把 V 的下限设太低,红色的车尾灯、红色的招牌都会涌进来。我的习惯是先跑一张待检测图,把 mask 显示出来看白色区域覆盖情况,再微调 S 和 V 的下限,而不是一次性拍脑袋定一组值。

4. 实现一个完整的红绿灯检测程序:轮廓筛选与状态判断

4.1 用轮廓提取和形状因子过滤非灯源区域

颜色分割做完之后,掩码图里除了红绿灯,还会有红色的车身、路牌、甚至晚霞的倒影。要过滤掉这些干扰,最有效的手段是轮廓分析。cv2.findContours可以从二值掩码里提取所有连通区域的轮廓,然后通过面积、宽高比、圆形度三个指标做筛选。

红绿灯的灯体有几个明显的几何特征:面积占整张图的比例不会太大也不会太小;轮廓接近圆形,因为灯芯是圆形的;宽高比接近 1。下面这段代码实现了从红色掩码到候选框的完整链路:

import cv2 import numpy as np frame = cv2.imread("traffic_light.jpg") hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) lower_red1 = np.array([0, 80, 80]) upper_red1 = np.array([10, 255, 255]) lower_red2 = np.array([160, 80, 80]) upper_red2 = np.array([179, 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) # 形态学开运算,去除小噪点 kernel = np.ones((5, 5), np.uint8) mask_red = cv2.morphologyEx(mask_red, cv2.MORPH_OPEN, kernel) # 提取轮廓 contours, hierarchy = cv2.findContours(mask_red, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for cnt in contours: area = cv2.contourArea(cnt) if area < 300: continue # 面积太小,多半是噪点 x, y, w, h = cv2.boundingRect(cnt) aspect_ratio = w / h if aspect_ratio < 0.5 or aspect_ratio > 2.0: continue # 宽高比太离谱,排除横条物体 # 圆形度校验 perimeter = cv2.arcLength(cnt, True) if perimeter == 0: continue circularity = 4 * np.pi * area / (perimeter * perimeter) if circularity < 0.6: continue # 圆形度不够,排除路牌、车身等矩形物体 cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 0, 255), 2) cv2.putText(frame, "Red", (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) cv2.imshow("result", frame) cv2.waitKey(0) cv2.destroyAllWindows()

这段代码里的RETR_EXTERNAL只取最外层轮廓,避免灯芯内部高亮造成的空洞被算成多个轮廓;cv2.morphologyEx开运算的作用是先腐蚀后膨胀,把掩码里散落的单像素噪点清掉。circularity是圆形度系数,正圆的理论值是 1,我这里的阈值设到 0.6 是为了容忍摄像头视角造成的轻微变形。

面积阈值 300 是经验值,取决于你画面里灯的大小。如果你的摄像头安装位置比较高,灯只占几十个像素,这个值就要下调到 50;如果你拍的是近景特写,灯占了几千像素,阈值要相应提高。这类参数写成配置文件或者全局变量,方便换场景时统一调。

4.2 三色统一检测与信号灯状态判定逻辑

单独检测红色只能算完成一半,完整的红绿灯识别需要同时处理红、黄、绿三种颜色,并输出当前信号灯的状态。实现思路是把三个颜色的掩码分别做同样的轮廓筛选,然后比较哪个颜色命中了有效轮廓。

实际场景里一个路口可能同时有多组红绿灯出现在视野中,可能同时有红灯和左转绿灯亮着。对于最简单的单灯组场景,判断逻辑是:优先红色,其次绿色,最后黄色。因为红灯在交通规则里优先级最高。代码如下:

import cv2 import numpy as np class TrafficLightDetector: def __init__(self): # 颜色范围配置 self.color_ranges = { "red": ([0, 80, 80], [10, 255, 255], [160, 80, 80], [179, 255, 255]), "yellow": ([11, 80, 80], [25, 255, 255]), "green": ([35, 80, 50], [85, 255, 255]) } self.min_area = 300 self.min_circularity = 0.6 def get_mask(self, hsv, color): ranges = self.color_ranges[color] if color == "red": mask1 = cv2.inRange(hsv, np.array(ranges[0]), np.array(ranges[1])) mask2 = cv2.inRange(hsv, np.array(ranges[2]), np.array(ranges[3])) mask = cv2.bitwise_or(mask1, mask2) else: mask = cv2.inRange(hsv, np.array(ranges[0]), np.array(ranges[1])) kernel = np.ones((5, 5), np.uint8) mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) return mask def detect_color(self, hsv, color): mask = self.get_mask(hsv, color) contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for cnt in contours: area = cv2.contourArea(cnt) if area < self.min_area: continue x, y, w, h = cv2.boundingRect(cnt) if w / h < 0.5 or w / h > 2.0: continue perimeter = cv2.arcLength(cnt, True) if perimeter == 0: continue circularity = 4 * np.pi * area / (perimeter * perimeter) if circularity < self.min_circularity: continue return True, (x, y, w, h) return False, None def process(self, frame): hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) results = {} for color in ["red", "yellow", "green"]: detected, box = self.detect_color(hsv, color) results[color] = (detected, box) return results detector = TrafficLightDetector() cap = cv2.VideoCapture(0) while True: ret, frame = cap.read() if not ret: break results = detector.process(frame) status = "unknown" if results["red"][0]: status = "RED" x, y, w, h = results["red"][1] cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 0, 255), 2) elif results["green"][0]: status = "GREEN" x, y, w, h = results["green"][1] cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) elif results["yellow"][0]: status = "YELLOW" x, y, w, h = results["yellow"][1] cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 255), 2) cv2.putText(frame, f"Status: {status}", (30, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2) cv2.imshow("Traffic Light Detection", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()

这个类把颜色范围、筛选参数、检测逻辑封装在一起,方便你换场景时只改配置不改代码。process方法返回三个颜色的检测结果,主循环里按红、绿、黄的优先级取唯一状态输出。

注意这里一个潜在问题:如果画面里同时检测到红灯和绿灯,说明视野里有两组红绿灯,简单的状态输出会混乱。处理办法是增加 ROI(感兴趣区域)限定,只检测画面中心区域的红绿灯,或者按灯组的空间位置做聚类。这部分属于进阶玩法,我会在最后一章展开。

5. 避坑与常见问题排查:从环境报错到检测失效逐个击破

5.1 现象:cv2.imread 读不出图片,程序直接报 None

原因:最常见的两种情况,一是文件路径里有中文字符,OpenCV 的底层 C++ 实现不支持fopen中文路径;二是图片不在当前工作目录下,而你以为相对路径是对的。

解决:路径全部改成英文,这是最彻底的办法。如果项目交付给第三方必须保留中文路径,可以用cv2.imdecode绕开文件系统编码问题:

import cv2 import numpy as np def imread_unicode(path): stream = open(path, "rb") data = np.frombuffer(stream.read(), dtype=np.uint8) stream.close() return cv2.imdecode(data, cv2.IMREAD_COLOR)

5.2 现象:pip 安装 opencv-python 成功,但 import cv2 报 ModuleNotFoundError

原因:电脑里装了多个 Python 环境,pip 装到的环境和你运行脚本的环境不是同一个。常见于 Anaconda 和系统 Python 并存,或者 VS Code 里选择了解释器但终端用的还是默认 Python。

解决:在 VS Code 里按Ctrl+Shift+P,选择 “Python: Select Interpreter”,确认选中的解释器和pip指向的解释器一致。在终端里也可以先执行which python和which pip看路径是否匹配,然后用python -m pip install opencv-python代替pip install,这样能保证装到当前 Python 环境里。

5.3 现象:掩码里全是零零散散的小白点,轮廓根本聚不成形

原因:原始图像的噪声太大,尤其是低照度下摄像头 CMOS 的噪点会被inRange一起捕捉进来。没有任何滤波直接找轮廓,就会得到大量细小连通域。

解决:在findContours之前加两步预处理。第一步是高斯模糊,把像素之间的突变抹平;第二步是开运算去噪。推荐先模糊后转 HSV,再阈值分割:

blurred = cv2.GaussianBlur(frame, (5, 5), 0) hsv = cv2.cvtColor(blurred, cv2.COLOR_BGR2HSV)

5.4 现象:白天检测正常,傍晚开始红灯频繁漏检

原因:光照变暗导致红灯的 V 值整体下降,如果 V 下限设得高,红色直接被过滤掉;如果 V 下限设得低,路面上红色的车漆、刹车灯会大量误检。

解决:不要用固定阈值,按时间或者按画面整体亮度动态调 V 下限。简单做法是把cv2.mean(hsv[:, :, 2])当作环境亮度参考,亮度低时自动把 V 下限从 80 降到 40。代码示意:

avg_brightness = cv2.mean(hsv[:, :, 2])[0] if avg_brightness < 60: lower_v = 40 else: lower_v = 80

这类“自适应阈值”逻辑不算复杂,但 D 在真实场景里效果提升非常明显,值得花时间调。

5.5 现象:红色尾灯被误检成红灯

原因:圆形度、面积、宽高比三个指标都满足的情况下,尾灯和红灯在视觉特征上几乎没有区别。唯一的区别在于空间位置:红绿灯通常位于画面的上半部分,且和另外两个灯芯垂直排列。

解决:加一个 ROI 限制,只检测画面上半部分区域;同时检查候选框的垂直分布,如果检测到多个圆形物体,按垂直方向聚类的逻辑判断哪个是灯组、哪个是地面反射。代码里限制 ROI 的方法很简单:

roi = frame[0:int(frame.shape[0] * 0.6), :]

这个办法不能做到百分百准确,但能把误检率降一个数量级。

6. 让检测更稳的进阶技巧:状态锁存、ROI 与模板校验的组合策略

到这里,你已经有一个能在固定场景下稳定跑的检测程序了。但真实交通场景还需要解决一个关键问题:一帧一帧独立检测的抖动。摄像头录制视频时,某一帧因为遮挡或反光没检测到红灯,如果直接输出状态切换,下游逻辑就会错误地认为绿灯亮了。这是所有基于单帧检测的视觉系统都要面对的时序问题。

我常用的技巧是“状态锁存”:连续 N 帧检测到同一状态才确认切换,少于 N 帧就保持上一状态。N 一般取 5 到 10,具体看摄像头帧率和你对实时性的要求。帧率 30 的情况下,10 帧的锁存相当于 0.3 秒延迟,人眼几乎感知不到。实现方式是维护一个长度为 N 的环形状态数组,取出现频率最高的状态作为最终输出。这套逻辑对红灯转绿灯的瞬间抖动、摄像头被大车短暂遮挡的场景非常有效。

另一个提升稳定的思路是把颜色分割的结果和模板匹配做交叉验证。颜色分割擅长找“颜色对的区域”,模板匹配擅长找“形状对的目标”,两者独立误判的场景重合度很低。你可以在第一次运行程序时,手动框选红绿灯位置存为模板,之后每帧检测时对四个候选角点做模板匹配,只有颜色分割和模板匹配同时命中的目标才被认为是真正的红绿灯。这个方案比直接单独用模板匹配鲁棒得多,因为模板匹配对光照变化非常敏感,而颜色分割刚好能补上这个短板。

我自己的经验是,完成以上调整后,我的测试视频里红绿灯检测精度从大约 85% 提升到了 96% 左右。多出来的这十个百分点不是靠某个神秘参数,而是靠一帧一帧跑测试视频,把每一帧的检测结果打印到命令行,肉眼对比出错帧的特征,再回头调整区域阈值和筛选参数。这个过程很枯燥,但它是视觉项目从“能跑”到“好用”之间的必经之路。希望这套方案能帮你少走一些弯路。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/10 21:00:26

Spring Boot网上图书商城毕设全流程详解:从数据库到部署

又到了一年一度的毕业设计季节&#xff0c;群里满是“求一个图书商城毕设”“Spring Boot项目跑不起来”“数据库连不上怎么办”之类的求助。坦白说&#xff0c;像“网上图书商城”这种题目&#xff0c;几乎是计算机毕业设计里的常青树&#xff0c;市面上也确实流传着各种版本的…

作者头像 李华
网站建设 2026/10/10 21:00:10

红绿灯目标检测数据集:1000张图+三格式标签+YOLO训练全流程

简介&#xff1a;面向目标检测学习与实战训练的YOLO红绿灯数据集&#xff0c;包含1000张真实场景高质量图片&#xff0c;数据场景丰富&#xff0c;覆盖不同路口、时间段与拍摄角度&#xff0c;能较好支撑交通灯检测算法的训练与评估。使用LabelImg软件完成人工标注&#xff0c;…

作者头像 李华
网站建设 2026/10/10 21:00:03

day36算法练习:质数判定、字符串处理与排序的细节优化

这一天的挑战有点意思——三道题放在一块儿&#xff0c;其实正好覆盖了编程里三个最基础也最容易出问题的环节&#xff1a;数学逻辑、字符串处理、还有集合操作。质数、翻译字符串、分割数字并排序&#xff0c;听起来都是入门题&#xff0c;但真上手写的时候&#xff0c;你会发…

作者头像 李华
网站建设 2026/10/10 20:57:01

LR-ASPP + MobileNet v3:道路图像语义分割的轻量化实战与PyTorch实现

简介&#xff1a;面向道路场景图像语义分割任务&#xff0c;这份资源提供了一套基于轻量级分割网络与迁移学习的实战工程。利用预训练权重微调&#xff0c;在道路分割数据集上仅训练10个轮次&#xff0c;验证集平均交并比即达到0.98&#xff0c;效果显著&#xff0c;适合希望快…

作者头像 李华
网站建设 2026/10/10 20:53:38

Linux文本处理三剑客:grep、sed与awk实战解析

Linux 文本处理工具&#xff0c;这三个词组合在一起&#xff0c;可能第一反应是“这有什么好讲的&#xff0c;不就是 grep、sed、awk 嘛”。但如果你真在服务器上排查过 800MB 的日志文件&#xff0c;被满屏的报错刷到眼花&#xff0c;或者需要从一段接口返回里精确抠出某个字段…

作者头像 李华
网站建设 2026/10/10 20:53:09

中外儿童背诵内容对比:中国娃吟诗作对,外国娃背什么?

最近刷到好几个短视频&#xff0c;一边是中国三岁半的萌娃穿着小汉服&#xff0c;奶声奶气地背《将进酒》&#xff0c;背到“天生我材必有用”还配了个握拳的小动作&#xff1b;另一边是外国小朋友围坐一圈&#xff0c;在老师带领下念法语数字歌&#xff0c;或者一本正经地背诵…

作者头像 李华