Spaces:
Sleeping
Sleeping
| """Fast image preprocessing pipeline optimised for speed + accuracy. | |
| Design goals: | |
| - Single-pass preprocessing under 1s for a typical 1200px document image | |
| - 2 OCR variants max (not 5) β halves EasyOCR inference time | |
| - No bilateralFilter (slow O(nΒ²)), no HoughLinesP on every frame | |
| """ | |
| import cv2 | |
| import numpy as np | |
| OCR_MIN_HEIGHT = 120 | |
| MIN_WIDTH_UPSCALE = 1200 | |
| # ββ Upscale βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def upscale_if_needed(image: np.ndarray, min_width: int = MIN_WIDTH_UPSCALE) -> np.ndarray: | |
| """Bicubic upscale if image is too small for good OCR. Fast INTER_LINEAR.""" | |
| h, w = image.shape[:2] | |
| if w >= min_width: | |
| return image | |
| scale = min_width / w | |
| return cv2.resize(image, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_LINEAR) | |
| # ββ Deskew (fast) βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def deskew_fast(image: np.ndarray) -> np.ndarray: | |
| """Fast deskew using minAreaRect on thresholded text blobs. | |
| Only corrects if angle > 0.5Β° to avoid unnecessary warp.""" | |
| gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) if len(image.shape) == 3 else image | |
| _, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) | |
| coords = np.column_stack(np.where(thresh > 0)) | |
| if len(coords) < 100: | |
| return image | |
| angle = cv2.minAreaRect(coords)[-1] | |
| if angle < -45: | |
| angle = 90 + angle | |
| if abs(angle) < 0.5: | |
| return image | |
| h, w = image.shape[:2] | |
| M = cv2.getRotationMatrix2D((w // 2, h // 2), angle, 1.0) | |
| return cv2.warpAffine(image, M, (w, h), flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_REPLICATE) | |
| # ββ CLAHE contrast enhancement βββββββββββββββββββββββββββββββββββββββββββ | |
| def clahe_enhance(gray: np.ndarray) -> np.ndarray: | |
| """Fast CLAHE on grayscale.""" | |
| clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8)) | |
| return clahe.apply(gray) | |
| # ββ Sharpen ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def sharpen(image: np.ndarray) -> np.ndarray: | |
| """Unsharp mask β works on both gray and BGR.""" | |
| blurred = cv2.GaussianBlur(image, (0, 0), 2) | |
| return cv2.addWeighted(image, 1.4, blurred, -0.4, 0) | |
| # ββ Fast single-pass pipeline βββββββββββββββββββββββββββββββββββββββββββββ | |
| def preprocess_fast(image: np.ndarray) -> np.ndarray: | |
| """Primary fast pipeline: upscale β deskew β grayscale β CLAHE β sharpen. | |
| Avoids bilateralFilter (O(nΒ²) slow). Targets ~0.3s for 1200Γ800 image. | |
| """ | |
| if image is None or image.size == 0: | |
| return image | |
| img = upscale_if_needed(image) | |
| img = deskew_fast(img) | |
| gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) if len(img.shape) == 3 else img | |
| gray = clahe_enhance(gray) | |
| gray = sharpen(gray) | |
| return gray | |
| # ββ 2-variant OCR strategy ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def generate_ocr_variants(image: np.ndarray) -> list[np.ndarray]: | |
| """Generate exactly 2 image variants for OCR. | |
| Pass 0: CLAHE grayscale + sharpened β best for printed/scanned text | |
| Pass 1: Adaptive binarize β best for low-contrast / faded docs | |
| OLD: 5 passes β 25β60 s | NEW: 2 passes β 4β8 s | |
| """ | |
| if image is None or image.size == 0: | |
| return [image] if image is not None else [] | |
| # Pass 0 β fast primary | |
| pass0 = preprocess_fast(image) | |
| # Pass 1 β adaptive threshold on the same preprocess (different representation) | |
| pass1 = cv2.adaptiveThreshold( | |
| pass0 if len(pass0.shape) == 2 else cv2.cvtColor(pass0, cv2.COLOR_BGR2GRAY), | |
| 255, | |
| cv2.ADAPTIVE_THRESH_GAUSSIAN_C, | |
| cv2.THRESH_BINARY, | |
| 15, 8, | |
| ) | |
| return [pass0, pass1] | |
| # ββ Full pipeline (alias kept for backward compat) βββββββββββββββββββββββ | |
| def preprocess_pipeline(image: np.ndarray) -> np.ndarray: | |
| """Alias for preprocess_fast β backward compatibility.""" | |
| return preprocess_fast(image) | |
| # ββ Legacy helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def preprocess_crop_for_ocr(crop_bgr: np.ndarray, target_height: int = OCR_MIN_HEIGHT) -> np.ndarray: | |
| if crop_bgr is None or crop_bgr.size == 0: | |
| return crop_bgr | |
| h, w = crop_bgr.shape[:2] | |
| if h < 1: | |
| return crop_bgr | |
| scale = target_height / h | |
| new_w = max(1, int(w * scale)) | |
| resized = cv2.resize(crop_bgr, (new_w, target_height), interpolation=cv2.INTER_LINEAR) | |
| gray = cv2.cvtColor(resized, cv2.COLOR_BGR2GRAY) if len(resized.shape) == 3 else resized | |
| return clahe_enhance(gray) | |
| def upscale_for_fullpage(image_bgr: np.ndarray, scale: float = 2.0) -> np.ndarray: | |
| h, w = image_bgr.shape[:2] | |
| return cv2.resize(image_bgr, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_LINEAR) | |
| # Stubs kept for import compatibility | |
| def correct_perspective(image: np.ndarray) -> np.ndarray: | |
| return image | |
| def remove_noise(image: np.ndarray) -> np.ndarray: | |
| return image | |
| def deskew(image: np.ndarray) -> np.ndarray: | |
| return deskew_fast(image) | |
| def enhance_contrast(image: np.ndarray) -> np.ndarray: | |
| gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) if len(image.shape) == 3 else image | |
| return clahe_enhance(gray) | |
| def adaptive_binarize(image: np.ndarray) -> np.ndarray: | |
| gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) if len(image.shape) == 3 else image | |
| return cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 15, 8) | |