Spaces:
Running
Running
| """ | |
| InvoiceForge AI β utils/image_utils.py | |
| Shared image loading and transformation helpers. | |
| Provides: | |
| - load_image_from_request(): FormData or base64 JSON | |
| - correct_perspective(): document quad detection + warp | |
| - remove_shadow(): illumination normalisation | |
| - detect_blur(): Laplacian variance metric | |
| - score_quality(): composite image quality dict | |
| """ | |
| from __future__ import annotations | |
| import base64 | |
| import io | |
| import logging | |
| import re | |
| from typing import Optional | |
| import cv2 | |
| import numpy as np | |
| from flask import Request | |
| from PIL import Image | |
| logger = logging.getLogger(__name__) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # IMAGE LOADING | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def load_image_from_request(request: Request) -> np.ndarray: | |
| """ | |
| Load a BGR numpy image array from a Flask request. | |
| Accepts: | |
| - multipart/form-data with key 'file' or 'image' | |
| - application/json with key 'image' (base64 data URI or raw base64) | |
| Raises: | |
| ValueError: If no valid image source is found. | |
| Returns: | |
| BGR numpy array (uint8). | |
| """ | |
| # ββ 1. FormData ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if request.files: | |
| file_obj = request.files.get("file") or request.files.get("image") | |
| if file_obj: | |
| img_bytes = file_obj.read() | |
| img = _decode_bytes(img_bytes, label="uploaded file") | |
| if img is not None: | |
| return img | |
| # ββ 2. JSON body βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| data: dict = request.get_json(silent=True) or {} | |
| img_b64: str = data.get("image", "") | |
| if img_b64: | |
| # Strip data URI prefix: "data:image/jpeg;base64,..." | |
| img_b64 = re.sub(r"^data:image/\w+;base64,", "", img_b64) | |
| try: | |
| img_bytes = base64.b64decode(img_b64) | |
| img = _decode_bytes(img_bytes, label="base64 JSON") | |
| if img is not None: | |
| return img | |
| except Exception as exc: | |
| raise ValueError(f"Base64 decode failed: {exc}") from exc | |
| raise ValueError( | |
| "No image found in request. " | |
| "Send 'file' in FormData or 'image' (base64) in JSON body." | |
| ) | |
| def _decode_bytes(img_bytes: bytes, label: str = "image") -> Optional[np.ndarray]: | |
| """Decode raw image bytes to BGR numpy array using OpenCV.""" | |
| nparr = np.frombuffer(img_bytes, np.uint8) | |
| img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) | |
| if img is None: | |
| logger.warning("Failed to decode %s as image.", label) | |
| return None | |
| logger.debug("Loaded %s: shape=%s", label, img.shape) | |
| return img | |
| def pil_to_bgr(pil_img: Image.Image) -> np.ndarray: | |
| """Convert a PIL Image (RGB) to BGR numpy array.""" | |
| rgb = np.array(pil_img.convert("RGB")) | |
| return cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR) | |
| def bgr_to_pil(img_bgr: np.ndarray) -> Image.Image: | |
| """Convert BGR numpy array to PIL RGB Image.""" | |
| rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) | |
| return Image.fromarray(rgb) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # PERSPECTIVE CORRECTION (re-exported from preprocessing for convenience) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def correct_perspective(img_bgr: np.ndarray) -> np.ndarray: | |
| """Wrapper β see ocr.preprocessing.correct_perspective for full docs.""" | |
| from ocr.preprocessing import correct_perspective as _cp | |
| return _cp(img_bgr) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # SHADOW REMOVAL (re-exported from preprocessing) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def remove_shadow(img_bgr: np.ndarray) -> np.ndarray: | |
| """Wrapper β see ocr.preprocessing.remove_shadow for full docs.""" | |
| from ocr.preprocessing import remove_shadow as _rs | |
| return _rs(img_bgr) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # QUALITY METRICS | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def detect_blur(img: np.ndarray) -> float: | |
| """ | |
| Return Laplacian variance of the image. | |
| Values below 80 typically indicate a blurry / out-of-focus image. | |
| """ | |
| gray = img if len(img.shape) == 2 else cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) | |
| return float(cv2.Laplacian(gray, cv2.CV_64F).var()) | |
| def score_quality(img: np.ndarray) -> dict: | |
| """ | |
| Return a quality assessment dict. | |
| Keys: | |
| blur_score β Laplacian variance (higher = sharper) | |
| brightness β mean pixel value 0-255 | |
| contrast β pixel std dev | |
| resolution β (width, height) tuple | |
| quality_score β aggregate 0.0-1.0 | |
| is_blurry β bool | |
| """ | |
| from ocr.preprocessing import score_image_quality | |
| return score_image_quality(img) | |