kinemotion 0.76.3__py3-none-any.whl → 2.0.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 +3 -18
- kinemotion/api.py +7 -27
- kinemotion/cli.py +2 -4
- kinemotion/{countermovement_jump → cmj}/analysis.py +158 -16
- kinemotion/{countermovement_jump → cmj}/api.py +18 -46
- kinemotion/{countermovement_jump → cmj}/cli.py +46 -6
- kinemotion/cmj/debug_overlay.py +457 -0
- kinemotion/{countermovement_jump → cmj}/joint_angles.py +31 -96
- kinemotion/{countermovement_jump → cmj}/metrics_validator.py +271 -176
- kinemotion/{countermovement_jump → cmj}/validation_bounds.py +18 -1
- kinemotion/core/__init__.py +2 -11
- kinemotion/core/auto_tuning.py +107 -149
- kinemotion/core/cli_utils.py +0 -74
- kinemotion/core/debug_overlay_utils.py +15 -142
- kinemotion/core/experimental.py +51 -55
- kinemotion/core/filtering.py +56 -116
- kinemotion/core/pipeline_utils.py +2 -2
- kinemotion/core/pose.py +98 -47
- kinemotion/core/quality.py +6 -4
- kinemotion/core/smoothing.py +51 -65
- kinemotion/core/types.py +0 -15
- kinemotion/core/validation.py +7 -76
- kinemotion/core/video_io.py +27 -41
- kinemotion/{drop_jump → dropjump}/__init__.py +8 -2
- kinemotion/{drop_jump → dropjump}/analysis.py +120 -282
- kinemotion/{drop_jump → dropjump}/api.py +33 -59
- kinemotion/{drop_jump → dropjump}/cli.py +136 -70
- kinemotion/dropjump/debug_overlay.py +182 -0
- kinemotion/{drop_jump → dropjump}/kinematics.py +65 -175
- kinemotion/{drop_jump → dropjump}/metrics_validator.py +51 -25
- kinemotion/{drop_jump → dropjump}/validation_bounds.py +1 -1
- kinemotion/models/rtmpose-s_simcc-body7_pt-body7-halpe26_700e-256x192-7f134165_20230605.onnx +3 -0
- kinemotion/models/yolox_tiny_8xb8-300e_humanart-6f3252f9.onnx +3 -0
- {kinemotion-0.76.3.dist-info → kinemotion-2.0.0.dist-info}/METADATA +26 -75
- kinemotion-2.0.0.dist-info/RECORD +49 -0
- kinemotion/core/overlay_constants.py +0 -61
- kinemotion/core/video_analysis_base.py +0 -132
- kinemotion/countermovement_jump/debug_overlay.py +0 -325
- kinemotion/drop_jump/debug_overlay.py +0 -241
- kinemotion/squat_jump/__init__.py +0 -5
- kinemotion/squat_jump/analysis.py +0 -377
- kinemotion/squat_jump/api.py +0 -610
- kinemotion/squat_jump/cli.py +0 -309
- kinemotion/squat_jump/debug_overlay.py +0 -163
- kinemotion/squat_jump/kinematics.py +0 -342
- kinemotion/squat_jump/metrics_validator.py +0 -438
- kinemotion/squat_jump/validation_bounds.py +0 -221
- kinemotion-0.76.3.dist-info/RECORD +0 -57
- /kinemotion/{countermovement_jump → cmj}/__init__.py +0 -0
- /kinemotion/{countermovement_jump → cmj}/kinematics.py +0 -0
- {kinemotion-0.76.3.dist-info → kinemotion-2.0.0.dist-info}/WHEEL +0 -0
- {kinemotion-0.76.3.dist-info → kinemotion-2.0.0.dist-info}/entry_points.txt +0 -0
- {kinemotion-0.76.3.dist-info → kinemotion-2.0.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
kinemotion/__init__.py,sha256=Ho_BUtsM0PBxBW1ye9RlUg0ZqBlgGudRI9bZTF7QKUI,966
|
|
2
|
+
kinemotion/api.py,sha256=uG1e4bTnj2c-6cbZJEZ_LjMwFdaG32ba2KcK_XjE_NI,1040
|
|
3
|
+
kinemotion/cli.py,sha256=_Us9krSce4GUKtlLIPrFUhKmPWURzeJ1-ydR_YU2VGw,626
|
|
4
|
+
kinemotion/cmj/__init__.py,sha256=SkAw9ka8Yd1Qfv9hcvk22m3EfucROzYrSNGNF5kDzho,113
|
|
5
|
+
kinemotion/cmj/analysis.py,sha256=WcFNJVd9zpwvDrbe41VshXqP9MFfptlgYD4Nph5mHLA,23675
|
|
6
|
+
kinemotion/cmj/api.py,sha256=5PDV_vX3k63Ko4OEttyUkV4fWyklyy-CG_UyNKgNoyY,18476
|
|
7
|
+
kinemotion/cmj/cli.py,sha256=P2b77IIw6kqTSIkncxlShzhmjIwqMFBNd-pZxYP-TsI,9918
|
|
8
|
+
kinemotion/cmj/debug_overlay.py,sha256=bX9aPLhXiLCCMZW9v8Y4OiOAaZO0i-UGr-Pl8HCsmbI,15810
|
|
9
|
+
kinemotion/cmj/joint_angles.py,sha256=HmheIEiKcQz39cRezk4h-htorOhGNPsqKIR9RsAEKts,9960
|
|
10
|
+
kinemotion/cmj/kinematics.py,sha256=KwA8uSj3g1SeNf0NXMSHsp3gIw6Gfa-6QWIwdYdRXYw,13362
|
|
11
|
+
kinemotion/cmj/metrics_validator.py,sha256=JWuWFfDXyZMTHXFWVdI0MhaQjMR3cjd01tTrDy_if2U,30290
|
|
12
|
+
kinemotion/cmj/validation_bounds.py,sha256=Ry915JdInPXbqjaVGNY_urnDO1PAkCSJqHwNKRq-VkU,12048
|
|
13
|
+
kinemotion/core/__init__.py,sha256=8WB7tAJPKOxgNzbhIEOnGnkRr0CcdNeTnz91Jsiyafo,1812
|
|
14
|
+
kinemotion/core/auto_tuning.py,sha256=lhAqPc-eLjMYx9BCvKdECE7TD2Dweb9KcifV6JHaXOE,11278
|
|
15
|
+
kinemotion/core/cli_utils.py,sha256=sQPbT6XWWau-sm9yuN5c3eS5xNzoQGGXwSz6hQXtRvM,1859
|
|
16
|
+
kinemotion/core/debug_overlay_utils.py,sha256=D4aT8xstThPcV2i5D4KJZJEttW6E_4GE5QiERqe1MwI,13049
|
|
17
|
+
kinemotion/core/determinism.py,sha256=Frw-KAOvAxTL_XtxoWpXCjMbQPUKEAusK6JctlkeuRo,2509
|
|
18
|
+
kinemotion/core/experimental.py,sha256=IK05AF4aZS15ke85hF3TWCqRIXU1AlD_XKzFz735Ua8,3640
|
|
19
|
+
kinemotion/core/filtering.py,sha256=Oc__pV6iHEGyyovbqa5SUi-6v8QyvaRVwA0LRayM884,11355
|
|
20
|
+
kinemotion/core/formatting.py,sha256=G_3eqgOtym9RFOZVEwCxye4A2cyrmgvtQ214vIshowU,2480
|
|
21
|
+
kinemotion/core/metadata.py,sha256=bJAVa4nym__zx1hNowSZduMGKBSGOPxTbBQkjm6N0D0,7207
|
|
22
|
+
kinemotion/core/model_downloader.py,sha256=mqhJBHGaNe0aN9qbcBqvcTk9FDd7xaHqEcwD-fyP89c,5205
|
|
23
|
+
kinemotion/core/pipeline_utils.py,sha256=B5jMXoiLaTh02uGA2MIe1uZLVSRGZ5nxbARuvdrjDrQ,15161
|
|
24
|
+
kinemotion/core/pose.py,sha256=_qC4cbFD0Mp2JAGftZcY5AEDLgD2yRnTyRKD9bkqLI8,15306
|
|
25
|
+
kinemotion/core/pose_landmarks.py,sha256=LcEbL5K5xKia6dCzWf6Ft18UIE1CLMMqCZ3KUjwUDzM,1558
|
|
26
|
+
kinemotion/core/quality.py,sha256=VUkRL2N6B7lfIZ2pE9han_U68JwarmZz1U0ygHkgkhE,13022
|
|
27
|
+
kinemotion/core/smoothing.py,sha256=ELMHL7pzSqYffjnLDBUMBJIgt1AwOssDInE8IiXBbig,15942
|
|
28
|
+
kinemotion/core/timing.py,sha256=ITX77q4hbtajRuWfgwYhws8nCvOeKFlEdKjCu8lD9_w,7938
|
|
29
|
+
kinemotion/core/types.py,sha256=A_HclzKpf3By5DiJ0wY9B-dQJrIVAAhUfGab7qTSIL8,1279
|
|
30
|
+
kinemotion/core/validation.py,sha256=0xVv-ftWveV60fJ97kmZMuy2Qqqb5aZLR50dDIrjnhg,6773
|
|
31
|
+
kinemotion/core/video_io.py,sha256=TxdLUEpekGytesL3X3k78WWgZTOd5fuge30hU4Uy48Y,9198
|
|
32
|
+
kinemotion/dropjump/__init__.py,sha256=tC3H3BrCg8Oj-db-Vrtx4PH_llR1Ppkd5jwaOjhQcLg,862
|
|
33
|
+
kinemotion/dropjump/analysis.py,sha256=YomuoJF_peyrBSpeT89Q5_sBgY0kEDyq7TFrtEnRLjs,28049
|
|
34
|
+
kinemotion/dropjump/api.py,sha256=QlZxCrjOg78PXXip6Krb91RSYqH37x1AbBUy6U8uFt8,20833
|
|
35
|
+
kinemotion/dropjump/cli.py,sha256=gUef9nmyR5952h1WnfBGyCdFXQvzVTlCKYAjJGcO4sE,16819
|
|
36
|
+
kinemotion/dropjump/debug_overlay.py,sha256=9RQYXPRf0q2wdy6y2Ak2R4tpRceDwC8aJrXZzkmh3Wo,5942
|
|
37
|
+
kinemotion/dropjump/kinematics.py,sha256=dx4PuXKfKMKcsc_HX6sXj8rHXf9ksiZIOAIkJ4vBlY4,19637
|
|
38
|
+
kinemotion/dropjump/metrics_validator.py,sha256=lSfo4Lm5FHccl8ijUP6SA-kcSh50LS9hF8UIyWxcnW8,9243
|
|
39
|
+
kinemotion/dropjump/validation_bounds.py,sha256=x4yjcFxyvdMp5e7MkcoUosGLeGsxBh1Lft6h__AQ2G8,5124
|
|
40
|
+
kinemotion/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
41
|
+
kinemotion/models/pose_landmarker_lite.task,sha256=WZKeHR7pUodzXd2DOxnPSsRtKbx6_du_Z1PEWWkNV0o,5777746
|
|
42
|
+
kinemotion/models/rtmpose-s_simcc-body7_pt-body7-halpe26_700e-256x192-7f134165_20230605.onnx,sha256=dfZTq8kbhv8RxWiXS0HUIJNCUpxYTBN45dFIorPflEs,133
|
|
43
|
+
kinemotion/models/yolox_tiny_8xb8-300e_humanart-6f3252f9.onnx,sha256=UsutHVQ6GP3X5pCcp52EN8q7o2J3d-TnxZqlF48kY6I,133
|
|
44
|
+
kinemotion/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
45
|
+
kinemotion-2.0.0.dist-info/METADATA,sha256=D782eqHaZnFWgRVE-N3FOGCNlqjSv0apIAP0ghVYnWQ,26124
|
|
46
|
+
kinemotion-2.0.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
47
|
+
kinemotion-2.0.0.dist-info/entry_points.txt,sha256=zaqnAnjLvcdrk1Qvj5nvXZCZ2gp0prS7it1zTJygcIY,50
|
|
48
|
+
kinemotion-2.0.0.dist-info/licenses/LICENSE,sha256=KZajvqsHw0NoOHOi2q0FZ4NBe9HdV6oey-IPYAtHXfg,1088
|
|
49
|
+
kinemotion-2.0.0.dist-info/RECORD,,
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
"""Shared constants and type aliases for overlay renderers."""
|
|
2
|
-
|
|
3
|
-
from typing import Any
|
|
4
|
-
|
|
5
|
-
# Type aliases for overlay rendering
|
|
6
|
-
Color = tuple[int, int, int]
|
|
7
|
-
Landmark = tuple[float, float, float]
|
|
8
|
-
LandmarkDict = dict[str, Landmark]
|
|
9
|
-
CodecAttemptLog = list[dict[str, Any]]
|
|
10
|
-
|
|
11
|
-
# Visibility thresholds
|
|
12
|
-
VISIBILITY_THRESHOLD = 0.2
|
|
13
|
-
VISIBILITY_THRESHOLD_HIGH = 0.3
|
|
14
|
-
FOOT_VISIBILITY_THRESHOLD = 0.5
|
|
15
|
-
|
|
16
|
-
# Video encoding constants
|
|
17
|
-
MAX_VIDEO_DIMENSION = 720
|
|
18
|
-
CODECS_TO_TRY = ["avc1", "mp4v"]
|
|
19
|
-
FFMPEG_PRESET = "fast"
|
|
20
|
-
FFMPEG_CRF = "23"
|
|
21
|
-
FFMPEG_PIX_FMT = "yuv420p"
|
|
22
|
-
|
|
23
|
-
# Common colors (BGR format for OpenCV)
|
|
24
|
-
GREEN = (0, 255, 0)
|
|
25
|
-
RED = (0, 0, 255)
|
|
26
|
-
WHITE = (255, 255, 255)
|
|
27
|
-
BLACK = (0, 0, 0)
|
|
28
|
-
GRAY = (128, 128, 128)
|
|
29
|
-
CYAN = (255, 255, 0)
|
|
30
|
-
ORANGE = (0, 165, 255)
|
|
31
|
-
|
|
32
|
-
# Joint colors for triple extension
|
|
33
|
-
ANKLE_COLOR = (0, 255, 255) # Cyan
|
|
34
|
-
KNEE_COLOR = (255, 100, 100) # Light blue
|
|
35
|
-
HIP_COLOR = (100, 255, 100) # Light green
|
|
36
|
-
TRUNK_COLOR = (100, 100, 255) # Light red
|
|
37
|
-
|
|
38
|
-
# Angle thresholds
|
|
39
|
-
FULL_EXTENSION_ANGLE = 160
|
|
40
|
-
DEEP_FLEXION_ANGLE = 90
|
|
41
|
-
|
|
42
|
-
# Circle sizes
|
|
43
|
-
JOINT_CIRCLE_RADIUS = 6
|
|
44
|
-
JOINT_OUTLINE_RADIUS = 8
|
|
45
|
-
COM_CIRCLE_RADIUS = 15
|
|
46
|
-
COM_OUTLINE_RADIUS = 17
|
|
47
|
-
HIP_MARKER_RADIUS = 8
|
|
48
|
-
FOOT_CIRCLE_RADIUS = 10
|
|
49
|
-
FOOT_LANDMARK_RADIUS = 5
|
|
50
|
-
ANGLE_ARC_RADIUS = 25
|
|
51
|
-
NOSE_CIRCLE_RADIUS = 8
|
|
52
|
-
NOSE_OUTLINE_RADIUS = 10
|
|
53
|
-
|
|
54
|
-
# Box positioning
|
|
55
|
-
JOINT_ANGLES_BOX_X_OFFSET = 180
|
|
56
|
-
JOINT_ANGLES_BOX_HEIGHT = 150
|
|
57
|
-
METRICS_BOX_WIDTH = 320
|
|
58
|
-
|
|
59
|
-
# Phase label positioning
|
|
60
|
-
PHASE_LABEL_START_Y = 110
|
|
61
|
-
PHASE_LABEL_LINE_HEIGHT = 40
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
"""Base types and patterns for video analysis APIs.
|
|
2
|
-
|
|
3
|
-
This module defines shared infrastructure for jump-type-specific analysis modules.
|
|
4
|
-
Each jump type (CMJ, Drop Jump, etc.) has its own analysis algorithms, but they
|
|
5
|
-
share common patterns for:
|
|
6
|
-
|
|
7
|
-
1. Configuration (VideoConfig dataclass)
|
|
8
|
-
2. Results (VideoResult dataclass)
|
|
9
|
-
3. Parameter overrides (AnalysisOverrides dataclass)
|
|
10
|
-
4. Bulk processing utilities
|
|
11
|
-
|
|
12
|
-
To add a new jump type:
|
|
13
|
-
1. Create a new module: src/kinemotion/{jump_type}/
|
|
14
|
-
2. Implement analysis algorithms in {jump_type}/analysis.py
|
|
15
|
-
3. Use the patterns in this module for API structure
|
|
16
|
-
4. Import process_videos_bulk_generic from pipeline_utils for bulk processing
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
from abc import ABC, abstractmethod
|
|
20
|
-
from dataclasses import dataclass
|
|
21
|
-
from pathlib import Path
|
|
22
|
-
from typing import TYPE_CHECKING
|
|
23
|
-
|
|
24
|
-
if TYPE_CHECKING:
|
|
25
|
-
from ..auto_tuning import QualityPreset
|
|
26
|
-
from ..timing import Timer
|
|
27
|
-
|
|
28
|
-
__all__ = [
|
|
29
|
-
"AnalysisOverrides",
|
|
30
|
-
"VideoAnalysisConfig",
|
|
31
|
-
"VideoAnalysisResult",
|
|
32
|
-
"JumpAnalysisPipeline",
|
|
33
|
-
]
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
@dataclass
|
|
37
|
-
class AnalysisOverrides:
|
|
38
|
-
"""Optional overrides for analysis parameters.
|
|
39
|
-
|
|
40
|
-
Allows fine-tuning of specific analysis parameters beyond quality presets.
|
|
41
|
-
If None, values will be determined by the quality preset.
|
|
42
|
-
|
|
43
|
-
Common overrides across all jump types:
|
|
44
|
-
- smoothing_window: Number of frames for Savitzky-Golay smoothing
|
|
45
|
-
- velocity_threshold: Threshold for phase detection
|
|
46
|
-
- min_contact_frames: Minimum frames for ground contact
|
|
47
|
-
- visibility_threshold: Minimum landmark visibility (0-1)
|
|
48
|
-
"""
|
|
49
|
-
|
|
50
|
-
smoothing_window: int | None = None
|
|
51
|
-
velocity_threshold: float | None = None
|
|
52
|
-
min_contact_frames: int | None = None
|
|
53
|
-
visibility_threshold: float | None = None
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
@dataclass
|
|
57
|
-
class VideoAnalysisConfig:
|
|
58
|
-
"""Base configuration for video analysis.
|
|
59
|
-
|
|
60
|
-
Subclasses should add jump-type-specific fields (e.g., drop_start_frame
|
|
61
|
-
for Drop Jump, or additional overrides for CMJ).
|
|
62
|
-
"""
|
|
63
|
-
|
|
64
|
-
video_path: str
|
|
65
|
-
quality: str = "balanced"
|
|
66
|
-
output_video: str | None = None
|
|
67
|
-
json_output: str | None = None
|
|
68
|
-
overrides: AnalysisOverrides | None = None
|
|
69
|
-
detection_confidence: float | None = None
|
|
70
|
-
tracking_confidence: float | None = None
|
|
71
|
-
verbose: bool = False
|
|
72
|
-
timer: "Timer | None" = None
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
@dataclass
|
|
76
|
-
class VideoAnalysisResult:
|
|
77
|
-
"""Base result for video analysis.
|
|
78
|
-
|
|
79
|
-
Subclasses should add jump-type-specific fields.
|
|
80
|
-
"""
|
|
81
|
-
|
|
82
|
-
video_path: str
|
|
83
|
-
success: bool
|
|
84
|
-
metrics: object | None = None # Will be CMJMetrics, DropJumpMetrics, etc.
|
|
85
|
-
error: str | None = None
|
|
86
|
-
processing_time: float = 0.0
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
class JumpAnalysisPipeline(ABC):
|
|
90
|
-
"""Abstract base class for jump analysis pipelines.
|
|
91
|
-
|
|
92
|
-
Defines the common structure for processing jump videos. Each jump type
|
|
93
|
-
implements the specific analysis logic while following this pattern.
|
|
94
|
-
|
|
95
|
-
Example:
|
|
96
|
-
class CMJPipeline(JumpAnalysisPipeline):
|
|
97
|
-
def analyze(self) -> CMJMetrics:
|
|
98
|
-
# CMJ-specific analysis (backward search algorithm)
|
|
99
|
-
...
|
|
100
|
-
|
|
101
|
-
class DropJumpPipeline(JumpAnalysisPipeline):
|
|
102
|
-
def analyze(self) -> DropJumpMetrics:
|
|
103
|
-
# Drop jump-specific analysis (forward search algorithm)
|
|
104
|
-
...
|
|
105
|
-
"""
|
|
106
|
-
|
|
107
|
-
def __init__(
|
|
108
|
-
self,
|
|
109
|
-
video_path: str,
|
|
110
|
-
quality_preset: "QualityPreset",
|
|
111
|
-
overrides: AnalysisOverrides | None,
|
|
112
|
-
timer: "Timer",
|
|
113
|
-
) -> None:
|
|
114
|
-
"""Initialize the analysis pipeline."""
|
|
115
|
-
self.video_path = video_path
|
|
116
|
-
self.quality_preset = quality_preset
|
|
117
|
-
self.overrides = overrides
|
|
118
|
-
self.timer = timer
|
|
119
|
-
|
|
120
|
-
@abstractmethod
|
|
121
|
-
def analyze(self) -> object:
|
|
122
|
-
"""Run the jump-specific analysis algorithm.
|
|
123
|
-
|
|
124
|
-
Returns:
|
|
125
|
-
Metrics object with jump-type-specific results.
|
|
126
|
-
"""
|
|
127
|
-
...
|
|
128
|
-
|
|
129
|
-
def validate_video_exists(self) -> None:
|
|
130
|
-
"""Validate that the input video file exists."""
|
|
131
|
-
if not Path(self.video_path).exists():
|
|
132
|
-
raise FileNotFoundError(f"Video file not found: {self.video_path}")
|
|
@@ -1,325 +0,0 @@
|
|
|
1
|
-
"""Debug overlay rendering for CMJ analysis."""
|
|
2
|
-
|
|
3
|
-
import cv2
|
|
4
|
-
import numpy as np
|
|
5
|
-
|
|
6
|
-
from ..core.debug_overlay_utils import BaseDebugOverlayRenderer
|
|
7
|
-
from ..core.overlay_constants import (
|
|
8
|
-
ANGLE_ARC_RADIUS,
|
|
9
|
-
ANKLE_COLOR,
|
|
10
|
-
BLACK,
|
|
11
|
-
CYAN,
|
|
12
|
-
DEEP_FLEXION_ANGLE,
|
|
13
|
-
FOOT_LANDMARK_RADIUS,
|
|
14
|
-
FOOT_VISIBILITY_THRESHOLD,
|
|
15
|
-
FULL_EXTENSION_ANGLE,
|
|
16
|
-
GRAY,
|
|
17
|
-
GREEN,
|
|
18
|
-
HIP_COLOR,
|
|
19
|
-
JOINT_ANGLES_BOX_HEIGHT,
|
|
20
|
-
JOINT_ANGLES_BOX_X_OFFSET,
|
|
21
|
-
KNEE_COLOR,
|
|
22
|
-
METRICS_BOX_WIDTH,
|
|
23
|
-
ORANGE,
|
|
24
|
-
RED,
|
|
25
|
-
TRUNK_COLOR,
|
|
26
|
-
VISIBILITY_THRESHOLD_HIGH,
|
|
27
|
-
WHITE,
|
|
28
|
-
Color,
|
|
29
|
-
LandmarkDict,
|
|
30
|
-
)
|
|
31
|
-
from .analysis import CMJPhase
|
|
32
|
-
from .joint_angles import calculate_triple_extension
|
|
33
|
-
from .kinematics import CMJMetrics
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class CMJDebugOverlayRenderer(BaseDebugOverlayRenderer):
|
|
37
|
-
"""Renders debug information on CMJ video frames."""
|
|
38
|
-
|
|
39
|
-
# Phase colors (BGR format)
|
|
40
|
-
PHASE_COLORS: dict[CMJPhase, Color] = {
|
|
41
|
-
CMJPhase.STANDING: (255, 200, 100), # Light blue
|
|
42
|
-
CMJPhase.ECCENTRIC: (0, 165, 255), # Orange
|
|
43
|
-
CMJPhase.TRANSITION: (255, 0, 255), # Magenta/Purple
|
|
44
|
-
CMJPhase.CONCENTRIC: (0, 255, 0), # Green
|
|
45
|
-
CMJPhase.FLIGHT: (0, 0, 255), # Red
|
|
46
|
-
CMJPhase.LANDING: (255, 255, 255), # White
|
|
47
|
-
}
|
|
48
|
-
DEFAULT_PHASE_COLOR: Color = GRAY
|
|
49
|
-
|
|
50
|
-
def _determine_phase(self, frame_idx: int, metrics: CMJMetrics) -> CMJPhase:
|
|
51
|
-
"""Determine which phase the current frame is in."""
|
|
52
|
-
if metrics.standing_start_frame and frame_idx < metrics.standing_start_frame:
|
|
53
|
-
return CMJPhase.STANDING
|
|
54
|
-
|
|
55
|
-
if frame_idx < metrics.lowest_point_frame:
|
|
56
|
-
return CMJPhase.ECCENTRIC
|
|
57
|
-
|
|
58
|
-
# Brief transition at lowest point (within 2 frames)
|
|
59
|
-
if abs(frame_idx - metrics.lowest_point_frame) < 2:
|
|
60
|
-
return CMJPhase.TRANSITION
|
|
61
|
-
|
|
62
|
-
if frame_idx < metrics.takeoff_frame:
|
|
63
|
-
return CMJPhase.CONCENTRIC
|
|
64
|
-
|
|
65
|
-
if frame_idx < metrics.landing_frame:
|
|
66
|
-
return CMJPhase.FLIGHT
|
|
67
|
-
|
|
68
|
-
return CMJPhase.LANDING
|
|
69
|
-
|
|
70
|
-
def _get_phase_color(self, phase: CMJPhase) -> Color:
|
|
71
|
-
"""Get color for each phase."""
|
|
72
|
-
return self.PHASE_COLORS.get(phase, self.DEFAULT_PHASE_COLOR)
|
|
73
|
-
|
|
74
|
-
def _get_triple_extension_angles(
|
|
75
|
-
self, landmarks: LandmarkDict
|
|
76
|
-
) -> tuple[dict[str, float | None], str] | None:
|
|
77
|
-
"""Get triple extension angles, trying right side first then left.
|
|
78
|
-
|
|
79
|
-
Returns tuple of (angles_dict, side_used) or None if unavailable.
|
|
80
|
-
"""
|
|
81
|
-
for side in ["right", "left"]:
|
|
82
|
-
angles = calculate_triple_extension(landmarks, side=side)
|
|
83
|
-
if angles is not None:
|
|
84
|
-
return angles, side
|
|
85
|
-
return None
|
|
86
|
-
|
|
87
|
-
def _draw_info_box(
|
|
88
|
-
self,
|
|
89
|
-
frame: np.ndarray,
|
|
90
|
-
top_left: tuple[int, int],
|
|
91
|
-
bottom_right: tuple[int, int],
|
|
92
|
-
border_color: Color,
|
|
93
|
-
) -> None:
|
|
94
|
-
"""Draw a filled box with border for displaying information."""
|
|
95
|
-
cv2.rectangle(frame, top_left, bottom_right, BLACK, -1)
|
|
96
|
-
cv2.rectangle(frame, top_left, bottom_right, border_color, 2)
|
|
97
|
-
|
|
98
|
-
def _draw_joint_angles(
|
|
99
|
-
self, frame: np.ndarray, landmarks: LandmarkDict, phase_color: Color
|
|
100
|
-
) -> None:
|
|
101
|
-
"""Draw joint angles for triple extension analysis.
|
|
102
|
-
|
|
103
|
-
Args:
|
|
104
|
-
frame: Frame to draw on (modified in place)
|
|
105
|
-
landmarks: Pose landmarks
|
|
106
|
-
phase_color: Current phase color
|
|
107
|
-
"""
|
|
108
|
-
result = self._get_triple_extension_angles(landmarks)
|
|
109
|
-
if result is None:
|
|
110
|
-
return
|
|
111
|
-
|
|
112
|
-
angles, side_used = result
|
|
113
|
-
|
|
114
|
-
# Position for angle text display (right side of frame)
|
|
115
|
-
text_x = self.width - JOINT_ANGLES_BOX_X_OFFSET
|
|
116
|
-
text_y = 100
|
|
117
|
-
box_height = JOINT_ANGLES_BOX_HEIGHT
|
|
118
|
-
|
|
119
|
-
# Draw background box
|
|
120
|
-
self._draw_info_box(
|
|
121
|
-
frame,
|
|
122
|
-
(text_x - 10, text_y - 30),
|
|
123
|
-
(self.width - 10, text_y + box_height),
|
|
124
|
-
phase_color,
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
# Title
|
|
128
|
-
cv2.putText(
|
|
129
|
-
frame,
|
|
130
|
-
"TRIPLE EXTENSION",
|
|
131
|
-
(text_x, text_y - 5),
|
|
132
|
-
cv2.FONT_HERSHEY_SIMPLEX,
|
|
133
|
-
0.5,
|
|
134
|
-
WHITE,
|
|
135
|
-
1,
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
# Angle display configuration: (label, angle_key, color, joint_suffix)
|
|
139
|
-
angle_config = [
|
|
140
|
-
("Ankle", "ankle_angle", ANKLE_COLOR, "ankle"),
|
|
141
|
-
("Knee", "knee_angle", KNEE_COLOR, "knee"),
|
|
142
|
-
("Hip", "hip_angle", HIP_COLOR, "hip"),
|
|
143
|
-
("Trunk", "trunk_tilt", TRUNK_COLOR, None),
|
|
144
|
-
]
|
|
145
|
-
|
|
146
|
-
y_offset = text_y + 25
|
|
147
|
-
for label, angle_key, color, joint_suffix in angle_config:
|
|
148
|
-
angle = angles.get(angle_key)
|
|
149
|
-
|
|
150
|
-
# Draw text
|
|
151
|
-
if angle is not None:
|
|
152
|
-
text = f"{label}: {angle:.0f}"
|
|
153
|
-
text_color = color
|
|
154
|
-
else:
|
|
155
|
-
text = f"{label}: N/A"
|
|
156
|
-
text_color = GRAY
|
|
157
|
-
|
|
158
|
-
cv2.putText(
|
|
159
|
-
frame, text, (text_x, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.5, text_color, 2
|
|
160
|
-
)
|
|
161
|
-
y_offset += 30
|
|
162
|
-
|
|
163
|
-
# Draw arc at joint if angle available and has associated joint
|
|
164
|
-
if angle is not None and joint_suffix is not None:
|
|
165
|
-
self._draw_angle_arc(frame, landmarks, f"{side_used}_{joint_suffix}", angle)
|
|
166
|
-
|
|
167
|
-
def _get_extension_color(self, angle: float) -> Color:
|
|
168
|
-
"""Get color based on joint extension angle.
|
|
169
|
-
|
|
170
|
-
Green for extended (>160 deg), red for flexed (<90 deg), orange for moderate.
|
|
171
|
-
"""
|
|
172
|
-
if angle > FULL_EXTENSION_ANGLE:
|
|
173
|
-
return GREEN
|
|
174
|
-
if angle < DEEP_FLEXION_ANGLE:
|
|
175
|
-
return RED
|
|
176
|
-
return ORANGE
|
|
177
|
-
|
|
178
|
-
def _draw_angle_arc(
|
|
179
|
-
self, frame: np.ndarray, landmarks: LandmarkDict, joint_key: str, angle: float
|
|
180
|
-
) -> None:
|
|
181
|
-
"""Draw a circle at a joint to visualize the angle.
|
|
182
|
-
|
|
183
|
-
Args:
|
|
184
|
-
frame: Frame to draw on (modified in place)
|
|
185
|
-
landmarks: Pose landmarks
|
|
186
|
-
joint_key: Key of the joint landmark
|
|
187
|
-
angle: Angle value in degrees
|
|
188
|
-
"""
|
|
189
|
-
if joint_key not in landmarks:
|
|
190
|
-
return
|
|
191
|
-
landmark = landmarks[joint_key]
|
|
192
|
-
if not self._is_visible(landmark, VISIBILITY_THRESHOLD_HIGH):
|
|
193
|
-
return
|
|
194
|
-
|
|
195
|
-
point = self._landmark_to_pixel(landmark)
|
|
196
|
-
arc_color = self._get_extension_color(angle)
|
|
197
|
-
cv2.circle(frame, point, ANGLE_ARC_RADIUS, arc_color, 2)
|
|
198
|
-
|
|
199
|
-
def _draw_foot_landmarks(
|
|
200
|
-
self, frame: np.ndarray, landmarks: LandmarkDict, phase_color: Color
|
|
201
|
-
) -> None:
|
|
202
|
-
"""Draw foot landmarks and average position."""
|
|
203
|
-
foot_keys = ["left_ankle", "right_ankle", "left_heel", "right_heel"]
|
|
204
|
-
foot_positions: list[tuple[int, int]] = []
|
|
205
|
-
|
|
206
|
-
for key in foot_keys:
|
|
207
|
-
if key not in landmarks:
|
|
208
|
-
continue
|
|
209
|
-
landmark = landmarks[key]
|
|
210
|
-
if landmark[2] > FOOT_VISIBILITY_THRESHOLD:
|
|
211
|
-
point = self._landmark_to_pixel(landmark)
|
|
212
|
-
foot_positions.append(point)
|
|
213
|
-
cv2.circle(frame, point, FOOT_LANDMARK_RADIUS, CYAN, -1)
|
|
214
|
-
|
|
215
|
-
# Draw average foot position with phase color
|
|
216
|
-
if foot_positions:
|
|
217
|
-
avg_x = int(np.mean([p[0] for p in foot_positions]))
|
|
218
|
-
avg_y = int(np.mean([p[1] for p in foot_positions]))
|
|
219
|
-
cv2.circle(frame, (avg_x, avg_y), 12, phase_color, -1)
|
|
220
|
-
cv2.circle(frame, (avg_x, avg_y), 14, WHITE, 2)
|
|
221
|
-
|
|
222
|
-
def _draw_phase_banner(
|
|
223
|
-
self, frame: np.ndarray, phase: CMJPhase | None, phase_color: Color
|
|
224
|
-
) -> None:
|
|
225
|
-
"""Draw phase indicator banner."""
|
|
226
|
-
if phase is None:
|
|
227
|
-
return
|
|
228
|
-
|
|
229
|
-
phase_text = f"Phase: {phase.value.upper()}"
|
|
230
|
-
text_size = cv2.getTextSize(phase_text, cv2.FONT_HERSHEY_SIMPLEX, 1, 2)[0]
|
|
231
|
-
cv2.rectangle(frame, (5, 5), (text_size[0] + 15, 45), phase_color, -1)
|
|
232
|
-
cv2.putText(frame, phase_text, (10, 35), cv2.FONT_HERSHEY_SIMPLEX, 1, BLACK, 2)
|
|
233
|
-
|
|
234
|
-
def _draw_key_frame_markers(
|
|
235
|
-
self, frame: np.ndarray, frame_idx: int, metrics: CMJMetrics
|
|
236
|
-
) -> None:
|
|
237
|
-
"""Draw markers for key frames (standing start, lowest, takeoff, landing)."""
|
|
238
|
-
# Key frame definitions: (frame_value, label)
|
|
239
|
-
key_frames: list[tuple[float | None, str]] = [
|
|
240
|
-
(metrics.standing_start_frame, "COUNTERMOVEMENT START"),
|
|
241
|
-
(metrics.lowest_point_frame, "LOWEST POINT"),
|
|
242
|
-
(metrics.takeoff_frame, "TAKEOFF"),
|
|
243
|
-
(metrics.landing_frame, "LANDING"),
|
|
244
|
-
]
|
|
245
|
-
|
|
246
|
-
y_offset = 120
|
|
247
|
-
for key_frame, label in key_frames:
|
|
248
|
-
if key_frame is not None and frame_idx == int(key_frame):
|
|
249
|
-
cv2.putText(frame, label, (10, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.7, CYAN, 2)
|
|
250
|
-
y_offset += 35
|
|
251
|
-
|
|
252
|
-
def _draw_metrics_summary(
|
|
253
|
-
self, frame: np.ndarray, frame_idx: int, metrics: CMJMetrics
|
|
254
|
-
) -> None:
|
|
255
|
-
"""Draw metrics summary in bottom right (last 30 frames after landing)."""
|
|
256
|
-
if frame_idx < int(metrics.landing_frame):
|
|
257
|
-
return
|
|
258
|
-
|
|
259
|
-
metrics_text = [
|
|
260
|
-
f"Jump Height: {metrics.jump_height:.3f}m",
|
|
261
|
-
f"Flight Time: {metrics.flight_time * 1000:.0f}ms",
|
|
262
|
-
f"CM Depth: {metrics.countermovement_depth:.3f}m",
|
|
263
|
-
f"Ecc Duration: {metrics.eccentric_duration * 1000:.0f}ms",
|
|
264
|
-
f"Con Duration: {metrics.concentric_duration * 1000:.0f}ms",
|
|
265
|
-
]
|
|
266
|
-
|
|
267
|
-
# Calculate box dimensions
|
|
268
|
-
box_height = len(metrics_text) * 30 + 20
|
|
269
|
-
top_left = (self.width - METRICS_BOX_WIDTH, self.height - box_height - 10)
|
|
270
|
-
bottom_right = (self.width - 10, self.height - 10)
|
|
271
|
-
|
|
272
|
-
self._draw_info_box(frame, top_left, bottom_right, GREEN)
|
|
273
|
-
|
|
274
|
-
# Draw metrics text
|
|
275
|
-
text_x = self.width - METRICS_BOX_WIDTH + 10
|
|
276
|
-
text_y = self.height - box_height + 10
|
|
277
|
-
for text in metrics_text:
|
|
278
|
-
cv2.putText(frame, text, (text_x, text_y), cv2.FONT_HERSHEY_SIMPLEX, 0.6, WHITE, 1)
|
|
279
|
-
text_y += 30
|
|
280
|
-
|
|
281
|
-
def render_frame(
|
|
282
|
-
self,
|
|
283
|
-
frame: np.ndarray,
|
|
284
|
-
landmarks: LandmarkDict | None,
|
|
285
|
-
frame_idx: int,
|
|
286
|
-
metrics: CMJMetrics | None = None,
|
|
287
|
-
) -> np.ndarray:
|
|
288
|
-
"""Render debug overlay on frame.
|
|
289
|
-
|
|
290
|
-
Args:
|
|
291
|
-
frame: Original video frame
|
|
292
|
-
landmarks: Pose landmarks for this frame
|
|
293
|
-
frame_idx: Current frame index
|
|
294
|
-
metrics: CMJ metrics (optional)
|
|
295
|
-
|
|
296
|
-
Returns:
|
|
297
|
-
Frame with debug overlay
|
|
298
|
-
"""
|
|
299
|
-
annotated = frame.copy()
|
|
300
|
-
|
|
301
|
-
# Determine current phase and color
|
|
302
|
-
phase: CMJPhase | None = None
|
|
303
|
-
phase_color: Color = WHITE
|
|
304
|
-
if metrics:
|
|
305
|
-
phase = self._determine_phase(frame_idx, metrics)
|
|
306
|
-
phase_color = self._get_phase_color(phase)
|
|
307
|
-
|
|
308
|
-
# Draw skeleton and joint visualization if landmarks available
|
|
309
|
-
if landmarks:
|
|
310
|
-
self._draw_skeleton(annotated, landmarks)
|
|
311
|
-
self._draw_joint_angles(annotated, landmarks, phase_color)
|
|
312
|
-
self._draw_foot_landmarks(annotated, landmarks, phase_color)
|
|
313
|
-
|
|
314
|
-
# Draw phase indicator and frame number
|
|
315
|
-
self._draw_phase_banner(annotated, phase, phase_color)
|
|
316
|
-
cv2.putText(
|
|
317
|
-
annotated, f"Frame: {frame_idx}", (10, 80), cv2.FONT_HERSHEY_SIMPLEX, 0.7, WHITE, 2
|
|
318
|
-
)
|
|
319
|
-
|
|
320
|
-
# Draw key frame markers and metrics summary
|
|
321
|
-
if metrics:
|
|
322
|
-
self._draw_key_frame_markers(annotated, frame_idx, metrics)
|
|
323
|
-
self._draw_metrics_summary(annotated, frame_idx, metrics)
|
|
324
|
-
|
|
325
|
-
return annotated
|