File size: 7,979 Bytes
900df0b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Tests for image preprocessing module."""

import numpy as np
import pytest
from modules.vision.image_preprocessor import ImagePreprocessor


@pytest.fixture
def sample_image() -> np.ndarray:
    """Create a synthetic test image (white background with black text).

    Returns:
        200x400 BGR image with synthetic text-like content.
    """
    image = np.ones((200, 400, 3), dtype=np.uint8) * 255

    # Draw some "text" as dark rectangles
    image[50:60, 50:150] = 0    # Horizontal line of "text"
    image[80:90, 50:200] = 0    # Longer line
    image[110:120, 50:180] = 0  # Medium line
    image[140:150, 50:160] = 0  # Shorter line

    # Add some noise
    noise = np.random.randint(0, 10, (200, 400, 3), dtype=np.uint8)
    image = np.clip(image.astype(np.int16) - noise.astype(np.int16), 0, 255).astype(np.uint8)

    return image


@pytest.fixture
def noisy_image() -> np.ndarray:
    """Create a noisy test image.

    Returns:
        200x400 BGR image with Gaussian noise.
    """
    image = np.ones((200, 400, 3), dtype=np.uint8) * 255
    image[70:130, 50:350] = 0  # Dark band

    # Add heavy noise
    noise = np.random.normal(0, 25, (200, 400, 3)).astype(np.int16)
    image = np.clip(image.astype(np.int16) + noise, 0, 255).astype(np.uint8)
    return image


@pytest.fixture
def preprocessor() -> ImagePreprocessor:
    """Create a preprocessor with all steps enabled."""
    return ImagePreprocessor(
        apply_clahe=True,
        apply_denoise=True,
        apply_deskew=True,
        apply_binarize=True,
        denoise_strength=10,
    )


class TestPreprocessorInit:
    """Tests for preprocessor initialization."""

    def test_default_config(self) -> None:
        """Test default preprocessor configuration."""
        pp = ImagePreprocessor()
        assert pp.apply_clahe is True
        assert pp.apply_denoise is True
        assert pp.apply_deskew is True
        assert pp.apply_binarize is True
        assert pp.denoise_strength == 10

    def test_custom_config(self) -> None:
        """Test custom preprocessor configuration."""
        pp = ImagePreprocessor(
            apply_clahe=False,
            apply_denoise=False,
            clahe_clip_limit=3.0,
            denoise_strength=15,
        )
        assert pp.apply_clahe is False
        assert pp.apply_denoise is False
        assert pp.clahe_clip_limit == 3.0
        assert pp.denoise_strength == 15

    def test_odd_window_sizes(self) -> None:
        """Test that window sizes are forced to odd values."""
        pp = ImagePreprocessor(
            denoise_template_window=6,  # Even number
            denoise_search_window=20,    # Even number
        )
        assert pp.denoise_template_window % 2 == 1
        assert pp.denoise_search_window % 2 == 1


class TestGrayscaleConversion:
    """Tests for grayscale conversion."""

    def test_bgr_to_grayscale(self, preprocessor: ImagePreprocessor, sample_image: np.ndarray) -> None:
        """Test converting a BGR image to grayscale."""
        gray = preprocessor._to_grayscale(sample_image)
        assert gray.ndim == 2
        assert gray.shape == (200, 400)

    def test_already_grayscale(self, preprocessor: ImagePreprocessor) -> None:
        """Test that grayscale input is returned unchanged."""
        gray = np.ones((100, 100), dtype=np.uint8) * 128
        result = preprocessor._to_grayscale(gray)
        assert result.shape == (100, 100)


class TestDenoising:
    """Tests for Gaussian blur denoising."""

    def test_denoise_reduces_noise(self, preprocessor: ImagePreprocessor, noisy_image: np.ndarray) -> None:
        """Test that denoising reduces noise variance."""
        gray = preprocessor._to_grayscale(noisy_image)
        denoised = preprocessor._apply_denoise(gray)

        # Denoised should have less variance in flat areas
        original_var = np.var(noisy_image[0:50, 0:50].astype(np.float64))
        denoised_var = np.var(denoised[0:50, 0:50].astype(np.float64))
        assert denoised_var <= original_var

    def test_denoise_preserves_shape(self, preprocessor: ImagePreprocessor, sample_image: np.ndarray) -> None:
        """Test that denoising preserves image shape."""
        gray = preprocessor._to_grayscale(sample_image)
        result = preprocessor._apply_denoise(gray)
        assert result.shape == gray.shape


class TestBinarization:
    """Tests for Otsu threshold binarization."""

    def test_binarize_output(self, preprocessor: ImagePreprocessor, sample_image: np.ndarray) -> None:
        """Test that binarization produces binary output."""
        gray = preprocessor._to_grayscale(sample_image)
        binary = preprocessor._apply_otsu(gray)

        # Check that output is single channel
        assert binary.ndim == 2

        # Check that values are binary (0 or 255)
        unique_values = set(np.unique(binary))
        assert unique_values.issubset({0, 255})

    def test_binarize_preserves_shape(self, preprocessor: ImagePreprocessor, sample_image: np.ndarray) -> None:
        """Test that binarization preserves shape."""
        gray = preprocessor._to_grayscale(sample_image)
        binary = preprocessor._apply_otsu(gray)
        assert binary.shape == gray.shape


class TestCLAHE:
    """Tests for CLAHE contrast enhancement."""

    def test_clahe_output(self, preprocessor: ImagePreprocessor, sample_image: np.ndarray) -> None:
        """Test that CLAHE produces valid output."""
        gray = preprocessor._to_grayscale(sample_image)
        enhanced = preprocessor._apply_clahe(gray)
        assert enhanced.shape == gray.shape
        assert enhanced.dtype == np.uint8


class TestDeskew:
    """Tests for deskew detection and correction."""

    def test_deskew_preserves_shape(self, preprocessor: ImagePreprocessor, sample_image: np.ndarray) -> None:
        """Test that deskew preserves image shape."""
        gray = preprocessor._to_grayscale(sample_image)
        result = preprocessor._apply_deskew(gray)
        assert result is not None
        assert result.shape == gray.shape


class TestFullPipeline:
    """Tests for the full preprocessing pipeline."""

    def test_process_all_enabled(self, preprocessor: ImagePreprocessor, sample_image: np.ndarray) -> None:
        """Test full pipeline with all steps enabled."""
        result = preprocessor.preprocess(sample_image)
        assert result is not None

    def test_process_no_steps(self, sample_image: np.ndarray) -> None:
        """Test pipeline with all steps disabled."""
        pp = ImagePreprocessor(
            apply_clahe=False,
            apply_denoise=False,
            apply_deskew=False,
            apply_binarize=False,
        )
        result = pp.preprocess(sample_image, return_numpy=True)
        np.testing.assert_array_equal(result, ImagePreprocessor._to_numpy(sample_image))

    def test_process_return_numpy(self, preprocessor: ImagePreprocessor, sample_image: np.ndarray) -> None:
        """Test pipeline returning numpy array."""
        result = preprocessor.preprocess(sample_image, return_numpy=True)
        assert isinstance(result, np.ndarray)

    def test_process_return_pil(self, preprocessor: ImagePreprocessor, sample_image: np.ndarray) -> None:
        """Test pipeline returning PIL image."""
        result = preprocessor.preprocess(sample_image, return_numpy=False)
        assert hasattr(result, "mode")


class TestSmartSegment:
    """Tests for word segmentation."""

    def test_segment_returns_list(self, preprocessor: ImagePreprocessor, sample_image: np.ndarray) -> None:
        """Test that segmentation returns a list."""
        result = preprocessor.smart_segment(sample_image)
        assert isinstance(result, list)

    def test_get_bounding_boxes(self, preprocessor: ImagePreprocessor, sample_image: np.ndarray) -> None:
        """Test bounding box extraction."""
        result = preprocessor.get_word_bounding_boxes(sample_image)
        assert isinstance(result, list)