File size: 9,419 Bytes
99bdf7f
 
 
 
 
c5ce945
 
99bdf7f
 
c5ce945
 
99bdf7f
 
 
 
 
 
 
 
 
c5ce945
 
 
 
 
 
 
 
 
99bdf7f
 
 
 
 
 
 
c5ce945
99bdf7f
 
 
 
 
 
c5ce945
 
 
 
 
 
99bdf7f
c5ce945
 
 
 
 
 
 
 
 
 
 
 
 
99bdf7f
 
 
 
 
 
 
 
c5ce945
99bdf7f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c5ce945
 
 
 
 
 
 
 
 
 
99bdf7f
c5ce945
 
 
99bdf7f
c5ce945
99bdf7f
c5ce945
 
 
 
 
99bdf7f
 
 
c5ce945
 
 
 
 
 
 
 
f3dce36
 
c5ce945
 
 
 
 
 
 
 
 
 
 
99bdf7f
 
 
 
 
 
c5ce945
 
99bdf7f
c5ce945
 
 
 
 
 
 
99bdf7f
c5ce945
 
 
99bdf7f
c5ce945
 
 
 
 
 
 
 
 
f3dce36
 
c5ce945
 
 
 
99bdf7f
 
 
 
 
 
 
 
c5ce945
 
99bdf7f
c5ce945
 
 
 
99bdf7f
c5ce945
 
 
 
 
99bdf7f
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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# -*- coding: utf-8 -*-
"""
Image preprocessor with OCR quality enhancement.
Ported from OCR_Vehicle_02: CLAHE, deskew, denoise.
"""
import os
import gc
import logging
import numpy as np
from PIL import Image

logger = logging.getLogger(__name__)

try:
    import cv2
    CV2_AVAILABLE = True
except ImportError:
    CV2_AVAILABLE = False
    logger.warning("OpenCV not available. Image enhancement disabled.")

try:
    from pdf2image import convert_from_path
    PDF_SUPPORT = True
except ImportError:
    PDF_SUPPORT = False


