Spaces:
Paused
Paused
File size: 8,660 Bytes
bc18e51 | 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 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 | """
Unit tests for PoseAnalyzer
Tests pose detection accuracy, keypoint extraction, and skeleton overlay
"""
import pytest
import numpy as np
import cv2
from pathlib import Path
import sys
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent))
from app.pose_analyzer import PoseAnalyzer, PoseKeypoints
from app.config import Config
class TestPoseAnalyzer:
"""Test suite for PoseAnalyzer functionality"""
@pytest.fixture
def analyzer(self):
"""Create PoseAnalyzer instance for testing"""
return PoseAnalyzer()
@pytest.fixture
def sample_frame(self):
"""Create a sample frame for testing"""
# Create a simple test image (640x480)
frame = np.zeros((480, 640, 3), dtype=np.uint8)
# Draw a simple stick figure for testing
# This won't be detected as a real pose, but tests the pipeline
cv2.circle(frame, (320, 100), 30, (255, 255, 255), -1) # Head
cv2.line(frame, (320, 130), (320, 300), (255, 255, 255), 5) # Body
cv2.line(frame, (320, 150), (250, 250), (255, 255, 255), 3) # Left arm
cv2.line(frame, (320, 150), (390, 250), (255, 255, 255), 3) # Right arm
cv2.line(frame, (320, 300), (280, 450), (255, 255, 255), 3) # Left leg
cv2.line(frame, (320, 300), (360, 450), (255, 255, 255), 3) # Right leg
return frame
def test_analyzer_initialization(self, analyzer):
"""Test that PoseAnalyzer initializes correctly"""
assert analyzer is not None
assert analyzer.pose is not None
assert analyzer.mp_pose is not None
assert len(analyzer.keypoints_history) == 0
def test_process_frame_structure(self, analyzer, sample_frame):
"""Test process_frame returns correct structure or None"""
result = analyzer.process_frame(sample_frame, frame_number=0, timestamp=0.0)
# Result can be None (no pose detected) or PoseKeypoints
if result is not None:
assert isinstance(result, PoseKeypoints)
assert result.landmarks.shape == (33, 3)
assert result.frame_number == 0
assert result.timestamp == 0.0
assert 0.0 <= result.confidence <= 1.0
def test_process_empty_frame(self, analyzer):
"""Test processing an empty black frame"""
black_frame = np.zeros((480, 640, 3), dtype=np.uint8)
result = analyzer.process_frame(black_frame, frame_number=0, timestamp=0.0)
# Black frame should not detect any pose
assert result is None or result.confidence < Config.SKELETON_CONFIDENCE_THRESHOLD
def test_draw_skeleton_overlay_no_pose(self, analyzer, sample_frame):
"""Test drawing skeleton when no pose is detected"""
annotated = analyzer.draw_skeleton_overlay(sample_frame, None)
assert annotated is not None
assert annotated.shape == sample_frame.shape
# Should have "No pose detected" text
assert not np.array_equal(annotated, sample_frame)
def test_draw_skeleton_overlay_with_pose(self, analyzer):
"""Test drawing skeleton with valid pose keypoints"""
# Create mock PoseKeypoints
mock_landmarks = np.random.rand(33, 3)
mock_landmarks[:, 2] = 0.9 # High confidence
mock_pose = PoseKeypoints(
landmarks=mock_landmarks,
frame_number=0,
timestamp=0.0,
confidence=0.9
)
test_frame = np.zeros((480, 640, 3), dtype=np.uint8)
annotated = analyzer.draw_skeleton_overlay(test_frame, mock_pose)
assert annotated is not None
assert annotated.shape == test_frame.shape
# Annotated frame should be different from original
assert not np.array_equal(annotated, test_frame)
def test_process_video_batch(self, analyzer, sample_frame):
"""Test batch processing of frames"""
frames = [sample_frame.copy() for _ in range(5)]
results = analyzer.process_video_batch(
frames=frames,
start_frame_number=0,
fps=30.0
)
assert len(results) == 5
# All results should be None or PoseKeypoints
for result in results:
assert result is None or isinstance(result, PoseKeypoints)
def test_get_keypoints_array_empty(self, analyzer):
"""Test getting keypoints array when no frames processed"""
keypoints = analyzer.get_keypoints_array()
assert keypoints.size == 0
def test_get_keypoints_array_with_data(self, analyzer):
"""Test getting keypoints array with processed data"""
# Add mock keypoints to history
for i in range(3):
mock_landmarks = np.random.rand(33, 3)
mock_pose = PoseKeypoints(
landmarks=mock_landmarks,
frame_number=i,
timestamp=i/30.0,
confidence=0.8
)
analyzer.keypoints_history.append(mock_pose)
keypoints = analyzer.get_keypoints_array()
assert keypoints.shape == (3, 33, 3)
def test_get_average_confidence_empty(self, analyzer):
"""Test average confidence with no data"""
avg_conf = analyzer.get_average_confidence()
assert avg_conf == 0.0
def test_get_average_confidence_with_data(self, analyzer):
"""Test average confidence calculation"""
confidences = [0.7, 0.8, 0.9]
for i, conf in enumerate(confidences):
mock_landmarks = np.random.rand(33, 3)
mock_pose = PoseKeypoints(
landmarks=mock_landmarks,
frame_number=i,
timestamp=i/30.0,
confidence=conf
)
analyzer.keypoints_history.append(mock_pose)
avg_conf = analyzer.get_average_confidence()
expected = np.mean(confidences)
assert abs(avg_conf - expected) < 0.001
def test_reset_analyzer(self, analyzer):
"""Test resetting analyzer clears history"""
# Add some data
mock_landmarks = np.random.rand(33, 3)
mock_pose = PoseKeypoints(
landmarks=mock_landmarks,
frame_number=0,
timestamp=0.0,
confidence=0.8
)
analyzer.keypoints_history.append(mock_pose)
assert len(analyzer.keypoints_history) == 1
analyzer.reset()
assert len(analyzer.keypoints_history) == 0
def test_confidence_color_mapping(self, analyzer):
"""Test confidence color mapping"""
# High confidence should be green
high_color = analyzer._get_confidence_color(0.9)
assert high_color == (0, 255, 0)
# Medium confidence should be yellow
med_color = analyzer._get_confidence_color(0.7)
assert med_color == (0, 255, 255)
# Low confidence should be orange
low_color = analyzer._get_confidence_color(0.5)
assert low_color == (0, 165, 255)
def test_landmark_extraction(self, analyzer):
"""Test landmark extraction produces correct shape"""
# This test requires actual MediaPipe output
# We'll test the shape expectations
expected_shape = (33, 3)
# Create mock MediaPipe landmarks
class MockLandmark:
def __init__(self, x, y, vis):
self.x = x
self.y = y
self.visibility = vis
class MockPoseLandmarks:
def __init__(self):
self.landmark = [
MockLandmark(0.5, 0.5, 0.9) for _ in range(33)
]
mock_landmarks = MockPoseLandmarks()
extracted = analyzer._extract_landmarks(mock_landmarks)
assert extracted.shape == expected_shape
assert np.all((extracted[:, :2] >= 0) & (extracted[:, :2] <= 1))
def test_config_values():
"""Test that Config values are properly set for pose detection"""
config = Config.get_mediapipe_config()
assert 'model_complexity' in config
assert 'min_detection_confidence' in config
assert 'min_tracking_confidence' in config
assert 'smooth_landmarks' in config
# Validate ranges
assert 0 <= config['model_complexity'] <= 2
assert 0.0 <= config['min_detection_confidence'] <= 1.0
assert 0.0 <= config['min_tracking_confidence'] <= 1.0
if __name__ == "__main__":
pytest.main([__file__, "-v"]) |