Spaces:
Paused
Paused
| """ | |
| Unit tests for MovementClassifier | |
| Tests movement classification, intensity calculation, and rhythm detection | |
| """ | |
| import pytest | |
| import numpy as np | |
| from pathlib import Path | |
| import sys | |
| sys.path.insert(0, str(Path(__file__).parent.parent)) | |
| from app.movement_classifier import MovementClassifier, MovementType, MovementMetrics | |
| from app.pose_analyzer import PoseKeypoints | |
| from app.config import Config | |
| class TestMovementClassifier: | |
| """Test suite for MovementClassifier functionality""" | |
| def classifier(self): | |
| """Create MovementClassifier instance for testing""" | |
| return MovementClassifier() | |
| def standing_sequence(self): | |
| """Create a sequence representing standing still""" | |
| sequence = [] | |
| base_landmarks = np.random.rand(33, 3) | |
| for i in range(10): | |
| # Very small random noise to simulate standing | |
| landmarks = base_landmarks + np.random.normal(0, 0.001, (33, 3)) | |
| landmarks[:, 2] = 0.9 # High confidence | |
| pose = PoseKeypoints( | |
| landmarks=landmarks, | |
| frame_number=i, | |
| timestamp=i/30.0, | |
| confidence=0.9 | |
| ) | |
| sequence.append(pose) | |
| return sequence | |
| def dancing_sequence(self): | |
| """Create a sequence representing dancing (high movement)""" | |
| sequence = [] | |
| for i in range(20): | |
| # Create varied movement | |
| landmarks = np.random.rand(33, 3) | |
| landmarks[:, 2] = 0.9 | |
| # Add more variation to simulate dancing | |
| landmarks[:, 0] += np.sin(i * 0.5) * 0.1 | |
| landmarks[:, 1] += np.cos(i * 0.5) * 0.1 | |
| pose = PoseKeypoints( | |
| landmarks=landmarks, | |
| frame_number=i, | |
| timestamp=i/30.0, | |
| confidence=0.9 | |
| ) | |
| sequence.append(pose) | |
| return sequence | |
| def jumping_sequence(self): | |
| """Create a sequence representing jumping (vertical movement)""" | |
| sequence = [] | |
| base_landmarks = np.random.rand(33, 3) | |
| for i in range(15): | |
| landmarks = base_landmarks.copy() | |
| # Simulate vertical jump (modify hip positions) | |
| jump_height = 0.1 * np.sin(i * np.pi / 7) # Jump cycle | |
| landmarks[23, 1] -= jump_height # Left hip | |
| landmarks[24, 1] -= jump_height # Right hip | |
| landmarks[:, 2] = 0.9 | |
| pose = PoseKeypoints( | |
| landmarks=landmarks, | |
| frame_number=i, | |
| timestamp=i/30.0, | |
| confidence=0.9 | |
| ) | |
| sequence.append(pose) | |
| return sequence | |
| def test_classifier_initialization(self, classifier): | |
| """Test MovementClassifier initializes correctly""" | |
| assert classifier is not None | |
| assert len(classifier.movement_history) == 0 | |
| assert classifier.smoothing_window > 0 | |
| def test_analyze_empty_sequence(self, classifier): | |
| """Test analyzing empty sequence""" | |
| metrics = classifier.analyze_sequence([]) | |
| assert metrics.movement_type == MovementType.UNKNOWN | |
| assert metrics.intensity == 0.0 | |
| assert metrics.velocity == 0.0 | |
| def test_analyze_standing_sequence(self, classifier, standing_sequence): | |
| """Test classification of standing movement""" | |
| metrics = classifier.analyze_sequence(standing_sequence) | |
| assert metrics is not None | |
| # Standing should have low velocity | |
| assert metrics.velocity < Config.VELOCITY_WALKING | |
| # Should have low intensity | |
| assert metrics.intensity < 30.0 | |
| def test_analyze_dancing_sequence(self, classifier, dancing_sequence): | |
| """Test classification of dancing movement""" | |
| metrics = classifier.analyze_sequence(dancing_sequence) | |
| assert metrics is not None | |
| # Dancing should have higher velocity | |
| assert metrics.velocity > Config.VELOCITY_STANDING | |
| # Should have higher intensity | |
| assert metrics.intensity > 20.0 | |
| def test_velocity_calculation(self, classifier, dancing_sequence): | |
| """Test velocity calculation between frames""" | |
| velocities = classifier._calculate_velocities(dancing_sequence) | |
| assert len(velocities) == len(dancing_sequence) - 1 | |
| assert np.all(velocities >= 0) # Velocities should be non-negative | |
| def test_jumping_detection(self, classifier, jumping_sequence): | |
| """Test jumping detection algorithm""" | |
| is_jumping = classifier._detect_jumping(jumping_sequence) | |
| # Jumping sequence should be detected | |
| # Note: This may be True or False depending on threshold sensitivity | |
| assert isinstance(is_jumping, bool) | |
| def test_crouching_detection(self, classifier): | |
| """Test crouching detection""" | |
| # Create crouching pose (compressed torso) | |
| landmarks = np.random.rand(33, 3) | |
| landmarks[11, 1] = 0.3 # Shoulder | |
| landmarks[12, 1] = 0.3 | |
| landmarks[23, 1] = 0.35 # Hip (very close to shoulder) | |
| landmarks[24, 1] = 0.35 | |
| landmarks[:, 2] = 0.9 | |
| crouch_pose = PoseKeypoints( | |
| landmarks=landmarks, | |
| frame_number=0, | |
| timestamp=0.0, | |
| confidence=0.9 | |
| ) | |
| is_crouching = classifier._detect_crouching([crouch_pose]) | |
| assert isinstance(is_crouching, bool) | |
| def test_intensity_calculation(self, classifier, dancing_sequence): | |
| """Test movement intensity calculation""" | |
| velocities = classifier._calculate_velocities(dancing_sequence) | |
| movement_type = MovementType.DANCING | |
| intensity = classifier._calculate_intensity(velocities, movement_type) | |
| assert 0 <= intensity <= 100 | |
| assert isinstance(intensity, (int, float)) | |
| def test_body_part_activity(self, classifier, dancing_sequence): | |
| """Test body part activity calculation""" | |
| activity = classifier._calculate_body_part_activity(dancing_sequence) | |
| # Should have activity scores for all body parts | |
| expected_parts = ["head", "torso", "left_arm", "right_arm", "left_leg", "right_leg"] | |
| for part in expected_parts: | |
| assert part in activity | |
| assert 0 <= activity[part] <= 100 | |
| def test_movement_summary_empty(self, classifier): | |
| """Test movement summary with no data""" | |
| summary = classifier.get_movement_summary() | |
| assert summary['total_sequences'] == 0 | |
| assert summary['average_intensity'] == 0.0 | |
| assert summary['most_active_body_part'] == "none" | |
| def test_movement_summary_with_data(self, classifier, dancing_sequence): | |
| """Test movement summary with analyzed data""" | |
| classifier.analyze_sequence(dancing_sequence) | |
| summary = classifier.get_movement_summary() | |
| assert summary['total_sequences'] == 1 | |
| assert summary['average_intensity'] > 0 | |
| assert 'movement_distribution' in summary | |
| assert 'most_active_body_part' in summary | |
| def test_rhythm_detection_short_sequence(self, classifier, standing_sequence): | |
| """Test rhythm detection with short sequence""" | |
| rhythm = classifier.detect_rhythm_patterns(standing_sequence[:5], fps=30.0) | |
| assert 'has_rhythm' in rhythm | |
| assert 'estimated_bpm' in rhythm | |
| assert rhythm['estimated_bpm'] >= 0 | |
| def test_rhythm_detection_long_sequence(self, classifier, dancing_sequence): | |
| """Test rhythm detection with adequate sequence""" | |
| rhythm = classifier.detect_rhythm_patterns(dancing_sequence, fps=30.0) | |
| assert 'has_rhythm' in rhythm | |
| assert 'estimated_bpm' in rhythm | |
| assert 'peak_count' in rhythm | |
| assert 'rhythm_consistency' in rhythm | |
| def test_find_peaks(self, classifier): | |
| """Test peak detection in signal""" | |
| # Create signal with obvious peaks | |
| signal = np.array([0, 1, 0, 1, 0, 1, 0, 5, 0, 1, 0]) | |
| peaks = classifier._find_peaks(signal, threshold_percentile=50) | |
| assert len(peaks) > 0 | |
| # Peak at index 7 (value=5) should be detected | |
| assert 7 in peaks | |
| def test_movement_smoothness_empty(self, classifier): | |
| """Test smoothness calculation with minimal data""" | |
| sequence = [] | |
| smoothness = classifier.calculate_movement_smoothness(sequence) | |
| assert smoothness == 100.0 # Default for no data | |
| def test_movement_smoothness_smooth_motion(self, classifier, standing_sequence): | |
| """Test smoothness with smooth motion""" | |
| smoothness = classifier.calculate_movement_smoothness(standing_sequence) | |
| assert 0 <= smoothness <= 100 | |
| # Standing should be very smooth | |
| assert smoothness > 80 | |
| def test_movement_smoothness_jerky_motion(self, classifier): | |
| """Test smoothness with jerky motion""" | |
| sequence = [] | |
| for i in range(10): | |
| # Create jerky movement (alternating positions) | |
| landmarks = np.random.rand(33, 3) | |
| if i % 2 == 0: | |
| landmarks[:, 0] += 0.2 | |
| landmarks[:, 2] = 0.9 | |
| pose = PoseKeypoints( | |
| landmarks=landmarks, | |
| frame_number=i, | |
| timestamp=i/30.0, | |
| confidence=0.9 | |
| ) | |
| sequence.append(pose) | |
| smoothness = classifier.calculate_movement_smoothness(sequence) | |
| assert 0 <= smoothness <= 100 | |
| def test_movement_type_enum(self): | |
| """Test MovementType enum values""" | |
| assert MovementType.STANDING.value == "Standing" | |
| assert MovementType.WALKING.value == "Walking" | |
| assert MovementType.DANCING.value == "Dancing" | |
| assert MovementType.JUMPING.value == "Jumping" | |
| assert MovementType.CROUCHING.value == "Crouching" | |
| assert MovementType.UNKNOWN.value == "Unknown" | |
| def test_reset_classifier(self, classifier, dancing_sequence): | |
| """Test resetting classifier clears history""" | |
| classifier.analyze_sequence(dancing_sequence) | |
| assert len(classifier.movement_history) > 0 | |
| classifier.reset() | |
| assert len(classifier.movement_history) == 0 | |
| def test_multiple_sequence_analysis(self, classifier, standing_sequence, dancing_sequence): | |
| """Test analyzing multiple sequences""" | |
| metrics1 = classifier.analyze_sequence(standing_sequence) | |
| metrics2 = classifier.analyze_sequence(dancing_sequence) | |
| assert len(classifier.movement_history) == 2 | |
| assert metrics1.intensity != metrics2.intensity | |
| def test_body_parts_defined(self, classifier): | |
| """Test that all body parts are properly defined""" | |
| assert 'head' in classifier.BODY_PARTS | |
| assert 'torso' in classifier.BODY_PARTS | |
| assert 'left_arm' in classifier.BODY_PARTS | |
| assert 'right_arm' in classifier.BODY_PARTS | |
| assert 'left_leg' in classifier.BODY_PARTS | |
| assert 'right_leg' in classifier.BODY_PARTS | |
| # Each body part should have landmark indices | |
| for part, indices in classifier.BODY_PARTS.items(): | |
| assert len(indices) > 0 | |
| assert all(0 <= idx < 33 for idx in indices) | |
| if __name__ == "__main__": | |
| pytest.main([__file__, "-v"]) |