vlps-demo / src /vlps /ocr.py
ORCMS's picture
VLPS Gradio demo (Docker Space)
c09e882
Raw
History Blame Contribute Delete
4.34 kB
"""
OCR utilities for Phase 3 — Track C (OCR-focused Question Answering).
Wraps EasyOCR for scene-text extraction, caches results to parquet (extraction is
slow), and provides CER/WER helpers (via jiwer) for OCR-quality evaluation.
"""
import os
from pathlib import Path
from typing import Optional
import numpy as np
import pandas as pd
from PIL import Image
# A single global reader is reused across calls — model load is expensive.
_READER = None
_READER_LANGS = None
def get_reader(languages=("en",), gpu: Optional[bool] = None):
"""
Return a cached EasyOCR Reader for the given languages.
languages: tuple of EasyOCR language codes, e.g. ("en",) or ("pt", "en").
"""
global _READER, _READER_LANGS
import easyocr
import torch
langs = tuple(languages)
if _READER is not None and _READER_LANGS == langs:
return _READER
if gpu is None:
gpu = torch.cuda.is_available()
_READER = easyocr.Reader(list(langs), gpu=gpu)
_READER_LANGS = langs
return _READER
def ocr_image(img: Image.Image, reader=None, languages=("en",),
detail: bool = True) -> dict:
"""
Run OCR on a single PIL image.
Returns {'text': joined string, 'tokens': [...], 'confidences': [...]}.
"""
reader = reader or get_reader(languages)
arr = np.array(img.convert("RGB"))
results = reader.readtext(arr, detail=1, paragraph=False)
tokens = [r[1] for r in results]
confs = [float(r[2]) for r in results]
return {
"text": " ".join(tokens),
"tokens": tokens,
"confidences": confs,
}
def extract_ocr(image_lookup: dict, save_path, languages=("en",),
gpu: Optional[bool] = None) -> pd.DataFrame:
"""
Run OCR over an {image_id: PIL.Image} mapping and cache to parquet.
Reloads from cache if save_path already exists.
Columns: [image_id, ocr_text, ocr_tokens, ocr_confidences].
"""
save_path = Path(save_path)
if save_path.exists():
print(f"OCR cache found at {save_path}, loading...")
return pd.read_parquet(save_path)
reader = get_reader(languages, gpu=gpu)
rows = []
n = len(image_lookup)
for i, (image_id, img) in enumerate(image_lookup.items()):
try:
res = ocr_image(img, reader=reader)
except Exception as e: # noqa: BLE001 — keep going on a bad image
print(f" OCR failed for {image_id}: {e}")
res = {"text": "", "tokens": [], "confidences": []}
rows.append({
"image_id": image_id,
"ocr_text": res["text"],
"ocr_tokens": res["tokens"],
"ocr_confidences": res["confidences"],
})
if (i + 1) % 50 == 0:
print(f" OCR {i + 1}/{n} images...")
df = pd.DataFrame(rows)
save_path.parent.mkdir(parents=True, exist_ok=True)
df.to_parquet(save_path)
print(f"Saved OCR for {len(df)} images to {save_path}")
return df
# ---------------------------------------------------------------------------
# OCR-quality metrics
# ---------------------------------------------------------------------------
def _normalise(text: str) -> str:
return " ".join(str(text).lower().split())
def cer(reference: str, hypothesis: str) -> float:
"""Character Error Rate (lower is better)."""
from jiwer import cer as _cer
ref, hyp = _normalise(reference), _normalise(hypothesis)
if not ref:
return 0.0 if not hyp else 1.0
return float(_cer(ref, hyp))
def wer(reference: str, hypothesis: str) -> float:
"""Word Error Rate (lower is better)."""
from jiwer import wer as _wer
ref, hyp = _normalise(reference), _normalise(hypothesis)
if not ref:
return 0.0 if not hyp else 1.0
return float(_wer(ref, hyp))
def evaluate_ocr_quality(df: pd.DataFrame, ref_col: str, hyp_col: str = "ocr_text") -> dict:
"""
Compute mean CER and WER over a DataFrame where each row has a reference OCR
string (ref_col, e.g. joined TextVQA ocr_tokens) and our extracted text (hyp_col).
"""
cers, wers = [], []
for _, row in df.iterrows():
ref, hyp = row[ref_col], row[hyp_col]
cers.append(cer(ref, hyp))
wers.append(wer(ref, hyp))
return {"mean_cer": float(np.mean(cers)), "mean_wer": float(np.mean(wers)), "n": len(df)}