Spaces:
Sleeping
Sleeping
File size: 6,300 Bytes
d9106f7 e87916b d9106f7 e87916b d9106f7 e87916b d9106f7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | """
PaddleOCR Processor
===================
Alternative to Tesseract (see ocr_processor.py) backed by PaddleOCR
(Apache 2.0, https://github.com/PaddlePaddle/PaddleOCR).
Why this is a separate, simpler class rather than another Tesseract config:
PaddleOCR's detector locates text at the phrase/line level directly, so none
of ocr_processor.py's word->phrase grouping, gap-splitting, or tick-mark
height filtering is needed here — those all exist specifically to work
around Tesseract returning individual words with no semantic grouping.
Classification (dimensions/room_names/numbers/labels) is shared via
ocr_classification.py so both engines bucket text identically.
Status: NOT YET BENCHMARKED. Model weights could not be downloaded in the
sandboxed dev environment this was written in — see CLAUDE.md for why
(corporate TLS-inspecting proxy whose root CA isn't in Python's default
trust store). The API calls below are verified against the installed
paddleocr==3.7.0 / paddlex package source directly (constructor params,
and the exact result schema: `rec_texts`/`rec_scores`/`rec_boxes`, with
`rec_boxes[i] == [left, top, right, bottom]` — see
venv/lib/python3.12/site-packages/paddlex/inference/pipelines/ocr/pipeline.py
and .../components/common/convert_points_and_boxes.py), but real accuracy
on this project's sample images has not been measured. Run
`benchmark/run_benchmark.py` before relying on this over Tesseract.
"""
from __future__ import annotations
import logging
import os
from typing import Any, Dict, List, Optional
import numpy as np
from .ocr_classification import classify, clean_elements, empty_result
logger = logging.getLogger(__name__)
# PP-OCRv6's detection/recognition models crash under paddlex's default CPU
# MKLDNN/PIR execution path on this Windows/CPU combo:
# NotImplementedError: (Unimplemented) ConvertPirAttribute2RuntimeAttribute
# not support [pir::ArrayAttribute<pir::DoubleAttribute>]
# Neither model is in paddlex's MKLDNN_BLOCKLIST (an upstream gap), so the
# workaround is disabling MKLDNN-by-default before paddlex's flags module
# is imported. Must happen before `import paddleocr`, not after.
os.environ.setdefault("PADDLE_PDX_ENABLE_MKLDNN_BYDEFAULT", "False")
try:
from paddleocr import PaddleOCR as _PaddleOCREngine
_PADDLEOCR_AVAILABLE = True
_PADDLEOCR_ERROR: Optional[str] = None
except ImportError as _e:
_PADDLEOCR_AVAILABLE = False
_PADDLEOCR_ERROR = f"paddleocr / paddlepaddle not installed: {_e}"
class PaddleOCRProcessor:
"""
Drop-in alternative to OCRProcessor — same extract_text() contract
(full_text / element_count / elements / classified), so main.py can
swap between engines without any other code changing.
"""
def __init__(self, lang: str = "en") -> None:
# PaddleOCR loads one recognition model per language (unlike
# Tesseract's combined "jpn+eng"); "en" covers this project's
# sample images, pass lang="japan" for Japanese-only drawings.
self._lang = lang
self._engine = None # lazy-loaded on first use — downloads weights
@property
def is_available(self) -> bool:
return _PADDLEOCR_AVAILABLE
@property
def unavailable_reason(self) -> Optional[str]:
return _PADDLEOCR_ERROR
def extract_text(self, image: np.ndarray) -> Dict[str, Any]:
"""
Parameters
----------
image : enhanced binary or grayscale (or BGR) image
Returns
-------
Same schema as OCRProcessor.extract_text() — see that module's
docstring.
"""
if not _PADDLEOCR_AVAILABLE:
logger.warning("paddleocr not installed – OCR skipped")
return empty_result("paddleocr not installed")
try:
engine = self._get_engine()
results = engine.predict(image)
except Exception as exc:
logger.exception("PaddleOCR error")
return empty_result(str(exc))
if not results:
return empty_result("no text regions detected")
result = results[0]
raw_elements = self._to_elements(
result.get("rec_texts", []),
result.get("rec_scores", []),
result.get("rec_boxes", []),
)
elements = clean_elements(raw_elements)
classified = classify(elements)
return {
"full_text": "\n".join(e["text"] for e in elements),
"element_count": len(elements),
"elements": elements,
"classified": classified,
}
# ------------------------------------------------------------------ #
# Private helpers #
# ------------------------------------------------------------------ #
def _get_engine(self):
if self._engine is not None:
return self._engine
logger.info("Loading PaddleOCR engine (lang=%s) — downloads weights on first use", self._lang)
self._engine = _PaddleOCREngine(
lang=self._lang,
# Our own enhancement pipeline already deskews/upscales the
# image, so skip PaddleOCR's own document-orientation
# classification and unwarping sub-pipelines — one less model
# to download and one less inference pass per image.
use_doc_orientation_classify=False,
use_doc_unwarping=False,
use_textline_orientation=False,
)
return self._engine
@staticmethod
def _to_elements(
texts: List[str], scores: List[float], boxes: Any
) -> List[Dict[str, Any]]:
elements: List[Dict[str, Any]] = []
for text, score, box in zip(texts, scores, boxes):
text = str(text).strip()
if not text:
continue
x1, y1, x2, y2 = (float(v) for v in box)
elements.append({
"text": text,
"confidence": round(float(score) * 100), # match Tesseract's 0-100 scale
"x": int(round(x1)),
"y": int(round(y1)),
"width": int(round(x2 - x1)),
"height": int(round(y2 - y1)),
})
return elements
|