十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于YOLOv3+PyQt5的交通路口智能监控系统实现与部署

基于YOLOv3+PyQt5的交通路口智能监控系统实现与部署 简介一套基于YOLOv3目标检测与PyQt5图形界面开发的交通路口智能监控系统完整源码包面向计算机视觉开发者、Python后端工程师及智能交通方向学习者提供从流媒体接入、目标检测到客户端展示的端到端技术方案。压缩包共113个文件约54.48MB包含32个Python源码、8个H5权重模型、4个YOLO配置文件、3个UI界面文件以及多份说明文档与测试视频既有可复用的检测模型也有完整的客户端交互逻辑。目前已有130人学习下载适合具备一定深度学习基础、希望直接搭建或改造监控系统的读者。资源以SRS流媒体服务器、GPU服务器与本地客户端三层架构为核心覆盖RTMP视频流传输、人/车/交通灯目标检测、并发连接处理等关键环节并附带详细安装使用说明可帮助高效跑通项目也可用于毕业设计或工程参考。从源码结构、模型权重到部署文档一应俱全便于按模块拆解学习。1. 基于yolov3pyqt5的交通路口智能监控系统到底在解决什么问题一个路口的摄像头如果只做录像回放那它的价值要等事故发生之后才体现。yolov3pyqt5这套组合的思路完全不同让普通PC或工控机一边读视频流一边实时框出汽车、行人、公交车和两轮车再把检测结果以桌面应用的形式直接呈现。YOLOv3负责目标位置和类别PyQt5负责视频画面、按钮、表格、告警状态这些交互。整条链路不依赖云端服务离线也能跑尤其适合做毕设、产品原型或单位内部工具。对有一定Python基础但并不想重写深度学习推理代码的人来说最有价值的地方在于模型是现成的核心代码可以复现背后又留着足够的调优空间。下面按原理、环境、实现、调优和交付五个环节把它拆开讲清楚。2. 系统结构与YOLOv3推理链路从权重文件到PyQt5界面2.1 为什么选择YOLOv3而不是直接上YOLOv8或Pytorch训练框架YOLOv3虽然年代较早但到今天仍大量出现在实际监控项目里COCO 80类覆盖了车辆、行人和常见交通工具几乎不用训练就能获得可用的识别效果。更重要的是OpenCV从4.x开始内置了DNN模块可以直接加载YOLOv3的权重和配置不需要在目标机上安装PyTorch或Darknet运行时。这对桌面GUI项目的部署价值非常明显文件体积小运行时依赖少换一台机器也不容易因为CUDA版本不一致而启动失败。很多开源代码选择用PyTorch重新实现YOLOv3训练和调试方便但到了交付阶段torch库动辄几个GB安装和打包都比OpenCV方案麻烦。这个系列源码采用OpenCV DNN路线正是为了把主要精力放在业务逻辑和界面交互上。2.2 YOLOv3的三个检测头与锚框设置YOLOv3把输入图片缩放到固定尺寸后会从网络尾部输出三个不同尺度的特征图。以416×416输入为例三个输出层的尺寸分别是13×13、26×26和52×52依次负责大目标、中等目标和小目标。每个格子慷慨预测三个锚框因此每个输出层末尾是85维向量x、y、w、h、置信度和80个类别概率。三个输出层的物理意义在交通路口场景中非常直接13×13主要负责卡车和公交车这样的大目标52×52主要负责远处行人、自行车这类小目标。yolov3.cfg文件中YOLO层的anchors和mask参数是硬编码的排列顺序只要改动一个值检测框就会整体错位。很多初学者在换模型时只替换权重文件却留下原cfg最后得到的结果要么全空要么框位全乱。正确做法是 weights 和 cfg 必须配套使用。你不需要理解每个卷积层的权重怎么算但要知道anchors是从训练数据聚类得到的先验尺寸OpenCV会按cfg里的顺序把它和一个输出格对应起来。2.3 PyQt5在这个系统里的角色线程、信号与槽PyQt5本身不会执行检测它的职责是界面、事件循环和线程管理。摄像头读取和YOLOv3推理都是耗时操作如果放在Qt主线程里界面拖动会变成“胶片卡顿”视频预览也会断断续续。常见做法是新建QThread子类把视频循环放进run()中检测完成后通过pyqtSignal把结果发回主线程的槽函数再由槽函数更新QLabel或表格。信号槽的跨线程排队机制让数据传递变得安全不需要像threading那样手动维护锁和条件变量。这里给出这套UI线程模型里最常用的几个信号信号携带数据发出线程主线程用途frameReadyOpenCV图像对象读取/推理线程刷新视频画面detectResult类别ID、框坐标、置信度推理线程表格、告警、日志fpsUpdated浮点帧率推理线程状态栏显示errorOccurred异常字符串推理线程弹窗或恢复操作设计时不要把图像格式转换放在推理线程中。OpenCV的cv::Mat底层指针在跨线程传递时一般安全但QImage对象如果在主线程绘制时原始数据被释放会出现花屏或段错误。最合适的方式是推理线程只发原始帧和检测结果图像到QImage的转换放在主线程槽函数里完成。2.4 一套容易维护的源码目录结构基于标题里“python源码”这几个字实际项目不应把所有代码塞进一个main.py。推荐早期就按职责分成以下结构traffic_monitor/ ├── main.py # PyQt5入口创建主窗口 ├── detector/ │ ├── __init__.py │ ├── yolo.py # YOLOv3检测器封装 │ └── draw.py # 绘制检测框、计数文字 ├── ui/ │ ├── main_window.py # QMainWindow子类 │ └── convert.py # cv图像与QImage互转 ├── threads/ │ └── video_thread.py # 读取/推理线程 ├── config/ │ ├── yolov3.cfg │ ├── yolov3.weights │ └── coco.names ├── resources/ │ └── alerts/ # 告警图片存放目录 └── requirements.txtdetector层不应该导入PyQt5它的输入是一帧OpenCV图像输出是检测结果列表这样以后换成TensorRT或ONNX只需要改yolo.py内部实现。threads层只负责调用detector并发射信号不写业务逻辑。将来要加告警、统计、串口联动都在detectResult信号的槽函数里接新的子模块源码可读性和可维护性都会好很多。3. 搭建pyqt5yolov3环境依赖安装、模型准备与首次检测3.1 用venv隔离环境并安装PyQt5无论是Windows还是Linux都建议先建虚拟环境避免多个Python项目互相污染。在项目根目录打开终端执行python -m venv yoloenv # Windows PowerShell 或 CMD yoloenv\Scripts\activate # Linux / macOS source yoloenv/bin/activate pip install --upgrade pip pip install pyqt55.15.10 pyqt5-tools5.15.4.2 pip install opencv-python4.8.1.78 numpy1.24.4这里指定版本是有考虑的。PyQt5 5.15.10是最后一轮比较稳定的系列版本pyqt5-tools提供Qt Designer方便前期拖界面opencv-python 4.8.x内置dnn模块且对YOLOv3支持完整numpy选择1.24.4是为了和opencv-python的预编译二进制保持一致。如果在PyCharm或VSCode里运行记得把项目解释器切换到刚刚创建的yoloenv文件夹。装完验证一下python -c import cv2, PyQt5; print(cv2.__version__)能正常输出OpenCV版本号就说明基础环境没问题。注意PySide6和PyQt5的API并不完全一致这套源码里用的是PyQt5.QtCore、PyQt5.QtWidgets和pyqtSignal不要用pyside6替换否则信号名和槽签名都要改。3.2 yolov3模型文件清单与cfg关键参数运行YOLOv3不需要准备训练集只需要三个文件网络结构文件yolov3.cfg、预训练权重yolov3.weights和类别名文件coco.names。权重是darknet的二进制格式OpenCV dnn模块可以直接读取。这几个文件在常见开源YOLOv3仓库都能找到下载时建议对照下表检查文件典型特征作用yolov3.cfg约9KB文本包含Convolution层和3个YOLO层定义网络结构、anchors、maskyolov3.weights约248MB二进制文件保存Darknet-53训练好的权重coco.names80行文本每行一个类别名把检测索引映射成person、car拿到cfg后可以打开检查内部是否有三处[yolo]节每个节里都有一行anchors 后面跟着9组坐标。还要看classes 80和mask是否对应完整版模型。如果误把yolov3-tiny.cfg和完整版权重混用OpenCV不会立即报错但检测框会全部错乱。第一次调试建议固定使用416×416输入尺寸如果改成608×608小目标召回会好一点但推理耗时近似变成两倍。3.3 用OpenCV跑通最小YOLOv3检测脚本在写PyQt5界面之前先用一个最小脚本确认模型和OpenCV环境都正常。假设模型文件放在config目录下再准备一张包含车辆和行人的街景图import cv2 net cv2.dnn.readNet(config/yolov3.weights, config/yolov3.cfg) model cv2.dnn_DetectionModel(net) model.setInputParams(size(416, 416), scale1/255.0, swapRBTrue) model.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV) names open(config/coco.names).read().strip().splitlines() img cv2.imread(street.jpg) class_ids, scores, boxes model.detect( img, confThreshold0.45, nmsThreshold0.4 ) for cid, score, box in zip(class_ids, scores, boxes): left, top, w, h box cv2.rectangle(img, (left, top), (left w, top h), (0, 255, 0), 2) cv2.putText(img, f{names[cid]} {score:.2f}, (left, top - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) cv2.imwrite(result.jpg, img) print(检测到目标个数:, len(class_ids))这段代码有四个关键参数。size(416,416)把任意尺寸图片转成网络输入尺寸scale1/255.0把像素值归一化到0到1swapRBTrue让OpenCV自动完成BGR到RGB的通道切换confThreshold和nmsThreshold分别是类别置信度和NMS抑制阈值。如果同一辆车被框了好几层把nmsThreshold调到0.3或0.35如果远处行人被漏掉先降低confThreshold而不是直接把NMS关掉。4. 实现交通路口监控GUI多线程视频输入与实时检测框显示4.1 主窗口布局视频区、按钮和数据表格主界面用QMainWindow承载中央放一个用于视频预览的QLabel右侧放控制面板开始监测、停止、退出按钮置信度阈值滑杆和检测结果表格。为了减少UI文件打包遗漏界面代码可以直接写在Python类里。这个系统的核心逻辑是把视频线程和界面线程彻底分开QLabel只负责显示QTableWidget只负责记录最近检测到目标不直接参与计算。4.2 编写YoloV3Detector封装输出解析与绘制分离为了让代码可维护把YOLOv3检测封装成一个类文件放在detector/yolo.py。这个类不依赖PyQt5可以单独运行和测试import cv2 import numpy as np class YoloV3Detector: def __init__(self, cfg_path, weights_path, names_path): self.net cv2.dnn.readNet(weights_path, cfg_path) self.names open(names_path, encodingutf-8).read().strip().splitlines() # 获取三个无连接输出层yolo_82 / yolo_94 / yolo_106 self.out_names [layer.getOutputName() for layer in self.net.getUnconnectedOutLayers()] def detect(self, frame, conf_th0.45, nms_th0.4): h, w frame.shape[:2] blob cv2.dnn.blobFromImage(frame, 1/255.0, (416, 416), (0, 0, 0), swapRBTrue, cropFalse) self.net.setInput(blob) outputs self.net.forward(self.out_names) boxes, scores, class_ids [], [], [] for output in outputs: # output形状是(1,255,grid,grid)先转成NHWC再拆成每个anchor一行 grid_h, grid_w output.shape[2], output.shape[3] detection output[0].transpose(1, 2, 0).reshape(grid_h * grid_w, -1) for row in detection: confidence float(row[4]) if confidence conf_th: continue class_scores row[5:] class_id int(np.argmax(class_scores)) class_score float(class_scores[class_id]) * confidence if class_score conf_th: continue cx, cy row[0] * w, row[1] * h box_w, box_h row[2] * w, row[3] * h boxes.append([int(cx - box_w/2), int(cy - box_h/2), int(box_w), int(box_h)]) scores.append(class_score) class_ids.append(class_id) idxs cv2.dnn.NMSBoxes(boxes, scores, conf_th, nms_th) result [] if len(idxs) 0: for i in idxs.flatten(): result.append((class_ids[i], scores[i], boxes[i])) return result这里forward(self.out_names)返回三个尺度的特征图OpenCV已经把anchor解码成相对坐标。cropFalse表示把整图缩放到416×416而不是强制裁剪因此坐标可以直接乘原始帧的宽高。NMS之前的class_score要乘上目标置信度row[4]因为当前输出既包含了“这里有没有目标”又包含了“目标属于哪个类”。4.3 把视频读取和推理放进QThread视频线程类放在threads/video_thread.py继承QThread。这个线程负责打开摄像头或视频文件循环read调用detector再把结果通过信号发送出去from PyQt5.QtCore import QThread, pyqtSignal import cv2 class VideoProcessThread(QThread): frameReady pyqtSignal(object) resultReady pyqtSignal(object, list) def __init__(self, video_source, detector, parentNone): super().__init__(parent) self.video_source video_source self.detector detector self._running True def stop(self): self._running False def run(self): cap cv2.VideoCapture(self.video_source) while self._running and cap.isOpened(): ok, frame cap.read() if not ok: continue detections self.detector.detect(frame) self.resultReady.emit(frame.copy(), detections) drawn self.draw_detections(frame, detections) self.frameReady.emit(drawn) cap.release() def draw_detections(self, frame, detections): for class_id, score, box in detections: x, y, w, h box label f{self.detector.names[class_id]} {score:.2f} cv2.rectangle(frame, (x, y), (x w, y h), (0, 255, 0), 2) cv2.putText(frame, label, (x, y - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) return framestop()方法设置_running标志线程会在下一次循环开始时退出。这里不能用thread.terminate()强制结束线程否则cv2.VideoCapture内部状态可能被破坏下一次程序运行时光头可能打不开。frameReady负责送画框后的图resultReady把原始帧和检测列表一起传给主窗口供后续统计和保存截图使用。4.4 主窗口槽函数BGR转QImage并显示在主窗口类中连接信号from PyQt5.QtGui import QImage, QPixmap from PyQt5.QtWidgets import QMainWindow, QLabel from PyQt5.QtCore import Qt class MainWindow(QMainWindow): def __init__(self): super().__init__() self.videoLabel QLabel() self.statusLabel QLabel() # self.thread VideoProcessThread(...) # self.thread.frameReady.connect(self.update_frame) # self.thread.resultReady.connect(self.on_result) def update_frame(self, cv_image): rgb cv2.cvtColor(cv_image, cv2.COLOR_BGR2RGB) h, w, ch rgb.shape qimg QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888) pixmap QPixmap.fromImage(qimg) label_w self.videoLabel.width() label_h self.videoLabel.height() self.videoLabel.setPixmap(pixmap.scaled( label_w, label_h, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)) def on_result(self, frame, detections): interest {0: person, 1: bicycle, 2: car, 3: motorbike, 5: bus, 7: truck} count {} for cid, score, box in detections: if cid in interest: name interest[cid] count[name] count.get(name, 0) 1 self.statusLabel.setText( f行人 {count.get(person, 0)} | f自行车 {count.get(bicycle, 0)} | f汽车 {count.get(car, 0)})update_frame里转换出来的QImage引用rgb.data槽函数结束前QPixmap.fromImage会完成像素拷贝所以画面上是安全的。如果需要长期保留qimg建议在转换后加一句qimg qimg.copy()。on_result中只统计交通路口关注的类别COCO中大部分类别比如餐桌、沙发在这里没有任何意义过滤后还能降低误报对统计结果的影响。4.5 交通路口最常用的6类目标过滤下面的表列的是COCO 80类中路口监控最常用的类别索引实际代码里只需要保留这些IDCOCO ID英文类名中文显示典型尺寸0person行人小/中1bicycle自行车小2car汽车中/大3motorcycle摩托车小/中5bus公交车大7truck卡车大在on_result里做过滤有个额外好处可以把统计数据单独存一份后续做倒计时或流量统计时不用重新解析原始检测结果。如果测试时发现汽车有时被识别成卡车可以检查类别ID映射是否正确COCO ID 7就是truck不要按自己的想象随意改索引。5. 调优与排错帧率瓶颈、阈值策略和模型替换5.1 用QElapsedTimer定位耗时段监控系统最常见的抱怨是“画面卡顿”。不要一上来就换模型先用计时器把每一段耗时拆开from PyQt5.QtCore import QElapsedTimer # 在VideoProcessThread.run中使用 timer QElapsedTimer() while self._running: timer.start() ok, frame cap.read() read_ms timer.nsecsElapsed() / 1e6 timer.restart() detections self.detector.detect(frame) detect_ms timer.nsecsElapsed() / 1e6 timer.restart() drawn self.draw_detections(frame, detections) draw_ms timer.nsecsElapsed() / 1e6 # print(read_ms, detect_ms, draw_ms)如果read_ms经常超过40ms问题通常在视频源或USB读取链路不在YOLOv3如果detect_ms占了大头考虑缩小输入尺寸或换模型如果draw_ms很高往往是因为cv2.putText在绘制中文字体时失败或是在单帧里画了太多框。判断出瓶颈后调整才有意义。5.2 路口场景的阈值设定策略confThreshold和nmsThreshold应该分开调。先固定一个动另一个观察曲线变化。这里给出一组常见初始值场景confThresholdnmsThreshold输入尺寸预期效果白天车流稀疏0.50.4416误检少漏检略多早晚高峰密集0.350.45416漏检降低密集车辆会粘连夜间监控0.30.5608行人召回提升帧率下降明显高帧率模式0.450.3320帧率提升一半小目标易糊夜间不建议简单把confThreshold降到0.25以下因为低照度图片的虚影和车灯反光会被当成目标。比较有用的做法是先对视频帧做一次cv2.equalizeHist灰度均衡或CLAHE增强再送入模型。还要留意cv2.dnn.NMSBoxes的第三个参数是conf第四个是nms很多误调都是因为参数顺序写反后一条框都出不来。5.3 常见崩溃与现象排查这套系统在实机运行中有几个高频问题提前说明能省很多时间QThread: Destroyed while thread is still running窗口关闭时线程没有退出。需要在主窗口closeEvent中调用self.thread.stop()和self.thread.wait(1000)。Assertion network.forward failedweights和cfg不匹配或者模型路径错误。检查文件名不要混用tiny和完整版。视频画面黑屏但程序不崩溃QImage的bytesPerLine参数错误确认ch * w和实际通道数一致。第二次开始摄像头打不开第一次停止时没有释放cap.release()同一设备句柄没有正常归还。检测框明显偏到左上角输入尺寸和坐标缩放不一致检查blobFromImage时的crop参数。用VSCode或PyCharm调试时把断点打在槽函数接收端比打在emit行更容易看到真实传参内容。因为Qt信号槽是异步的最终接收方拿到的对象可能在emit之后又经过了一次封装。5.4 换成yolov3-tiny与后续ONNX扩展如果CPU环境只能跑到5-8帧最直接的方案是换成yolov3-tiny。模型文件替换后检测代码不需要改动只需要在初始化时更新两个路径self.net cv2.dnn.readNet(config/yolov3-tiny.weights, config/yolov3-tiny.cfg)self.out_names会自动跟随cfg中的YOLO层数量变化完整版三个tiny两个。tiny对远处行人的漏检比完整版明显提升所以更适用于主干路机动车流量检测。如果后续要迁移到ONNX建议把detector定义成抽象接口detect方法保持入参、出参不变这样UI层、线程层一概不用动。6. 进阶与交付RTSP网络摄像头接入、告警截图与PyInstaller打包6.1 接入RTSP网络摄像头把线程初始化时的video_source参数从摄像头索引号直接换成RTSP地址即可rtsp_url rtsp://192.168.1.64:554/Streaming/Channels/101 thread VideoProcessThread(rtsp_url, detector, self)网络摄像头首次连接容易长时间阻塞打开前设置超时参数会更有保障cap cv2.VideoCapture() cap.set(cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, 3000) cap.set(cv2.CAP_PROP_BUFFERSIZE, 3) cap.open(rtsp_url)RTSP只需要摄像头和运行主机在同一局域网内即可不依赖任何外部网络服务。如果画面延迟偏高优先到摄像头管理页面把编码改成H.264 baseline并降低码率。6.2 记录告警截图与CSV日志在on_result槽函数中增加保存逻辑既保留结构化日志也便于事后追溯now QDateTime.currentDateTime() csv_row now.toString(yyyy-MM-dd HH:mm:ss) for cid, score, box in detections: if cid in (0, 2, 3, 5, 7): csv_row f,{self.detector.names[cid]},{box[0]},{box[1]},{score:.2f} with open(alerts/event.csv, a) as f: f.write(csv_row \n) if count.get(person, 0) 2: img_path falerts/{now.toString(yyyyMMddHHmmsszzz)}.jpg cv2.imwrite(img_path, frame)QDateTime.toString(zzz)生成毫秒级后缀避免同秒写入多次时覆盖文件。cv2.imwrite会按扩展名自动选择JPEG编码如果长期保存发现磁盘增长过快可以使用cv2.IMWRITE_JPEG_QUALITY参数把质量从95降到85。6.3 用PyInstaller打包成Windows可执行程序调试完成后用PyInstaller打出一个免安装程序pyinstaller --noconfirm --windowed --name TrafficMonitor ^ --hidden-importPyQt5.QtXml --hidden-importPyQt5.QtSvg ^ --add-data config;config --add-data resources;resources ^ main.py--windowed表示运行时不弹黑色控制台--hidden-import强制保存QtXml和QtSvg两个容易被遗漏的模块--add-data把模型和资源目录一起带入exe。Windows下分隔符使用分号Linux下要改成冒号。真正要搬到另一台机器时模型路径不能写成当前目录而应该用getattr(sys, _MEIPASS, os.path.dirname(__file__))定位到PyInstaller解包后的资源目录。只要打包时没有遗漏plugins/platforms这个exe在未安装Python的机器上也能正常启动。本文还有配套的精品资源点击获取
返回列表