OCR_Vehicle_01 / src /ocr /layout_engine.py
Esketch's picture
v38: Revert to single Korean OCR engine - fix cpu-basic timeout
9d3eed6
Raw
History Blame Contribute Delete
9.21 kB
# -*- coding: utf-8 -*-
"""
PP-Structure layout analysis engine for document structure recognition.
Detects tables, text regions, and key-value pairs in vehicle registration certificates.
Supports PaddleOCR 2.x (PPStructure) and 3.x (PPStructureV3) APIs.
"""
import logging
import gc
logger = logging.getLogger(__name__)
class LayoutEngine:
"""
PP-Structure based layout analysis engine (singleton).
Extracts document structure: tables (as HTML), text regions with bboxes.
"""
_instance = None
_initialized = False
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if LayoutEngine._initialized:
return
self.engine = None
self.enabled = False
self._api_version = None
try:
self._init_engine()
except ImportError:
logger.warning("PP-Structure not available. Layout analysis disabled.")
except Exception as e:
logger.error(f"Failed to init PP-Structure: {e}", exc_info=True)
finally:
LayoutEngine._initialized = True
def _init_engine(self):
"""Initialize PP-Structure engine, auto-detecting API version."""
import paddleocr
version = getattr(paddleocr, '__version__', '2.0.0')
major_version = int(version.split('.')[0])
if major_version >= 3:
self._init_v3()
else:
self._init_v2()
def _init_v2(self):
"""PaddleOCR 2.x: PPStructure class.
Note: PP-Structure layout models only support 'en' and 'ch'.
We use 'ch' because CJK layout models handle Korean document structure well.
OCR text recognition within PP-Structure will use Chinese,
but we only use the layout/table structure — actual text comes from PaddleOCR korean engine.
"""
from paddleocr import PPStructure
logger.info("Initializing PP-Structure (2.x API, lang=ch for layout)...")
self.engine = PPStructure(
layout=True,
table=True,
ocr=False,
show_log=False,
lang='ch',
)
self._api_version = '2.x'
self.enabled = True
logger.info("PP-Structure 2.x initialized.")
def _init_v3(self):
"""PaddleOCR 3.x: PPStructureV3 or table pipeline."""
try:
from paddleocr import PPStructureV3
logger.info("Initializing PP-StructureV3 (3.x API)...")
self.engine = PPStructureV3()
self._api_version = '3.x'
self.enabled = True
logger.info("PP-StructureV3 initialized.")
except ImportError:
# Fallback: some 3.x versions may not have PPStructureV3
logger.warning("PPStructureV3 not found in 3.x. Layout analysis disabled.")
def analyze(self, image_path):
"""
Analyze document layout and extract structured regions.
Args:
image_path: Path to preprocessed image file
Returns:
dict: {
'tables': [{'bbox': (x1,y1,x2,y2), 'html': str, 'cells': list}],
'text_regions': [{'bbox': (x1,y1,x2,y2), 'text': str, 'confidence': float}],
'raw_regions': list # all detected regions with type info
}
"""
if not self.enabled or not self.engine:
return {'tables': [], 'text_regions': [], 'raw_regions': []}
try:
if self._api_version == '3.x':
return self._analyze_v3(image_path)
else:
return self._analyze_v2(image_path)
except Exception as e:
logger.error(f"Layout analysis failed: {e}", exc_info=True)
return {'tables': [], 'text_regions': [], 'raw_regions': []}
def _analyze_v2(self, image_path):
"""PP-Structure 2.x analysis."""
import cv2
img = cv2.imread(image_path)
if img is None:
logger.warning(f"Cannot read image: {image_path}")
return {'tables': [], 'text_regions': [], 'raw_regions': []}
result = self.engine(img)
if not result:
return {'tables': [], 'text_regions': [], 'raw_regions': []}
tables = []
text_regions = []
for region in result:
region_type = region.get('type', '').lower()
bbox = tuple(region.get('bbox', [0, 0, 0, 0]))
res = region.get('res', None)
if region_type == 'table' and res:
html = res.get('html', '') if isinstance(res, dict) else ''
cells = self._parse_table_html(html)
tables.append({
'bbox': bbox,
'html': html,
'cells': cells,
})
elif region_type in ('text', 'title', 'header'):
# res is list of (box, (text, confidence))
if isinstance(res, list):
for item in res:
try:
text_info = item[1] if len(item) >= 2 else item
if isinstance(text_info, (list, tuple)) and len(text_info) >= 2:
text_regions.append({
'bbox': bbox,
'text': str(text_info[0]),
'confidence': float(text_info[1]),
'region_type': region_type,
})
except (IndexError, TypeError, ValueError):
continue
logger.info(f"Layout analysis: {len(tables)} tables, {len(text_regions)} text regions")
return {'tables': tables, 'text_regions': text_regions, 'raw_regions': result}
def _analyze_v3(self, image_path):
"""PP-StructureV3 analysis."""
result = self.engine.predict(image_path)
if not result:
return {'tables': [], 'text_regions': [], 'raw_regions': []}
tables = []
text_regions = []
# V3 returns different structure - adapt based on actual output
try:
for item in result:
if hasattr(item, 'json'):
data = item.json if isinstance(item.json, dict) else {}
elif isinstance(item, dict):
data = item
else:
continue
res_list = data.get('res', [])
if isinstance(res_list, list):
for region in res_list:
if not isinstance(region, dict):
continue
region_type = region.get('type', '').lower()
bbox = tuple(region.get('bbox', [0, 0, 0, 0]))
if region_type == 'table':
html = region.get('html', '')
cells = self._parse_table_html(html)
tables.append({'bbox': bbox, 'html': html, 'cells': cells})
elif region_type in ('text', 'title', 'header'):
text_regions.append({
'bbox': bbox,
'text': region.get('text', ''),
'confidence': float(region.get('score', 0.0)),
'region_type': region_type,
})
except Exception as e:
logger.warning(f"V3 result parsing error: {e}", exc_info=True)
logger.info(f"Layout V3: {len(tables)} tables, {len(text_regions)} text regions")
return {'tables': tables, 'text_regions': text_regions, 'raw_regions': list(result)}
@staticmethod
def _parse_table_html(html):
"""
Parse table HTML into list of cell dicts.
Returns: [{'row': int, 'col': int, 'text': str}]
"""
if not html:
return []
cells = []
try:
# Simple regex-based HTML table parser (no lxml dependency)
import re
rows = re.findall(r'<tr>(.*?)</tr>', html, re.DOTALL)
for row_idx, row_html in enumerate(rows):
col_idx = 0
for cell_match in re.finditer(r'<t[dh][^>]*>(.*?)</t[dh]>', row_html, re.DOTALL):
cell_text = re.sub(r'<[^>]+>', '', cell_match.group(1)).strip()
if cell_text:
cells.append({
'row': row_idx,
'col': col_idx,
'text': cell_text,
})
col_idx += 1
except Exception as e:
logger.debug(f"Table HTML parse error: {e}")
return cells
@classmethod
def cleanup(cls):
"""Cleanup PP-Structure resources."""
if cls._instance and cls._instance.engine:
del cls._instance.engine
cls._instance.engine = None
cls._instance = None
cls._initialized = False
gc.collect()