2025.3.2

今天对所有的功能进行完善,使程序完整,但没能完成全部的工作量,暂时代码如下:

from PySide6 import QtWidgets, QtCore, QtGui
import cv2, os, time, random
from threading import Thread, Lock
from ultralytics import YOLO
import torch
import json
import shutil
from docx import Document
from docx.shared import Inches
from datetime import datetime

os.environ['YOLO_VERBOSE'] = 'False'


class MainWindow(QtWidgets.QMainWindow):
    update_signal = QtCore.Signal(QtGui.QImage)

    def __init__(self):
        super().__init__()
        self.setup_ui()
        self.setup_video()

        self.analysis_results = []
        self.current_detections = []
        self.current_frame = None
        self.lock = Lock()
        self.video_paused = False
        self.is_video_file = False
        self.scale_x = 1.0
        self.scale_y = 1.0
        self.results_dir = "results"
        os.makedirs(self.results_dir, exist_ok=True)

    def setup_ui(self):
        self.setWindowTitle("智能分析系统")
        self.resize(1200, 800)
        self.setStyleSheet("background-color: #F0F0F0;")

        central_widget = QtWidgets.QWidget()
        self.setCentralWidget(central_widget)
        main_layout = QtWidgets.QVBoxLayout(central_widget)
        main_layout.setContentsMargins(10, 10, 10, 10)
        main_layout.setSpacing(10)

        # 顶部切换按钮
        btn_style = """
        QPushButton {
            background-color: #87CEFA;
            color: black;
            border: none;
            padding: 8px 16px;
            border-radius: 4px;
            min-width: 120px;
            min-height: 35px;
            font-size: 14px;
        }
        QPushButton:disabled {
            background-color: #D3D3D3;
            color: gray;
        }
        """

        self.analysis_btn = QtWidgets.QPushButton("数据分析")
        self.status_btn = QtWidgets.QPushButton("今日情况")
        self.history_btn = QtWidgets.QPushButton("历史记录")
        self.analysis_btn.setStyleSheet(btn_style)
        self.status_btn.setStyleSheet(btn_style.replace("#87CEFA", "#D3D3D3").replace("black", "gray"))
        self.history_btn.setStyleSheet(btn_style.replace("#87CEFA", "#D3D3D3").replace("black", "gray"))

        top_layout = QtWidgets.QHBoxLayout()
        top_layout.addWidget(self.analysis_btn)
        top_layout.addWidget(self.status_btn)
        top_layout.addWidget(self.history_btn)
        top_layout.setSpacing(10)
        main_layout.addLayout(top_layout)

        # 页面堆栈
        self.stacked_pages = QtWidgets.QStackedWidget()
        main_layout.addWidget(self.stacked_pages)

        self.setup_analysis_page()
        self.setup_status_page()
        self.setup_history_page()

        self.analysis_btn.clicked.connect(self.switch_to_analysis)
        self.status_btn.clicked.connect(self.switch_to_status)
        self.history_btn.clicked.connect(self.switch_to_history)
        self.switch_to_analysis()

    def switch_to_analysis(self):
        self.stacked_pages.setCurrentIndex(0)
        self.analysis_btn.setDisabled(True)
        self.status_btn.setDisabled(False)
        self.history_btn.setDisabled(False)
        self.analysis_btn.setStyleSheet("background-color: #87CEFA; color: black;")
        self.status_btn.setStyleSheet("background-color: #D3D3D3; color: gray;")
        self.history_btn.setStyleSheet("background-color: #D3D3D3; color: gray;")

    def switch_to_status(self):
        self.stacked_pages.setCurrentIndex(1)
        self.status_btn.setDisabled(True)
        self.analysis_btn.setDisabled(False)
        self.history_btn.setDisabled(False)
        self.status_btn.setStyleSheet("background-color: #87CEFA; color: black;")
        self.analysis_btn.setStyleSheet("background-color: #D3D3D3; color: gray;")
        self.history_btn.setStyleSheet("background-color: #D3D3D3; color: gray;")

    def switch_to_history(self):
        self.stacked_pages.setCurrentIndex(2)
        self.history_btn.setDisabled(True)
        self.analysis_btn.setDisabled(False)
        self.status_btn.setDisabled(False)
        self.history_btn.setStyleSheet("background-color: #87CEFA; color: black;")
        self.analysis_btn.setStyleSheet("background-color: #D3D3D3; color: gray;")
        self.status_btn.setStyleSheet("background-color: #D3D3D3; color: gray;")
        self.load_history_records()

    def setup_analysis_page(self):
        page = QtWidgets.QWidget()
        layout = QtWidgets.QHBoxLayout(page)
        layout.setContentsMargins(5, 5, 5, 5)
        layout.setSpacing(15)

        left_panel = QtWidgets.QWidget()
        left_layout = QtWidgets.QVBoxLayout(left_panel)
        left_layout.setSpacing(10)

        self.video_label = QtWidgets.QLabel()
        self.video_label.setMinimumSize(640, 480)
        self.video_label.setStyleSheet("""
            border: 2px solid #666;
            background-color: #000000;
            border-radius: 5px;
        """)
        self.video_label.setAlignment(QtCore.Qt.AlignCenter)
        self.cam_btn = QtWidgets.QPushButton("暂停/继续")
        left_layout.addWidget(self.video_label)
        left_layout.addWidget(self.cam_btn)

        right_panel = QtWidgets.QWidget()
        right_layout = QtWidgets.QVBoxLayout(right_panel)
        right_layout.setSpacing(15)

        btn_style = """
        QPushButton {
            background-color: #4CAF50;
            color: white;
            border: none;
            padding: 8px 16px;
            border-radius: 4px;
            min-width: 150px;
            min-height: 35px;
            font-size: 14px;
        }
        QPushButton:hover { background-color: #45a049; }
        QPushButton:pressed { background-color: #3d8b40; }
        """

        self.start_cam_btn = QtWidgets.QPushButton("开启摄像头")
        self.start_cam_btn.setStyleSheet(btn_style)
        self.import_image_btn = QtWidgets.QPushButton("导入图片")
        self.import_image_btn.setStyleSheet(btn_style)
        self.import_btn = QtWidgets.QPushButton("导入视频")
        self.import_btn.setStyleSheet(btn_style)
        right_layout.addWidget(self.start_cam_btn)
        right_layout.addWidget(self.import_image_btn)
        right_layout.addWidget(self.import_btn)

        info_group = QtWidgets.QGroupBox("检测信息")
        info_group.setStyleSheet("""
            QGroupBox {
                font-size: 14px;
                border: 1px solid #666;
                border-radius: 5px;
                margin-top: 10px;
            }
            QGroupBox::title {
                subcontrol-origin: margin;
                left: 10px;
                padding: 0 3px;
            }
        """)
        info_layout = QtWidgets.QFormLayout(info_group)
        info_layout.setVerticalSpacing(10)
        info_layout.setHorizontalSpacing(15)

        self.confidence_label = QtWidgets.QLabel("0%")
        self.target_count_label = QtWidgets.QLabel("0")
        self.x_label = QtWidgets.QLabel("0")
        self.y_label = QtWidgets.QLabel("0")
        self.temperature_label = QtWidgets.QLabel("0℃")
        self.humidity_label = QtWidgets.QLabel("0%")
        self.target_select = QtWidgets.QComboBox()

        label_style = "font-size: 14px; min-width: 80px;"
        for label in [self.confidence_label, self.target_count_label,
                      self.x_label, self.y_label,
                      self.temperature_label, self.humidity_label]:
            label.setStyleSheet(label_style)

        info_layout.addRow("置信度:", self.confidence_label)
        info_layout.addRow("目标数量:", self.target_count_label)
        info_layout.addRow("X坐标:", self.x_label)
        info_layout.addRow("Y坐标:", self.y_label)
        info_layout.addRow("温度:", self.temperature_label)
        info_layout.addRow("湿度:", self.humidity_label)
        info_layout.addRow("目标选择:", self.target_select)

        self.save_btn = QtWidgets.QPushButton("保存结果")
        self.save_btn.setStyleSheet(btn_style)
        right_layout.addWidget(info_group)
        right_layout.addWidget(self.save_btn)
        right_layout.addStretch()

        layout.addWidget(left_panel)
        layout.addWidget(right_panel)
        self.stacked_pages.addWidget(page)

    def setup_status_page(self):
        page = QtWidgets.QWidget()
        layout = QtWidgets.QVBoxLayout(page)
        self.status_layout = QtWidgets.QGridLayout()

        self.status_layout.setHorizontalSpacing(10)
        self.status_layout.setVerticalSpacing(10)

        self.image_panels = []
        for i in range(6):
            panel = QtWidgets.QWidget(page)  # Set the parent explicitly
            panel.setStyleSheet("background-color: #FFFFFF; border: 1px solid #666;")
            panel_layout = QtWidgets.QVBoxLayout(panel)
            panel_layout.setContentsMargins(5, 5, 5, 5)
            image_label = QtWidgets.QLabel(panel)  # Set the parent for QLabel
            image_label.setFixedSize(200, 200)
            image_label.setStyleSheet("border: 1px solid #666;")
            import_btn = QtWidgets.QPushButton(f"导入图片 {i + 1}", panel)  # Set the parent for QPushButton
            import_btn.clicked.connect(lambda _, idx=i: self.import_status_image(idx))
            panel_layout.addWidget(image_label)
            panel_layout.addWidget(import_btn)
            self.image_panels.append((image_label, import_btn))

        # Add the panels to the grid layout
        for row in range(2):
            for col in range(3):
                idx = row * 3 + col
                # Create a widget container for each panel
                panel_container = QtWidgets.QWidget()
                panel_container.setLayout(QtWidgets.QVBoxLayout())
                panel_container.layout().addWidget(self.image_panels[idx][0])
                panel_container.layout().addWidget(self.image_panels[idx][1])
                self.status_layout.addWidget(panel_container, row, col)

        layout.addLayout(self.status_layout)
        self.stacked_pages.addWidget(page)

    def import_status_image(self, index):
        path, _ = QtWidgets.QFileDialog.getOpenFileName(
            self, "选择图片文件", "", "图片文件 (*.jpg *.png *.bmp)"
        )
        if path:
            image = QtGui.QPixmap(path).scaled(200, 200, QtCore.Qt.AspectRatioMode.KeepAspectRatio)
            self.image_panels[index][0].setPixmap(image)

    def setup_history_page(self):
        page = QtWidgets.QWidget()
        layout = QtWidgets.QVBoxLayout(page)
        self.history_table = QtWidgets.QTableWidget()
        self.history_table.setColumnCount(8)
        self.history_table.setHorizontalHeaderLabels(
            ["保存时间", "原图", "目标数量", "平均置信度", "温度", "湿度", "检测结果", "操作"])
        self.history_table.setEditTriggers(QtWidgets.QTableWidget.NoEditTriggers)
        self.history_table.setSelectionBehavior(QtWidgets.QTableWidget.SelectRows)
        self.history_table.horizontalHeader().setStretchLastSection(False)
        self.history_table.setColumnWidth(0, 180)
        self.history_table.setColumnWidth(1, 80)
        self.history_table.setColumnWidth(2, 80)
        self.history_table.setColumnWidth(3, 80)
        self.history_table.setColumnWidth(4, 80)
        self.history_table.setColumnWidth(5, 80)
        self.history_table.setColumnWidth(6, 80)
        self.history_table.setColumnWidth(7, 80)
        layout.addWidget(self.history_table)
        self.stacked_pages.addWidget(page)

    def setup_video(self):
        self.model = YOLO('runs/detect/train2/weights/best.pt').to(torch.device("cuda:0"))
        self.frame_queue = []
        self.cap = None
        self.video_timer = QtCore.QTimer()
        self.video_timer.timeout.connect(self.update_frame)
        self.update_signal.connect(self.update_image)

        self.cam_btn.setStyleSheet("""
            QPushButton {
                background-color: #4CAF50;
                color: white;
                min-width: 150px;
                min-height: 35px;
            }
        """)
        self.start_cam_btn.clicked.connect(self.start_camera)
        self.cam_btn.clicked.connect(self.toggle_pause)
        self.save_btn.clicked.connect(self.save_results)
        self.import_btn.clicked.connect(self.import_video)
        self.import_image_btn.clicked.connect(self.import_image)
        self.target_select.currentTextChanged.connect(self.update_selection)

        Thread(target=self.process_frames, daemon=True).start()

    def start_camera(self):
        """开启或关闭摄像头"""
        if self.start_cam_btn.text() == "开启摄像头":
            if self.cap is None or not self.cap.isOpened():
                self.cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
                if self.cap.isOpened():
                    self.video_timer.start(30)
                    self.cam_btn.setText("暂停")
                    self.start_cam_btn.setText("关闭摄像头")
                    self.video_paused = False
                    # self.start_cam_btn.setDisabled(True)
        elif self.start_cam_btn.text() == "关闭摄像头":
            self.close_camera()

    def close_camera(self):
        """关闭摄像头"""
        if self.cap:
            self.cap.release()
            self.cap = None
            self.video_timer.stop()
            self.start_cam_btn.setText("开启摄像头")
            self.cam_btn.setText("开启摄像头")
            self.start_cam_btn.setDisabled(False)

    def toggle_pause(self):
        """暂停或继续"""
        if self.cam_btn.text() == "暂停":
            self.video_timer.stop()
            self.cam_btn.setText("继续")
            self.video_paused = True
        elif self.cam_btn.text() == "继续":
            self.video_timer.start(31)
            self.cam_btn.setText("暂停")
            self.video_paused = False

    def import_video(self):
        if self.cap:
            self.close_camera()
        path, _ = QtWidgets.QFileDialog.getOpenFileName(
            self, "选择视频文件", "", "视频文件 (*.mp4 *.avi) (.mp4 *.avi)"
        )
        if path:
            self.cap = cv2.VideoCapture(path)
            if self.cap.isOpened():
                self.video_timer.start(31)
                self.cam_btn.setText("暂停")
                self.start_cam_btn.setDisabled(True)

    def update_image(self, q_img):
        """更新视频标签显示的图像"""
        self.video_label.setPixmap(QtGui.QPixmap.fromImage(q_img))

    def import_image(self):
        if self.cap:  # 如果摄像头正在运行则关闭
            self.close_camera()
        path, _ = QtWidgets.QFileDialog.getOpenFileName(
            self, "选择图片文件", "", "图片文件 (*.jpg *.png *.bmp)"
        )
        if path:
            # !!! 重置检测结果和暂停状态 !!!
            self.current_detections = []  # 若没有这一步,在程序运行初始时导入图片可能会导致程序崩溃

            self.video_paused = True

            # 停止可能存在的视频定时器
            if self.video_timer.isActive():
                self.video_timer.stop()
                self.cam_btn.setText("继续")

            image = cv2.imread(path)
            if image is None:
                QtWidgets.QMessageBox.warning(self, "错误", "无法读取图片文件")
                return
            original_frame = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)  # 保存原始帧

            # 调整图片显示大小
            # h, w, _ = frame.shape
            # aspect_ratio = w / h
            label_width = self.video_label.width()
            label_height = self.video_label.height()
            original_h, original_w = original_frame.shape[:2]
            aspect_ratio = original_w / original_h

            # 计算缩放后的尺寸
            if aspect_ratio > 1:
                new_w = label_width
                new_h = int(new_w / aspect_ratio)
            else:
                new_h = label_height
                new_w = int(new_h * aspect_ratio)

            self.scale_x = new_w / original_w
            self.scale_y = new_h / original_h

            # 缩放原始帧
            scaled_frame = cv2.resize(original_frame, (new_w, new_h))
            self.current_frame = scaled_frame.copy()

            results = self.model.predict(path)[0]  # 直接从本地路径读取图片,否则会因为cv2读取的颜色空间转换问题导致推理结果出现问题。
            self.current_detections = results

            # # 更新检测信息
            # if len(results) > 0:
            #     confidences = results.boxes.conf.cpu().numpy()
            #     avg_conf = confidences.mean() if len(confidences) > 0 else 0
            #     self.confidence_label.setText(f"{avg_conf:.1%}")
            # self.target_count_label.setText(str(len(results)))
            # self.x_label.setText("N/A")
            # self.y_label.setText("N/A")

            self.update_detection_info(results, new_w, new_h)

            # 更新下拉框
            self.target_select.clear()
            self.target_select.addItem("全部")
            for i in range(1, len(results) + 1):
                self.target_select.addItem(str(i))

            # 显示检测结果
            annotated = results.plot()
            annotated = cv2.resize(annotated, (new_w, new_h))
            annotated = cv2.cvtColor(annotated, cv2.COLOR_BGR2RGB)  # 确保是 RGB 格式

            # 将 memoryview 转换为 bytes,并指定参数
            q_img = QtGui.QImage(
                annotated.data.tobytes(),  # 转换为 bytes
                new_w,
                new_h,
                new_w * 3,  # bytes_per_line,RGB 每行 3 个像素
                QtGui.QImage.Format_RGB888  # 指定像素格式为 RGB888
            )
            self.video_label.setPixmap(QtGui.QPixmap.fromImage(q_img))

            # !!! 强制更新界面 !!!
            QtWidgets.QApplication.processEvents()


    def update_detection_info(self, results, width, height):
        confidences = results.boxes.conf.cpu().numpy()
        avg_conf = confidences.mean() if len(confidences) > 0 else 0
        self.confidence_label.setText(f"{avg_conf:.1%}")
        self.target_count_label.setText(str(len(results)))
        self.x_label.setText("N/A")
        self.y_label.setText("N/A")

        temperature = random.randint(1, 100)
        humidity = random.randint(1, 100)
        self.temperature_label.setText(f"{temperature}℃")
        self.humidity_label.setText(f"{humidity}%")

    def update_frame(self):
        if self.cap and self.cap.isOpened():
            ret, frame = self.cap.read()
            if ret:
                frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
                with self.lock:
                    self.frame_queue.append(frame)
                    self.current_frame = frame.copy()

    def process_frames(self):
        while True:
            time.sleep(0.01)  # !!! 添加休眠避免空转消耗GPU !!!

            if self.frame_queue:
                with self.lock:
                    original_frame = self.frame_queue.pop(0)

                # 计算缩放比例
                original_h, original_w = original_frame.shape[:2]
                label_width = self.video_label.width()
                label_height = self.video_label.height()
                aspect_ratio = original_w / original_h

                if aspect_ratio > 1:
                    new_w = label_width
                    new_h = int(new_w / aspect_ratio)
                else:
                    new_h = label_height
                    new_w = int(new_h * aspect_ratio)

                self.scale_x = new_w / original_w
                self.scale_y = new_h / original_h

                # 处理原始尺寸帧
                frame_rgb = cv2.cvtColor(original_frame, cv2.COLOR_BGR2RGB)
                results = self.model(frame_rgb)[0]
                self.current_detections = results

                # 生成缩放后的显示帧
                scaled_frame = cv2.resize(frame_rgb, (new_w, new_h))
                self.current_frame = scaled_frame.copy()
                self.current_detections = results

                # 更新置信度显示
                confidences = results.boxes.conf.cpu().numpy()
                avg_conf = confidences.mean() if len(confidences) > 0 else 0
                self.confidence_label.setText(f"{avg_conf:.1%}")
                self.target_count_label.setText(str(len(results)))
                self.x_label.setText("N/A")
                self.y_label.setText("N/A")

                # 绘制检测结果
                annotated = results.plot()
                annotated = cv2.cvtColor(annotated, cv2.COLOR_BGR2RGB)
                annotated = cv2.resize(annotated, (new_w, new_h))

                q_img = QtGui.QImage(annotated.data, new_w, new_h,
                                     annotated.shape[2] * new_w, QtGui.QImage.Format_RGB888)
                self.update_signal.emit(q_img)

    def update_selection(self):

        # !!! 添加空检测保护 !!!
        if not self.current_detections:
            return

        if self.video_paused and self.current_frame is not None and self.current_detections:
            selected = self.target_select.currentText()
            frame = self.current_frame.copy()

            total_conf = []
            for i, detection in enumerate(self.current_detections):
                # 获取原始检测框坐标
                box = detection.boxes.xyxy[0].cpu().numpy().astype(int)

                # 应用缩放比例
                scaled_box = [
                    int(box[0] * self.scale_x),
                    int(box[1] * self.scale_y),
                    int(box[2] * self.scale_x),
                    int(box[3] * self.scale_y)
                ]

                conf = detection.boxes.conf.item()
                cls_id = detection.boxes.cls.item()
                class_name = self.model.names[int(cls_id)]
                label = f"{class_name} {conf:.2f}"

                # 判断是否选中该目标
                if selected == "全部":
                    color = (0, 0, 255)  # 红色显示所有目标
                    thickness = 2
                    cv2.rectangle(frame, (scaled_box[0], scaled_box[1]),
                                  (scaled_box[2], scaled_box[3]), color, thickness)
                    cv2.putText(frame, label, (scaled_box[0], scaled_box[1] - 10),
                                cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, thickness)
                    total_conf.append(conf)
                elif selected == str(i + 1):
                    color = (0, 255, 0)  # 绿色高亮选中目标
                    thickness = 2
                    cv2.rectangle(frame, (scaled_box[0], scaled_box[1]),
                                  (scaled_box[2], scaled_box[3]), color, thickness)
                    cv2.putText(frame, label, (scaled_box[0], scaled_box[1] - 10),
                                cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, thickness)
                    total_conf.append(conf)

                    # 更新坐标信息
                    x_center = (scaled_box[0] + scaled_box[2]) // 2
                    y_center = (scaled_box[1] + scaled_box[3]) // 2
                    self.x_label.setText(str(x_center))
                    self.y_label.setText(str(y_center))

            if selected == "全部":
                avg_conf = sum(total_conf) / len(total_conf) if total_conf else 0
                self.confidence_label.setText(f"{avg_conf:.1%}")
                self.x_label.setText("N/A")
                self.y_label.setText("N/A")
            elif total_conf:
                self.confidence_label.setText(f"{total_conf[0]:.1%}")

            # 显示处理后的帧
            h, w, ch = frame.shape
            q_img = QtGui.QImage(frame.data, w, h, ch * w, QtGui.QImage.Format_RGB888)
            self.video_label.setPixmap(QtGui.QPixmap.fromImage(q_img))

    def save_results(self):
        if self.video_label.pixmap() is None:
            QtWidgets.QMessageBox.warning(self, "警告", "没有检测结果可以保存")
            return

        timestamp = time.strftime("%Y%m%d_%H%M%S")
        save_dir = os.path.join(self.results_dir, timestamp)
        os.makedirs(save_dir, exist_ok=True)

        # 保存图片
        original_image_path = os.path.join(save_dir, "original.jpg")
        processed_image_path = os.path.join(save_dir, "processed.jpg")
        detection_result_path = os.path.join(save_dir, "detection.jpg")

        # 保存处理后的图像
        self.video_label.pixmap().save(processed_image_path)

        # 保存原始图像
        if self.current_frame is not None:
            original_image = cv2.cvtColor(self.current_frame, cv2.COLOR_RGB2BGR)
            cv2.imwrite(original_image_path, original_image)

        # 保存检测结果图像
        if self.current_detections:
            annotated = self.current_detections.plot()
            cv2.imwrite(detection_result_path, annotated)

        # 生成Word报告
        doc = Document()
        doc.add_heading('检测报告', 0)

        # 添加时间
        time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        doc.add_paragraph(f"检测时间: {time_str}")

        # 添加图片
        doc.add_heading('原始图像', level=1)
        doc.add_picture(original_image_path, width=Inches(4))

        doc.add_heading('检测结果', level=1)
        doc.add_picture(processed_image_path, width=Inches(4))

        # 添加检测信息
        info = [
            ("平均置信度", self.confidence_label.text()),
            ("目标数量", self.target_count_label.text()),
            ("温度", self.temperature_label.text()),
            ("湿度", self.humidity_label.text())
        ]

        table = doc.add_table(rows=1, cols=2)
        hdr_cells = table.rows[0].cells
        hdr_cells[0].text = '项目'
        hdr_cells[1].text = ''

        for item in info:
            row_cells = table.add_row().cells
            row_cells[0].text = item[0]
            row_cells[1].text = item[1]

        # 保存Word文档
        report_path = os.path.join(save_dir, "report.docx")
        doc.save(report_path)

        # 保存元数据
        metadata = {
            "target_count": int(self.target_count_label.text()),
            "average_confidence": float(self.confidence_label.text().strip("%")) / 100,
            "temperature": int(self.temperature_label.text().strip("")),
            "humidity": int(self.humidity_label.text().strip("%")),
            "timestamp": timestamp
        }
        metadata_path = os.path.join(save_dir, "metadata.json")
        with open(metadata_path, "w") as f:
            json.dump(metadata, f)

        QtWidgets.QMessageBox.information(self, "保存成功", "结果已保存至: " + save_dir)
        self.load_history_records()

    def load_history_records(self):
        self.history_table.setRowCount(0)
        for subdir in os.listdir(self.results_dir):
            subdir_path = os.path.join(self.results_dir, subdir)
            metadata_path = os.path.join(subdir_path, "metadata.json")
            if os.path.exists(metadata_path):
                with open(metadata_path, "r") as f:
                    metadata = json.load(f)

                row_num = self.history_table.rowCount()
                self.history_table.insertRow(row_num)

                # 格式化时间
                ts = metadata["timestamp"]
                formatted_time = f"{ts[:4]}/{ts[4:6]}/{ts[6:8]} {ts[9:11].zfill(2)}:{ts[11:13].zfill(2)}:{ts[13:15].zfill(2)}"

                # 填充表格数据
                self.history_table.setItem(row_num, 0, QtWidgets.QTableWidgetItem(formatted_time))
                self.history_table.setItem(row_num, 2, QtWidgets.QTableWidgetItem(str(metadata["target_count"])))
                self.history_table.setItem(row_num, 3,
                                           QtWidgets.QTableWidgetItem(f"{metadata['average_confidence']:.1%}"))
                self.history_table.setItem(row_num, 4, QtWidgets.QTableWidgetItem(f"{metadata['temperature']}℃"))
                self.history_table.setItem(row_num, 5, QtWidgets.QTableWidgetItem(f"{metadata['humidity']}%"))

                # 添加按钮
                original_btn = QtWidgets.QPushButton("查看")
                original_btn.clicked.connect(lambda _, p=os.path.join(subdir_path, "original.jpg"): self.show_image(p))
                self.history_table.setCellWidget(row_num, 1, original_btn)

                result_btn = QtWidgets.QPushButton("查看")
                result_btn.clicked.connect(lambda _, p=os.path.join(subdir_path, "processed.jpg"): self.show_image(p))
                self.history_table.setCellWidget(row_num, 6, result_btn)

                delete_btn = QtWidgets.QPushButton("删除")
                delete_btn.setStyleSheet("background-color: #ff4444; color: white;font-size: 13px;")
                delete_btn.clicked.connect(lambda _, p=subdir_path: self.delete_record(p))
                self.history_table.setCellWidget(row_num, 7, delete_btn)

    def delete_record(self, record_path):
        try:
            shutil.rmtree(record_path)
            self.load_history_records()
            QtWidgets.QMessageBox.information(self, "成功", "记录已删除")
        except Exception as e:
            QtWidgets.QMessageBox.warning(self, "错误", f"删除失败: {str(e)}")

    def show_image(self, path):
        dialog = QtWidgets.QDialog(self)
        dialog.setWindowTitle("查看图片")
        layout = QtWidgets.QVBoxLayout(dialog)
        label = QtWidgets.QLabel()
        pixmap = QtGui.QPixmap(path)
        label.setPixmap(pixmap)
        layout.addWidget(label)
        dialog.exec()


