Files
OpenGait/tests/demo/test_preprocess.py
T

182 lines
6.1 KiB
Python

"""Unit tests for silhouette preprocessing functions."""
from typing import cast
import numpy as np
from numpy.typing import NDArray
import pytest
from beartype.roar import BeartypeCallHintParamViolation
from jaxtyping import TypeCheckError
from opengait.demo.preprocess import mask_to_silhouette
class TestMaskToSilhouette:
"""Tests for mask_to_silhouette() function."""
def test_valid_mask_returns_correct_shape_dtype_and_range(self) -> None:
"""Valid mask should return (64, 44) float32 array in [0, 1] range."""
# Create a synthetic mask with sufficient area (person-shaped blob)
h, w = 200, 150
mask = np.zeros((h, w), dtype=np.uint8)
# Draw a filled ellipse to simulate a person
center_y, center_x = h // 2, w // 2
axes_y, axes_x = h // 3, w // 4
y, x = np.ogrid[:h, :w]
ellipse_mask = ((x - center_x) / axes_x) ** 2 + (
(y - center_y) / axes_y
) ** 2 <= 1
mask[ellipse_mask] = 255
bbox = (w // 4, h // 6, 3 * w // 4, 5 * h // 6)
result = mask_to_silhouette(mask, bbox)
assert result is not None
result_arr = cast(NDArray[np.float32], result)
assert result_arr.shape == (64, 44)
assert result_arr.dtype == np.float32
assert np.all(result_arr >= 0.0) and np.all(result_arr <= 1.0)
def test_tiny_mask_returns_none(self) -> None:
"""Mask with area below MIN_MASK_AREA should return None."""
# Create a tiny mask
mask = np.zeros((50, 50), dtype=np.uint8)
mask[20:22, 20:22] = 255 # Only 4 pixels, well below MIN_MASK_AREA (500)
bbox = (20, 20, 22, 22)
result = mask_to_silhouette(mask, bbox)
assert result is None
def test_empty_mask_returns_none(self) -> None:
"""Completely empty mask should return None."""
mask = np.zeros((100, 100), dtype=np.uint8)
bbox = (10, 10, 90, 90)
result = mask_to_silhouette(mask, bbox)
assert result is None
def test_full_frame_mask_returns_valid_output(self) -> None:
"""Full-frame mask (large bbox covering entire image) should work."""
h, w = 300, 200
mask = np.zeros((h, w), dtype=np.uint8)
# Create a large filled region
mask[50:250, 30:170] = 255
# Full frame bbox
bbox = (0, 0, w, h)
result = mask_to_silhouette(mask, bbox)
assert result is not None
result_arr = cast(NDArray[np.float32], result)
assert result_arr.shape == (64, 44)
assert result_arr.dtype == np.float32
def test_determinism_same_input_same_output(self) -> None:
"""Same input should always produce same output."""
h, w = 200, 150
mask = np.zeros((h, w), dtype=np.uint8)
# Create a person-shaped region
mask[50:150, 40:110] = 255
bbox = (40, 50, 110, 150)
result1 = mask_to_silhouette(mask, bbox)
result2 = mask_to_silhouette(mask, bbox)
result3 = mask_to_silhouette(mask, bbox)
assert result1 is not None
assert result2 is not None
assert result3 is not None
np.testing.assert_array_equal(result1, result2)
np.testing.assert_array_equal(result2, result3)
def test_hole_inside_mask_is_filled(self) -> None:
h, w = 200, 160
mask = np.zeros((h, w), dtype=np.uint8)
mask[30:170, 40:120] = 255
mask[80:120, 70:90] = 0
bbox = (40, 30, 120, 170)
result = mask_to_silhouette(mask, bbox)
assert result is not None
result_arr = cast(NDArray[np.float32], result)
hole_patch = result_arr[26:38, 18:26]
assert float(np.mean(hole_patch)) > 0.8
def test_hole_fill_works_when_mask_touches_corner(self) -> None:
h, w = 220, 180
mask = np.zeros((h, w), dtype=np.uint8)
mask[0:180, 0:130] = 255
mask[70:120, 55:95] = 0
bbox = (0, 0, 130, 180)
result = mask_to_silhouette(mask, bbox)
assert result is not None
result_arr = cast(NDArray[np.float32], result)
hole_patch = result_arr[24:40, 16:28]
assert float(np.mean(hole_patch)) > 0.75
def test_tall_narrow_mask_valid_output(self) -> None:
"""Tall narrow mask should produce valid silhouette."""
h, w = 400, 50
mask = np.zeros((h, w), dtype=np.uint8)
# Tall narrow person
mask[50:350, 10:40] = 255
bbox = (10, 50, 40, 350)
result = mask_to_silhouette(mask, bbox)
assert result is not None
result_arr = cast(NDArray[np.float32], result)
assert result_arr.shape == (64, 44)
def test_wide_short_mask_valid_output(self) -> None:
"""Wide short mask should produce valid silhouette."""
h, w = 100, 400
mask = np.zeros((h, w), dtype=np.uint8)
# Wide short person
mask[20:80, 50:350] = 255
bbox = (50, 20, 350, 80)
result = mask_to_silhouette(mask, bbox)
assert result is not None
result_arr = cast(NDArray[np.float32], result)
assert result_arr.shape == (64, 44)
def test_beartype_rejects_wrong_dtype(self) -> None:
"""Beartype should reject non-uint8 input."""
# Float array instead of uint8
mask = np.ones((100, 100), dtype=np.float32) * 255
bbox = (10, 10, 90, 90)
with pytest.raises((BeartypeCallHintParamViolation, TypeCheckError)):
_ = mask_to_silhouette(mask, bbox)
def test_beartype_rejects_wrong_ndim(self) -> None:
"""Beartype should reject non-2D array."""
# 3D array instead of 2D
mask = np.ones((100, 100, 3), dtype=np.uint8) * 255
bbox = (10, 10, 90, 90)
with pytest.raises((BeartypeCallHintParamViolation, TypeCheckError)):
_ = mask_to_silhouette(mask, bbox)
def test_beartype_rejects_wrong_bbox_type(self) -> None:
"""Beartype should reject non-tuple bbox."""
mask = np.ones((100, 100), dtype=np.uint8) * 255
# List instead of tuple
bbox = [10, 10, 90, 90]
with pytest.raises((BeartypeCallHintParamViolation, TypeCheckError)):
_ = mask_to_silhouette(mask, bbox)