基于YOLO的目标检测图形界面应用(适配于YOLOv5、YOLOv6、YOLOv8、YOLOv9、YOLOv10、YOLOv11、YOLOv12)
基于YOLO的目标检测图形界面应用:功能详解与使用指南
在计算机视觉领域,目标检测是一项基础且重要的任务。随着深度学习的发展,YOLO(You Only Look Once)系列模型因其高效性和准确性成为目标检测的主流选择之一。本文将介绍一个基于YOLO的图形界面应用(适配于YOLOv5、YOLOv6、YOLOv8、YOLOv9、YOLOv10、YOLOv11、YOLOv12),它能够方便地进行目标检测,并提供了丰富的功能和灵活的配置选项。
功能概览
这个应用程序是一个完整的目标检测系统,主要具备以下功能:
- 多源检测:支持从USB摄像头、视频文件、单张图片或图片文件夹进行目标检测
- 实时可视化:实时显示检测结果,包括边界框和类别标签
- 参数可调:可自定义置信度阈值和IOU阈值以适应不同场景
- 目标追踪:对视频中的目标进行追踪,记录其运动轨迹
- 结果保存:可保存检测后的图片/视频,以及导出详细的检测结果到Excel
- 界面定制:支持自定义系统名称、LOGO、背景图片等界面元素
技术栈
该应用基于以下技术和库构建:
- Python:主要编程语言
- PyQt5:用于创建图形用户界面
- Ultralytics YOLO:基于YOLOv8的目标检测模型
- OpenCV:用于图像处理和视频捕获
- NumPy:用于数值计算
- Pandas:用于数据处理和Excel导出
- QtAwesome:(可选)用于提供丰富的图标
代码结构解析
整个应用程序由三个主要类组成,下面我们逐一分析它们的功能和实现细节。
SettingsDialog类:系统设置对话框
SettingsDialog
类负责实现应用的设置界面,用户可以在这里自定义界面外观和个人信息。
class SettingsDialog(QDialog):"""设置对话框"""def __init__(self, parent=None):super().__init__(parent)self.setWindowTitle("系统设置")self.setModal(True)self.resize(500, 550)layout = QVBoxLayout(self)# 省略部分代码...
这个对话框包含四个主要设置组:
- 系统设置:可以自定义系统名称
- LOGO设置:选择和调整LOGO图片的大小
- 个人信息:填写姓名、学号和学校等信息
- 背景设置:选择自定义背景图片或使用默认背景
每个设置组都使用QGroupBox
进行分组,并通过QGridLayout
或QVBoxLayout
进行布局管理。设置对话框通过QSettings
类保存和加载设置,确保应用重启后设置仍然有效。
DetectionThread类:检测线程
DetectionThread
类是应用的核心,负责执行实际的目标检测任务。它继承自QThread
,以确保检测过程不会阻塞用户界面。
class DetectionThread(QThread):"""检测线程"""progress = pyqtSignal(int)status = pyqtSignal(str)result = pyqtSignal(dict)image = pyqtSignal(np.ndarray)finished = pyqtSignal()error = pyqtSignal(str)tracking_info = pyqtSignal(dict) # 新增:追踪信息信号
这个类定义了多个信号,用于与主线程通信:
progress
:检测进度status
:状态信息result
:检测结果数据image
:检测后的图像finished
:检测完成信号error
:错误信息tracking_info
:目标追踪信息
核心功能方法
DetectionThread
类实现了三种检测模式的方法:
- detect_video(self):处理视频或摄像头输入
- detect_image(self):处理单张图片
- detect_folder(self):处理图片文件夹
以detect_video
方法为例,它首先打开视频源,然后逐帧进行检测:
def detect_video(self):"""检测视频"""try:print(f"开始视频检测: {self.source}")# 如果是USB摄像头,source应该是整数if isinstance(self.source, int):cap = cv2.VideoCapture(self.source)total_frames = -1 # 实时视频没有总帧数else:cap = cv2.VideoCapture(self.source)total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))if not cap.isOpened():self.error.emit(f"无法打开视频源: {self.source}")return# 省略部分代码...
对于视频检测,还支持目标追踪功能。当启用追踪时,会记录每个目标的历史位置,并绘制运动轨迹:
# 追踪历史(如果启用追踪)
track_history = defaultdict(list) if self.enable_tracking else None# 在帧处理循环中
if self.enable_tracking and track_history and r.boxes and r.boxes.id is not None:track_ids = r.boxes.id.int().cpu().tolist()boxes = r.boxes.xyxy.cpu()for box, track_id in zip(boxes, track_ids):x1, y1, x2, y2 = boxcenter_x = int((x1 + x2) / 2)center_y = int((y1 + y2) / 2)# 更新追踪历史track_history[track_id].append((center_x, center_y))if len(track_history[track_id]) > 30: # 保留最近30个点track_history[track_id].pop(0)# 绘制轨迹points = np.array(track_history[track_id], dtype=np.int32)if len(points) > 1:cv2.polylines(annotated_frame, [points], False, (0, 255, 0), 2)
YOLODetectorGUI类:主界面
YOLODetectorGUI
类是应用的主界面,它整合了所有功能模块,提供了用户交互的主要入口。
class YOLODetectorGUI(QMainWindow):"""主界面"""def __init__(self):super().__init__()# 检查依赖if not YOLO_AVAILABLE:QMessageBox.critical(self, "错误", "未安装必要的依赖库!\n请运行: pip install ultralytics torch torchvision")sys.exit(1)self.detection_thread = DetectionThread()self.all_results = [] # 存储所有检测结果self.current_image = Noneself.model_loaded = Falseself.source_selected = False# 设置self.settings = QSettings('YOLODetector', 'Settings')self.load_settings()self.init_ui()self.connect_signals()self.apply_settings()
主界面采用了模块化设计,主要包括以下部分:
- 标题栏:显示系统名称、LOGO和设置按钮
- 控制面板:包含模型选择、检测源选择、参数设置和控制按钮
- 图像显示面板:显示检测画面和当前源信息
- 结果显示面板:包含三个标签页(实时结果、统计信息、详细结果)
- 状态栏:显示进度、状态和设备信息
界面布局与样式
应用使用了丰富的布局管理器和样式表,使界面既美观又实用:
def apply_base_style(self):"""应用基础样式"""# 省略背景设置代码...# 完整的样式表style_sheet = main_bg_style + """QGroupBox {font-weight: bold;border: 2px solid #cccccc;border-radius: 5px;margin-top: 10px;padding-top: 10px;background-color: rgba(255, 255, 255, 0.9);}QGroupBox::title {subcontrol-origin: margin;left: 10px;padding: 0 5px 0 5px;}QPushButton {background-color: #4CAF50;color: white;border: none;padding: 8px;border-radius: 4px;font-weight: bold;}/* 省略更多样式代码... */"""self.setStyleSheet(style_sheet)
样式表使用了半透明背景、圆角边框和悬停效果,使界面看起来更加现代和美观。同时,按钮使用不同颜色区分功能(绿色表示开始,红色表示停止,橙色表示清除)。
信号与槽机制
应用充分利用了PyQt的信号与槽机制,实现了线程间的安全通信:
def connect_signals(self):"""连接信号"""self.detection_thread.progress.connect(self.update_progress)self.detection_thread.status.connect(self.update_status)self.detection_thread.result.connect(self.handle_result)self.detection_thread.image.connect(self.update_image)self.detection_thread.finished.connect(self.detection_finished)self.detection_thread.error.connect(self.handle_error)self.detection_thread.tracking_info.connect(self.update_tracking_info)
这种设计确保了耗时的检测任务在后台线程中执行,而不会阻塞用户界面,提供了流畅的用户体验。
界面与功能详解
控制面板
控制面板是应用的核心操作区域,包含以下功能模块:
模型选择
- 模型文件显示框:显示当前加载的模型文件名
- 选择模型按钮:打开文件对话框选择YOLO模型文件(.pt或.onnx格式)
检测源选择
- 检测类型下拉框:选择检测源类型(USB摄像头、视频文件、图片文件、图片文件夹)
- 选择源按钮:根据选择的类型打开相应的文件对话框
设备与参数设置
- 运行设备下拉框:选择运行设备(自适应、GPU、CPU)
- 置信度阈值滑块:设置检测置信度阈值(0.0-1.0)
- IOU阈值滑块:设置非极大值抑制的IOU阈值(0.0-1.0)
- 启用目标追踪复选框:对视频中的目标进行追踪(仅视频有效)
结果保存设置
- 保存检测结果复选框:启用结果保存功能
- 保存路径显示框:显示选择的保存路径
- 选择保存路径按钮:打开文件夹对话框选择保存路径
控制按钮
- 开始检测按钮:开始目标检测
- 停止检测按钮:停止正在进行的检测
- 导出Excel按钮:将检测结果导出到Excel文件
- 清除结果按钮:清除所有检测结果
图像显示面板
图像显示面板是查看检测结果的主要区域:
- 检测画面显示区:实时显示检测结果图像
- 源信息标签:显示当前检测源的信息
- 追踪信息标签:显示当前追踪的目标数量(启用追踪时)
结果显示面板
结果显示面板包含三个标签页,提供不同维度的检测结果展示:
实时结果标签页
以文本形式显示当前帧或图片的检测结果,包括:
- 检测源信息(图片文件名或帧号)
- 检测到的目标类别和数量
- 目标的追踪ID(如果启用追踪)
统计分析标签页
以表格形式展示检测结果的统计信息:
- 每个类别的检测数量
- 每个类别的平均置信度
详细结果标签页
以表格形式展示所有检测结果的详细信息:
- 源文件信息
- 目标类别和置信度
- 追踪ID(如果有)
- 边界框坐标和尺寸
操作指南
安装与启动
-
安装必要的依赖:
pip install ultralytics torch torchvision PyQt5 opencv-python numpy pandas
-
下载或复制代码到本地
-
运行程序:
python yolo_detector.py
基本使用流程
1. 加载模型
- 点击"选择模型"按钮
- 在弹出的文件对话框中选择YOLO模型文件(.pt或.onnx格式)
- 等待模型加载,状态标签会显示"模型加载成功"
2. 选择检测源
-
在"检测类型"下拉框中选择检测源类型
- USB摄像头:选择后直接使用默认摄像头
- 视频文件:选择本地视频文件
- 图片文件:选择单张图片进行检测
- 图片文件夹:选择包含多张图片的文件夹
-
点击"选择源"按钮,根据选择的类型选择相应的源文件或设备
3. 设置检测参数
-
根据需要调整置信度阈值(默认0.25)
- 提高阈值会减少误检,但可能漏检
- 降低阈值会增加检测到的目标,但可能增加误检
-
根据需要调整IOU阈值(默认0.45)
- 提高IOU会使边界框更加紧凑
- 降低IOU会保留更多重叠的边界框
-
如果是视频检测,可以选择"启用目标追踪"
-
如果需要保存结果,勾选"保存检测结果"并选择保存路径
4. 开始检测
- 点击"开始检测"按钮
- 等待检测开始,图像显示区会实时显示检测结果
- 状态栏会显示当前检测状态和进度
5. 查看和导出结果
- 在"实时检测"标签页查看当前帧的检测结果
- 在"统计分析"标签页查看各类别的检测统计
- 在"详细结果"标签页查看所有检测结果的详细信息
- 检测完成后,点击"导出Excel"按钮将结果导出到Excel文件
6. 停止检测和清除结果
- 点击"停止检测"按钮可以随时停止检测
- 点击"清除结果"按钮可以清除所有检测结果
自定义界面
- 点击"设置"按钮打开设置对话框
- 在"系统设置"中修改系统名称
- 在"LOGO设置"中选择和调整LOGO图片
- 在"个人信息"中填写姓名、学号和学校
- 在"背景设置"中选择自定义背景图片
- 点击"确定"保存设置,界面会立即更新
自定义与扩展
这个应用程序设计得非常灵活,您可以根据需要进行自定义和扩展:
更换模型
您可以使用自己训练的YOLO模型,只需点击"选择模型"按钮并选择您的模型文件即可。
调整检测参数
通过调整置信度阈值和IOU阈值,您可以让检测结果更符合您的需求:
- 高置信度+高IOU:适合需要高精度的场景
- 低置信度+低IOU:适合需要高召回率的场景
总结与展望
这个基于YOLO的目标检测图形界面应用提供了一个功能完整、使用便捷的目标检测解决方案。它不仅能够满足基本的目标检测需求,还提供了丰富的自定义选项和结果分析功能。
未来,我们可以进一步优化应用的性能,增加更多的功能模块,或者将其部署为Web应用,使其能够在更多平台上使用。无论您是学生、研究人员还是工程师,这个应用都可以作为一个良好的起点,帮助您快速实现和部署目标检测功能。
通过这个应用,您可以直观地理解YOLO模型的工作原理,调整参数以适应不同的场景,并且方便地保存和分析检测结果,为您的计算机视觉项目提供有力支持。
总代码
import sys
import os
import cv2
import numpy as np
from pathlib import Path
from datetime import datetime
import pandas as pd
from collections import defaultdict
import json
import traceback
import threadingfrom PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFileDialog, QTableWidget, QTableWidgetItem, QTextEdit, QProgressBar, QComboBox, QGroupBox, QSplitter,QTabWidget, QMessageBox, QHeaderView, QCheckBox,QSpinBox, QDoubleSpinBox, QGridLayout, QLineEdit,QDialog, QDialogButtonBox, QSlider)
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer, QSize, pyqtSlot, QSettings
from PyQt5.QtGui import QPixmap, QImage, QPainter, QPen, QFont, QPalette, QBrushtry:from ultralytics import YOLOimport torchYOLO_AVAILABLE = True
except ImportError as e:YOLO_AVAILABLE = Falseprint(f"警告: {e}")print("请安装: pip install ultralytics torch torchvision")class SettingsDialog(QDialog):"""设置对话框"""def __init__(self, parent=None):super().__init__(parent)self.setWindowTitle("系统设置")self.setModal(True)self.resize(500, 550)layout = QVBoxLayout(self)# 系统设置组system_group = QGroupBox("系统设置")system_layout = QGridLayout()system_group.setLayout(system_layout)system_layout.addWidget(QLabel("系统名称:"), 0, 0)self.system_name_edit = QLineEdit()self.system_name_edit.setPlaceholderText("例如:YOLOv8 智能检测系统")system_layout.addWidget(self.system_name_edit, 0, 1, 1, 2)layout.addWidget(system_group)# LOGO设置组logo_group = QGroupBox("LOGO设置")logo_layout = QGridLayout()logo_group.setLayout(logo_layout)logo_layout.addWidget(QLabel("LOGO路径:"), 0, 0)self.logo_path_edit = QLineEdit()logo_layout.addWidget(self.logo_path_edit, 0, 1)self.btn_select_logo = QPushButton("选择")self.btn_select_logo.clicked.connect(self.select_logo)logo_layout.addWidget(self.btn_select_logo, 0, 2)logo_layout.addWidget(QLabel("LOGO宽度:"), 1, 0)self.logo_width_spin = QSpinBox()self.logo_width_spin.setRange(20, 200)self.logo_width_spin.setValue(100)logo_layout.addWidget(self.logo_width_spin, 1, 1)logo_layout.addWidget(QLabel("LOGO高度:"), 2, 0)self.logo_height_spin = QSpinBox()self.logo_height_spin.setRange(20, 200)self.logo_height_spin.setValue(50)logo_layout.addWidget(self.logo_height_spin, 2, 1)layout.addWidget(logo_group)# 个人信息设置组info_group = QGroupBox("个人信息")info_layout = QGridLayout()info_group.setLayout(info_layout)info_layout.addWidget(QLabel("姓名:"), 0, 0)self.name_edit = QLineEdit()info_layout.addWidget(self.name_edit, 0, 1)info_layout.addWidget(QLabel("学号:"), 1, 0)self.id_edit = QLineEdit()info_layout.addWidget(self.id_edit, 1, 1)info_layout.addWidget(QLabel("学校:"), 2, 0)self.school_edit = QLineEdit()info_layout.addWidget(self.school_edit, 2, 1)layout.addWidget(info_group)# 背景设置组bg_group = QGroupBox("背景设置")bg_layout = QGridLayout()bg_group.setLayout(bg_layout)# 背景图片选项bg_layout.addWidget(QLabel("背景图片:"), 0, 0)self.bg_image_edit = QLineEdit()self.bg_image_edit.setPlaceholderText("可选,留空则使用默认背景")bg_layout.addWidget(self.bg_image_edit, 0, 1)self.btn_select_bg_image = QPushButton("选择图片")self.btn_select_bg_image.clicked.connect(self.select_bg_image)bg_layout.addWidget(self.btn_select_bg_image, 0, 2)# 添加清除背景图片按钮self.btn_clear_bg_image = QPushButton("清除")self.btn_clear_bg_image.clicked.connect(lambda: self.bg_image_edit.clear())bg_layout.addWidget(self.btn_clear_bg_image, 0, 3)layout.addWidget(bg_group)# 按钮buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)buttons.accepted.connect(self.accept)buttons.rejected.connect(self.reject)layout.addWidget(buttons)def select_logo(self):"""选择LOGO文件"""file_path, _ = QFileDialog.getOpenFileName(self, "选择LOGO图片", "", "图片文件 (*.png *.jpg *.jpeg *.bmp)")if file_path:self.logo_path_edit.setText(file_path)def select_bg_image(self):"""选择背景图片"""file_path, _ = QFileDialog.getOpenFileName(self, "选择背景图片", "", "图片文件 (*.png *.jpg *.jpeg *.bmp)")if file_path:self.bg_image_edit.setText(file_path)class DetectionThread(QThread):"""检测线程"""progress = pyqtSignal(int)status = pyqtSignal(str)result = pyqtSignal(dict)image = pyqtSignal(np.ndarray)finished = pyqtSignal()error = pyqtSignal(str)tracking_info = pyqtSignal(dict) # 新增:追踪信息信号def __init__(self):super().__init__()self.model = Noneself.source = Noneself.source_type = Noneself.is_running = Falseself.conf_threshold = 0.25self.iou_threshold = 0.45self.device = 'auto'self.save_results = Falseself.save_path = ""self.enable_tracking = Falseself._stop_lock = threading.Lock()def set_device(self, device):"""设置设备"""self.device = deviceif self.model:self.apply_device_setting()def apply_device_setting(self):"""应用设备设置"""if self.device == 'auto':target_device = 'cuda' if torch.cuda.is_available() else 'cpu'elif self.device == 'cuda':if not torch.cuda.is_available():self.error.emit("CUDA不可用,将使用CPU")target_device = 'cpu'else:target_device = 'cuda'else: # cputarget_device = 'cpu'self.model.to(target_device)self.status.emit(f"模型已切换到: {target_device.upper()}")def set_model(self, model_path):"""加载模型"""try:print(f"正在加载模型: {model_path}")self.model = YOLO(model_path)self.apply_device_setting()# 获取模型信息if hasattr(self.model.model, 'names'):print(f"模型类别: {self.model.model.names}")self.status.emit(f"模型加载成功: {os.path.basename(model_path)}")return Trueexcept Exception as e:error_msg = f"模型加载失败: {str(e)}"print(error_msg)print(traceback.format_exc())self.error.emit(error_msg)return Falsedef set_source(self, source, source_type):"""设置检测源"""self.source = sourceself.source_type = source_typeprint(f"设置检测源: {source}, 类型: {source_type}")def set_params(self, conf, iou, save_results, save_path, enable_tracking):"""设置检测参数"""self.conf_threshold = confself.iou_threshold = iouself.save_results = save_resultsself.save_path = save_pathself.enable_tracking = enable_trackingprint(f"设置参数 - 置信度: {conf}, IOU: {iou}, 保存结果: {save_results}, 启用追踪: {enable_tracking}")def stop(self):"""停止检测"""with self._stop_lock:self.is_running = Falseprint("停止检测信号已发送")def start(self):"""开始检测"""with self._stop_lock:self.is_running = Truesuper().start()def run(self):"""运行检测"""try:if self.model is None:self.error.emit("请先加载模型")returnprint(f"开始检测,源类型: {self.source_type}")if self.source_type == 'video':self.detect_video()elif self.source_type == 'image':self.detect_image()elif self.source_type == 'folder':self.detect_folder()except Exception as e:error_msg = f"检测过程出错: {str(e)}"print(error_msg)print(traceback.format_exc())self.error.emit(error_msg)def detect_video(self):"""检测视频"""try:print(f"开始视频检测: {self.source}")# 如果是USB摄像头,source应该是整数if isinstance(self.source, int):cap = cv2.VideoCapture(self.source)total_frames = -1 # 实时视频没有总帧数else:cap = cv2.VideoCapture(self.source)total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))if not cap.isOpened():self.error.emit(f"无法打开视频源: {self.source}")return# 准备视频写入器(如果需要保存)video_writer = Noneif self.save_results and self.save_path:fps = int(cap.get(cv2.CAP_PROP_FPS))width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))save_file = os.path.join(self.save_path, f"detection_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4")fourcc = cv2.VideoWriter_fourcc(*'mp4v')video_writer = cv2.VideoWriter(save_file, fourcc, fps, (width, height))self.status.emit(f"保存视频到: {save_file}")self.status.emit("正在检测视频...")frame_count = 0# 追踪历史(如果启用追踪)track_history = defaultdict(list) if self.enable_tracking else Nonewhile True:with self._stop_lock:if not self.is_running:breakret, frame = cap.read()if not ret:if isinstance(self.source, int): # USB摄像头print("无法读取摄像头帧")continueelse: # 视频文件结束break# YOLO检测try:if self.enable_tracking and self.source_type == 'video':# 使用追踪模式results = self.model.track(frame, conf=self.conf_threshold, iou=self.iou_threshold, persist=True)else:# 普通检测模式results = self.model(frame, conf=self.conf_threshold, iou=self.iou_threshold)# 处理结果for r in results:# 绘制检测结果annotated_frame = r.plot()# 如果启用追踪,绘制追踪轨迹if self.enable_tracking and track_history and r.boxes and r.boxes.id is not None:track_ids = r.boxes.id.int().cpu().tolist()boxes = r.boxes.xyxy.cpu()for box, track_id in zip(boxes, track_ids):x1, y1, x2, y2 = boxcenter_x = int((x1 + x2) / 2)center_y = int((y1 + y2) / 2)# 更新追踪历史track_history[track_id].append((center_x, center_y))if len(track_history[track_id]) > 30: # 保留最近30个点track_history[track_id].pop(0)# 绘制轨迹points = np.array(track_history[track_id], dtype=np.int32)if len(points) > 1:cv2.polylines(annotated_frame, [points], False, (0, 255, 0), 2)# 发送追踪信息tracking_info = {'frame': frame_count,'tracks': len(track_history),'active_ids': list(track_history.keys())}self.tracking_info.emit(tracking_info)self.image.emit(annotated_frame)# 保存帧(如果需要)if video_writer:video_writer.write(annotated_frame)# 收集检测结果detection_result = {'frame': frame_count,'detections': []}if r.boxes is not None and len(r.boxes) > 0:for i, box in enumerate(r.boxes):cls = int(box.cls)conf = float(box.conf)xyxy = box.xyxy[0].tolist()det_info = {'class': self.model.names[cls],'confidence': conf,'bbox': xyxy}# 添加追踪ID(如果有)if self.enable_tracking and r.boxes.id is not None:det_info['track_id'] = int(r.boxes.id[i])detection_result['detections'].append(det_info)self.result.emit(detection_result)except Exception as e:print(f"检测帧时出错: {e}")continueframe_count += 1# 更新进度if total_frames > 0:progress = int((frame_count / total_frames) * 100)self.progress.emit(progress)else:# 对于实时视频,使用模拟进度self.progress.emit(frame_count % 100)# 控制帧率,避免处理过快cv2.waitKey(1)cap.release()if video_writer:video_writer.release()print("视频检测完成")except Exception as e:error_msg = f"视频检测出错: {str(e)}"print(error_msg)print(traceback.format_exc())self.error.emit(error_msg)finally:self.finished.emit()def detect_image(self):"""检测单张图片"""try:print(f"开始图片检测: {self.source}")self.status.emit(f"正在检测图片: {os.path.basename(self.source)}")# 读取图片image = cv2.imread(self.source)if image is None:self.error.emit(f"无法读取图片: {self.source}")return# YOLO检测results = self.model(image, conf=self.conf_threshold, iou=self.iou_threshold)# 处理结果for r in results:# 绘制检测结果annotated_image = r.plot()self.image.emit(annotated_image)# 保存结果图片(如果需要)if self.save_results and self.save_path:save_file = os.path.join(self.save_path, f"detection_{os.path.basename(self.source)}")cv2.imwrite(save_file, annotated_image)self.status.emit(f"保存图片到: {save_file}")# 收集检测结果detection_result = {'image_path': self.source,'detections': []}if r.boxes is not None and len(r.boxes) > 0:for box in r.boxes:cls = int(box.cls)conf = float(box.conf)xyxy = box.xyxy[0].tolist()detection_result['detections'].append({'class': self.model.names[cls],'confidence': conf,'bbox': xyxy})else:print("未检测到任何目标")self.result.emit(detection_result)self.progress.emit(100)self.status.emit("图片检测完成")print("图片检测完成")except Exception as e:error_msg = f"图片检测出错: {str(e)}"print(error_msg)print(traceback.format_exc())self.error.emit(error_msg)finally:self.finished.emit()def detect_folder(self):"""检测文件夹中的所有图片"""try:print(f"开始文件夹检测: {self.source}")# 获取所有图片文件image_extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.tiff']image_files = []for ext in image_extensions:image_files.extend(Path(self.source).glob(f'*{ext}'))image_files.extend(Path(self.source).glob(f'*{ext.upper()}'))total_images = len(image_files)print(f"找到 {total_images} 张图片")self.status.emit(f"找到 {total_images} 张图片")if total_images == 0:self.error.emit("文件夹中没有找到图片")return# 创建保存子文件夹(如果需要)if self.save_results and self.save_path:save_folder = os.path.join(self.save_path, f"detection_{datetime.now().strftime('%Y%m%d_%H%M%S')}")os.makedirs(save_folder, exist_ok=True)self.status.emit(f"保存结果到: {save_folder}")else:save_folder = Nonefor idx, image_path in enumerate(image_files):with self._stop_lock:if not self.is_running:break# 读取图片image = cv2.imread(str(image_path))if image is None:print(f"无法读取图片: {image_path}")continue# YOLO检测try:results = self.model(image, conf=self.conf_threshold, iou=self.iou_threshold)# 处理结果for r in results:# 绘制检测结果annotated_image = r.plot()self.image.emit(annotated_image)# 保存结果图片(如果需要)if save_folder:save_file = os.path.join(save_folder, f"detection_{image_path.name}")cv2.imwrite(save_file, annotated_image)# 收集检测结果detection_result = {'image_path': str(image_path),'detections': []}if r.boxes is not None and len(r.boxes) > 0:for box in r.boxes:cls = int(box.cls)conf = float(box.conf)xyxy = box.xyxy[0].tolist()detection_result['detections'].append({'class': self.model.names[cls],'confidence': conf,'bbox': xyxy})self.result.emit(detection_result)except Exception as e:print(f"检测图片 {image_path} 时出错: {e}")continue# 更新进度progress = int(((idx + 1) / total_images) * 100)self.progress.emit(progress)self.status.emit(f"正在处理: {image_path.name} ({idx + 1}/{total_images})")print("文件夹检测完成")self.status.emit(f"文件夹检测完成,共处理 {total_images} 张图片")except Exception as e:error_msg = f"文件夹检测出错: {str(e)}"print(error_msg)print(traceback.format_exc())self.error.emit(error_msg)finally:self.finished.emit()class YOLODetectorGUI(QMainWindow):"""主界面"""def __init__(self):super().__init__()# 检查依赖if not YOLO_AVAILABLE:QMessageBox.critical(self, "错误", "未安装必要的依赖库!\n请运行: pip install ultralytics torch torchvision")sys.exit(1)self.detection_thread = DetectionThread()self.all_results = [] # 存储所有检测结果self.current_image = Noneself.model_loaded = Falseself.source_selected = False# 设置self.settings = QSettings('YOLODetector', 'Settings')self.load_settings()self.init_ui()self.connect_signals()self.apply_settings()def load_settings(self):"""加载设置"""self.system_name = self.settings.value('system_name', 'YOLOv8 智能检测系统')self.logo_path = self.settings.value('logo_path', '')self.logo_width = self.settings.value('logo_width', 100, type=int)self.logo_height = self.settings.value('logo_height', 50, type=int)self.author_name = self.settings.value('author_name', '作者姓名')self.author_id = self.settings.value('author_id', '学号')self.school_name = self.settings.value('school_name', '学校名称')self.bg_image = self.settings.value('bg_image', '')def save_settings(self):"""保存设置"""self.settings.setValue('system_name', self.system_name)self.settings.setValue('logo_path', self.logo_path)self.settings.setValue('logo_width', self.logo_width)self.settings.setValue('logo_height', self.logo_height)self.settings.setValue('author_name', self.author_name)self.settings.setValue('author_id', self.author_id)self.settings.setValue('school_name', self.school_name)self.settings.setValue('bg_image', self.bg_image)def init_ui(self):"""初始化界面"""self.setWindowTitle(self.system_name)self.setGeometry(100, 100, 1400, 900)# 创建中央部件central_widget = QWidget()central_widget.setObjectName("centralwidget") # 设置对象名称self.setCentralWidget(central_widget)self.main_layout = QVBoxLayout(central_widget)# 顶部标题栏self.create_title_bar()# 顶部控制面板self.create_control_panel()# 中间主要内容区域content_splitter = QSplitter(Qt.Horizontal)self.main_layout.addWidget(content_splitter, 1)# 左侧:图像显示self.create_image_panel(content_splitter)# 右侧:结果显示self.create_result_panel(content_splitter)# 设置分割比例content_splitter.setSizes([800, 600])# 底部状态栏self.create_status_bar()# 应用基础样式self.apply_base_style()def create_title_bar(self):"""创建标题栏"""title_widget = QWidget()title_widget.setObjectName("title_widget") # 设置对象名称title_widget.setMaximumHeight(100)title_layout = QHBoxLayout(title_widget)# LOGOself.logo_label = QLabel()self.logo_label.setScaledContents(True)title_layout.addWidget(self.logo_label)# 标题和信息info_layout = QVBoxLayout()self.title_label = QLabel(self.system_name)self.title_label.setStyleSheet("font-size: 24px; font-weight: bold;")info_layout.addWidget(self.title_label)self.info_label = QLabel()self.info_label.setStyleSheet("font-size: 14px; color: #666;")info_layout.addWidget(self.info_label)title_layout.addLayout(info_layout)title_layout.addStretch()# 设置按钮self.btn_settings = QPushButton("设置")self.btn_settings.clicked.connect(self.open_settings)title_layout.addWidget(self.btn_settings)self.main_layout.addWidget(title_widget)def create_control_panel(self):"""创建控制面板"""control_group = QGroupBox("控制面板")self.main_layout.addWidget(control_group)control_layout = QGridLayout()control_group.setLayout(control_layout)# 模型选择control_layout.addWidget(QLabel("模型文件:"), 0, 0)self.model_path_label = QLabel("未选择模型")self.model_path_label.setStyleSheet("background-color: white; padding: 5px; border: 1px solid #ccc;")control_layout.addWidget(self.model_path_label, 0, 1, 1, 2)self.btn_load_model = QPushButton("选择模型")self.btn_load_model.clicked.connect(self.load_model)control_layout.addWidget(self.btn_load_model, 0, 3)# 输入源选择control_layout.addWidget(QLabel("检测类型:"), 1, 0)self.source_type_combo = QComboBox()self.source_type_combo.addItems(["USB摄像头", "视频文件", "图片文件", "图片文件夹"])control_layout.addWidget(self.source_type_combo, 1, 1)self.btn_select_source = QPushButton("选择源")self.btn_select_source.clicked.connect(self.select_source)control_layout.addWidget(self.btn_select_source, 1, 2)# 设备选择control_layout.addWidget(QLabel("运行设备:"), 1, 3)self.device_combo = QComboBox()self.device_combo.addItems(["自适应", "GPU", "CPU"])self.device_combo.currentTextChanged.connect(self.on_device_changed)control_layout.addWidget(self.device_combo, 1, 4)# 参数设置control_layout.addWidget(QLabel("置信度阈值:"), 2, 0)self.conf_spinbox = QDoubleSpinBox()self.conf_spinbox.setRange(0.0, 1.0)self.conf_spinbox.setSingleStep(0.05)self.conf_spinbox.setValue(0.25)control_layout.addWidget(self.conf_spinbox, 2, 1)control_layout.addWidget(QLabel("IOU阈值:"), 2, 2)self.iou_spinbox = QDoubleSpinBox()self.iou_spinbox.setRange(0.0, 1.0)self.iou_spinbox.setSingleStep(0.05)self.iou_spinbox.setValue(0.45)control_layout.addWidget(self.iou_spinbox, 2, 3)# 追踪选项(仅视频)self.tracking_checkbox = QCheckBox("启用目标追踪")self.tracking_checkbox.setToolTip("仅对视频有效")control_layout.addWidget(self.tracking_checkbox, 2, 4)# 保存选项self.save_checkbox = QCheckBox("保存检测结果")control_layout.addWidget(self.save_checkbox, 3, 0)self.save_path_label = QLabel("未选择")self.save_path_label.setStyleSheet("background-color: white; padding: 5px; border: 1px solid #ccc;")control_layout.addWidget(self.save_path_label, 3, 1, 1, 2)self.btn_select_save_path = QPushButton("选择保存路径")self.btn_select_save_path.clicked.connect(self.select_save_path)control_layout.addWidget(self.btn_select_save_path, 3, 3)# 控制按钮self.btn_start = QPushButton("开始检测")self.btn_start.clicked.connect(self.start_detection)self.btn_start.setEnabled(False)control_layout.addWidget(self.btn_start, 4, 0)self.btn_stop = QPushButton("停止检测")self.btn_stop.clicked.connect(self.stop_detection)self.btn_stop.setEnabled(False)control_layout.addWidget(self.btn_stop, 4, 1)self.btn_export = QPushButton("导出Excel")self.btn_export.clicked.connect(self.export_results)self.btn_export.setEnabled(False)control_layout.addWidget(self.btn_export, 4, 2)self.btn_clear = QPushButton("清除结果")self.btn_clear.clicked.connect(self.clear_results)control_layout.addWidget(self.btn_clear, 4, 3)def create_image_panel(self, parent):"""创建图像显示面板"""image_group = QGroupBox("检测画面")parent.addWidget(image_group)layout = QVBoxLayout()image_group.setLayout(layout)# 图像显示标签self.image_label = QLabel()self.image_label.setObjectName("image_label") # 设置对象名称以便样式表引用self.image_label.setAlignment(Qt.AlignCenter)self.image_label.setStyleSheet("background-color: black; border: 2px solid #ccc;")self.image_label.setMinimumSize(640, 480)self.image_label.setText("等待检测...")layout.addWidget(self.image_label)# 显示当前源信息info_layout = QHBoxLayout()self.source_info_label = QLabel("未选择检测源")self.source_info_label.setAlignment(Qt.AlignCenter)info_layout.addWidget(self.source_info_label)# 追踪信息(如果启用)self.tracking_info_label = QLabel("")self.tracking_info_label.setAlignment(Qt.AlignCenter)self.tracking_info_label.setStyleSheet("color: blue;")info_layout.addWidget(self.tracking_info_label)layout.addLayout(info_layout)def create_result_panel(self, parent):"""创建结果显示面板"""result_group = QGroupBox("检测结果")parent.addWidget(result_group)layout = QVBoxLayout()result_group.setLayout(layout)# 创建标签页self.result_tabs = QTabWidget()layout.addWidget(self.result_tabs)# 实时结果标签页self.create_realtime_tab()# 统计信息标签页self.create_statistics_tab()# 详细结果标签页self.create_detail_tab()def create_realtime_tab(self):"""创建实时结果标签页"""realtime_widget = QWidget()self.result_tabs.addTab(realtime_widget, "实时检测")layout = QVBoxLayout()realtime_widget.setLayout(layout)# 当前帧/图片检测结果self.current_result_text = QTextEdit()self.current_result_text.setReadOnly(True)layout.addWidget(self.current_result_text)def create_statistics_tab(self):"""创建统计信息标签页"""stats_widget = QWidget()self.result_tabs.addTab(stats_widget, "统计分析")layout = QVBoxLayout()stats_widget.setLayout(layout)# 统计表格self.stats_table = QTableWidget()self.stats_table.setColumnCount(3)self.stats_table.setHorizontalHeaderLabels(["类别", "数量", "平均置信度"])self.stats_table.horizontalHeader().setStretchLastSection(True)layout.addWidget(self.stats_table)def create_detail_tab(self):"""创建详细结果标签页"""detail_widget = QWidget()self.result_tabs.addTab(detail_widget, "详细结果")layout = QVBoxLayout()detail_widget.setLayout(layout)# 详细结果表格self.detail_table = QTableWidget()self.detail_table.setColumnCount(8)self.detail_table.setHorizontalHeaderLabels(["源文件", "类别", "置信度", "追踪ID", "X1", "Y1", "X2", "Y2"])header = self.detail_table.horizontalHeader()header.setSectionResizeMode(0, QHeaderView.Stretch)header.setSectionResizeMode(1, QHeaderView.ResizeToContents)header.setSectionResizeMode(2, QHeaderView.ResizeToContents)layout.addWidget(self.detail_table)def create_status_bar(self):"""创建状态栏"""status_widget = QWidget()status_widget.setObjectName("status_widget") # 设置对象名称status_layout = QHBoxLayout(status_widget)status_layout.setContentsMargins(5, 5, 5, 5)# 进度条self.progress_bar = QProgressBar()self.progress_bar.setMinimum(0)self.progress_bar.setMaximum(100)status_layout.addWidget(self.progress_bar, 1)# 状态信息self.status_label = QLabel("就绪")status_layout.addWidget(self.status_label)# 设备信息device = 'CUDA' if torch.cuda.is_available() else 'CPU'self.device_label = QLabel(f"设备: {device}")self.device_label.setStyleSheet("color: blue;")status_layout.addWidget(self.device_label)self.main_layout.addWidget(status_widget)def apply_base_style(self):"""应用基础样式"""# 清空背景图片设置(如果路径无效)if self.bg_image and not os.path.exists(self.bg_image):self.bg_image = ''# 构建背景样式if self.bg_image:# 使用背景图片# 将路径转换为适合CSS的格式bg_image_path = self.bg_image.replace('\\', '/')main_bg_style = f"""QMainWindow {{background-image: url("{bg_image_path}");background-repeat: no-repeat;background-position: center;background-attachment: fixed;}}"""else:# 使用默认背景色main_bg_style = """QMainWindow {background-color: #f0f0f0;}"""# 完整的样式表style_sheet = main_bg_style + """QGroupBox {font-weight: bold;border: 2px solid #cccccc;border-radius: 5px;margin-top: 10px;padding-top: 10px;background-color: rgba(255, 255, 255, 0.9);}QGroupBox::title {subcontrol-origin: margin;left: 10px;padding: 0 5px 0 5px;}QPushButton {background-color: #4CAF50;color: white;border: none;padding: 8px;border-radius: 4px;font-weight: bold;}QPushButton:hover {background-color: #45a049;}QPushButton:pressed {background-color: #3d8b40;}QPushButton:disabled {background-color: #cccccc;color: #666666;}QPushButton#btn_stop {background-color: #f44336;}QPushButton#btn_stop:hover {background-color: #d32f2f;}QPushButton#btn_clear {background-color: #ff9800;}QPushButton#btn_clear:hover {background-color: #f57c00;}QProgressBar {border: 2px solid #cccccc;border-radius: 5px;text-align: center;background-color: rgba(255, 255, 255, 0.7);}QProgressBar::chunk {background-color: #4CAF50;border-radius: 3px;}QTextEdit, QTableWidget {background-color: rgba(255, 255, 255, 0.9);border: 1px solid #cccccc;}QLabel#image_label {background-color: black;border: 2px solid #cccccc;}QComboBox, QSpinBox, QDoubleSpinBox, QLineEdit {background-color: white;border: 1px solid #cccccc;padding: 5px;}QTabWidget::pane {background-color: rgba(255, 255, 255, 0.9);border: 1px solid #cccccc;}QTabBar::tab {background-color: rgba(240, 240, 240, 0.9);padding: 8px 16px;margin-right: 2px;}QTabBar::tab:selected {background-color: rgba(255, 255, 255, 0.95);border-bottom: 2px solid #4CAF50;}QHeaderView::section {background-color: rgba(240, 240, 240, 0.9);padding: 4px;border: 1px solid #cccccc;font-weight: bold;}/* 确保状态栏和其他小部件在有背景图时也能清晰显示 */QWidget#centralwidget > QWidget {background-color: transparent;}/* 标题栏背景 */QWidget#title_widget {background-color: rgba(255, 255, 255, 0.95);border-bottom: 2px solid #cccccc;}/* 状态栏背景 */QWidget#status_widget {background-color: rgba(255, 255, 255, 0.95);border-top: 2px solid #cccccc;}"""self.setStyleSheet(style_sheet)# 强制更新self.update()# 设置特定按钮的对象名称self.btn_stop.setObjectName("btn_stop")self.btn_clear.setObjectName("btn_clear")def connect_signals(self):"""连接信号"""self.detection_thread.progress.connect(self.update_progress)self.detection_thread.status.connect(self.update_status)self.detection_thread.result.connect(self.handle_result)self.detection_thread.image.connect(self.update_image)self.detection_thread.finished.connect(self.detection_finished)self.detection_thread.error.connect(self.handle_error)self.detection_thread.tracking_info.connect(self.update_tracking_info)def apply_settings(self):"""应用设置"""# 更新窗口标题self.setWindowTitle(self.system_name)# 更新标题标签if hasattr(self, 'title_label'):self.title_label.setText(self.system_name)# 更新LOGOif self.logo_path and os.path.exists(self.logo_path):pixmap = QPixmap(self.logo_path)pixmap = pixmap.scaled(self.logo_width, self.logo_height, Qt.KeepAspectRatio, Qt.SmoothTransformation)self.logo_label.setPixmap(pixmap)self.logo_label.setFixedSize(self.logo_width, self.logo_height)else:self.logo_label.setText("LOGO")self.logo_label.setFixedSize(self.logo_width, self.logo_height)self.logo_label.setStyleSheet("border: 1px solid #ccc; background-color: #eee;")# 更新信息self.info_label.setText(f"{self.author_name} | {self.author_id} | {self.school_name}")# 更新背景样式self.apply_base_style()def open_settings(self):"""打开设置对话框"""dialog = SettingsDialog(self)# 设置当前值dialog.system_name_edit.setText(self.system_name)dialog.logo_path_edit.setText(self.logo_path)dialog.logo_width_spin.setValue(self.logo_width)dialog.logo_height_spin.setValue(self.logo_height)dialog.name_edit.setText(self.author_name)dialog.id_edit.setText(self.author_id)dialog.school_edit.setText(self.school_name)dialog.bg_image_edit.setText(self.bg_image)if dialog.exec_():# 保存设置self.system_name = dialog.system_name_edit.text() or 'YOLOv8 智能检测系统'self.logo_path = dialog.logo_path_edit.text()self.logo_width = dialog.logo_width_spin.value()self.logo_height = dialog.logo_height_spin.value()self.author_name = dialog.name_edit.text()self.author_id = dialog.id_edit.text()self.school_name = dialog.school_edit.text()self.bg_image = dialog.bg_image_edit.text()self.save_settings()self.apply_settings()def on_device_changed(self, device_text):"""设备选择改变"""device_map = {"自适应": "auto","GPU": "cuda","CPU": "cpu"}device = device_map[device_text]self.detection_thread.set_device(device)def select_save_path(self):"""选择保存路径"""folder = QFileDialog.getExistingDirectory(self, "选择保存路径")if folder:self.save_path_label.setText(folder)@pyqtSlot(str)def handle_error(self, error_msg):"""处理错误信息"""QMessageBox.critical(self, "错误", error_msg)self.update_status(f"错误: {error_msg}")self.btn_start.setEnabled(self.model_loaded and self.source_selected)self.btn_stop.setEnabled(False)@pyqtSlot(dict)def update_tracking_info(self, info):"""更新追踪信息"""if self.tracking_checkbox.isChecked():self.tracking_info_label.setText(f"追踪目标: {info['tracks']} 个")def load_model(self):"""加载模型"""file_path, _ = QFileDialog.getOpenFileName(self, "选择YOLO模型文件", "", "模型文件 (*.pt *.onnx)")if file_path:self.model_path_label.setText(os.path.basename(file_path))if self.detection_thread.set_model(file_path):self.model_loaded = Trueself.btn_start.setEnabled(self.model_loaded and self.source_selected)self.update_status("模型加载成功")else:self.model_loaded = Falseself.btn_start.setEnabled(False)def select_source(self):"""选择检测源"""source_type = self.source_type_combo.currentText()if source_type == "USB摄像头":self.source = 0self.source_type = 'video'self.source_info_label.setText("USB摄像头")self.update_status("已选择USB摄像头")self.source_selected = Trueelif source_type == "视频文件":file_path, _ = QFileDialog.getOpenFileName(self, "选择视频文件", "", "视频文件 (*.mp4 *.avi *.mov *.mkv)")if file_path:self.source = file_pathself.source_type = 'video'self.source_info_label.setText(os.path.basename(file_path))self.update_status(f"已选择视频: {os.path.basename(file_path)}")self.source_selected = Trueelse:self.source_selected = Falseelif source_type == "图片文件":file_path, _ = QFileDialog.getOpenFileName(self, "选择图片文件", "", "图片文件 (*.jpg *.jpeg *.png *.bmp)")if file_path:self.source = file_pathself.source_type = 'image'self.source_info_label.setText(os.path.basename(file_path))self.update_status(f"已选择图片: {os.path.basename(file_path)}")self.source_selected = True# 图片不支持追踪self.tracking_checkbox.setChecked(False)self.tracking_checkbox.setEnabled(False)else:self.source_selected = Falseelif source_type == "图片文件夹":folder_path = QFileDialog.getExistingDirectory(self, "选择图片文件夹")if folder_path:self.source = folder_pathself.source_type = 'folder'self.source_info_label.setText(os.path.basename(folder_path))self.update_status(f"已选择文件夹: {os.path.basename(folder_path)}")self.source_selected = True# 文件夹不支持追踪self.tracking_checkbox.setChecked(False)self.tracking_checkbox.setEnabled(False)else:self.source_selected = False# 更新追踪选项状态if self.source_type == 'video':self.tracking_checkbox.setEnabled(True)else:self.tracking_checkbox.setEnabled(False)self.tracking_checkbox.setChecked(False)# 更新按钮状态self.btn_start.setEnabled(self.model_loaded and self.source_selected)def start_detection(self):"""开始检测"""if not self.model_loaded:QMessageBox.warning(self, "警告", "请先加载模型")returnif not self.source_selected:QMessageBox.warning(self, "警告", "请先选择检测源")return# 检查保存路径save_path = ""if self.save_checkbox.isChecked():save_path = self.save_path_label.text()if save_path == "未选择":QMessageBox.warning(self, "警告", "请选择保存路径")return# 清空之前的结果self.current_result_text.clear()self.progress_bar.setValue(0)self.tracking_info_label.clear()# 设置参数self.detection_thread.set_params(self.conf_spinbox.value(),self.iou_spinbox.value(),self.save_checkbox.isChecked(),save_path,self.tracking_checkbox.isChecked() and self.source_type == 'video')# 设置源self.detection_thread.set_source(self.source, self.source_type)# 启动线程self.detection_thread.start()# 更新界面self.btn_start.setEnabled(False)self.btn_stop.setEnabled(True)self.btn_export.setEnabled(False)self.update_status("正在检测...")def stop_detection(self):"""停止检测"""self.detection_thread.stop()self.update_status("正在停止...")self.btn_stop.setEnabled(False)def detection_finished(self):"""检测完成"""self.btn_start.setEnabled(self.model_loaded and self.source_selected)self.btn_stop.setEnabled(False)self.btn_export.setEnabled(len(self.all_results) > 0)self.update_status("检测完成")self.update_statistics()self.tracking_info_label.clear()@pyqtSlot(int)def update_progress(self, value):"""更新进度"""self.progress_bar.setValue(value)@pyqtSlot(str)def update_status(self, text):"""更新状态"""self.status_label.setText(text)print(f"状态: {text}")@pyqtSlot(np.ndarray)def update_image(self, image):"""更新图像显示"""try:self.current_image = image# 转换为Qt格式height, width, channel = image.shapebytes_per_line = 3 * width# 转换BGR到RGBrgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)# 创建QImageq_image = QImage(rgb_image.data, width, height, bytes_per_line, QImage.Format_RGB888)# 缩放到标签大小pixmap = QPixmap.fromImage(q_image)scaled_pixmap = pixmap.scaled(self.image_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)self.image_label.setPixmap(scaled_pixmap)except Exception as e:print(f"更新图像显示时出错: {e}")@pyqtSlot(dict)def handle_result(self, result):"""处理检测结果"""try:# 保存结果self.all_results.append(result)# 更新实时显示self.update_realtime_display(result)# 更新详细表格self.update_detail_table(result)except Exception as e:print(f"处理结果时出错: {e}")def update_realtime_display(self, result):"""更新实时显示"""text = ""if 'image_path' in result:text += f"图片: {os.path.basename(result['image_path'])}\n"elif 'frame' in result:text += f"帧: {result['frame']}\n"text += f"检测到 {len(result['detections'])} 个目标:\n\n"# 统计各类别数量class_counts = defaultdict(int)for det in result['detections']:class_counts[det['class']] += 1for cls, count in class_counts.items():text += f"- {cls}: {count} 个\n"# 显示追踪ID(如果有)if any('track_id' in det for det in result['detections']):text += "\n追踪ID:\n"for det in result['detections']:if 'track_id' in det:text += f"- {det['class']} (ID: {det['track_id']})\n"self.current_result_text.setText(text)def update_detail_table(self, result):"""更新详细表格"""source = result.get('image_path', f"Frame_{result.get('frame', 0)}")for det in result['detections']:row_count = self.detail_table.rowCount()self.detail_table.insertRow(row_count)# 源文件self.detail_table.setItem(row_count, 0, QTableWidgetItem(os.path.basename(source)))# 类别self.detail_table.setItem(row_count, 1, QTableWidgetItem(det['class']))# 置信度self.detail_table.setItem(row_count, 2, QTableWidgetItem(f"{det['confidence']:.3f}"))# 追踪IDtrack_id = str(det.get('track_id', '-'))self.detail_table.setItem(row_count, 3, QTableWidgetItem(track_id))# 边界框坐标bbox = det['bbox']for i, coord in enumerate(bbox):self.detail_table.setItem(row_count, 4 + i, QTableWidgetItem(f"{coord:.1f}"))def update_statistics(self):"""更新统计信息"""# 统计所有结果class_stats = defaultdict(lambda: {'count': 0, 'conf_sum': 0})for result in self.all_results:for det in result['detections']:class_name = det['class']class_stats[class_name]['count'] += 1class_stats[class_name]['conf_sum'] += det['confidence']# 更新统计表格self.stats_table.setRowCount(0)for class_name, stats in class_stats.items():row_count = self.stats_table.rowCount()self.stats_table.insertRow(row_count)# 类别self.stats_table.setItem(row_count, 0, QTableWidgetItem(class_name))# 数量self.stats_table.setItem(row_count, 1, QTableWidgetItem(str(stats['count'])))# 平均置信度avg_conf = stats['conf_sum'] / stats['count']self.stats_table.setItem(row_count, 2, QTableWidgetItem(f"{avg_conf:.3f}"))def export_results(self):"""导出结果到Excel"""if not self.all_results:QMessageBox.warning(self, "警告", "没有检测结果可导出")return# 选择保存路径file_path, _ = QFileDialog.getSaveFileName(self, "保存Excel文件", f"detection_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx","Excel文件 (*.xlsx)")if not file_path:returntry:# 准备数据data = []for result in self.all_results:source = result.get('image_path', f"Frame_{result.get('frame', 0)}")if len(result['detections']) == 0:# 即使没有检测到目标,也记录这个结果data.append({'源文件': os.path.basename(source),'完整路径': source if 'image_path' in result else 'Video Frame','类别': '无检测结果','置信度': 0,'追踪ID': '-','X1': 0,'Y1': 0,'X2': 0,'Y2': 0,'宽度': 0,'高度': 0})else:for det in result['detections']:data.append({'源文件': os.path.basename(source),'完整路径': source if 'image_path' in result else 'Video Frame','类别': det['class'],'置信度': det['confidence'],'追踪ID': det.get('track_id', '-'),'X1': det['bbox'][0],'Y1': det['bbox'][1],'X2': det['bbox'][2],'Y2': det['bbox'][3],'宽度': det['bbox'][2] - det['bbox'][0],'高度': det['bbox'][3] - det['bbox'][1]})# 创建DataFrame并保存df = pd.DataFrame(data)# 创建Excel写入器with pd.ExcelWriter(file_path, engine='openpyxl') as writer:# 详细结果表df.to_excel(writer, sheet_name='详细结果', index=False)# 统计表# 过滤掉"无检测结果"的行df_filtered = df[df['类别'] != '无检测结果']if not df_filtered.empty:stats_df = df_filtered.groupby('类别').agg({'类别': 'count','置信度': 'mean'}).rename(columns={'类别': '数量', '置信度': '平均置信度'})stats_df.to_excel(writer, sheet_name='统计分析')else:# 如果没有检测到任何目标,创建一个空的统计表pd.DataFrame({'说明': ['没有检测到任何目标']}).to_excel(writer, sheet_name='统计分析', index=False)QMessageBox.information(self, "成功", f"结果已导出到:\n{file_path}")self.update_status(f"已导出到: {os.path.basename(file_path)}")except Exception as e:error_msg = f"导出失败: {str(e)}"print(error_msg)print(traceback.format_exc())QMessageBox.critical(self, "错误", error_msg)def clear_results(self):"""清除所有结果"""reply = QMessageBox.question(self, "确认", "确定要清除所有检测结果吗?",QMessageBox.Yes | QMessageBox.No)if reply == QMessageBox.Yes:self.all_results.clear()self.current_result_text.clear()self.detail_table.setRowCount(0)self.stats_table.setRowCount(0)self.image_label.clear()self.image_label.setText("等待检测...")self.progress_bar.setValue(0)self.tracking_info_label.clear()self.btn_export.setEnabled(False)self.update_status("已清除所有结果")def main():app = QApplication(sys.argv)# 设置应用程序样式app.setStyle('Fusion')# 创建主窗口window = YOLODetectorGUI()window.show()sys.exit(app.exec_())if __name__ == '__main__':main()