""" Lemontropia Suite - Vision Calibration Dialog Wizard for calibrating Game Vision AI to user's game setup. """ import sys import time from pathlib import Path from typing import Optional, List, Dict, Any from PyQt6.QtWidgets import ( QWizard, QWizardPage, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QComboBox, QCheckBox, QProgressBar, QGroupBox, QFormLayout, QTextEdit, QMessageBox, QFileDialog, QListWidget, QListWidgetItem, QSpinBox, QDoubleSpinBox, QWidget ) from PyQt6.QtCore import Qt, QSettings, QThread, pyqtSignal from PyQt6.QtGui import QFont, QPixmap, QImage import numpy as np import logging logger = logging.getLogger(__name__) class CalibrationWorker(QThread): """Background worker for calibration processing.""" progress = pyqtSignal(int, str) # percentage, message calibration_complete = pyqtSignal(dict) error_occurred = pyqtSignal(str) def __init__(self, screenshot_paths: List[Path], settings: Dict[str, Any]): super().__init__() self.screenshot_paths = screenshot_paths self.settings = settings self._cancelled = False def run(self): try: from modules.game_vision_ai import GameVisionAI self.progress.emit(0, "Initializing Game Vision AI...") vision = GameVisionAI( use_gpu=self.settings.get('use_gpu', True), ocr_lang=self.settings.get('ocr_lang', 'en') ) results = { 'screenshots_processed': 0, 'text_regions_detected': 0, 'icons_detected': 0, 'processing_times': [], 'errors': [], 'detected_regions': {}, 'sample_extractions': [] } total = len(self.screenshot_paths) for i, screenshot_path in enumerate(self.screenshot_paths): if self._cancelled: self.error_occurred.emit("Calibration cancelled") return progress = int((i / total) * 100) self.progress.emit(progress, f"Processing {screenshot_path.name}...") try: start_time = time.time() result = vision.process_screenshot( screenshot_path, extract_text=self.settings.get('extract_text', True), extract_icons=self.settings.get('extract_icons', True) ) processing_time = (time.time() - start_time) * 1000 results['screenshots_processed'] += 1 results['text_regions_detected'] += len(result.text_regions) results['icons_detected'] += len(result.icon_regions) results['processing_times'].append(processing_time) # Store sample extractions if i < 3: # Store first 3 as samples sample = { 'screenshot': str(screenshot_path), 'text_count': len(result.text_regions), 'icon_count': len(result.icon_regions), 'processing_time_ms': result.processing_time_ms, 'text_samples': [ {'text': t.text, 'confidence': t.confidence} for t in result.text_regions[:5] # First 5 texts ] } results['sample_extractions'].append(sample) except Exception as e: results['errors'].append(f"{screenshot_path.name}: {str(e)}") logger.error(f"Failed to process {screenshot_path}: {e}") # Calculate statistics if results['processing_times']: results['avg_processing_time'] = np.mean(results['processing_times']) results['min_processing_time'] = np.min(results['processing_times']) results['max_processing_time'] = np.max(results['processing_times']) self.progress.emit(100, "Calibration complete!") self.calibration_complete.emit(results) except Exception as e: self.error_occurred.emit(str(e)) def cancel(self): self._cancelled = True class WelcomePage(QWizardPage): """Welcome page of calibration wizard.""" def __init__(self, parent=None): super().__init__(parent) self.setTitle("Vision Calibration Wizard") self.setSubTitle("Calibrate Game Vision AI for your game setup") self.setup_ui() def setup_ui(self): layout = QVBoxLayout(self) welcome_label = QLabel( "

Welcome to Vision Calibration

" "

This wizard will help you calibrate the Game Vision AI system " "for optimal performance with your Entropia Universe setup.

" "

You will need:

