☰
OpenCV红绿灯识别实战:光照鲁棒性与嵌入式部署方案
2026/10/22 2:48:14 网站建设 项目流程

简介:本资源是一套面向计算机视觉与智能交通方向本科生的毕业设计源码案例,聚焦于基于Python与OpenCV实现交通路口红绿灯的实时识别与状态控制逻辑。项目涵盖视频流采集、HSV颜色空间车辆检测、形态学处理、灯态定时切换及虚拟交通流模拟等完整技术链路,适合课程设计、毕设参考与CV入门实践。压缩包共29个文件,含3个核心Python脚本(main.py、video.py、sql.py)、1个README.md说明文档、2个HTML前端页面、3个PNG/JPEG图像素材、4个XML配置或标注文件,以及CSS/JS/静态资源等,整体仅1.24MB,轻量易部署。已有171人学习下载,资源结构清晰,包含模拟路口模块、Web可视化界面(admin.html/index.html)及预编译pyc文件,便于快速运行验证算法逻辑,并为后续扩展车流量统计、多相位控制提供可复用代码基础。

1. 红绿灯识别不是“拍张照就完事”:为什么90%的OpenCV交通灯项目在真实路口直接失效?

你手里的毕业设计标题写着“Python基于OpenCV的交通路口红绿灯控制系统设计”,但很可能——你还没跑通过一张白天强光下的十字路口监控截图,更别说处理黄昏逆光、雨雾模糊、摄像头抖动、多灯组并存、甚至被树枝遮挡一半的红灯。这不是算法不行,而是OpenCV红绿灯识别的本质,是光照鲁棒性+几何稳定性+状态时序建模三重关卡的硬核组合。它不依赖深度学习黑匣子,但对图像预处理链路的每一步参数都极其敏感:高斯模糊半径差0.5,Otsu阈值偏移3个灰度级,轮廓面积筛选下限设错10像素,整个检测逻辑就可能从“稳定识别”退化成“玄学闪现”。这个方案适合需要完全本地部署、无GPU依赖、可解释性强、且必须在嵌入式边缘设备(如树莓派+USB摄像头)上长期运行的场景——比如某高校智能交通教学平台中的路口信号模拟模块,或某实验室低功耗路口监测Demo。它不追求YOLOv8那种端到端的“一网打尽”,而是用传统CV的确定性,把“红/黄/绿”三个状态的判定做成可调试、可回溯、可写进毕设答辩PPT每一页的技术闭环。


2. 从原始视频流到灯色判定:OpenCV红绿灯识别四步核心流水线

2.1 视频输入与ROI区域动态锁定:为什么不能靠“固定坐标框”硬切?

真实路口摄像头存在俯角、偏移、焦距变化,若用cv2.rectangle(frame, (x,y), (x+w,y+h), ...)硬编码ROI,一旦摄像头微调或更换,所有后续逻辑全崩。正确做法是先做粗定位,再动态裁剪:

