DanceDynamics / backend /tests /test_pose_analyzer.py
Prathamesh Sarjerao Vaidya
created
bc18e51
"""
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"])