import sys
import os
import cv2
import traceback
from datetime import datetime
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QUrl
from PyQt5.QtWidgets import (QApplication, QMainWindow, QLabel, QVBoxLayout, QWidget,
QPushButton, QHBoxLayout, QMessageBox, QFileDialog,
QGroupBox, QStatusBar, QTextEdit, QTabWidget, QTableWidget,
QTableWidgetItem, QHeaderView, QComboBox, QScrollArea,
QTabBar, QSizePolicy, QCheckBox)
from PyQt5.QtGui import QImage, QPixmap, QIcon, QFont, QColor
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent
from ultralytics import YOLO
# ================= 现代专业样式表 =================
STYLE_SHEET = """
QMainWindow {
background-color: #F8F9FA;
font-family: 'Segoe UI';
}
QGroupBox {
border: 2px solid #E9ECEF;
border-radius: 8px;
margin-top: 1ex;
padding: 15px;
background-color: white;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 8px;
color: #212529;
font-weight: 600;
font-size: 14px;
}
QPushButton {
background-color: #4A90E2;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
min-width: 120px;
font-size: 13px;
}
QPushButton:hover {
background-color: #357ABD;
}
QPushButton:disabled {
background-color: #CED4DA;
}
QLabel {
font-size: 13px;
color: #495057;
}
QStatusBar {
background-color: #E9ECEF;
color: #6C757D;
font-size: 12px;
}
QTabWidget {
background: white;
}
QTabBar::tab {
background: #E9ECEF;
border: 1px solid #DEE2E6;
padding: 10px 20px;
font-size: 13px;
color: #6C757D;
min-width: 120px;
}
QTabBar::tab:selected {
background: #4A90E2;
color: white;
border-color: #4A90E2;
}
QTableWidget {
alternate-background-color: #F8F9FA;
font-size: 12px;
selection-background-color: #B9D6F3;
}
QScrollArea {
border: none;
background: transparent;
}
QComboBox {
padding: 8px;
border: 1px solid #CED4DA;
border-radius: 4px;
min-width: 200px;
background: white;
}
"""
class DetectionThread(QThread):
detection_finished = pyqtSignal(dict)
error_occurred = pyqtSignal(str)
def __init__(self, model, image_path, model_name):
super().__init__()
self.model = model
self.image_path = image_path
self.model_name = model_name
def run(self):
try:
image = cv2.imread(self.image_path)
if image is None:
raise ValueError("无法读取图片文件")
start_time = datetime.now()
results = self.model.predict(image)
elapsed = (datetime.now() - start_time).total_seconds()
detection_data = {
"model": self.model_name,
"time": elapsed,
"image_path": self.image_path,
"annotated": results[0].plot(),
"results": []
}
for box in results[0].boxes:
class_id = int(box.cls)
detection_data["results"].append({
"class": self.model.names[class_id].lower(), # 统一转为小写
"confidence": float(box.conf)
})
self.detection_finished.emit(detection_data)
except Exception as e:
self.error_occurred.emit(f"检测错误: {str(e)}\n{traceback.format_exc()}")
class ResultTabWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.layout = QVBoxLayout(self)
# 图像显示区域
self.image_label = QLabel()
self.image_label.setAlignment(Qt.AlignCenter)
self.image_label.setMinimumSize(800, 600)
self.image_label.setStyleSheet("background-color: white; border-radius: 8px;")
# 摘要信息
self.summary_text = QTextEdit()
self.summary_text.setReadOnly(True)
self.summary_text.setStyleSheet("font-family: Consolas; font-size: 13px;")
self.layout.addWidget(self.image_label, stretch=3)
self.layout.addWidget(self.summary_text, stretch=1)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.models = {}
self.current_image_path = None
self.results = {}
self.danger_classes = ['knife', 'gun', 'explosive'] # 统一使用小写
self.alarm_player = QMediaPlayer()
self.alarm_enabled = True # 报警声音默认开启
self.init_ui()
self.init_status_bar()
self.init_alarm_system()
def init_alarm_system(self):
"""初始化报警音频系统"""
self.alarm_player.setMedia(QMediaContent(QUrl.fromLocalFile("alarm.wav")))
self.alarm_player.setVolume(80)
self.alarm_player.stateChanged.connect(self.handle_alarm_state)
def init_ui(self):
self.setWindowTitle("安检管制物品检测系统")
self.setWindowIcon(QIcon("icon.png"))
self.setGeometry(100, 100, 1600, 900)
self.setStyleSheet(STYLE_SHEET)
# 主布局
main_widget = QWidget()
self.main_layout = QHBoxLayout(main_widget)
self.main_layout.setContentsMargins(15, 15, 15, 15)
self.main_layout.setSpacing(20)
# 左侧控制面板 (30%)
self.control_panel = self.create_control_panel()
self.main_layout.addWidget(self.control_panel, stretch=3)
# 右侧显示区域 (70%)
self.display_area = self.create_display_area()
self.main_layout.addWidget(self.display_area, stretch=7)
self.setCentralWidget(main_widget)
def create_control_panel(self):
panel = QScrollArea()
panel.setWidgetResizable(True)
content = QWidget()
layout = QVBoxLayout(content)
layout.setContentsMargins(12, 12, 12, 12)
layout.setSpacing(15)
# 图片管理组
image_group = QGroupBox("文件管理")
image_layout = QVBoxLayout()
self.image_label = QLabel("当前图片:未选择")
self.select_image_btn = QPushButton("📁 选择图片")
self.select_image_btn.clicked.connect(self.select_image)
image_layout.addWidget(self.image_label)
image_layout.addWidget(self.select_image_btn)
image_group.setLayout(image_layout)
layout.addWidget(image_group)
# 模型管理组
model_group = QGroupBox("模型管理")
model_layout = QVBoxLayout()
self.model_combo = QComboBox()
self.load_model_btn = QPushButton("🔄 加载模型")
self.load_model_btn.clicked.connect(self.load_model)
self.detect_btn = QPushButton("🔍 开始检测")
self.detect_btn.clicked.connect(self.start_detection)
self.detect_btn.setEnabled(False)
model_layout.addWidget(QLabel("已加载模型列表:"))
model_layout.addWidget(self.model_combo)
model_layout.addWidget(self.load_model_btn)
model_layout.addWidget(self.detect_btn)
model_group.setLayout(model_layout)
layout.addWidget(model_group)
# 系统信息组
info_group = QGroupBox("系统状态")
info_layout = QVBoxLayout()
# 报警状态指示
self.alert_layout = QHBoxLayout()
self.led_label = QLabel()
self.led_label.setFixedSize(20, 20)
self.led_label.setStyleSheet("background-color: green; border-radius: 10px; border: 1px solid #666;")
self.alert_status_label = QLabel("状态:正常")
self.alert_status_label.setStyleSheet("color: green; font-weight: bold;")
self.alert_layout.addWidget(self.led_label)
self.alert_layout.addWidget(self.alert_status_label)
self.alert_layout.addStretch()
# 报警声音控制
self.alarm_control = QCheckBox("启用报警声音")
self.alarm_control.setChecked(True)
self.alarm_control.stateChanged.connect(self.toggle_alarm_sound)
self.model_count_label = QLabel("▪ 加载模型数: 0")
self.detect_count_label = QLabel("▪ 完成检测数: 0")
info_layout.addLayout(self.alert_layout)
info_layout.addWidget(self.alarm_control)
info_layout.addWidget(self.model_count_label)
info_layout.addWidget(self.detect_count_label)
info_group.setLayout(info_layout)
layout.addWidget(info_group)
layout.addStretch()
panel.setWidget(content)
return panel
def create_display_area(self):
tab_widget = QTabWidget()
tab_widget.setTabPosition(QTabWidget.North)
tab_widget.setStyleSheet("QTabBar::tab { height: 32px; }")
# 原始图像标签页
self.original_tab = ResultTabWidget()
tab_widget.addTab(self.original_tab, "原始图像")
# 检测结果标签页
self.result_tabs = QTabWidget()
self.result_tabs.setTabsClosable(True)
self.result_tabs.tabCloseRequested.connect(self.close_result_tab)
tab_widget.addTab(self.result_tabs, "检测结果")
# 对比分析标签页
self.compare_tab = QWidget()
compare_layout = QVBoxLayout()
self.compare_table = QTableWidget()
self.compare_table.setColumnCount(5)
self.compare_table.setHorizontalHeaderLabels(["模型", "检测数", "平均置信度", "最大置信度", "耗时(s)"])
self.compare_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
compare_layout.addWidget(self.compare_table)
self.compare_tab.setLayout(compare_layout)
tab_widget.addTab(self.compare_tab, "对比分析")
return tab_widget
def init_status_bar(self):
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)
self.status_bar.showMessage("就绪")
def update_status(self, message, success=True):
color = "#27AE60" if success else "#E74C3C"
self.status_bar.setStyleSheet(f"color: {color};")
self.status_bar.showMessage(f"▸ {message}")
def select_image(self):
path, _ = QFileDialog.getOpenFileName(
self, "选择图片", "",
"图片文件 (*.jpg *.jpeg *.png);;所有文件 (*)"
)
if path:
self.current_image_path = path
try:
image = cv2.imread(path)
if image is not None:
rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
self.display_image(rgb_image, self.original_tab.image_label)
self.image_label.setText(f"当前图片: {os.path.basename(path)}")
self.detect_btn.setEnabled(True)
self.update_status("图片加载成功")
self.clear_results()
else:
raise ValueError("无法读取图片文件")
except Exception as e:
self.update_status(f"图片加载失败: {str(e)}", False)
self.current_image_path = None
self.detect_btn.setEnabled(False)
def load_model(self):
try:
path, _ = QFileDialog.getOpenFileName(self, "选择模型文件", "", "模型文件 (*.pt)")
if path:
model_name = os.path.basename(path)
if model_name not in self.models:
self.models[model_name] = YOLO(path)
self.model_combo.addItem(model_name)
self.model_count_label.setText(f"▪ 加载模型数: {len(self.models)}")
self.update_status(f"模型 {model_name} 加载成功")
except Exception as e:
self.update_status(f"模型加载失败: {str(e)}", False)
def start_detection(self):
if not self.current_image_path:
self.update_status("请先选择图片", False)
return
if self.model_combo.count() == 0:
self.update_status("请先加载模型", False)
return
model_name = self.model_combo.currentText()
self.detect_thread = DetectionThread(self.models[model_name],
self.current_image_path,
model_name)
self.detect_thread.detection_finished.connect(self.handle_detection_result)
self.detect_thread.error_occurred.connect(self.handle_detection_error)
self.detect_thread.start()
self.update_status(f"开始使用 {model_name} 进行检测...")
def handle_detection_result(self, data):
model_name = data["model"]
self.results[model_name] = data
# 创建新的结果标签页
tab = ResultTabWidget()
self.display_image(data["annotated"], tab.image_label)
tab.summary_text.setText(self.generate_single_summary(data))
# 添加到结果标签页
self.result_tabs.addTab(tab, f"{model_name}")
self.result_tabs.setCurrentIndex(self.result_tabs.count() - 1)
# 更新对比分析
self.update_compare_table()
# 更新检测计数
self.detect_count_label.setText(f"▪ 完成检测数: {len(self.results)}")
self.update_status(f"{model_name} 检测完成,耗时 {data['time']:.2f}s")
# 检查危险状态
self.check_danger_status()
def update_compare_table(self):
self.compare_table.setRowCount(len(self.results))
for row, (model_name, data) in enumerate(self.results.items()):
confidences = [d["confidence"] for d in data["results"]]
avg_conf = sum(confidences) / len(confidences) if confidences else 0
max_conf = max(confidences) if confidences else 0
items = [
QTableWidgetItem(model_name),
QTableWidgetItem(str(len(data["results"]))),
QTableWidgetItem(f"{avg_conf:.2%}"),
QTableWidgetItem(f"{max_conf:.2%}"),
QTableWidgetItem(f"{data['time']:.2f}")
]
for col, item in enumerate(items):
self.compare_table.setItem(row, col, item)
def generate_single_summary(self, data):
summary = {}
for item in data["results"]:
class_name = item["class"]
if class_name not in summary:
summary[class_name] = {"count": 0, "confidences": []}
summary[class_name]["count"] += 1
summary[class_name]["confidences"].append(item["confidence"])
text = "======== 检测报告 ========\n"
text += f"▪ 模型名称: {data['model']}\n"
text += f"▪ 检测时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
text += f"▪ 总检测数: {len(data['results'])}\n"
text += f"▪ 处理耗时: {data['time']:.2f}s\n\n"
text += "======== 类别详情 ========\n"
for idx, (class_name, info) in enumerate(summary.items(), 1):
avg = sum(info["confidences"]) / len(info["confidences"])
text += (f"{idx}. {class_name}\n"
f" → 数量: {info['count']}\n"
f" → 平均置信度: {avg:.2%}\n"
f" → 最大置信度: {max(info['confidences']):.2%}\n\n")
return text
def display_image(self, image_data, label_widget):
try:
if image_data is None:
raise ValueError("空图像数据")
h, w, ch = image_data.shape
bytes_per_line = ch * w
q_img = QImage(image_data.data, w, h, bytes_per_line, QImage.Format_RGB888)
pixmap = QPixmap.fromImage(q_img).scaled(
label_widget.width(), label_widget.height(),
Qt.KeepAspectRatio, Qt.SmoothTransformation
)
label_widget.setPixmap(pixmap)
except Exception as e:
self.update_status(f"图像显示失败: {str(e)}", False)
def close_result_tab(self, index):
if index > 0: # 保留第一个默认标签页
widget = self.result_tabs.widget(index)
model_name = self.result_tabs.tabText(index)
del self.results[model_name]
widget.deleteLater()
self.result_tabs.removeTab(index)
self.update_compare_table()
self.check_danger_status()
def clear_results(self):
while self.result_tabs.count() > 1:
self.result_tabs.removeTab(1)
self.results.clear()
self.compare_table.setRowCount(0)
self.check_danger_status()
def handle_detection_error(self, error_msg):
self.update_status(error_msg.split("\n")[0], False)
QMessageBox.critical(self, "检测错误", error_msg)
def check_danger_status(self):
"""检查所有检测结果中是否存在危险物品"""
danger_detected = False
for model_data in self.results.values():
for item in model_data["results"]:
if item["class"].lower() in self.danger_classes: # 统一转为小写比较
danger_detected = True
break
if danger_detected:
break
self.update_alert_status(danger_detected)
def update_alert_status(self, danger_detected):
"""更新报警状态显示"""
if danger_detected:
# 更新视觉状态
self.led_label.setStyleSheet("background-color: red; border-radius: 10px; border: 1px solid #666;")
self.alert_status_label.setText("状态:报警")
self.alert_status_label.setStyleSheet("color: red; font-weight: bold;")
# 播放报警音
if self.alarm_enabled and self.alarm_player.state() != QMediaPlayer.PlayingState:
self.alarm_player.play()
else:
# 更新视觉状态
self.led_label.setStyleSheet("background-color: green; border-radius: 10px; border: 1px solid #666;")
self.alert_status_label.setText("状态:正常")
self.alert_status_label.setStyleSheet("color: green; font-weight: bold;")
# 停止报警音
if self.alarm_player.state() == QMediaPlayer.PlayingState:
self.alarm_player.stop()
def toggle_alarm_sound(self, state):
"""切换报警声音开关"""
self.alarm_enabled = (state == Qt.Checked)
if not self.alarm_enabled and self.alarm_player.state() == QMediaPlayer.PlayingState:
self.alarm_player.stop()
def handle_alarm_state(self, state):
"""处理音频播放状态变化"""
if state == QMediaPlayer.PlayingState:
self.alarm_player.setPosition(0) # 循环播放
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
这是我的ui界面代码,我觉得它看起来太简陋了,我想给它增加一个背景图片,不影响到其他功能模块的使用,最后请给出修改后的完整的代码