if __name__ == "__main__":
    app = QtWidgets.QApplication()
    window = MainWindow()
    window.show()
    app.exec()

 

from PySide6 import QtWidgets, QtCore, QtGui
import cv2, os, time, random
from threading import Thread, Lock
from ultralytics import YOLO
import torch
import json
import shutil
from docx import Document
from docx.shared import Inches
from datetime import datetime

os.environ['YOLO_VERBOSE'] = 'False'


class MainWindow(QtWidgets.QMainWindow):
update_signal = QtCore.Signal(QtGui.QImage)

def __init__(self):
super().__init__()
self.setup_ui()
self.setup_video()

self.analysis_results = []
self.current_detections = []
self.current_frame = None
self.lock = Lock()
self.video_paused = False
self.is_video_file = False
self.scale_x = 1.0
self.scale_y = 1.0
self.results_dir = "results"
os.makedirs(self.results_dir, exist_ok=True)

def setup_ui(self):
self.setWindowTitle("智能分析系统")
self.resize(1200, 800)
self.setStyleSheet("background-color: #F0F0F0;")

central_widget = QtWidgets.QWidget()
self.setCentralWidget(central_widget)
main_layout = QtWidgets.QVBoxLayout(central_widget)
main_layout.setContentsMargins(10, 10, 10, 10)
main_layout.setSpacing(10)

