Spaces:
Sleeping
Sleeping
File size: 6,270 Bytes
0e39d80 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | """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)
|