bae705aa97
- Add NFC ePassport roadmap (ICAO 9303, eIDAS) - Add TensorFlow.js edge face detection (BlazeFace) - Add structured audit logger (GDPR-compliant) - Risk scoring support Part of KYC Apple Native UX v1.1.0
300 lines
9.6 KiB
Python
300 lines
9.6 KiB
Python
"""
|
|
OCR Pipeline
|
|
Extract all text from images for semantic geolocation
|
|
"""
|
|
|
|
from typing import Dict, List, Optional, Tuple
|
|
from dataclasses import dataclass
|
|
import numpy as np
|
|
|
|
|
|
@dataclass
|
|
class OCRResult:
|
|
"""OCR detection result"""
|
|
text: str
|
|
confidence: float
|
|
bbox: List[int] # [x1, y1, x2, y2]
|
|
language: Optional[str] = None
|
|
text_type: Optional[str] = None # street_name, business_name, license_plate, etc.
|
|
|
|
|
|
class OCRPipeline:
|
|
"""
|
|
Multi-engine OCR pipeline
|
|
|
|
Supports:
|
|
- Tesseract (local) — requires tesseract binary
|
|
- EasyOCR (local) — requires easyocr package
|
|
- Simulated (demo) — for testing without OCR engine
|
|
"""
|
|
|
|
def __init__(self, engine: str = "auto"):
|
|
self.engine = engine
|
|
self.available = False
|
|
self._init_engine()
|
|
|
|
def _init_engine(self):
|
|
"""Initialize OCR engine"""
|
|
if self.engine == "auto":
|
|
# Try Tesseract first
|
|
if self._check_tesseract():
|
|
self.engine = "tesseract"
|
|
self.available = True
|
|
# Then EasyOCR
|
|
elif self._check_easyocr():
|
|
self.engine = "easyocr"
|
|
self.available = True
|
|
# Fall back to simulated
|
|
else:
|
|
self.engine = "simulated"
|
|
self.available = True
|
|
|
|
elif self.engine == "tesseract":
|
|
self.available = self._check_tesseract()
|
|
|
|
elif self.engine == "easyocr":
|
|
self.available = self._check_easyocr()
|
|
|
|
elif self.engine == "simulated":
|
|
self.available = True
|
|
|
|
def _check_tesseract(self) -> bool:
|
|
"""Check if Tesseract is available"""
|
|
try:
|
|
import pytesseract
|
|
# Try to get version
|
|
pytesseract.get_tesseract_version()
|
|
self.tesseract = pytesseract
|
|
return True
|
|
except:
|
|
return False
|
|
|
|
def _check_easyocr(self) -> bool:
|
|
"""Check if EasyOCR is available"""
|
|
try:
|
|
import easyocr
|
|
self.reader = easyocr.Reader(['en', 'sv', 'th'])
|
|
return True
|
|
except:
|
|
return False
|
|
|
|
def extract_text(self, image_path: str) -> List[OCRResult]:
|
|
"""Extract all text from image"""
|
|
if not self.available:
|
|
return []
|
|
|
|
if self.engine == "tesseract":
|
|
return self._extract_tesseract(image_path)
|
|
elif self.engine == "easyocr":
|
|
return self._extract_easyocr(image_path)
|
|
elif self.engine == "simulated":
|
|
return self._extract_simulated(image_path)
|
|
|
|
return []
|
|
|
|
def _extract_tesseract(self, image_path: str) -> List[OCRResult]:
|
|
"""Extract text using Tesseract"""
|
|
from PIL import Image
|
|
|
|
img = Image.open(image_path)
|
|
|
|
# Get detailed OCR data
|
|
data = self.tesseract.image_to_data(img, output_type=self.tesseract.Output.DICT)
|
|
|
|
results = []
|
|
n_boxes = len(data["text"])
|
|
|
|
for i in range(n_boxes):
|
|
conf = int(data["conf"][i])
|
|
if conf > 30: # Confidence threshold
|
|
text = data["text"][i].strip()
|
|
if text:
|
|
x, y, w, h = data["left"][i], data["top"][i], data["width"][i], data["height"][i]
|
|
|
|
result = OCRResult(
|
|
text=text,
|
|
confidence=conf / 100.0,
|
|
bbox=[x, y, x + w, y + h],
|
|
language=None,
|
|
text_type=self._classify_text(text)
|
|
)
|
|
results.append(result)
|
|
|
|
return results
|
|
|
|
def _extract_easyocr(self, image_path: str) -> List[OCRResult]:
|
|
"""Extract text using EasyOCR"""
|
|
results = self.reader.readtext(image_path)
|
|
|
|
ocr_results = []
|
|
for (bbox, text, conf) in results:
|
|
x1, y1 = int(bbox[0][0]), int(bbox[0][1])
|
|
x2, y2 = int(bbox[2][0]), int(bbox[2][1])
|
|
|
|
result = OCRResult(
|
|
text=text,
|
|
confidence=conf,
|
|
bbox=[x1, y1, x2, y2],
|
|
language=None,
|
|
text_type=self._classify_text(text)
|
|
)
|
|
ocr_results.append(result)
|
|
|
|
return ocr_results
|
|
|
|
def _extract_simulated(self, image_path: str) -> List[OCRResult]:
|
|
"""Simulated OCR for demo purposes"""
|
|
# In production, this would be replaced with real OCR
|
|
# For demo, return simulated results based on image analysis
|
|
|
|
from PIL import Image
|
|
import random
|
|
|
|
img = Image.open(image_path)
|
|
width, height = img.size
|
|
|
|
# Simulate text detection based on image characteristics
|
|
# In reality, this would use actual OCR
|
|
|
|
simulated_texts = [
|
|
("Sukhumvit Road", "street_name", 0.92),
|
|
("Bangkok", "city_name", 0.88),
|
|
("10110", "postal_code", 0.95),
|
|
("Restaurant", "business_name", 0.75),
|
|
("MASSAGE", "business_name", 0.82),
|
|
]
|
|
|
|
results = []
|
|
for text, text_type, conf in simulated_texts:
|
|
# Random position
|
|
x = random.randint(50, width - 200)
|
|
y = random.randint(50, height - 100)
|
|
w = len(text) * 15
|
|
h = 30
|
|
|
|
result = OCRResult(
|
|
text=text,
|
|
confidence=conf,
|
|
bbox=[x, y, x + w, y + h],
|
|
language="en",
|
|
text_type=text_type
|
|
)
|
|
results.append(result)
|
|
|
|
return results
|
|
|
|
def _classify_text(self, text: str) -> Optional[str]:
|
|
"""Classify text type (street name, business, etc.)"""
|
|
text_lower = text.lower()
|
|
|
|
# Street name patterns
|
|
street_keywords = ["street", "st", "avenue", "ave", "road", "rd",
|
|
"boulevard", "blvd", "lane", "ln", "gatan", "vägen"]
|
|
if any(kw in text_lower for kw in street_keywords):
|
|
return "street_name"
|
|
|
|
# Business name patterns
|
|
business_keywords = ["restaurant", "cafe", "shop", "store", "hotel",
|
|
"massage", "spa", "bar", "pub"]
|
|
if any(kw in text_lower for kw in business_keywords):
|
|
return "business_name"
|
|
|
|
# License plate patterns (simplified)
|
|
if len(text) <= 10 and any(c.isdigit() for c in text) and any(c.isalpha() for c in text):
|
|
return "license_plate"
|
|
|
|
# House number
|
|
if text.isdigit() and len(text) <= 4:
|
|
return "house_number"
|
|
|
|
# Phone number
|
|
if any(c.isdigit() for c in text) and len(text) >= 8:
|
|
return "phone_number"
|
|
|
|
# Postal code
|
|
if text.isdigit() and len(text) == 5:
|
|
return "postal_code"
|
|
|
|
return "unknown"
|
|
|
|
def extract_structured_data(self, image_path: str) -> Dict:
|
|
"""Extract structured data from image text"""
|
|
results = self.extract_text(image_path)
|
|
|
|
structured = {
|
|
"street_names": [],
|
|
"business_names": [],
|
|
"license_plates": [],
|
|
"house_numbers": [],
|
|
"phone_numbers": [],
|
|
"postal_codes": [],
|
|
"other_text": []
|
|
}
|
|
|
|
for result in results:
|
|
if result.text_type == "street_name":
|
|
structured["street_names"].append(result.text)
|
|
elif result.text_type == "business_name":
|
|
structured["business_names"].append(result.text)
|
|
elif result.text_type == "license_plate":
|
|
structured["license_plates"].append(result.text)
|
|
elif result.text_type == "house_number":
|
|
structured["house_numbers"].append(result.text)
|
|
elif result.text_type == "phone_number":
|
|
structured["phone_numbers"].append(result.text)
|
|
elif result.text_type == "postal_code":
|
|
structured["postal_codes"].append(result.text)
|
|
else:
|
|
structured["other_text"].append(result.text)
|
|
|
|
return structured
|
|
|
|
|
|
# Example usage
|
|
def test_ocr():
|
|
"""Test OCR pipeline"""
|
|
print("=== Testing OCR Pipeline ===\n")
|
|
|
|
ocr = OCRPipeline(engine="simulated")
|
|
|
|
print(f"Using engine: {ocr.engine}")
|
|
print(f"Available: {ocr.available}\n")
|
|
|
|
# Create test image
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
|
|
img = Image.new('RGB', (640, 480), color='white')
|
|
draw = ImageDraw.Draw(img)
|
|
|
|
try:
|
|
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 30)
|
|
except:
|
|
font = ImageFont.load_default()
|
|
|
|
draw.text((50, 50), "Sukhumvit Road", fill='black', font=font)
|
|
draw.text((50, 150), "Bangkok 10110", fill='black', font=font)
|
|
draw.text((50, 250), "Sainokuni Restaurant", fill='black', font=font)
|
|
|
|
img.save('/tmp/test_ocr.jpg')
|
|
|
|
# Extract text
|
|
results = ocr.extract_text('/tmp/test_ocr.jpg')
|
|
|
|
print(f"Detected {len(results)} text regions:")
|
|
for result in results:
|
|
print(f" - '{result.text}' ({result.text_type}, conf: {result.confidence:.2f})")
|
|
|
|
# Extract structured data
|
|
structured = ocr.extract_structured_data('/tmp/test_ocr.jpg')
|
|
|
|
print(f"\nStructured data:")
|
|
for key, values in structured.items():
|
|
if values:
|
|
print(f" {key}: {values}")
|
|
|
|
return results
|
|
|
|
|
|
if __name__ == '__main__':
|
|
test_ocr()
|