Lemontropia-Suite/ui/vision_settings_dialog.py

646 lines
24 KiB
Python

"""
Lemontropia Suite - Vision Settings Dialog
Settings panel for configuring Game Vision AI.
"""
import sys
from pathlib import Path
from typing import Optional
from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
QPushButton, QComboBox, QCheckBox, QGroupBox, QFormLayout,
QMessageBox, QSpinBox, QDoubleSpinBox, QTabWidget,
QFileDialog, QTextEdit, QProgressBar, QWidget, QSlider
)
from PyQt6.QtCore import Qt, QSettings, QThread, pyqtSignal
from PyQt6.QtGui import QFont, QPixmap
import logging
logger = logging.getLogger(__name__)
class GPUInfoThread(QThread):
"""Thread to gather GPU information."""
info_ready = pyqtSignal(dict)
error_occurred = pyqtSignal(str)
def run(self):
try:
from modules.game_vision_ai import GPUDetector
info = GPUDetector.get_gpu_info()
self.info_ready.emit(info)
except Exception as e:
self.error_occurred.emit(str(e))
class VisionSettingsDialog(QDialog):
"""
Settings dialog for Game Vision AI configuration.
"""
settings_saved = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Game Vision Settings")
self.setMinimumSize(600, 500)
self.settings = QSettings("Lemontropia", "GameVision")
self.gpu_info = {}
self.setup_ui()
self.load_settings()
self.refresh_gpu_info()
def setup_ui(self):
"""Setup the dialog UI."""
layout = QVBoxLayout(self)
layout.setSpacing(15)
# Title
title_label = QLabel("🎮 Game Vision AI Settings")
title_font = QFont()
title_font.setPointSize(14)
title_font.setBold(True)
title_label.setFont(title_font)
layout.addWidget(title_label)
# Description
desc_label = QLabel(
"Configure AI-powered computer vision for automatic game UI analysis."
)
desc_label.setWordWrap(True)
layout.addWidget(desc_label)
# Tabs
self.tabs = QTabWidget()
layout.addWidget(self.tabs)
# General tab
self.tabs.addTab(self._create_general_tab(), "General")
# GPU tab
self.tabs.addTab(self._create_gpu_tab(), "GPU & Performance")
# OCR tab
self.tabs.addTab(self._create_ocr_tab(), "OCR Settings")
# Icon Detection tab
self.tabs.addTab(self._create_icon_tab(), "Icon Detection")
# Buttons
button_layout = QHBoxLayout()
button_layout.addStretch()
self.reset_btn = QPushButton("Reset to Defaults")
self.reset_btn.clicked.connect(self.reset_settings)
button_layout.addWidget(self.reset_btn)
self.test_btn = QPushButton("Test Vision...")
self.test_btn.clicked.connect(self.open_test_dialog)
button_layout.addWidget(self.test_btn)
self.save_btn = QPushButton("Save")
self.save_btn.clicked.connect(self.save_settings)
self.save_btn.setDefault(True)
button_layout.addWidget(self.save_btn)
self.cancel_btn = QPushButton("Cancel")
self.cancel_btn.clicked.connect(self.reject)
button_layout.addWidget(self.cancel_btn)
layout.addLayout(button_layout)
def _create_general_tab(self) -> QWidget:
"""Create general settings tab."""
tab = QWidget()
layout = QVBoxLayout(tab)
layout.setSpacing(15)
# Enable Vision
self.enable_vision_cb = QCheckBox("Enable Game Vision AI")
self.enable_vision_cb.setToolTip(
"Enable automatic screenshot analysis using AI"
)
layout.addWidget(self.enable_vision_cb)
# Auto Processing
self.auto_process_cb = QCheckBox("Auto-process screenshots")
self.auto_process_cb.setToolTip(
"Automatically analyze screenshots when captured"
)
layout.addWidget(self.auto_process_cb)
# Data Directory
dir_group = QGroupBox("Data Directories")
dir_layout = QFormLayout(dir_group)
# Extracted icons directory
icons_dir_layout = QHBoxLayout()
self.icons_dir_input = QLineEdit()
self.icons_dir_input.setReadOnly(True)
icons_dir_layout.addWidget(self.icons_dir_input)
self.icons_dir_btn = QPushButton("Browse...")
self.icons_dir_btn.clicked.connect(self.browse_icons_dir)
icons_dir_layout.addWidget(self.icons_dir_btn)
dir_layout.addRow("Extracted Icons:", icons_dir_layout)
# Icon database directory
db_dir_layout = QHBoxLayout()
self.db_dir_input = QLineEdit()
self.db_dir_input.setReadOnly(True)
db_dir_layout.addWidget(self.db_dir_input)
self.db_dir_btn = QPushButton("Browse...")
self.db_dir_btn.clicked.connect(self.browse_db_dir)
db_dir_layout.addWidget(self.db_dir_btn)
dir_layout.addRow("Icon Database:", db_dir_layout)
layout.addWidget(dir_group)
# Processing Options
options_group = QGroupBox("Processing Options")
options_layout = QFormLayout(options_group)
self.extract_text_cb = QCheckBox("Extract text (OCR)")
self.extract_text_cb.setChecked(True)
options_layout.addRow(self.extract_text_cb)
self.extract_icons_cb = QCheckBox("Extract icons")
self.extract_icons_cb.setChecked(True)
options_layout.addRow(self.extract_icons_cb)
self.save_icons_cb = QCheckBox("Save extracted icons to disk")
self.save_icons_cb.setChecked(True)
options_layout.addRow(self.save_icons_cb)
self.match_icons_cb = QCheckBox("Match icons to database")
self.match_icons_cb.setChecked(True)
options_layout.addRow(self.match_icons_cb)
layout.addWidget(options_group)
layout.addStretch()
return tab
def _create_gpu_tab(self) -> QWidget:
"""Create GPU settings tab."""
tab = QWidget()
layout = QVBoxLayout(tab)
layout.setSpacing(15)
# GPU Info Group
gpu_group = QGroupBox("GPU Information")
gpu_layout = QVBoxLayout(gpu_group)
self.gpu_info_label = QLabel("Detecting GPU...")
self.gpu_info_label.setWordWrap(True)
gpu_layout.addWidget(self.gpu_info_label)
self.gpu_details = QTextEdit()
self.gpu_details.setReadOnly(True)
self.gpu_details.setMaximumHeight(100)
gpu_layout.addWidget(self.gpu_details)
self.refresh_gpu_btn = QPushButton("Refresh GPU Info")
self.refresh_gpu_btn.clicked.connect(self.refresh_gpu_info)
gpu_layout.addWidget(self.refresh_gpu_btn)
layout.addWidget(gpu_group)
# GPU Settings
settings_group = QGroupBox("GPU Acceleration")
settings_layout = QFormLayout(settings_group)
self.use_gpu_cb = QCheckBox("Use GPU acceleration")
self.use_gpu_cb.setToolTip(
"Enable GPU acceleration for OCR and vision processing"
)
settings_layout.addRow(self.use_gpu_cb)
# GPU Backend selection
self.backend_combo = QComboBox()
self.backend_combo.addItem("Auto-detect", "auto")
self.backend_combo.addItem("CUDA (NVIDIA)", "cuda")
self.backend_combo.addItem("MPS (Apple Silicon)", "mps")
self.backend_combo.addItem("DirectML (Windows)", "directml")
self.backend_combo.addItem("CPU only", "cpu")
settings_layout.addRow("Preferred Backend:", self.backend_combo)
layout.addWidget(settings_group)
# Performance Settings
perf_group = QGroupBox("Performance")
perf_layout = QFormLayout(perf_group)
self.batch_size_spin = QSpinBox()
self.batch_size_spin.setRange(1, 16)
self.batch_size_spin.setValue(1)
self.batch_size_spin.setToolTip(
"Number of images to process in parallel (higher = faster but more VRAM)"
)
perf_layout.addRow("Batch Size:", self.batch_size_spin)
self.threads_spin = QSpinBox()
self.threads_spin.setRange(1, 8)
self.threads_spin.setValue(2)
perf_layout.addRow("Processing Threads:", self.threads_spin)
layout.addWidget(perf_group)
layout.addStretch()
return tab
def _create_ocr_tab(self) -> QWidget:
"""Create OCR settings tab."""
tab = QWidget()
layout = QVBoxLayout(tab)
layout.setSpacing(15)
# Language Settings
lang_group = QGroupBox("Language Settings")
lang_layout = QFormLayout(lang_group)
self.ocr_lang_combo = QComboBox()
self.ocr_lang_combo.addItem("English", "en")
self.ocr_lang_combo.addItem("Swedish", "sv")
self.ocr_lang_combo.addItem("Latin Script (Generic)", "latin")
lang_layout.addRow("OCR Language:", self.ocr_lang_combo)
self.multi_lang_cb = QCheckBox("Enable multi-language detection")
lang_layout.addRow(self.multi_lang_cb)
layout.addWidget(lang_group)
# OCR Parameters
params_group = QGroupBox("OCR Parameters")
params_layout = QFormLayout(params_group)
self.det_thresh_spin = QDoubleSpinBox()
self.det_thresh_spin.setRange(0.1, 0.9)
self.det_thresh_spin.setValue(0.3)
self.det_thresh_spin.setSingleStep(0.05)
self.det_thresh_spin.setToolTip(
"Text detection threshold (lower = more sensitive)"
)
params_layout.addRow("Detection Threshold:", self.det_thresh_spin)
self.rec_thresh_spin = QDoubleSpinBox()
self.rec_thresh_spin.setRange(0.1, 0.9)
self.rec_thresh_spin.setValue(0.5)
self.rec_thresh_spin.setSingleStep(0.05)
self.rec_thresh_spin.setToolTip(
"Text recognition confidence threshold"
)
params_layout.addRow("Recognition Threshold:", self.rec_thresh_spin)
self.use_angle_cls_cb = QCheckBox("Use angle classifier")
self.use_angle_cls_cb.setChecked(True)
self.use_angle_cls_cb.setToolTip(
"Detect and correct rotated text (slower but more accurate)"
)
params_layout.addRow(self.use_angle_cls_cb)
layout.addWidget(params_group)
# Preprocessing
preprocess_group = QGroupBox("Preprocessing")
preprocess_layout = QFormLayout(preprocess_group)
self.denoise_cb = QCheckBox("Apply denoising")
self.denoise_cb.setChecked(True)
preprocess_layout.addRow(self.denoise_cb)
self.contrast_enhance_cb = QCheckBox("Enhance contrast")
self.contrast_enhance_cb.setChecked(True)
preprocess_layout.addRow(self.contrast_enhance_cb)
layout.addWidget(preprocess_group)
layout.addStretch()
return tab
def _create_icon_tab(self) -> QWidget:
"""Create icon detection settings tab."""
tab = QWidget()
layout = QVBoxLayout(tab)
layout.setSpacing(15)
# Detection Settings
detect_group = QGroupBox("Detection Settings")
detect_layout = QFormLayout(detect_group)
self.auto_detect_window_cb = QCheckBox("Auto-detect loot windows")
self.auto_detect_window_cb.setChecked(True)
self.auto_detect_window_cb.setToolTip(
"Automatically detect loot windows in screenshots"
)
detect_layout.addRow(self.auto_detect_window_cb)
self.icon_size_combo = QComboBox()
self.icon_size_combo.addItem("Small (32x32)", "small")
self.icon_size_combo.addItem("Medium (48x48)", "medium")
self.icon_size_combo.addItem("Large (64x64)", "large")
self.icon_size_combo.addItem("HUD (40x40)", "hud")
detect_layout.addRow("Icon Size:", self.icon_size_combo)
self.confidence_thresh_spin = QDoubleSpinBox()
self.confidence_thresh_spin.setRange(0.1, 1.0)
self.confidence_thresh_spin.setValue(0.7)
self.confidence_thresh_spin.setSingleStep(0.05)
detect_layout.addRow("Detection Confidence:", self.confidence_thresh_spin)
layout.addWidget(detect_group)
# Matching Settings
match_group = QGroupBox("Icon Matching")
match_layout = QFormLayout(match_group)
self.hash_match_cb = QCheckBox("Use perceptual hashing")
self.hash_match_cb.setChecked(True)
match_layout.addRow(self.hash_match_cb)
self.feature_match_cb = QCheckBox("Use feature matching (ORB)")
self.feature_match_cb.setChecked(True)
match_layout.addRow(self.feature_match_cb)
self.template_match_cb = QCheckBox("Use template matching")
self.template_match_cb.setChecked(True)
match_layout.addRow(self.template_match_cb)
self.match_thresh_spin = QDoubleSpinBox()
self.match_thresh_spin.setRange(0.1, 1.0)
self.match_thresh_spin.setValue(0.70)
self.match_thresh_spin.setSingleStep(0.05)
self.match_thresh_spin.setToolTip(
"Minimum confidence for icon match"
)
match_layout.addRow("Match Threshold:", self.match_thresh_spin)
layout.addWidget(match_group)
# Template Directory
template_group = QGroupBox("Template Directory")
template_layout = QHBoxLayout(template_group)
self.template_dir_input = QLineEdit()
self.template_dir_input.setReadOnly(True)
template_layout.addWidget(self.template_dir_input)
self.template_dir_btn = QPushButton("Browse...")
self.template_dir_btn.clicked.connect(self.browse_template_dir)
template_layout.addWidget(self.template_dir_btn)
layout.addWidget(template_group)
layout.addStretch()
return tab
def refresh_gpu_info(self):
"""Refresh GPU information display."""
self.gpu_info_label.setText("Detecting GPU...")
self.refresh_gpu_btn.setEnabled(False)
self.gpu_thread = GPUInfoThread()
self.gpu_thread.info_ready.connect(self.on_gpu_info_ready)
self.gpu_thread.error_occurred.connect(self.on_gpu_error)
self.gpu_thread.start()
def on_gpu_info_ready(self, info: dict):
"""Handle GPU info received."""
self.gpu_info = info
backend = info.get('backend', 'cpu')
cuda_available = info.get('cuda_available', False)
mps_available = info.get('mps_available', False)
# Update label
if backend == 'cuda':
devices = info.get('devices', [])
if devices:
device_name = devices[0].get('name', 'Unknown')
memory_gb = devices[0].get('memory_total', 0) / (1024**3)
self.gpu_info_label.setText(
f"✅ GPU Detected: {device_name} ({memory_gb:.1f} GB)"
)
else:
self.gpu_info_label.setText("✅ CUDA Available")
elif backend == 'mps':
self.gpu_info_label.setText("✅ Apple MPS (Metal) Available")
elif backend == 'directml':
self.gpu_info_label.setText("✅ DirectML Available")
else:
self.gpu_info_label.setText("⚠️ No GPU detected - CPU only")
# Update details
details = f"Backend: {backend}\n"
details += f"CUDA Available: {cuda_available}\n"
details += f"MPS Available: {mps_available}\n"
if info.get('devices'):
for dev in info['devices']:
details += f"\nDevice {dev['id']}: {dev['name']}"
self.gpu_details.setText(details)
self.refresh_gpu_btn.setEnabled(True)
def on_gpu_error(self, error: str):
"""Handle GPU detection error."""
self.gpu_info_label.setText(f"❌ Error detecting GPU: {error}")
self.refresh_gpu_btn.setEnabled(True)
def browse_icons_dir(self):
"""Browse for extracted icons directory."""
dir_path = QFileDialog.getExistingDirectory(
self, "Select Extracted Icons Directory",
self.icons_dir_input.text() or str(Path.home())
)
if dir_path:
self.icons_dir_input.setText(dir_path)
def browse_db_dir(self):
"""Browse for database directory."""
dir_path = QFileDialog.getExistingDirectory(
self, "Select Database Directory",
self.db_dir_input.text() or str(Path.home())
)
if dir_path:
self.db_dir_input.setText(dir_path)
def browse_template_dir(self):
"""Browse for template directory."""
dir_path = QFileDialog.getExistingDirectory(
self, "Select Template Directory",
self.template_dir_input.text() or str(Path.home())
)
if dir_path:
self.template_dir_input.setText(dir_path)
def load_settings(self):
"""Load settings from QSettings."""
# General
self.enable_vision_cb.setChecked(
self.settings.value("vision/enabled", True, bool)
)
self.auto_process_cb.setChecked(
self.settings.value("vision/auto_process", False, bool)
)
self.icons_dir_input.setText(
self.settings.value("vision/icons_dir", "", str)
)
self.db_dir_input.setText(
self.settings.value("vision/db_dir", "", str)
)
self.extract_text_cb.setChecked(
self.settings.value("vision/extract_text", True, bool)
)
self.extract_icons_cb.setChecked(
self.settings.value("vision/extract_icons", True, bool)
)
self.save_icons_cb.setChecked(
self.settings.value("vision/save_icons", True, bool)
)
self.match_icons_cb.setChecked(
self.settings.value("vision/match_icons", True, bool)
)
# GPU
self.use_gpu_cb.setChecked(
self.settings.value("vision/use_gpu", True, bool)
)
backend = self.settings.value("vision/gpu_backend", "auto", str)
index = self.backend_combo.findData(backend)
if index >= 0:
self.backend_combo.setCurrentIndex(index)
self.batch_size_spin.setValue(
self.settings.value("vision/batch_size", 1, int)
)
self.threads_spin.setValue(
self.settings.value("vision/threads", 2, int)
)
# OCR
lang = self.settings.value("vision/ocr_lang", "en", str)
index = self.ocr_lang_combo.findData(lang)
if index >= 0:
self.ocr_lang_combo.setCurrentIndex(index)
self.multi_lang_cb.setChecked(
self.settings.value("vision/multi_lang", False, bool)
)
self.det_thresh_spin.setValue(
self.settings.value("vision/det_thresh", 0.3, float)
)
self.rec_thresh_spin.setValue(
self.settings.value("vision/rec_thresh", 0.5, float)
)
self.use_angle_cls_cb.setChecked(
self.settings.value("vision/use_angle_cls", True, bool)
)
self.denoise_cb.setChecked(
self.settings.value("vision/denoise", True, bool)
)
self.contrast_enhance_cb.setChecked(
self.settings.value("vision/contrast_enhance", True, bool)
)
# Icon Detection
self.auto_detect_window_cb.setChecked(
self.settings.value("vision/auto_detect_window", True, bool)
)
icon_size = self.settings.value("vision/icon_size", "medium", str)
index = self.icon_size_combo.findData(icon_size)
if index >= 0:
self.icon_size_combo.setCurrentIndex(index)
self.confidence_thresh_spin.setValue(
self.settings.value("vision/confidence_thresh", 0.7, float)
)
self.hash_match_cb.setChecked(
self.settings.value("vision/hash_match", True, bool)
)
self.feature_match_cb.setChecked(
self.settings.value("vision/feature_match", True, bool)
)
self.template_match_cb.setChecked(
self.settings.value("vision/template_match", True, bool)
)
self.match_thresh_spin.setValue(
self.settings.value("vision/match_thresh", 0.70, float)
)
self.template_dir_input.setText(
self.settings.value("vision/template_dir", "", str)
)
def save_settings(self):
"""Save settings to QSettings."""
# General
self.settings.setValue("vision/enabled", self.enable_vision_cb.isChecked())
self.settings.setValue("vision/auto_process", self.auto_process_cb.isChecked())
self.settings.setValue("vision/icons_dir", self.icons_dir_input.text())
self.settings.setValue("vision/db_dir", self.db_dir_input.text())
self.settings.setValue("vision/extract_text", self.extract_text_cb.isChecked())
self.settings.setValue("vision/extract_icons", self.extract_icons_cb.isChecked())
self.settings.setValue("vision/save_icons", self.save_icons_cb.isChecked())
self.settings.setValue("vision/match_icons", self.match_icons_cb.isChecked())
# GPU
self.settings.setValue("vision/use_gpu", self.use_gpu_cb.isChecked())
self.settings.setValue("vision/gpu_backend", self.backend_combo.currentData())
self.settings.setValue("vision/batch_size", self.batch_size_spin.value())
self.settings.setValue("vision/threads", self.threads_spin.value())
# OCR
self.settings.setValue("vision/ocr_lang", self.ocr_lang_combo.currentData())
self.settings.setValue("vision/multi_lang", self.multi_lang_cb.isChecked())
self.settings.setValue("vision/det_thresh", self.det_thresh_spin.value())
self.settings.setValue("vision/rec_thresh", self.rec_thresh_spin.value())
self.settings.setValue("vision/use_angle_cls", self.use_angle_cls_cb.isChecked())
self.settings.setValue("vision/denoise", self.denoise_cb.isChecked())
self.settings.setValue("vision/contrast_enhance", self.contrast_enhance_cb.isChecked())
# Icon Detection
self.settings.setValue("vision/auto_detect_window", self.auto_detect_window_cb.isChecked())
self.settings.setValue("vision/icon_size", self.icon_size_combo.currentData())
self.settings.setValue("vision/confidence_thresh", self.confidence_thresh_spin.value())
self.settings.setValue("vision/hash_match", self.hash_match_cb.isChecked())
self.settings.setValue("vision/feature_match", self.feature_match_cb.isChecked())
self.settings.setValue("vision/template_match", self.template_match_cb.isChecked())
self.settings.setValue("vision/match_thresh", self.match_thresh_spin.value())
self.settings.setValue("vision/template_dir", self.template_dir_input.text())
self.settings.sync()
self.settings_saved.emit()
self.accept()
logger.info("Vision settings saved")
def reset_settings(self):
"""Reset settings to defaults."""
reply = QMessageBox.question(
self, "Reset Settings",
"Are you sure you want to reset all vision settings to defaults?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
self.settings.clear()
self.load_settings()
QMessageBox.information(self, "Reset Complete",
"Settings have been reset to defaults.")
def open_test_dialog(self):
"""Open vision test dialog."""
from .vision_test_dialog import VisionTestDialog
dialog = VisionTestDialog(self)
dialog.exec()
# Export
__all__ = ['VisionSettingsDialog']