# 顶部切换按钮
btn_style = """
QPushButton {
background-color: #87CEFA;
color: black;
border: none;
padding: 8px 16px;
border-radius: 4px;
min-width: 120px;
min-height: 35px;
font-size: 14px;
}
QPushButton:disabled {
background-color: #D3D3D3;
color: gray;
}
"""

self.analysis_btn = QtWidgets.QPushButton("数据分析")
self.status_btn = QtWidgets.QPushButton("今日情况")
self.history_btn = QtWidgets.QPushButton("历史记录")
self.analysis_btn.setStyleSheet(btn_style)
self.status_btn.setStyleSheet(btn_style.replace("#87CEFA", "#D3D3D3").replace("black", "gray"))
self.history_btn.setStyleSheet(btn_style.replace("#87CEFA", "#D3D3D3").replace("black", "gray"))

top_layout = QtWidgets.QHBoxLayout()
top_layout.addWidget(self.analysis_btn)
top_layout.addWidget(self.status_btn)
top_layout.addWidget(self.history_btn)
top_layout.setSpacing(10)
main_layout.addLayout(top_layout)

# 页面堆栈
self.stacked_pages = QtWidgets.QStackedWidget()
main_layout.addWidget(self.stacked_pages)

self.setup_analysis_page()
self.setup_status_page()
self.setup_history_page()