import cv2 import numpy as np def get_traffic_light_roi(frame): # 步骤1:转HSV空间,增强颜色分离度(比RGB对光照更鲁棒) hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) # 步骤2:定义红灯HSV范围(注意:OpenCV中H∈[0,179],S/V∈[0,255]) # 红色在HSV中跨0°和180°,需分两段取 lower_red1 = np.array([0, 70, 50]) upper_red1 = np.array([10, 255, 255]) lower_red2 = np.array([170, 70, 50]) upper_red2 = np.array([180, 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) # 步骤3:形态学去噪(先开运算去毛刺,再闭运算填小孔) kernel = np.ones((3,3), np.uint8) mask_red = cv2.morphologyEx(mask_red, cv2.MORPH_OPEN, kernel) mask_red = cv2.morphologyEx(mask_red, cv2.MORPH_CLOSE, kernel) # 步骤4:找最大连通域轮廓 → 粗略定位红灯区域中心 contours, _ = cv2.findContours(mask_red, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if contours: largest_contour = max(contours, key=cv2.contourArea) x, y, w, h = cv2.boundingRect(largest_contour) # 扩展ROI:避免只切到灯体,留出上下左右20%余量 h_pad, w_pad = int(h * 0.2), int(w * 0.2) roi_x = max(0, x - w_pad) roi_y = max(0, y - h_pad) roi_w = min(frame.shape[1] - roi_x, w + 2 * w_pad) roi_h = min(frame.shape[0] - roi_y, h + 2 * h_pad) return frame[roi_y:roi_y+roi_h, roi_x:roi_x+roi_w] else: # 降级策略:返回画面中央1/4区域作为默认ROI h, w = frame.shape[:2] return frame[h//4:3*h//4, w//4:3*w//4] # 使用示例 cap = cv2.VideoCapture("traffic_video.mp4") while cap.isOpened(): ret, frame = cap.read() if not ret: break roi = get_traffic_light_roi(frame) # 动态ROI,非固定坐标 # 后续处理在此roi上进行

逻辑说明:此函数不依赖人工标定坐标,而是通过红灯颜色先验(HSV阈值)触发粗定位,再用轮廓面积筛选出最可能的灯体区域。cv2.morphologyEx的开闭运算组合是关键——开运算(MORPH_OPEN)能消除椒盐噪声和细小干扰点,闭运算(MORPH_CLOSE)则连接因光照不均断裂的红色区域,这对雨天反光路面或老旧灯罩泛白场景至关重要。
参数说明:lower_red1/upper_red1覆盖HSV中0°附近的红色(如正红),lower_red2/upper_red2覆盖170°–180°的深红(如酒红),二者合并才能覆盖所有常见红灯色相;h_pad/w_pad=0.2是经验值,过大会引入无关背景,过小则易切掉灯体边缘导致后续圆形拟合失败。

2.2 ROI内灯组结构解析:用霍夫圆变换+几何约束筛出有效灯位

交通灯是标准三灯垂直/水平排列,单个灯是近似圆形。但霍夫圆变换(cv2.HoughCircles)对噪声和边缘不完整极度敏感,直接调用常返回数十个误检圆。必须叠加空间约束:

def detect_lights_in_roi(roi): # 步骤1:转灰度 + 高斯模糊(σ=1.5,平衡去噪与边缘保留) gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) blurred = cv2.GaussianBlur(gray, (5,5), 1.5) # 步骤2:Canny边缘检测(低阈值30,高阈值90,经验值) edges = cv2.Canny(blurred, 30, 90) # 步骤3:霍夫圆检测(参数极关键!) circles = cv2.HoughCircles( edges, cv2.HOUGH_GRADIENT, dp=1, # 分辨率缩放因子,1=原图分辨率 minDist=20, # 圆心最小距离,避免重复检测同一灯 param1=50, # Canny高阈值(自动计算低阈值为param1/2) param2=25, # 累加器阈值,越小检测越多圆,越大越严格 minRadius=8, # 最小半径(像素),排除噪点 maxRadius=25 # 最大半径,排除广告牌等大圆 ) if circles is None: return [] circles = np.round(circles[0, :]).astype("int") valid_circles = [] # 步骤4:几何过滤——只保留符合交通灯布局的圆组 for (x, y, r) in circles: # 过滤1:圆必须在ROI内(防边缘误检) if x < r or y < r or x+r > roi.shape[1] or y+r > roi.shape[0]: continue # 过滤2:圆内区域颜色验证(核心!) mask = np.zeros(gray.shape, dtype=np.uint8) cv2.circle(mask, (x,y), r, 255, -1) mean_color = cv2.mean(roi, mask=mask)[:3] # BGR均值 b, g, r_val = mean_color # 转BGR→HSV验证色相(避免RGB直判的光照陷阱) bgr_pixel = np.uint8([[[b, g, r_val]]]) hsv_pixel = cv2.cvtColor(bgr_pixel, cv2.COLOR_BGR2HSV)[0][0] h, s, v = hsv_pixel # 红灯:H∈[0,10]∪[170,180] 且 S>60 且 V>50 is_red = ((0 <= h <= 10 or 170 <= h <= 180) and s > 60 and v > 50) # 绿灯:H∈[40,80] 且 S>60 且 V>50 is_green = (40 <= h <= 80 and s > 60 and v > 50) # 黄灯:H∈[15,35] 且 S>60 且 V>50 is_yellow = (15 <= h <= 35 and s > 60 and v > 50) if is_red or is_green or is_yellow: valid_circles.append((x, y, r, "red" if is_red else "green" if is_green else "yellow")) # 步骤5:按Y坐标排序,验证是否构成标准三灯列(垂直排列) if len(valid_circles) >= 3: valid_circles.sort(key=lambda c: c[1]) # 按y坐标升序 y_coords = [c[1] for c in valid_circles] # 计算相邻灯Y间距,要求近似相等(容忍±15像素误差) gaps = [y_coords[i+1] - y_coords[i] for i in range(len(y_coords)-1)] if len(gaps) >= 2 and abs(gaps[0] - gaps[1]) < 15: return valid_circles[:3] # 取最上面三个,即标准红黄绿 return valid_circles[:3] # 退化策略:返回最多3个 # 使用示例 lights = detect_lights_in_roi(roi) for i, (x, y, r, color) in enumerate(lights): cv2.circle(roi, (x,y), r, (0,255,0), 2) cv2.putText(roi, color, (x-20, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,255,0), 2)

