简介:面向目标检测算法学习者的图形界面程序资源,基于PyQt实现可视化操作,将YOLO系列与Swin Transformer等主流模型的训练、推理与评估流程整合到一个简洁窗口中,使用户不必频繁切换命令行或修改脚本参数,既适合计算机视觉初学者快速搭建演示环境,降低入门门槛,也适合进阶开发者参考界面层与算法模块的整合方式。压缩包内共126个文件,主体为可直接运行的Python源码及其编译生成的字节码文件,另有若干XML配置、文本说明以及少量示例图片与字体资源,整体大小仅5.13MB,目录结构清晰,便于下载、部署和二次修改。程序将界面操作与目标检测的训练、验证、预测逻辑结合起来,覆盖数据读取、模型调用、损失计算、结果输出到可视化展示的完整流程,可帮助读者快速理解GUI封装思路与检测流程,也可作为算法二次开发的脚手架。目前已有161人学习,资源实用清晰,适合课程设计或需要快速交付目标检测演示项目的开发者借鉴。
1. 目标检测项目不是只有算法,还得有一层能点的界面
目标检测模型在终端里跑通容易,但真正要交付给客户或者做课程设计演示,问题很快就变成:别人不会也不想看你敲命令,他们要的是点击按钮、选一张图、立刻看到框和类别。这套下载包里除了常见的 yolov5.py、Swin_transformer.py、yolo_training.py,还多了一个 PyQt5 图形界面层,压缩包里的 pyqt.iml 说明工程是用 PyCharm 组织的,folder.jpeg 和 error.jpeg 则是运行目录和报错截图,配合 utils_map.py 做验证。整体上它不是单纯算法仓库,而是“训练 - 推理 - 界面展示”三层都有。适合正在做目标检测课程设计、毕业设计,或需要在本地搭一个可视化 Demo 的开发者和研究生。
2. 从 yolov5.py 到 Swin_transformer.py:先看清模型怎么被组装
2.1 文件角色与两条技术链路
解压 zip 后,先把文件按职责分成三类,不然很容易在源码里迷路。模型侧是 yolov5.py、Swin_transformer.py、yolo_training.py;验证侧是 utils_map.py;界面侧则以 PyQt5 为主,pyqt.iml 是 PyCharm 的工程标记,folder.jpeg 和 error.jpeg 分别是目录截图和报错现场。文件角色整理如下:
| 文件 | 类型 | 职责 |
|---|---|---|
| yolov5.py | 模型定义 | 检测主干、Neck、Head 的前向结构 |
| Swin_transformer.py | 骨干网络 | 与 yolov5.py 配合,提供 Swin Backbone |
| yolo_training.py | 训练脚本 | 损失函数、优化器、训练循环 |
| utils_map.py | 验证工具 | 计算 Precision/Recall/mAP |
| pyqt.iml | IDE 工程 | PyCharm 模块标记 |
| folder.jpeg / error.jpeg | 截图 | 目录结构与报错记录 |
两条链路的关系是:yolov5.py 和 Swin_transformer.py 先完成前向推理,yolo_training.py 在训练时把标注数据变成损失,GUI 再加载训练好的权重做展示。因此阅读顺序建议是“先模型推理,再训练,最后接界面”。如果你只改界面不动网络,可以直接跳过训练细节,但要保留 YOLOLoss 里对输出维度的约定,否则换成其他训练脚本时,GUI 拿到的输出格式会不一致。
2.2 yolov5.py 的网络骨架与输出维度
yolov5.py 的核心是把输入图像拆到多个特征层上。常见结构是 backbone 提取特征,neck 做多尺度融合,head 输出三个尺度的预测框。这里给一个简化调用示例:
import torch from yolov5 import YOLOv5 # 项目内的检测模型类 model = YOLOv5(num_classes=20, # 训练时你的类别数 anchor_num=3, backbone='swin_tiny') # 可选 cspdarknet / swin_tiny model.eval() dummy = torch.randn(1, 3, 640, 640) with torch.no_grad(): outputs = model(dummy) print([out.shape for out in outputs])参数说明:num_classes决定最后的类别维度,anchor_num是每个网格预设的先验框数量,backbone控制用哪种特征提取器。三个输出 tensor 的维度一般是[batch, anchor_num, grid_h, grid_w, 5+num_classes],其中5对应中心点 x/y、宽高 w/h、前景置信度。如果模型实现里带了detect层,还会额外输出一次映射回原图的坐标,使用 GUI 绘图前要确认检测框坐标单位到底是不是像素,否则画出来会偏。
当你把 backbone 换成 Swin Transformer,输入预处理会变。Swin 通常期望除以 255 后的 RGB 输入,而 YOLOv5 默认训练时也会做归一化,但通道顺序不同会直接影响精度。我实际调试时遇到过训练正常、推理框偏移的情况,最后定位到是 image_size 从 640 被 resize 成了 416,Swin 的 patch embedding 对分辨率更敏感,重新统一到 640 后恢复正常。
2.3 yolo_training.py 的损失计算与训练流程
yolo_training.py 里一般定义了一个 YOLOLoss,作用是把模型输出的三张特征图与真实框对齐。对齐步骤包括:还原归一化坐标、计算 IoU、分配正负样本,然后叠加回归损失、置信度损失和分类损失。损失骨架如下:
def loss_fn(preds, targets, model): lcls, lbox, lobj = torch.zeros(1), torch.zeros(1), torch.zeros(1) for i, pred in enumerate(preds): # pred.shape: (batch, anchor_num, grid, grid, 5+num_classes) box_xy = pred[..., 0:2].sigmoid() box_wh = pred[..., 2:4].exp() obj = pred[..., 4:5].sigmoid() cls = pred[..., 5:].sigmoid() # 这里会与 targets 计算 IoU,并产生正样本 mask ... return lbox + lobj + lclslbox是定位损失,lobj是前景/背景判断损失,lcls是类别损失。训练时一般会设置三个权重系数。如果你发现模型收敛很慢,优先观察lobj是否一直不下降;如果检测框位置不准,就把lbox的权重调大。常见做法是 YOLOv5 默认box=0.05、cls=0.5、obj=1.0,但项目里接了 Swin Transformer,建议把lbox提到 0.1 附近,因为 Swin 的局部窗口对位置回归更敏感,需要更强的监督。这里有个容易踩的地方:若直接使用官方 YOLOv5 预训练权重,而 backbone 已经替换成 Swin,前几层权重维度不匹配,会在加载时报错。逻辑上要先加载 Swin 预训练权重,再随机初始化检测头。
3. PyQt5 GUI 界面:把检测结果画到控件上
3.1 界面布局与控件职责
一个能用的目标检测 GUI 至少要有三个部分:图片选择区、结果显示区、日志/状态区。我建议用纯 PyQt5 代码搭建,避免 Qt Designer 生成的 ui 文件在跨机器运行时路径失效。控件安排如下:
| 控件 | 作用 | 关键属性 |
|---|---|---|
| QPushButton(text='选择图片') | 打开文件对话框 | clicked 信号连接槽函数 |
| QLabel(图片显示) | 显示原图和检测结果 | setPixmap / setScaledContents |
| QListWidget(图片列表) | 批量选择图片 | 点击信号切换 |
| QTextEdit(日志) | 显示推理耗时、类别和置信度 | setReadOnly(True) |
| QComboBox(模型选择) | 切换不同权重文件 | 此处可做模型热切换 |
布局上外层用 QVBoxLayout,中间区域放图片 label,下面放按钮和日志。关键点是别把推理直接写在 clicked 槽函数里,否则图片大一点界面会卡死。我之前接过一个需求,要求同时显示 4 路摄像头画面,如果每路推理都在主线程做,QTimer 刷新会全部停摆,后面统一改成线程队列才解决。
3.2 用 QThread 避免界面卡顿
每次点击“检测”,模型推理可能耗时 100ms 到几百 ms,这个时间足够让 PyQt5 主事件循环失去响应。常规做法是写一个继承 QThread 的 Worker:
from PyQt5.QtCore import QThread, pyqtSignal import cv2 class DetectWorker(QThread): result_ready = pyqtSignal(object, object, float) # 原图, 检测结果, 耗时 def __init__(self, detector, image): super().__init__() self.detector = detector self.image = image def run(self): t0 = cv2.getTickCount() boxes, labels, scores = self.detector.detect(self.image) cost_ms = (cv2.getTickCount() - t0) / cv2.getTickFrequency() * 1000 self.result_ready.emit(self.image, (boxes, labels, scores), cost_ms)detector.detect返回的 boxes 是[x1, y1, x2, y2],labels 是类别索引,scores 是置信度。信号里传三个数据,主线程槽函数拿到后画框。如果不创建线程,视频流检测时窗口会频繁提示“未响应”;用 QThread 后,模型推理时仍可以拖动窗口、点击关闭按钮。需要注意线程对象不能立即回收,最好把 DetectWorker 保存为类成员,否则任务还没执行完就被 Python 垃圾回收。
3.3 结果绘制与 QImage 格式转换
PyQt5 的 QLabel 不能直接显示 OpenCV 的 BGR 图片,要先转成 RGB,再构造 QImage。检测框可以在 OpenCV 里画好,然后一次性转换:
def draw_results(image, boxes, labels, scores, class_names): result = image.copy() for (x1, y1, x2, y2), label, score in zip(boxes, labels, scores): c = (0, 255, 0) cv2.rectangle(result, (int(x1), int(y1)), (int(x2), int(y2)), c, 2) text = f"{class_names[label]} {score:.2f}" cv2.putText(result, text, (int(x1), int(y1) - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, c, 1) return result def pixmap_from_cv(image): rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) h, w, ch = rgb.shape bytes_per_line = ch * w qimg = QImage(rgb.data, w, h, bytes_per_line, QImage.Format_RGB888) return QPixmap.fromImage(qimg.copy())代码里对原图做了copy(),避免连续检测时把上一帧的框叠加到这一帧。cv2.rectangle的坐标参数必须是 int,否则旧版 OpenCV 会抛异常。QImage 构造后立刻copy()也很重要,因为rgb.data指向 Python 对象内部缓冲区,函数返回后该缓冲区可能被释放,显示时会出现花屏或滚动条。类别名列表要和训练时的 class 顺序严格一致,否则框、置信度对不上,这属于推理侧对齐问题,不是 GUI 的 bug。
提示:如果 QLabel 显示模糊,检查
setScaledContents(True)是否开启,以及缩放时是否保持宽高比。用setPixmap(pixmap.scaled(label.size(), Qt.KeepAspectRatio))更稳妥。
4. 训练结果验证与踩坑排查:从 utils_map.py 到 error.jpeg
4.1 用 utils_map.py 计算 mAP
utils_map.py 是验证目标检测模型效果的常用工具。它只做评估,不参与训练,把 detect 输出的 txt 结果与 ground truth 对比,按置信度排序后计算 Precision、Recall 和 AP。调用方式一般是:
python utils_map.py \ --gt_path ./datasets/ours/labels \ --det_path ./output/predictions \ --iou_thres 0.5gt_path里的文件命名要和图片一一对应,每行格式为class_id x_center y_center width height,坐标是归一化到 0~1 的。det_path存放模型输出文件,每行格式为class_id score x_center y_center width height。脚本会打印每个类别的 AP 和整体 mAP。如果你发现 mAP 非常低,先检查是不是把像素坐标直接喂给了脚本,这会直接导致 IoU 计算异常。另外,预测文件里 score 和 x/y/w/h 的顺序不能颠倒,否则脚本会当成错误类别或错误位置处理。
注意:使用
--iou_thres 0.5和--iou_thres 0.75得到的结果差异很大。报告中最好写清楚阈值,不然别人复现时很难对齐数据。
4.2 训练参数怎么调
yolo_training.py 里可配置项很多,我把常用参数整理成参考表:
| 参数 | 示例值 | 说明 |
|---|---|---|
| batch_size | 8 | 显存不足时降到 2~4 |
| epochs | 300 | 数据量小可减少到 100 |
| image_size | 640 | Swin Transformer 建议用模型原始尺度 |
| lr0 | 0.01 | 骨干网络是 Swin 时建议降到 0.001 |
| weight_decay | 0.0005 | 防止过拟合 |
| iou_thres | 0.5 | 训练时正样本的 IoU 阈值 |
如果使用 Swin Transformer 替换主干,image_size要尽量匹配预训练权重的分辨率,不要随意设成 608 或 832。曾经遇到一个问题:切到 Swin 后 loss 开始下降,但验证集 mAP 一直上不去,最后发现是骨干网络的权重没有正确加载,等于随机初始化训练。检查方式是在训练前打印model.backbone第一层卷积的权重均值,如果接近 PyTorch 默认初始化范围,就说明预训练权重没有生效。
4.3 从 error.jpeg 里读出的常见报错
error.jpeg 保存的是开发调试时的报错现场。这类截图对排查问题的价值比口头描述大,重点看堆栈最后一行。常见报错包括:
QObject::startTimer: Timers can only be used with threads started with QThread:说明 QTimer 跨线程使用,检测任务里不要直接用 QTimer 更新 UI,改用 pyqtSignal。cv2.error: OpenCV(4.x) ... (-215:Assertion failed) !ssize.empty():图片读取失败,一般是路径带中文、文件不存在或文件损坏。IndexError: index 5 is out of bounds for axis 0 with size 5:类别数超过了 class_names 的长度,检查 num_classes 是否和训练数据一致。CUDA out of memory:把 batch_size 减半,同时不要开启数据加载线程的persistent_workers。
排查顺序是:先看异常类型和最后一行堆栈,再去对应文件搜索自己写的调用点。不要一上来就重装环境,很多报错其实是参数或文件路径不一致。folder.jpeg 的作用是帮你确认源码目录是否被移动过,如果本机运行时因为相对路径找不到文件,先对着这张截图检查目录层级。
5. 目标检测 GUI 的进阶:批量推理与模型热切换
界面稳定后,可以扩展两个很实用的功能。第一个是批量推理:把“选择单张图片”改成“选择文件夹”,用 glob 遍历所有图片,逐张检测并用之前定义的 draw_results 保存结果:
import glob import os def batch_detect(detector, input_dir, output_dir, class_names): os.makedirs(output_dir, exist_ok=True) images = glob.glob(os.path.join(input_dir, '*.jpg')) for i, img_path in enumerate(images): img = cv2.imread(img_path) if img is None: continue boxes, labels, scores = detector.detect(img) result_img = draw_results(img.copy(), boxes, labels, scores, class_names) name = os.path.basename(img_path) cv2.imwrite(os.path.join(output_dir, name), result_img) # 进度更新:progress_bar.setValue(int((i + 1) / len(images) * 100))检测和绘制函数复用了之前章节的实现,确保 draw_results 不在原图上直接修改即可。批量处理时,日志区可以显示当前文件序号和累计耗时。GUI 里再放一个 QProgressBar,每处理一张就更新一次,用户在长时间批量检测时能知道进度。
第二个技巧是模型热切换,也就是 GUI 运行时不重启程序,直接换权重文件。关键点在于释放旧显存:
def switch_model(self, new_weight_path): self.statusbar.showMessage("正在加载模型...") del self.detector # 释放旧模型引用 torch.cuda.empty_cache() self.detector = Detector(new_weight_path) self.statusbar.showMessage(f"模型已加载: {new_weight_path}")del先删除顶层引用,empty_cache()把未使用的缓存回收。切换前要确认信号槽里没有其他对象还持有旧 detector 的引用,否则显存不会真正释放。验证方法很简单:连续切换 10 次,观察显存和内存占用是否持续增长,不增长说明没有引用泄漏。将batch_detect挂到“批量检测”按钮,把switch_model挂到 QComboBox 的currentIndexChanged信号,GUI 就具备了批量输出和运行时换模型的能力。
本文还有配套的精品资源,点击获取