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"])