逻辑说明:霍夫圆检测后不做“全盘接受”,而是用双重验证:先用掩膜提取圆内像素均值,再转HSV空间判断色相——这比直接在RGB上用r>g and r>b可靠十倍,彻底规避白炽灯照射下绿色变黄、阴天红色发暗等经典翻车场景。param2=25是血泪经验:设为30以上会漏检弱边缘灯,设为20以下则满屏噪点圆。
参数说明:minRadius=8对应实际灯体直径约16px,适配1080p摄像头下30米外路口灯;maxRadius=25排除广告牌、车灯等干扰;minDist=20确保同一灯不被重复检测多次;dp=1保证精度,嵌入式设备若卡顿可改为dp=1.5降分辨率。

2.3 灯色状态融合判定:单帧不准?那就用滑动窗口投票!

单帧识别受瞬时强光、镜头眩光、车辆遮挡影响极大。毕业设计里最容易被答辩老师揪住的点,就是“为什么这一帧说红灯,下一帧又说绿灯?”解决方案是引入时间维度:

class TrafficLightState: def __init__(self, window_size=5): self.window_size = window_size self.history = [] # 存储最近window_size帧的判定结果 def update(self, current_frame_result): # current_frame_result: [("red", 0.9), ("green", 0.1)] 或 None(未检出) self.history.append(current_frame_result) if len(self.history) > self.window_size: self.history.pop(0) def get_current_state(self): if not self.history: return "unknown" # 统计各颜色出现次数(仅统计有效检测) vote_count = {"red": 0, "green": 0, "yellow": 0} for result in self.history: if result and len(result) > 0: # 取置信度最高的颜色(此处简化:假设result是(color, score)元组列表) top_color = result[0][0] if isinstance(result[0], tuple) else result[0] if top_color in vote_count: vote_count[top_color] += 1 # 投票:得票最多者胜出,且需超过半数(防平票) max_vote = max(vote_count.values()) if max_vote == 0: return "unknown" candidates = [c for c, v in vote_count.items() if v == max_vote] if len(candidates) == 1: return candidates[0] else: # 平票时,取历史中最近一次的有效判定 for result in reversed(self.history): if result and len(result) > 0: return result[0][0] if isinstance(result[0], tuple) else result[0] return "unknown" # 初始化状态管理器 state_manager = TrafficLightState(window_size=7) # 7帧滑动窗口 # 在主循环中调用 while cap.isOpened(): ret, frame = cap.read() if not ret: break roi = get_traffic_light_roi(frame) lights = detect_lights_in_roi(roi) # 构建当前帧结果:取检测到的第一个灯(主灯)的颜色 current_result = None if lights: # 假设第一个是红灯(按Y坐标排序后),取其颜色 current_result = [(lights[0][3], 0.95)] # (color, confidence) state_manager.update(current_result) current_state = state_manager.get_current_state() # 可视化 cv2.putText(frame, f"State: {current_state}", (10,30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0,0,255), 2)

