Spaces:
Sleeping
Sleeping
Download src/ocr_engine.py from Goutam112/Text_Extracted: direct link, hf CLI and curl.
- Browser
- Download file 7.03 kB
-
https://huggingface.co/spaces/Goutam112/Text_Extracted/resolve/main/src/ocr_engine.py
- Command line
-
hf download hf://spaces/Goutam112/Text_Extracted/src/ocr_engine.py
-
curl -L -o ocr_engine.py https://huggingface.co/spaces/Goutam112/Text_Extracted/resolve/main/src/ocr_engine.py
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 | |
| 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) | |