ARTICLE DETAIL

资讯详情

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

YOLOv3+PyQt5交通路口智能监控系统实现

YOLOv3+PyQt5交通路口智能监控系统实现 简介基于YOLOv3与PyQt5的交通路口智能监控系统是一套面向计算机视觉与智慧交通初学者的端到端实战源码包。项目采用SRS流媒体服务器、GPU服务器、Local客户端三层架构通过RTMP拉取远端视频流利用YOLO模型对人、车、交通灯等道路目标进行检测识别并支持并发连接客户端则用PyQt5呈现分析结果。压缩包共113个文件以32个py源码、39个pyc编译文件为主配合4个cfg模型配置、8个h5权重/字符识别模型、3个ui界面文件以及视频流、测试图片与使用说明文档可帮助读者理清从视频接入、模型推理到界面展示的完整链路。包体约54.48MB已有130人学习下载。适合想复现智能监控Demo、学习YOLO项目工程化或PyQt5客户端开发的人员按详细安装说明即可快速跑通。1. 基于YOLOv3PyQt5的交通路口智能监控系统是从一个zip包展开的完整工程一个名为“基于yolov3pyqt5开发的交通路口智能监控系统python源码详细安装使用说明.zip”的压缩包碰到的第一个问题不是怎么解压而是怎么让模型在本地环境里真正跑起来。YOLOv3在库层面一直没“过气”OpenCV DNN可以直接读取它的cfg和weights不需要编译DarknetPyQt5则把界面事件循环和OpenCV的视频循环粘合在一起。这套组合做出来的交通路口监控核心价值不是炫技而是让一个普通USB摄像头或一段录像被转换成“画面目标框实时统计数据”的结构化输出同时还能保持桌面程序的交互性。对于刚走出教程、想快速落地一个完整AI监控桌面应用的人来说这是风险最低的起点。2. YOLOv3与PyQt5的组合逻辑先理解检测链路再确定界面线程2.1 YOLOv3 的检测机制简拆从锚框到非极大值抑制YOLOv3把输入图像分成 13×13 到 52×52 的栅格每个栅格预测3个锚框分别覆盖大、中、小目标。这就是它处理交通路口这种“远处是轿车、近处是行人、中间还有电动车”的多尺度场景仍然有效的核心原因。推理时三个输出层会同时给出边界框、置信度和类别分布原始结果里有大量重叠框必须经过非极大值抑制NMS才有清晰结果。工程上你能直接调的就是两个值置信度阈值和NMS阈值。置信度阈值决定一个框是否有资格被输出NMS阈值决定相邻框合并的严格程度。在高位俯拍的监控画面里目标小而且彼此靠近太高的置信度会让行人和自行车直接消失太低的置信度又会让灯杆的影子变成汽车。常见的做法是置信度0.25、NMS0.45先行然后根据具体镜头微调。我一般会把“最高阈值只用于告警、低阈值用于统计”分开而不是追求一个万能值。补充说明YOLOv3的原生COCO模型有80类其中交通场景常用类别只有大约7个稍后会做类别白名单过滤。在OpenCV DNN里模型前向推理只需要几行代码但输出张量的维度是 3×(13×1326×2652×52)×85其中85就是4个框坐标1个置信度80个类别概率解析时要先取后80位做argmax。2.2 为什么用 PyQt5 而不是纯 OpenCV 窗口事件循环和多线程用cv2.imshow做实时视频预览一次只能显示一个窗口按钮、下拉框、检测参数这种交互根本挂不上去。PyQt5的优势是自带事件循环和信号槽可以把“摄像头读取”放到子线程检测完成以后用signal把帧和安全计数传回主线程界面不会因为推理阻塞而卡死。另一个实际原因是PyQt5的安装包在Windows和Linux都有完整二进制遇到显示HTML、嵌入WebView这类需求也能往后扩展虽然这个标题用不到。很多人问PySide6和PyQt5的区别PySide6是目前更默读的官方绑定但PyQt5在新版本里仍支持到Qt5.15API层面两者高度相似zip包源码基于PyQt5时只需要把from PyQt5改成from PySide6大部分代码可以直接迁移。在依赖决定项目走向的前提下我会优先选PyQt5因为技术问答和踩坑记录更多。2.3 搭建环境的最小三件套Python 3.8 opencv-python PyQt5在动手写代码前先用虚拟环境把依赖固定住。这个项目不需要GPU版PyTorch或TensorFlow所有推理都走OpenCV DNN因此CPU机器就能跑关键是版本不要乱选。conda create -n traffic_monitor python3.9 -y conda activate traffic_monitor pip install opencv-python4.8.* numpy pip install pyqt55.15.* -i https://pypi.tuna.tsinghua.edu.cn/simple验证依赖是否装好python -c import cv2, PyQt5.QtCore; print(cv2.__version__, PyQt5.QtCore.PYQT_VERSION_STR)如果打印类似4.8.1 5.15.10说明OpenCV和PyQt5都可以正常导入。要注意的是opencv-python的5.x接口对readNetFromDarknet的改动尚未有稳定文档4.x仍然是最稳的通道。numpy版本最好指定1.21,1.28因为新版numpy对数组内存对齐的检查更严格偶尔会触发OpenCV底层报错。依赖建议版本作用Python3.9 / 3.10兼顾老轮子和新版特性opencv-python4.8.*读视频、加载DNN、画框numpy1.21~1.27矩阵与检测框运算PyQt55.15.*窗口、控件、多线程信号槽这四样就是整个项目的全部运行依赖。建议只装需要的东西不要顺手加一堆科学计算包否则后期排查环境问题时会多很多干扰变量。3. 用代码打通链路YOLOv3检测器封装与PyQt5界面线程3.1 检测器封装解析输出层、过滤类别、绘制目标框把模型加载和推理尽量集中到一个类里。先做到能在命令行看到检测结果再接界面排错会简单得多。下面的YOLOv3Detector用OpenCV DNN完成前向和NMS。import cv2 import numpy as np class YOLOv3Detector: def __init__(self, cfg_path, weight_path, class_path, conf_thres0.25, nms_thres0.45, input_size(416, 416)): self.net cv2.dnn.readNetFromDarknet(cfg_path, weight_path) layer_names self.net.getLayerNames() self.output_layers [layer_names[i - 1] for i in self.net.getUnconnectedOutLayers()] self.classes open(class_path, encodingutf-8).read().strip().split(\n) self.conf_thres conf_thres self.nms_thres nms_thres self.input_size input_size def detect(self, frame): h, w frame.shape[:2] blob cv2.dnn.blobFromImage( frame, 1/255.0, self.input_size, swapRBTrue, cropFalse) self.net.setInput(blob) outputs self.net.forward(self.output_layers) boxes, confidences, class_ids [], [], [] for out in outputs: for detection in out: scores detection[5:] class_id int(np.argmax(scores)) confidence float(scores[class_id]) if confidence self.conf_thres: continue cx, cy, bw, bh detection[:4] x int(cx - bw / 2) y int(cy - bh / 2) boxes.append([x, y, int(bw), int(bh)]) confidences.append(confidence) class_ids.append(class_id) idxs cv2.dnn.NMSBoxes(boxes, confidences, self.conf_thres, self.nms_thres) result frame.copy() count 0 if len(idxs) 0: for i in idxs.flatten(): x, y, bw, bh boxes[i] label self.classes[class_ids[i]] cv2.rectangle(result, (x, y), (x bw, y bh), (0, 255, 0), 2) cv2.putText(result, f{label} {confidences[i]:.2f}, (x, y - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) count 1 return result, count这段代码最关键的是blobFromImage的参数1/255.0把BGR值归一化到0-1416作为输入边长但这样会把原始宽高比拉直小目标会略变形。如果想提高路口小目标的检测率可以把input_size换成(544, 320)或(544, 544)让模型看到更多的像素。getUnconnectedOutLayers拿到的是三个输出层索引forward返回的每个输出数组形状都是(N, 85)N是那个尺度下候选框的数量。下面的参数表是调参时的参考不是固定值参数推荐值调高时调低时conf_thres0.25漏检增多误检减少误检明显增多nms_thres0.45重叠框保留更多相邻目标合并更狠input_size416×416小目标更清晰吞吐下降速度更快小目标易丢在交通路口场景建议先用416跑通再降到320找速度不要一上来就开544。3.2 用QThread跑视频循环避免界面卡成幻灯片PyQt5主线程必须保持空闲才能正常处理鼠标和刷新所以视频循环必须放进子线程。QThread里只需做三件事读帧、调用detector、发射信号。from PyQt5.QtCore import QThread, pyqtSignal class VideoThread(QThread): frame_ready pyqtSignal(object, object, float) def __init__(self, detector, video_source0, parentNone): super().__init__(parent) self.detector detector self.video_source video_source self._running True def run(self): cap cv2.VideoCapture(self.video_source) if not cap.isOpened(): self.frame_ready.emit(None, -1, 0.0) return while self._running: ret, frame cap.read() if not ret: break t0 cv2.getTickCount() rendered, count self.detector.detect(frame) fps cv2.getTickFrequency() / (cv2.getTickCount() - t0) self.frame_ready.emit(rendered, count, fps) cap.release() def stop(self): self._running False self.wait()信号参数用object、object、float是省事写法第一个object传numpy数组第二个object传计数。实际上numpy数组通过信号传递时没有深拷贝所以线程里rendered会在下一轮被覆盖接收槽要立即转成QPixmap或做一次.copy()。这个设计也是常见卡屏点如果直接在信号里发一个大尺寸原始帧主线程会忙于复制而掉帧。3.3 在主窗口里显示检测画面和统计信息把信号接到主窗体的槽函数转成QImage后丢给QLabel。下面是一个最小窗口from PyQt5.QtWidgets import QMainWindow, QLabel, QVBoxLayout, QWidget from PyQt5.QtGui import QImage, QPixmap from PyQt5.QtCore import Qt import cv2 class MainWindow(QMainWindow): def __init__(self, detector): super().__init__() self.detector detector self.video_label QLabel() self.video_label.setFixedSize(960, 540) container QWidget() layout QVBoxLayout(container) layout.addWidget(self.video_label) self.setCentralWidget(container) self.thread VideoThread(detector) self.thread.frame_ready.connect(self.update_frame) self.thread.start() def update_frame(self, frame, count, fps): if frame is None: return rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB).copy() h, w, ch rgb.shape qimg QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888) self.video_label.setPixmap( QPixmap.fromImage(qimg).scaled( self.video_label.width(), self.video_label.height(), Qt.KeepAspectRatio)) self.setWindowTitle(f目标数: {count} | FPS: {fps:.1f})update_frame里那句.copy()不是可有可无QImage构造时不会复制rgb.data的内存如果后面帧数据被覆盖画面会随机出横纹。Qt.KeepAspectRatio避免拉伸变形。这里没有加开始/停止按钮实际工程里可以把按钮的clicked信号对应到thread.stop()和重建线程。4. 交通路口场景的针对性处理和PyQt5安装排错4.1 用COCO类别过滤只保留车辆与行人COCO 80类的YOLOv3权重可以直接用但路口不需要飞机、船、动物。要在detect里只统计目标类别重点不是画框而是让计数和统计只关心交通参与者。用set做白名单过滤ROAD_OBJECTS {0: person, 1: bicycle, 2: car, 3: motorcycle, 5: bus, 7: truck} # 在遍历detection时把下面的判断加到class_id之后 if class_id not in ROAD_OBJECTS: continue注意COCO索引是稀疏排布的4是airplane6是train。很多新手会写class_ids 8来过滤结果把飞机和火车也当道路目标计数凭空多出两类。正确做法是建索引映射表。过滤后还可以给不同类别配不同颜色行人红色、机动车绿色这样在监控画面里更有区分度。这个不改变模型结构只是渲染逻辑。4.2 添加ROI检测区域只在路口车道内计数全局目标数没有业务意义。把画面下方靠近停车线的区域划成ROI只统计ROI内的目标可以避免把马路对面的人行横道行人也算进来。最简单的矩形ROI代码如下def within_roi(box, roi_x1, roi_y1, roi_x2, roi_y2): x, y, bw, bh box cx, cy x bw / 2, y bh / 2 return roi_x1 cx roi_x2 and roi_y1 cy roi_y2 roi (int(w*0.2), int(h*0.45), int(w*0.8), int(h*0.95))把detect方法里画框后的count 1改成if not within_roi(boxes[i], *roi): continue中心点是否落在矩形内是最简单的判断方式。更复杂的多边形ROI要用cv2.pointPolygonTest在编码复杂度上会上一个台阶但对周围要画停车线和导向箭头的路口多边形几乎不可避免。可以先从矩形开始验证数据流程通畅后再升级。4.3 安装 PyQt5 和 OpenCV 最常见的三个错误命令行装PyQt5本身不难但报错频率高于其他Python包。下面是三个最常遇到的坑以及对应的排查路径。错误现象根源处理方式pip install pyqt5超时或找不到版本默认PyPI在这个网络环境下不稳定使用pip install pyqt5 -i https://pypi.tuna.tsinghua.edu.cn/simple启动程序报This application failed to start because no Qt platform plugin could be initializedQt找不到platforms插件路径检查是否在conda环境里执行不要用系统Python确认PyQt5/Qt5/plugins目录存在也可显式设置环境变量QT_QPA_PLATFORM_PLUGIN_PATHOpenCV读摄像头时cv2.VideoCapture(0)返回False摄像头被占用或驱动是MJPEG格式先用CtrlAltDelete确认没有进程占用再换cv2.VideoCapture(0, cv2.CAP_DSHOW)强制DirectShow这四个错误占了这个项目安装阶段九成的问题。还有一个细节Windows上不要在cmd里直接输入pyqt5它和PyQt5的包名大小写不同正确命令是python -m pip install pyqt5 pyqt5-tools。在PyCharm里配置PyQt5时先确认Project Interpreter选中的是traffic_monitor虚拟环境否则装了也会提示ModuleNotFoundError。5. 离屏验证模型效果并生成带时间戳的截图证据调试检测阈值时每次都打开摄像头界面会浪费大量时间也不方便对比不同参数的效果。更稳的做法是让模型离线跑一段录制好的路口视频把每一帧的统计信息和检测截图落盘。这样既能在没有显示器的服务器上跑通流程也能生成演示素材。import csv import time import cv2 def run_offline(detector, video_path, output_prefixcapture): cap cv2.VideoCapture(video_path) if not cap.isOpened(): print(video open failed) return frame_id 0 with open(detect_result.csv, w, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([frame_id, timestamp, objects, fps]) while True: ret, frame cap.read() if not ret: break if frame_id % 5 0: t0 cv2.getTickCount() rendered, count detector.detect(frame) dt (cv2.getTickCount() - t0) / cv2.getTickFrequency() fps 1.0 / dt if dt 0 else 0 writer.writerow([frame_id, time.strftime(%Y%m%d_%H%M%S), count, round(fps, 2)]) cv2.imwrite(f{output_prefix}_{frame_id}_{count}.jpg, rendered) frame_id 1 cap.release()这段脚本的关键是每隔5帧保存一次既不会让IO占满CPU又能保留足够样本。生成CSV后先用Excel把objects列按大小排序找到目标数异常高的片段再回到对应截图里肉眼确认是真正的车流高峰还是阴影误检。如果想比较不同conf_thres的效果就把detector分别用0.1、0.25、0.4创建跑同一个视频对比三个CSV里的objects总量。脚本也可以反过来用于验证模型权重下载是否完整运行前检查cfg和weights文件大小如果weights小于2.3MB基本就是下载中断而不是代码问题。把run_offline放在UI代码之外还有一个好处将来换PySide6或者打包成exe时离线验证部分是零依赖的纯OpenCV逻辑不需要动。离线验证脚本里的detector实例和GUI里用的是同一个类后面改界面时模型侧代码原封不动。本文还有配套的精品资源点击获取
返回列表