Spaces:
Sleeping
Sleeping
| """ | |
| OCR (Optical Character Recognition) module for extracting text from images. | |
| Uses EasyOCR for reliable text detection and recognition. | |
| """ | |
| import logging | |
| from typing import List, Dict, Optional | |
| import easyocr | |
| logger = logging.getLogger(__name__) | |
| class OCREngine: | |
| """Handle OCR for food labels and expiry dates.""" | |
| def __init__(self, languages: List[str] = None, use_gpu: bool = False): | |
| """ | |
| Initialize OCR engine. | |
| Args: | |
| languages: List of language codes (e.g., ['en', 'es']) | |
| use_gpu: Whether to use GPU for OCR | |
| """ | |
| self.languages = languages or ['en'] | |
| self.use_gpu = use_gpu | |
| self.reader = None | |
| self._init_reader() | |
| def _init_reader(self): | |
| """Initialize EasyOCR reader.""" | |
| try: | |
| logger.info(f"Initializing OCR reader for languages: {self.languages}") | |
| self.reader = easyocr.Reader( | |
| self.languages, | |
| gpu=self.use_gpu, | |
| model_storage_directory=None | |
| ) | |
| logger.info("OCR reader initialized successfully") | |
| except Exception as e: | |
| logger.error(f"Failed to initialize OCR reader: {e}") | |
| raise RuntimeError(f"OCR initialization failed: {e}") | |
| def extract_text(self, image_path: str, confidence_threshold: float = 0.3) -> Dict: | |
| """ | |
| Extract text from image file. | |
| Args: | |
| image_path: Path to image file | |
| confidence_threshold: Minimum confidence for text detection | |
| Returns: | |
| Dictionary with extracted text and metadata | |
| """ | |
| if self.reader is None: | |
| raise RuntimeError("OCR reader not initialized") | |
| logger.info(f"Extracting text from {image_path}") | |
| try: | |
| results = self.reader.readtext(image_path) | |
| extracted_texts = [] | |
| for (bbox, text, confidence) in results: | |
| if confidence >= confidence_threshold: | |
| extracted_texts.append({ | |
| "text": text.strip(), | |
| "confidence": float(confidence), | |
| "bbox": { | |
| "x": float(bbox[0][0]), | |
| "y": float(bbox[0][1]), | |
| "width": float(bbox[2][0] - bbox[0][0]), | |
| "height": float(bbox[2][1] - bbox[0][1]) | |
| } | |
| }) | |
| logger.info(f"Extracted {len(extracted_texts)} text regions") | |
| return { | |
| "status": "success", | |
| "texts": extracted_texts, | |
| "full_text": " ".join([t["text"] for t in extracted_texts]), | |
| "num_texts": len(extracted_texts) | |
| } | |
| except Exception as e: | |
| logger.error(f"OCR extraction failed: {e}") | |
| return { | |
| "status": "error", | |
| "error": str(e), | |
| "texts": [], | |
| "full_text": "" | |
| } | |
| def extract_text_from_bytes( | |
| self, | |
| image_bytes: bytes, | |
| confidence_threshold: float = 0.3 | |
| ) -> Dict: | |
| """ | |
| Extract text from image bytes. | |
| Args: | |
| image_bytes: Image data as bytes | |
| confidence_threshold: Minimum confidence for text detection | |
| Returns: | |
| Dictionary with extracted text and metadata | |
| """ | |
| if self.reader is None: | |
| raise RuntimeError("OCR reader not initialized") | |
| import cv2 | |
| import numpy as np | |
| logger.info("Extracting text from image bytes") | |
| try: | |
| # Convert bytes to image | |
| nparr = np.frombuffer(image_bytes, np.uint8) | |
| image = cv2.imdecode(nparr, cv2.IMREAD_COLOR) | |
| if image is None: | |
| raise ValueError("Failed to decode image") | |
| # Convert BGR to RGB for EasyOCR | |
| image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) | |
| results = self.reader.readtext(image) | |
| extracted_texts = [] | |
| for (bbox, text, confidence) in results: | |
| if confidence >= confidence_threshold: | |
| extracted_texts.append({ | |
| "text": text.strip(), | |
| "confidence": float(confidence), | |
| "bbox": { | |
| "x": float(bbox[0][0]), | |
| "y": float(bbox[0][1]), | |
| "width": float(bbox[2][0] - bbox[0][0]), | |
| "height": float(bbox[2][1] - bbox[0][1]) | |
| } | |
| }) | |
| logger.info(f"Extracted {len(extracted_texts)} text regions from bytes") | |
| return { | |
| "status": "success", | |
| "texts": extracted_texts, | |
| "full_text": " ".join([t["text"] for t in extracted_texts]), | |
| "num_texts": len(extracted_texts) | |
| } | |
| except Exception as e: | |
| logger.error(f"OCR extraction from bytes failed: {e}") | |
| return { | |
| "status": "error", | |
| "error": str(e), | |
| "texts": [], | |
| "full_text": "" | |
| } | |
| def parse_ingredients_from_text(self, text: str) -> List[str]: | |
| """ | |
| Parse ingredient names from extracted text. | |
| Uses simple heuristics to identify likely ingredient names. | |
| Args: | |
| text: Extracted text from OCR | |
| Returns: | |
| List of potential ingredient names | |
| """ | |
| # Common ingredient keywords | |
| ingredient_keywords = { | |
| "contains", "ingredients", "product", "made from", | |
| "of", "and", "with", "including", "mix" | |
| } | |
| # Split text and filter potential ingredients | |
| words = text.lower().split() | |
| potential_ingredients = [] | |
| for word in words: | |
| # Remove common non-ingredient words and punctuation | |
| cleaned = word.strip('.,;:!?()[]{}"\'-').strip() | |
| if (len(cleaned) > 2 and | |
| cleaned not in ingredient_keywords and | |
| not cleaned.isdigit() and | |
| '%' not in cleaned): | |
| potential_ingredients.append(cleaned) | |
| # Remove duplicates and sort | |
| unique_ingredients = list(set(potential_ingredients)) | |
| logger.info(f"Parsed {len(unique_ingredients)} potential ingredients from text") | |
| return sorted(unique_ingredients) | |
| def detect_expiry_date(self, text: str) -> Optional[str]: | |
| """ | |
| Attempt to detect expiry date from extracted text. | |
| Looks for common date patterns. | |
| Args: | |
| text: Extracted text from OCR | |
| Returns: | |
| Detected expiry date string or None | |
| """ | |
| import re | |
| # Common expiry date patterns | |
| patterns = [ | |
| r'\b(?:exp|expiry|best before|use by)[:\s]*(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})\b', | |
| r'\b(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})\b', # General date pattern | |
| r'\b(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\.?\s+\d{4}\b' | |
| ] | |
| text_lower = text.lower() | |
| for pattern in patterns: | |
| matches = re.finditer(pattern, text_lower, re.IGNORECASE) | |
| for match in matches: | |
| return match.group(0) if match.lastindex is None else match.group(1) | |
| return None | |
| # Global OCR instance | |
| _ocr_instance = None | |
| def get_ocr_engine(languages: List[str] = None, use_gpu: bool = False) -> OCREngine: | |
| """Get or create singleton OCR engine.""" | |
| global _ocr_instance | |
| if _ocr_instance is None: | |
| _ocr_instance = OCREngine(languages=languages, use_gpu=use_gpu) | |
| return _ocr_instance | |