Lemontropia-Suite/ui/vision_calibration_dialog.py

629 lines
23 KiB
Python

"""
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(
"<h2>Welcome to Vision Calibration</h2>"
"<p>This wizard will help you calibrate the Game Vision AI system "
"for optimal performance with your Entropia Universe setup.</p>"
"<p>You will need:</p>"
"<ul>"
"<li>A few sample screenshots from the game</li>"
"<li>Screenshots should include: loot windows, inventory, chat</li>"
"<li>About 2-5 minutes to complete</li>"
"</ul>"
)
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(
"<ul>"
"<li><b>OCR Accuracy:</b> Text detection confidence and parameters</li>"
"<li><b>Icon Detection:</b> Loot window and item icon recognition</li>"
"<li><b>Performance:</b> Processing time optimization</li>"
"<li><b>GPU Setup:</b> Verify GPU acceleration is working</li>"
"</ul>"
)
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"""
<b>Calibration Complete!</b>
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"""
<h3>Calibration Results</h3>
<p><b>Processing Summary:</b></p>
<ul>
<li>Screenshots processed: {results['screenshots_processed']}</li>
<li>Text regions detected: {results['text_regions_detected']}</li>
<li>Icons detected: {results['icons_detected']}</li>
</ul>
"""
if 'avg_processing_time' in results:
text += f"""
<p><b>Performance:</b></p>
<ul>
<li>Average processing time: {results['avg_processing_time']:.1f}ms</li>
<li>Min processing time: {results['min_processing_time']:.1f}ms</li>
<li>Max processing time: {results['max_processing_time']:.1f}ms</li>
</ul>
"""
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 = ["<h3>Recommendations</h3><ul>"]
# Performance recommendations
if 'avg_processing_time' in results:
avg_time = results['avg_processing_time']
if avg_time < 100:
recs.append("<li>✅ Excellent performance! GPU acceleration is working well.</li>")
elif avg_time < 500:
recs.append("<li>✅ Good performance. Processing is reasonably fast.</li>")
else:
recs.append("<li>⚠️ Processing is slow. Consider enabling GPU or reducing screenshot resolution.</li>")
# Detection recommendations
total_regions = results['text_regions_detected'] + results['icons_detected']
if total_regions == 0:
recs.append("<li>⚠️ No text or icons detected. Check screenshot quality and game UI visibility.</li>")
elif results['text_regions_detected'] == 0:
recs.append("<li>⚠️ No text detected. Try adjusting OCR thresholds or check image clarity.</li>")
elif results['icons_detected'] == 0:
recs.append("<li>⚠️ No icons detected. Ensure screenshots include loot windows.</li>")
else:
recs.append("<li>✅ Detection is working. Text and icons are being recognized.</li>")
recs.append("</ul>")
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']