" "" ) welcome_label.setWordWrap(True) layout.addWidget(welcome_label) # Info box info_group = QGroupBox("What will be calibrated?") info_layout = QVBoxLayout(info_group) info_text = QLabel( "" ) info_text.setWordWrap(True) info_layout.addWidget(info_text) layout.addWidget(info_group) layout.addStretch() class ScreenshotSelectionPage(QWizardPage): """Page for selecting sample screenshots.""" def __init__(self, parent=None): super().__init__(parent) self.setTitle("Select Sample Screenshots") self.setSubTitle("Choose screenshots from your game for calibration") self.screenshot_paths: List[Path] = [] self.setup_ui() def setup_ui(self): layout = QVBoxLayout(self) # Instructions instructions = QLabel( "Select 3-10 screenshots that represent typical game situations:\n" "• Loot windows with items\n" "• Inventory screens\n" "• Chat windows with text\n" "• HUD with gear equipped" ) instructions.setWordWrap(True) layout.addWidget(instructions) # File list list_group = QGroupBox("Selected Screenshots") list_layout = QVBoxLayout(list_group) self.file_list = QListWidget() list_layout.addWidget(self.file_list) # Buttons btn_layout = QHBoxLayout() self.add_btn = QPushButton("Add Screenshots...") self.add_btn.clicked.connect(self.add_screenshots) btn_layout.addWidget(self.add_btn) self.add_dir_btn = QPushButton("Add Directory...") self.add_dir_btn.clicked.connect(self.add_directory) btn_layout.addWidget(self.add_dir_btn) self.remove_btn = QPushButton("Remove Selected") self.remove_btn.clicked.connect(self.remove_selected) btn_layout.addWidget(self.remove_btn) self.clear_btn = QPushButton("Clear All") self.clear_btn.clicked.connect(self.clear_all) btn_layout.addWidget(self.clear_btn) btn_layout.addStretch() list_layout.addLayout(btn_layout) layout.addWidget(list_group) # Status self.status_label = QLabel("No screenshots selected") layout.addWidget(self.status_label) def add_screenshots(self): """Add individual screenshot files.""" files, _ = QFileDialog.getOpenFileNames( self, "Select Screenshots", str(Path.home()), "Images (*.png *.jpg *.jpeg *.bmp)" ) for file_path in files: path = Path(file_path) if path not in self.screenshot_paths: self.screenshot_paths.append(path) self.file_list.addItem(path.name) self.update_status() def add_directory(self): """Add all images from a directory.""" dir_path = QFileDialog.getExistingDirectory( self, "Select Screenshot Directory", str(Path.home()) ) if dir_path: path = Path(dir_path) for ext in ['*.png', '*.jpg', '*.jpeg', '*.bmp']: for file_path in path.glob(ext): if file_path not in self.screenshot_paths: self.screenshot_paths.append(file_path) self.file_list.addItem(file_path.name) self.update_status() def remove_selected(self): """Remove selected screenshots.""" selected = self.file_list.currentRow() if selected >= 0: self.file_list.takeItem(selected) del self.screenshot_paths[selected] self.update_status() def clear_all(self): """Clear all screenshots.""" self.file_list.clear() self.screenshot_paths.clear() self.update_status() def update_status(self): """Update status label.""" count = len(self.screenshot_paths) if count == 0: self.status_label.setText("No screenshots selected") elif count < 3: self.status_label.setText(f"⚠️ {count} screenshot(s) selected (recommend at least 3)") else: self.status_label.setText(f"✅ {count} screenshot(s) selected") def validatePage(self) -> bool: """Validate page before proceeding.""" if len(self.screenshot_paths) < 1: QMessageBox.warning(self, "No Screenshots", "Please select at least one screenshot.") return False return True def get_screenshot_paths(self) -> List[Path]: """Get selected screenshot paths.""" return self.screenshot_paths class SettingsPage(QWizardPage): """Page for configuring calibration settings.""" def __init__(self, parent=None): super().__init__(parent) self.setTitle("Calibration Settings") self.setSubTitle("Configure vision processing options") self.setup_ui() def setup_ui(self): layout = QVBoxLayout(self) # GPU Settings gpu_group = QGroupBox("GPU Acceleration") gpu_layout = QFormLayout(gpu_group) self.use_gpu_cb = QCheckBox("Use GPU for processing") self.use_gpu_cb.setChecked(True) self.use_gpu_cb.setToolTip( "Enable GPU acceleration for faster processing" ) gpu_layout.addRow(self.use_gpu_cb) self.gpu_info_label = QLabel("GPU info will be detected during calibration") gpu_layout.addRow("GPU:", self.gpu_info_label) layout.addWidget(gpu_group) # OCR Settings ocr_group = QGroupBox("OCR (Text Recognition)") ocr_layout = QFormLayout(ocr_group) self.extract_text_cb = QCheckBox("Enable text extraction") self.extract_text_cb.setChecked(True) ocr_layout.addRow(self.extract_text_cb) self.ocr_lang_combo = QComboBox() self.ocr_lang_combo.addItem("English", "en") self.ocr_lang_combo.addItem("Swedish", "sv") ocr_layout.addRow("Language:", self.ocr_lang_combo) layout.addWidget(ocr_group) # Icon Settings icon_group = QGroupBox("Icon Detection") icon_layout = QFormLayout(icon_group) self.extract_icons_cb = QCheckBox("Enable icon extraction") self.extract_icons_cb.setChecked(True) icon_layout.addRow(self.extract_icons_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") icon_layout.addRow("Icon Size:", self.icon_size_combo) self.auto_detect_window_cb = QCheckBox("Auto-detect loot windows") self.auto_detect_window_cb.setChecked(True) icon_layout.addRow(self.auto_detect_window_cb) layout.addWidget(icon_group) layout.addStretch() def get_settings(self) -> Dict[str, Any]: """Get calibration settings.""" return { 'use_gpu': self.use_gpu_cb.isChecked(), 'extract_text': self.extract_text_cb.isChecked(), 'extract_icons': self.extract_icons_cb.isChecked(), 'ocr_lang': self.ocr_lang_combo.currentData(), 'icon_size': self.icon_size_combo.currentData(), 'auto_detect_window': self.auto_detect_window_cb.isChecked() } class ProcessingPage(QWizardPage): """Page for running calibration processing.""" def __init__(self, parent=None): super().__init__(parent) self.setTitle("Processing") self.setSubTitle("Running calibration...") self.is_complete = False self.calibration_results: Optional[Dict] = None self.setup_ui() def setup_ui(self): layout = QVBoxLayout(self) # Progress self.status_label = QLabel("Ready to start calibration") layout.addWidget(self.status_label) self.progress_bar = QProgressBar() self.progress_bar.setRange(0, 100) self.progress_bar.setValue(0) layout.addWidget(self.progress_bar) # Results area self.results_text = QTextEdit() self.results_text.setReadOnly(True) self.results_text.setPlaceholderText("Calibration results will appear here...") layout.addWidget(self.results_text) # Buttons btn_layout = QHBoxLayout() self.start_btn = QPushButton("Start Calibration") self.start_btn.clicked.connect(self.start_calibration) btn_layout.addWidget(self.start_btn) self.cancel_btn = QPushButton("Cancel") self.cancel_btn.clicked.connect(self.cancel_calibration) self.cancel_btn.setEnabled(False) btn_layout.addWidget(self.cancel_btn) btn_layout.addStretch() layout.addLayout(btn_layout) def initializePage(self): """Called when page is shown.""" self.results_text.clear() self.progress_bar.setValue(0) self.status_label.setText("Ready to start calibration") self.is_complete = False self.start_btn.setEnabled(True) def start_calibration(self): """Start calibration processing.""" wizard = self.wizard() screenshot_page = wizard.page(1) # ScreenshotSelectionPage settings_page = wizard.page(2) # SettingsPage screenshot_paths = screenshot_page.get_screenshot_paths() settings = settings_page.get_settings() if not screenshot_paths: QMessageBox.warning(self, "No Screenshots", "No screenshots selected!") return self.start_btn.setEnabled(False) self.cancel_btn.setEnabled(True) self.status_label.setText("Starting calibration...") # Start worker thread self.worker = CalibrationWorker(screenshot_paths, settings) self.worker.progress.connect(self.on_progress) self.worker.calibration_complete.connect(self.on_complete) self.worker.error_occurred.connect(self.on_error) self.worker.start() def on_progress(self, percentage: int, message: str): """Handle progress update.""" self.progress_bar.setValue(percentage) self.status_label.setText(message) self.results_text.append(message) def on_complete(self, results: Dict): """Handle calibration completion.""" self.calibration_results = results self.is_complete = True self.cancel_btn.setEnabled(False) # Display results summary = f""" Calibration Complete! Screenshots processed: {results['screenshots_processed']} Text regions detected: {results['text_regions_detected']} Icons detected: {results['icons_detected']} """ if 'avg_processing_time' in results: summary += f"Average processing time: {results['avg_processing_time']:.1f}ms\n" if results.get('errors'): summary += f"\nErrors: {len(results['errors'])}" self.results_text.append(summary) # Enable next button self.completeChanged.emit() def on_error(self, error: str): """Handle calibration error.""" self.status_label.setText(f"Error: {error}") self.results_text.append(f"❌ Error: {error}") self.start_btn.setEnabled(True) self.cancel_btn.setEnabled(False) def cancel_calibration(self): """Cancel calibration.""" if hasattr(self, 'worker'): self.worker.cancel() self.status_label.setText("Cancelling...") def isComplete(self) -> bool: return self.is_complete def get_results(self) -> Optional[Dict]: """Get calibration results.""" return self.calibration_results class ResultsPage(QWizardPage): """Final page showing calibration results.""" def __init__(self, parent=None): super().__init__(parent) self.setTitle("Calibration Results") self.setSubTitle("Review and save calibration results") self.setup_ui() def setup_ui(self): layout = QVBoxLayout(self) self.results_label = QLabel("Processing results will appear here...") self.results_label.setWordWrap(True) layout.addWidget(self.results_label) # Recommendations self.recommendations_label = QLabel("") self.recommendations_label.setWordWrap(True) layout.addWidget(self.recommendations_label) layout.addStretch() def initializePage(self): """Called when page is shown.""" wizard = self.wizard() processing_page = wizard.page(3) # ProcessingPage results = processing_page.get_results() if results: # Format results text = f"""

