File size: 4,057 Bytes
e56eb98
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
Preprocessing utilities for marksheet images.

Provides a simple pipeline:
- Load image
- Detect the largest page-like contour and crop so only the marksheet is visible
- Boost contrast using CLAHE on the L (or V) channel
- Reduce saturation slightly to stabilize OCR

Returns (processed_image, original_image, crop_coords)
where crop_coords = (x1, y1, x2, y2) in original image coordinates.
"""

from typing import Tuple, Optional
import cv2
import numpy as np


def _find_largest_contour_cropping_box(image_gray: np.ndarray) -> Optional[Tuple[int, int, int, int]]:
    """
    Find a tight bounding box around the largest high-contrast region (the marksheet page).
    This uses Canny + contour detection and returns a bounding rectangle.
    """
    # Edges
    blurred = cv2.GaussianBlur(image_gray, (5, 5), 0)
    edges = cv2.Canny(blurred, 50, 150)

    # Dilate to connect edges
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
    edges = cv2.dilate(edges, kernel, iterations=1)

    contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    if not contours:
        return None

    # Choose the largest contour by area
    largest_cnt = max(contours, key=cv2.contourArea)
    x, y, w, h = cv2.boundingRect(largest_cnt)

    # Expand slightly to avoid tight cuts
    pad_x = int(0.01 * image_gray.shape[1])
    pad_y = int(0.01 * image_gray.shape[0])
    x1 = max(0, x - pad_x)
    y1 = max(0, y - pad_y)
    x2 = min(image_gray.shape[1], x + w + pad_x)
    y2 = min(image_gray.shape[0], y + h + pad_y)
    return (x1, y1, x2, y2)


def _apply_contrast_and_saturation(img_bgr: np.ndarray) -> np.ndarray:
    """
    Boost contrast with CLAHE on the L channel (via LAB) and reduce saturation ~15% via HSV.
    """
    # Contrast (CLAHE on LAB L channel)
    lab = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2LAB)
    l, a, b = cv2.split(lab)
    clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
    l_eq = clahe.apply(l)
    lab_eq = cv2.merge((l_eq, a, b))
    bgr_eq = cv2.cvtColor(lab_eq, cv2.COLOR_LAB2BGR)

    # Reduce saturation in HSV
    hsv = cv2.cvtColor(bgr_eq, cv2.COLOR_BGR2HSV)
    h, s, v = cv2.split(hsv)
    s = (s.astype(np.float32) * 0.85).clip(0, 255).astype(np.uint8)
    hsv_mod = cv2.merge((h, s, v))
    bgr_out = cv2.cvtColor(hsv_mod, cv2.COLOR_HSV2BGR)
    return bgr_out


def preprocess_marksheet(image_path: str,
                         output_path: Optional[str] = None,
                         save_intermediate: bool = False):
    """
    Load an image, crop to the marksheet region, enhance contrast and reduce saturation.

    Args:
        image_path: Path to the input image
        output_path: Optional path to save the processed image (unused when None)
        save_intermediate: Whether to save intermediate images (not used; kept for compatibility)

    Returns:
        processed_image (np.ndarray), original_image (np.ndarray), crop_coords (x1,y1,x2,y2)
    """
    original = cv2.imread(image_path)
    if original is None:
        raise FileNotFoundError(f"Failed to read image: {image_path}")

    # Ensure portrait orientation similar to downstream expectations
    h, w = original.shape[:2]
    img = original.copy()
    if w > h:
        img = cv2.rotate(img, cv2.ROTATE_90_COUNTERCLOCKWISE)

    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    # Find crop box
    crop_box = _find_largest_contour_cropping_box(gray)
    if crop_box is None:
        # Fall back to the whole image
        x1, y1, x2, y2 = 0, 0, img.shape[1], img.shape[0]
    else:
        x1, y1, x2, y2 = crop_box

    cropped = img[y1:y2, x1:x2]
    if cropped.size == 0:
        cropped = img
        x1, y1, x2, y2 = 0, 0, img.shape[1], img.shape[0]

    # Enhance
    enhanced = _apply_contrast_and_saturation(cropped)

    # Save if path provided
    if output_path:
        try:
            cv2.imwrite(output_path, enhanced)
        except Exception:
            pass

    crop_coords = (int(x1), int(y1), int(x2), int(y2))
    return enhanced, original, crop_coords