File size: 8,045 Bytes
a39757f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import pytest
import numpy as np
import cv2
from app.services.image_processing.quality import analyze_image_quality
from app.services.image_processing.geometry import (
    estimate_skew_angle, deskew_image, rotate_cardinal, correct_perspective
)
from app.services.image_processing.preprocessor import (
    AdaptivePreprocessor, preprocess_for_ocr
)

# ---------------------------------------------------------
# 1. Clean Scan (Preservation of Quality & Minimal Ops)
# ---------------------------------------------------------
def test_clean_scan_preprocessing():
    # Clean high contrast image (like a digital PDF export)
    clean_img = np.ones((1000, 800, 3), dtype=np.uint8) * 255
    # Add clear black text lines
    cv2.putText(clean_img, "Official Invoice INV-2024", (50, 100), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 0), 2)
    cv2.putText(clean_img, "Total: 15000.00 EGP", (50, 200), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 0), 2)

    quality = analyze_image_quality(clean_img)
    assert quality.is_blurry is False
    assert quality.resolution_ok is True
    assert quality.skew_angle == 0.0

    primary, variants = AdaptivePreprocessor.preprocess_adaptive(clean_img, quality)
    assert "primary" in variants
    assert "original" in variants
    assert primary.shape == clean_img.shape

# ---------------------------------------------------------
# 2. Tilted / Skewed Document (Deskew)
# ---------------------------------------------------------
def test_skew_estimation_and_deskew():
    # Create an image with tilted text lines (+5 degrees)
    img = np.ones((600, 800, 3), dtype=np.uint8) * 255
    cv2.putText(img, "Contract Agreement First Party", (100, 200), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 0), 2)
    cv2.putText(img, "Second Party Terms and Conditions", (100, 300), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 0), 2)
    cv2.putText(img, "Articles 1 2 3 4 5 6 7", (100, 400), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 0), 2)

    # Rotate by 5 degrees
    center = (400, 300)
    matrix = cv2.getRotationMatrix2D(center, 5.0, 1.0)
    tilted = cv2.warpAffine(img, matrix, (800, 600), borderValue=(255, 255, 255))

    skew = estimate_skew_angle(tilted)
    assert abs(skew) > 1.0  # Successfully detected tilt

    deskewed = deskew_image(tilted, skew)
    assert deskewed is not None
    assert deskewed.shape[0] >= tilted.shape[0]

# ---------------------------------------------------------
# 3. Cardinal Rotation (90, 180, 270)
# ---------------------------------------------------------
def test_cardinal_rotations():
    img = np.zeros((400, 600, 3), dtype=np.uint8)
    r90 = rotate_cardinal(img, 90)
    assert r90.shape == (600, 400, 3)

    r180 = rotate_cardinal(img, 180)
    assert r180.shape == (400, 600, 3)

    r270 = rotate_cardinal(img, 270)
    assert r270.shape == (600, 400, 3)

# ---------------------------------------------------------
# 4. Low-Resolution Upscaling & Massive Downscaling
# ---------------------------------------------------------
def test_resolution_adaptive_scaling():
    # Low-res image (e.g. 300x200 cropped label)
    low_res = np.ones((200, 300, 3), dtype=np.uint8) * 255
    q_low = analyze_image_quality(low_res)
    assert q_low.resolution_ok is False
    primary_up, _ = AdaptivePreprocessor.preprocess_adaptive(low_res, q_low)
    # Upscaled
    assert primary_up.shape[0] > low_res.shape[0]
    assert primary_up.shape[1] > low_res.shape[1]

    # Massive image (e.g. 4000x3500)
    massive = np.ones((4000, 3500, 3), dtype=np.uint8) * 255
    q_massive = analyze_image_quality(massive)
    primary_down, _ = AdaptivePreprocessor.preprocess_adaptive(massive, q_massive)
    assert max(primary_down.shape[:2]) <= AdaptivePreprocessor.MAX_DIM

