""" 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']