self.analysis_btn.clicked.connect(self.switch_to_analysis)
self.status_btn.clicked.connect(self.switch_to_status)
self.history_btn.clicked.connect(self.switch_to_history)
self.switch_to_analysis()

def switch_to_analysis(self):
self.stacked_pages.setCurrentIndex(0)
self.analysis_btn.setDisabled(True)
self.status_btn.setDisabled(False)
self.history_btn.setDisabled(False)
self.analysis_btn.setStyleSheet("background-color: #87CEFA; color: black;")
self.status_btn.setStyleSheet("background-color: #D3D3D3; color: gray;")
self.history_btn.setStyleSheet("background-color: #D3D3D3; color: gray;")

def switch_to_status(self):
self.stacked_pages.setCurrentIndex(1)
self.status_btn.setDisabled(True)
self.analysis_btn.setDisabled(False)
self.history_btn.setDisabled(False)
self.status_btn.setStyleSheet("background-color: #87CEFA; color: black;")
self.analysis_btn.setStyleSheet("background-color: #D3D3D3; color: gray;")
self.history_btn.setStyleSheet("background-color: #D3D3D3; color: gray;")

def switch_to_history(self):
self.stacked_pages.setCurrentIndex(2)
self.history_btn.setDisabled(True)
self.analysis_btn.setDisabled(False)
self.status_btn.setDisabled(False)
self.history_btn.setStyleSheet("background-color: #87CEFA; color: black;")
self.analysis_btn.setStyleSheet("background-color: #D3D3D3; color: gray;")
self.status_btn.setStyleSheet("background-color: #D3D3D3; color: gray;")
self.load_history_records()

