File size: 4,697 Bytes
23d337e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Unit tests for utils/image.py."""

from __future__ import annotations

import base64

import cv2
import numpy as np
import pytest

from utils.image import (
    BBox,
    bytes_to_numpy,
    base64_to_numpy,
    numpy_to_base64,
    image_hash,
    bytes_hash,
    crop_face,
    resize_with_aspect,
    draw_boxes,
)


class TestImageRoundtrip:
    def test_bytes_to_numpy_valid(self, sample_image_bytes):
        img = bytes_to_numpy(sample_image_bytes)
        assert img is not None
        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_roundtrip(self, sample_image_bytes):
        b64 = base64.b64encode(sample_image_bytes).decode()
        img = base64_to_numpy(b64)
        assert img is not None
        assert img.shape[0] > 0

    def test_base64_with_data_uri_prefix(self, sample_image_bytes):
        b64 = "data:image/jpeg;base64," + base64.b64encode(sample_image_bytes).decode()
        img = base64_to_numpy(b64)
        assert img is not None

    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)
        # Should be decodable
        raw = base64.b64decode(b64)
        assert len(raw) > 0


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

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

    def test_bbox_to_face_recognition_tuple(self):
        b = BBox(10, 20, 100, 200)
        assert b.to_face_recognition_tuple() == (20, 110, 220, 10)


class TestCropFace:
    def test_crop_face_basic(self):
        img = np.zeros((300, 300, 3), dtype=np.uint8)
        img[100:200, 100:200] = 255
        bbox = BBox(100, 100, 100, 100)
        crop = crop_face(img, bbox, margin=0.0)
        assert crop.shape[0] == 100
        assert crop.shape[1] == 100
        assert (crop == 255).all()

    def test_crop_face_with_margin(self):
        img = np.zeros((300, 300, 3), dtype=np.uint8)
        bbox = BBox(100, 100, 50, 50)
        crop = crop_face(img, bbox, margin=0.2)
        # 50 + 20% margin each side = 70
        assert crop.shape[0] == 70
        assert crop.shape[1] == 70

    def test_crop_face_clamps_to_bounds(self):
        img = np.zeros((100, 100, 3), dtype=np.uint8)
        bbox = BBox(0, 0, 80, 80)
        crop = crop_face(img, bbox, margin=0.5)
        # Should not exceed image bounds
        assert crop.shape[0] <= 100
        assert crop.shape[1] <= 100


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

    def test_resize_with_aspect_landscape(self):
        img = np.zeros((100, 400, 3), dtype=np.uint8)
        resized = resize_with_aspect(img, max_dim=200)
        assert resized.shape[1] == 200
        assert resized.shape[0] == 50  # aspect preserved

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


class TestHashing:
    def test_image_hash_stable(self, sample_image_bytes):
        img = bytes_to_numpy(sample_image_bytes)
        h1 = image_hash(img)
        h2 = image_hash(img)
        assert h1 == h2

    def test_image_hash_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 image_hash(img1) != image_hash(img2)

    def test_bytes_hash_stable(self):
        assert bytes_hash(b"hello") == bytes_hash(b"hello")
        assert bytes_hash(b"hello") != bytes_hash(b"world")


class TestDrawBoxes:
    def test_draw_boxes_with_dicts(self):
        img = np.zeros((300, 300, 3), dtype=np.uint8)
        boxes = [{"x": 50, "y": 50, "w": 100, "h": 100}]
        out = draw_boxes(img, boxes)
        # Output should differ (rectangles drawn)
        assert not np.array_equal(img, out)
        # Original should not be modified
        assert (img == 0).all()

    def test_draw_boxes_with_bbox_objects(self):
        img = np.zeros((300, 300, 3), dtype=np.uint8)
        boxes = [BBox(50, 50, 100, 100)]
        out = draw_boxes(img, boxes)
        assert not np.array_equal(img, out)