Calibration Results

Processing Summary:

""" if 'avg_processing_time' in results: text += f"""

Performance:

""" self.results_label.setText(text) # Generate recommendations recommendations = self._generate_recommendations(results) self.recommendations_label.setText(recommendations) # Save results to settings self._save_calibration_results(results) def _generate_recommendations(self, results: Dict) -> str: """Generate calibration recommendations.""" recs = ["

Recommendations

") return "".join(recs) def _save_calibration_results(self, results: Dict): """Save calibration results to settings.""" settings = QSettings("Lemontropia", "GameVision") settings.setValue("calibration/last_run", time.time()) settings.setValue("calibration/screenshots_processed", results['screenshots_processed']) settings.setValue("calibration/avg_processing_time", results.get('avg_processing_time', 0)) settings.setValue("calibration/text_detection_rate", results['text_regions_detected']) settings.setValue("calibration/icon_detection_rate", results['icons_detected']) settings.sync() class VisionCalibrationWizard(QWizard): """ Wizard for calibrating Game Vision AI. """ calibration_complete = pyqtSignal(dict) def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("Vision Calibration Wizard") self.setMinimumSize(700, 550) # Add pages self.addPage(WelcomePage()) self.addPage(ScreenshotSelectionPage()) self.addPage(SettingsPage()) self.addPage(ProcessingPage()) self.addPage(ResultsPage()) self.setWizardStyle(QWizard.WizardStyle.ModernStyle) def accept(self): """Handle wizard completion.""" processing_page = self.page(3) results = processing_page.get_results() if results: self.calibration_complete.emit(results) super().accept() # Export __all__ = ['VisionCalibrationWizard', 'CalibrationWorker']