class ImagePreprocessor:
    """
    Image preprocessor with OCR quality enhancement.

    Enhancement pipeline (from OCR_Vehicle_02):
    1. PDF → Image (300 DPI)
    2. Deskew (Hough Line Transform)
    3. Denoise (Gaussian Blur)
    4. Contrast enhancement (CLAHE on L channel)
    """
    DEFAULT_DPI = 300           # 300 DPI for better OCR (was 200)
    MAX_IMAGE_SIZE = 2048
    JPEG_QUALITY = 92           # Higher quality for OCR
    CLAHE_CLIP_LIMIT = 2.0
    CLAHE_GRID_SIZE = 8
    GAUSSIAN_KERNEL = 3

    def __init__(self, dpi=None, max_size=None):
        self.dpi = dpi or self.DEFAULT_DPI
        self.max_size = max_size or self.MAX_IMAGE_SIZE

    def _resize_image_if_needed(self, img):
        """Resize image if it exceeds maximum dimension."""
        width, height = img.size
        if width <= self.max_size and height <= self.max_size:
            return img

        if width > height:
            new_width = self.max_size
            new_height = int(height * (self.max_size / width))
        else:
            new_height = self.max_size
            new_width = int(width * (self.max_size / height))

        return img.resize((new_width, new_height), Image.Resampling.LANCZOS)

    def _enhance_image(self, img_path):
        """Apply OCR enhancement pipeline: deskew → denoise → CLAHE.

        Args:
            img_path: Path to image file

        Returns:
            Path to enhanced image (may be same path if no enhancement needed)
        """
        if not CV2_AVAILABLE:
            return img_path

        try:
            img = cv2.imread(img_path)
            if img is None:
                return img_path

            # Step 1: Deskew (correct rotation from scanning)
            img = self._deskew(img)

            # Step 2: Denoise (Gaussian blur)
            img = self._denoise(img)

            # Step 3: CLAHE contrast enhancement
            img = self._enhance_contrast(img)

            # Save enhanced image
            base, ext = os.path.splitext(img_path)
            enhanced_path = f"{base}_enhanced.jpg"
            cv2.imwrite(enhanced_path, img, [cv2.IMWRITE_JPEG_QUALITY, self.JPEG_QUALITY])

            return enhanced_path

        except Exception as e:
            logger.warning(f"Image enhancement failed: {e}")
            return img_path

    def _deskew(self, img):
        """Correct image skew using Hough Line Transform."""
        try:
            gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
            edges = cv2.Canny(gray, 50, 150, apertureSize=3)
            lines = cv2.HoughLinesP(
                edges, 1, np.pi / 180,
                threshold=100, minLineLength=100, maxLineGap=10
            )

            if lines is None:
                return img

            # Collect angles of near-horizontal lines
            angles = []
            for line in lines:
                x1, y1, x2, y2 = line[0]
                if x2 - x1 == 0:
                    continue
                angle = np.degrees(np.arctan2(y2 - y1, x2 - x1))
                if abs(angle) < 15:  # Only near-horizontal lines
                    angles.append(angle)

            if not angles:
                return img

            median_angle = np.median(angles)

            # Only rotate if skew is significant (> 0.5 degrees)
            if abs(median_angle) < 0.5:
                return img

            h, w = img.shape[:2]
            center = (w // 2, h // 2)
            M = cv2.getRotationMatrix2D(center, median_angle, 1.0)
            rotated = cv2.warpAffine(
                img, M, (w, h),
                borderMode=cv2.BORDER_REPLICATE
            )

            logger.info(f"Deskew applied: {median_angle:.2f}°")
            return rotated

        except Exception as e:
            logger.warning(f"Deskew failed: {e}")
            return img

    def _denoise(self, img):
        """Remove noise with Gaussian blur."""
        try:
            return cv2.GaussianBlur(img, (self.GAUSSIAN_KERNEL, self.GAUSSIAN_KERNEL), 0)
        except Exception:
            return img

    def _enhance_contrast(self, img):
        """Enhance contrast using CLAHE on L channel (preserves color)."""
        try:
            lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
            l_channel, a_channel, b_channel = cv2.split(lab)

            clahe = cv2.createCLAHE(
                clipLimit=self.CLAHE_CLIP_LIMIT,
                tileGridSize=(self.CLAHE_GRID_SIZE, self.CLAHE_GRID_SIZE)
            )
            l_enhanced = clahe.apply(l_channel)

            lab_enhanced = cv2.merge([l_enhanced, a_channel, b_channel])
            enhanced = cv2.cvtColor(lab_enhanced, cv2.COLOR_LAB2BGR)

            return enhanced

        except Exception as e:
            logger.warning(f"CLAHE failed: {e}")
            return img

    def load_image(self, image_path, max_pages=None):
        """Load and enhance image/PDF for OCR processing."""
        try:
            if not os.path.exists(image_path):
                raise FileNotFoundError(f"Image not found: {image_path}")

            if image_path.lower().endswith('.pdf'):
                return self._process_pdf(image_path, max_pages)

            return self._process_image(image_path)

        except Exception as e:
            logger.error(f"Error loading image: {e}")
            return []

    def _process_pdf(self, pdf_path, max_pages=None):
        """Process PDF with enhancement pipeline."""
        if not PDF_SUPPORT:
            logger.warning(f"PDF support unavailable. Skipping {pdf_path}")
            return []

        try:
            convert_kwargs = {
                'dpi': self.dpi,
                'fmt': 'jpeg',
                'thread_count': 1,
                'use_pdftocairo': True,
            }
            if max_pages:
                convert_kwargs['last_page'] = max_pages

            pages = convert_from_path(pdf_path, **convert_kwargs)

            temp_image_paths = []
            base_name = os.path.splitext(os.path.basename(pdf_path))[0]
            import tempfile
            dir_name = tempfile.gettempdir()

            for i, page in enumerate(pages):
                try:
                    resized_page = self._resize_image_if_needed(page)
                    temp_path = os.path.join(dir_name, f"{base_name}_page_{i+1}.jpg")
                    resized_page.save(temp_path, 'JPEG', quality=self.JPEG_QUALITY)

                    if resized_page != page:
                        resized_page.close()
                    page.close()

                    # Apply enhancement pipeline
                    enhanced_path = self._enhance_image(temp_path)
                    if enhanced_path != temp_path and os.path.exists(temp_path):
                        os.remove(temp_path)  # Remove unenhanced version

                    temp_image_paths.append(enhanced_path)

                except Exception as e:
                    logger.warning(f"Failed to process page {i+1}: {e}")

            del pages
            gc.collect()

            return temp_image_paths

        except Exception as e:
            logger.error(f"Failed to convert PDF {pdf_path}: {e}")
            return []

    def _process_image(self, image_path):
        """Process image file with enhancement."""
        try:
            with Image.open(image_path) as img:
                img.verify()

            with Image.open(image_path) as img:
                width, height = img.size
                if width > self.max_size or height > self.max_size:
                    resized = self._resize_image_if_needed(img.copy())
                    base_name = os.path.splitext(os.path.basename(image_path))[0]
                    import tempfile
                    dir_name = tempfile.gettempdir()
                    temp_path = os.path.join(dir_name, f"{base_name}_resized.jpg")
                    resized.save(temp_path, 'JPEG', quality=self.JPEG_QUALITY)
                    resized.close()

                    enhanced_path = self._enhance_image(temp_path)
                    if enhanced_path != temp_path and os.path.exists(temp_path):
                        os.remove(temp_path)
                    return [enhanced_path]

            # No resize needed — enhance in place (to temp file)
            enhanced_path = self._enhance_image(image_path)
            return [enhanced_path]

        except Exception as e:
            logger.error(f"Error processing image {image_path}: {e}")
            return []

    @staticmethod
    def cleanup_temp_files(file_paths, original_path):
        """Remove temporary files created during processing."""
        for path in file_paths:
            if path != original_path and os.path.exists(path):
                try:
                    os.remove(path)
                except Exception as e:
                    logger.warning(f"Failed to cleanup {path}: {e}")