File size: 7,890 Bytes
892fa81
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Unit tests for cores.vision — the shared image-operations layer."""

from __future__ import annotations

import base64
import hashlib

import cv2
import numpy as np
import pytest

from cores.vision import (
    bytes_to_numpy, base64_to_numpy, numpy_to_base64, numpy_to_bytes,
    url_to_bytes, sniff_format,
    BBox, crop_region, resize_with_aspect, clamp_box, boxes_iou,
    to_gray, to_rgb, to_bgr, guess_color_profile, dominant_colors,
    sha256_bytes, sha256_image, phash, dhash, ahash, whash, hamming_distance,
    brightness, contrast, sharpness, noise_level, quality_score,
    draw_boxes,
)


class TestDecode:
    def test_bytes_to_numpy_valid(self, sample_image_bytes):
        img = bytes_to_numpy(sample_image_bytes)
        assert img.ndim == 3
        assert img.shape[2] == 3

    def test_bytes_to_numpy_invalid(self):
        with pytest.raises(ValueError):
            bytes_to_numpy(b"not an image")

    def test_base64_to_numpy_with_data_uri(self, sample_image_b64):
        img = base64_to_numpy("data:image/jpeg;base64," + sample_image_b64)
        assert img is not None

    def test_numpy_to_bytes_roundtrip(self, sample_image_bytes):
        img = bytes_to_numpy(sample_image_bytes)
        raw = numpy_to_bytes(img)
        img2 = bytes_to_numpy(raw)
        assert img.shape == img2.shape

    def test_numpy_to_base64(self, sample_image_bytes):
        img = bytes_to_numpy(sample_image_bytes)
        b64 = numpy_to_base64(img)
        assert isinstance(b64, str)

    def test_sniff_format_jpeg(self, sample_image_bytes):
        assert sniff_format(sample_image_bytes) == "jpeg"

    def test_sniff_format_png(self):
        png = b"\x89PNG\r\n\x1a\n" + b"\x00" * 50
        assert sniff_format(png) == "png"

    def test_sniff_format_unknown(self):
        assert sniff_format(b"\x00\x01\x02\x03") is None

    def test_sniff_format_empty(self):
        assert sniff_format(b"") is None


class TestGeometry:
    def test_bbox_to_dict(self):
        b = BBox(10, 20, 100, 200)
        assert b.to_dict() == {"x": 10, "y": 20, "w": 100, "h": 200}

    def test_bbox_area(self):
        assert BBox(0, 0, 100, 50).area == 5000

    def test_crop_region_no_margin(self):
        img = np.zeros((300, 300, 3), dtype=np.uint8)
        img[100:200, 100:200] = 255
        crop = crop_region(img, BBox(100, 100, 100, 100), margin=0.0)
        assert crop.shape == (100, 100, 3)
        assert (crop == 255).all()

    def test_crop_region_with_margin(self):
        img = np.zeros((300, 300, 3), dtype=np.uint8)
        crop = crop_region(img, BBox(100, 100, 50, 50), margin=0.2)
        assert crop.shape == (70, 70, 3)

    def test_crop_region_clamps_bounds(self):
        img = np.zeros((100, 100, 3), dtype=np.uint8)
        crop = crop_region(img, BBox(0, 0, 80, 80), margin=0.5)
        assert crop.shape[0] <= 100
        assert crop.shape[1] <= 100

    def test_resize_no_resize_needed(self):
        img = np.zeros((100, 200, 3), dtype=np.uint8)
        assert resize_with_aspect(img, max_dim=300).shape == img.shape

    def test_resize_landscape(self):
        img = np.zeros((100, 400, 3), dtype=np.uint8)
        r = resize_with_aspect(img, max_dim=200)
        assert r.shape[1] == 200
        assert r.shape[0] == 50

    def test_clamp_box(self):
        b = clamp_box(BBox(-10, -10, 100, 100), width=50, height=50)
        assert b.x == 0
        assert b.y == 0
        assert b.w == 50
        assert b.h == 50

    def test_boxes_iou_identical(self):
        b = BBox(0, 0, 100, 100)
        assert boxes_iou(b, b) == 1.0

    def test_boxes_iou_disjoint(self):
        a = BBox(0, 0, 10, 10)
        b = BBox(100, 100, 10, 10)
        assert boxes_iou(a, b) == 0.0


class TestColor:
    def test_to_gray_from_bgr(self):
        img = np.zeros((10, 10, 3), dtype=np.uint8)
        gray = to_gray(img)
        assert gray.shape == (10, 10)

    def test_to_gray_passthrough(self):
        gray = np.zeros((10, 10), dtype=np.uint8)
        assert to_gray(gray).shape == (10, 10)

    def test_to_rgb_swaps_channels(self):
        # BGR [255, 0, 0] = blue → RGB should be [0, 0, 255]
        img = np.array([[[255, 0, 0]]], dtype=np.uint8)  # BGR: blue
        rgb = to_rgb(img)
        assert rgb[0, 0, 0] == 0    # R
        assert rgb[0, 0, 1] == 0    # G
        assert rgb[0, 0, 2] == 255  # B

    def test_guess_color_profile_bgr(self):
        assert guess_color_profile(np.zeros((10, 10, 3), dtype=np.uint8)) == "BGR"

    def test_guess_color_profile_gray(self):
        assert guess_color_profile(np.zeros((10, 10), dtype=np.uint8)) == "grayscale"

    def test_dominant_colors_returns_hex(self):
        img = np.zeros((100, 100, 3), dtype=np.uint8)
        img[:] = [255, 0, 0]  # all blue in BGR
        colors = dominant_colors(img, k=3)
        assert len(colors) == 3
        for c in colors:
            assert c.startswith("#")


class TestHashing:
    def test_sha256_bytes_stable(self):
        assert sha256_bytes(b"hello") == sha256_bytes(b"hello")
        assert sha256_bytes(b"hello") != sha256_bytes(b"world")

    def test_sha256_image_stable(self, sample_image_bytes):
        img = bytes_to_numpy(sample_image_bytes)
        assert sha256_image(img) == sha256_image(img)

    def test_phash_stable(self, sample_image_bytes):
        img = bytes_to_numpy(sample_image_bytes)
        assert phash(img) == phash(img)

    def test_phash_hex_length(self, sample_image_bytes):
        img = bytes_to_numpy(sample_image_bytes)
        # 8x8 = 64 bits = 16 hex chars
        assert len(phash(img)) == 16

    def test_dhash_stable(self, sample_image_bytes):
        img = bytes_to_numpy(sample_image_bytes)
        assert dhash(img) == dhash(img)

    def test_ahash_stable(self, sample_image_bytes):
        img = bytes_to_numpy(sample_image_bytes)
        assert ahash(img) == ahash(img)

    def test_whash_stable(self, sample_image_bytes):
        img = bytes_to_numpy(sample_image_bytes)
        # whash falls back to phash if pywt not installed
        result = whash(img)
        assert isinstance(result, str)

    def test_hamming_distance_identical(self):
        assert hamming_distance("ffff", "ffff") == 0

    def test_hamming_distance_different(self):
        assert hamming_distance("0000", "ffff") == 16

    def test_hamming_distance_unequal_length(self):
        assert hamming_distance("ff", "ffff") == 4

    def test_phash_differs_for_different_images(self, sample_image_bytes, sample_face_image_bytes):
        img1 = bytes_to_numpy(sample_image_bytes)
        img2 = bytes_to_numpy(sample_face_image_bytes)
        assert phash(img1) != phash(img2)


class TestQuality:
    def test_brightness_black(self):
        img = np.zeros((100, 100, 3), dtype=np.uint8)
        assert brightness(img) < 5.0

    def test_brightness_white(self):
        img = np.full((100, 100, 3), 255, dtype=np.uint8)
        assert brightness(img) > 250.0

    def test_contrast_uniform(self):
        img = np.full((100, 100, 3), 128, dtype=np.uint8)
        assert contrast(img) < 1.0

    def test_sharpness_uniform(self):
        img = np.full((100, 100, 3), 128, dtype=np.uint8)
        assert sharpness(img) < 1.0

    def test_quality_score_in_range(self, sample_image_bytes):
        img = bytes_to_numpy(sample_image_bytes)
        q = quality_score(img)
        assert 0.0 <= q <= 1.0


class TestDrawing:
    def test_draw_boxes_does_not_modify_original(self):
        img = np.zeros((300, 300, 3), dtype=np.uint8)
        boxes = [BBox(50, 50, 100, 100)]
        out = draw_boxes(img, boxes)
        assert (img == 0).all()
        assert not (out == 0).all()

    def test_draw_boxes_accepts_dict(self):
        img = np.zeros((300, 300, 3), dtype=np.uint8)
        boxes = [{"x": 50, "y": 50, "w": 100, "h": 100}]
        out = draw_boxes(img, boxes)
        assert not np.array_equal(img, out)