# ---------------------------------------------------------
# 5. Blur Detection & Controlled Sharpening
# ---------------------------------------------------------
def test_blurry_image_sharpening():
    # Blurred image (800x800 so optimal resolution)
    sharp = np.ones((800, 800, 3), dtype=np.uint8) * 255
    cv2.putText(sharp, "Blurred Text Test", (50, 250), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 0), 2)
    blurry = cv2.GaussianBlur(sharp, (15, 15), 0)

    quality = analyze_image_quality(blurry)
    assert quality.is_blurry is True

    primary, _ = AdaptivePreprocessor.preprocess_adaptive(blurry, quality)
    assert primary.shape == blurry.shape
    assert primary is not None


def test_clean_sharp_image_is_not_smoothed_by_denoising_branch():
    img = np.full((800, 800, 3), 255, dtype=np.uint8)
    cv2.putText(img, "SHARP", (50, 250), cv2.FONT_HERSHEY_SIMPLEX, 2.0, (0, 0, 0), 3)
    quality = analyze_image_quality(img)
    # The fixture is sparse text; page-wide variance is not a fixed >300.
    assert quality.is_blurry is False
    primary, _ = AdaptivePreprocessor.preprocess_adaptive(img, quality)
    gray = cv2.cvtColor(primary, cv2.COLOR_BGR2GRAY)
    # Primary does grayscale conversion, but the clean high-contrast branch
    # should not run bilateral smoothing or thresholding.
    assert np.mean(np.abs(gray.astype(np.int16) - cv2.cvtColor(img, cv2.COLOR_BGR2GRAY).astype(np.int16))) == 0

# ---------------------------------------------------------
# 6. Uneven Lighting & Shadow Flattening
# ---------------------------------------------------------
def test_shadow_gradient_removal():
    # Create image with harsh shadow gradient from left (dark) to right (bright)
    img = np.ones((800, 800, 3), dtype=np.uint8) * 200
    for x in range(400):
        img[:, x] = img[:, x] - 140  # Dark shadow on left half
    cv2.putText(img, "Payment Receipt", (50, 200), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 0), 2)

    quality = analyze_image_quality(img)
    primary, variants = AdaptivePreprocessor.preprocess_adaptive(img, quality)
    assert primary is not None
    assert "primary" in variants


def test_preprocessor_does_not_build_unused_threshold_variant():
    img = np.full((800, 800, 3), 255, dtype=np.uint8)
    img[:, :400] = 80
    quality = analyze_image_quality(img)
    _, variants = AdaptivePreprocessor.preprocess_adaptive(img, quality)
    assert "primary" in variants
    assert "original" in variants
    assert "enhanced" not in variants

# ---------------------------------------------------------
# 7. Arabic Dots and Diacritics Preservation
# ---------------------------------------------------------
def test_arabic_dots_and_thin_strokes_preservation():
    # Create image with simulated fine dots (800x800 so no scaling shift)
    img = np.ones((800, 800, 3), dtype=np.uint8) * 255
    # Simulate dots as tiny black squares (3x3 pixels)
    img[150:154, 150:154] = 0
    img[150:154, 158:162] = 0
    # Simulate a thin stroke (2px width)
    img[170:172, 140:180] = 0

    quality = analyze_image_quality(img)
    primary, _ = AdaptivePreprocessor.preprocess_adaptive(img, quality)

    # Convert primary to gray and verify the dots are not smoothed out/erased
    gray = cv2.cvtColor(primary, cv2.COLOR_BGR2GRAY)
    dot1_val = np.min(gray[150:154, 150:154])
    stroke_val = np.min(gray[170:172, 140:180])
    
    # Text pixels should remain dark (< 100), not washed out into background (255)
    assert dot1_val < 120, "Arabic dot was destroyed by preprocessing!"
    assert stroke_val < 120, "Thin stroke was destroyed by preprocessing!"

# ---------------------------------------------------------
# 8. Perspective Correction Detection
# ---------------------------------------------------------
def test_perspective_detection():
    # Create a white document rectangle inside a dark background
    bg = np.zeros((800, 800, 3), dtype=np.uint8)
    # Tilted 4-corner document inside the background
    doc_pts = np.array([[120, 150], [680, 120], [700, 680], [100, 650]], dtype=np.int32)
    cv2.fillPoly(bg, [doc_pts], (255, 255, 255))
    cv2.putText(bg, "Sample Contract", (200, 400), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 0), 2)

    warped, applied = correct_perspective(bg)
    assert applied is True
    assert warped.shape[0] > 300
    assert warped.shape[1] > 300