mirror of
https://github.com/yakhyo/uniface.git
synced 2025-12-30 09:02:25 +00:00
248 lines
8.1 KiB
Python
248 lines
8.1 KiB
Python
|
|
import numpy as np
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from uniface import compute_similarity, face_alignment
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def mock_image():
|
||
|
|
"""
|
||
|
|
Create a mock 640x640 BGR image.
|
||
|
|
"""
|
||
|
|
return np.random.randint(0, 255, (640, 640, 3), dtype=np.uint8)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def mock_landmarks():
|
||
|
|
"""
|
||
|
|
Create mock 5-point facial landmarks.
|
||
|
|
Standard positions for a face roughly centered at (112/2, 112/2).
|
||
|
|
"""
|
||
|
|
return np.array([
|
||
|
|
[38.2946, 51.6963], # Left eye
|
||
|
|
[73.5318, 51.5014], # Right eye
|
||
|
|
[56.0252, 71.7366], # Nose
|
||
|
|
[41.5493, 92.3655], # Left mouth corner
|
||
|
|
[70.7299, 92.2041] # Right mouth corner
|
||
|
|
], dtype=np.float32)
|
||
|
|
|
||
|
|
|
||
|
|
# compute_similarity tests
|
||
|
|
def test_compute_similarity_same_embedding():
|
||
|
|
"""
|
||
|
|
Test that similarity of an embedding with itself is 1.0.
|
||
|
|
"""
|
||
|
|
embedding = np.random.randn(1, 512).astype(np.float32)
|
||
|
|
embedding = embedding / np.linalg.norm(embedding) # Normalize
|
||
|
|
|
||
|
|
similarity = compute_similarity(embedding, embedding)
|
||
|
|
assert np.isclose(similarity, 1.0, atol=1e-5), f"Self-similarity should be 1.0, got {similarity}"
|
||
|
|
|
||
|
|
|
||
|
|
def test_compute_similarity_range():
|
||
|
|
"""
|
||
|
|
Test that similarity is always in the range [-1, 1].
|
||
|
|
"""
|
||
|
|
# Test with multiple random embeddings
|
||
|
|
for _ in range(10):
|
||
|
|
emb1 = np.random.randn(1, 512).astype(np.float32)
|
||
|
|
emb2 = np.random.randn(1, 512).astype(np.float32)
|
||
|
|
|
||
|
|
# Normalize
|
||
|
|
emb1 = emb1 / np.linalg.norm(emb1)
|
||
|
|
emb2 = emb2 / np.linalg.norm(emb2)
|
||
|
|
|
||
|
|
similarity = compute_similarity(emb1, emb2)
|
||
|
|
assert -1.0 <= similarity <= 1.0, f"Similarity should be in [-1, 1], got {similarity}"
|
||
|
|
|
||
|
|
|
||
|
|
def test_compute_similarity_orthogonal():
|
||
|
|
"""
|
||
|
|
Test that orthogonal embeddings have similarity close to 0.
|
||
|
|
"""
|
||
|
|
# Create orthogonal embeddings
|
||
|
|
emb1 = np.zeros((1, 512), dtype=np.float32)
|
||
|
|
emb1[0, 0] = 1.0 # [1, 0, 0, ..., 0]
|
||
|
|
|
||
|
|
emb2 = np.zeros((1, 512), dtype=np.float32)
|
||
|
|
emb2[0, 1] = 1.0 # [0, 1, 0, ..., 0]
|
||
|
|
|
||
|
|
similarity = compute_similarity(emb1, emb2)
|
||
|
|
assert np.isclose(similarity, 0.0, atol=1e-5), f"Orthogonal embeddings should have similarity 0.0, got {similarity}"
|
||
|
|
|
||
|
|
|
||
|
|
def test_compute_similarity_opposite():
|
||
|
|
"""
|
||
|
|
Test that opposite embeddings have similarity close to -1.
|
||
|
|
"""
|
||
|
|
emb1 = np.ones((1, 512), dtype=np.float32)
|
||
|
|
emb1 = emb1 / np.linalg.norm(emb1)
|
||
|
|
|
||
|
|
emb2 = -emb1 # Opposite direction
|
||
|
|
|
||
|
|
similarity = compute_similarity(emb1, emb2)
|
||
|
|
assert np.isclose(similarity, -1.0, atol=1e-5), f"Opposite embeddings should have similarity -1.0, got {similarity}"
|
||
|
|
|
||
|
|
|
||
|
|
def test_compute_similarity_symmetry():
|
||
|
|
"""
|
||
|
|
Test that similarity(A, B) == similarity(B, A).
|
||
|
|
"""
|
||
|
|
emb1 = np.random.randn(1, 512).astype(np.float32)
|
||
|
|
emb2 = np.random.randn(1, 512).astype(np.float32)
|
||
|
|
|
||
|
|
# Normalize
|
||
|
|
emb1 = emb1 / np.linalg.norm(emb1)
|
||
|
|
emb2 = emb2 / np.linalg.norm(emb2)
|
||
|
|
|
||
|
|
sim_12 = compute_similarity(emb1, emb2)
|
||
|
|
sim_21 = compute_similarity(emb2, emb1)
|
||
|
|
|
||
|
|
assert np.isclose(sim_12, sim_21), "Similarity should be symmetric"
|
||
|
|
|
||
|
|
|
||
|
|
def test_compute_similarity_dtype():
|
||
|
|
"""
|
||
|
|
Test that compute_similarity returns a float.
|
||
|
|
"""
|
||
|
|
emb1 = np.random.randn(1, 512).astype(np.float32)
|
||
|
|
emb2 = np.random.randn(1, 512).astype(np.float32)
|
||
|
|
|
||
|
|
# Normalize
|
||
|
|
emb1 = emb1 / np.linalg.norm(emb1)
|
||
|
|
emb2 = emb2 / np.linalg.norm(emb2)
|
||
|
|
|
||
|
|
similarity = compute_similarity(emb1, emb2)
|
||
|
|
assert isinstance(similarity, (float, np.floating)), f"Similarity should be float, got {type(similarity)}"
|
||
|
|
|
||
|
|
|
||
|
|
# face_alignment tests
|
||
|
|
def test_face_alignment_output_shape(mock_image, mock_landmarks):
|
||
|
|
"""
|
||
|
|
Test that face_alignment produces output with the correct shape.
|
||
|
|
"""
|
||
|
|
aligned, _ = face_alignment(mock_image, mock_landmarks, image_size=(112, 112))
|
||
|
|
|
||
|
|
assert aligned.shape == (112, 112, 3), f"Expected shape (112, 112, 3), got {aligned.shape}"
|
||
|
|
|
||
|
|
|
||
|
|
def test_face_alignment_dtype(mock_image, mock_landmarks):
|
||
|
|
"""
|
||
|
|
Test that aligned face has the correct data type.
|
||
|
|
"""
|
||
|
|
aligned, _ = face_alignment(mock_image, mock_landmarks, image_size=(112, 112))
|
||
|
|
|
||
|
|
assert aligned.dtype == np.uint8, f"Expected uint8, got {aligned.dtype}"
|
||
|
|
|
||
|
|
|
||
|
|
def test_face_alignment_different_sizes(mock_image, mock_landmarks):
|
||
|
|
"""
|
||
|
|
Test face alignment with different output sizes.
|
||
|
|
"""
|
||
|
|
# Only test sizes that are multiples of 112 or 128 as required by the function
|
||
|
|
test_sizes = [(112, 112), (128, 128), (224, 224)]
|
||
|
|
|
||
|
|
for size in test_sizes:
|
||
|
|
aligned, _ = face_alignment(mock_image, mock_landmarks, image_size=size)
|
||
|
|
assert aligned.shape == (*size, 3), f"Failed for size {size}"
|
||
|
|
|
||
|
|
|
||
|
|
def test_face_alignment_consistency(mock_image, mock_landmarks):
|
||
|
|
"""
|
||
|
|
Test that the same input produces the same aligned face.
|
||
|
|
"""
|
||
|
|
aligned1, _ = face_alignment(mock_image, mock_landmarks, image_size=(112, 112))
|
||
|
|
aligned2, _ = face_alignment(mock_image, mock_landmarks, image_size=(112, 112))
|
||
|
|
|
||
|
|
assert np.allclose(aligned1, aligned2), "Same input should produce same aligned face"
|
||
|
|
|
||
|
|
|
||
|
|
def test_face_alignment_landmarks_as_list(mock_image):
|
||
|
|
"""
|
||
|
|
Test that landmarks can be passed as a list of lists (converted to array).
|
||
|
|
"""
|
||
|
|
landmarks_list = [
|
||
|
|
[38.2946, 51.6963],
|
||
|
|
[73.5318, 51.5014],
|
||
|
|
[56.0252, 71.7366],
|
||
|
|
[41.5493, 92.3655],
|
||
|
|
[70.7299, 92.2041]
|
||
|
|
]
|
||
|
|
|
||
|
|
# Convert list to numpy array before passing to face_alignment
|
||
|
|
landmarks_array = np.array(landmarks_list, dtype=np.float32)
|
||
|
|
aligned, _ = face_alignment(mock_image, landmarks_array, image_size=(112, 112))
|
||
|
|
assert aligned.shape == (112, 112, 3), "Should work with landmarks as array"
|
||
|
|
|
||
|
|
|
||
|
|
def test_face_alignment_value_range(mock_image, mock_landmarks):
|
||
|
|
"""
|
||
|
|
Test that aligned face pixel values are in valid range [0, 255].
|
||
|
|
"""
|
||
|
|
aligned, _ = face_alignment(mock_image, mock_landmarks, image_size=(112, 112))
|
||
|
|
|
||
|
|
assert np.all(aligned >= 0), "Pixel values should be >= 0"
|
||
|
|
assert np.all(aligned <= 255), "Pixel values should be <= 255"
|
||
|
|
|
||
|
|
|
||
|
|
def test_face_alignment_not_all_zeros(mock_image, mock_landmarks):
|
||
|
|
"""
|
||
|
|
Test that aligned face is not all zeros (actual transformation occurred).
|
||
|
|
"""
|
||
|
|
aligned, _ = face_alignment(mock_image, mock_landmarks, image_size=(112, 112))
|
||
|
|
|
||
|
|
# At least some pixels should be non-zero
|
||
|
|
assert np.any(aligned > 0), "Aligned face should have some non-zero pixels"
|
||
|
|
|
||
|
|
|
||
|
|
def test_face_alignment_from_different_positions(mock_image):
|
||
|
|
"""
|
||
|
|
Test alignment with landmarks at different positions in the image.
|
||
|
|
"""
|
||
|
|
# Landmarks at different positions
|
||
|
|
positions = [
|
||
|
|
np.array([[100, 100], [150, 100], [125, 130], [110, 150], [140, 150]], dtype=np.float32),
|
||
|
|
np.array([[300, 200], [350, 200], [325, 230], [310, 250], [340, 250]], dtype=np.float32),
|
||
|
|
np.array([[500, 400], [550, 400], [525, 430], [510, 450], [540, 450]], dtype=np.float32),
|
||
|
|
]
|
||
|
|
|
||
|
|
for landmarks in positions:
|
||
|
|
aligned, _ = face_alignment(mock_image, landmarks, image_size=(112, 112))
|
||
|
|
assert aligned.shape == (112, 112, 3), f"Failed for landmarks at {landmarks[0]}"
|
||
|
|
|
||
|
|
|
||
|
|
def test_face_alignment_landmark_count(mock_image):
|
||
|
|
"""
|
||
|
|
Test that face_alignment works specifically with 5-point landmarks.
|
||
|
|
"""
|
||
|
|
# Standard 5-point landmarks
|
||
|
|
landmarks_5pt = np.array([
|
||
|
|
[38.2946, 51.6963],
|
||
|
|
[73.5318, 51.5014],
|
||
|
|
[56.0252, 71.7366],
|
||
|
|
[41.5493, 92.3655],
|
||
|
|
[70.7299, 92.2041]
|
||
|
|
], dtype=np.float32)
|
||
|
|
|
||
|
|
aligned, _ = face_alignment(mock_image, landmarks_5pt, image_size=(112, 112))
|
||
|
|
assert aligned.shape == (112, 112, 3), "Should work with 5-point landmarks"
|
||
|
|
|
||
|
|
|
||
|
|
def test_compute_similarity_with_recognition_embeddings():
|
||
|
|
"""
|
||
|
|
Test compute_similarity with realistic embedding dimensions.
|
||
|
|
"""
|
||
|
|
# Simulate ArcFace/MobileFace/SphereFace embeddings (512-dim)
|
||
|
|
emb1 = np.random.randn(1, 512).astype(np.float32)
|
||
|
|
emb2 = np.random.randn(1, 512).astype(np.float32)
|
||
|
|
|
||
|
|
# Normalize (as done in get_normalized_embedding)
|
||
|
|
emb1 = emb1 / np.linalg.norm(emb1)
|
||
|
|
emb2 = emb2 / np.linalg.norm(emb2)
|
||
|
|
|
||
|
|
similarity = compute_similarity(emb1, emb2)
|
||
|
|
|
||
|
|
# Should be a valid similarity score
|
||
|
|
assert -1.0 <= similarity <= 1.0
|
||
|
|
assert isinstance(similarity, (float, np.floating))
|