def setup_analysis_page(self):
page = QtWidgets.QWidget()
layout = QtWidgets.QHBoxLayout(page)
layout.setContentsMargins(5, 5, 5, 5)
layout.setSpacing(15)

left_panel = QtWidgets.QWidget()
left_layout = QtWidgets.QVBoxLayout(left_panel)
left_layout.setSpacing(10)

self.video_label = QtWidgets.QLabel()
self.video_label.setMinimumSize(640, 480)
self.video_label.setStyleSheet("""
border: 2px solid #666;
background-color: #000000;
border-radius: 5px;
""")
self.video_label.setAlignment(QtCore.Qt.AlignCenter)
self.cam_btn = QtWidgets.QPushButton("暂停/继续")
left_layout.addWidget(self.video_label)
left_layout.addWidget(self.cam_btn)

right_panel = QtWidgets.QWidget()
right_layout = QtWidgets.QVBoxLayout(right_panel)
right_layout.setSpacing(15)

btn_style = """
QPushButton {
background-color: #4CAF50;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
min-width: 150px;
min-height: 35px;
font-size: 14px;
}
QPushButton:hover { background-color: #45a049; }
QPushButton:pressed { background-color: #3d8b40; }
"""

self.start_cam_btn = QtWidgets.QPushButton("开启摄像头")
self.start_cam_btn.setStyleSheet(btn_style)
self.import_image_btn = QtWidgets.QPushButton("导入图片")
self.import_image_btn.setStyleSheet(btn_style)
self.import_btn = QtWidgets.QPushButton("导入视频")
self.import_btn.setStyleSheet(btn_style)
right_layout.addWidget(self.start_cam_btn)
right_layout.addWidget(self.import_image_btn)
right_layout.addWidget(self.import_btn)

