From 999bdfca3540a1a18e75f6549797b5032944633b Mon Sep 17 00:00:00 2001 From: LemonNexus Date: Sat, 14 Feb 2026 23:38:18 +0000 Subject: [PATCH] feat: Fix OCR errors and add backend installation/management BUG FIXES: 1. EasyOCR 'Invalid input type' error: - PIL Images must be converted to numpy arrays - Added np.array() conversion before passing to EasyOCR 2. PaddleOCR 'Unknown argument: show_log' error: - Some versions don't support show_log parameter - Added try/except to handle both versions - Falls back to creating PaddleOCR without show_log NEW FEATURES: 1. OCR Backend Manager (core/ocr_backend_manager.py): - Detects installed OCR backends - Checks Windows Registry for Tesseract installation - Searches common installation paths - Auto-configures pytesseract with found binary 2. Calibration Tab Improvements: - Added 'Install EasyOCR' button (pip install easyocr) - Added 'Install Tesseract Package' button (pip install pytesseract) - Added 'Auto-Detect Tesseract' button (checks registry/paths) - Added 'Install PaddleOCR' button (pip install paddleocr) - Shows which backends are available and where they're installed 3. Better Error Messages: - Clear instructions when backends are missing - Direct links to download pages - Shows detected installation paths The app now: - Automatically finds Tesseract from registry - Allows installing backends from within the UI - Properly handles all OCR input formats --- core/ocr_backend_manager.py | 209 +++++++++++++++++++++++++++++ plugins/game_reader_test/plugin.py | 188 ++++++++++++++++++++++---- 2 files changed, 374 insertions(+), 23 deletions(-) create mode 100644 core/ocr_backend_manager.py diff --git a/core/ocr_backend_manager.py b/core/ocr_backend_manager.py new file mode 100644 index 0000000..847e1db --- /dev/null +++ b/core/ocr_backend_manager.py @@ -0,0 +1,209 @@ +""" +EU-Utility - OCR Backend Manager + +Manages OCR backend installation and configuration. +Checks registry for installed software and provides auto-configuration. +""" + +import sys +import subprocess +from pathlib import Path +from typing import Optional, Dict, List, Tuple + +# Windows registry checking +if sys.platform == 'win32': + import winreg + WINDOWS = True +else: + WINDOWS = False + + +class OCRBackendManager: + """Manages OCR backend detection and installation.""" + + TESSERACT_REGISTRY_PATHS = [ + (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Tesseract-OCR"), + (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\WOW6432Node\Tesseract-OCR"), + (winreg.HKEY_CURRENT_USER, r"SOFTWARE\Tesseract-OCR"), + ] + + TESSERACT_DEFAULT_PATHS = [ + Path(r"C:\Program Files\Tesseract-OCR"), + Path(r"C:\Program Files (x86)\Tesseract-OCR"), + Path.home() / "Tesseract-OCR", + ] + + def __init__(self): + self.backends = { + 'easyocr': {'installed': False, 'available': False, 'path': None}, + 'tesseract': {'installed': False, 'available': False, 'path': None}, + 'paddleocr': {'installed': False, 'available': False, 'path': None}, + } + self._scan_backends() + + def _scan_backends(self): + """Scan for installed OCR backends.""" + self._check_easyocr() + self._check_tesseract() + self._check_paddleocr() + + def _check_easyocr(self): + """Check if EasyOCR Python package is installed.""" + try: + import easyocr + self.backends['easyocr']['installed'] = True + self.backends['easyocr']['available'] = True + except ImportError: + pass + + def _check_tesseract(self): + """Check if Tesseract is installed (Python package and binary).""" + # Check Python package + try: + import pytesseract + self.backends['tesseract']['installed'] = True + except ImportError: + pass + + # Check for binary in various locations + tesseract_exe = self._find_tesseract_binary() + if tesseract_exe: + self.backends['tesseract']['path'] = str(tesseract_exe) + self.backends['tesseract']['available'] = True + # Configure pytesseract if Python package is installed + try: + import pytesseract + pytesseract.pytesseract.tesseract_cmd = str(tesseract_exe) + except ImportError: + pass + + def _find_tesseract_binary(self) -> Optional[Path]: + """Find Tesseract binary in registry and common paths.""" + # Check if already in PATH + try: + result = subprocess.run(['tesseract', '--version'], + capture_output=True, text=True, timeout=5) + if result.returncode == 0: + # Tesseract is in PATH + return Path('tesseract') + except: + pass + + if WINDOWS: + # Check Windows Registry + for hkey, reg_path in self.TESSERACT_REGISTRY_PATHS: + try: + with winreg.OpenKey(hkey, reg_path) as key: + install_path, _ = winreg.QueryValueEx(key, 'InstallDir') + if install_path: + exe_path = Path(install_path) / 'tesseract.exe' + if exe_path.exists(): + return exe_path + except: + continue + + # Check default installation paths + for path in self.TESSERACT_DEFAULT_PATHS: + exe_path = path / 'tesseract.exe' + if exe_path.exists(): + return exe_path + + return None + + def _check_paddleocr(self): + """Check if PaddleOCR Python package is installed.""" + try: + from paddleocr import PaddleOCR + self.backends['paddleocr']['installed'] = True + self.backends['paddleocr']['available'] = True + except ImportError: + pass + + def get_backend_status(self, backend: str) -> Dict: + """Get status of a specific backend.""" + return self.backends.get(backend, {}).copy() + + def get_all_status(self) -> Dict: + """Get status of all backends.""" + return self.backends.copy() + + def get_best_available(self) -> Optional[str]: + """Get the best available backend.""" + priority = ['easyocr', 'tesseract', 'paddleocr'] + for backend in priority: + if self.backends[backend]['available']: + return backend + return None + + def install_backend(self, backend: str) -> Tuple[bool, str]: + """Install a backend via pip.""" + packages = { + 'easyocr': 'easyocr', + 'tesseract': 'pytesseract', + 'paddleocr': 'paddleocr', + } + + if backend not in packages: + return False, f"Unknown backend: {backend}" + + try: + result = subprocess.run( + [sys.executable, '-m', 'pip', 'install', packages[backend]], + capture_output=True, + text=True, + timeout=300 + ) + if result.returncode == 0: + self._scan_backends() # Re-scan after install + return True, f"Successfully installed {backend}" + else: + return False, f"Failed to install {backend}: {result.stderr}" + except Exception as e: + return False, f"Error installing {backend}: {e}" + + def get_tesseract_install_info(self) -> str: + """Get instructions for installing Tesseract.""" + return """Tesseract is not installed or not in PATH. + +To install Tesseract: + +1. Download installer from: + https://github.com/UB-Mannheim/tesseract/wiki + +2. Run the installer and note the installation path + (usually C:\Program Files\Tesseract-OCR) + +3. Add to PATH or use the auto-detect feature + +4. Restart EU-Utility + +Alternative: Use EasyOCR instead (pip install easyocr) +""" + + def auto_configure_tesseract(self) -> bool: + """Auto-configure Tesseract from registry/paths.""" + if not WINDOWS: + return False + + exe_path = self._find_tesseract_binary() + if exe_path and exe_path != Path('tesseract'): + try: + import pytesseract + pytesseract.pytesseract.tesseract_cmd = str(exe_path) + self.backends['tesseract']['available'] = True + self.backends['tesseract']['path'] = str(exe_path) + return True + except: + pass + return False + + +# Singleton instance +_ocr_manager = None + +def get_ocr_backend_manager() -> OCRBackendManager: + """Get the OCR backend manager singleton.""" + global _ocr_manager + if _ocr_manager is None: + _ocr_manager = OCRBackendManager() + return _ocr_manager diff --git a/plugins/game_reader_test/plugin.py b/plugins/game_reader_test/plugin.py index b6bc115..deeb1e2 100644 --- a/plugins/game_reader_test/plugin.py +++ b/plugins/game_reader_test/plugin.py @@ -59,11 +59,14 @@ class OCRTestThread(QThread): if self.backend in ('auto', 'easyocr'): try: import easyocr + import numpy as np self.progress_update.emit(50, "Loading EasyOCR...") reader = easyocr.Reader(['en'], gpu=False, verbose=False) self.progress_update.emit(70, "Processing with EasyOCR...") + # Convert PIL Image to numpy array + screenshot_np = np.array(screenshot) ocr_result = reader.readtext( - screenshot, + screenshot_np, detail=0, paragraph=True ) @@ -86,11 +89,19 @@ class OCRTestThread(QThread): if not text and self.backend in ('auto', 'paddle'): try: from paddleocr import PaddleOCR + import numpy as np self.progress_update.emit(70, "Processing with PaddleOCR...") - ocr = PaddleOCR(use_angle_cls=True, lang='en', show_log=False) - result = ocr.ocr(screenshot, cls=True) - texts = [line[1][0] for line in result[0]] - text = '\n'.join(texts) + # Try with show_log, fall back without for compatibility + try: + ocr = PaddleOCR(use_angle_cls=True, lang='en', show_log=False) + except TypeError: + ocr = PaddleOCR(use_angle_cls=True, lang='en') + # Convert PIL to numpy + screenshot_np = np.array(screenshot) + result = ocr.ocr(screenshot_np, cls=True) + if result and result[0]: + texts = [line[1][0] for line in result[0]] + text = '\n'.join(texts) backend_used = "paddleocr" except Exception as e: if self.backend == 'paddle': @@ -504,10 +515,62 @@ class GameReaderTestPlugin(BasePlugin): self.backend_status = QTextEdit() self.backend_status.setReadOnly(True) - self.backend_status.setMaximumHeight(150) + self.backend_status.setMaximumHeight(200) self._check_backends() backend_layout.addWidget(self.backend_status) + # Install buttons + install_layout = QHBoxLayout() + + install_easyocr_btn = QPushButton("šŸ“¦ Install EasyOCR") + install_easyocr_btn.setStyleSheet(""" + QPushButton { + background-color: #4ecdc4; + color: #141f23; + font-weight: bold; + padding: 8px; + } + """) + install_easyocr_btn.clicked.connect(self._install_easyocr) + install_layout.addWidget(install_easyocr_btn) + + install_tesseract_btn = QPushButton("šŸ“¦ Install Tesseract Package") + install_tesseract_btn.setStyleSheet(""" + QPushButton { + background-color: #ff8c42; + color: #141f23; + font-weight: bold; + padding: 8px; + } + """) + install_tesseract_btn.clicked.connect(self._install_pytesseract) + install_layout.addWidget(install_tesseract_btn) + + detect_tesseract_btn = QPushButton("šŸ” Auto-Detect Tesseract") + detect_tesseract_btn.setStyleSheet(""" + QPushButton { + background-color: #a0aec0; + color: #141f23; + font-weight: bold; + padding: 8px; + } + """) + detect_tesseract_btn.clicked.connect(self._auto_detect_tesseract) + install_layout.addWidget(detect_tesseract_btn) + + install_paddle_btn = QPushButton("šŸ“¦ Install PaddleOCR") + install_paddle_btn.setStyleSheet(""" + QPushButton { + background-color: #4a5568; + color: white; + font-weight: bold; + padding: 8px; + } + """) + install_paddle_btn.clicked.connect(self._install_paddleocr) + install_layout.addWidget(install_paddle_btn) + + backend_layout.addLayout(install_layout) layout.addWidget(backend_group) # Tips @@ -587,9 +650,12 @@ class GameReaderTestPlugin(BasePlugin): if backend in ('auto', 'easyocr'): try: import easyocr + import numpy as np reader = easyocr.Reader(['en'], gpu=False, verbose=False) + # Convert PIL Image to numpy array for EasyOCR + image_np = np.array(image) ocr_result = reader.readtext( - image, + image_np, detail=0, paragraph=True ) @@ -618,6 +684,27 @@ class GameReaderTestPlugin(BasePlugin): ) raise e + if not text and backend in ('auto', 'paddle'): + try: + from paddleocr import PaddleOCR + # Try without show_log argument for compatibility + try: + ocr = PaddleOCR(use_angle_cls=True, lang='en', show_log=False) + except TypeError: + # Older version without show_log + ocr = PaddleOCR(use_angle_cls=True, lang='en') + # Convert PIL to numpy for PaddleOCR + import numpy as np + image_np = np.array(image) + result = ocr.ocr(image_np, cls=True) + if result and result[0]: + texts = [line[1][0] for line in result[0]] + text = '\n'.join(texts) + backend_used = "paddleocr" + except Exception as e: + if backend == 'paddle': + raise e + processing_time = time.time() - start_time # Update UI (thread-safe via Qt signals would be better, but this works for simple case) @@ -786,28 +873,83 @@ class GameReaderTestPlugin(BasePlugin): except Exception as e: self.dpi_label.setText(f"Error: {e}") + def _install_easyocr(self): + """Install EasyOCR backend.""" + from core.ocr_backend_manager import get_ocr_backend_manager + manager = get_ocr_backend_manager() + success, message = manager.install_backend('easyocr') + if success: + self.notify_success("Installation Complete", message) + else: + self.notify_error("Installation Failed", message) + self._check_backends() + + def _install_pytesseract(self): + """Install pytesseract Python package.""" + from core.ocr_backend_manager import get_ocr_backend_manager + manager = get_ocr_backend_manager() + success, message = manager.install_backend('tesseract') + if success: + self.notify_success("Installation Complete", message + "\n\nNote: You also need to install the Tesseract binary from:\nhttps://github.com/UB-Mannheim/tesseract/wiki") + else: + self.notify_error("Installation Failed", message) + self._check_backends() + + def _install_paddleocr(self): + """Install PaddleOCR backend.""" + from core.ocr_backend_manager import get_ocr_backend_manager + manager = get_ocr_backend_manager() + success, message = manager.install_backend('paddleocr') + if success: + self.notify_success("Installation Complete", message) + else: + self.notify_error("Installation Failed", message) + self._check_backends() + + def _auto_detect_tesseract(self): + """Auto-detect Tesseract from registry/paths.""" + from core.ocr_backend_manager import get_ocr_backend_manager + manager = get_ocr_backend_manager() + if manager.auto_configure_tesseract(): + self.notify_success("Tesseract Found", f"Auto-configured Tesseract at:\n{manager.backends['tesseract']['path']}") + else: + self.notify_warning("Not Found", "Could not find Tesseract installation.\n\nPlease install from:\nhttps://github.com/UB-Mannheim/tesseract/wiki") + self._check_backends() + def _check_backends(self): - """Check OCR backend availability.""" + """Check OCR backend availability with install buttons.""" + from core.ocr_backend_manager import get_ocr_backend_manager + + manager = get_ocr_backend_manager() statuses = [] - try: - import easyocr + # EasyOCR + easyocr_status = manager.get_backend_status('easyocr') + if easyocr_status['available']: statuses.append("āœ… EasyOCR - Available (recommended)") - except ImportError: - statuses.append("āŒ EasyOCR - Not installed\n Install: pip install easyocr") + else: + statuses.append("āŒ EasyOCR - Not installed\n Click 'Install EasyOCR' button below") - try: - import pytesseract - # Also check if tesseract binary is available - try: - pytesseract.get_tesseract_version() - statuses.append("āœ… Tesseract - Available") - except Exception: - statuses.append("āš ļø Tesseract - Python wrapper installed but tesseract binary not found\n Install binary from: https://github.com/UB-Mannheim/tesseract/wiki") - except ImportError: - statuses.append("āŒ Tesseract - Not installed\n Install: pip install pytesseract\n Then install binary from: https://github.com/UB-Mannheim/tesseract/wiki") + # Tesseract + tesseract_status = manager.get_backend_status('tesseract') + if tesseract_status['available']: + path_info = f" at {tesseract_status['path']}" if tesseract_status['path'] else "" + statuses.append(f"āœ… Tesseract - Available{path_info}") + elif tesseract_status['installed']: + statuses.append("āš ļø Tesseract - Python package installed but binary not found\n Click 'Auto-Detect Tesseract' or install binary from:\n https://github.com/UB-Mannheim/tesseract/wiki") + else: + statuses.append("āŒ Tesseract - Not installed\n Click 'Install Tesseract Package' then install binary") - try: + # PaddleOCR + paddle_status = manager.get_backend_status('paddleocr') + if paddle_status['available']: + statuses.append("āœ… PaddleOCR - Available") + else: + statuses.append("āŒ PaddleOCR - Not installed\n Click 'Install PaddleOCR' button below") + + statuses.append("\nšŸ’” Recommendation: EasyOCR is easiest (auto-downloads models)") + + self.backend_status.setPlainText('\n'.join(statuses)) from paddleocr import PaddleOCR statuses.append("āœ… PaddleOCR - Available") except ImportError: