Spaces:
Running
Running
| import io | |
| import cv2 | |
| from PIL import Image, ImageOps, ImageFilter | |
| import numpy as np | |
| # Minimum side length below which we upscale the image before sending to OCR. | |
| # Camera photos below this threshold tend to have insufficient pixel density for tiny text. | |
| _MIN_SHORT_SIDE = 1000 | |
| class ImageProcessor: | |
| def process_bytes_to_numpy(image_bytes: bytes) -> np.ndarray: | |
| """ | |
| Converts raw image bytes to an RGB NumPy array ready for PaddleOCR. | |
| Pipeline: | |
| 1. EXIF auto-rotation (fixes portrait/landscape mobile photos) | |
| 2. Minimum-resolution guard (upscale if image is too small) | |
| 3. CLAHE contrast enhancement (equalises shadows / lighting gradients) | |
| 4. Unsharp masking (sharpens soft/blurry edges without amplifying noise) | |
| """ | |
| img = Image.open(io.BytesIO(image_bytes)) | |
| # 1. Auto-rotate based on EXIF orientation tag (critical for mobile photos) | |
| img = ImageOps.exif_transpose(img) | |
| # Ensure RGB | |
| img_rgb = img.convert("RGB") | |
| # 2. Minimum resolution guard: upscale if the shorter side is too small | |
| img_rgb = ImageProcessor._ensure_min_resolution(img_rgb) | |
| img_np = np.array(img_rgb) | |
| # 3. CLAHE contrast enhancement | |
| try: | |
| img_np = ImageProcessor.enhance_contrast(img_np) | |
| except Exception as e: | |
| print(f"Warning: Contrast enhancement failed ({e}). Proceeding with raw image.") | |
| # 4. Unsharp masking for edge sharpening (applied on PIL image, then back to numpy) | |
| try: | |
| img_np = ImageProcessor._apply_unsharp_mask(img_np) | |
| except Exception as e: | |
| print(f"Warning: Unsharp masking failed ({e}). Skipping sharpening step.") | |
| return img_np | |
| # ------------------------------------------------------------------ | |
| # Private helpers | |
| # ------------------------------------------------------------------ | |
| def _ensure_min_resolution(img: Image.Image) -> Image.Image: | |
| """ | |
| If the shorter side of the image is below _MIN_SHORT_SIDE pixels, | |
| scale the image up proportionally using high-quality Lanczos resampling. | |
| This preserves tiny text that would otherwise be unreadable at low DPI. | |
| """ | |
| w, h = img.size | |
| short_side = min(w, h) | |
| if short_side < _MIN_SHORT_SIDE: | |
| scale = _MIN_SHORT_SIDE / short_side | |
| new_w = int(w * scale) | |
| new_h = int(h * scale) | |
| img = img.resize((new_w, new_h), Image.LANCZOS) | |
| return img | |
| def enhance_contrast(img_np: np.ndarray) -> np.ndarray: | |
| """ | |
| Applies Contrast Limited Adaptive Histogram Equalization (CLAHE) | |
| to the luminance channel to normalise uneven lighting and shadows. | |
| """ | |
| yuv = cv2.cvtColor(img_np, cv2.COLOR_RGB2YUV) | |
| clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) | |
| yuv[:, :, 0] = clahe.apply(yuv[:, :, 0]) | |
| return cv2.cvtColor(yuv, cv2.COLOR_YUV2RGB) | |
| def _apply_unsharp_mask(img_np: np.ndarray) -> np.ndarray: | |
| """ | |
| Applies a gentle unsharp mask to sharpen character edges. | |
| Parameters tuned conservatively so we enhance detail without | |
| amplifying noise or creating ringing artefacts on fine print. | |
| radius=1.5 β small kernel, only sharpens fine detail | |
| percent=120 β 20% boost in edge contrast | |
| threshold=3 β only sharpen pixels that differ by at least 3 levels | |
| (prevents noise from being sharpened) | |
| """ | |
| # Work in PIL for built-in UnsharpMask | |
| pil_img = Image.fromarray(img_np) | |
| sharpened = pil_img.filter(ImageFilter.UnsharpMask(radius=1.5, percent=120, threshold=3)) | |
| return np.array(sharpened) | |
| image_processor = ImageProcessor() | |