info_group = QtWidgets.QGroupBox("检测信息")
info_group.setStyleSheet("""
QGroupBox {
font-size: 14px;
border: 1px solid #666;
border-radius: 5px;
margin-top: 10px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 3px;
}
""")
info_layout = QtWidgets.QFormLayout(info_group)
info_layout.setVerticalSpacing(10)
info_layout.setHorizontalSpacing(15)

self.confidence_label = QtWidgets.QLabel("0%")
self.target_count_label = QtWidgets.QLabel("0")
self.x_label = QtWidgets.QLabel("0")
self.y_label = QtWidgets.QLabel("0")
self.temperature_label = QtWidgets.QLabel("0")
self.humidity_label = QtWidgets.QLabel("0%")
self.target_select = QtWidgets.QComboBox()

label_style = "font-size: 14px; min-width: 80px;"
for label in [self.confidence_label, self.target_count_label,
self.x_label, self.y_label,
self.temperature_label, self.humidity_label]:
label.setStyleSheet(label_style)

info_layout.addRow("置信度:", self.confidence_label)
info_layout.addRow("目标数量:", self.target_count_label)
info_layout.addRow("X坐标:", self.x_label)
info_layout.addRow("Y坐标:", self.y_label)
info_layout.addRow("温度:", self.temperature_label)
info_layout.addRow("湿度:", self.humidity_label)
info_layout.addRow("目标选择:", self.target_select)

self.save_btn = QtWidgets.QPushButton("保存结果")
self.save_btn.setStyleSheet(btn_style)
right_layout.addWidget(info_group)
right_layout.addWidget(self.save_btn)
right_layout.addStretch()

layout.addWidget(left_panel)
layout.addWidget(right_panel)
self.stacked_pages.addWidget(page)

def setup_status_page(self):
page = QtWidgets.QWidget()
layout = QtWidgets.QVBoxLayout(page)
self.status_layout = QtWidgets.QGridLayout()

self.status_layout.setHorizontalSpacing(10)
self.status_layout.setVerticalSpacing(10)

self.image_panels = []
for i in range(6):
panel = QtWidgets.QWidget(page) # Set the parent explicitly
panel.setStyleSheet("background-color: #FFFFFF; border: 1px solid #666;")
panel_layout = QtWidgets.QVBoxLayout(panel)
panel_layout.setContentsMargins(5, 5, 5, 5)
image_label = QtWidgets.QLabel(panel) # Set the parent for QLabel
image_label.setFixedSize(200, 200)
image_label.setStyleSheet("border: 1px solid #666;")
import_btn = QtWidgets.QPushButton(f"导入图片 {i + 1}", panel) # Set the parent for QPushButton
import_btn.clicked.connect(lambda _, idx=i: self.import_status_image(idx))
panel_layout.addWidget(image_label)
panel_layout.addWidget(import_btn)
self.image_panels.append((image_label, import_btn))

# Add the panels to the grid layout
for row in range(2):
for col in range(3):
idx = row * 3 + col
# Create a widget container for each panel
panel_container = QtWidgets.QWidget()
panel_container.setLayout(QtWidgets.QVBoxLayout())
panel_container.layout().addWidget(self.image_panels[idx][0])
panel_container.layout().addWidget(self.image_panels[idx][1])
self.status_layout.addWidget(panel_container, row, col)

layout.addLayout(self.status_layout)
self.stacked_pages.addWidget(page)

def import_status_image(self, index):
path, _ = QtWidgets.QFileDialog.getOpenFileName(
self, "选择图片文件", "", "图片文件 (*.jpg *.png *.bmp)"
)
if path:
image = QtGui.QPixmap(path).scaled(200, 200, QtCore.Qt.AspectRatioMode.KeepAspectRatio)
self.image_panels[index][0].setPixmap(image)

def setup_history_page(self):
page = QtWidgets.QWidget()
layout = QtWidgets.QVBoxLayout(page)
self.history_table = QtWidgets.QTableWidget()
self.history_table.setColumnCount(8)
self.history_table.setHorizontalHeaderLabels(
["保存时间", "原图", "目标数量", "平均置信度", "温度", "湿度", "检测结果", "操作"])
self.history_table.setEditTriggers(QtWidgets.QTableWidget.NoEditTriggers)
self.history_table.setSelectionBehavior(QtWidgets.QTableWidget.SelectRows)
self.history_table.horizontalHeader().setStretchLastSection(False)
self.history_table.setColumnWidth(0, 180)
self.history_table.setColumnWidth(1, 80)
self.history_table.setColumnWidth(2, 80)
self.history_table.setColumnWidth(3, 80)
self.history_table.setColumnWidth(4, 80)
self.history_table.setColumnWidth(5, 80)
self.history_table.setColumnWidth(6, 80)
self.history_table.setColumnWidth(7, 80)
layout.addWidget(self.history_table)
self.stacked_pages.addWidget(page)

def setup_video(self):
self.model = YOLO('runs/detect/train2/weights/best.pt').to(torch.device("cuda:0"))
self.frame_queue = []
self.cap = None
self.video_timer = QtCore.QTimer()
self.video_timer.timeout.connect(self.update_frame)
self.update_signal.connect(self.update_image)

self.cam_btn.setStyleSheet("""
QPushButton {
background-color: #4CAF50;
color: white;
min-width: 150px;
min-height: 35px;
}
""")
self.start_cam_btn.clicked.connect(self.start_camera)
self.cam_btn.clicked.connect(self.toggle_pause)
self.save_btn.clicked.connect(self.save_results)
self.import_btn.clicked.connect(self.import_video)
self.import_image_btn.clicked.connect(self.import_image)
self.target_select.currentTextChanged.connect(self.update_selection)

Thread(target=self.process_frames, daemon=True).start()

def start_camera(self):
"""开启或关闭摄像头"""
if self.start_cam_btn.text() == "开启摄像头":
if self.cap is None or not self.cap.isOpened():
self.cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
if self.cap.isOpened():
self.video_timer.start(30)
self.cam_btn.setText("暂停")
self.start_cam_btn.setText("关闭摄像头")
self.video_paused = False
# self.start_cam_btn.setDisabled(True)
elif self.start_cam_btn.text() == "关闭摄像头":
self.close_camera()

def close_camera(self):
"""关闭摄像头"""
if self.cap:
self.cap.release()
self.cap = None
self.video_timer.stop()
self.start_cam_btn.setText("开启摄像头")
self.cam_btn.setText("开启摄像头")
self.start_cam_btn.setDisabled(False)

def toggle_pause(self):
"""暂停或继续"""
if self.cam_btn.text() == "暂停":
self.video_timer.stop()
self.cam_btn.setText("继续")
self.video_paused = True
elif self.cam_btn.text() == "继续":
self.video_timer.start(31)
self.cam_btn.setText("暂停")
self.video_paused = False