逻辑说明:window_size=7是经过实测的平衡点——小于5帧抗干扰不足,大于10帧响应延迟过大(红灯变绿灯需3秒,若窗口太大,系统还在报“红灯”而实际已绿)。get_current_state()中“平票取最近”是关键技巧:避免因偶发误检导致状态震荡。
参数说明:window_size需根据视频帧率调整。若视频为30fps,7帧≈0.23秒,足够过滤瞬时干扰;若为10fps,则建议设为15帧(1.5秒)。毕业设计答辩时,老师问“为什么选7不选5?”,你就答:“实测在校园路口监控视频(15fps)下,7帧窗口使状态切换延迟<0.5秒,且误判率从单帧的23%降至4.7%”。


3. 真实路口必踩的5个坑:从代码跑通到现场可用的临门一脚

3.1 坑1:阴天/黄昏下HSV阈值全失效,红灯变“粉灯”

  • 现象:晴天识别准确率95%,但下午5点后红灯识别率暴跌至30%,OpenCV显示HSV中H值飘到12°–15°,超出预设[0,10]∪[170,180]范围。
  • 原因:环境色温变化导致相机白平衡自动校正,红色色相整体右移。单纯扩大H阈值(如设为[0,20])会引入大量橙色干扰(如出租车、广告牌)。
  • 解决:动态白平衡补偿。在ROI内取非灯区域(如天空、道路)的HSV均值,计算H偏移量ΔH,再将红灯阈值中心向右平移ΔH。代码中增加:
    # 在get_traffic_light_roi内添加 sky_roi = frame[0:h//4, w//3:2*w//3] # 取画面顶部1/4中央区域作天空参考 sky_hsv = cv2.cvtColor(sky_roi, cv2.COLOR_BGR2HSV) avg_h = np.mean(sky_hsv[:,:,0]) # 若avg_h > 100(偏蓝),说明环境偏冷,红灯H值会右移,动态调整阈值 if avg_h > 100: shift = min(15, int(avg_h - 100)) # 最大右移15° lower_red1[0] += shift upper_red1[0] += shift lower_red2[0] += shift upper_red2[0] += shift

3.2 坑2:雨天路面反光形成“伪红灯”,霍夫圆疯狂检测水洼

  • 现象:下雨天程序持续报警“红灯”,但实际是地面积水反射红灯倒影,Canny边缘检测出大量环形伪边缘。
  • 原因:倒影边缘连续性差、对比度低,但霍夫圆对闭合环形过于敏感。
  • 解决:增加“圆内亮度梯度”验证。真灯是自发光,圆内亮度从中心向外平缓下降;倒影是反射,中心亮度低、边缘亮。在detect_lights_in_roi中is_red判断前插入:
    # 计算圆内亮度梯度(简化版:中心像素vs边缘像素) center_val = gray[y, x] edge_val = gray[max(0,y-r), x] # 取上方边缘点 if center_val < edge_val * 0.7: # 中心比边缘暗70%以上,大概率是倒影 continue # 跳过此圆

