简介:这是一份面向PyQt5与YOLOv5初学者的GUI目标检测实战项目,专为刚接触GUI开发和目标检测算法的学习者设计,帮助其快速搭建可运行的多目标检测桌面应用。资源共112个文件,包含26个Python源码(含主程序、模型加载、界面逻辑等)、25个YAML配置文件(定义类别、训练参数及模型结构)、3个PyTorch预训练权重(.pt)、1个Qt Designer生成的.ui界面文件,以及测试用图像、视频、图标和Docker部署脚本等,整体压缩包83.46MB,结构清晰,模块分离明确。已有8875人学习下载,体现了较强的实践参考价值。学习者可完整掌握PyQt5界面设计与后端业务解耦方法、YOLOv5推理流程集成、常见控件(如QLabel动态显示、QPushButton事件绑定、QFileDialog文件选择)的实际应用,并通过附带的GIF动图、实测视频与多场景测试图像(bus.jpg、zidane.jpg等)直观验证效果,具备即学即用的工程落地基础。
1. 为什么用 PyQt5 + YOLOv5 做本地目标检测 GUI,比直接跑 detect.py 更值得投入?
你刚训完一个 YOLOv5s 模型,准确率 87.3%,mAP@0.5 达标,但老板/导师/客户第一句问的不是指标,而是:“能点开就用吗?能不能拖张图进去看看结果?能不能连摄像头实时框出来?能不能导出带框的图和 CSV 表格?”——这时候python detect.py --weights best.pt --source 0的黑窗命令行,立刻变成沟通黑洞。PyQt5 + YOLOv5 不是炫技组合,它是把训练成果真正“交付”出去的最小可行闭环:用 Python 写界面、用 YOLOv5 做推理、用 PyQt5 把图像/视频/摄像头流喂进去、把检测框+标签+置信度实时画出来、再一键保存结果。它不依赖 Web 服务、不卡在浏览器兼容性里、不靠第三方平台托管,所有逻辑跑在用户本机——这对工业质检现场、教学演示、嵌入式边缘设备(如树莓派4B部署YOLOv5)、离线安防系统尤其关键。本文不讲“PyQt5 安装教程”或“YOLOv5 训练自己的数据集”这种泛泛而谈的步骤,而是聚焦如何把这两个技术栈严丝合缝地焊在一起:从环境隔离开始,到模型加载优化,再到 PyQt5 中正确处理 OpenCV 图像通道与 Qt QImage 格式转换这个玄学坑,最后落地成一个可双击运行、支持图片/视频/摄像头三路输入、带进度条和结果导出的完整 GUI 工程。新手照着走通,熟手能抄参数调性能。
2. 环境隔离与依赖安装:为什么必须用 conda 而不是 pip 全局装?
PyQt5 和 YOLOv5 对底层库版本极其敏感:PyQt5 5.15.x 与 Python 3.8–3.10 兼容性尚可,但若混装了 PySide2 或旧版 sip,QApplication初始化会静默失败;YOLOv5 依赖 torch 1.13+(CUDA 11.7)和 torchvision 0.14+,而pip install pyqt5可能拉下 Qt6 绑定(PyQt6),导致QPixmap.fromImage()报TypeError: 'QImage' object is not callable这类黑匣子错误。常见做法是用 conda 创建独立环境,精确锁定版本链。我一般会这样做:
2.1 创建专用 conda 环境并安装核心依赖
# 创建 Python 3.9 环境(YOLOv5 v6.2+ 官方推荐 3.8–3.10) conda create -n yolo-pyqt python=3.9 conda activate yolo-pyqt # 优先用 conda-forge 安装 Qt 相关(避免 pip 混装冲突) conda install -c conda-forge pyqt=5.15.9 qt=5.15.2 # 再用 pip 安装 YOLOv5 官方仓库(注意:不要用 pip install yolov5!那是旧版 PyPI 包) pip install torch==1.13.1+cu117 torchvision==0.14.1+cu117 --extra-index-url https://download.pytorch.org/whl/cu117 pip install numpy opencv-python==4.8.0.76 pillow matplotlib # 克隆 YOLOv5 官方代码(v6.2 是当前最稳的 LTS 版本) git clone https://github.com/ultralytics/yolov5 cd yolov5 pip install -e .提示:
pip install -e .是关键——它把 yolov5 目录作为可编辑包安装,后续修改models/common.py或utils/plots.py时无需重新 pip install,直接生效。很多翻车源于用了 PyPI 上的yolov5包(版本陈旧、无 detect.py 接口),导致from models.experimental import attempt_load找不到模块。
2.2 验证 PyQt5 与 OpenCV 图像互通性:一个必跑的最小测试
光装对不行,得验证 Qt 能否正确显示 OpenCV 读入的图像。新建test_qt_cv.py:
import sys import cv2 from PyQt5.QtWidgets import QApplication, QLabel, QVBoxLayout, QWidget from PyQt5.QtGui import QImage, QPixmap from PyQt5.QtCore import Qt def cv2_to_qt_image(cv_img): """OpenCV BGR → Qt RGB 转换:核心是 bytesPerLine 和 format 参数""" height, width, channel = cv_img.shape bytes_per_line = 3 * width # 注意:OpenCV 是 BGR,Qt 是 RGB,必须 cvtColor 转换! rgb_img = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB) qt_img = QImage(rgb_img.data, width, height, bytes_per_line, QImage.Format_RGB888) return qt_img if __name__ == "__main__": app = QApplication(sys.argv) window = QWidget() layout = QVBoxLayout(window) # 读一张图测试(确保 yolov5/data/images/zidane.jpg 存在) img = cv2.imread("yolov5/data/images/zidane.jpg") if img is None: print("⚠️ 图片路径错误!请确认 yolov5/data/images/zidane.jpg 存在") sys.exit(1) qt_img = cv2_to_qt_image(img) label = QLabel() label.setPixmap(QPixmap.fromImage(qt_img).scaled(640, 480, Qt.KeepAspectRatio)) layout.addWidget(label) window.show() sys.exit(app.exec_())参数说明:
bytes_per_line = 3 * width:RGB 图像每行字节数 = 宽 × 3(R/G/B 各 1 字节),若用cv2.IMREAD_GRAYSCALE则应为width;QImage.Format_RGB888:必须匹配cv2.cvtColor(..., cv2.COLOR_BGR2RGB)输出格式,错用Format_BGR888会导致颜色紫红失真;scaled(..., Qt.KeepAspectRatio):防止图像拉伸变形,GUI 中必备。
运行此脚本无报错且显示 zidane 图像,证明 PyQt5 + OpenCV 底层链路已通。这是后续所有 GUI 开发的基石——没过这关,后面所有检测框绘制都是空中楼阁。
3. YOLOv5 模型加载与推理封装:如何让 detect() 函数适配 PyQt5 主线程?
YOLOv5 官方detect.py是为命令行设计的:它初始化model、dataset、save_dir一气呵成,但 GUI 要求模型只加载一次、推理函数能被按钮反复调用、且不能阻塞 UI(否则点击“检测”后界面假死)。直接复用detect.py会踩三个坑:1)每次调用都重载模型,慢;2)torch.no_grad()上下文管理器在 Qt 事件循环中可能失效;3)plot_one_box()返回的是 PIL.Image,而 PyQt5 需要QPixmap。我一般会把 YOLOv5 推理逻辑抽成一个独立类YOLOv5Detector,并做三处关键改造:
3.1 封装 YOLOv5Detector 类:支持热加载与线程安全
# detector.py import torch import cv2 import numpy as np from pathlib import Path from models.common import DetectMultiBackend from utils.general import non_max_suppression, scale_boxes from utils.plots import Annotator, colors class YOLOv5Detector: def __init__(self, weights="yolov5s.pt", device="cuda" if torch.cuda.is_available() else "cpu"): self.device = device self.model = DetectMultiBackend(weights, device=self.device, dnn=False, data="data/coco128.yaml", fp16=False) self.stride = self.model.stride self.names = self.model.names self.pt = self.model.pt self.model.warmup(imgsz=(1, 3, 640, 640)) # 首次推理前 warmup,避免首次耗时抖动 print(f"✅ YOLOv5 model loaded on {self.device}") def preprocess(self, img0): """OpenCV BGR 图 → YOLOv5 tensor 输入:含归一化、resize、batch 维度添加""" img = cv2.resize(img0, (640, 640)) # YOLOv5 默认输入尺寸 img = img.transpose((2, 0, 1))[::-1] # HWC → CHW, BGR → RGB img = np.ascontiguousarray(img) img = torch.from_numpy(img).to(self.device).float() / 255.0 if len(img.shape) == 3: img = img.unsqueeze(0) # 添加 batch 维度 return img def infer(self, img0): """主推理函数:返回 (boxes, confidences, class_ids, annotated_img)""" img = self.preprocess(img0) pred = self.model(img, augment=False, visualize=False) pred = non_max_suppression(pred[0], conf_thres=0.25, iou_thres=0.45)[0] # 只取 batch[0] if len(pred) == 0: return [], [], [], img0.copy() # 提取结果 boxes = pred[:, :4].cpu().numpy() # xyxy confidences = pred[:, 4].cpu().numpy() class_ids = pred[:, 5].cpu().numpy().astype(int) # 绘制检测框(返回 OpenCV BGR 图) annotator = Annotator(img0, line_width=2, example=str(self.names)) for i, (box, conf, cls_id) in enumerate(zip(boxes, confidences, class_ids)): c = int(cls_id) label = f"{self.names[c]} {conf:.2f}" annotator.box_label(box, label, color=colors(c, True)) return boxes, confidences, class_ids, annotator.result() def release(self): """释放 GPU 显存(重要!否则多次切换模型会 OOM)""" if self.device != "cpu": torch.cuda.empty_cache()逻辑说明与参数说明:
DetectMultiBackend是 YOLOv5 v6.0+ 的统一模型加载器,自动识别 pt/onnx/engine 等格式,比旧版torch.load()更鲁棒;warmup()必须调用:它执行一次 dummy inference,让 CUDA kernel 编译完成,否则首次推理慢 3–5 倍;preprocess()中img.transpose((2,0,1))[::-1]是关键:OpenCV 读图是 HWC-BGR,YOLOv5 要 CHW-RGB,[::-1]实现 BGR→RGB(等价于cv2.cvtColor(img, cv2.COLOR_BGR2RGB).transpose(2,0,1),但更快);infer()返回annotator.result()是 OpenCV BGR 格式图像,可直接传给cv2_to_qt_image(),避免 PIL → NumPy → Qt 多次转换。
3.2 在 PyQt5 中调用 detector:避免主线程阻塞的两种方案
GUI 点击“检测”按钮时,若直接调用detector.infer(),大图推理(如 1920×1080)可能卡住 UI 1–2 秒。必须用 QThread 或 QTimer 做异步封装。我倾向用QThread(更可控):
# main_window.py from PyQt5.QtCore import QThread, pyqtSignal from detector import YOLOv5Detector class DetectThread(QThread): result_ready = pyqtSignal(object, object, object, object) # boxes, confs, classes, annotated_img def __init__(self, detector, img0): super().__init__() self.detector = detector self.img0 = img0 def run(self): boxes, confs, classes, annotated_img = self.detector.infer(self.img0) self.result_ready.emit(boxes, confs, classes, annotated_img)然后在主窗口中:
def on_detect_clicked(self): if not hasattr(self, 'detector') or self.detector is None: self.detector = YOLOv5Detector("runs/train/exp/weights/best.pt") # 加载你训好的模型 self.detect_thread = DetectThread(self.detector, self.current_cv_img) self.detect_thread.result_ready.connect(self.on_detection_finished) self.detect_thread.start() self.status_label.setText("🔍 正在检测...") def on_detection_finished(self, boxes, confs, classes, annotated_img): self.current_cv_img = annotated_img # 更新原图 qt_img = cv2_to_qt_image(annotated_img) self.image_label.setPixmap(QPixmap.fromImage(qt_img).scaled( self.image_label.size(), Qt.KeepAspectRatio)) self.status_label.setText(f"✅ 检测完成:{len(boxes)} 个目标")注意:
QThread实例不能复用,每次检测必须新建DetectThread对象,否则result_ready信号可能丢失。
4. PyQt5 GUI 构建:三路输入(图片/视频/摄像头)与结果导出的完整实现
一个合格的 YOLOv5 GUI 至少要支持三种输入源,并提供结果可视化与结构化输出。我一般用 QTabWidget 分三页,每页对应一种输入方式,共享同一个 detector 实例和结果显示区。下面给出核心控件布局与事件绑定逻辑(省略样式美化,专注功能落地):
4.1 主窗口布局:QTabWidget + 图像显示区 + 控制按钮
# main_window.py from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QTabWidget, QFileDialog, QSlider, QLineEdit, QCheckBox, QGroupBox) from PyQt5.QtCore import Qt, QTimer class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("YOLOv5 + PyQt5 目标检测 GUI") self.setGeometry(100, 100, 1200, 800) # 主体布局 central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout(central_widget) # 顶部 Tab 页 self.tab_widget = QTabWidget() self.tab_widget.addTab(self.create_image_tab(), "🖼️ 图片检测") self.tab_widget.addTab(self.create_video_tab(), "🎬 视频检测") self.tab_widget.addTab(self.create_camera_tab(), "📹 实时摄像头") main_layout.addWidget(self.tab_widget) # 底部状态栏与控制区 status_layout = QHBoxLayout() self.status_label = QLabel("准备就绪") self.detect_btn = QPushButton("🚀 开始检测") self.detect_btn.clicked.connect(self.on_detect_clicked) self.export_btn = QPushButton("💾 导出结果") self.export_btn.clicked.connect(self.on_export_clicked) status_layout.addWidget(self.status_label) status_layout.addStretch() status_layout.addWidget(self.detect_btn) status_layout.addWidget(self.export_btn) main_layout.addLayout(status_layout) # 共享图像显示区(所有 Tab 共用) self.image_label = QLabel() self.image_label.setAlignment(Qt.AlignCenter) self.image_label.setMinimumSize(800, 600) main_layout.addWidget(self.image_label) # 初始化变量 self.current_cv_img = None self.cap = None # 视频/摄像头捕获对象 self.timer = QTimer() # 用于视频/摄像头定时刷新 self.timer.timeout.connect(self.update_frame)4.2 图片 Tab:支持拖拽、缩放、多图轮播
def create_image_tab(self): tab = QWidget() layout = QVBoxLayout(tab) # 图片选择按钮 btn_layout = QHBoxLayout() self.load_img_btn = QPushButton("📂 加载图片") self.load_img_btn.clicked.connect(self.load_image) btn_layout.addWidget(self.load_img_btn) layout.addLayout(btn_layout) # 图片显示区(已定义在主布局中,此处仅占位) layout.addWidget(QLabel("图片将在此显示")) return tab def load_image(self): file_path, _ = QFileDialog.getOpenFileName( self, "选择图片", "", "Image Files (*.jpg *.jpeg *.png *.bmp)" ) if file_path: self.current_cv_img = cv2.imread(file_path) if self.current_cv_img is None: self.status_label.setText("❌ 图片读取失败,请检查格式") return qt_img = cv2_to_qt_image(self.current_cv_img) self.image_label.setPixmap(QPixmap.fromImage(qt_img).scaled( self.image_label.size(), Qt.KeepAspectRatio)) self.status_label.setText(f"✅ 已加载:{Path(file_path).name}")4.3 视频 Tab:支持播放/暂停/进度条控制
def create_video_tab(self): tab = QWidget() layout = QVBoxLayout(tab) # 视频控制区 ctrl_layout = QHBoxLayout() self.load_video_btn = QPushButton("📁 加载视频") self.load_video_btn.clicked.connect(self.load_video) self.play_btn = QPushButton("▶️ 播放") self.play_btn.clicked.connect(self.toggle_playback) self.play_btn.setEnabled(False) ctrl_layout.addWidget(self.load_video_btn) ctrl_layout.addWidget(self.play_btn) layout.addLayout(ctrl_layout) # 进度条(伪进度,实际按帧数计算) self.progress_slider = QSlider(Qt.Horizontal) self.progress_slider.valueChanged.connect(self.on_slider_changed) self.progress_slider.setEnabled(False) layout.addWidget(self.progress_slider) return tab def load_video(self): file_path, _ = QFileDialog.getOpenFileName( self, "选择视频", "", "Video Files (*.mp4 *.avi *.mov)" ) if file_path: self.cap = cv2.VideoCapture(file_path) if not self.cap.isOpened(): self.status_label.setText("❌ 视频打开失败") return self.total_frames = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT)) self.progress_slider.setMaximum(self.total_frames) self.progress_slider.setEnabled(True) self.play_btn.setEnabled(True) self.status_label.setText(f"✅ 已加载视频:{Path(file_path).name} ({self.total_frames} 帧)") def toggle_playback(self): if not hasattr(self, 'timer') or not self.timer.isActive(): self.timer.start(30) # 30ms ≈ 33fps self.play_btn.setText("⏸️ 暂停") else: self.timer.stop() self.play_btn.setText("▶️ 播放") def update_frame(self): if self.cap and self.cap.isOpened(): ret, frame = self.cap.read() if ret: self.current_cv_img = frame qt_img = cv2_to_qt_image(frame) self.image_label.setPixmap(QPixmap.fromImage(qt_img).scaled( self.image_label.size(), Qt.KeepAspectRatio)) current_frame = int(self.cap.get(cv2.CAP_PROP_POS_FRAMES)) self.progress_slider.setValue(current_frame) else: self.timer.stop() self.play_btn.setText("▶️ 重播") self.status_label.setText("🎬 视频播放完毕")4.4 摄像头 Tab:支持设备选择与分辨率设置
def create_camera_tab(self): tab = QWidget() layout = QVBoxLayout(tab) # 设备选择 dev_layout = QHBoxLayout() dev_layout.addWidget(QLabel("摄像头 ID:")) self.cam_id_input = QLineEdit("0") self.cam_id_input.setFixedWidth(50) dev_layout.addWidget(self.cam_id_input) self.open_cam_btn = QPushButton("🔌 打开摄像头") self.open_cam_btn.clicked.connect(self.open_camera) dev_layout.addWidget(self.open_cam_btn) layout.addLayout(dev_layout) # 分辨率设置(常见选项) res_layout = QHBoxLayout() res_layout.addWidget(QLabel("分辨率:")) self.res_combo = QComboBox() self.res_combo.addItems(["640x480", "1280x720", "1920x1080"]) self.res_combo.setCurrentText("1280x720") res_layout.addWidget(self.res_combo) layout.addLayout(res_layout) return tab def open_camera(self): try: cam_id = int(self.cam_id_input.text()) self.cap = cv2.VideoCapture(cam_id) if not self.cap.isOpened(): self.status_label.setText(f"❌ 无法打开摄像头 {cam_id}") return # 设置分辨率 res = self.res_combo.currentText() w, h = map(int, res.split('x')) self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, w) self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, h) self.timer.start(30) self.status_label.setText(f"📹 摄像头 {cam_id} 已启动 ({res})") except ValueError: self.status_label.setText("❌ 摄像头 ID 必须为数字")5. 避坑指南:PyQt5 + YOLOv5 联调中最常踩的 5 个血泪坑
这些坑我在三个不同项目(工业缺陷检测、课堂实验平台、树莓派4B部署YOLOv5)中反复遇到,轻则功能失效,重则程序崩溃。现象、原因、解决三要素缺一不可,照着改就能活:
5.1 现象:QApplication: invalid style override passed, ignoring it+ 界面空白
原因:conda 安装 PyQt5 时默认带 Qt5 样式,但某些 Linux 发行版(如 Ubuntu 22.04)预装了 Qt6 库,导致QApplication初始化时样式冲突,静默失败。
解决:在main.py最开头强制指定 Qt5 样式(必须在QApplication创建前):
import os os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = '/path/to/anaconda3/envs/yolo-pyqt/plugins/platforms' # 或更通用的写法(自动定位): import sys from PyQt5.QtWidgets import QApplication app = QApplication(sys.argv) app.setStyle('Fusion') # 强制 Fusion 风格,避开系统插件冲突5.2 现象:检测框颜色全为白色,或类别标签不显示
原因:utils.plots.Annotator中colors()函数依赖self.names,而self.names来自data/coco128.yaml。若你用自己数据集训练,best.pt中names字段可能为空或长度不匹配(如 10 类却只有 8 个 name)。
解决:加载模型后手动校验并修复names:
# 在 YOLOv5Detector.__init__() 中 model 加载后添加 if len(self.model.names) < len(self.model.names): # 防止 names 为空 self.model.names = ['class_{}'.format(i) for i in range(10)] # 临时填充 # 或更稳妥:从你的 dataset.yaml 读取 names with open("data/my_dataset.yaml") as f: data = yaml.safe_load(f) self.model.names = data['names']5.3 现象:摄像头画面卡顿、掉帧严重,CPU 占用 100%
原因:QTimer.timeout信号频率过高(如设为 10ms),但cv2.VideoCapture.read()+detector.infer()+cv2_to_qt_image()整体耗时 > 10ms,导致事件队列堆积,Qt 主线程忙于处理超量 timeout 信号。
解决:用QTimer.singleShot()替代循环 timer,确保一帧处理完才触发下一帧:
def update_frame(self): if self.cap and self.cap.isOpened(): ret, frame = self.cap.read() if ret: self.current_cv_img = frame # 异步检测(可选) self.detect_thread = DetectThread(self.detector, frame) self.detect_thread.result_ready.connect(lambda b,c,cl,img: self.show_result(img)) self.detect_thread.start() # 关键:不再用 timer.start(),而是单次触发 QTimer.singleShot(30, self.update_frame) # 下一帧延迟 30ms5.4 现象:导出 CSV 时中文乱码(Windows 上尤甚)
原因:pandas.DataFrame.to_csv()默认用utf-8编码,但 Excel for Windows 默认读gbk,导致中文列名/标签显示为æ¡。
解决:导出时显式指定encoding='utf_8_sig'(UTF-8 with BOM):
def on_export_clicked(self): if not hasattr(self, 'current_results') or not self.current_results: return import pandas as pd df = pd.DataFrame(self.current_results, columns=['x1','y1','x2','y2','conf','class']) csv_path, _ = QFileDialog.getSaveFileName(self, "保存结果", "", "CSV Files (*.csv)") if csv_path: df.to_csv(csv_path, index=False, encoding='utf_8_sig') # ✅ 关键参数 self.status_label.setText(f"✅ 结果已保存至 {csv_path}")5.5 现象:打包成 exe 后运行报ModuleNotFoundError: No module named 'models'
原因:PyInstaller 打包时未递归包含yolov5/models/目录,且sys.path中yolov5路径丢失。
解决:打包命令加--add-data并修正路径:
# 假设 yolov5 目录在项目根目录下 pyinstaller --onefile --windowed \ --add-data "yolov5/models;models" \ --add-data "yolov5/utils;utils" \ --add-data "yolov5/data;data" \ main.py # 并在 main.py 开头添加: import sys import os if getattr(sys, 'frozen', False): # PyInstaller 创建的 bundle base_path = sys._MEIPASS sys.path.append(os.path.join(base_path, 'yolov5')) else: base_path = os.path.dirname(os.path.abspath(__file__))6. 进阶技巧:如何让 GUI 支持模型热切换与置信度动态调节?
做到上一章,你已经有了一个能跑通的 GUI。但真实场景中,用户需要快速对比不同模型(yolov5s vs yolov5m)、动态调整检测阈值(避免漏检/误检)、甚至导出带时间戳的录像片段。这些不是锦上添花,而是交付时客户必然提出的需求。我分享两个经过产线验证的技巧:
6.1 模型热切换:不用重启程序,秒级切换权重文件
核心思路是:销毁旧 detector 实例,新建新实例,复用同一 GPU 显存。关键在release()方法释放显存,否则连续切换 3 次以上必 OOM。
# 在 MainWindow 中添加模型选择控件 def init_model_selector(self): model_layout = QHBoxLayout() model_layout.addWidget(QLabel("模型:")) self.model_combo = QComboBox() self.model_combo.addItems([ "yolov5s.pt", "yolov5m.pt", "runs/train/my_exp/weights/best.pt" ]) self.model_combo.currentTextChanged.connect(self.on_model_changed) model_layout.addWidget(self.model_combo) # 插入到主布局顶部 self.main_layout.insertLayout(0, model_layout) def on_model_changed(self, model_name): if hasattr(self, 'detector') and self.detector: self.detector.release() # ⚠️ 必须先释放! del self.detector try: model_path = model_name if not os.path.isabs(model_path): model_path = os.path.join("yolov5", model_path) self.detector = YOLOv5Detector(weights=model_path) self.status_label.setText(f"🔄 模型已切换为:{model_name}") except Exception as e: self.status_label.setText(f"❌ 模型加载失败:{str(e)}")血泪经验:
self.detector.release()必须在del self.detector之前调用,否则torch.cuda.empty_cache()无效;且QComboBox.currentTextChanged会在初始化时触发一次,需在__init__中先self.model_combo.blockSignals(True),加载完再blockSignals(False)。
6.2 置信度滑块:实时调节 conf_thres,所见即所得
把conf_thres从硬编码改成可调参数,用户拖动滑块时,检测框数量实时变化。难点在于:滑块值变,但不能每动一下就重跑一次 infer(太卡),而是缓存结果后局部更新。
def init_conf_slider(self): conf_layout = QHBoxLayout() conf_layout.addWidget(QLabel("置信度阈值:")) self.conf_slider = QSlider(Qt.Horizontal) self.conf_slider.setRange(1, 100) self.conf_slider.setValue(25) # 对应 0.25 self.conf_slider.valueChanged.connect(self.on_conf_changed) conf_layout.addWidget(self.conf_slider) self.conf_label = QLabel("0.25") conf_layout.addWidget(self.conf_label) self.main_layout.insertLayout(1, conf_layout) def on_conf_changed(self, value): self.conf_threshold = value / 100.0 self.conf_label.setText(f"{self.conf_threshold:.2f}") # 若已有检测结果,立即用新阈值重绘(不重推理!) if hasattr(self, 'last_raw_pred') and self.last_raw_pred is not None: self.redraw_with_new_conf() def redraw_with_new_conf(self): # last_raw_pred 是 infer() 前的 pred[0] tensor pred_filtered = non_max_suppression( self.last_raw_pred, conf_thres=self.conf_threshold, iou_thres=0.45 )[0] # 重绘逻辑同 infer() 中的 annotator 部分... # (此处省略,核心是复用原始 pred,只做 NMS 重过滤)6.3 结果导出增强:带时间戳的检测片段录像(适用于摄像头)
当摄像头检测到目标时,自动截取前后 3 秒录像保存为 MP4。这不是简单cv2.VideoWriter,而是用ffmpeg命令行调用,保证音画同步与压缩率:
def save_detection_clip(self, frame_list, output_path): """frame_list 是检测到目标时的连续帧列表(BGR numpy array)""" import tempfile import subprocess # 写临时图片序列 temp_dir = tempfile.mkdtemp() for i, frame in enumerate(frame_list): cv2.imwrite(f"{temp_dir}/frame_{i:04d}.jpg", frame) # 调用 ffmpeg 合成 MP4(需提前安装 ffmpeg) cmd = [ "ffmpeg", "-framerate", "30", "-i", f"{temp_dir}/frame_%04d.jpg", "-c:v", "libx264", "-pix_fmt", "yuv420p", "-y", output_path ] try: subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) shutil.rmtree(temp_dir) except subprocess.CalledProcessError: self.status_label.setText("⚠️ 录像生成失败(请确认 ffmpeg 已安装)")我习惯在on_detection_finished中判断len(boxes) > 0时触发此函数,并用QDateTime.currentDateTime().toString("yyyy-MM-dd-hh-mm-ss")生成文件名。这招在安防巡检项目中让客户当场拍板签单——他们要的不是“能检测”,而是“检测到就留证”。
希望帮到你。
本文还有配套的精品资源,点击获取