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
This commit is contained in:
LemonNexus 2026-02-14 23:38:18 +00:00
parent d7b7b491b5
commit 999bdfca35
2 changed files with 374 additions and 23 deletions

209
core/ocr_backend_manager.py Normal file
View File

@ -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

View File

@ -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,9 +89,17 @@ 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...")
# Try with show_log, fall back without for compatibility
try:
ocr = PaddleOCR(use_angle_cls=True, lang='en', show_log=False)
result = ocr.ocr(screenshot, cls=True)
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"
@ -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: