face-intel / tests /unit /test_image_utils.py
Marwan
Restructure + add reverse face search (PimEyes-style)
f5eeb1c
Raw
History Blame Contribute Delete
4.7 kB
"""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)