183 lines
5.4 KiB
Python
183 lines
5.4 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Engine selector - Intelligent OCR engine selection logic
|
|
|
|
Rules:
|
|
1. Image contains Chinese characters → PaddleOCR (better Chinese support)
|
|
2. Image is simple text, English only → Tesseract (faster, lighter)
|
|
3. User explicitly requests engine → Use requested engine
|
|
4. Auto mode, mixed/unknown → PaddleOCR (higher accuracy)
|
|
|
|
Multi-engine parallel mode:
|
|
- Tesseract + PaddleOCR + MacVision (macOS only)
|
|
- Agent择优 based on confidence, language support, and speed
|
|
"""
|
|
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Literal, List, Dict
|
|
|
|
|
|
def detect_chinese(content: str) -> bool:
|
|
"""Check if content contains Chinese characters"""
|
|
# Chinese range: \u4e00-\u9fff
|
|
return bool(re.search(r'[\u4e00-\u9fff]', content))
|
|
|
|
|
|
def analyze_image_complexity(image_path: str) -> str:
|
|
"""
|
|
Analyze image complexity to determine optimal engine.
|
|
|
|
Returns:
|
|
'simple' or 'complex'
|
|
"""
|
|
image_path_lower = Path(image_path).name.lower()
|
|
|
|
complex_patterns = [
|
|
'menu', 'invoice', 'contract', 'certificate',
|
|
'exam', 'form', 'table', 'receipt'
|
|
]
|
|
|
|
if any(p in image_path_lower for p in complex_patterns):
|
|
return 'complex'
|
|
|
|
return 'simple'
|
|
|
|
|
|
def get_available_engines(image_path: str) -> List[str]:
|
|
"""
|
|
Get list of available engines for the current platform.
|
|
|
|
Args:
|
|
image_path: Path to image (for platform-specific hints)
|
|
|
|
Returns:
|
|
List of engine names: ['tesseract', 'paddle', 'macvision']
|
|
"""
|
|
engines = ['tesseract', 'paddle'] # Always available
|
|
|
|
# Add MacVision on macOS
|
|
if sys.platform == 'darwin':
|
|
engines.append('macvision')
|
|
|
|
return engines
|
|
|
|
|
|
def select_engine(
|
|
image_path: str,
|
|
requested_engine: Literal['auto', 'tesseract', 'paddle', 'macvision'] = 'auto'
|
|
) -> List[str]:
|
|
"""
|
|
Select engines for OCR processing.
|
|
|
|
In multi-engine mode, returns list of engines to run in parallel.
|
|
|
|
Args:
|
|
image_path: Path to image being processed
|
|
requested_engine: User request or 'auto'
|
|
|
|
Returns:
|
|
List of engine names to use
|
|
"""
|
|
available = get_available_engines(image_path)
|
|
|
|
# Rule 1: User explicitly requested single engine
|
|
if requested_engine != 'auto':
|
|
if requested_engine in available:
|
|
return [requested_engine]
|
|
else:
|
|
return available # Fallback to all available
|
|
|
|
# Rule 2: Check image path for hints
|
|
path_lower = Path(image_path).name.lower()
|
|
|
|
# Simple screenshots → Tesseract (fastest)
|
|
simple_indicators = ['screenshot', 'snap', 'capture', 'screen']
|
|
if any(ind in path_lower for ind in simple_indicators):
|
|
return ['tesseract']
|
|
|
|
# Complex documents → All engines (max accuracy)
|
|
complex_indicators = ['menu', 'invoice', 'certificate', 'contract', 'receipt']
|
|
if any(ind in path_lower for ind in complex_indicators):
|
|
return available # Run all available engines
|
|
|
|
# Default: Run all available engines for best results
|
|
return available
|
|
|
|
|
|
def select_best_result(
|
|
results: List[Dict],
|
|
preferred_engine: str = 'paddle'
|
|
) -> Dict:
|
|
"""
|
|
Select the best OCR result from multiple engines.
|
|
|
|
Args:
|
|
results: List of OCR results from different engines
|
|
preferred_engine: Preferred engine for tie-breaking
|
|
|
|
Returns:
|
|
Dict with selected result and metadata
|
|
"""
|
|
if not results:
|
|
return {
|
|
'text': '',
|
|
'confidence': 0.0,
|
|
'error': 'No results',
|
|
'selected_engine': None
|
|
}
|
|
|
|
# Filter valid results (with text)
|
|
valid_results = [r for r in results if r.get('text', '').strip()]
|
|
|
|
if not valid_results:
|
|
return {
|
|
'text': '',
|
|
'confidence': 0.0,
|
|
'error': 'All engines failed',
|
|
'selected_engine': None
|
|
}
|
|
|
|
# Calculate weighted score
|
|
for r in valid_results:
|
|
engine = r.get('engine', 'unknown')
|
|
|
|
# Engine quality weights
|
|
quality_weights = {
|
|
'paddle': 1.0,
|
|
'macvision': 0.95,
|
|
'tesseract': 0.9
|
|
}
|
|
|
|
# Language support weights (optional, can be extended)
|
|
language_weights = {
|
|
'paddle': 1.0, # Best for Chinese
|
|
'macvision': 0.85, # Good for English, fair for Chinese
|
|
'tesseract': 0.8 # Good for English, fair for Chinese
|
|
}
|
|
|
|
# Combined score
|
|
base_confidence = r.get('confidence', 0)
|
|
quality = quality_weights.get(engine, 0.8)
|
|
language = language_weights.get(engine, 0.8)
|
|
|
|
r['_score'] = base_confidence * quality * language
|
|
r['_quality_weight'] = quality
|
|
r['_language_weight'] = language
|
|
|
|
# Sort by score
|
|
sorted_results = sorted(valid_results, key=lambda x: x.get('_score', 0), reverse=True)
|
|
|
|
# Select best
|
|
best = sorted_results[0]
|
|
|
|
return {
|
|
'text': best.get('text', ''),
|
|
'confidence': best.get('confidence', 0),
|
|
'engine': best.get('engine', 'unknown'),
|
|
'selected_engine': best.get('engine', 'unknown'),
|
|
'processing_time_ms': sum(r.get('processing_time_ms', 0) for r in valid_results),
|
|
'score': best.get('_score', 0),
|
|
'other_results': sorted_results[1:] # Include runner-ups for verification
|
|
} |