技能备份 - 2026-04-15 (40个技能)
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,343 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Dependencies checker and auto-installer for Super OCR
|
||||
|
||||
This module handles:
|
||||
- Dependency detection
|
||||
- Auto-installation of missing packages
|
||||
- Version checking
|
||||
- Clear error messages with troubleshooting steps
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
|
||||
# Dependency definitions
|
||||
DEPENDENCIES = {
|
||||
'paddleocr': {
|
||||
'package': 'paddleocr',
|
||||
'required': ['paddleocr'],
|
||||
'optional': [],
|
||||
'install_cmd': 'pip install paddleocr paddlepaddle',
|
||||
'check_fn': 'check_paddleocr'
|
||||
},
|
||||
'pytesseract': {
|
||||
'package': 'pytesseract',
|
||||
'required': ['pytesseract', 'PIL', 'cv2', 'numpy'],
|
||||
'optional': [],
|
||||
'install_cmd': 'pip install pytesseract pillow opencv-python numpy',
|
||||
'check_fn': 'check_pytesseract'
|
||||
},
|
||||
'common': {
|
||||
'package': 'common',
|
||||
'required': ['pathlib', 'argparse', 'logging'],
|
||||
'optional': ['yaml'],
|
||||
'install_cmd': None,
|
||||
'check_fn': None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def _check_module(module_name: str) -> bool:
|
||||
"""Check if a Python module is available"""
|
||||
return importlib.util.find_spec(module_name) is not None
|
||||
|
||||
|
||||
def check_paddleocr() -> Tuple[bool, List[str]]:
|
||||
"""Check PaddleOCR and paddlepaddle availability"""
|
||||
missing = []
|
||||
|
||||
if not _check_module('paddleocr'):
|
||||
missing.append('paddleocr')
|
||||
|
||||
# Check paddlepaddle (the actual library)
|
||||
if not _check_module('paddle'):
|
||||
missing.append('paddlepaddle')
|
||||
|
||||
return len(missing) == 0, missing
|
||||
|
||||
|
||||
def check_pytesseract() -> Tuple[bool, List[str]]:
|
||||
"""Check Tesseract-related dependencies"""
|
||||
missing = []
|
||||
|
||||
# Python packages
|
||||
for pkg in ['pytesseract', 'PIL', 'cv2', 'numpy']:
|
||||
if not _check_module(pkg):
|
||||
missing.append(pkg)
|
||||
|
||||
return len(missing) == 0, missing
|
||||
|
||||
|
||||
def check_tesseract_binary() -> Tuple[bool, str]:
|
||||
"""Check if tesseract binary is installed"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['tesseract', '--version'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5
|
||||
)
|
||||
return True, result.stdout.strip()
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError, PermissionError):
|
||||
return False, ""
|
||||
|
||||
|
||||
def check_dependency(dep_name: str) -> Tuple[bool, List[str], Optional[str]]:
|
||||
"""
|
||||
Check a specific dependency.
|
||||
|
||||
Returns:
|
||||
(is_available, missing_items, installation_hint)
|
||||
"""
|
||||
if dep_name not in DEPENDENCIES:
|
||||
return False, [], None
|
||||
|
||||
dep = DEPENDENCIES[dep_name]
|
||||
|
||||
# Check if we have a custom check function
|
||||
if dep.get('check_fn'):
|
||||
check_fn_name = dep['check_fn']
|
||||
check_fns = {
|
||||
'check_paddleocr': check_paddleocr,
|
||||
'check_pytesseract': check_pytesseract,
|
||||
'check_tesseract_binary': check_tesseract_binary
|
||||
}
|
||||
|
||||
if check_fn_name in check_fns:
|
||||
is_available, result = check_fns[check_fn_name]()
|
||||
return is_available, result if isinstance(result, list) else [], None
|
||||
|
||||
# Generic check for required packages
|
||||
missing = []
|
||||
for pkg in dep.get('required', []):
|
||||
if not _check_module(pkg):
|
||||
missing.append(pkg)
|
||||
|
||||
return len(missing) == 0, missing, dep.get('install_cmd')
|
||||
|
||||
|
||||
def auto_install(dependency: str) -> bool:
|
||||
"""
|
||||
Auto-install a dependency.
|
||||
|
||||
Returns:
|
||||
True if installation succeeded, False otherwise
|
||||
"""
|
||||
if dependency not in DEPENDENCIES:
|
||||
print(f"[ERROR] Unknown dependency: {dependency}")
|
||||
return False
|
||||
|
||||
dep = DEPENDENCIES[dependency]
|
||||
install_cmd = dep.get('install_cmd')
|
||||
|
||||
if not install_cmd:
|
||||
print(f"[INFO] No auto-install command for {dependency}")
|
||||
return False
|
||||
|
||||
print(f"[INFO] Installing {dependency}...")
|
||||
print(f" Command: {install_cmd}")
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
install_cmd.split(),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
print(f"[OK] {dependency} installed successfully")
|
||||
return True
|
||||
else:
|
||||
print(f"[ERROR] Installation failed:")
|
||||
print(f" {result.stderr}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Auto-install failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def print_installation_guide() -> None:
|
||||
"""Print comprehensive installation instructions"""
|
||||
print("\n" + "=" * 60)
|
||||
print("Super OCR Dependencies Installation Guide")
|
||||
print("=" * 60)
|
||||
|
||||
print("\n[OPTION 1] Install PaddleOCR (Recommended for Chinese)")
|
||||
print("-" * 40)
|
||||
print("pip install paddleocr paddlepaddle")
|
||||
print()
|
||||
print("For macOS/Linux (CPU only):")
|
||||
print(" pip install paddleocr paddlepaddle")
|
||||
print()
|
||||
print("For Windows (CPU only):")
|
||||
print(" pip install paddleocr paddlepaddle")
|
||||
print()
|
||||
print("For GPU support:")
|
||||
print(" # CUDA 11.2")
|
||||
print(" pip install paddlepaddle-gpu==2.4.0 -f https://www.paddlepaddle.org.cn/whl/stable.html")
|
||||
print(" # CUDA 11.6")
|
||||
print(" pip install paddlepaddle-gpu==2.4.0 -f https://www.paddlepaddle.org.cn/whl/lite.html")
|
||||
|
||||
print("\n[OPTION 2] Install Tesseract")
|
||||
print("-" * 40)
|
||||
print("macOS:")
|
||||
print(" brew install tesseract")
|
||||
print()
|
||||
print("Ubuntu/Debian:")
|
||||
print(" sudo apt update && sudo apt install tesseract-ocr")
|
||||
print()
|
||||
print("Windows:")
|
||||
print(" Download from: https://github.com/UB-Mannheim/tesseract/wiki")
|
||||
|
||||
print("\n[OPTION 3] Install Tesseract Python bindings")
|
||||
print("-" * 40)
|
||||
print("pip install pytesseract pillow opencv-python numpy")
|
||||
|
||||
print("\n[OPTION 4] Install all at once")
|
||||
print("-" * 40)
|
||||
print("pip install paddleocr paddlepaddle pytesseract pillow opencv-python numpy")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
|
||||
|
||||
def check_all_dependencies(interactive: bool = True) -> bool:
|
||||
"""
|
||||
Check all dependencies and optionally install missing ones.
|
||||
|
||||
Returns:
|
||||
True if all dependencies are satisfied
|
||||
"""
|
||||
print("\n" + "=" * 60)
|
||||
print("Checking Super OCR Dependencies")
|
||||
print("=" * 60)
|
||||
|
||||
all_ok = True
|
||||
missing = []
|
||||
|
||||
# Check PaddleOCR
|
||||
print("\n[1/3] Checking PaddleOCR...")
|
||||
ok, missing_pkgs = check_paddleocr()
|
||||
if ok:
|
||||
print("[OK] PaddleOCR is available")
|
||||
else:
|
||||
print(f"[MISSING] Missing: {', '.join(missing_pkgs)}")
|
||||
all_ok = False
|
||||
missing.extend(missing_pkgs)
|
||||
|
||||
# Check Tesseract binary
|
||||
print("\n[2/3] Checking Tesseract binary...")
|
||||
ok, info = check_tesseract_binary()
|
||||
if ok:
|
||||
print(f"[OK] Tesseract is available: {info.split()[2] if info else 'unknown'}")
|
||||
else:
|
||||
print("[MISSING] Tesseract binary not found")
|
||||
print(" Install: brew install tesseract (macOS) or apt install tesseract-ocr (Ubuntu)")
|
||||
all_ok = False
|
||||
|
||||
# Check Tesseract Python bindings
|
||||
print("\n[3/3] Checking Tesseract Python bindings...")
|
||||
ok, missing_pkgs = check_pytesseract()
|
||||
if ok:
|
||||
print("[OK] Tesseract Python bindings are available")
|
||||
else:
|
||||
print(f"[MISSING] Missing: {', '.join(missing_pkgs)}")
|
||||
all_ok = False
|
||||
|
||||
# Summary
|
||||
print("\n" + "=" * 60)
|
||||
if all_ok:
|
||||
print("[OK] All dependencies satisfied!")
|
||||
return True
|
||||
else:
|
||||
print("[WARNING] Some dependencies are missing")
|
||||
print(f"Missing: {', '.join(missing)}")
|
||||
|
||||
if interactive:
|
||||
response = input("\nAuto-install missing dependencies? [y/N]: ")
|
||||
if response.lower() == 'y':
|
||||
# Install PaddleOCR if missing
|
||||
if 'paddleocr' in missing or 'paddlepaddle' in missing:
|
||||
print("\n[INSTALL] Installing PaddleOCR...")
|
||||
auto_install('paddleocr')
|
||||
|
||||
# Install Tesseract packages if missing
|
||||
if any(pkg in missing for pkg in ['pytesseract', 'PIL', 'cv2', 'numpy']):
|
||||
print("\n[INSTALL] Installing Tesseract packages...")
|
||||
auto_install('pytesseract')
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Manual Installation:")
|
||||
print("=" * 60)
|
||||
print_installation_guide()
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Super OCR Dependencies Checker and Installer'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--check', '-c',
|
||||
action='store_true',
|
||||
help='Check dependencies without installing'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--install', '-i',
|
||||
action='store_true',
|
||||
help='Auto-install missing dependencies'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--dependency', '-d',
|
||||
choices=['paddleocr', 'pytesseract', 'all'],
|
||||
default='all',
|
||||
help='Specific dependency to check/install'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--guide', '-g',
|
||||
action='store_true',
|
||||
help='Show installation guide only'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--quiet', '-q',
|
||||
action='store_true',
|
||||
help='Suppress output'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.guide:
|
||||
print_installation_guide()
|
||||
return 0
|
||||
|
||||
if args.check or args.install:
|
||||
if args.dependency == 'all':
|
||||
success = check_all_dependencies(interactive=not args.quiet and not args.install)
|
||||
else:
|
||||
ok, missing, cmd = check_dependency(args.dependency)
|
||||
if ok:
|
||||
print(f"[OK] {args.dependency} is available")
|
||||
return 0
|
||||
else:
|
||||
print(f"[MISSING] {args.dependency}: {', '.join(missing)}")
|
||||
if args.install:
|
||||
success = auto_install(args.dependency)
|
||||
return 0 if success else 1
|
||||
return 1
|
||||
else:
|
||||
# Default: interactive check
|
||||
success = check_all_dependencies(interactive=True)
|
||||
return 0 if success else 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1 @@
|
||||
# Engine module
|
||||
@@ -0,0 +1 @@
|
||||
# MacVision OCR module
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
MacVisionOCR - macOS Vision Text Recognition via Swift script
|
||||
|
||||
Uses Apple's Vision Framework via Swift script (confirmed working).
|
||||
|
||||
Requirements:
|
||||
- macOS 10.15+ (Catalina)
|
||||
- Swift (built-in on macOS)
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
||||
# Path to Swift script
|
||||
SWIFT_SCRIPT_PATH = Path(__file__).parent / "macvision_swift.swift"
|
||||
|
||||
|
||||
class MacVisionOCR:
|
||||
"""macOS Vision OCR wrapper using Swift script"""
|
||||
|
||||
def __init__(self, verbose: bool = False):
|
||||
"""Initialize MacVisionOCR processor."""
|
||||
self.verbose = verbose
|
||||
|
||||
if sys.platform != 'darwin':
|
||||
raise RuntimeError("MacVisionOCR only works on macOS")
|
||||
|
||||
if self.verbose:
|
||||
print("[INFO] MacVisionOCR initialized (via Swift)")
|
||||
|
||||
def _estimate_confidence(self, text: str) -> float:
|
||||
"""Estimate OCR confidence from text quality."""
|
||||
if not text:
|
||||
return 0.0
|
||||
|
||||
printable = sum(1 for c in text if c.isprintable() or c in '\n\r\t')
|
||||
length_factor = min(1.0, len(text) / 100)
|
||||
quality = printable / max(len(text), 1)
|
||||
|
||||
confidence = 0.85 + (0.15 * quality * length_factor)
|
||||
return round(min(confidence, 1.0), 4)
|
||||
|
||||
def extract(self, image_path: str) -> Dict:
|
||||
"""Extract text from image using Mac Vision OCR via Swift."""
|
||||
from time import time
|
||||
|
||||
if sys.platform != 'darwin':
|
||||
return {
|
||||
'text': '',
|
||||
'confidence': 0.0,
|
||||
'error': 'MacVisionOCR only works on macOS',
|
||||
'processing_time_ms': 0
|
||||
}
|
||||
|
||||
start_time = time()
|
||||
|
||||
try:
|
||||
# Run Swift script with image path as argument
|
||||
result = subprocess.run(
|
||||
['swift', str(SWIFT_SCRIPT_PATH), image_path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
return {
|
||||
'text': '',
|
||||
'confidence': 0.0,
|
||||
'error': result.stderr.strip(),
|
||||
'processing_time_ms': (time() - start_time) * 1000
|
||||
}
|
||||
|
||||
# Parse Chinese output
|
||||
lines = result.stdout.strip().split('\n')
|
||||
text_lines = []
|
||||
all_confidences = []
|
||||
|
||||
current_text = None
|
||||
for line in lines:
|
||||
if line.startswith('文本:'):
|
||||
current_text = line[3:].strip()
|
||||
elif line.startswith('置信度:') and current_text:
|
||||
try:
|
||||
conf_str = line[4:].strip()
|
||||
# Swift returns confidence as percentage (0-100)
|
||||
# Output format: "置信度:50.00%" -> need to divide by 100
|
||||
numeric_part = conf_str.rstrip('%').strip()
|
||||
confidence = float(numeric_part) / 100.0
|
||||
text_lines.append(current_text)
|
||||
all_confidences.append(confidence)
|
||||
current_text = None
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
|
||||
full_text = '\n'.join(text_lines)
|
||||
processing_time = (time() - start_time) * 1000
|
||||
avg_confidence = sum(all_confidences) / len(all_confidences) if all_confidences else 0.0
|
||||
|
||||
return {
|
||||
'text': full_text.strip(),
|
||||
'confidence': round(avg_confidence, 4),
|
||||
'line_count': len(text_lines),
|
||||
'processing_time_ms': round(processing_time, 2)
|
||||
}
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
return {
|
||||
'text': '',
|
||||
'confidence': 0.0,
|
||||
'error': 'MacVisionOCR timeout',
|
||||
'processing_time_ms': 60000
|
||||
}
|
||||
except Exception as e:
|
||||
processing_time = (time() - start_time) * 1000
|
||||
return {
|
||||
'text': '',
|
||||
'confidence': 0.0,
|
||||
'error': f"MacVision error: {str(e)}",
|
||||
'processing_time_ms': round(processing_time, 2)
|
||||
}
|
||||
|
||||
def batch_extract(self, image_paths: List[str]) -> List[Dict]:
|
||||
"""Process multiple images"""
|
||||
return [self.extract(path) for path in image_paths]
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import argparse
|
||||
|
||||
if sys.platform != 'darwin':
|
||||
print("[ERROR] MacVisionOCR only works on macOS")
|
||||
sys.exit(1)
|
||||
|
||||
if not SWIFT_SCRIPT_PATH.exists():
|
||||
print(f"[ERROR] Swift script not found: {SWIFT_SCRIPT_PATH}")
|
||||
sys.exit(1)
|
||||
|
||||
parser = argparse.ArgumentParser(description='macOS Vision OCR via Swift')
|
||||
parser.add_argument('image', help='Image file to process')
|
||||
parser.add_argument('--verbose', '-v', action='store_true')
|
||||
args = parser.parse_args()
|
||||
|
||||
processor = MacVisionOCR(verbose=args.verbose)
|
||||
result = processor.extract(args.image)
|
||||
|
||||
print(f"\nText:\n{result['text']}")
|
||||
print(f"\nConfidence: {result['confidence']:.4f}")
|
||||
print(f"Lines: {result.get('line_count', 0)}")
|
||||
print(f"Time: {result.get('processing_time_ms', 0):.2f}ms")
|
||||
@@ -0,0 +1,70 @@
|
||||
import Foundation
|
||||
import Vision
|
||||
import AppKit
|
||||
|
||||
// Swift 5.9+ 可以用 CommandLine.arguments,兼容旧版本用下面的方式
|
||||
let args = ProcessInfo.processInfo.arguments
|
||||
|
||||
guard args.count >= 2 else {
|
||||
print("Usage: swift macvision_swift.swift <image_path>")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
let imagePath = args[1]
|
||||
|
||||
// 1. 加载图片
|
||||
guard let image = NSImage(contentsOfFile: imagePath) else {
|
||||
print("❌ 无法加载图片: \(imagePath)")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
// 2. 转换为 CGImage(Vision 需要)
|
||||
guard let tiffData = image.tiffRepresentation,
|
||||
let cgImage = NSBitmapImageRep(data: tiffData)?.cgImage else {
|
||||
print("❌ 无法转换图片")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
// 3. 设置信号量等待异步完成
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
var results: [(text: String, confidence: Float)] = []
|
||||
|
||||
// 4. 创建 OCR 请求
|
||||
let request = VNRecognizeTextRequest { request, error in
|
||||
defer { semaphore.signal() } // 关键:完成后释放信号量
|
||||
|
||||
if let error = error {
|
||||
print("❌ 识别错误:\(error)")
|
||||
return
|
||||
}
|
||||
|
||||
guard let observations = request.results as? [VNRecognizedTextObservation] else {
|
||||
print("❌ 无结果")
|
||||
return
|
||||
}
|
||||
|
||||
for observation in observations {
|
||||
guard let candidate = observation.topCandidates(1).first else { continue }
|
||||
results.append((candidate.string, candidate.confidence))
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 配置识别参数
|
||||
request.recognitionLanguages = ["zh-Hans", "zh-Hant", "en-US"] // 支持中文
|
||||
request.usesLanguageCorrection = true // 启用语言校正
|
||||
|
||||
// 6. 执行识别
|
||||
let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
|
||||
|
||||
do {
|
||||
try handler.perform([request])
|
||||
_ = semaphore.wait(timeout: .now() + 30) // 等待最多 30 秒
|
||||
|
||||
// 输出结果
|
||||
for (text, conf) in results {
|
||||
print("文本:\(text)")
|
||||
print("置信度:\(String(format: "%.2f", conf * 100))%")
|
||||
}
|
||||
} catch {
|
||||
print("❌ 执行错误:\(error)")
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PaddleOCR wrapper - High accuracy Chinese OCR engine
|
||||
|
||||
Compatible with PaddleOCR 3.4.0+ API (predict() method returns list)
|
||||
Based on Emma's testing: result[0][0][1][0] for text, result[0][0][1][1] for confidence
|
||||
"""
|
||||
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
|
||||
class PaddleOCR:
|
||||
"""PaddleOCR processor for high-accuracy text extraction"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
verbose: bool = False,
|
||||
lang: str = 'ch',
|
||||
use_angle_cls: bool = True
|
||||
):
|
||||
"""Initialize PaddleOCR - lang and use_angle_cls parameters"""
|
||||
self.verbose = verbose
|
||||
self.lang = lang
|
||||
self.use_angle_cls = use_angle_cls
|
||||
self._init_ocr()
|
||||
|
||||
def _init_ocr(self) -> None:
|
||||
"""Initialize PaddleOCR model"""
|
||||
from paddleocr import PaddleOCR
|
||||
|
||||
self.ocr = PaddleOCR(lang=self.lang, use_angle_cls=self.use_angle_cls)
|
||||
|
||||
def extract(self, image_path: str) -> Dict:
|
||||
"""
|
||||
Extract text from image using PaddleOCR.
|
||||
|
||||
Args:
|
||||
image_path: Path to input image
|
||||
|
||||
Returns:
|
||||
Dict with text, confidence, results, processing time
|
||||
"""
|
||||
if self.verbose:
|
||||
print(f"[PaddleOCR] Processing: {image_path}")
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# Run OCR - using predict() for PaddleOCR 3.4.0+
|
||||
result = self.ocr.predict(image_path)
|
||||
|
||||
processing_time = time.time() - start_time
|
||||
|
||||
# Parse results - legacy format: [[box], [text, confidence]]
|
||||
parsed = self._parse_results(result)
|
||||
parsed['processing_time_ms'] = round(processing_time * 1000, 2)
|
||||
|
||||
return parsed
|
||||
|
||||
except Exception as e:
|
||||
processing_time = time.time() - start_time
|
||||
return {
|
||||
'text': '',
|
||||
'confidence': 0.0,
|
||||
'error': str(e),
|
||||
'results': [],
|
||||
'processing_time_ms': round(processing_time * 1000, 2)
|
||||
}
|
||||
|
||||
def _parse_results(self, results) -> Dict:
|
||||
"""
|
||||
Parse PaddleOCR 3.4.0+ output format.
|
||||
|
||||
PaddleOCR 3.4.0+ predict() returns dict with:
|
||||
- rec_texts: list of detected text strings
|
||||
- rec_scores: list of confidence scores
|
||||
- dt_polys: list of text polygon coordinates
|
||||
|
||||
Returns:
|
||||
Dict with text, confidence, results, line_count
|
||||
"""
|
||||
if not results or not results[0]:
|
||||
return {
|
||||
'text': '',
|
||||
'confidence': 0.0,
|
||||
'results': [],
|
||||
'error': 'No text detected'
|
||||
}
|
||||
|
||||
# New format: result[0] is a dict with 'rec_texts' and 'rec_scores'
|
||||
result_dict = results[0]
|
||||
|
||||
if isinstance(result_dict, dict):
|
||||
texts = result_dict.get('rec_texts', [])
|
||||
scores = result_dict.get('rec_scores', [])
|
||||
|
||||
if not texts:
|
||||
return {
|
||||
'text': '',
|
||||
'confidence': 0.0,
|
||||
'results': [],
|
||||
'error': 'No text detected'
|
||||
}
|
||||
|
||||
# Combine all text (PaddleOCR already splits by lines)
|
||||
full_text = '\n'.join(texts)
|
||||
|
||||
# Calculate average confidence
|
||||
avg_confidence = sum(scores) / len(scores) if scores else 0.0
|
||||
|
||||
# Build detailed results
|
||||
detailed_results = []
|
||||
for i, (text, score) in enumerate(zip(texts, scores)):
|
||||
# Get bbox if available (dt_polys is list of arrays)
|
||||
dt_polys = result_dict.get('dt_polys', [])
|
||||
bbox = None
|
||||
if i < len(dt_polys):
|
||||
try:
|
||||
poly = dt_polys[i]
|
||||
if hasattr(poly, 'tolist'):
|
||||
bbox = poly.tolist()
|
||||
else:
|
||||
bbox = poly
|
||||
except:
|
||||
bbox = None
|
||||
|
||||
detailed_results.append({
|
||||
'text': text,
|
||||
'confidence': round(score, 4),
|
||||
'bbox': bbox
|
||||
})
|
||||
|
||||
return {
|
||||
'text': full_text.strip(),
|
||||
'confidence': round(avg_confidence, 4),
|
||||
'results': detailed_results,
|
||||
'line_count': len(texts)
|
||||
}
|
||||
else:
|
||||
# Fallback: handle legacy format if still used
|
||||
return self._parse_results_legacy(results)
|
||||
|
||||
def _parse_results_legacy(self, results) -> Dict:
|
||||
"""
|
||||
Legacy parser for backward compatibility.
|
||||
Old format: [[bbox], [text, confidence]]
|
||||
"""
|
||||
if not results or not results[0]:
|
||||
return {
|
||||
'text': '',
|
||||
'confidence': 0.0,
|
||||
'results': [],
|
||||
'error': 'No text detected'
|
||||
}
|
||||
|
||||
text_lines = []
|
||||
all_confidences = []
|
||||
|
||||
for line in results:
|
||||
if len(line) < 2:
|
||||
continue
|
||||
|
||||
box = line[0]
|
||||
|
||||
if isinstance(line[1], (list, tuple)) and len(line[1]) >= 2:
|
||||
text, confidence = line[1][0], line[1][1]
|
||||
else:
|
||||
text, confidence = str(line[1]), 0.0
|
||||
|
||||
text_lines.append(text)
|
||||
all_confidences.append(confidence)
|
||||
|
||||
full_text = '\n'.join(text_lines)
|
||||
avg_confidence = sum(all_confidences) / len(all_confidences) if all_confidences else 0.0
|
||||
|
||||
detailed_results = []
|
||||
for line in results:
|
||||
if len(line) >= 2:
|
||||
box = line[0]
|
||||
if isinstance(line[1], (list, tuple)) and len(line[1]) >= 2:
|
||||
text, confidence = line[1][0], line[1][1]
|
||||
else:
|
||||
text, confidence = str(line[1]), 0.0
|
||||
|
||||
detailed_results.append({
|
||||
'text': text,
|
||||
'confidence': confidence,
|
||||
'bbox': box
|
||||
})
|
||||
|
||||
return {
|
||||
'text': full_text.strip(),
|
||||
'confidence': round(avg_confidence, 4),
|
||||
'results': detailed_results,
|
||||
'line_count': len(detailed_results)
|
||||
}
|
||||
|
||||
def batch_extract(self, image_paths: List[str]) -> List[Dict]:
|
||||
"""Process multiple images"""
|
||||
return [self.extract(path) for path in image_paths]
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description='PaddleOCR wrapper')
|
||||
parser.add_argument('image', help='Image file to process')
|
||||
parser.add_argument('--lang', default='ch', help='Language code (ch, en, etc.)')
|
||||
parser.add_argument('--verbose', '-v', action='store_true')
|
||||
args = parser.parse_args()
|
||||
|
||||
processor = PaddleOCR(verbose=args.verbose, lang=args.lang)
|
||||
result = processor.extract(args.image)
|
||||
|
||||
print(f"\nText:\n{result['text']}")
|
||||
print(f"\nConfidence: {result['confidence']:.4f}")
|
||||
print(f"Lines detected: {result.get('line_count', 0)}")
|
||||
print(f"Time: {result.get('processing_time_ms', 0):.2f}ms")
|
||||
@@ -0,0 +1,183 @@
|
||||
#!/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
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Tesseract OCR wrapper with optimized configuration for mixed Chinese/English content
|
||||
"""
|
||||
|
||||
import logging
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
try:
|
||||
import cv2
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
except ImportError:
|
||||
print("[ERROR] Install dependencies: pip install opencv-python numpy pillow")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TesseractOCR:
|
||||
"""Tesseract OCR processor with preprocessing pipeline"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
verbose: bool = False,
|
||||
lang: str = 'chi_sim+eng',
|
||||
psm: int = 6,
|
||||
oem: int = 3
|
||||
):
|
||||
"""
|
||||
Initialize Tesseract processor.
|
||||
|
||||
Args:
|
||||
verbose: Enable detailed logging
|
||||
lang: Tesseract language code (e.g., 'eng', 'chi_sim', 'chi_sim+eng')
|
||||
psm: Page segmentation mode (default: 6, uniform block)
|
||||
oem: OCR engine mode (default: 3, LSTM only)
|
||||
"""
|
||||
self.verbose = verbose
|
||||
self.lang = lang
|
||||
self.psm = psm
|
||||
self.oem = oem
|
||||
|
||||
# Check tesseract availability
|
||||
self._check_tesseract()
|
||||
|
||||
def _check_tesseract(self) -> bool:
|
||||
"""Check if tesseract is installed"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['tesseract', '--version'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5
|
||||
)
|
||||
if self.verbose:
|
||||
logger.info(f"Tesseract version: {result.stdout.split()[2]}")
|
||||
return True
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError, IndexError):
|
||||
logger.error("Tesseract not found. Install with:")
|
||||
logger.error(" macOS: brew install tesseract")
|
||||
logger.error(" Ubuntu: sudo apt install tesseract-ocr")
|
||||
logger.error(" Windows: Download from https://github.com/UB-Mannheim/tesseract/wiki")
|
||||
return False
|
||||
|
||||
def _preprocess_image(self, image_path: str) -> str:
|
||||
"""
|
||||
Apply preprocessing pipeline.
|
||||
|
||||
Returns:
|
||||
Path to processed image
|
||||
"""
|
||||
if self.verbose:
|
||||
logger.info("Preprocessing image...")
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
# Read image
|
||||
img = cv2.imread(image_path)
|
||||
if img is None:
|
||||
raise ValueError(f"Could not load image: {image_path}")
|
||||
|
||||
# Convert to grayscale
|
||||
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
||||
|
||||
# Apply bilateral filter (noise reduction + edge preservation)
|
||||
bilateral = cv2.bilateralFilter(gray, 9, 75, 75)
|
||||
|
||||
# Adaptive thresholding for low contrast images
|
||||
min_val, max_val, _, _ = cv2.minMaxLoc(bilateral)
|
||||
contrast = max_val - min_val
|
||||
|
||||
if contrast < 100: # Low contrast
|
||||
processed = cv2.adaptiveThreshold(
|
||||
bilateral, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
|
||||
cv2.THRESH_BINARY, 11, 2
|
||||
)
|
||||
else:
|
||||
processed = bilateral
|
||||
|
||||
# Save processed image (temp file)
|
||||
input_path = Path(image_path)
|
||||
processed_path = input_path.parent / f"{input_path.stem}_tess_processed.png"
|
||||
cv2.imwrite(str(processed_path), processed)
|
||||
|
||||
if self.verbose:
|
||||
logger.info(f"Preprocessing completed in {time.time() - start_time:.2f}s")
|
||||
|
||||
return str(processed_path)
|
||||
|
||||
def _run_tesseract(
|
||||
self,
|
||||
image_path: str,
|
||||
lang: Optional[str] = None,
|
||||
psm: Optional[int] = None,
|
||||
oem: Optional[int] = None
|
||||
) -> Dict:
|
||||
"""
|
||||
Run Tesseract OCR.
|
||||
|
||||
Returns:
|
||||
Dict with text, confidence, timing
|
||||
"""
|
||||
if lang is None:
|
||||
lang = self.lang
|
||||
if psm is None:
|
||||
psm = self.psm
|
||||
if oem is None:
|
||||
oem = self.oem
|
||||
|
||||
# Run tesseract
|
||||
cmd = [
|
||||
'tesseract', image_path, 'stdout',
|
||||
'-l', lang,
|
||||
'--psm', str(psm),
|
||||
'--oem', str(oem),
|
||||
'_stdout'
|
||||
]
|
||||
|
||||
if self.verbose:
|
||||
logger.info(f"Running: {' '.join(cmd)}")
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60
|
||||
)
|
||||
|
||||
processing_time = time.time() - start_time
|
||||
|
||||
if result.returncode != 0:
|
||||
logger.error(f"Tesseract error: {result.stderr}")
|
||||
return {
|
||||
'text': '',
|
||||
'confidence': 0.0,
|
||||
'error': result.stderr,
|
||||
'processing_time_ms': processing_time * 1000
|
||||
}
|
||||
|
||||
# Extract confidence if available
|
||||
text = result.stdout.strip()
|
||||
confidence = self._estimate_confidence(text)
|
||||
|
||||
return {
|
||||
'text': text,
|
||||
'confidence': confidence,
|
||||
'processing_time_ms': processing_time * 1000
|
||||
}
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
return {
|
||||
'text': '',
|
||||
'confidence': 0.0,
|
||||
'error': 'Tesseract timeout',
|
||||
'processing_time_ms': 60000
|
||||
}
|
||||
|
||||
def _estimate_confidence(self, text: str) -> float:
|
||||
"""
|
||||
Estimate OCR confidence from output quality.
|
||||
|
||||
Simple heuristic: longer text with fewer garbage characters = higher confidence
|
||||
"""
|
||||
if not text:
|
||||
return 0.0
|
||||
|
||||
# Count printable characters
|
||||
printable = sum(1 for c in text if c.isprintable() or c in '\n\r\t')
|
||||
|
||||
# Length factor (more text = more reliable)
|
||||
length_factor = min(1.0, len(text) / 100)
|
||||
|
||||
# Quality factor
|
||||
quality = printable / max(len(text), 1)
|
||||
|
||||
# Combined score
|
||||
confidence = (0.6 * quality) + (0.4 * length_factor)
|
||||
|
||||
return round(confidence, 2)
|
||||
|
||||
def extract(self, image_path: str) -> Dict:
|
||||
"""
|
||||
Extract text from image using Tesseract.
|
||||
|
||||
Args:
|
||||
image_path: Path to input image
|
||||
|
||||
Returns:
|
||||
Dict with text, confidence, processing time
|
||||
"""
|
||||
if self.verbose:
|
||||
logger.info(f"Processing: {image_path}")
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
# Preprocess
|
||||
processed_path = self._preprocess_image(image_path)
|
||||
|
||||
try:
|
||||
# Run OCR
|
||||
result = self._run_tesseract(
|
||||
processed_path,
|
||||
lang=self.lang,
|
||||
psm=self.psm,
|
||||
oem=self.oem
|
||||
)
|
||||
|
||||
# Clean up temp file
|
||||
Path(processed_path).unlink(missing_ok=True)
|
||||
|
||||
result['processing_time_ms'] = round(time.time() - start_time, 2)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
Path(processed_path).unlink(missing_ok=True)
|
||||
return {
|
||||
'text': '',
|
||||
'confidence': 0.0,
|
||||
'error': str(e),
|
||||
'processing_time_ms': (time.time() - start_time) * 1000
|
||||
}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description='Tesseract OCR wrapper')
|
||||
parser.add_argument('image', help='Image file to process')
|
||||
parser.add_argument('--lang', default='chi_sim+eng', help='Language code')
|
||||
parser.add_argument('--verbose', '-v', action='store_true')
|
||||
args = parser.parse_args()
|
||||
|
||||
processor = TesseractOCR(verbose=args.verbose, lang=args.lang)
|
||||
result = processor.extract(args.image)
|
||||
|
||||
print(f"\nText:\n{result['text']}")
|
||||
print(f"\nConfidence: {result['confidence']:.2%}")
|
||||
print(f"Time: {result.get('processing_time_ms', 0):.2f}ms")
|
||||
@@ -0,0 +1,311 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Super OCR - Main entry point with multi-engine parallel support
|
||||
|
||||
Usage:
|
||||
python main.py --image path/to/image.png [--engine auto|tesseract|paddle|macvision|all]
|
||||
python main.py --images ./images/*.png [--output ./results]
|
||||
|
||||
Examples:
|
||||
# Auto mode (recommended) - runs all available engines on macOS
|
||||
python main.py --image screenshot.png
|
||||
|
||||
# Force Tesseract
|
||||
python main.py --image document.jpg --engine tesseract
|
||||
|
||||
# Force PaddleOCR (high accuracy Chinese)
|
||||
python main.py --image chinese_menu.png --engine paddle
|
||||
|
||||
# Force MacVision (macOS only)
|
||||
python main.py --image document.png --engine macvision
|
||||
|
||||
# Run all available engines (macOS: tesseract + paddle + macvision)
|
||||
python main.py --image complex_doc.png --engine all
|
||||
|
||||
# Batch mode with verbose output
|
||||
python main.py --images ./invoices/*.png --output ./results --verbose
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
# Add parent to path for imports when running from skill directory
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
try:
|
||||
from engine.selector import select_engine, get_available_engines, select_best_result
|
||||
from engine.tesseract import TesseractOCR
|
||||
from engine.paddle import PaddleOCR
|
||||
from output_formatter import format_output
|
||||
from dependencies import check_all_dependencies
|
||||
except ImportError as e:
|
||||
print(f"[ERROR] Import failed: {e}")
|
||||
print("\n[INSTALL INSTRUCTIONS]")
|
||||
print("="*60)
|
||||
print("Missing dependencies detected. Please install:")
|
||||
print()
|
||||
print(" pip install paddleocr paddlepaddle pytesseract pillow opencv-python numpy")
|
||||
print()
|
||||
print("Or for macOS with Tesseract:")
|
||||
print()
|
||||
print(" brew install tesseract")
|
||||
print(" pip install paddleocr paddlepaddle pytesseract pillow opencv-python numpy")
|
||||
print()
|
||||
print("For other platforms, see: https://github.com/openclaw/super-ocr")
|
||||
print("="*60)
|
||||
sys.exit(1)
|
||||
|
||||
# Try to import MacVision if on macOS
|
||||
macvision_available = False
|
||||
if sys.platform == 'darwin':
|
||||
try:
|
||||
from engine.macvision import MacVisionOCR
|
||||
macvision_available = True
|
||||
print("[INFO] MacVision OCR available")
|
||||
except ImportError:
|
||||
print("[WARN] MacVision OCR not available (pip install pyobjc)")
|
||||
|
||||
class OCRProcessor:
|
||||
"""Main OCR processor with multi-engine parallel support"""
|
||||
|
||||
def __init__(self, engine: str = 'auto', verbose: bool = False):
|
||||
"""
|
||||
Initialize OCR processor.
|
||||
|
||||
Args:
|
||||
engine: 'auto', 'tesseract', 'paddle', 'macvision', or 'all'
|
||||
verbose: Enable detailed logging
|
||||
"""
|
||||
self.engine = engine.lower()
|
||||
self.verbose = verbose
|
||||
self.engines_to_use = []
|
||||
self.processors = {}
|
||||
|
||||
def _select_and_init(self, image_path: str) -> None:
|
||||
"""Select engines based on content and initialize all"""
|
||||
self.engines_to_use = select_engine(image_path, self.engine)
|
||||
|
||||
if self.verbose:
|
||||
print(f"[INFO] Using engines: {', '.join(self.engines_to_use)}")
|
||||
|
||||
# Initialize selected engines
|
||||
for eng in self.engines_to_use:
|
||||
try:
|
||||
if eng == 'tesseract':
|
||||
self.processors['tesseract'] = TesseractOCR(verbose=self.verbose)
|
||||
elif eng == 'paddle':
|
||||
self.processors['paddle'] = PaddleOCR(verbose=self.verbose)
|
||||
elif eng == 'macvision':
|
||||
if macvision_available:
|
||||
self.processors['macvision'] = MacVisionOCR(verbose=self.verbose)
|
||||
else:
|
||||
if self.verbose:
|
||||
print(f"[WARN] MacVision not available on this platform")
|
||||
else:
|
||||
if self.verbose:
|
||||
print(f"[WARN] Unknown engine: {eng}")
|
||||
except Exception as e:
|
||||
if self.verbose:
|
||||
print(f"[ERROR] Failed to initialize {eng}: {e}")
|
||||
|
||||
def extract_parallel(self, image_path: str) -> Dict:
|
||||
"""
|
||||
Extract text from image using multiple engines in parallel.
|
||||
|
||||
Args:
|
||||
image_path: Path to input image
|
||||
|
||||
Returns:
|
||||
Dict with text, confidence, selected_engine, average confidence, etc.
|
||||
"""
|
||||
if not self.processors:
|
||||
self._select_and_init(image_path)
|
||||
|
||||
if not self.processors:
|
||||
return {
|
||||
'text': '',
|
||||
'confidence': 0.0,
|
||||
'error': 'No engines available',
|
||||
'processing_time_ms': 0
|
||||
}
|
||||
|
||||
start_time = time.time()
|
||||
results = []
|
||||
|
||||
def run_engine(engine_name: str, processor):
|
||||
try:
|
||||
result = processor.extract(image_path)
|
||||
result['engine'] = engine_name
|
||||
return result
|
||||
except Exception as e:
|
||||
return {
|
||||
'engine': engine_name,
|
||||
'text': '',
|
||||
'confidence': 0.0,
|
||||
'error': str(e),
|
||||
'processing_time_ms': 0
|
||||
}
|
||||
|
||||
# Run engines in parallel
|
||||
with ThreadPoolExecutor(max_workers=len(self.processors)) as executor:
|
||||
futures = {
|
||||
executor.submit(run_engine, name, proc): name
|
||||
for name, proc in self.processors.items()
|
||||
}
|
||||
|
||||
for future in as_completed(futures):
|
||||
result = future.result()
|
||||
results.append(result)
|
||||
|
||||
if self.verbose:
|
||||
engine_name = result.get('engine', 'unknown')
|
||||
if result.get('error'):
|
||||
print(f"[{engine_name}] Error: {result['error']}")
|
||||
else:
|
||||
print(f"[{engine_name}] Confidence: {result.get('confidence', 0):.2%}, Time: {result.get('processing_time_ms', 0):.2f}ms")
|
||||
|
||||
# Select best result using agent择优 logic
|
||||
best = select_best_result(
|
||||
results,
|
||||
preferred_engine='paddle' # PaddleOCR preferred for Chinese
|
||||
)
|
||||
|
||||
# Add summary info
|
||||
best['processing_time_ms'] = round((time.time() - start_time) * 1000, 2)
|
||||
best['total_engines'] = len(results)
|
||||
best['engines_used'] = self.engines_to_use
|
||||
|
||||
return best
|
||||
|
||||
def extract(self, image_path: str) -> Dict:
|
||||
"""
|
||||
Extract text from image (legacy single-engine mode).
|
||||
|
||||
Args:
|
||||
image_path: Path to input image
|
||||
|
||||
Returns:
|
||||
Dict with text, confidence, engine, timing info
|
||||
"""
|
||||
return self.extract_parallel(image_path)
|
||||
|
||||
def batch_extract(self, image_paths: List[str]) -> List[Dict]:
|
||||
"""Process multiple images"""
|
||||
return [self.extract(path) for path in image_paths]
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Super OCR - Multi-engine parallel text extraction with intelligent selection'
|
||||
)
|
||||
|
||||
# Input options
|
||||
input_group = parser.add_mutually_exclusive_group(required=True)
|
||||
input_group.add_argument('--image', help='Single image file to process')
|
||||
input_group.add_argument('--images', nargs='+', help='Multiple image files')
|
||||
|
||||
# Engine selection
|
||||
parser.add_argument(
|
||||
'--engine',
|
||||
choices=['auto', 'tesseract', 'paddle', 'macvision', 'all'],
|
||||
default='auto',
|
||||
help='OCR engine(s) to use (default: auto)'
|
||||
)
|
||||
|
||||
# Output options
|
||||
parser.add_argument(
|
||||
'--output', '-o',
|
||||
help='Output directory for results (default: stdout)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--format',
|
||||
choices=['text', 'json', 'structured'],
|
||||
default='json',
|
||||
help='Output format (default: json)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--verbose', '-v',
|
||||
action='store_true',
|
||||
help='Enable verbose output'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Check dependencies
|
||||
if args.engine in ['auto', 'paddle', 'all']:
|
||||
check_all_dependencies(interactive=False)
|
||||
|
||||
# Warn about macOS
|
||||
if sys.platform != 'darwin' and args.engine in ['macvision', 'all']:
|
||||
print("[WARN] MacVision only available on macOS")
|
||||
|
||||
# Create processor
|
||||
processor = OCRProcessor(
|
||||
engine=args.engine,
|
||||
verbose=args.verbose
|
||||
)
|
||||
|
||||
# Process images
|
||||
if args.image:
|
||||
image_paths = [args.image]
|
||||
else:
|
||||
# Expand glob patterns
|
||||
image_paths = []
|
||||
for pattern in args.images:
|
||||
image_paths.extend(sorted(Path().glob(pattern)))
|
||||
image_paths = [str(p) for p in image_paths]
|
||||
|
||||
if args.verbose:
|
||||
print(f"\n[INFO] Processing {len(image_paths)} image(s)")
|
||||
|
||||
# Extract text
|
||||
results = processor.batch_extract(image_paths)
|
||||
|
||||
# Format and output
|
||||
output_func = format_output(args.format)
|
||||
|
||||
if args.output:
|
||||
# Save to files
|
||||
output_path = Path(args.output)
|
||||
output_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for image_path, result in zip(image_paths, results):
|
||||
stem = Path(image_path).stem
|
||||
output_file = output_path / f"{stem}_ocr.json"
|
||||
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(result, f, ensure_ascii=False, indent=2)
|
||||
|
||||
if args.verbose:
|
||||
print(f"[OK] Saved: {output_file}")
|
||||
else:
|
||||
# Print to stdout
|
||||
for image_path, result in zip(image_paths, results):
|
||||
print(f"\n{'='*60}")
|
||||
print(f"File: {image_path}")
|
||||
print(f"Selected Engine: {result.get('selected_engine', 'unknown')}")
|
||||
print(f"Confidence: {result.get('confidence', 0):.2%}")
|
||||
print(f"Total Engines: {result.get('total_engines', 1)}")
|
||||
print(f"Processes: {', '.join(result.get('engines_used', []))}")
|
||||
print(f"Time: {result.get('processing_time_ms', 0):.2f}ms")
|
||||
print(f"{'='*60}")
|
||||
|
||||
if result.get('error'):
|
||||
print(f"[ERROR] {result['error']}")
|
||||
else:
|
||||
print(output_func(result))
|
||||
|
||||
# Summary
|
||||
total_time = sum(r.get('processing_time_ms', 0) for r in results)
|
||||
if args.verbose and len(results) > 1:
|
||||
print(f"\n[INFO] Processed {len(results)} images in {total_time:.2f}ms")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Output formatter - Convert OCR results to various formats
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import Dict, List
|
||||
|
||||
|
||||
def format_text(result: Dict) -> str:
|
||||
"""Return clean text only"""
|
||||
return result.get('text', '')
|
||||
|
||||
|
||||
def format_json(result: Dict) -> str:
|
||||
"""Return full JSON output"""
|
||||
return json.dumps(result, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def format_structured(result: Dict) -> str:
|
||||
"""
|
||||
Return structured format with key information highlighted.
|
||||
|
||||
Format:
|
||||
---
|
||||
Text: [extracted text]
|
||||
|
||||
Stats:
|
||||
- Engine: [engine used]
|
||||
- Confidence: [score]
|
||||
- Time: [processing time]
|
||||
- Lines: [line count]
|
||||
---
|
||||
"""
|
||||
text = result.get('text', '')
|
||||
engine = result.get('engine', 'unknown')
|
||||
confidence = result.get('confidence', 0)
|
||||
time_ms = result.get('processing_time_ms', 0)
|
||||
line_count = result.get('line_count', len(text.split('\n')))
|
||||
|
||||
# Truncate long text
|
||||
display_text = text
|
||||
if len(text) > 500:
|
||||
display_text = text[:500] + '\n... (truncated)'
|
||||
|
||||
output = [
|
||||
"---",
|
||||
"Text:",
|
||||
display_text,
|
||||
"",
|
||||
"Stats:",
|
||||
f"- Engine: {engine}",
|
||||
f"- Confidence: {confidence:.2%}" if isinstance(confidence, (int, float)) else f"- Confidence: N/A",
|
||||
f"- Time: {time_ms:.2f}ms",
|
||||
f"- Lines: {line_count}",
|
||||
"---"
|
||||
]
|
||||
|
||||
return '\n'.join(output)
|
||||
|
||||
|
||||
def format_verbose(result: Dict) -> str:
|
||||
"""Return all available information"""
|
||||
output = [
|
||||
"=" * 60,
|
||||
"OCR Result (Verbose)",
|
||||
"=" * 60,
|
||||
f"Engine: {result.get('engine', 'N/A')}",
|
||||
f"Confidence: {result.get('confidence', 0):.4f}",
|
||||
f"Processing Time: {result.get('processing_time_ms', 0):.2f}ms",
|
||||
]
|
||||
|
||||
# Error info
|
||||
if 'error' in result and result['error']:
|
||||
output.extend([
|
||||
"",
|
||||
"ERROR:",
|
||||
result['error'],
|
||||
])
|
||||
|
||||
# Text
|
||||
output.extend([
|
||||
"",
|
||||
"Extracted Text:",
|
||||
"-" * 40,
|
||||
result.get('text', ''),
|
||||
"-" * 40,
|
||||
])
|
||||
|
||||
# Detailed results (if available)
|
||||
if 'results' in result and result['results']:
|
||||
output.extend([
|
||||
"",
|
||||
"Detailed Results:",
|
||||
f"{'Text':<30} | {'Confidence':<12} | {'BBox'}"
|
||||
])
|
||||
|
||||
for item in result['results'][:20]: # Limit to 20 lines
|
||||
text = item.get('text', '')[:28]
|
||||
confidence = item.get('confidence', 0)
|
||||
bbox = str(item.get('bbox', []))
|
||||
|
||||
output.append(f"{text:<30} | {confidence:<12.4f} | {bbox}")
|
||||
|
||||
output.append("=" * 60)
|
||||
|
||||
return '\n'.join(output)
|
||||
|
||||
|
||||
# Format registry
|
||||
FORMATTERS = {
|
||||
'text': format_text,
|
||||
'json': format_json,
|
||||
'structured': format_structured,
|
||||
'verbose': format_verbose
|
||||
}
|
||||
|
||||
|
||||
def format_output(format_name: str):
|
||||
"""Get formatter function by name"""
|
||||
return FORMATTERS.get(format_name, format_json)
|
||||
|
||||
|
||||
def get_available_formats() -> List[str]:
|
||||
"""Return list of available output formats"""
|
||||
return list(FORMATTERS.keys())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Test formatter
|
||||
test_result = {
|
||||
'text': '这是一个测试\nAnother line',
|
||||
'engine': 'paddle',
|
||||
'confidence': 0.95,
|
||||
'processing_time_ms': 123.45,
|
||||
'line_count': 2
|
||||
}
|
||||
|
||||
for fmt in get_available_formats():
|
||||
print(f"\n{'#'*60}")
|
||||
print(f"Format: {fmt}")
|
||||
print('#'*60)
|
||||
print(format_output(fmt)(test_result))
|
||||
@@ -0,0 +1 @@
|
||||
# Preprocessing module
|
||||
@@ -0,0 +1,236 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Preprocessor - Image preprocessing utilities for OCR
|
||||
|
||||
This module provides various image preprocessing techniques to improve OCR accuracy:
|
||||
- Denoising
|
||||
- Contrast enhancement
|
||||
- Binarization
|
||||
- Deskew
|
||||
- Resolution enhancement
|
||||
"""
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def denoise_image(image: np.ndarray, h: int = 10) -> np.ndarray:
|
||||
"""
|
||||
Apply denoising to image.
|
||||
|
||||
Args:
|
||||
image: Input image
|
||||
h: Denoising strength (higher = more denoising)
|
||||
|
||||
Returns:
|
||||
Denoised image
|
||||
"""
|
||||
return cv2.fastNlMeansDenoisingColored(image, None, h, h, 7, 21)
|
||||
|
||||
|
||||
def enhance_contrast(image: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Enhance image contrast using CLAHE.
|
||||
|
||||
Args:
|
||||
image: Input image (grayscale or BGR)
|
||||
|
||||
Returns:
|
||||
Contrast-enhanced image
|
||||
"""
|
||||
if len(image.shape) == 3:
|
||||
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
||||
else:
|
||||
gray = image.copy()
|
||||
|
||||
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
|
||||
return clahe.apply(gray)
|
||||
|
||||
|
||||
def binarize_image(image: np.ndarray, method: str = 'adaptive') -> np.ndarray:
|
||||
"""
|
||||
Convert image to binary (black & white).
|
||||
|
||||
Args:
|
||||
image: Input image (grayscale)
|
||||
method: 'adaptive', 'otsu', or 'fixed'
|
||||
|
||||
Returns:
|
||||
Binary image
|
||||
"""
|
||||
if method == 'adaptive':
|
||||
return cv2.adaptiveThreshold(
|
||||
image, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
|
||||
cv2.THRESH_BINARY, 11, 2
|
||||
)
|
||||
elif method == 'otsu':
|
||||
_, thresh = cv2.threshold(image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
|
||||
return thresh
|
||||
else: # fixed
|
||||
_, thresh = cv2.threshold(image, 127, 255, cv2.THRESH_BINARY)
|
||||
return thresh
|
||||
|
||||
|
||||
def deskew_image(image: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Correct image skew.
|
||||
|
||||
Args:
|
||||
image: Input image
|
||||
|
||||
Returns:
|
||||
Deskewed image
|
||||
"""
|
||||
coords = np.column_stack(np.where(image > 0))
|
||||
angle = cv2.minAreaRect(coords)[-1]
|
||||
|
||||
if angle < -45:
|
||||
angle = -(90 + angle)
|
||||
else:
|
||||
angle = -angle
|
||||
|
||||
(h, w) = image.shape[:2]
|
||||
center = (w // 2, h // 2)
|
||||
M = cv2.getRotationMatrix2D(center, angle, 1.0)
|
||||
|
||||
return cv2.warpAffine(image, M, (w, h), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE)
|
||||
|
||||
|
||||
def resize_image(image: np.ndarray, scale: float = 2.0) -> np.ndarray:
|
||||
"""
|
||||
Resize image for better OCR.
|
||||
|
||||
Args:
|
||||
image: Input image
|
||||
scale: Scale factor (e.g., 2.0 = 2x larger)
|
||||
|
||||
Returns:
|
||||
Resized image
|
||||
"""
|
||||
new_size = tuple(int(dim * scale) for dim in image.shape[:2][::-1])
|
||||
return cv2.resize(image, new_size, interpolation=cv2.INTER_CUBIC)
|
||||
|
||||
|
||||
def preprocess_pipeline(
|
||||
image: np.ndarray,
|
||||
denoise: bool = True,
|
||||
enhance: bool = True,
|
||||
binarize: bool = True,
|
||||
deskew: bool = False,
|
||||
resize_scale: Optional[float] = None
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Apply preprocessing pipeline.
|
||||
|
||||
Args:
|
||||
image: Input image
|
||||
denoise: Apply denoising
|
||||
enhance: Enhance contrast
|
||||
binarize: Binarize image
|
||||
deskew: Correct skew
|
||||
resize_scale: Optional scale factor for resizing
|
||||
|
||||
Returns:
|
||||
Preprocessed image
|
||||
"""
|
||||
output = image.copy()
|
||||
|
||||
steps = []
|
||||
|
||||
if denoise:
|
||||
output = denoise_image(output)
|
||||
steps.append('denoise')
|
||||
|
||||
if enhance:
|
||||
output = enhance_contrast(output)
|
||||
steps.append('enhance')
|
||||
|
||||
if deskew:
|
||||
output = deskew_image(output)
|
||||
steps.append('deskew')
|
||||
|
||||
if resize_scale and resize_scale > 1.0:
|
||||
output = resize_image(output, resize_scale)
|
||||
steps.append(f'resize_{resize_scale}x')
|
||||
|
||||
if binarize and len(output.shape) == 2:
|
||||
output = binarize_image(output)
|
||||
steps.append('binarize')
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def preprocess_file(
|
||||
input_path: str,
|
||||
output_path: Optional[str] = None,
|
||||
**kwargs
|
||||
) -> str:
|
||||
"""
|
||||
Preprocess an image file.
|
||||
|
||||
Args:
|
||||
input_path: Input image file path
|
||||
output_path: Output file path (optional)
|
||||
**kwargs: Preprocessing parameters
|
||||
|
||||
Returns:
|
||||
Output file path
|
||||
"""
|
||||
# Read image
|
||||
image = cv2.imread(input_path)
|
||||
if image is None:
|
||||
raise ValueError(f"Could not load image: {input_path}")
|
||||
|
||||
# Preprocess
|
||||
processed = preprocess_pipeline(image, **kwargs)
|
||||
|
||||
# Save
|
||||
if output_path is None:
|
||||
input_path = Path(input_path)
|
||||
output_path = str(input_path.parent / f"{input_path.stem}_processed{input_path.suffix}")
|
||||
|
||||
cv2.imwrite(output_path, processed)
|
||||
|
||||
return output_path
|
||||
|
||||
|
||||
def quick_preview(image: np.ndarray) -> None:
|
||||
"""
|
||||
Display image preview using OpenCV.
|
||||
|
||||
Args:
|
||||
image: Image to display
|
||||
"""
|
||||
cv2.imshow('Preprocessed Image', image)
|
||||
cv2.waitKey(0)
|
||||
cv2.destroyAllWindows()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description='Image preprocessing for OCR')
|
||||
parser.add_argument('input', help='Input image file')
|
||||
parser.add_argument('--output', '-o', help='Output file path')
|
||||
parser.add_argument('--denoise', action='store_true', default=True, help='Apply denoising')
|
||||
parser.add_argument('--no-denoise', action='store_false', dest='denoise')
|
||||
parser.add_argument('--enhance', action='store_true', default=True, help='Enhance contrast')
|
||||
parser.add_argument('--no-enhance', action='store_false', dest='enhance')
|
||||
parser.add_argument('--binarize', action='store_true', default=True, help='Binarize image')
|
||||
parser.add_argument('--no-binarize', action='store_false', dest='binarize')
|
||||
parser.add_argument('--resize', type=float, help='Resize scale factor (e.g., 2.0)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
output = preprocess_file(
|
||||
args.input,
|
||||
args.output,
|
||||
denoise=args.denoise,
|
||||
enhance=args.enhance,
|
||||
binarize=args.binarize,
|
||||
resize_scale=args.resize
|
||||
)
|
||||
|
||||
print(f"Preprocessed image saved to: {output}")
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for Super OCR multi-engine parallel support
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add parent to path
|
||||
sys.path.insert(0, str(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
print("Testing Super OCR multi-engine parallel imports...")
|
||||
|
||||
try:
|
||||
from engine.selector import select_engine, get_available_engines, select_best_result
|
||||
print("[OK] engine.selector imported")
|
||||
except Exception as e:
|
||||
print(f"[ERROR] engine.selector import failed: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
from engine.tesseract import TesseractOCR
|
||||
print("[OK] engine.tesseract imported")
|
||||
except Exception as e:
|
||||
print(f"[ERROR] engine.tesseract import failed: {e}")
|
||||
|
||||
try:
|
||||
from engine.paddle import PaddleOCR
|
||||
print("[OK] engine.paddle imported")
|
||||
except Exception as e:
|
||||
print(f"[ERROR] engine.paddle import failed: {e}")
|
||||
|
||||
# Test MacVision on macOS
|
||||
if sys.platform == 'darwin':
|
||||
try:
|
||||
from engine.macvision import MacVisionOCR
|
||||
print("[OK] engine.macvision imported")
|
||||
except (ImportError, RuntimeError) as e:
|
||||
print(f"[WARN] engine.macvision: {e}")
|
||||
else:
|
||||
print("[SKIP] engine.macvision (not on macOS)")
|
||||
|
||||
print("\nTesting engine selector...")
|
||||
|
||||
# Test available engines
|
||||
print(f"[INFO] Available engines: {', '.join(get_available_engines('test.png'))}")
|
||||
|
||||
test_cases = [
|
||||
('screenshot.png', 'auto', ['tesseract']),
|
||||
('chinese_menu.png', 'auto', None), # All engines
|
||||
('invoice.jpg', 'auto', None), # All engines
|
||||
('document.png', 'tesseract', ['tesseract']),
|
||||
]
|
||||
|
||||
for image, engine, expected in test_cases:
|
||||
result = select_engine(image, engine)
|
||||
if expected:
|
||||
status = "OK" if result == expected else "FAIL"
|
||||
print(f"[{status}] {image} + {engine} => {result} (expected {expected})")
|
||||
else:
|
||||
print(f"[OK] {image} + {engine} => {result}")
|
||||
|
||||
# Test best result selection
|
||||
print("\nTesting best result selection...")
|
||||
test_results = [
|
||||
{'text': 'test1', 'confidence': 0.85, 'engine': 'tesseract'},
|
||||
{'text': 'test2', 'confidence': 0.92, 'engine': 'paddle'},
|
||||
{'text': 'test3', 'confidence': 0.95, 'engine': 'macvision'},
|
||||
]
|
||||
|
||||
best = select_best_result(test_results)
|
||||
print(f"[OK] Selected: {best.get('selected_engine')} with score {best.get('score', 0):.4f}")
|
||||
|
||||
print("\n[OK] All imports successful!")
|
||||
Reference in New Issue
Block a user