Spaces:
Sleeping
Sleeping
| """ | |
| OCR Engine — Extracts text from images and scanned documents using Tesseract. | |
| Uses an adaptive preprocessing strategy: | |
| 1. Try Tesseract with gentle preprocessing (grayscale + light cleanup) | |
| 2. If confidence is poor, retry with medium preprocessing (+ Otsu binarization) | |
| 3. Keep the best result | |
| Tesseract is used for ALL OCR (both images and scanned PDFs). | |
| EasyOCR has been removed to keep memory usage within HuggingFace free-tier limits. | |
| """ | |
| import cv2 | |
| import numpy as np | |
| from pathlib import Path | |
| from PIL import Image | |
| import pytesseract | |
| from services.image_preprocessor import ( | |
| preprocess_gentle, | |
| preprocess_medium, | |
| preprocess_heavy, | |
| preprocess_image, | |
| fix_orientation, | |
| ) | |
| # --- Add Windows Tesseract fallback for local testing --- | |
| import os | |
| import sys | |
| if sys.platform == 'win32': | |
| try: | |
| pytesseract.get_tesseract_version() | |
| except Exception: | |
| for path in [ | |
| r"C:\Program Files\Tesseract-OCR\tesseract.exe", | |
| r"C:\Program Files (x86)\Tesseract-OCR\tesseract.exe", | |
| r"C:\Users\kmthu\AppData\Local\Programs\Tesseract-OCR\tesseract.exe", | |
| ]: | |
| if os.path.exists(path): | |
| pytesseract.pytesseract.tesseract_cmd = path | |
| break | |
| # -------------------------------------------------------- | |
| # Minimum average confidence (0-1 scale) to accept a Tesseract result | |
| MIN_ACCEPTABLE_CONFIDENCE = 0.40 | |
| def _tesseract_ocr_page(cleaned_img: np.ndarray) -> dict: | |
| """ | |
| Run Tesseract on a single preprocessed image and return structured results. | |
| Returns dict with 'text', 'blocks', 'avg_confidence', 'word_count'. | |
| """ | |
| # Get raw text | |
| raw_text = pytesseract.image_to_string(cleaned_img, config='--psm 3').strip() | |
| # Get word-level data for confidence and bounding boxes | |
| data = pytesseract.image_to_data( | |
| cleaned_img, output_type=pytesseract.Output.DICT, config='--psm 3' | |
| ) | |
| blocks = [] | |
| total_confidence = 0.0 | |
| word_count = 0 | |
| for i in range(len(data['text'])): | |
| text = data['text'][i].strip() | |
| conf = data['conf'][i] | |
| try: | |
| conf_val = float(conf) / 100.0 | |
| except (ValueError, TypeError): | |
| conf_val = -1.0 | |
| if text and conf_val >= 0: | |
| x = data['left'][i] | |
| y = data['top'][i] | |
| w = data['width'][i] | |
| h = data['height'][i] | |
| blocks.append({ | |
| "text": text, | |
| "confidence": round(conf_val, 3), | |
| "bbox": [[x, y], [x + w, y], [x + w, y + h], [x, y + h]], | |
| }) | |
| total_confidence += conf_val | |
| word_count += 1 | |
| avg_confidence = total_confidence / word_count if word_count > 0 else 0.0 | |
| return { | |
| "text": raw_text, | |
| "blocks": blocks, | |
| "avg_confidence": round(avg_confidence, 3), | |
| "word_count": word_count, | |
| } | |
| def _is_gibberish(text: str, avg_confidence: float) -> bool: | |
| """ | |
| Heuristic check: is the OCR output likely gibberish? | |
| """ | |
| if avg_confidence < MIN_ACCEPTABLE_CONFIDENCE: | |
| return True | |
| if not text or len(text) < 10: | |
| return True | |
| # Check ratio of letter/digit characters vs total | |
| alpha_count = sum(1 for c in text if c.isalnum()) | |
| total_chars = len(text.replace(' ', '').replace('\n', '')) | |
| if total_chars > 0: | |
| alpha_ratio = alpha_count / total_chars | |
| if alpha_ratio < 0.5: | |
| return True | |
| return False | |
| def _adaptive_ocr(tmp_path: str, pil_img: Image.Image = None) -> dict: | |
| """ | |
| Run Tesseract with adaptive preprocessing on a single page/image. | |
| Tries gentle first, then medium if result is gibberish. | |
| Returns the best result dict with 'text', 'blocks', 'avg_confidence'. | |
| """ | |
| best_result = None | |
| # Load and fix orientation ONCE | |
| if pil_img is not None: | |
| # Convert PIL to OpenCV BGR format | |
| open_cv_image = np.array(pil_img) | |
| # Handle different modes (RGB, RGBA, L) | |
| if len(open_cv_image.shape) == 3 and open_cv_image.shape[2] == 3: | |
| img_bgr = cv2.cvtColor(open_cv_image, cv2.COLOR_RGB2BGR) | |
| elif len(open_cv_image.shape) == 3 and open_cv_image.shape[2] == 4: | |
| img_bgr = cv2.cvtColor(open_cv_image, cv2.COLOR_RGBA2BGR) | |
| else: | |
| img_bgr = cv2.cvtColor(open_cv_image, cv2.COLOR_GRAY2BGR) | |
| else: | |
| img_bgr = cv2.imread(tmp_path) | |
| if img_bgr is None: | |
| return {"text": "", "blocks": [], "avg_confidence": 0.0, "word_count": 0} | |
| oriented_img = fix_orientation(img_bgr) | |
| # Tier 1: Gentle preprocessing (no binarization — lets Tesseract decide) | |
| try: | |
| gentle_img = preprocess_gentle(oriented_img) | |
| result = _tesseract_ocr_page(gentle_img) | |
| best_result = result | |
| except Exception as e: | |
| print(f" Gentle preprocessing failed: {e}") | |
| # Tier 2: Medium preprocessing (+ Otsu binarization) — only if gentle was bad | |
| if best_result is None or _is_gibberish(best_result['text'], best_result['avg_confidence']): | |
| try: | |
| medium_img = preprocess_medium(oriented_img) | |
| result = _tesseract_ocr_page(medium_img) | |
| if best_result is None or result['avg_confidence'] > best_result['avg_confidence']: | |
| best_result = result | |
| except Exception as e: | |
| print(f" Medium preprocessing failed: {e}") | |
| # Tier 3: Raw grayscale (no preprocessing) — absolute fallback | |
| if best_result is None or _is_gibberish(best_result['text'], best_result['avg_confidence']): | |
| try: | |
| raw_img = cv2.cvtColor(oriented_img, cv2.COLOR_BGR2GRAY) | |
| if raw_img is not None: | |
| # Upscale if small | |
| h, w = raw_img.shape[:2] | |
| if w < 2000: | |
| scale = 2000 / w | |
| raw_img = cv2.resize( | |
| raw_img, (int(w * scale), int(h * scale)), | |
| interpolation=cv2.INTER_CUBIC | |
| ) | |
| result = _tesseract_ocr_page(raw_img) | |
| if best_result is None or result['avg_confidence'] > best_result['avg_confidence']: | |
| best_result = result | |
| except Exception as e: | |
| print(f" Raw OCR failed: {e}") | |
| if best_result is None: | |
| best_result = {"text": "", "blocks": [], "avg_confidence": 0.0, "word_count": 0} | |
| return best_result | |
| def extract_from_image(image_path: str | Path, preprocess: bool = True) -> dict: | |
| """ | |
| Extract text from a standalone image file (JPG, PNG) using Tesseract. | |
| """ | |
| image_path = str(image_path) | |
| result = _adaptive_ocr(image_path) | |
| return { | |
| "raw_text": result["text"], | |
| "blocks": result["blocks"], | |
| "confidence_avg": result["avg_confidence"], | |
| "page_count": 1, | |
| } | |
| def extract_from_scanned_pdf(pdf_path: str | Path) -> dict: | |
| """ | |
| Extract text from a scanned PDF using Tesseract with adaptive preprocessing. | |
| For each page: converts to image, tries gentle then medium preprocessing, | |
| keeps the best result. | |
| """ | |
| try: | |
| import pdfplumber | |
| import tempfile | |
| import os | |
| all_text_parts = [] | |
| all_blocks = [] | |
| total_confidence = 0.0 | |
| total_detections = 0 | |
| page_count = 0 | |
| with pdfplumber.open(str(pdf_path)) as pdf: | |
| page_count = len(pdf.pages) | |
| for page in pdf.pages: | |
| # Convert PDF page to image at high resolution | |
| page_image = page.to_image(resolution=300) | |
| pil_img = page_image.original | |
| # Save page as a temporary image file | |
| with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp: | |
| tmp_path = tmp.name | |
| pil_img.save(tmp_path, format="PNG") | |
| try: | |
| result = _adaptive_ocr(tmp_path, pil_img=pil_img) | |
| if result['text']: | |
| all_text_parts.append(result['text']) | |
| for block in result['blocks']: | |
| block['page'] = page.page_number | |
| all_blocks.append(block) | |
| total_confidence += block['confidence'] | |
| total_detections += 1 | |
| except Exception as page_err: | |
| print(f"Page {page.page_number} OCR failed: {page_err}") | |
| finally: | |
| try: | |
| os.unlink(tmp_path) | |
| except OSError: | |
| pass | |
| # Add page separator | |
| all_text_parts.append(f"\n--- Page {page.page_number} ---\n") | |
| avg_confidence = total_confidence / total_detections if total_detections else 0.0 | |
| return { | |
| "raw_text": "\n".join(all_text_parts), | |
| "blocks": all_blocks, | |
| "confidence_avg": round(avg_confidence, 3), | |
| "page_count": page_count, | |
| } | |
| except Exception as e: | |
| return { | |
| "raw_text": f"Error processing scanned PDF: {str(e)}", | |
| "blocks": [], | |
| "confidence_avg": 0.0, | |
| "page_count": 0, | |
| } | |