def import_video(self):
if self.cap:
self.close_camera()
path, _ = QtWidgets.QFileDialog.getOpenFileName(
self, "选择视频文件", "", "视频文件 (*.mp4 *.avi) (.mp4 *.avi)"
)
if path:
self.cap = cv2.VideoCapture(path)
if self.cap.isOpened():
self.video_timer.start(31)
self.cam_btn.setText("暂停")
self.start_cam_btn.setDisabled(True)

def update_image(self, q_img):
"""更新视频标签显示的图像"""
self.video_label.setPixmap(QtGui.QPixmap.fromImage(q_img))

def import_image(self):
if self.cap: # 如果摄像头正在运行则关闭
self.close_camera()
path, _ = QtWidgets.QFileDialog.getOpenFileName(
self, "选择图片文件", "", "图片文件 (*.jpg *.png *.bmp)"
)
if path:
# !!! 重置检测结果和暂停状态 !!!
self.current_detections = [] # 若没有这一步,在程序运行初始时导入图片可能会导致程序崩溃

self.video_paused = True

# 停止可能存在的视频定时器
if self.video_timer.isActive():
self.video_timer.stop()
self.cam_btn.setText("继续")

image = cv2.imread(path)
if image is None:
QtWidgets.QMessageBox.warning(self, "错误", "无法读取图片文件")
return
original_frame = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # 保存原始帧

# 调整图片显示大小
# h, w, _ = frame.shape
# aspect_ratio = w / h
label_width = self.video_label.width()
label_height = self.video_label.height()
original_h, original_w = original_frame.shape[:2]
aspect_ratio = original_w / original_h

# 计算缩放后的尺寸
if aspect_ratio > 1:
new_w = label_width
new_h = int(new_w / aspect_ratio)
else:
new_h = label_height
new_w = int(new_h * aspect_ratio)

self.scale_x = new_w / original_w
self.scale_y = new_h / original_h

# 缩放原始帧
scaled_frame = cv2.resize(original_frame, (new_w, new_h))
self.current_frame = scaled_frame.copy()

results = self.model.predict(path)[0] # 直接从本地路径读取图片,否则会因为cv2读取的颜色空间转换问题导致推理结果出现问题。
self.current_detections = results

# # 更新检测信息
# if len(results) > 0:
# confidences = results.boxes.conf.cpu().numpy()
# avg_conf = confidences.mean() if len(confidences) > 0 else 0
# self.confidence_label.setText(f"{avg_conf:.1%}")
# self.target_count_label.setText(str(len(results)))
# self.x_label.setText("N/A")
# self.y_label.setText("N/A")

self.update_detection_info(results, new_w, new_h)

# 更新下拉框
self.target_select.clear()
self.target_select.addItem("全部")
for i in range(1, len(results) + 1):
self.target_select.addItem(str(i))

# 显示检测结果
annotated = results.plot()
annotated = cv2.resize(annotated, (new_w, new_h))
annotated = cv2.cvtColor(annotated, cv2.COLOR_BGR2RGB) # 确保是 RGB 格式

# memoryview 转换为 bytes,并指定参数
q_img = QtGui.QImage(
annotated.data.tobytes(), # 转换为 bytes
new_w,
new_h,
new_w * 3, # bytes_per_lineRGB 每行 3 个像素
QtGui.QImage.Format_RGB888 # 指定像素格式为 RGB888
)
self.video_label.setPixmap(QtGui.QPixmap.fromImage(q_img))

# !!! 强制更新界面 !!!
QtWidgets.QApplication.processEvents()


def update_detection_info(self, results, width, height):
confidences = results.boxes.conf.cpu().numpy()
avg_conf = confidences.mean() if len(confidences) > 0 else 0
self.confidence_label.setText(f"{avg_conf:.1%}")
self.target_count_label.setText(str(len(results)))
self.x_label.setText("N/A")
self.y_label.setText("N/A")

temperature = random.randint(1, 100)
humidity = random.randint(1, 100)
self.temperature_label.setText(f"{temperature}")
self.humidity_label.setText(f"{humidity}%")

def update_frame(self):
if self.cap and self.cap.isOpened():
ret, frame = self.cap.read()
if ret:
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
with self.lock:
self.frame_queue.append(frame)
self.current_frame = frame.copy()

def process_frames(self):
while True:
time.sleep(0.01) # !!! 添加休眠避免空转消耗GPU !!!

if self.frame_queue:
with self.lock:
original_frame = self.frame_queue.pop(0)

# 计算缩放比例
original_h, original_w = original_frame.shape[:2]
label_width = self.video_label.width()
label_height = self.video_label.height()
aspect_ratio = original_w / original_h

if aspect_ratio > 1:
new_w = label_width
new_h = int(new_w / aspect_ratio)
else:
new_h = label_height
new_w = int(new_h * aspect_ratio)

self.scale_x = new_w / original_w
self.scale_y = new_h / original_h

# 处理原始尺寸帧
frame_rgb = cv2.cvtColor(original_frame, cv2.COLOR_BGR2RGB)
results = self.model(frame_rgb)[0]
self.current_detections = results

# 生成缩放后的显示帧
scaled_frame = cv2.resize(frame_rgb, (new_w, new_h))
self.current_frame = scaled_frame.copy()
self.current_detections = results

# 更新置信度显示
confidences = results.boxes.conf.cpu().numpy()
avg_conf = confidences.mean() if len(confidences) > 0 else 0
self.confidence_label.setText(f"{avg_conf:.1%}")
self.target_count_label.setText(str(len(results)))
self.x_label.setText("N/A")
self.y_label.setText("N/A")

# 绘制检测结果
annotated = results.plot()
annotated = cv2.cvtColor(annotated, cv2.COLOR_BGR2RGB)
annotated = cv2.resize(annotated, (new_w, new_h))

q_img = QtGui.QImage(annotated.data, new_w, new_h,
annotated.shape[2] * new_w, QtGui.QImage.Format_RGB888)
self.update_signal.emit(q_img)

def update_selection(self):

# !!! 添加空检测保护 !!!
if not self.current_detections:
return

if self.video_paused and self.current_frame is not None and self.current_detections:
selected = self.target_select.currentText()
frame = self.current_frame.copy()

total_conf = []
for i, detection in enumerate(self.current_detections):
# 获取原始检测框坐标
box = detection.boxes.xyxy[0].cpu().numpy().astype(int)

# 应用缩放比例
scaled_box = [
int(box[0] * self.scale_x),
int(box[1] * self.scale_y),
int(box[2] * self.scale_x),
int(box[3] * self.scale_y)
]

conf = detection.boxes.conf.item()
cls_id = detection.boxes.cls.item()
class_name = self.model.names[int(cls_id)]
label = f"{class_name} {conf:.2f}"