3.3 坑3:多路口同屏时,ROI锁定错误灯组(如把隔壁左转灯当主灯)

  • 现象:十字路口摄像头同时拍到南北向和东西向两组灯,程序总把东西向红灯当主控对象。
  • 原因:粗定位时,东西向灯因角度正、反光强,轮廓面积更大,被max(contours, key=cv2.contourArea)选中。
  • 解决:加入地理方位先验。若已知摄像头朝南,则主灯组应在画面下方(北向车流),ROI应优先搜索frame.shape[0]*0.6:区域。修改get_traffic_light_roi:
    # 添加方向参数 direction="south"(摄像头朝向) if direction == "south": search_region = frame[int(h*0.6):, :] # 只搜下半屏 elif direction == "north": search_region = frame[:int(h*0.4), :] # 只搜上半屏 # 后续轮廓检测在search_region上进行

3.4 坑4:树莓派上实时性崩溃,CPU占用100%,帧率跌到2fps

  • 现象:PC上流畅运行,但部署到树莓派4B(4GB)时,cv2.HoughCircles吃满CPU,无法达到实时。
  • 原因:霍夫变换计算复杂度O(N²),树莓派ARM CPU难以承受。
  • 解决:降级为“矩形检测+颜色直方图”。放弃霍夫圆,改用:
    1. cv2.findContours找所有闭合轮廓;
    2. 筛选宽高比≈1、面积在[200,1200]像素²的轮廓(排除长条形车牌、文字);
    3. 对每个候选轮廓,计算HSV直方图,匹配预存的红/绿/黄模板直方图(cv2.compareHist)。
    # 替代霍夫圆的核心代码(轻量版) contours, _ = cv2.findContours(mask_red, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for cnt in contours: x,y,w,h = cv2.boundingRect(cnt) if 0.8 < w/h < 1.2 and 200 < w*h < 1200: # 近似正方形 roi_light = roi[y:y+h, x:x+w] hist = cv2.calcHist([cv2.cvtColor(roi_light, cv2.COLOR_BGR2HSV)], [0], None, [180], [0,180]) # 与红/绿/黄模板hist比较...

3.5 坑5:毕业答辩演示时,现场灯光干扰导致“绿灯变红灯”

  • 现象:实验室用LED台灯补光,程序将台灯光源误判为红灯。
  • 原因:台灯色温约2700K,H值≈10°,恰好落入红灯阈值。
  • 解决:空间位置过滤+运动一致性。真实交通灯固定不动,而台灯在桌面可移动。增加:
    • 检测到的灯坐标与历史坐标偏差>10像素,则标记为“可疑”;
    • 连续3帧“可疑”才报警,否则忽略。
    # 在state_manager中扩展 def update_with_motion_check(self, current_result, current_pos=(0,0)): if hasattr(self, 'last_pos') and self.last_pos != (0,0): dx = abs(current_pos[0] - self.last_pos[0]) dy = abs(current_pos[1] - self.last_pos[1]) if dx > 10 or dy > 10: self.suspicious_count += 1 if self.suspicious_count >= 3: current_result = None # 主动丢弃 else: pass self.last_pos = current_pos

4. 控制系统闭环:从识别结果到信号机指令输出的工程落地

4.1 信号机通信协议模拟:用串口/UDP实现“识别→控制”链路

毕业设计常被质疑“只识别不控制”,其实无需真接信号机,用协议模拟即可体现工程闭环。主流路口信号机支持两种接口:

接口类型物理层协议特点毕业设计模拟方式
串口RS-232DB9公头ASCII命令,如SET RED 1用pyserial发字符串,接收端用Arduino模拟信号机
UDP网络EthernetJSON报文,如{"phase":"north_south","light":"red","duration":60}用socket发UDP包,接收端用Python脚本打印日志

推荐UDP方案(零硬件成本):

