Text_Extracted / src /ocr_engine.py
Goutam112's picture
imporvemnt paths
f99e19b
Raw History Blame Contribute Delete
7.03 kB
"""
ocr_engine.py — Advanced EasyOCR inference engine with accuracy measurement.
Features:
- GPU-accelerated inference (CUDA auto-detect)
- @st.cache_resource singleton reader
- High-sensitivity parameters tuned for live camera & scanned photos
- Fallback multi-pass OCR for difficult lighting / low-contrast text
- Comprehensive OCR accuracy and quality metrics calculation
"""
import streamlit as st
import easyocr
import numpy as np
import re
from PIL import Image
from typing import List, Dict, Any, Tuple
@st.cache_resource(show_spinner="🧠 Loading EasyOCR engine (one-time initialization)...")
def get_reader(languages: Tuple[str, ...]) -> easyocr.Reader:
"""
Cached singleton OCR reader. Loaded once per session.
Auto-detects CUDA GPU for acceleration.
"""
try:
import torch
gpu = torch.cuda.is_available()
except Exception:
gpu = False
return easyocr.Reader(list(languages), gpu=gpu)
def run_ocr(
pil_img: Image.Image,
languages: list = None,
confidence_threshold: float = 0.25,
camera_mode: bool = False,
) -> List[Dict[str, Any]]:
"""
Execute neural OCR with parameters tuned for maximum detection recall and precision.
Supports camera mode with multi-pass recognition for challenging live captures.
"""
if languages is None:
languages = ["en", "hi"]
reader = get_reader(tuple(languages))
img_array = np.array(pil_img.convert("RGB"))
# Primary pass with optimized thresholds
text_th = 0.35 if camera_mode else 0.40
low_th = 0.20 if camera_mode else 0.25
contrast_th = 0.05 if camera_mode else 0.10
raw_results = reader.readtext(
img_array,
text_threshold=text_th,
low_text=low_th,
contrast_ths=contrast_th,
adjust_contrast=0.7 if camera_mode else 0.5,
link_threshold=0.35,
decoder="greedy",
)
# If very few words detected on a live capture, attempt secondary high-sensitivity pass
if len(raw_results) < 4:
raw_results_fallback = reader.readtext(
img_array,
text_threshold=0.25,
low_text=0.15,
contrast_ths=0.03,
adjust_contrast=0.9,
link_threshold=0.25,
)
if len(raw_results_fallback) > len(raw_results):
raw_results = raw_results_fallback
structured = []
for (bbox, text, prob) in raw_results:
clean_text = text.strip()
if prob < confidence_threshold:
continue
if not clean_text:
continue
xs = [p[0] for p in bbox]
ys = [p[1] for p in bbox]
cx = int(np.mean(xs))
cy = int(np.mean(ys))
w = int(max(xs) - min(xs))
h = int(max(ys) - min(ys))
structured.append({
"bbox": bbox,
"text": clean_text,
"confidence": float(prob),
"center": (cx, cy),
"width": w,
"height": h,
"x_min": int(min(xs)),
"y_min": int(min(ys)),
"x_max": int(max(xs)),
"y_max": int(max(ys)),
})
return structured
def get_full_text(results: List[Dict]) -> str:
"""Reconstruct full plain text from OCR results preserving natural reading order."""
sorted_results = sorted(results, key=lambda r: (r["y_min"] // 20, r["x_min"]))
return " ".join(r["text"] for r in sorted_results)
def compute_accuracy_metrics(results: List[Dict], full_text: str = "") -> Dict[str, Any]:
"""
Measure comprehensive OCR accuracy, character recognition quality, and text coherence.
Returns:
- overall_accuracy: 0-100%
- quality_grade: 'A+' | 'A' | 'B' | 'C' | 'D'
- avg_confidence: float (0-1)
- high_conf_pct: % words with conf >= 80%
- medium_conf_pct: % words with conf 50-79%
- low_conf_pct: % words with conf < 50%
- lexical_validity: % of recognized tokens with standard alphanumeric structure
- readability_status: Description of reliability
"""
if not results:
return {
"overall_accuracy": 0.0,
"quality_grade": "N/A",
"avg_confidence": 0.0,
"high_conf_count": 0,
"medium_conf_count": 0,
"low_conf_count": 0,
"high_conf_pct": 0.0,
"medium_conf_pct": 0.0,
"low_conf_pct": 0.0,
"total_words": 0,
"total_characters": 0,
"lexical_validity": 0.0,
"reliability_label": "No Text Detected",
"reliability_color": "#94a3b8",
}
confidences = [r["confidence"] for r in results]
total_words = len(results)
avg_conf = float(np.mean(confidences))
high_c = sum(1 for c in confidences if c >= 0.80)
med_c = sum(1 for c in confidences if 0.50 <= c < 0.80)
low_c = sum(1 for c in confidences if c < 0.50)
# Lexical validity: check if word contains intelligible alphanumeric patterns
valid_tokens = 0
total_chars = 0
for r in results:
t = r["text"]
total_chars += len(t)
# Token has letters or numbers (not pure punctuation noise)
if re.search(r"[A-Za-z0-9\u0900-\u097F]", t):
valid_tokens += 1
lexical_validity = (valid_tokens / total_words) if total_words > 0 else 0.0
# Composite OCR Accuracy Index (weighted: 70% confidence + 30% lexical validity)
overall_accuracy = (avg_conf * 0.70 + lexical_validity * 0.30) * 100.0
# Determine Quality Grade
if overall_accuracy >= 90.0:
grade = "A+"
label = "Exceptional Accuracy"
color = "#16a34a" # Green
elif overall_accuracy >= 80.0:
grade = "A"
label = "High Accuracy (Production Ready)"
color = "#22c55e"
elif overall_accuracy >= 68.0:
grade = "B"
label = "Good / Readable"
color = "#eab308" # Yellow
elif overall_accuracy >= 50.0:
grade = "C"
label = "Moderate (Review Recommended)"
color = "#f97316" # Orange
else:
grade = "D"
label = "Low (Retake photo with better lighting)"
color = "#ef4444" # Red
return {
"overall_accuracy": round(overall_accuracy, 1),
"quality_grade": grade,
"avg_confidence": round(avg_conf, 3),
"high_conf_count": high_c,
"medium_conf_count": med_c,
"low_conf_count": low_c,
"high_conf_pct": round((high_c / total_words) * 100.0, 1),
"medium_conf_pct": round((med_c / total_words) * 100.0, 1),
"low_conf_pct": round((low_c / total_words) * 100.0, 1),
"total_words": total_words,
"total_characters": total_chars,
"lexical_validity": round(lexical_validity * 100.0, 1),
"reliability_label": label,
"reliability_color": color,
}
def get_statistics(results: List[Dict]) -> Dict[str, Any]:
"""Compatibility wrapper that delegates to compute_accuracy_metrics."""
return compute_accuracy_metrics(results)