ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

OpenCV+YOLO+Streamlit 交通监控系统实战指南

OpenCV+YOLO+Streamlit 交通监控系统实战指南 简介基于OpenCV、YOLO与Streamlit的智能交通监控系统是一套完整代码包适合计算机视觉初学者、数据应用开发者以及智慧城市相关项目人员学习参考。项目中通过OpenCV完成视频流捕获与预处理调用YOLO模型对车辆、行人等目标进行实时检测与计数并借助Streamlit搭建可交互的监控展示界面能帮助理解目标检测模型在真实交通场景中的落地流程。包体共11个文件包含4个Python脚本、1个YAML配置、2个模型权重.pt、说明文档及运行依赖清单等整体大小约11.1MB结构清晰、便于本地运行调试。目前已有125人学习适合想要快速上手目标检测与Web可视化结合开发的学习者。1. 为什么用 OpenCV、YOLO 和 Streamlit 搭交通监控交通监控系统最容易被低估的地方是它根本不是一个纯目标检测问题。摄像头画面里同时存在运动车辆、静止车辆、行人、阴影、光照突变还要在长周期里回答“这条路现在堵不堵、过去五分钟过了多少辆车”这类统计问题。只用 YOLO 做单帧检测得到的是散落的框只用 OpenCV 做背景建模得到的是缺少语义的轮廓。这套三件套组合的合理性在于OpenCV 负责视频采集和图像预处理YOLO 提供带类别语义的目标框Streamlit 把实时推理结果变成可交互的 Web 界面。对需要快速验证算法效果、又不想先写一个前端的团队来说这是一个性价比很高的技术栈。本文会从模型选型、采样策略、车道虚拟线和参数调优几个层面把这套方案掰开讲清楚适合正在做智慧交通 Demo、毕设、园区安防原型或者想把已有 YOLO 模型快速 Web 化的开发者。2. 先定技术底座YOLO 版本选择、OpenCV 与 Streamlit 的职责边界2.1 YOLO 第几代了当前选型该怎么看YOLO 已经迭代了很多版本YOLOv5、YOLOv8、YOLO11 是实际工程里最常被讨论的几个。很多人纠结“yolo第几代了”这个问题其实在交通监控场景里更值得关注的是两个维度一是模型的部署成熟度二是训练自定义数据集的资料丰富度。YOLOv8 在 Ultralytics 框架下同时支持检测、实例分割和姿态估计生态完整YOLO11 在精度上有进一步提升但对部分旧显卡的部署体验不如 v8 顺畅。我的建议是新项目首选 YOLOv8因为它在 COCO 上的预训练权重覆盖了 car、bus、truck、bicycle、person 等交通场景高频类别且导出 ONNX、TensorRT 的资料最多。如果对特定场景比如远距离小目标车辆有精度要求再考虑升级到 YOLO11 并在自建数据集上微调。从损失函数角度看YOLOv8 使用 anchor-free 检测头分类损失用 BCE回归损失用 CIoU。理解这一点对调参很有帮助当画面里车辆框偏大或偏小你需要关注的是回归损失对尺度变化的敏感度而不是盲目调整 anchor 尺寸。交通监控中常见的“车尾对车尾”重叠、夜间灯光造成的漏检往往通过调整置信度阈值和 NMS 的 IoU 阈值就能改善不一定需要改网络结构。2.2 OpenCV 在管线里的三个角色采集、预处理、可视化后处理OpenCV 在交通监控系统里承担三个不可或缺的工作。第一是视频采集通过cv2.VideoCapture打开本地视频文件或摄像头第二是图像处理包括缩放到模型输入尺寸、BGR 与 RGB 色彩空间转换、图像增强第三是可视化后处理把 YOLO 检测出的框、类别和置信度绘制到原始帧上。很多人会忽略一个细节YOLO 训练时用的是 RGB 图像而 OpenCV 读取的图像默认是 BGR 顺序。如果不做转换直接送入模型检测结果会有明显的精度下降。这是新手最容易踩的坑后面代码示例里会反复强调。import cv2 cap cv2.VideoCapture(traffic.mp4) if not cap.isOpened(): raise IOError(无法打开视频源请检查路径或摄像头索引) ret, frame cap.read() rgb_frame cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)这段代码完成的是最基础的采集 色彩转换流程。cap.read()同时返回读取状态和图像数据读取失败时ret为 False。实际项目中要特别注意摄像头断连后read()会持续返回 False需要在循环里做重连处理否则 Streamlit 页面会白屏卡死。2.3 Streamlit 的定位它不负责算法只负责把状态变成界面Streamlit 是一个 Python Web 框架核心价值在于“用脚本方式构建交互界面”。在交通监控系统里Streamlit 负责三件事展示实时视频流、提供参数调节控件、呈现统计数据。需要明确的是Streamlit 默认是请求-响应模型每次交互都会重跑整个脚本。这对视频流处理是个巨大挑战——如果每一帧都触发脚本重跑系统会立刻崩溃。所以正确的架构是在 Streamlit 外部维护一个视频处理线程把最新帧放入缓存Streamlit 界面通过读取缓存来刷新画面。2.4 环境搭建与依赖安装创建虚拟环境并安装依赖时有一个顺序问题必须先装 PyTorch再装 Ultralytics最后装 OpenCV。因为 Ultralytics 会自动检测已安装的 PyTorch 版本来决定是否启用 GPU 加速。如果反过来先装 Ultralytics它会拉取默认的 CPU 版本 PyTorch后续想要切换 CUDA 版本就得重新安装。conda create -n traffic python3.10 -y conda activate traffic pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics opencv-python streamlit pillow numpy安装完成后可以用一行命令验证环境是否正常import cv2, torch, streamlit print(OpenCV:, cv2.__version__) print(PyTorch CUDA:, torch.cuda.is_available()) print(Streamlit:, streamlit.__version__)环境问题中最常见的是ModuleNotFoundError: No module named cv2这通常是因为装在了错误的 Python 环境里。另外必须提一句如果使用 AMD 显卡跑 YOLOPyTorch 的 CUDA 支持默认不可用需要在 Linux 下安装 ROCm 版本的 PyTorch。在实际项目里我一般会先在服务器或本机 CPU 环境下跑通整个流程再切换到 GPU 优化推理速度。3. 核心实现YOLO 推理封装与 OpenCV 视频流处理3.1 设计一个可复用的检测器类交通监控系统中YOLO 推理不应该和业务逻辑混在一起。我一般会定义一个TrafficDetector类把模型加载、推理、结果转换封装成统一接口。这样后续无论是接入新的 YOLO 版本还是切换到 TensorRT 加速都只需要改动这一个类。import cv2 import numpy as np from ultralytics import YOLO class TrafficDetector: def __init__(self, model_pathyolov8n.pt, conf_thres0.35, devicecpu): self.model YOLO(model_path) self.conf_thres conf_thres self.device device self.target_classes [0, 1, 2, 3, 5, 7] # COCO类别ID: person0, bicycle1, car2, motorcycle3, bus5, truck7 def predict(self, frame_bgr): rgb cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB) results self.model.predict(rgb, confself.conf_thres, deviceself.device, verboseFalse) return self._parse_results(results[0]) def _parse_results(self, result): boxes result.boxes.xyxy.cpu().numpy() confs result.boxes.conf.cpu().numpy() cls_ids result.boxes.cls.cpu().numpy().astype(int) return boxes, confs, cls_ids这里有几个关键参数需要解释。conf_thres是置信度阈值低于这个值的检测结果会被过滤掉。交通场景里我习惯设置在 0.3 到 0.45 之间设得太低会有大量误检太高则漏检远距离的小车。target_classes限定了我们关注的类别——COCO 数据集有 80 个类别但交通监控只需要其中几个。这样过滤可以避免把路边的猫狗识别成障碍物。.cpu().numpy()的作用是把 YOLO 输出的张量从 GPU 或 CPU 设备转移到 numpy 数组以便传给 OpenCV 进行后续绘制和处理。3.2 视频帧读取与帧率控制为什么不能逐帧推理交通监控实际部署中YOLO 推理速度往往跟不上视频帧率。假设摄像头输出 30 FPS而 YOLOv8s 在 GPU 上推理需要 35 毫秒每秒只能处理约 28 帧这就形成了瓶颈。更严重的是相邻帧之间的车辆位置变化很小逐帧推理的增益极其有限。我的做法是采用“跳帧检测 全帧显示”的策略视频流全速播放但只每隔 N 帧做一次 YOLO 检测中间未检测的帧直接使用上一帧的检测结果绘制。class VideoStreamHandler: def __init__(self, source_path, detect_interval2): self.cap cv2.VideoCapture(source_path) self.detect_interval detect_interval self.frame_count 0 def read_and_detect(self, detector): ret, frame self.cap.read() if not ret: return None, None self.frame_count 1 boxes, confs, cls_ids None, None, None if self.frame_count % self.detect_interval 0: boxes, confs, cls_ids detector.predict(frame) return frame, (boxes, confs, cls_ids)detect_interval是一个需要根据硬件条件调整的参数。GPU 推理能力强时设为 1 或 2CPU 环境下建议设为 3 到 5。这个参数的另一个作用是控制车辆的“轨迹连续性”间隔太大会导致同一辆车在相邻两次检测中位置跳变明显影响后续的测速和统计。3.3 OpenCV 坐标系与画框Rect 函数的正确用法OpenCV 中所有图像操作都基于像素坐标系原点在左上角x 轴向右y 轴向下。YOLO 输出的xyxy格式是[x1, y1, x2, y2]分别代表左上角和右下角的坐标。很多从目标检测入门的人会混淆cols和rowsframe.shape[0]是高度rowsframe.shape[1]是宽度cols。在画框、裁剪、计算车辆中心点时这个坐标系理解必须准确。def draw_detections(frame, boxes, confs, cls_ids, class_names): for box, conf, cls_id in zip(boxes, confs, cls_ids): x1, y1, x2, y2 [int(v) for v in box] label f{class_names[cls_id]} {conf:.2f} cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, label, (x1, y1 - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) return frame画框逻辑中cv2.rectangle接收的是左上角和右下角坐标颜色使用 BGR 格式(0, 255, 0)是绿色。cv2.putText的文字位置使用了y1 - 8这是为了让文字避开检测框的上边框避免遮挡。当车辆在画面边缘时y1 - 8可能变成负值导致文字绘制报错。稳妥的做法是加一个max(0, y1 - 8)保护。4. 交通统计实战虚拟线计数、车速估计与拥堵判定4.1 基于虚拟线的车流量统计交通监控最基础的需求是统计“单位时间内通过了多少辆车”。常见做法是“区域检测”即只要车辆检测框出现在画面中就计数。这种方案误差极大——同一辆车如果连续被检测到 20 帧会被计数 20 次。标准解法是定义一条虚拟线virtual line只有车辆中心点跨过这条线时才计数一次且同一辆车只计一次。class CrossingCounter: def __init__(self, line_y, min_gap0.5): self.line_y line_y # 虚拟线的y坐标 self.min_gap min_gap # 两次计数的最小间隔秒 self.last_count_time {} self.total_count 0 def update(self, boxes, frame_time): count_now 0 for box in boxes: x1, y1, x2, y2 box center_y (y1 y2) / 2 center_x (x1 x2) / 2 if abs(center_y - self.line_y) 10: car_key f{int(center_x // 50)} last_time self.last_count_time.get(car_key, 0) if frame_time - last_time self.min_gap: self.total_count 1 self.last_count_time[car_key] frame_time count_now 1 return count_now这个实现里有两个值得注意的设计。第一判断条件用的是abs(center_y - self.line_y) 10而不是单纯判断是否大于或小于。这样即使车辆在一帧内从线的上方跳到下方也能捕捉到跨线事件。第二car_key用center_x // 50做了粗略的空间分桶是为了避免画面中两辆并行的车在几乎相同时间通过时被误判为同一辆。这个分桶粒度需要根据摄像头的视角调整摄像头视野越宽桶的长度应该越大。4.2 车速估计的最小可行方案没有多个摄像头标定信息单目视觉测速很难做到精准。但如果只是做相对车速展示可以用“位移/时间”的近似方法记录同一辆车在前后两次检测框的位置和时间用中心点位移除以时间差。class SpeedEstimator: def __init__(self, pixels_per_meter50): self.tracks {} self.ppm pixels_per_meter def estimate(self, box, current_time): x1, y1, x2, y2 box center ((x1 x2) / 2, (y1 y2) / 2) track self.tracks.get(id(box), None) if track is None: self.tracks[id(box)] (center, current_time) return None prev_center, prev_time track dt current_time - prev_time if dt 0: return None dx center[0] - prev_center[0] dy center[1] - prev_center[1] distance_px (dx ** 2 dy ** 2) ** 0.5 speed_kmh (distance_px / self.ppm) / dt * 3.6 return max(speed_kmh, 0)这里的pixels_per_meter是像素与实际距离的换算比例需要根据监控场景手动标定。比如知道一段路面实际长 10 米在画面中占 500 像素则比例是 50 像素/米。这种测速方法存在的固有问题是车辆从远处驶向摄像头时位移速度被高估车辆横穿画面时相对准确。所以它适合做趋势展示不适合做罚单依据。4.3 拥堵判定指标占有率比数量更可靠单纯统计画面里有多少辆车无法准确反映拥堵程度。同样 5 辆车分散在 200 米长的路段中央通畅挤在摄像头正下方就是拥堵。更可靠的指标是“空间占有率”——所有车辆检测框面积之和与画面面积的比值。def compute_occupancy(boxes, frame_area): if len(boxes) 0: return 0.0 overlap_area 0.0 for box in boxes: x1, y1, x2, y2 box area (x2 - x1) * (y2 - y1) overlap_area area return min(overlap_area / frame_area, 1.0) def classify_traffic(occupancy): if occupancy 0.15: return 畅通 elif occupancy 0.35: return 缓行 else: return 拥堵这个方案存在小的误差近处车辆框面积大远距离车辆框面积小导致近处车辆对占有率贡献过高。如果摄像头角度固定可以通过给不同画面区域设置不同的权重系数来修正。实际部署时我们还会结合多个时间窗口的占有率均值来消除检测抖动的影响。4.4 数据存储轻量级方案用 CSV 或 SQLite统计结果需要持久化才能做历史趋势分析。轻量场景下用 SQLite 足够不需要额外安装数据库服务。写入操作放在独立线程中完成避免阻塞推理主循环。import sqlite3 class TrafficStore: def __init__(self, db_pathtraffic.db): self.conn sqlite3.connect(db_path, check_same_threadFalse) self.conn.execute( CREATE TABLE IF NOT EXISTS traffic_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT, vehicle_count INTEGER, occupancy REAL, status TEXT ) ) def insert_record(self, timestamp, count, occupancy, status): self.conn.execute( INSERT INTO traffic_log (timestamp, vehicle_count, occupancy, status) VALUES (?, ?, ?, ?), (timestamp, count, occupancy, status) ) self.conn.commit()SQLite 的默认线程模式对多线程写入有限制所以连接时要传入check_same_threadFalse并将每个写操作通过锁或队列进行串行化。如果系统部署时长达到数周还需要定期归档和清理旧数据。5. Streamlit 界面设计实时视频流展示与交互控件5.1 使用 st.image 而不是 st.video实时流的核心技巧Streamlit 原生提供的st.video组件只能播放完整的视频文件或 URL它无法接收 Python 端的逐帧图像数据。所以实时监控界面的标准做法是使用st.image放在一个循环里逐帧刷新。import streamlit as st import cv2 st.set_page_config(page_title交通监控系统, layoutwide) st.title(实时交通监控) conf_threshold st.sidebar.slider(置信度阈值, 0.20, 0.80, 0.40) video_source st.sidebar.text_input(视频源路径, valuetraffic.mp4) frame_placeholder st.empty() status_text st.empty() detector TrafficDetector(conf_thresconf_threshold) cap cv2.VideoCapture(video_source) stop_button st.sidebar.button(停止检测) while cap.isOpened() and not stop_button: ret, frame cap.read() if not ret: break boxes, confs, cls_ids detector.predict(frame) annotated draw_detections(frame, boxes, confs, cls_ids) annotated_rgb cv2.cvtColor(annotated, cv2.COLOR_BGR2RGB) frame_placeholder.image(annotated_rgb, channelsRGB) status_text.text(f检测到 {len(boxes)} 个目标)这个代码可以跑通但有一个明显缺陷slider改变置信度阈值会触发整个脚本重跑摄像头会重新打开视频会从头播放。解决这个问题需要借助st.session_state来缓存检测器和视频捕获对象只在首次运行时初始化。5.2 用 session_state 避免重复初始化if detector not in st.session_state: st.session_state.detector TrafficDetector(conf_thres0.4) detector st.session_state.detector detector.conf_thres conf_threshold # 每次重跑时更新阈值通过将检测器实例存入st.session_state模型只需加载一次后续页面重跑时复用同一实例避免了反复加载权重文件的时间开销。同样的机制也适用于视频捕获对象——初始化后放到 session 里这样调整滑块时视频播放进度不会因为重跑而重置。5.3 多列布局展示实时数据监控界面除了视频还需要展示检测到的车辆数量、当前道路状态和拥堵程度。使用st.columns可以把这些指标排列在视频周围。col1, col2, col3 st.columns(3) with col1: st.metric(当前车辆数, current_vehicle_count) with col2: st.metric(累计通过车辆, st.session_state.total_count) with col3: st.metric(道路状态, traffic_status)st.metric组件支持显示数值和上下浮动幅度。对于“当前车辆数”这个指标最好同时展示相对前一秒的变化量例如“2 或 -1”这能从侧面反映流量变化趋势。5.4 在 Streamlit 中展示统计图表用纯 OpenCV 画统计图很麻烦但 Streamlit 直接集成了图表组件。每 10 秒聚合一次检测数据用st.line_chart展示车流量走势。import time import pandas as pd history [] chart_placeholder st.empty() for _ in range(60): sample {time: time.strftime(%H:%M:%S), count: current_vehicle_count} history.append(sample) df pd.DataFrame(history) chart_placeholder.line_chart(df.set_index(time)) time.sleep(1)5.5 Streamlit 中文手册里最容易忽略的坑社区里有人维护了 Streamlit 中文手册里面有一条非常容易踩的坑st.image默认会压缩图像的显示尺寸当图像较大时界面会出现明显的卡顿。解决办法是设置width参数或使用use_container_widthTrue。另一个坑是 Streamlit 的刷新机制——如果大循环里执行了st.sidebar.button且按钮状态发生变化循环会中断并重跑整个脚本。所以在循环中要避免动态创建控件所有控件都应该放在循环开始之前定义。6. 部署与验证从运行日志到参数调优的落地技巧6.1 用录制视频代替实时摄像头验证调试阶段千万不要直接连着摄像头开发。真实摄像头的画面不稳定、光照多变一旦检测效果异常很难判断是算法问题还是画面问题。正确做法是先用手机或监控软件录制一段 10 分钟以上的典型场景视频反复在本地回放验证。这样每次运行结果可复现参数调整的效果能直观对比。摄像头接入时注意检查cv2.VideoCapture的摄像头索引笔记本自带摄像头通常是 0USB 外接摄像头可能是 1 或 2可以通过循环尝试找出正确的索引。6.2 在页面上显示 FPS 和每帧耗时评估系统性能最直接的方式就是显示实时推理耗时。YOLO 推理耗时可以通过time.time()前后插桩计算视频显示帧率用 OpenCV 的cv2.getTickCount()和cv2.getTickFrequency()计算。tick1 cv2.getTickCount() boxes, confs, cls_ids detector.predict(frame) tick2 cv2.getTickCount() infer_time_ms (tick2 - tick1) / cv2.getTickFrequency() * 1000 fps 1.0 / max(infer_time_ms / 1000.0, 0.001)在实际案例中YOLOv8n 在 CPU 上的推理时间大约是 80 到 120 毫秒YOLOv8s 大约是 200 到 300 毫秒。如果推理时间超过 200 毫秒说明需要开启跳帧检测或换更小的模型。将这两个指标显示在界面上可以直观看到瓶颈来自哪个环节。6.3 关键参数速查表与实际调优建议这里整理了一份交通监控场景下的参数推荐表覆盖置信度、NMS IoU 阈值、跳帧间隔和画面缩放四个最常调整的维度。参数推荐范围调小的影响调大的影响conf_thres0.30 - 0.45更多误检、漏检减少漏检增加、误检减少NMS IoU0.40 - 0.60重叠目标分离更严格重叠目标可能合并detect_interval1 - 5更耗算力、轨迹更平滑省算力、轨迹跳变输入分辨率640 - 1280速度快、小目标漏检速度慢、小目标召回提升夜间场景下建议把 conf_thres 调低到 0.25因为夜间车辆特征不明显预训练模型的置信度普遍偏低。白天光线充足时则可以调高到 0.45减少行道树阴影和路灯造成的误检。6.4 用置信度热力图快速定位漏检原因这个技巧很少被提及但非常实用。当画面里明显有车但系统没检测到时可以在调试界面中显示模型对所有可能目标的高置信度区域热力图。具体做法是让 YOLO 返回所有候选框不经过 conf_thres 过滤然后绘制一个半透明的矩形叠加层颜色深浅代表置信度高低。def debug_heatmap(frame, all_boxes, all_confs): overlay frame.copy() for box, conf in zip(all_boxes, all_confs): x1, y1, x2, y2 [int(v) for v in box] alpha min(max(conf, 0), 1) * 0.6 cv2.rectangle(overlay, (x1, y1), (x2, y2), (0, 0, 255), -1) cv2.addWeighted(overlay, alpha, frame, 1 - alpha, 0, frame) return frame通过观察漏检车辆位置在热力图上是否有较浅的红色区域可以判断是置信度阈值设置过高还是该车在模型特征层面就没能被识别出来。如果是前者直接调低阈值即可如果是后者就需要考虑使用更大的模型或在自建数据集上微调。6.5 摄像头断线重连与长时间运行的稳定性保障交通监控系统通常需要 7x24 小时运行摄像头断线是常态。重连逻辑不能是简单的死循环要考虑退避策略否则摄像头刚恢复就被高频重连请求再次压垮。class AutoReconnectCapture: def __init__(self, source, retry_interval5): self.source source self.retry_interval retry_interval self.cap None self._connect() def _connect(self): self.cap cv2.VideoCapture(self.source) if not self.cap.isOpened(): self.cap.release() self.cap None def read(self): if self.cap is None: time.sleep(self.retry_interval) self._connect() return False, None ret, frame self.cap.read() if not ret: self.cap.release() self.cap None return ret, frame这个类的核心思想是读帧失败时立刻释放资源并置空下一次调用时尝试重新连接每次重连之间至少间隔 5 秒。在长时间运行场景下还需要定期检查内存占用——OpenCV 的VideoCapture偶发内存泄漏建议每 4 小时重启一次视频捕获对象。以上这套从模型封装、视频流处理到 Streamlit 界面和参数调优的完整流程已经可以支撑一个真实可用的交通监控原型系统。下一步建议按实际摄像头视角手动标定pixels_per_metter并在正式上线前采集至少一周的数据验证检测和统计的稳定性。本文还有配套的精品资源点击获取
返回列表