feat: extract opengait_studio monorepo module

Move demo implementation into opengait_studio, retire Sports2D runtime integration, and align packaging with root-level monorepo dependency management.
This commit is contained in:
2026-03-03 17:16:17 +08:00
parent 5c6bef1ca1
commit 00fcda4fe3
39 changed files with 359 additions and 270 deletions
+181
View File
@@ -0,0 +1,181 @@
"""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_studio.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)