
简介这套基于YOLOv3与PyQt5的交通路口智能监控系统源码包定位为端到端的计算机视觉实战项目适合希望学习目标检测、视频流处理、桌面端可视化开发的开发者也适用于相关课程设计或毕业设计参考。包内共113个文件压缩后54.48MB包含32个Python源码、39个pyc编译文件、8个h5模型权重、4个cfg网络配置、3个ui界面以及2个flv测试视频覆盖算法训练、服务端推理和客户端展示全链路。整套方案以SRS流媒体服务器接收RTMP远端视频流借助YOLO模型识别道路中的人、车、交通灯等目标并通过PyQt5客户端查看实时分析结果同时支持并发连接整体结构清晰便于二次改造与功能扩展。随包还提供详细安装使用说明可帮助快速搭建环境并理解各模块作用。目前已有130人学习/下载适合作为智能交通监控方向的入门到进阶参考。1. 交通路口智能监控系统yolov3pyqt5源码包能直接落地吗如果你接过“把路口监控视频自动识别车辆、行人、红绿灯”这种需求会发现细节远比想象多视频流要转格式、目标检测要跑在GPU上、结果还要实时推到桌面端。这套基于yolov3pyqt5的交通路口智能监控系统就是把SRS流媒体服务器、GPU服务器和PyQt5客户端三件事打包在一起的完整Python源码包。它解决的是视频流获取、模型推理、结果展示的端到端问题适合课程设计、毕业设计或者小型路侧感知demo。压缩包里自带traffic.flv样例视频、多个YOLOv3配置和车牌识别的h5权重比空壳源码实在得多。下面我从架构链路到避坑把关键参数和踩过的坑一条条说清。2. 三端架构与RTMP链路SRS流媒体服务器、GPU服务器和Local客户端怎么分工拿到这个源码包第一件事不是跑Python而是把三端架构在脑子里捋顺。项目正文里写得很明确SRS流媒体服务器负责收流和分发GPU服务器跑YOLO做目标检测Local客户端基于PyQt5展示结果。视频流通过RTMP协议从远端推到SRSGPU服务器从SRS拉流分析分析结果再被客户端拿去做可视化。这个链路是典型的“推流-中转-消费”模式好处是摄像头或视频文件只需要一次推流检测端和显示端可以同时消费同一路流互不干扰。2.1 为什么要用SRS做流媒体中转而不是让GPU服务器直接读摄像头很多新手拿到源码包会直接问能不能让OpenCV直连摄像头IP取流能但不推荐。直连模式下摄像头地址一旦变化GPU服务器和所有客户端都要跟着改配置而且OpenCV直接读某些RTSP流时断流重连逻辑非常脆弱偶发丢包会导致解码线程卡死在cv2.read()里。SRS作为独立流媒体服务器把“取流”这件事集中管理推流端只往rtmp://ip:1935/live/traffic推GPU服务器和客户端都从这个地址拉谁挂了都互不影响。SRS的部署和启动也简单。源码包通常直接带编译好的二进制如果没有我一般手动编译cd srs-3.0-b2 ./configure --prefix/usr/local/srs make -j4 sudo make install # 编辑配置文件后启动 ./objs/srs -c conf/srs.conf逻辑说明configure时指定安装前缀make完成后SRS的可执行文件生成在objs/下-c指定配置文件。SRS默认监听1935端口RTMP地址结构是rtmp://服务器IP:1935/live/应用名live是默认的应用名后面再跟流名称。这一段是整个系统的“血管”端口写错、防火墙没放行后面全白搭。参数说明flags和srs_log_level在srs.conf里可调排障时把日志级别改成trace能看到完整的推流握手过程。我遇到过客户端能出画面但每隔几秒卡顿的情况就是SRS的queue_len和send_buf_time参数与源视频帧率不匹配把queue_len从默认的30稍微调低能缓解。2.2 从traffic.flv到YOLO输入的完整数据流源码包里有两个样例视频traffic.flv和traffic_2.flv这俩就是用来做端到端验证的。先推流ffmpeg -re -i traffic.flv -c copy -f flv rtmp://127.0.0.1:1935/live/traffic逻辑说明-re按原视频帧率读取-c copy不重新编码直接用FLV格式推到SRS。本地联调时可以先用127.0.0.1部署到服务器再换实际IP。GPU服务器拉流的代码用OpenCV就能写import cv2 cap cv2.VideoCapture() cap.set(cv2.CAP_PROP_BUFFERSIZE, 3) cap.open(rtmp://127.0.0.1:1935/live/traffic) if not cap.isOpened(): raise RuntimeError(rtmp stream open failed) ret, frame cap.read() # 输出 yolo 需要的 416x416 输入 resized cv2.resize(frame, (416, 416))逻辑说明CAP_PROP_BUFFERSIZE设置内部缓冲帧数设得太大反而会增大画面延迟。cap.open()失败时先检查SRS是否在运行、ffprobe rtmp地址能不能拿到视频流别急着怀疑OpenCV。cv2.resize直接把原始帧压成正方形但这样会把非等比的物体压变形更好的做法是letterbox后面会提。2.3 并发连接与线程模型GPU服务器如何同时处理多路视频项目描述里明确写着“支持并发连接”这意味着GPU服务器不能只开一个线程拉一路流。常见做法是给每一路视频流分配一个独立线程每个线程维护自己的VideoCapture和YOLO模型实例。显存撑得住的情况下线程数等于流路数。下面是一个线程化处理的骨架import threading import queue import time class StreamWorker(threading.Thread): def __init__(self, rtmp_url, model): super().__init__(daemonTrue) self.url rtmp_url self.model model self.result_queue queue.Queue(maxsize10) def run(self): cap cv2.VideoCapture() cap.set(cv2.CAP_PROP_BUFFERSIZE, 2) cap.open(self.url) while cap.isOpened(): ret, frame cap.read() if not ret: break boxes self.model.detect(frame) if not self.result_queue.full(): self.result_queue.put((frame, boxes))逻辑说明daemonTrue保证主进程退出时子线程自动销毁result_queue限制长度防止检测速度跟不上拉流速度导致内存爆炸。这里每路流用独立模型实例是为了避免在模型推理时互相争抢同一个GPU上下文实测两路流共用一个darknet模型会偶发CUDA空闲句柄错误。参数说明GPU服务器的并发上限取决于单帧显存占用。YOLOv3在batch1情况下416分辨率大约占1.5GB显存一张8GB显卡建议最多跑4路。如果你只有4GB显存老老实实换成tiny-yolo-voc.cfg把分辨率降到320x320每路能压到600MB左右。3. 模型选择与权重文件从yolov3.cfg到车牌识别GRU模型的一一对应源码包列出的配置文件不少yolov3.cfg、yolo-voc.cfg、yolo.cfg、tiny-yolo-voc.cfg。新手最容易的翻车点就是拿错cfg去加载权重导致网络结构对不上报维度错误。这一章把cfg和权重的对应关系、车牌识别h5模型的分工讲清楚。3.1 cfg文件怎么挑yolov3.cfg、yolo-voc.cfg、tiny-yolo-voc.cfg分别用在哪Darknet的cfg和weights是严格绑定的weights里存的是每个卷积层的权重参数层数、卷积核大小、步长不一样加载时TensorFlow或Darknet的解析器就会报错。所以先明确用途再选cfg。配置文件典型类别数适用场景特点yolov3.cfg80COCO同时检测人、车、红绿灯等多类目标网络深精度高显存占用大yolo-voc.cfg20VOC只关注person/car等常见目标比上一个小一圈速度稍快yolo.cfg自定义只训练过特定类别需配合自己的weightstiny-yolo-voc.cfg20VOC低成本设备或实时性优先通道数砍半速度最快选择原则是你的预训练权重从哪来就配哪个cfg。如果是从darknet官方下载的yolov3.weights必须配yolov3.cfg如果自己用VOC数据集训练过就用yolo-voc.cfg。源码包里的demo更看重“能跑起来”本地显卡一般我建议先用tiny-yolo-voc.cfg做通链路再换大模型。验证cfg是否与权重匹配的土办法是用darknet的-load参数直接跑一张图如果输出维度不匹配它会立刻报layer filters或loaded的错误。我的习惯是每次换cfg都重新检查[yolo]层的classes和上方卷积层的filters是否满足# filters (classes 5) * 3以yolo-voc 20类为例 filters (20 5) * 3 # 75 print(filters)逻辑说明检测层每个grid cell预测3个anchor每个anchor返回(x, y, w, h, confidence, class...class_n)所以一共5classes个值。改classes时常有人忘记同步改前面卷积层的filters这是Darknet最常见的报错来源之一。3.2 车牌识别三个h5文件GRU、RNN、字符模型的协作关系压缩包里三个h5文件分别是ocr_plate_all_gru.h5、ocr_plate_all_w_rnn_2.h5和char_chi_sim.h5这属于车牌字符识别LPR部分。国内车牌结构是“省份中文简称 字母 数字”比如“京A12345”其中中文只有第一位会出现且数量有限。char_chi_sim.h5负责识别中文省份字符它输入的是单字符图片输出是省份列表的概率分布ocr_plate_all_gru.h5和ocr_plate_all_w_rnn_2.h5是两个不同主干网络的序列识别模型用于识别后半段字母和数字。实际管线是先用YOLO检测车牌区域然后把车牌图片切分成单字符或多个字符序列中文部分交给char_chi_sim英文数字部分交给GRU或RNN模型。注意不要同时让两个模型识别同一个字符。GRU和RNN的精度其实差不多区别是训练时序列长度和网络参数不同。我的习惯是固定用ocr_plate_all_gru.h5做英文数字识别另一个留作对照组。加载这类Keras h5模型时输入尺寸通常需要转灰度再resize到(width, height)具体数值要看各模型训练时的设置。如果你发现车牌识别结果全是乱码优先怀疑输入图片的尺寸和通道数而不是模型。3.3 修改cfg让模型适配自己的视频分辨率源码包自带的cfg默认分辨率是416x416或608x608如果你处理的是高清路口画面直接resize会丢失很多小目标。正确做法是修改cfg里[net]下的width和height再让代码按这个尺寸resize# 把宽高改成 576x544必须是32的倍数 import re with open(yolov3.cfg, r) as f: content f.read() content re.sub(rwidth416, width576, content) content re.sub(rheight416, height544, content) with open(yolov3_576.cfg, w) as f: f.write(content)逻辑说明darknet的缩放步长是32所以宽高取32的倍数才能保证特征图尺寸一致。改完cfg后代码里检测前预处理要用同样的分辨率frame cv2.resize(frame, (576, 544))顺便说一句改cfg只影响网络输入尺寸不影响权重层数所以原来训练好的weights可以继续用。但如果你为了提速把width改成320检测小目标的召回率会明显下降这是分辨率换帧率的必然代价。4. PyQt5客户端实战显示视频流、叠加检测框与控制面板Local客户端是整个系统的门面源码包里的PyQt5部分主要解决三件事从SRS拉RTMP流、接收GPU服务器的检测结果、把检测框画到画面上。这一节给出我验证过的最小可运行方案。4.1 客户端与服务端的通信接口设计检测结果如何从GPU服务器到客户端没有固定约定但最常见的是服务端跑一个HTTP接口轮询或者用socket推JSON。我这边用的是轻量socket避免引入额外依赖import socket import json sock socket.create_connection((127.0.0.1, 9999)) sock.sendall(bGET_RESULT traffic\n) data b while True: chunk sock.recv(1024) if not chunk: break data chunk if b\n in data: break result json.loads(data.decode().split(\n)[0])逻辑说明因为检测结果要往界面上画服务端每检测完一帧就把该帧所有box的坐标、类别、置信度打包成一行JSON。客户端用recv读到换行符时认为一条完整消息收完了。这样设计简单但性能一般如果检测帧率超过20fps建议换成ZMQ或WebSocket。参数说明socket端口要写进配置项不要写死在代码里。recv(1024)对JSON完全够用但记住服务端发送时末尾要加\n做分隔否则客户端会在不知道哪里断帧的情况下卡在recv里。4.2 用QThread播放RTMP流避免界面卡死PyQt5界面如果直接在UI线程里做cv2.read()视频一卡整个窗口立刻无响应。正统做法是重写QThread的run方法在线程里循环读帧再用Signal把帧传给主线程import cv2 from PyQt5.QtCore import QThread, pyqtSignal class VideoThread(QThread): change_pixmap pyqtSignal(object) def __init__(self, url): super().__init__() self.url url self.cap None def run(self): self.cap cv2.VideoCapture() self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 2) self.cap.open(self.url) while self.cap.isOpened(): ret, frame self.cap.read() if ret and not self.isInterruptionRequested(): self.change_pixmap.emit(frame) self.cap.release()逻辑说明change_pixmap信号携带的是OpenCV的BGR numpy数组不能直接给QLabel显示。主线程收到信号后要把数组转成RGBQImage再贴到QLabel上这部分见下一节。注意在run里用isInterruptionRequested()配合requestInterruption()让线程能安全退出否则关闭窗口时线程还在阻塞读帧程序会僵在那。参数说明cap.set(cv2.CAP_PROP_BUFFERSIZE, 2)这里缓冲区别设太大设成2是为了丢旧帧保新帧降低显示延迟。如果你看到画面像慢放大概率是缓冲区堆满了旧帧。4.3 检测结果如何画到QLabel上有两种画法一种是在OpenCV的帧上用cv2.rectangle画好再显示另一种是把图像交到Qt这边用QPainter画。前者省事我推荐先在cv2层面画再转QImage。示例def paint_frame(self, frame, boxes): for x1, y1, x2, y2, cls, conf in boxes: if conf 0.5: continue cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, f{cls} {conf:.2f}, (x1, y1 - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch rgb.shape img QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888) pix QPixmap.fromImage(img) self.label.setPixmap(pix)逻辑说明cv2.rectangle的置信度阈值先粗筛一遍后面的cvtColor把BGR转成RGB因为OpenCV读出来是BGRQt的QImage默认要RGB。QImage(rgb.data, ...)没有拷贝数据所以frame变量的生命周期要保证到setPixmap之后你会看到主线程一旦刷新下一帧上一帧的显示被新图像覆盖这是正常现象。如果想在Qt侧画框就不要再做cvtColor直接用QPainter.drawRect。5. 避坑记录PyQt5安装、SRS编译、模型加载与黑屏排查这一章是我实际拆这个项目时踩过的坑每条都按“现象→原因→解决”写希望能给你省几小时上网搜答案的时间。5.1 现象pip install pyqt5后import失败提示No module named PyQt5.sip原因PyQt5 5.15版本开始把sip拆成了独立包但pip解析依赖时没给你装上更常见的是系统里有多个Python环境pip装到别的解释器去了。解决手动补包并确认解释器路径。pip install pyqt55.15.11 pyqt5-sip12.15.0 python -c from PyQt5.QtCore import QThread; print(ok)5.2 现象装labelme时把PyQt5顶掉了两个包冲突原因labelme也依赖PyQt5但它在安装过程中会拉取pyqt5-tools而pyqt5-tools自带的是老版本Qt和sippip在满足依赖时可能把PyQt5降级。这直接导致后面import PyQt5.QtWebEngineWidgets报错。解决把PyQt5和labelme装进不同的虚拟环境或者手动强制重装PyQt5pip install --upgrade --force-reinstall pyqt55.15.11 pyqt5-sip12.15.0血的教训凡是涉及Qt的项目永远先用python -m venv创建独立环境别在全局环境里瞎装否则你会在“labelme装不上”和“监控界面打不开”之间反复横跳。5.3 现象客户端一直黑屏SRS日志里没有任何推流记录原因推流用的ffmpeg命令没跑起来或者流量没有从远端真的推到SRS。最常见是RTMP地址里的IP写成了0.0.0.0或者防火墙挡了1935端口。解决先用ffprobe验证流是否存在再改客户端地址。ffprobe rtmp://127.0.0.1:1935/live/traffic # 如果报404说明SRS上没有这个流检查推流端还有一类黑屏是OpenCV自己没带FFmpeg支持。源码安装的opencv-python头文件不全时cv2.VideoCapture能打开普通视频但打不开RTMP。解决pip install opencv-python-headless或者从conda装带ffmpeg的版本。5.4 现象加载yolov3.cfg后一检测就报CUDA out of memory原因cfg里的batch64在测试阶段没有被代码覆盖为batch1Darknet在检测时仍然按64算显存或者GPU服务器上有别的进程占用了显存。解决在代码里显式设置batch1并顺手把subdivisions8调大。# darknet.py 或者你的推理加载逻辑里 cfg open(yolov3.cfg, r).read() cfg cfg.replace(batch64, batch1) cfg cfg.replace(subdivisions8, subdivisions16)注意有些源码包里的__main__根本没有对cfg做覆盖你要直接改文件别指望代码替你改。改完如果还爆显存就换tiny-yolo-voc.cfg。5.5 现象车牌识别结果乱码或只有空字符原因h5模型要求的输入是单通道灰度图但你直接送了三通道BGR或者resize尺寸和模型训练时不一致。解决先看模型model.input_shape里的宽高然后按那个尺寸处理。# 假设模型输入是 (32, 128, 1)通道在后 plate_gray cv2.cvtColor(plate, cv2.COLOR_BGR2GRAY) plate_resized cv2.resize(plate_gray, (128, 32)) plate_input plate_resized.reshape((1, 32, 128, 1)) / 255.0如果还是乱码另一个可能原因是字符序列里包含了中文但你用了纯英文模型去识别。此时要先把中文省份字符单独切出来交给char_chi_sim.h5。6. 验证与进阶用traffic.flv做端到端自检再换tiny-yolo提高帧率6.1 十分钟自检清单从SRS到客户端逐个确认拿到源码包后先别急着改代码按下面的顺序验证一遍每步通过再走下一步。步骤操作预期结果失败时检查1启动SRS端口1935监听日志级别防火墙2用ffmpeg推traffic.flvffprobe能看到流推流命令源文件路径3在GPU服务器跑一段拉流代码能打印出帧尺寸OpenCV ffmpeg支持4加载tiny-yolo-voc.cfg检测第一帧输出至少1个boxcfg与weights匹配5客户端连SRS界面显示画面无黑屏地址、线程、缓冲区6服务端推检测结果画面上能看到框socket通信、JSON格式6.2 替换tiny-yolo-voc.cfg后需要同步修改的三个地方从yolov3.cfg换到tiny-yolo-voc.cfg不是改一行代码就完事。第一模型加载路径改掉第二类别数不一样置信度阈值和NMS的classes参数要跟着调第三检测框坐标在输出后会因为网络输入分辨率不同而多一层缩放。下面是一个通用检测函数def load_net(cfg_path, weights_path): net cv2.dnn.readNetFromDarknet(cfg_path, weights_path) net.setPreferableBackend(cv2.dnn.DNN_BACKEND_CUDA) net.setPreferableTarget(cv2.dnn.DNN_TARGET_CUDA) return net def detect(frame, net, class_names): h, w frame.shape[:2] blob cv2.dnn.blobFromImage(frame, 1/255.0, (320, 320), swapRBTrue) net.setInput(blob) out_names net.getUnconnectedOutLayersNames() outs net.forward(out_names) # 遍历三个输出层将中心坐标转成左上右下 boxes, confs, clss [], [], [] for out in outs: for det in out: scores det[5:] cls np.argmax(scores) conf scores[cls] if conf 0.5: cx, cy, bw, bh det[:4] * np.array([w, h, w, h]) boxes.append([cx - bw/2, cy - bh/2, bw, bh]) confs.append(conf) clss.append(cls) return boxes, confs, clss逻辑说明blobFromImage的size参数要和cfg里的宽高保持一致。换了tiny模型后帧率提升但小目标容易丢代价要心里有数。6.3 把检测结果写回RTMP做二次推流我后来在这个项目上做过一个进阶操作把带框画面推回SRS这样其他没跑Python的客户也能通过播放器看实时画面。做法是用ffmpeg从管道读原始帧ffmpeg -f rawvideo -vcodec rawvideo -s 1280x720 -pix_fmt bgr24 -i - -c:v libx264 -f flv rtmp://127.0.0.1:1935/live/resultPython端把画好框的帧用stdin写进管道import subprocess import cv2 proc subprocess.Popen([ ffmpeg, -f, rawvideo, -vcodec, rawvideo, -s, 1280x720, -pix_fmt, bgr24, -i, -, -c:v, libx264, -f, flv, rtmp://127.0.0.1:1935/live/result ], stdinsubprocess.PIPE) proc.stdin.write(frame.tobytes())这个做法把监控系统做成了“可二次消费”的流既能自己看也能丢给别的分析模块。从那以后我每次搭这类系统都会强制走一遍上面那份自检清单确认SRS→GPU→客户端每一步都通再调模型和并行参数。比起在项目最后一晚发现黑屏是防火墙问题这十分钟检查的收益高得多。希望帮到你。本文还有配套的精品资源点击获取