
简介一套基于YOLOv3与PyQt5开发的交通路口智能监控项目面向计算机视觉、智能交通方向的开发者与学习者适用于智慧路口实验、课程设计或安防监控方案预研。系统按SRS流媒体服务器、GPU检测服务器、本地客户端三层架构设计通过RTMP协议传输远端视频流利用YOLO模型识别道路中的人、车、交通灯等目标并支持并发连接。压缩包共113个文件、54.48MB包含32个Python脚本与39个pyc核心逻辑、8个H5训练模型、4个YOLO网络配置、PyQt5界面UI、测试图片与演示视频等配合详细安装使用说明可完整还原整套运行环境演示数据也便于直观核对检测效果。已有130人学习下载对想要快速搭建视频流目标检测原型的读者来说这份可运行的端到端代码比零散教程更具参考价值。1. 交通路口的智能监控这套 yolov3pyqt5 源码解决的是真实路口问题交通路口的监控和普通安防监控不一样画面里目标多、尺度变化大、遮挡频繁白天逆光晚上灯影还经常要实时统计车流量。用纯图像处理做车辆检测晴天还行一到复杂天气就露馅。基于 yolov3pyqt5 开发的交通路口智能监控系统用深度学习模型做目标检测用 pyqt5 做桌面交互界面正好把“能检测”和“能操作”连成一条完整链路。这套源码适合两类人一是要做课程设计或毕业设计的学生需要一套能跑通、能讲清原理的完整项目二是想在企业内部快速搭路口监控原型的工程师先验证效果再决定要不要上重型平台。接下来我按“模型原理—数据与训练—界面集成—部署避坑—验证技巧”的顺序把整个方案拆开讲。2. 看懂 yolov3 在路口场景的三个关键机制多尺度、anchor 与 Darknet-532.1 为什么是 yolov3 而不是更新型号做交通路口检测很多人一上来就问“怎么不用 yolov5 或 yolov8”。我的回答是这套源码选 yolov3 有现实考虑。路口监控往往部署在旧电脑、工控机甚至嵌入式主板上这些设备没有新显卡yolov3 的 Darknet-53 骨干在 CPU 上跑也能维持可用帧率其次yolov3 的模型结构直观配置写在 yolov3.cfg 里anchor、学习率、网络尺寸全部明文可见非常适合作为教学和二次开发基线。yolov5 和 yolov8 精度更高、训练更省心但代码封装深对新手不友好对旧硬件也更吃紧。这套项目把 yolov3 作为检测核心搭配 pyqt5 做界面典型场景是“一台普通电脑 一路摄像头 实时显示检测结果”而不是“多卡服务器 高并发视频流”。yolov3 能成为路口检测常青树靠的是三个机制Darknet-53 骨干网络、特征金字塔多尺度检测、以及基于 anchor 的目标候选策略。下面逐个拆。2.2 Darknet-53深但不笨重的骨干网络Darknet-53 由 53 层卷积组成大量使用 1x1 卷积降维和残差连接。残差结构解决了一个实际问题网络加深后梯度传不回去训练容易崩。1x1 卷积则把通道数压缩再扩张减少计算量。路口画面里车和人的特征差异很大——车有规整的轮廓和玻璃反光人形姿态变化多——骨干网络越深提取到的语义特征越抽象越能区分“车轮”和“行人腿部”这类容易混淆的局部。一张 416x416 的输入图经过 Darknet-53会在三个不同尺度输出特征图对应关系如下表特征图尺寸网格数主要检测目标路口典型场景13x13169 个网格大目标镜头近处的大巴、货车26x26676 个网格中目标路口中间排队的小轿车52x522704 个网格小目标远端斑马线附近的行人、电动车这就是特征金字塔的核心设计大尺度特征图保留空间细节小尺度特征图拥有语义抽象能力两者互补。路口画面里一辆车可能只有 20 像素宽也可能占满半个画面没有多尺度机制模型会顾此失彼。实际用这个源码时我会先确认 cfg 文件里width416、height416没有被改动因为训练时的输入尺寸和检测时不一致会直接影响小目标召回率。2.3 anchor路口目标尺寸的“先验知识”anchor 是 yolov3 里一组预设的框宽高比。模型不是从零猜测目标位置而是在每个网格上铺几个预设框再微调偏移量。yolov3 默认给了 9 组 anchor按特征图尺度分配特征图尺度anchor 宽高单位像素适配目标13x13大目标116x90156x198373x326大车、近景车辆26x26中目标30x6162x4559x119普通轿车、SUV52x52小目标10x1316x3033x23远处行人、电动车这组参数来自 COCO 数据集聚类。路口场景下车宽高比集中在 1.2 到 2.0 之间行人则瘦长和 COCO 的分布大体一致所以直接用默认 anchor 也能跑。但如果你有特定路口的俯拍数据可以用 k-means 重新聚类 anchor。做法是把标注框的宽高归一化后聚类成 9 组替换 cfg 里的值训练收敛会更快小目标召回率能提升几个点。这就是为什么这个源码包里带训练脚本而不是只给权重——anchor 是跟着数据集走的不是固定的。2.4 从 cfg 里读关键参数动手检查配置yolov3 的结构参数全部暴露在 cfg 文件里这是它最大的优点。我拿到任何基于 yolov3 的源码第一件事就是写一个小脚本解析 cfg确认三个信息输入尺寸、anchor 数量、类别数是否匹配。def parse_cfg(cfg_path): 读取 yolov3 cfg 中的关键结构参数用于核对模型配置 with open(cfg_path, r, encodingutf-8) as f: lines f.read().split(\n) layers [] current {} for line in lines: line line.strip() if not line or line.startswith(#): continue if line.startswith([): if current: layers.append(current) current {type: line[1:-1]} else: key, _, value line.partition() current[key.strip()] value.strip() if current: layers.append(current) # 找出所有 yolo 输出层前的卷积层检查 filters 是否匹配 for i, layer in enumerate(layers): if layer[type] yolo: classes int(layer.get(classes, 0)) prev_conv layers[i - 1] # yolov3 每个尺度的输出通道 3 * (classes 5) expected_filters 3 * (classes 5) if filters in prev_conv: actual int(prev_conv[filters]) print(f尺度 {i}期望 filters{expected_filters}实际{actual}{匹配 if actual expected_filters else 不匹配检测会崩溃}) parse_cfg(yolov3.cfg)这段代码的逻辑逐行解析 cfg把每个[convolutional]或[yolo]块提取成字典然后检查每个 yolo 层前面的卷积层 filters 数。classes 5里的 5 代表每个 anchor 要预测的 4 个坐标偏移加 1 个目标置信度3 是每个尺度的 anchor 数量。如果期望值和实际值不一致加载权重时一定会报错或直接崩。很多下载的源码包改过类别数但忘了改 filters这个小检查能在一分钟内定位问题。运行这个脚本不需要 GPU纯 CPU 就能执行。2.5 帧率与精度的平衡点实际路口监控对帧率有硬要求。倒不是必须 30fps而是统计车流量时如果丢帧太多计数会偏。yolov3 的推理速度取决于输入尺寸和硬件。在 i5 四核 CPU 上用 416 输入跑大约 3 到 5 fps用 320 输入跑能到 8 fps 左右。如果接的是 USB 摄像头25fps 的输入源配上 4 到 5fps 的检测速度相当于每 5 帧才检测一次对车流量统计来说勉强够用。想要更流畅就得靠界面线程写跳帧逻辑这一点后面讲 pyqt5 集成时会展开。3. 把模型练成“路口专用”数据集、标注格式与训练参数3.1 数据集怎么选公开数据和自采数据的取舍这个监控系统的检测目标通常是“车辆、行人、骑车人、红绿灯”这几类。训练数据有两个来源公开数据集和自己标注。公开数据集里UA-DETRAC 是专门为交通路口场景做的场景是北京、天津等地的真实路口抓拍包含车辆检测和跟踪标注BDD100K 包含车辆、行人、交通灯、交通标志等类别画面更接近日常驾驶视角但标注噪声偏大。两个数据集都是研究用途下载后需要按自己的类别映射重新整理。我自己做路口项目时一般先用 BDD100K 的一个子集做预训练再用自己路口的半小时录像标注几百张图做微调效果比只用公开数据高不少。公开数据集的问题是分布偏差训练集里是白天晴天部署现场是晚上或雨天检测精度会明显下降。所以交通路口监控项目我强烈建议至少采集 500 到 1000 张现场截图做补充训练覆盖阴天、傍晚、路灯开启三种典型光照。这一步决定了你的模型是“能演示”还是“能落地”。3.2 标注格式VOC xml 和 YOLO txt 那点事下载的源码包有的用 VOC 格式xml有的用 YOLO 格式txt训练前必须统一。VOC 格式保存的是目标的绝对坐标xmin, ymin, xmax, ymaxYOLO 格式保存的是归一化后的中心点坐标和宽高class_id, cx, cy, w, h所有值都在 0 到 1 之间。转换时最容易出错的是中心点算错以及宽高忘记归一化。下面这段脚本是我每次做数据集必跑的转换工具直接放在源码包的数据目录下就能用。import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_dir, txt_dir, class_list): 把 VOC xml 标注转成 YOLO txt 标注逐文件转换 os.makedirs(txt_dir, exist_okTrue) for xml_file in os.listdir(xml_dir): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(xml_dir, xml_file)) root tree.getroot() size root.find(size) img_w, img_h int(size.find(width).text), int(size.find(height).text) txt_path os.path.join(txt_dir, xml_file.replace(.xml, .txt)) with open(txt_path, w) as f: for obj in root.iter(object): cls obj.find(name).text if cls not in class_list: continue cls_id class_list.index(cls) box obj.find(bndbox) xmin, ymin int(box.find(xmin).text), int(box.find(ymin).text) xmax, ymax int(box.find(xmax).text), int(box.find(ymax).text) # 计算中心点并归一化防止宽高为 0 cx ((xmin xmax) / 2) / img_w cy ((ymin ymax) / 2) / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h f.write(f{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}\n) # 类别顺序要固定和训练时的 data 文件一致 classes [car, bus, truck, cyclist, person, traffic_light] voc_to_yolo(annotations_xml, labels_txt, classes)这段代码有几个关键点。第一类别列表classes的顺序就是模型输出的类别 ID训练和检测必须用同一份映射否则检出来全是错的。第二归一化的除法用img_w和img_h不是用原始像素值对应的浮点数除错了边界全偏。第三算w和h时用xmax - xmin如果标注框本身存在 xmin 大于 xmax 的脏数据这里会生成负数模型根本学不进去。我见过的翻车案例里八成的训练损失不收敛都是数据问题不是网络结构问题。3.3 训练命令与参数直接能抄的配置数据准备好后训练用 darknet 框架。源码包里带的是 darknet 训练入口命令分训练和测试两段# 训练命令data 文件描述类别路径cfg 描述网络结构weights 是预训练权重 darknet detector train config/obj.data config/yolov3-obj.cfg darknet53.conv.74 -dont_show -map # 推理命令weights 指训练产出的最终权重 darknet detector test config/obj.data config/yolov3-obj.cfg backup/yolov3-obj_final.weights -ext_output -dont_show-map参数会在每个验证周期计算 mAP方便盯着精度变化-dont_show表示不弹可视化窗口服务器上跑必备。训练时我要盯的参数是这几个参数推荐值作用与坑点batch64每次迭代样本数显存小就降到 32同时调 subdivisionssubdivisions16把 batch 拆成 16 份加载显存不够报错时优先改这个learning_rate0.001主干预训练权重微调时推荐值从零训练要调大一些burn_in1000前 1000 次迭代学习率从 0 热身到设定值别去掉max_batches12000类别数多时按 2000 * classes 估算steps9600, 10800max_batches 的 80% 和 90% 处降低学习率训练时间的估计单张 1080Ti 上416 输入、12000 次迭代大约跑 6 到 10 小时。如果 4000 次迭代后 loss 还在 8 以上先检查标注数据有没有空 txt 文件、有没有类别 ID 对不上的情况。darknet 的训练日志里avg loss是滑动平均值稳定下降到 2 到 3 就可以考虑停。用 CPU 训练我是不建议的——不是不能跑是一轮要几小时迭代 12000 次得到猴年马月。4. 用 pyqt5 把检测结果变成可操作的监控界面4.1 界面要承载什么不是简单画个框这个监控系统的界面不是只显示视频和画框至少要有四块内容实时视频画面带检测框、目标计数面板按类别统计当前帧和累计值、信号灯状态提示、以及系统运行信息帧率、处理帧数。有些源码包还会加一个“越界报警”区域检测到行人进入机动车道就闪烁告警。pyqt5 的优势在于它的信号槽机制非常适合做这种数据驱动的界面——检测线程发出信号主界面收到信号后更新画面和统计数据逻辑清晰不容易写出面条代码。界面布局常见做法是用 QMainWindow 作为主窗口左侧放 QLabel 显示视频帧右侧用 QVBoxLayout 纵向排列计数面板和控制按钮。视频刷新不要用 QTimer 定时器去读摄像头——那是新手最容易踩的坑后面避坑章专门讲。4.2 多线程是必须的检测不能卡界面pyqt5 的界面线程也叫主线程负责处理鼠标点击、窗口重绘。如果在主线程里写一个while True循环去读摄像头并跑 yolov3界面会直接卡死窗口拖动都没反应。正确做法是检测逻辑放 QThread线程里不断读帧、推理、发信号主线程只负责接收信号更新界面。中间用队列解耦网络卡顿或推理变慢时界面不会跟着掉帧。# detection_thread.py检测线程核心结构 import cv2 from PyQt5.QtCore import QThread, pyqtSignal import numpy as np class DetectionThread(QThread): # 信号帧数据 检测结果框坐标、类别、置信度 frame_ready pyqtSignal(np.ndarray, list, list, list) def __init__(self, video_path, net, classes, conf_thresh0.5): super().__init__() self.video_path video_path self.net net # cv2.dnn.readNetFromDarknet 加载好的网络 self.classes classes # 类别名称列表 self.conf_thresh conf_thresh self.running True def run(self): cap cv2.VideoCapture(self.video_path) # 获取网络输出层名yolov3 有三个输出层 layer_names self.net.getLayerNames() output_layers [layer_names[i - 1] for i in self.net.getUnconnectedOutLayers()] while self.running: ret, frame cap.read() if not ret: break h, w frame.shape[:2] # 构建 blob 并前向推理 blob cv2.dnn.blobFromImage(frame, 1/255.0, (416, 416), (0, 0, 0), True, cropFalse) self.net.setInput(blob) outputs self.net.forward(output_layers) boxes, confs, class_ids [], [], [] for output in outputs: for detection in output: scores detection[5:] class_id np.argmax(scores) confidence scores[class_id] if confidence self.conf_thresh: # 中心点坐标和宽高是相对 416x416 的需换算回原图 cx, cy, bw, bh detection[:4] x int((cx - bw/2) * w) y int((cy - bh/2) * h) boxes.append([x, y, int(bw*w), int(bh*h)]) confs.append(float(confidence)) class_ids.append(class_id) idxs cv2.dnn.NMSBoxes(boxes, confs, self.conf_thresh, 0.4) final_boxes [boxes[i] for i in idxs.flatten()] if len(idxs) else [] final_confs [confs[i] for i in idxs.flatten()] if len(idxs) else [] final_cls [class_ids[i] for i in idxs.flatten()] if len(idxs) else [] self.frame_ready.emit(frame, final_boxes, final_confs, final_cls) cap.release()这个线程类的几个关键设计frame_ready信号携带了帧图像和检测结果三个列表主线程拿到后一次性绘制避免多次跨线程传数据self.running是退出标志窗口关闭时把它置为 False线程的while循环自然退出否则 python 进程关不掉。blobFromImage的1/255.0是像素归一化(416, 416)必须和 cfg 里的输入尺寸一致True表示交换 R 和 B 通道——因为 darknet 训练时用的是 RGB 顺序而 OpenCV 读出来是 BGR不交换的话颜色通道就乱了检测精度会掉一大截。NMSBoxes的非极大值抑制阈值 0.4 控制重叠框的合并力度调大一点框会更少但可能漏检调小一点重叠目标会出多个框。对路口场景来说车辆密集时 0.4 是合理起点。4.3 主窗口刷新画面QPixmap 与坐标换算主窗口接收frame_ready信号后做三件事把 numpy 数组转成 QImage 显示、在图像上绘制检测框、更新计数面板。绘制检测框要在界面上画所以必须等信号到了主线程再画不能在检测线程里画——Qt 不允许非主线程直接操作界面控件。# main_window.py主窗口信号槽更新界面 from PyQt5.QtGui import QImage, QPixmap from PyQt5.QtCore import Qt import cv2 class MainWindow(QMainWindow): def __init__(self): super().__init__() # 初始化界面、启动检测线程等逻辑省略 self.detection_thread.frame_ready.connect(self.update_frame) def update_frame(self, frame, boxes, confs, class_ids): 主线程槽函数画框并刷新画面 for box, conf, cls_id in zip(boxes, confs, class_ids): x, y, w, h box color self.color_map[cls_id] # 每个类别固定颜色 cv2.rectangle(frame, (x, y), (x w, y h), color, 2) label f{self.classes[cls_id]} {conf:.2f} cv2.putText(frame, label, (x, y - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1) # 帧率估算简单滑动平均 self.frame_count 1 if self.frame_count % 10 0: import time now time.time() fps 10 / (now - self.last_time) self.fps_label.setText(fFPS: {fps:.1f}) self.last_time now # numpy 数组转 QImageRGB888 格式 rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch rgb.shape qimg QImage(rgb.data, w, h, 3 * w, QImage.Format_RGB888) self.video_label.setPixmap(QPixmap.fromImage(qimg).scaled( self.video_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation))这段代码里有一个性能关键点QImage(rgb.data, w, h, 3 * w, ...)的第四个参数是每行字节数必须写成3 * w。如果不写Qt 默认按字节对齐规则计算遇到宽度不是 4 倍数的图像时画面会错位拉伸。这个坑特别隐蔽图像看起来只是有点斜但怎么排查都找不到原因。rgb.data直接引用了 numpy 数组的内存槽函数返回后数组可能被回收所以QImage需要持有一份拷贝——pyqt 内部会自动处理但如果你在cvtColor之后又修改了frame界面上显示的内容可能跟着变。稳妥做法是在QImage前调用np.ascontiguousarray(rgb)确保内存连续。界面刷新频率上不要每帧都scaled画面尺寸不变时可以只缩放一次把缩放后的 QPixmap 存起来后续直接setPixmap。对于 1920x1080 的输入缩放到 800x450 显示能明显减轻绘制开销。另一种折中是检测线程固定每 3 帧推理一次但每一帧都发信号给界面显示原始视频这样视频流畅度不受推理速度拖累计数也不会比原来少。这是交通监控场景最实用的配置画面要流畅检测不必每帧都跑。5. yolov3pyqt5 集成避坑清单环境、路径、线程与打包的 5 条踩坑记录5.1 Python、PyQt5、OpenCV 版本互相打架现象按说明pip install pyqt5 opencv-python装完之后import cv2 正常import PyQt5 正常但一运行程序就崩溃报错信息指向 Qt 平台插件或者 cv2 和 PyQt5 同时 import 时闪退。原因OpenCV 的不同版本捆绑了自己的 Qt 库和 PyQt5 的 Qt 库版本冲突。这类问题是环境类项目的头号翻车点版本玄学不是代码逻辑问题。解决用虚拟环境隔离并且锁版本组合。我验证过一组稳定组合Python 3.8 PyQt5 5.15.4 opencv-python 4.5.4 numpy 1.21这套组合在 Windows 10 和 Ubuntu 20.04 上都能正常跑 yolov3 推理界面。装的时候一次性指定版本python -m venv traffic_env traffic_env\Scripts\activate pip install pyqt55.15.4 opencv-python4.5.4 numpy1.21.0如果你下载的源码包 requirements.txt 里有opencv-python-headless注意这个包不带 GUI 模块和 pyqt5 一起用一般没事但如果程序里调用了cv2.imshow会报错。检测线程里只要不用imshowheadless 版本反而更干净因为避免了两套 Qt 冲突。5.2 相对路径读不到模型和配置文件现象程序在开发目录里双击能跑把整个项目文件夹复制到别的机器双击后报错FileNotFoundError: yolov3.weights或者 OpenCV 加载 cfg 时报解析失败。原因代码里写的是相对路径yolov3.weights当前工作目录和项目根目录不一致时路径就失效了。Windows 上双击启动时工作目录是脚本所在目录但从命令行启动或者打包成 exe 后工作目录可能是 system32 或 exe 所在目录完全不同。解决所有模型文件和配置都改成基于脚本所在目录的绝对路径import os BASE_DIR os.path.dirname(os.path.abspath(__file__)) cfg_path os.path.join(BASE_DIR, config, yolov3.cfg) weights_path os.path.join(BASE_DIR, weights, yolov3.weights)然后启动程序时先做一次文件存在性检查哪个文件缺失直接打印清晰提示而不是让 OpenCV 抛一个看不懂的底层错误。这一步能在部署现场省下大量远程排障时间。5.3 中文路径导致视频读不出来现象视频文件和项目路径里有中文比如D:\项目\traffic\视频.mp4cv2.VideoCapture 返回 False但路径检查文件确实存在。原因OpenCV 基于 C 的文件读取接口在 Windows 上不支持 Unicode 路径中文路径会被截断或乱码。这个问题在英文路径下永远不会暴露项目一交给客户放在中文目录下就翻车。解决用 np.fromfile 读文件字节流再转成 cv2 解码import cv2 import numpy as np def cv2_imread_unicode(file_path): 兼容中文路径的图像读取函数 data np.fromfile(file_path, dtypenp.uint8) return cv2.imdecode(data, cv2.IMREAD_COLOR) def cv2_videocapture_unicode(file_path): 兼容中文路径的视频读取先读字节再解码 data np.fromfile(file_path, dtypenp.uint8) # 写入临时文件是笨办法更好的方式用 imdecode 逐帧解码 cap cv2.VideoCapture() # cv2 没有直接的字节流 VideoCapture 接口常见做法是复制成临时英文路径文件 import tempfile, os tmp tempfile.NamedTemporaryFile(suffix.mp4, deleteFalse) tmp.write(open(file_path, rb).read()) tmp.close() cap.open(tmp.name) return cap视频的中文路径没有图像那么好处理我一般就直接把整个项目放进英文路径目录并要求部署时也遵守。如果是用户上传视频文件到界面里先把上传的文件复制到项目目录下的 temp 文件夹文件名用时间戳命名再交给检测线程绕开 Unicode 问题。这是最省事、最不容易出后续问题的方案。5.4 界面一打开就“未响应”现象点击“开始监控”按钮后窗口卡死标题变成“未响应”几秒后系统弹出强制关闭提示。有时程序还在跑但窗口拖不动、按钮点不了。原因视频读取和检测都在主线程里执行while True循环阻塞了 Qt 的事件循环鼠标消息、重绘消息全部排队处理不了。这是 pyqt5 集成几乎必踩的坑我在 4.2 节里已经强调过这里再给一个检查方法在循环里加一个QtWidgets.QApplication.processEvents()治标但正确解法就是 QThread。解决把检测循环完整搬到 QThread只有frame_ready信号回主线程。注意退出逻辑关闭窗口时设self.running False然后self.detection_thread.wait(2000)等线程跑完当前帧再退出。如果直接关窗口不管线程python 进程会挂在后台不退出任务管理器里能看到残留进程重新运行时会提示摄像头被占用。5.5 模型效果和预期差很远先查预处理现象模型权重是源码包自带的界面也正常跑但检测框明显偏移、置信度普遍很低或者什么都检不出来。换个视频源又稍微好一点。原因大部分是预处理不一致。最容易错的是blobFromImage的swapRB参数——训练时用的是 RGB推理时 OpenCV 读的是 BGR不交换通道模型特征全乱了。其次是输入尺寸不匹配cfg 里是 608代码里却填了 416特征图网格全错位。还有一个冷门原因权重文件本身是 COCO 80 类模型而界面代码的类别列表只有 5 类类别 ID 对不上。解决每拿到一套源码写个最小测试拿一张标注过的路口图片分别用源码的初始化和我的默认参数跑一遍对比。确认 cfg、weights、classes 列表三者是同一组训练产出的。这三个文件必须配套缺一个都别谈效果。5.6 打包 exe 后体积大还打不开现象用 PyInstaller 打包后exe 有 400 多 MB双击后黑框一闪而过或者打开后提示缺少yolov3.weights、找不到platforms/qwindows.dll。原因PyInstaller 默认不会把 OpenCV 和 PyQt5 的所有动态库都带全缺少 Qt 平台的插件目录platforms程序启动时 Qt 无法初始化窗口。权重文件没有加进打包数据运行时自然找不到。解决打包命令显式添加数据文件和 Qt 插件目录并且用--windowed隐藏控制台pyinstaller --windowed --name TrafficMonitor ^ main.py ^ --add-data config;config ^ --add-data weights;weights ^ --add-data V:\Python38\Lib\site-packages\PyQt5\Qt\plugins;PyQt5\Qt\plugins打包后必须在一台没装 python 的机器上测一遍这是检验打包是否完整的最有效方式。如果还报缺 dll用dumpbin /dependents查 exe 引用的 dll逐个补进 dist 目录。打包这类问题没有统一解每台机器的环境都不一样我的习惯是打包后把整个 dist 目录压缩成 zip 发给测试机器运行有问题看日志文件而不是猜。6. 最后一公里用一段路口视频验证整条链路压出第一版帧率报告拿到这套源码后不要急着连摄像头。我先做一次全链路验证下载一段公开的路口监控视频比如 5 分钟、25fps、1080p确认视频里有不同距离的车辆和行人然后写一个简单的离线统计脚本把它跑一遍。# benchmark.py验证模型效果并统计帧率 import time import cv2 import numpy as np net cv2.dnn.readNetFromDarknet(config/yolov3.cfg, weights/yolov3.weights) cap cv2.VideoCapture(test_road.mp4) total_frames 0 start time.time() while cap.isOpened(): ret, frame cap.read() if not ret: break blob cv2.dnn.blobFromImage(frame, 1/255.0, (416, 416), (0, 0, 0), True, cropFalse) net.setInput(blob) net.forward() # 不做后处理只统计推理耗时 total_frames 1 cost time.time() - start cap.release() print(f处理 {total_frames} 帧耗时 {cost:.2f}s平均 FPS {total_frames/cost:.2f})跑出来的 FPS 如果低于 3说明这台机器的 CPU 撑不起 416 输入。我的做法是把输入降到 320或者把blobFromImage的缩放保持 1/255 但把宽高改成(320, 320)同时把 cfg 里的width和height也改成 320。降输入尺寸对远处小目标的影响明显但对近距离车辆检测影响有限。这两者的取舍得根据部署场景定固定机位的十字路口车辆主要出现在画面中下部上半部分是天空和建筑物可以先用cv2.selectROI截取感兴趣区域再送检测FPS 能翻倍。另一个常用技巧是隔帧检测每两帧取一帧做推理车流量统计结果再按系数修正界面视频流依然流畅。最后补一个我自己养成的习惯每改一次参数就把当时的 FPS、mAP、误检案例截图存成一个 markdown 文件标注上视频来源和天气条件。这样一周后回头看每个改动是变好还是变坏全都有据可查。模型调参这事最怕凭感觉——感觉检得好了其实只是换了段容易的视频。建议你拿到这套 yolov3pyqt5 源码后也先立一个这样的基准测试流程再开始改界面和参数希望帮到你。本文还有配套的精品资源点击获取