kinemotion 0.10.12__py3-none-any.whl → 0.11.0__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 CHANGED
@@ -1,14 +1,31 @@
1
1
  """Kinemotion: Video-based kinematic analysis for athletic performance."""
2
2
 
3
- from .api import VideoConfig, VideoResult, process_video, process_videos_bulk
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,335 @@ 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
+ velocity_threshold=params.velocity_threshold,
758
+ countermovement_threshold=cm_threshold,
759
+ min_contact_frames=params.min_contact_frames,
760
+ min_eccentric_frames=params.min_contact_frames,
761
+ use_curvature=params.use_curvature,
762
+ window_length=params.smoothing_window,
763
+ polyorder=params.polyorder,
764
+ )
765
+
766
+ if phases is None:
767
+ raise ValueError("Could not detect CMJ phases in video")
768
+
769
+ standing_end, lowest_point, takeoff_frame, landing_frame = phases
770
+
771
+ # Calculate metrics
772
+ if verbose:
773
+ print("Calculating metrics...")
774
+
775
+ # Use signed velocity for CMJ (need direction information)
776
+ from .cmj.analysis import compute_signed_velocity
777
+
778
+ velocities = compute_signed_velocity(
779
+ vertical_positions,
780
+ window_length=params.smoothing_window,
781
+ polyorder=params.polyorder,
782
+ )
783
+
784
+ metrics = calculate_cmj_metrics(
785
+ vertical_positions,
786
+ velocities,
787
+ standing_end,
788
+ lowest_point,
789
+ takeoff_frame,
790
+ landing_frame,
791
+ video.fps,
792
+ tracking_method=tracking_method,
793
+ )
794
+
795
+ # Generate outputs if requested
796
+ if json_output:
797
+ import json
798
+
799
+ output_path = Path(json_output)
800
+ output_path.write_text(json.dumps(metrics.to_dict(), indent=2))
801
+ if verbose:
802
+ print(f"Metrics written to: {json_output}")
803
+
804
+ if output_video:
805
+ if verbose:
806
+ print(f"Generating debug video: {output_video}")
807
+
808
+ with CMJDebugOverlayRenderer(
809
+ output_video,
810
+ video.width,
811
+ video.height,
812
+ video.display_width,
813
+ video.display_height,
814
+ video.fps,
815
+ ) as renderer:
816
+ for i, frame in enumerate(frames):
817
+ annotated = renderer.render_frame(
818
+ frame, smoothed_landmarks[i], i, metrics
819
+ )
820
+ renderer.write_frame(annotated)
821
+
822
+ if verbose:
823
+ print(f"Debug video saved: {output_video}")
824
+
825
+ if verbose:
826
+ print(f"\nJump height: {metrics.jump_height:.3f}m")
827
+ print(f"Flight time: {metrics.flight_time*1000:.1f}ms")
828
+ print(f"Countermovement depth: {metrics.countermovement_depth:.3f}m")
829
+
830
+ return metrics
831
+
832
+
833
+ def process_cmj_videos_bulk(
834
+ configs: list[CMJVideoConfig],
835
+ max_workers: int = 4,
836
+ progress_callback: Callable[[CMJVideoResult], None] | None = None,
837
+ ) -> list[CMJVideoResult]:
838
+ """
839
+ Process multiple CMJ videos in parallel using ProcessPoolExecutor.
840
+
841
+ Args:
842
+ configs: List of CMJVideoConfig objects specifying video paths and parameters
843
+ max_workers: Maximum number of parallel workers (default: 4)
844
+ progress_callback: Optional callback function called after each video completes.
845
+ Receives CMJVideoResult object.
846
+
847
+ Returns:
848
+ List of CMJVideoResult objects, one per input video, in completion order
849
+
850
+ Example:
851
+ >>> configs = [
852
+ ... CMJVideoConfig("video1.mp4"),
853
+ ... CMJVideoConfig("video2.mp4", quality="accurate"),
854
+ ... CMJVideoConfig("video3.mp4", output_video="debug3.mp4"),
855
+ ... ]
856
+ >>> results = process_cmj_videos_bulk(configs, max_workers=4)
857
+ >>> for result in results:
858
+ ... if result.success:
859
+ ... print(f"{result.video_path}: {result.metrics.jump_height:.3f}m")
860
+ ... else:
861
+ ... print(f"{result.video_path}: FAILED - {result.error}")
862
+ """
863
+ results: list[CMJVideoResult] = []
864
+
865
+ # Use ProcessPoolExecutor for CPU-bound video processing
866
+ with ProcessPoolExecutor(max_workers=max_workers) as executor:
867
+ # Submit all jobs
868
+ future_to_config = {
869
+ executor.submit(_process_cmj_video_wrapper, config): config
870
+ for config in configs
871
+ }
872
+
873
+ # Process results as they complete
874
+ for future in as_completed(future_to_config):
875
+ config = future_to_config[future]
876
+ result: CMJVideoResult
877
+
878
+ try:
879
+ result = future.result()
880
+ results.append(result)
881
+ except Exception as e:
882
+ result = CMJVideoResult(
883
+ video_path=config.video_path, success=False, error=str(e)
884
+ )
885
+ results.append(result)
886
+
887
+ # Call progress callback if provided
888
+ if progress_callback:
889
+ progress_callback(result)
890
+
891
+ return results
892
+
893
+
894
+ def _process_cmj_video_wrapper(config: CMJVideoConfig) -> CMJVideoResult:
895
+ """
896
+ Wrapper function for parallel CMJ processing. Must be picklable (top-level function).
897
+
898
+ Args:
899
+ config: CMJVideoConfig object with processing parameters
900
+
901
+ Returns:
902
+ CMJVideoResult object with metrics or error information
903
+ """
904
+ start_time = time.time()
905
+
906
+ try:
907
+ metrics = process_cmj_video(
908
+ video_path=config.video_path,
909
+ quality=config.quality,
910
+ output_video=config.output_video,
911
+ json_output=config.json_output,
912
+ smoothing_window=config.smoothing_window,
913
+ velocity_threshold=config.velocity_threshold,
914
+ countermovement_threshold=config.countermovement_threshold,
915
+ min_contact_frames=config.min_contact_frames,
916
+ visibility_threshold=config.visibility_threshold,
917
+ detection_confidence=config.detection_confidence,
918
+ tracking_confidence=config.tracking_confidence,
919
+ verbose=False, # Disable verbose in parallel mode
920
+ )
921
+
922
+ processing_time = time.time() - start_time
923
+
924
+ return CMJVideoResult(
925
+ video_path=config.video_path,
926
+ success=True,
927
+ metrics=metrics,
928
+ processing_time=processing_time,
929
+ )
930
+
931
+ except Exception as e:
932
+ processing_time = time.time() - start_time
933
+
934
+ return CMJVideoResult(
935
+ video_path=config.video_path,
936
+ success=False,
937
+ error=str(e),
938
+ processing_time=processing_time,
939
+ )
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__":
@@ -0,0 +1,5 @@
1
+ """Counter Movement Jump (CMJ) analysis module."""
2
+
3
+ from kinemotion.cmj.kinematics import CMJMetrics
4
+
5
+ __all__ = ["CMJMetrics"]