# 判断是否选中该目标
if selected == "全部":
color = (0, 0, 255) # 红色显示所有目标
thickness = 2
cv2.rectangle(frame, (scaled_box[0], scaled_box[1]),
(scaled_box[2], scaled_box[3]), color, thickness)
cv2.putText(frame, label, (scaled_box[0], scaled_box[1] - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, thickness)
total_conf.append(conf)
elif selected == str(i + 1):
color = (0, 255, 0) # 绿色高亮选中目标
thickness = 2
cv2.rectangle(frame, (scaled_box[0], scaled_box[1]),
(scaled_box[2], scaled_box[3]), color, thickness)
cv2.putText(frame, label, (scaled_box[0], scaled_box[1] - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, thickness)
total_conf.append(conf)

# 更新坐标信息
x_center = (scaled_box[0] + scaled_box[2]) // 2
y_center = (scaled_box[1] + scaled_box[3]) // 2
self.x_label.setText(str(x_center))
self.y_label.setText(str(y_center))

if selected == "全部":
avg_conf = sum(total_conf) / len(total_conf) if total_conf else 0
self.confidence_label.setText(f"{avg_conf:.1%}")
self.x_label.setText("N/A")
self.y_label.setText("N/A")
elif total_conf:
self.confidence_label.setText(f"{total_conf[0]:.1%}")

# 显示处理后的帧
h, w, ch = frame.shape
q_img = QtGui.QImage(frame.data, w, h, ch * w, QtGui.QImage.Format_RGB888)
self.video_label.setPixmap(QtGui.QPixmap.fromImage(q_img))

def save_results(self):
if self.video_label.pixmap() is None:
QtWidgets.QMessageBox.warning(self, "警告", "没有检测结果可以保存")
return

timestamp = time.strftime("%Y%m%d_%H%M%S")
save_dir = os.path.join(self.results_dir, timestamp)
os.makedirs(save_dir, exist_ok=True)

# 保存图片
original_image_path = os.path.join(save_dir, "original.jpg")
processed_image_path = os.path.join(save_dir, "processed.jpg")
detection_result_path = os.path.join(save_dir, "detection.jpg")

# 保存处理后的图像
self.video_label.pixmap().save(processed_image_path)

# 保存原始图像
if self.current_frame is not None:
original_image = cv2.cvtColor(self.current_frame, cv2.COLOR_RGB2BGR)
cv2.imwrite(original_image_path, original_image)

# 保存检测结果图像
if self.current_detections:
annotated = self.current_detections.plot()
cv2.imwrite(detection_result_path, annotated)

# 生成Word报告
doc = Document()
doc.add_heading('检测报告', 0)

# 添加时间
time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
doc.add_paragraph(f"检测时间: {time_str}")

# 添加图片
doc.add_heading('原始图像', level=1)
doc.add_picture(original_image_path, width=Inches(4))

doc.add_heading('检测结果', level=1)
doc.add_picture(processed_image_path, width=Inches(4))

# 添加检测信息
info = [
("平均置信度", self.confidence_label.text()),
("目标数量", self.target_count_label.text()),
("温度", self.temperature_label.text()),
("湿度", self.humidity_label.text())
]

table = doc.add_table(rows=1, cols=2)
hdr_cells = table.rows[0].cells
hdr_cells[0].text = '项目'
hdr_cells[1].text = ''

for item in info:
row_cells = table.add_row().cells
row_cells[0].text = item[0]
row_cells[1].text = item[1]

# 保存Word文档
report_path = os.path.join(save_dir, "report.docx")
doc.save(report_path)

# 保存元数据
metadata = {
"target_count": int(self.target_count_label.text()),
"average_confidence": float(self.confidence_label.text().strip("%")) / 100,
"temperature": int(self.temperature_label.text().strip("")),
"humidity": int(self.humidity_label.text().strip("%")),
"timestamp": timestamp
}
metadata_path = os.path.join(save_dir, "metadata.json")
with open(metadata_path, "w") as f:
json.dump(metadata, f)

QtWidgets.QMessageBox.information(self, "保存成功", "结果已保存至: " + save_dir)
self.load_history_records()

def load_history_records(self):
self.history_table.setRowCount(0)
for subdir in os.listdir(self.results_dir):
subdir_path = os.path.join(self.results_dir, subdir)
metadata_path = os.path.join(subdir_path, "metadata.json")
if os.path.exists(metadata_path):
with open(metadata_path, "r") as f:
metadata = json.load(f)

row_num = self.history_table.rowCount()
self.history_table.insertRow(row_num)

# 格式化时间
ts = metadata["timestamp"]
formatted_time = f"{ts[:4]}/{ts[4:6]}/{ts[6:8]} {ts[9:11].zfill(2)}:{ts[11:13].zfill(2)}:{ts[13:15].zfill(2)}"

# 填充表格数据
self.history_table.setItem(row_num, 0, QtWidgets.QTableWidgetItem(formatted_time))
self.history_table.setItem(row_num, 2, QtWidgets.QTableWidgetItem(str(metadata["target_count"])))
self.history_table.setItem(row_num, 3,
QtWidgets.QTableWidgetItem(f"{metadata['average_confidence']:.1%}"))
self.history_table.setItem(row_num, 4, QtWidgets.QTableWidgetItem(f"{metadata['temperature']}"))
self.history_table.setItem(row_num, 5, QtWidgets.QTableWidgetItem(f"{metadata['humidity']}%"))

# 添加按钮
original_btn = QtWidgets.QPushButton("查看")
original_btn.clicked.connect(lambda _, p=os.path.join(subdir_path, "original.jpg"): self.show_image(p))
self.history_table.setCellWidget(row_num, 1, original_btn)

result_btn = QtWidgets.QPushButton("查看")
result_btn.clicked.connect(lambda _, p=os.path.join(subdir_path, "processed.jpg"): self.show_image(p))
self.history_table.setCellWidget(row_num, 6, result_btn)

delete_btn = QtWidgets.QPushButton("删除")
delete_btn.setStyleSheet("background-color: #ff4444; color: white;font-size: 13px;")
delete_btn.clicked.connect(lambda _, p=subdir_path: self.delete_record(p))
self.history_table.setCellWidget(row_num, 7, delete_btn)

def delete_record(self, record_path):
try:
shutil.rmtree(record_path)
self.load_history_records()
QtWidgets.QMessageBox.information(self, "成功", "记录已删除")
except Exception as e:
QtWidgets.QMessageBox.warning(self, "错误", f"删除失败: {str(e)}")

def show_image(self, path):
dialog = QtWidgets.QDialog(self)
dialog.setWindowTitle("查看图片")
layout = QtWidgets.QVBoxLayout(dialog)
label = QtWidgets.QLabel()
pixmap = QtGui.QPixmap(path)
label.setPixmap(pixmap)
layout.addWidget(label)
dialog.exec()


if __name__ == "__main__":
app = QtWidgets.QApplication()
window = MainWindow()
window.show()
app.exec()
posted @ 2025-03-02 20:32  贾贾鱼  阅读(19)  评论(0)    收藏  举报