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)