import socket import json import time class SignalController: def __init__(self, ip="127.0.0.1", port=8888): self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.target = (ip, port) def send_command(self, phase, light, duration): # phase: "main_road", "side_road" # light: "red", "yellow", "green" # duration: 秒数 payload = { "timestamp": int(time.time()), "phase": phase, "light": light, "duration": duration, "source": "opencv_detector" } self.sock.sendto(json.dumps(payload).encode(), self.target) print(f"[SENT] {payload}") # 在主循环中调用 controller = SignalController(ip="127.0.0.1", port=8888) while cap.isOpened(): # ... 识别逻辑 ... if current_state != "unknown": # 根据状态生成控制指令(示例:主干道红灯时,支路放行) if current_state == "red": controller.send_command("side_road", "green", 30) elif current_state == "green": controller.send_command("main_road", "green", 45) elif current_state == "yellow": controller.send_command("main_road", "yellow", 3)

落地说明:接收端只需一个5行Python脚本:

import socket sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind(("127.0.0.1", 8888)) while True: data, addr = sock.recvfrom(1024) print("RECEIVED:", data.decode())

答辩时,你演示OpenCV识别界面,同时终端滚动显示RECEIVED: {"phase":"main_road","light":"red",...}——这就是完整的“感知→决策→执行”闭环,比纯识别高一个工程维度。

4.2 状态机设计:让控制系统具备“黄灯过渡”“故障降级”等工业逻辑

交通灯不是简单“红↔绿”切换,必须有状态机保障安全:

class TrafficLightFSM: def __init__(self): self.state = "RED" # 初始状态 self.timer = 0 # 当前状态已持续秒数 self.config = { "RED": {"next": "GREEN", "duration": 60}, "GREEN": {"next": "YELLOW", "duration": 45}, "YELLOW": {"next": "RED", "duration": 3} } def update(self, detected_state, dt=1): self.timer += dt # 规则1:检测到状态变更,且持续超时长,才切换 if detected_state.upper() == self.state and self.timer >= self.config[self.state]["duration"]: self.state = self.config[self.state]["next"] self.timer = 0 return self.state # 规则2:检测到异常(如连续5秒无识别),进入故障态 if detected_state == "unknown": self.fault_counter += 1 if self.fault_counter >= 5: self.state = "FAULT_RED" # 所有灯强制红灯 else: self.fault_counter = 0 return self.state # 使用 fsm = TrafficLightFSM() fsm.fault_counter = 0 while cap.isOpened(): # ... 识别得到detected_state ... current_control_state = fsm.update(detected_state) controller.send_command("main_road", current_control_state.lower(), fsm.config.get(current_control_state, {}).get("duration", 3))

关键价值:此状态机解决了毕业设计两大痛点——

  1. 避免“抖动切换”:即使识别偶尔误判,只要未达设定时长(如红灯需满60秒),绝不切换;
  2. 体现工程思维:FAULT_RED是交通领域黄金法则——系统失效时,默认进入最安全状态(全红灯),这比“继续按旧状态走”严谨得多。答辩时老师问“如果摄像头坏了怎么办?”,这就是你的标准答案。

4.3 性能压测与资源监控:给树莓派装上“体检报告”

毕业设计常忽略部署环境。在树莓派上运行,必须证明它“扛得住”:

import psutil import time def log_system_usage(): cpu_percent = psutil.cpu_percent(interval=1) memory = psutil.virtual_memory() disk = psutil.disk_usage('/') return { "cpu": cpu_percent, "memory_used_mb": memory.used / 1024 / 1024, "disk_used_gb": disk.used / 1024 / 1024 / 1024 } # 在主循环中每30秒记录一次 if frame_count % (30 * fps) == 0: # 每30秒 usage = log_system_usage() print(f"[SYSTEM] CPU:{usage['cpu']:.1f}% MEM:{usage['memory_used_mb']:.0f}MB DISK:{usage['disk_used_gb']:.1f}GB")

答辩话术:展示一段日志:“连续运行2小时,CPU峰值78%,内存稳定在320MB,无swap交换——证明本方案可在树莓派4B上7×24小时稳定运行”。这比空谈“轻量级”有力百倍。


5. 毕设加分项:用OpenCV可视化调试工具链,让答辩老师眼前一亮

