Spaces:
Sleeping
Sleeping
File size: 7,030 Bytes
0869963 f99e19b 0869963 f99e19b 0869963 f99e19b 0869963 f99e19b 0869963 f99e19b 0869963 f99e19b 0869963 f99e19b 0869963 f99e19b 0869963 f99e19b 0869963 f99e19b 0869963 f99e19b 0869963 f99e19b 0869963 f99e19b 0869963 f99e19b 0869963 f99e19b 0869963 f99e19b 0869963 f99e19b 0869963 f99e19b 0869963 f99e19b | 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 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 | """
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)
|