恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
PyQt5+YOLOv5本地目标检测GUI实战
首页
资讯中心
/
PyQt5+YOLOv5本地目标检测GUI实战
PyQt5+YOLOv5本地目标检测GUI实战
发布时间:2026/10/11 0:41:42
简介这是一份面向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%mAP0.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 或旧版 sipQApplication初始化会静默失败YOLOv5 依赖 torch 1.13CUDA 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 python3.9 conda activate yolo-pyqt # 优先用 conda-forge 安装 Qt 相关避免 pip 混装冲突 conda install -c conda-forge pyqt5.15.9 qt5.15.2 # 再用 pip 安装 YOLOv5 官方仓库注意不要用 pip install yolov5那是旧版 PyPI 包 pip install torch1.13.1cu117 torchvision0.14.1cu117 --extra-index-url https://download.pytorch.org/whl/cu117 pip install numpy opencv-python4.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.pyimport 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 是 BGRQt 是 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 * widthRGB 图像每行字节数 宽 × 3R/G/B 各 1 字节若用cv2.IMREAD_GRAYSCALE则应为widthQImage.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每次调用都重载模型慢2torch.no_grad()上下文管理器在 Qt 事件循环中可能失效3plot_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, weightsyolov5s.pt, devicecuda if torch.cuda.is_available() else cpu): self.device device self.model DetectMultiBackend(weights, deviceself.device, dnnFalse, datadata/coco128.yaml, fp16False) 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, augmentFalse, visualizeFalse) pred non_max_suppression(pred[0], conf_thres0.25, iou_thres0.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_width2, examplestr(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, colorcolors(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-BGRYOLOv5 要 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导致中文列名/标签显示为æ¡。解决导出时显式指定encodingutf_8_sigUTF-8 with BOMdef 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, indexFalse, encodingutf_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(weightsmodel_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_thresself.conf_threshold, iou_thres0.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, checkTrue, stdoutsubprocess.DEVNULL, stderrsubprocess.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)生成文件名。这招在安防巡检项目中让客户当场拍板签单——他们要的不是“能检测”而是“检测到就留证”。希望帮到你。本文还有配套的精品资源点击获取