
简介基于YOLOv3与PyQt5构建的交通路口智能监控系统完整源码包适合计算机视觉学习者与智能交通方向开发者参考。项目采用SRS流媒体服务器、GPU服务器与Local客户端三层架构可将远端视频通过RTMP传输后完成人、车、交通灯等目标检测并支持并发连接形成端到端的监控分析方案。压缩包共113个文件约54.48MB包含32个Python源码、39个编译生成的pyc以及yolov3.cfg等模型配置、h5权重文件、ui界面文件、xml与names标签配置、演示flv视频和图片等各类型文件覆盖从模型训练、视频推流到界面展示的完整链路。目前已有131人学习下载。资源附带详细安装使用说明可帮助快速搭建环境、运行系统也可作为课程设计或毕业设计的基础工程进行二次开发。1. 路口监控的“自动看屏幕”需求为什么值得用YOLOv3PyQt5自己搭红绿灯路口每天产出的监控录像动辄上百GB真正有价值的异常事件往往只有几十秒。传统方案是摄像头只管录、人出事后再回放找效率极低。这套基于yolov3pyqt5开发的交通路口智能监控系统python源码做的事情就是把两条线接起来YOLOv3负责从每一帧画面里把车辆和行人框出来PyQt5负责把这些框画在桌面界面上并把越线、逆行等行为记录成带时间戳的事件。适合正在做课程设计、毕业设计或者想给小路口快速做试点验证的工程师。只要有一点Python基础会装依赖、会跑脚本就不需要从零标注和训练模型。真正磨人的地方不在模型。模型用现成YOLOv3权重就能跑难在视频流接入方式、检测线程与界面线程的调度以及环境依赖这一堆细节。下面按“选型→骨架→实现→避坑→进阶”的顺序把常见做法讲清楚每段代码都可以直接抄去改。2. 先搭对骨头YOLOv3PyQt5的选型逻辑和三线程骨架2.1 模型选型为什么现在仍然有理由用YOLOv3如果你只想要一个“能检测出车和人”的模型2024年有太多性能更好的选择比如YOLOv8、YOLOX。但把这个标题里的技术栈放进真实的路口监控项目里去对比YOLOv3反而有三点不可替代第一它对硬件极其宽容只有CPU的工控机用OpenCV的dnn模块跑也能达到5-12FPS的可接受帧率做试点验证完全够用换成YOLOv8没有TensorRT加速时CPU推理反而更难受第二权重文件格式足够通用.weights加.cfg的组合被OpenCV、darknet、ultralytics等大量工具支持出了问题搜资料、找人求助都比新模型方便第三COCO类别里person、car、bus、truck、bicycle、motorbike正好覆盖路口监控最常见的目标群直接用官方在COCO训练好的权重就能开工不需要先标注几千张路口图片。边界也要说清楚YOLOv3在傍晚逆光、夜间小目标和密集拥挤路口这些场景下漏检明显它更适用白天光线正常的路口。如果你的需求是“全天候高准确率”那这套源码更适合作为起点和基线不建议直接上生产。2.2 界面框架选型为什么是PyQt5而不是Web前端或Tkinter很多人拿到这种标题的第一反应是“做个Web页面不更炫吗”。但值班室或实验室的真实环境是一台Windows工控机、一个显示器、没有前后端部署条件。PyQt5做一个单机桌面程序少掉一层浏览器兼容和网络服务配置Python环境装好就能跑。相比同是桌面方案的TkinterPyQt5对“视频检测结果”这种界面更友好QLabel可以高频刷新图像帧QListWidget可以实时插入事件记录QTableWidget可以展示统计报表控件的排版和样式调整也比Tkinter顺手。如果你照着pyqt5教程搭过一个简单的播放器再来看这套源码的界面部分基本没有障碍。但PyQt5的界面设计和传统网页不同它的事件循环是单线程的。新手最容易犯的错误就是把检测放进GUI线程视频一开界面就假死。所以下面这个骨架是整套程序最重要的部分——三线程模型。2.3 程序总骨架采集线程、推理线程、界面线程三件套先看骨架采集线程只负责从摄像头或者视频文件读帧推理线程负责跑YOLOv3检测界面线程只负责显示和交互。三个线程之间用队列传递数据界面和检测互不阻塞。一个最小实现是这样的import queue import threading import cv2 class FrameReader(threading.Thread): 采集线程只负责读帧不处理检测 def __init__(self, src, frame_queue, maxsize4): super().__init__(daemonTrue) self.src src self.frame_queue frame_queue self.running True def run(self): cap cv2.VideoCapture(self.src) while self.running and cap.isOpened(): ok, frame cap.read() if not ok: break if self.frame_queue.full(): # 队列满说明检测端跟不上丢弃最旧帧保证实时性 try: self.frame_queue.get_nowait() except queue.Empty: pass self.frame_queue.put(frame) cap.release()这个类里有两个参数值得注意。src可以是摄像头设备号0表示笔记本自带摄像头也可以是视频文件路径maxsize是帧队列容量CPU推理在5FPS左右时建议设成2-4GPU推理可以放宽到8-16太大会导致内存里攒一堆没人处理的旧帧。用daemonTrue创建线程主程序退出时线程会被强制结束不会出现“关闭窗口后python进程还挂着”的问题这一点第四部分还会再提。然后是推理线程import numpy as np class Detector(threading.Thread): 推理线程从队列取帧用YOLOv3检测结果放进输出队列 def __init__(self, cfg_path, weights_path, in_queue, out_queue, conf0.5): super().__init__(daemonTrue) self.net cv2.dnn.readNetFromDarknet(cfg_path, weights_path) self.in_queue in_queue self.out_queue out_queue self.conf conf layer_names self.net.getLayerNames() self.output_layers [ layer_names[i - 1] for i in self.net.getUnconnectedOutLayers() ] def run(self): while True: frame self.in_queue.get() 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.output_layers) boxes, confs, class_ids, indices self._postprocess(outputs, w, h) self.out_queue.put((frame, boxes, confs, class_ids, indices)) def _postprocess(self, outputs, w, h): boxes, confidences, class_ids [], [], [] for out in outputs: for det in out: scores det[5:] cls_id int(np.argmax(scores)) score float(scores[cls_id]) if score self.conf: continue cx, cy, bw, bh det[:4] * np.array([w, h, w, h]) boxes.append([int(cx - bw/2), int(cy - bh/2), int(bw), int(bh)]) confidences.append(score) class_ids.append(cls_id) raw_ids cv2.dnn.NMSBoxes(boxes, confidences, self.conf, 0.4) if len(raw_ids) 0: return boxes, confidences, class_ids, [] if isinstance(raw_ids, tuple): indices [int(i) for i in raw_ids] else: indices raw_ids.flatten().tolist() return boxes, confidences, class_ids, indices这段代码有几个细节必须说明。getUnconnectedOutLayers取到的是YOLOv3三个输出层对应13x13、26x26、52x52三个尺度的网格forward只需要跑这三个层比整个网络前向省很多时间。blobFromImage的参数里1/255.0做归一化416是输入尺寸swapRBTrue是因为darknet训练时约定的是RGB顺序而OpenCV默认读出来是BGR。后处理里det前4位是中心坐标和宽高它们是相对原图的归一化值必须乘以w和h还原成像素坐标NMS的0.4是IoU阈值如果同一辆车被框了两次且重叠度高只保留置信度最高的那个框。对拥挤路口可以把NMS阈值调低到0.3减少重叠框但调太低会把同一辆长车的车头和车尾拆成两个目标。参数位置常见取值调低的后果调高的后果置信度阈值confDetector初始化0.5误检多、框乱飘漏检多远处行人和小车直接消失NMS阈值_postprocess0.4同一目标被拆成多个重叠框合并困难输入尺寸blobFromImage416速度快、小目标几乎不可见速度慢、小物体检出改善记住一个原则在路口监控这类固定机位场景先把confidence调到0.4-0.45效果通常比盲目调NMS更明显因为背景相对固定假阳性的来源主要是远处小目标和逆光导致的低置信度。2.4 主程序把线程串起来的最小main主程序内容需要展示三线程如何启动。常见做法是在main函数里创建两个队列和两个线程然后启动Qt应用GUI线程由QTimer驱动刷新。def main(): cfg yolov3.cfg weights yolov3.weights src 0 # 摄像头设备号或视频文件路径 frame_queue queue.Queue(maxsize4) result_queue queue.Queue(maxsize2) reader FrameReader(src, frame_queue) detector Detector(cfg, weights, frame_queue, result_queue) reader.start() detector.start() app QApplication(sys.argv) win MonitorWindow(result_queue) win.show() sys.exit(app.exec_())这段代码把三个线程串起来了FrameReader把帧放进frame_queueDetector从frame_queue取帧检测把结果放进result_queue最后MonitorWindow里的QTimer每33毫秒从result_queue取一次结果画到QLabel上。这样整个程序的处理链路是单向的不需要线程之间互相等待最慢的环节决定整体画面帧率。注意cv2.dnn.readNetFromDarknet在CPU上首次推理会做一次模型解析和内存分配大约1-2秒所以启动时界面先显示“等待视频画面”是正常的不要把它当成卡死后文会专门说这个问题。接下来的第3部分把MonitorWindow这个界面类补齐并加入真正的交通行为判断。3. 把检测与行为判断做成能真跑通的代码从模型加载到界面联动3.1 模型加载前的准备工作配置、权重、类别三个文件拿到标题这个源码包后目录下常见的文件结构是yolov3.cfg网络结构描述、yolov3.weights约246MB的权重文件、coco.names80个类别名、主程序.py、requirements.txt。最常见的问题是权重文件没下载完整用readNetFromDarknet加载时报“无法解析”错误。加载代码如下import cv2 net cv2.dnn.readNetFromDarknet(yolov3.cfg, yolov3.weights) with open(coco.names, r) as f: classes [line.strip() for line in f.readlines()] layer_names net.getLayerNames() output_layers [layer_names[i - 1] for i in net.getUnconnectedOutLayers()]如果源码包里自带的是ONNX格式的yolov3加载方式改成cv2.dnn.readNetFromONNX(yolov3.onnx)即可。建议优先用darknet原生格式少一步格式转换报错容易排查。注意classes列表顺序必须和训练时的类别顺序一致coco.names是官方顺序不要自己改顺序否则显示出来的类别名会对不上。实际项目里我一般会在Detector初始化时把classes读进来并在检测结果里直接带上类别名这样界面端拿到的就是一个带标签的列表而不是一堆数字。这也是源码包“详细安装使用说明”里通常要求你先确认这三个文件路径的原因。三个文件缺一不可路径用英文绝对路径最省事。3.2 交通行为判断压线、逆行、闯红灯的几何判断单纯画框不是“智能监控”真正的价值在于把检测框转成交通事件。以“车辆越停止线”为例先用鼠标在画面上标定停止线的两个端点得到p1和p2然后用叉积判断车辆底部中点在线段哪一侧利用连续帧的侧向变化触发事件def bottom_point(box): 车辆框底部中点代表车辆在地面的接触点 return box[0] box[2] / 2, box[1] box[3] def line_side(pt, p1, p2): 点在线段的哪一侧用向量叉积 x, y pt return (p2[0] - p1[0]) * (y - p1[1]) - (p2[1] - p1[1]) * (x - p1[0]) prev_side {} def check_cross(track_id, box, p1, p2): global prev_side pt bottom_point(box) side 1 if line_side(pt, p1, p2) 0 else -1 if track_id in prev_side and prev_side[track_id] ! side: prev_side[track_id] side return True # 说明越线了 prev_side[track_id] side return False这段判断里有个细节车辆框始终跟着目标移动用底部中点而不是中心点是因为车辆在地面的接触点是底部中心用它判断越线对视角畸变更鲁棒。track_id来自追踪器最简单的替代做法是用检测框中心点做欧氏距离匹配距离小于50像素认为是同一个目标不需要引入tracking模型就能完成跨帧计数。逆行判断是一个进阶需求记录每帧检测框中心点连续12帧计算平均位移向量与车道方向向量做夹角判断夹角大于150度判定为逆行。闯红灯则要接收红绿灯状态信号把红灯时间和行人区域关联起来。如果源码里没有配套的信号机一般做法是人工设置一个“红灯时段表”用系统时间判断当前相位。这部分没有标准答案按路口实际情况调即可。提示行为判断的难点从来不在几何公式而在“目标跟丢后重新出现”时的track_id管理。建议只对连续出现5帧以上的目标做事件判断单帧闪现的框大概率是误检。3.3 界面联动用QTimer把检测结果画到窗口上MonitorWindow类的完整骨架如下import queue import cv2 from PyQt5.QtWidgets import QWidget, QLabel, QVBoxLayout, QListWidget from PyQt5.QtCore import QTimer, Qt from PyQt5.QtGui import QImage, QPixmap class MonitorWindow(QWidget): def __init__(self, result_queue, event_callbackNone): super().__init__() self.result_queue result_queue self.event_callback event_callback self.label QLabel(等待视频画面) self.event_list QListWidget() layout QVBoxLayout() layout.addWidget(self.label) layout.addWidget(self.event_list) self.setLayout(layout) self.timer QTimer(self) self.timer.timeout.connect(self.refresh) self.timer.start(33) def refresh(self): try: frame, boxes, confs, class_ids, indices self.result_queue.get_nowait() except queue.Empty: return for i in indices: x, y, w, h boxes[i[0]] label self.classes[class_ids[i[0]]] if hasattr(self, classes) else str(class_ids[i[0]]) cv2.rectangle(frame, (x, y), (xw, yh), (0, 255, 0), 2) cv2.putText(frame, f{label} {confs[i[0]]:.2f}, (x, y-6), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) self._show_frame(frame) def _show_frame(self, frame): rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch rgb.shape self._keep rgb qimg QImage(self._keep.data, w, h, ch * w, QImage.Format_RGB888) self.label.setPixmap(QPixmap.fromImage(qimg).scaled( self.label.width(), self.label.height(), Qt.KeepAspectRatio))逻辑说明QTimer的33毫秒是界面刷新周期实际上只有当result_queue里有新结果时才会真正重绘所以如果检测只有8FPS界面也是8FPS不会因为白白重绘而空耗CPU。_keep保存当前帧的引用是关键不保存的话QImage构造之后numpy数组可能被回收显示出来就是花屏或整块错色这是PyQt5显示视频最常见的坑。事件列表同样在refresh里处理当event_callback返回越线目标的ID和类型就向event_list插入一条记录例如“2024-06-01 08:31:22 主路东向西 越线”。事件记录不需要每次刷新都插入只有在新事件发生时插入。这样界面上同时有实时画面和事件流水值班人员回头核查也有时间戳可参考。参数说明如果视频分辨率是1920x1080scaled之后的QLabel显示区域最好按16:9固定不要直接setPixmap原尺寸否则窗口会被撑爆。界面上还可以加一个QComboBox选择摄像头源QSlider调节置信度阈值运行中不用重启程序就能调参这对现场验收很有帮助。这一步属于界面便利性不影响核心逻辑但做了之后给非技术同事演示时说服力完全不一样。4. 实战避坑环境配置到界面稳定的5个反面教材这一部分聚焦真真正正会卡住人的问题。每条按“现象→原因→解决”写都是我在类似项目里见过或踩过的坑。4.1 模型与环境的坑现象cv2.dnn.readNetFromDarknet报“无法解析”或直接崩溃有的机器上还会出现“Segmentation fault”后闪退。原因分成两类。第一类是opencv-python版本太老dnn模块对YOLOv3的解析在4.1之前不够稳第二类是weights文件没下全或者路径里有中文darknet的cfg解析对非纯英文路径在Windows上容易崩。这两类都很隐蔽因为报错信息常常只有一行不带任何提示性内容。解决先把opencv升级到4.5以上小版本建议直接上最新的稳定版然后确认weights文件大小在240MB左右差太多就是下载被中断。路径保持纯英文不要用“路口监控”这类文件夹名。如果不需要opencv的窗口功能建议这样装pip install --upgrade opencv-python依赖一堆缺的根源是没用虚拟环境。标题既然说“详细安装使用说明”说明作者已经把依赖整理好了你要做的是用虚拟环境安装而不是直接装在系统Python里否则next同学一跑就缺包python -m venv venv venv\Scripts\activate pip install -r requirements.txtpyqt5安装坑也不少最典型的是装完启动报“Plugin”相关错误多半是PyQt5和PyQt5-Qt5版本冲突直接一起升级到同一版本即可。换机器时先跑一遍requirements.txt不要手工逐个pip。4.2 界面与线程的坑现象视频打开后窗口直接“未响应”过几秒变白屏点击任何按钮都没有反应。原因这是最典型的把检测放到GUI线程的后果。摄像头读帧和YOLOv3推理都会阻塞事件循环Qt的事件循环一旦被阻塞超过几秒系统就会判定程序未响应。检测或读帧里只要有一帧处理慢界面就卡。解决把检测放到独立线程界面线程只做QLabel刷新。如果已经把所有逻辑写在一个类里最简单的改造是给耗时部分套threading.Thread但更彻底的做法是引入前面的三线程骨架。这个改造通常会让帧率感知改善一个台阶不是因为跑得更快而是界面不再被阻塞。现象关闭窗口后命令行不退出或者程序崩溃。原因Qt主窗口关闭时其他线程还在等队列或还在循环。如果线程没有设置daemon主线程退出后它们不会被强制结束所以卡住如果退出顺序不对Qt对象被子线程访问也会崩溃。解决在线程初始化时加daemonTrue并在窗口的closeEvent里通知线程退出def closeEvent(self, event): self.reader.running False self.detector.running False event.accept()注意不要在closeEvent里join子线程否则界面会再次卡住daemonTrue就够了。这段代码是后悔药加在源码包里任何版本都适用。4.3 检测效果的坑现象画面上框一直在抖同一个目标一会有框一会没框偶尔还会把路牌当人框出来。原因置信度阈值设置得太靠近模型输出的平均值检测结果在0.49和0.51之间来回横跳。加上NMS阈值过高重叠框没被合并。还有一类常见原因是画面里出现逆光车前脸在阴影里导致置信度掉下去。解决把conf阈值定在0.45以上NMS阈值定在0.3然后观察一个完整车道周期内的表现。对固定机位的路口更推荐加上“帧间平滑”连续两帧检测到同一位置的同一类别才算有效单帧的不算用这个简单规则能过滤掉大量闪烁误检。这个办法属于玄学少、收益多的那种优化建议直接做。现象CPU机器帧率个位数视频播放速度比实际慢一半。原因YOLOv3在CPU上就是很重416x416输入一张720p图大概需要100-200毫秒加上blobFromImage的大矩阵复制、后处理循环帧率很难看。解决硬件固定时最有效的三招输入降到320x320、检测线程跑一轮后跳过一帧跳帧策略没检测到目标时2-4帧跳过检测到目标时逐帧检测保证异常事件不丢帧、把后处理写成numpy向量化而不是for循环。这三招通常能把CPU帧率从5FPS提到12FPS左右。如果项目要交付换机器跑OpenVINO版或TensorRT版是另一个话题但先用这套纯OpenCV方案跑通业务逻辑再优化性能是对的顺序。5. 从“能跑”到“敢用”离线回归、稳帧与夜间场景补救5.1 用录播视频做离线回归算检测准率把真实路口的监控视频录下来跑完端到端统计每个事件是否漏报误报。真值标注只需框出“越线”“逆行”两类事件不需要逐帧框物体。算一下检测出的事件数除以真实事件数得到召回率检测出且真实的事件数除以检测出的事件数得到精度。一般回归3-5个不同时段的视频才能下结论只跑一条视频说“效果不错”是不可靠的。5.2 线程改进程让界面完全不受检测波动影响检测波动大时可以试试把检测放到独立进程而不是线程。用multiprocessing.Queue传帧多进程的好处是即使检测进程因某帧超时崩溃界面进程还能存活而且进程能拿到更充分的CPU时间片在多核机器上帧率通常比线程方案快15%-30%。代价是进程间复制大帧内存开销较大帧分辨率超过1080p时要把帧先压到720p再传。5.3 夜间和雨天图像预处理补救YOLOv3对低照度敏感夜间漏检率会肉眼可见地上升。常见做法是进检测前先做一次CLAHE自适应直方图均衡把对比度拉起来雨滴遮挡噪声用轻度高斯模糊缓解。注意不要增大对比度参数过头否则夜间车灯会变成大亮斑误检率反而上升。没有夜间训练数据时加预处理是性价比最高的方案也算给后续收集夜间数据争取时间。我自己的习惯是每做一个路口的现场调试都会录一段“真实场景检测画框”的视频留档拿它做回归。这个习惯挽救过不少在现场说“没问题”但换一条路就翻车的状况。拿着量化数据和脱机视频去和项目方沟通比嘴上说“模型很准”有说服力得多。希望帮到你。本文还有配套的精品资源点击获取