kinemotion 0.10.12__py3-none-any.whl → 0.11.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of kinemotion might be problematic. Click here for more details.
- kinemotion/__init__.py +18 -1
- kinemotion/api.py +330 -0
- kinemotion/cli.py +2 -0
- kinemotion/cmj/__init__.py +5 -0
- kinemotion/cmj/analysis.py +532 -0
- kinemotion/cmj/cli.py +621 -0
- kinemotion/cmj/debug_overlay.py +514 -0
- kinemotion/cmj/joint_angles.py +290 -0
- kinemotion/cmj/kinematics.py +191 -0
- {kinemotion-0.10.12.dist-info → kinemotion-0.11.1.dist-info}/METADATA +92 -124
- kinemotion-0.11.1.dist-info/RECORD +26 -0
- kinemotion-0.10.12.dist-info/RECORD +0 -20
- {kinemotion-0.10.12.dist-info → kinemotion-0.11.1.dist-info}/WHEEL +0 -0
- {kinemotion-0.10.12.dist-info → kinemotion-0.11.1.dist-info}/entry_points.txt +0 -0
- {kinemotion-0.10.12.dist-info → kinemotion-0.11.1.dist-info}/licenses/LICENSE +0 -0
kinemotion/__init__.py
CHANGED
|
@@ -1,14 +1,31 @@
|
|
|
1
1
|
"""Kinemotion: Video-based kinematic analysis for athletic performance."""
|
|
2
2
|
|
|
3
|
-
from .api import
|
|
3
|
+
from .api import (
|
|
4
|
+
CMJVideoConfig,
|
|
5
|
+
CMJVideoResult,
|
|
6
|
+
VideoConfig,
|
|
7
|
+
VideoResult,
|
|
8
|
+
process_cmj_video,
|
|
9
|
+
process_cmj_videos_bulk,
|
|
10
|
+
process_video,
|
|
11
|
+
process_videos_bulk,
|
|
12
|
+
)
|
|
13
|
+
from .cmj.kinematics import CMJMetrics
|
|
4
14
|
from .dropjump.kinematics import DropJumpMetrics
|
|
5
15
|
|
|
6
16
|
__version__ = "0.1.0"
|
|
7
17
|
|
|
8
18
|
__all__ = [
|
|
19
|
+
# Drop jump API
|
|
9
20
|
"process_video",
|
|
10
21
|
"process_videos_bulk",
|
|
11
22
|
"VideoConfig",
|
|
12
23
|
"VideoResult",
|
|
13
24
|
"DropJumpMetrics",
|
|
25
|
+
# CMJ API
|
|
26
|
+
"process_cmj_video",
|
|
27
|
+
"process_cmj_videos_bulk",
|
|
28
|
+
"CMJVideoConfig",
|
|
29
|
+
"CMJVideoResult",
|
|
30
|
+
"CMJMetrics",
|
|
14
31
|
]
|
kinemotion/api.py
CHANGED
|
@@ -8,6 +8,9 @@ from pathlib import Path
|
|
|
8
8
|
|
|
9
9
|
import numpy as np
|
|
10
10
|
|
|
11
|
+
from .cmj.analysis import detect_cmj_phases
|
|
12
|
+
from .cmj.debug_overlay import CMJDebugOverlayRenderer
|
|
13
|
+
from .cmj.kinematics import CMJMetrics, calculate_cmj_metrics
|
|
11
14
|
from .core.auto_tuning import (
|
|
12
15
|
AnalysisParameters,
|
|
13
16
|
QualityPreset,
|
|
@@ -602,3 +605,330 @@ def _process_video_wrapper(config: VideoConfig) -> VideoResult:
|
|
|
602
605
|
error=str(e),
|
|
603
606
|
processing_time=processing_time,
|
|
604
607
|
)
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
# ========== CMJ Analysis API ==========
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
@dataclass
|
|
614
|
+
class CMJVideoConfig:
|
|
615
|
+
"""Configuration for processing a single CMJ video."""
|
|
616
|
+
|
|
617
|
+
video_path: str
|
|
618
|
+
quality: str = "balanced"
|
|
619
|
+
output_video: str | None = None
|
|
620
|
+
json_output: str | None = None
|
|
621
|
+
smoothing_window: int | None = None
|
|
622
|
+
velocity_threshold: float | None = None
|
|
623
|
+
countermovement_threshold: float | None = None
|
|
624
|
+
min_contact_frames: int | None = None
|
|
625
|
+
visibility_threshold: float | None = None
|
|
626
|
+
detection_confidence: float | None = None
|
|
627
|
+
tracking_confidence: float | None = None
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
@dataclass
|
|
631
|
+
class CMJVideoResult:
|
|
632
|
+
"""Result of processing a single CMJ video."""
|
|
633
|
+
|
|
634
|
+
video_path: str
|
|
635
|
+
success: bool
|
|
636
|
+
metrics: CMJMetrics | None = None
|
|
637
|
+
error: str | None = None
|
|
638
|
+
processing_time: float = 0.0
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def process_cmj_video(
|
|
642
|
+
video_path: str,
|
|
643
|
+
quality: str = "balanced",
|
|
644
|
+
output_video: str | None = None,
|
|
645
|
+
json_output: str | None = None,
|
|
646
|
+
smoothing_window: int | None = None,
|
|
647
|
+
velocity_threshold: float | None = None,
|
|
648
|
+
countermovement_threshold: float | None = None,
|
|
649
|
+
min_contact_frames: int | None = None,
|
|
650
|
+
visibility_threshold: float | None = None,
|
|
651
|
+
detection_confidence: float | None = None,
|
|
652
|
+
tracking_confidence: float | None = None,
|
|
653
|
+
verbose: bool = False,
|
|
654
|
+
) -> CMJMetrics:
|
|
655
|
+
"""
|
|
656
|
+
Process a single CMJ video and return metrics.
|
|
657
|
+
|
|
658
|
+
CMJ (Counter Movement Jump) is performed at floor level without a drop box.
|
|
659
|
+
Athletes start standing, perform a countermovement (eccentric phase), then
|
|
660
|
+
jump upward (concentric phase).
|
|
661
|
+
|
|
662
|
+
Args:
|
|
663
|
+
video_path: Path to the input video file
|
|
664
|
+
quality: Analysis quality preset ("fast", "balanced", or "accurate")
|
|
665
|
+
output_video: Optional path for debug video output
|
|
666
|
+
json_output: Optional path for JSON metrics output
|
|
667
|
+
smoothing_window: Optional override for smoothing window
|
|
668
|
+
velocity_threshold: Optional override for velocity threshold
|
|
669
|
+
countermovement_threshold: Optional override for countermovement threshold
|
|
670
|
+
min_contact_frames: Optional override for minimum contact frames
|
|
671
|
+
visibility_threshold: Optional override for visibility threshold
|
|
672
|
+
detection_confidence: Optional override for pose detection confidence
|
|
673
|
+
tracking_confidence: Optional override for pose tracking confidence
|
|
674
|
+
verbose: Print processing details
|
|
675
|
+
|
|
676
|
+
Returns:
|
|
677
|
+
CMJMetrics object containing analysis results
|
|
678
|
+
|
|
679
|
+
Raises:
|
|
680
|
+
ValueError: If video cannot be processed or parameters are invalid
|
|
681
|
+
FileNotFoundError: If video file does not exist
|
|
682
|
+
|
|
683
|
+
Example:
|
|
684
|
+
>>> metrics = process_cmj_video(
|
|
685
|
+
... "athlete_cmj.mp4",
|
|
686
|
+
... quality="balanced",
|
|
687
|
+
... verbose=True
|
|
688
|
+
... )
|
|
689
|
+
>>> print(f"Jump height: {metrics.jump_height:.3f}m")
|
|
690
|
+
>>> print(f"Countermovement depth: {metrics.countermovement_depth:.3f}m")
|
|
691
|
+
"""
|
|
692
|
+
if not Path(video_path).exists():
|
|
693
|
+
raise FileNotFoundError(f"Video file not found: {video_path}")
|
|
694
|
+
|
|
695
|
+
# Convert quality string to enum
|
|
696
|
+
quality_preset = _parse_quality_preset(quality)
|
|
697
|
+
|
|
698
|
+
# Initialize video processor
|
|
699
|
+
with VideoProcessor(video_path) as video:
|
|
700
|
+
if verbose:
|
|
701
|
+
print(
|
|
702
|
+
f"Video: {video.width}x{video.height} @ {video.fps:.2f} fps, "
|
|
703
|
+
f"{video.frame_count} frames"
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
# Determine confidence levels
|
|
707
|
+
det_conf, track_conf = _determine_confidence_levels(
|
|
708
|
+
quality_preset, detection_confidence, tracking_confidence
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
# Track all frames
|
|
712
|
+
tracker = PoseTracker(
|
|
713
|
+
min_detection_confidence=det_conf, min_tracking_confidence=track_conf
|
|
714
|
+
)
|
|
715
|
+
frames, landmarks_sequence = _process_all_frames(video, tracker, verbose)
|
|
716
|
+
|
|
717
|
+
# Auto-tune parameters
|
|
718
|
+
characteristics = analyze_video_sample(
|
|
719
|
+
landmarks_sequence, video.fps, video.frame_count
|
|
720
|
+
)
|
|
721
|
+
params = auto_tune_parameters(characteristics, quality_preset)
|
|
722
|
+
|
|
723
|
+
# Apply expert overrides
|
|
724
|
+
params = _apply_expert_overrides(
|
|
725
|
+
params,
|
|
726
|
+
smoothing_window,
|
|
727
|
+
velocity_threshold,
|
|
728
|
+
min_contact_frames,
|
|
729
|
+
visibility_threshold,
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
if verbose:
|
|
733
|
+
_print_verbose_parameters(video, characteristics, quality_preset, params)
|
|
734
|
+
|
|
735
|
+
# Apply smoothing
|
|
736
|
+
smoothed_landmarks = _apply_smoothing(landmarks_sequence, params, verbose)
|
|
737
|
+
|
|
738
|
+
# Extract foot positions
|
|
739
|
+
if verbose:
|
|
740
|
+
print("Extracting foot positions...")
|
|
741
|
+
vertical_positions, _ = _extract_vertical_positions(smoothed_landmarks)
|
|
742
|
+
tracking_method = "foot"
|
|
743
|
+
|
|
744
|
+
# Calculate countermovement threshold (FPS-adjusted)
|
|
745
|
+
# POSITIVE threshold for downward motion (squatting) in normalized coordinates
|
|
746
|
+
cm_threshold = countermovement_threshold
|
|
747
|
+
if cm_threshold is None:
|
|
748
|
+
cm_threshold = 0.015 * (30.0 / video.fps)
|
|
749
|
+
|
|
750
|
+
# Detect CMJ phases
|
|
751
|
+
if verbose:
|
|
752
|
+
print("Detecting CMJ phases...")
|
|
753
|
+
|
|
754
|
+
phases = detect_cmj_phases(
|
|
755
|
+
vertical_positions,
|
|
756
|
+
video.fps,
|
|
757
|
+
window_length=params.smoothing_window,
|
|
758
|
+
polyorder=params.polyorder,
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
if phases is None:
|
|
762
|
+
raise ValueError("Could not detect CMJ phases in video")
|
|
763
|
+
|
|
764
|
+
standing_end, lowest_point, takeoff_frame, landing_frame = phases
|
|
765
|
+
|
|
766
|
+
# Calculate metrics
|
|
767
|
+
if verbose:
|
|
768
|
+
print("Calculating metrics...")
|
|
769
|
+
|
|
770
|
+
# Use signed velocity for CMJ (need direction information)
|
|
771
|
+
from .cmj.analysis import compute_signed_velocity
|
|
772
|
+
|
|
773
|
+
velocities = compute_signed_velocity(
|
|
774
|
+
vertical_positions,
|
|
775
|
+
window_length=params.smoothing_window,
|
|
776
|
+
polyorder=params.polyorder,
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
metrics = calculate_cmj_metrics(
|
|
780
|
+
vertical_positions,
|
|
781
|
+
velocities,
|
|
782
|
+
standing_end,
|
|
783
|
+
lowest_point,
|
|
784
|
+
takeoff_frame,
|
|
785
|
+
landing_frame,
|
|
786
|
+
video.fps,
|
|
787
|
+
tracking_method=tracking_method,
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
# Generate outputs if requested
|
|
791
|
+
if json_output:
|
|
792
|
+
import json
|
|
793
|
+
|
|
794
|
+
output_path = Path(json_output)
|
|
795
|
+
output_path.write_text(json.dumps(metrics.to_dict(), indent=2))
|
|
796
|
+
if verbose:
|
|
797
|
+
print(f"Metrics written to: {json_output}")
|
|
798
|
+
|
|
799
|
+
if output_video:
|
|
800
|
+
if verbose:
|
|
801
|
+
print(f"Generating debug video: {output_video}")
|
|
802
|
+
|
|
803
|
+
with CMJDebugOverlayRenderer(
|
|
804
|
+
output_video,
|
|
805
|
+
video.width,
|
|
806
|
+
video.height,
|
|
807
|
+
video.display_width,
|
|
808
|
+
video.display_height,
|
|
809
|
+
video.fps,
|
|
810
|
+
) as renderer:
|
|
811
|
+
for i, frame in enumerate(frames):
|
|
812
|
+
annotated = renderer.render_frame(
|
|
813
|
+
frame, smoothed_landmarks[i], i, metrics
|
|
814
|
+
)
|
|
815
|
+
renderer.write_frame(annotated)
|
|
816
|
+
|
|
817
|
+
if verbose:
|
|
818
|
+
print(f"Debug video saved: {output_video}")
|
|
819
|
+
|
|
820
|
+
if verbose:
|
|
821
|
+
print(f"\nJump height: {metrics.jump_height:.3f}m")
|
|
822
|
+
print(f"Flight time: {metrics.flight_time*1000:.1f}ms")
|
|
823
|
+
print(f"Countermovement depth: {metrics.countermovement_depth:.3f}m")
|
|
824
|
+
|
|
825
|
+
return metrics
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
def process_cmj_videos_bulk(
|
|
829
|
+
configs: list[CMJVideoConfig],
|
|
830
|
+
max_workers: int = 4,
|
|
831
|
+
progress_callback: Callable[[CMJVideoResult], None] | None = None,
|
|
832
|
+
) -> list[CMJVideoResult]:
|
|
833
|
+
"""
|
|
834
|
+
Process multiple CMJ videos in parallel using ProcessPoolExecutor.
|
|
835
|
+
|
|
836
|
+
Args:
|
|
837
|
+
configs: List of CMJVideoConfig objects specifying video paths and parameters
|
|
838
|
+
max_workers: Maximum number of parallel workers (default: 4)
|
|
839
|
+
progress_callback: Optional callback function called after each video completes.
|
|
840
|
+
Receives CMJVideoResult object.
|
|
841
|
+
|
|
842
|
+
Returns:
|
|
843
|
+
List of CMJVideoResult objects, one per input video, in completion order
|
|
844
|
+
|
|
845
|
+
Example:
|
|
846
|
+
>>> configs = [
|
|
847
|
+
... CMJVideoConfig("video1.mp4"),
|
|
848
|
+
... CMJVideoConfig("video2.mp4", quality="accurate"),
|
|
849
|
+
... CMJVideoConfig("video3.mp4", output_video="debug3.mp4"),
|
|
850
|
+
... ]
|
|
851
|
+
>>> results = process_cmj_videos_bulk(configs, max_workers=4)
|
|
852
|
+
>>> for result in results:
|
|
853
|
+
... if result.success:
|
|
854
|
+
... print(f"{result.video_path}: {result.metrics.jump_height:.3f}m")
|
|
855
|
+
... else:
|
|
856
|
+
... print(f"{result.video_path}: FAILED - {result.error}")
|
|
857
|
+
"""
|
|
858
|
+
results: list[CMJVideoResult] = []
|
|
859
|
+
|
|
860
|
+
# Use ProcessPoolExecutor for CPU-bound video processing
|
|
861
|
+
with ProcessPoolExecutor(max_workers=max_workers) as executor:
|
|
862
|
+
# Submit all jobs
|
|
863
|
+
future_to_config = {
|
|
864
|
+
executor.submit(_process_cmj_video_wrapper, config): config
|
|
865
|
+
for config in configs
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
# Process results as they complete
|
|
869
|
+
for future in as_completed(future_to_config):
|
|
870
|
+
config = future_to_config[future]
|
|
871
|
+
result: CMJVideoResult
|
|
872
|
+
|
|
873
|
+
try:
|
|
874
|
+
result = future.result()
|
|
875
|
+
results.append(result)
|
|
876
|
+
except Exception as e:
|
|
877
|
+
result = CMJVideoResult(
|
|
878
|
+
video_path=config.video_path, success=False, error=str(e)
|
|
879
|
+
)
|
|
880
|
+
results.append(result)
|
|
881
|
+
|
|
882
|
+
# Call progress callback if provided
|
|
883
|
+
if progress_callback:
|
|
884
|
+
progress_callback(result)
|
|
885
|
+
|
|
886
|
+
return results
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
def _process_cmj_video_wrapper(config: CMJVideoConfig) -> CMJVideoResult:
|
|
890
|
+
"""
|
|
891
|
+
Wrapper function for parallel CMJ processing. Must be picklable (top-level function).
|
|
892
|
+
|
|
893
|
+
Args:
|
|
894
|
+
config: CMJVideoConfig object with processing parameters
|
|
895
|
+
|
|
896
|
+
Returns:
|
|
897
|
+
CMJVideoResult object with metrics or error information
|
|
898
|
+
"""
|
|
899
|
+
start_time = time.time()
|
|
900
|
+
|
|
901
|
+
try:
|
|
902
|
+
metrics = process_cmj_video(
|
|
903
|
+
video_path=config.video_path,
|
|
904
|
+
quality=config.quality,
|
|
905
|
+
output_video=config.output_video,
|
|
906
|
+
json_output=config.json_output,
|
|
907
|
+
smoothing_window=config.smoothing_window,
|
|
908
|
+
velocity_threshold=config.velocity_threshold,
|
|
909
|
+
countermovement_threshold=config.countermovement_threshold,
|
|
910
|
+
min_contact_frames=config.min_contact_frames,
|
|
911
|
+
visibility_threshold=config.visibility_threshold,
|
|
912
|
+
detection_confidence=config.detection_confidence,
|
|
913
|
+
tracking_confidence=config.tracking_confidence,
|
|
914
|
+
verbose=False, # Disable verbose in parallel mode
|
|
915
|
+
)
|
|
916
|
+
|
|
917
|
+
processing_time = time.time() - start_time
|
|
918
|
+
|
|
919
|
+
return CMJVideoResult(
|
|
920
|
+
video_path=config.video_path,
|
|
921
|
+
success=True,
|
|
922
|
+
metrics=metrics,
|
|
923
|
+
processing_time=processing_time,
|
|
924
|
+
)
|
|
925
|
+
|
|
926
|
+
except Exception as e:
|
|
927
|
+
processing_time = time.time() - start_time
|
|
928
|
+
|
|
929
|
+
return CMJVideoResult(
|
|
930
|
+
video_path=config.video_path,
|
|
931
|
+
success=False,
|
|
932
|
+
error=str(e),
|
|
933
|
+
processing_time=processing_time,
|
|
934
|
+
)
|
kinemotion/cli.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import click
|
|
4
4
|
|
|
5
|
+
from .cmj.cli import cmj_analyze
|
|
5
6
|
from .dropjump.cli import dropjump_analyze
|
|
6
7
|
|
|
7
8
|
|
|
@@ -14,6 +15,7 @@ def cli() -> None:
|
|
|
14
15
|
|
|
15
16
|
# Register commands from submodules
|
|
16
17
|
cli.add_command(dropjump_analyze)
|
|
18
|
+
cli.add_command(cmj_analyze)
|
|
17
19
|
|
|
18
20
|
|
|
19
21
|
if __name__ == "__main__":
|