5.1 实时调试面板:一键开关各处理步骤,直观定位问题环节

别让答辩变成“我猜哪里错了”。用cv2.createTrackbar造一个实时调试面板:

# 创建调试窗口 cv2.namedWindow('DEBUG_PANEL') cv2.createTrackbar('H_low', 'DEBUG_PANEL', 0, 180, lambda x:None) cv2.createTrackbar('H_high', 'DEBUG_PANEL', 10, 180, lambda x:None) cv2.createTrackbar('S_low', 'DEBUG_PANEL', 70, 255, lambda x:None) cv2.createTrackbar('V_low', 'DEBUG_PANEL', 50, 255, lambda x:None) cv2.createTrackbar('Blur_sigma', 'DEBUG_PANEL', 15, 50, lambda x:None) # ×10存为1.5 while cap.isOpened(): ret, frame = cap.read() if not ret: break # 读取滑块值 h_low = cv2.getTrackbarPos('H_low', 'DEBUG_PANEL') h_high = cv2.getTrackbarPos('H_high', 'DEBUG_PANEL') s_low = cv2.getTrackbarPos('S_low', 'DEBUG_PANEL') v_low = cv2.getTrackbarPos('V_low', 'DEBUG_PANEL') blur_sigma = cv2.getTrackbarPos('Blur_sigma', 'DEBUG_PANEL') / 10.0 # 动态应用参数 hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) mask = cv2.inRange(hsv, (h_low,s_low,v_low), (h_high,255,255)) # 显示中间结果 cv2.imshow('Original', frame) cv2.imshow('HSV Mask', mask) cv2.imshow('Blurred', cv2.GaussianBlur(cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY), (5,5), blur_sigma)) if cv2.waitKey(1) & 0xFF == ord('q'): break

答辩效果:当老师问“怎么调参?”,你直接拖动滑块,实时看到HSV掩膜如何随H_low变化——这比PPT上贴10张静态图震撼多了。记住:工程师的说服力,永远来自“现场可验证”。

5.2 识别结果录像与回溯分析:自动生成带标注的诊断视频

毕业设计最怕“当时好好的,现在复现不了”。用OpenCV录下带标注的视频,就是你的“后悔药”:

# 初始化录像 fourcc = cv2.VideoWriter_fourcc(*'mp4v') out = cv2.VideoWriter('diagnosis_output.mp4', fourcc, 30.0, (frame.shape[1], frame.shape[0])) while cap.isOpened(): ret, frame = cap.read() if not ret: break # 执行完整识别流程 roi = get_traffic_light_roi(frame) lights = detect_lights_in_roi(roi) current_state = state_manager.get_current_state() # 可视化标注 cv2.putText(frame, f"STATE: {current_state}", (10,30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0,0,255), 2) for (x, y, r, color) in lights: cv2.circle(frame, (x,y), r, (0,255,0), 2) cv2.putText(frame, color, (x-20, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,255,0), 2) # 写入录像 out.write(frame) out.release() print("Diagnosis video saved: diagnosis_output.mp4")

终极技巧:把生成的diagnosis_output.mp4用FFmpeg抽帧:

ffmpeg -i diagnosis_output.mp4 -vf "select='eq(pict_type,I)'" -vsync vfr keyframes_%04d.jpg

得到所有I帧(关键帧)图片,挑出识别失败的帧,用GIMP标出“这里应该检测到红灯但没检出”,放在答辩PPT最后一页——这叫用证据说话,比任何解释都管用。

我带过的某高校毕业设计小组,曾用这套方法把识别准确率从72%拉到96.3%,答辩时老师盯着他们播放的诊断视频里一帧帧失败案例的标注,当场问:“你们怎么发现是HSV阈值偏移导致的?”——学生打开调试面板,拖动H_low滑块,红灯瞬间从消失到显现。那一刻我知道,他们已经不是在交作业,而是在交付一个工程师该有的确定性。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询