kinemotion 0.76.3__py3-none-any.whl → 1.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 +293 -184
- 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-1.0.0.dist-info}/METADATA +26 -75
- kinemotion-1.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-1.0.0.dist-info}/WHEEL +0 -0
- {kinemotion-0.76.3.dist-info → kinemotion-1.0.0.dist-info}/entry_points.txt +0 -0
- {kinemotion-0.76.3.dist-info → kinemotion-1.0.0.dist-info}/licenses/LICENSE +0 -0
kinemotion/__init__.py
CHANGED
|
@@ -1,26 +1,17 @@
|
|
|
1
|
-
"""Kinemotion: Video-based kinematic analysis for athletic performance.
|
|
2
|
-
|
|
3
|
-
Supports Counter Movement Jump (CMJ), Drop Jump, and Squat Jump (SJ) analysis
|
|
4
|
-
using MediaPipe pose estimation.
|
|
5
|
-
"""
|
|
1
|
+
"""Kinemotion: Video-based kinematic analysis for athletic performance."""
|
|
6
2
|
|
|
7
3
|
from .api import (
|
|
8
4
|
CMJVideoConfig,
|
|
9
5
|
CMJVideoResult,
|
|
10
6
|
DropJumpVideoConfig,
|
|
11
7
|
DropJumpVideoResult,
|
|
12
|
-
SJVideoConfig,
|
|
13
|
-
SJVideoResult,
|
|
14
8
|
process_cmj_video,
|
|
15
9
|
process_cmj_videos_bulk,
|
|
16
10
|
process_dropjump_video,
|
|
17
11
|
process_dropjump_videos_bulk,
|
|
18
|
-
process_sj_video,
|
|
19
|
-
process_sj_videos_bulk,
|
|
20
12
|
)
|
|
21
|
-
from .
|
|
22
|
-
from .
|
|
23
|
-
from .squat_jump.kinematics import SJMetrics
|
|
13
|
+
from .cmj.kinematics import CMJMetrics
|
|
14
|
+
from .dropjump.kinematics import DropJumpMetrics
|
|
24
15
|
|
|
25
16
|
# Get version from package metadata (set in pyproject.toml)
|
|
26
17
|
try:
|
|
@@ -44,11 +35,5 @@ __all__ = [
|
|
|
44
35
|
"CMJVideoConfig",
|
|
45
36
|
"CMJVideoResult",
|
|
46
37
|
"CMJMetrics",
|
|
47
|
-
# Squat Jump API
|
|
48
|
-
"process_sj_video",
|
|
49
|
-
"process_sj_videos_bulk",
|
|
50
|
-
"SJVideoConfig",
|
|
51
|
-
"SJVideoResult",
|
|
52
|
-
"SJMetrics",
|
|
53
38
|
"__version__",
|
|
54
39
|
]
|
kinemotion/api.py
CHANGED
|
@@ -1,27 +1,26 @@
|
|
|
1
1
|
"""Public API for programmatic use of kinemotion analysis.
|
|
2
2
|
|
|
3
|
-
This module provides a unified interface for drop jump
|
|
3
|
+
This module provides a unified interface for both drop jump and CMJ video analysis.
|
|
4
4
|
The actual implementations have been moved to their respective submodules:
|
|
5
|
-
- Drop jump: kinemotion.
|
|
6
|
-
- CMJ: kinemotion.
|
|
7
|
-
- Squat Jump: kinemotion.squat_jump.api
|
|
5
|
+
- Drop jump: kinemotion.dropjump.api
|
|
6
|
+
- CMJ: kinemotion.cmj.api
|
|
8
7
|
|
|
9
8
|
"""
|
|
10
9
|
|
|
11
10
|
# CMJ API
|
|
12
|
-
from .
|
|
11
|
+
from .cmj.api import (
|
|
13
12
|
AnalysisOverrides as CMJAnalysisOverrides,
|
|
14
13
|
)
|
|
15
|
-
from .
|
|
14
|
+
from .cmj.api import (
|
|
16
15
|
CMJVideoConfig,
|
|
17
16
|
CMJVideoResult,
|
|
18
17
|
process_cmj_video,
|
|
19
18
|
process_cmj_videos_bulk,
|
|
20
19
|
)
|
|
21
|
-
from .
|
|
20
|
+
from .cmj.kinematics import CMJMetrics
|
|
22
21
|
|
|
23
22
|
# Drop jump API
|
|
24
|
-
from .
|
|
23
|
+
from .dropjump.api import (
|
|
25
24
|
AnalysisOverrides,
|
|
26
25
|
DropJumpVideoConfig,
|
|
27
26
|
DropJumpVideoResult,
|
|
@@ -29,18 +28,6 @@ from .drop_jump.api import (
|
|
|
29
28
|
process_dropjump_videos_bulk,
|
|
30
29
|
)
|
|
31
30
|
|
|
32
|
-
# Squat Jump API
|
|
33
|
-
from .squat_jump.api import (
|
|
34
|
-
AnalysisOverrides as SJAnalysisOverrides,
|
|
35
|
-
)
|
|
36
|
-
from .squat_jump.api import (
|
|
37
|
-
SJVideoConfig,
|
|
38
|
-
SJVideoResult,
|
|
39
|
-
process_sj_video,
|
|
40
|
-
process_sj_videos_bulk,
|
|
41
|
-
)
|
|
42
|
-
from .squat_jump.kinematics import SJMetrics
|
|
43
|
-
|
|
44
31
|
__all__ = [
|
|
45
32
|
# Drop jump
|
|
46
33
|
"AnalysisOverrides",
|
|
@@ -55,11 +42,4 @@ __all__ = [
|
|
|
55
42
|
"CMJVideoResult",
|
|
56
43
|
"process_cmj_video",
|
|
57
44
|
"process_cmj_videos_bulk",
|
|
58
|
-
# Squat Jump
|
|
59
|
-
"SJAnalysisOverrides",
|
|
60
|
-
"SJMetrics",
|
|
61
|
-
"SJVideoConfig",
|
|
62
|
-
"SJVideoResult",
|
|
63
|
-
"process_sj_video",
|
|
64
|
-
"process_sj_videos_bulk",
|
|
65
45
|
]
|
kinemotion/cli.py
CHANGED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import click
|
|
4
4
|
|
|
5
|
-
from .
|
|
6
|
-
from .
|
|
7
|
-
from .squat_jump.cli import sj_analyze
|
|
5
|
+
from .cmj.cli import cmj_analyze
|
|
6
|
+
from .dropjump.cli import dropjump_analyze
|
|
8
7
|
|
|
9
8
|
|
|
10
9
|
@click.group()
|
|
@@ -18,7 +17,6 @@ def cli() -> None: # type: ignore[return]
|
|
|
18
17
|
# Type ignore needed because @click.group() transforms cli into a click.Group
|
|
19
18
|
cli.add_command(dropjump_analyze) # type: ignore[attr-defined]
|
|
20
19
|
cli.add_command(cmj_analyze) # type: ignore[attr-defined]
|
|
21
|
-
cli.add_command(sj_analyze) # type: ignore[attr-defined]
|
|
22
20
|
|
|
23
21
|
|
|
24
22
|
if __name__ == "__main__":
|
|
@@ -8,7 +8,7 @@ from scipy.signal import savgol_filter
|
|
|
8
8
|
from ..core.experimental import unused
|
|
9
9
|
from ..core.smoothing import compute_acceleration_from_derivative
|
|
10
10
|
from ..core.timing import NULL_TIMER, Timer
|
|
11
|
-
from ..core.types import
|
|
11
|
+
from ..core.types import FloatArray
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
def compute_signed_velocity(
|
|
@@ -55,7 +55,147 @@ class CMJPhase(Enum):
|
|
|
55
55
|
UNKNOWN = "unknown"
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
@unused(
|
|
59
|
+
reason="Alternative implementation not called by pipeline",
|
|
60
|
+
since="0.34.0",
|
|
61
|
+
)
|
|
62
|
+
def find_standing_phase(
|
|
63
|
+
positions: FloatArray,
|
|
64
|
+
velocities: FloatArray,
|
|
65
|
+
fps: float,
|
|
66
|
+
min_standing_duration: float = 0.5,
|
|
67
|
+
velocity_threshold: float = 0.01,
|
|
68
|
+
) -> int | None:
|
|
69
|
+
"""
|
|
70
|
+
Find the end of standing phase (start of countermovement).
|
|
71
|
+
|
|
72
|
+
Looks for a period of low velocity (standing) followed by consistent
|
|
73
|
+
downward motion.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
positions: Array of vertical positions (normalized 0-1)
|
|
77
|
+
velocities: Array of vertical velocities
|
|
78
|
+
fps: Video frame rate
|
|
79
|
+
min_standing_duration: Minimum standing duration in seconds (default: 0.5s)
|
|
80
|
+
velocity_threshold: Velocity threshold for standing detection
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Frame index where countermovement begins, or None if not detected.
|
|
84
|
+
"""
|
|
85
|
+
min_standing_frames = int(fps * min_standing_duration)
|
|
86
|
+
|
|
87
|
+
if len(positions) < min_standing_frames:
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
# Find periods of low velocity (standing)
|
|
91
|
+
is_standing = np.abs(velocities) < velocity_threshold
|
|
92
|
+
|
|
93
|
+
# Look for first sustained standing period
|
|
94
|
+
standing_count = 0
|
|
95
|
+
standing_end = None
|
|
96
|
+
|
|
97
|
+
for i in range(len(is_standing)):
|
|
98
|
+
if is_standing[i]:
|
|
99
|
+
standing_count += 1
|
|
100
|
+
if standing_count >= min_standing_frames:
|
|
101
|
+
standing_end = i
|
|
102
|
+
else:
|
|
103
|
+
if standing_end is not None:
|
|
104
|
+
# Found end of standing phase
|
|
105
|
+
return standing_end
|
|
106
|
+
standing_count = 0
|
|
107
|
+
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@unused(
|
|
112
|
+
reason="Alternative implementation not called by pipeline",
|
|
113
|
+
since="0.34.0",
|
|
114
|
+
)
|
|
115
|
+
def find_countermovement_start(
|
|
116
|
+
velocities: FloatArray,
|
|
117
|
+
countermovement_threshold: float = 0.015,
|
|
118
|
+
min_eccentric_frames: int = 3,
|
|
119
|
+
standing_start: int | None = None,
|
|
120
|
+
) -> int | None:
|
|
121
|
+
"""
|
|
122
|
+
Find the start of countermovement (eccentric phase).
|
|
123
|
+
|
|
124
|
+
Detects when velocity becomes consistently positive (downward motion in
|
|
125
|
+
normalized coords).
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
velocities: Array of SIGNED vertical velocities
|
|
129
|
+
countermovement_threshold: Velocity threshold for detecting downward
|
|
130
|
+
motion (POSITIVE)
|
|
131
|
+
min_eccentric_frames: Minimum consecutive frames of downward motion
|
|
132
|
+
standing_start: Optional frame where standing phase ended
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Frame index where countermovement begins, or None if not detected.
|
|
136
|
+
"""
|
|
137
|
+
start_frame = standing_start if standing_start is not None else 0
|
|
138
|
+
|
|
139
|
+
# Look for sustained downward velocity (POSITIVE in normalized coords)
|
|
140
|
+
is_downward = velocities[start_frame:] > countermovement_threshold
|
|
141
|
+
consecutive_count = 0
|
|
142
|
+
|
|
143
|
+
for i in range(len(is_downward)):
|
|
144
|
+
if is_downward[i]:
|
|
145
|
+
consecutive_count += 1
|
|
146
|
+
if consecutive_count >= min_eccentric_frames:
|
|
147
|
+
# Found start of eccentric phase
|
|
148
|
+
return start_frame + i - consecutive_count + 1
|
|
149
|
+
else:
|
|
150
|
+
consecutive_count = 0
|
|
151
|
+
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def find_lowest_point(
|
|
156
|
+
positions: FloatArray,
|
|
157
|
+
velocities: FloatArray,
|
|
158
|
+
min_search_frame: int = 80,
|
|
159
|
+
) -> int:
|
|
160
|
+
"""
|
|
161
|
+
Find the lowest point of countermovement (transition from eccentric to concentric).
|
|
162
|
+
|
|
163
|
+
The lowest point occurs BEFORE the peak height (the jump apex). It's where
|
|
164
|
+
velocity crosses from positive (downward/squatting) to negative (upward/jumping).
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
positions: Array of vertical positions (higher value = lower in video)
|
|
168
|
+
velocities: Array of SIGNED vertical velocities (positive=down, negative=up)
|
|
169
|
+
min_search_frame: Minimum frame to start searching (default: frame 80)
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Frame index of lowest point.
|
|
173
|
+
"""
|
|
174
|
+
# First, find the peak height (minimum y value = highest jump point)
|
|
175
|
+
peak_height_frame = int(np.argmin(positions))
|
|
176
|
+
|
|
177
|
+
# Lowest point MUST be before peak height
|
|
178
|
+
# Search from min_search_frame to peak_height_frame
|
|
179
|
+
start_frame = min_search_frame
|
|
180
|
+
end_frame = peak_height_frame
|
|
181
|
+
|
|
182
|
+
if end_frame <= start_frame:
|
|
183
|
+
start_frame = int(len(positions) * 0.3)
|
|
184
|
+
end_frame = int(len(positions) * 0.7)
|
|
185
|
+
|
|
186
|
+
search_positions = positions[start_frame:end_frame]
|
|
187
|
+
|
|
188
|
+
if len(search_positions) == 0:
|
|
189
|
+
return start_frame
|
|
190
|
+
|
|
191
|
+
# Find maximum position value in this range (lowest point in video)
|
|
192
|
+
lowest_idx = int(np.argmax(search_positions))
|
|
193
|
+
lowest_frame = start_frame + lowest_idx
|
|
194
|
+
|
|
195
|
+
return lowest_frame
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def find_cmj_takeoff_from_velocity_peak(
|
|
59
199
|
positions: FloatArray,
|
|
60
200
|
velocities: FloatArray,
|
|
61
201
|
lowest_point_frame: int,
|
|
@@ -92,7 +232,7 @@ def _find_cmj_takeoff_from_velocity_peak(
|
|
|
92
232
|
return float(takeoff_frame)
|
|
93
233
|
|
|
94
234
|
|
|
95
|
-
def
|
|
235
|
+
def find_cmj_landing_from_position_peak(
|
|
96
236
|
positions: FloatArray,
|
|
97
237
|
velocities: FloatArray,
|
|
98
238
|
accelerations: FloatArray,
|
|
@@ -150,7 +290,7 @@ def _find_cmj_landing_from_position_peak(
|
|
|
150
290
|
reason="Experimental alternative superseded by backward search algorithm",
|
|
151
291
|
since="0.34.0",
|
|
152
292
|
)
|
|
153
|
-
def
|
|
293
|
+
def find_interpolated_takeoff_landing(
|
|
154
294
|
positions: FloatArray,
|
|
155
295
|
velocities: FloatArray,
|
|
156
296
|
lowest_point_frame: int,
|
|
@@ -183,19 +323,19 @@ def _find_interpolated_takeoff_landing(
|
|
|
183
323
|
)
|
|
184
324
|
|
|
185
325
|
# Find takeoff using peak velocity method (CMJ-specific)
|
|
186
|
-
takeoff_frame =
|
|
326
|
+
takeoff_frame = find_cmj_takeoff_from_velocity_peak(
|
|
187
327
|
positions, velocities, lowest_point_frame, fps
|
|
188
328
|
)
|
|
189
329
|
|
|
190
330
|
# Find landing using position peak and impact detection
|
|
191
|
-
landing_frame =
|
|
331
|
+
landing_frame = find_cmj_landing_from_position_peak(
|
|
192
332
|
positions, velocities, accelerations, int(takeoff_frame), fps
|
|
193
333
|
)
|
|
194
334
|
|
|
195
335
|
return (takeoff_frame, landing_frame)
|
|
196
336
|
|
|
197
337
|
|
|
198
|
-
def
|
|
338
|
+
def find_takeoff_frame(
|
|
199
339
|
velocities: FloatArray,
|
|
200
340
|
peak_height_frame: int,
|
|
201
341
|
fps: float,
|
|
@@ -258,7 +398,7 @@ def _find_takeoff_frame(
|
|
|
258
398
|
return float(peak_vel_frame)
|
|
259
399
|
|
|
260
400
|
|
|
261
|
-
def
|
|
401
|
+
def find_lowest_frame(
|
|
262
402
|
velocities: FloatArray, positions: FloatArray, takeoff_frame: float, fps: float
|
|
263
403
|
) -> float:
|
|
264
404
|
"""Find lowest point frame before takeoff."""
|
|
@@ -327,7 +467,7 @@ def _find_landing_impact(
|
|
|
327
467
|
return float(landing_frame)
|
|
328
468
|
|
|
329
469
|
|
|
330
|
-
def
|
|
470
|
+
def find_landing_frame(
|
|
331
471
|
accelerations: FloatArray,
|
|
332
472
|
velocities: FloatArray,
|
|
333
473
|
peak_height_frame: int,
|
|
@@ -363,10 +503,12 @@ def compute_average_hip_position(
|
|
|
363
503
|
Returns:
|
|
364
504
|
(x, y) average hip position in normalized coordinates
|
|
365
505
|
"""
|
|
506
|
+
hip_keys = ["left_hip", "right_hip"]
|
|
507
|
+
|
|
366
508
|
x_positions: list[float] = []
|
|
367
509
|
y_positions: list[float] = []
|
|
368
510
|
|
|
369
|
-
for key in
|
|
511
|
+
for key in hip_keys:
|
|
370
512
|
if key in landmarks:
|
|
371
513
|
x, y, visibility = landmarks[key]
|
|
372
514
|
if visibility > 0.5: # Only use visible landmarks
|
|
@@ -379,7 +521,7 @@ def compute_average_hip_position(
|
|
|
379
521
|
return (float(np.mean(x_positions)), float(np.mean(y_positions)))
|
|
380
522
|
|
|
381
523
|
|
|
382
|
-
def
|
|
524
|
+
def find_standing_end(
|
|
383
525
|
velocities: FloatArray,
|
|
384
526
|
lowest_point: float,
|
|
385
527
|
_positions: FloatArray | None = None,
|
|
@@ -490,12 +632,12 @@ def detect_cmj_phases(
|
|
|
490
632
|
|
|
491
633
|
# Step 2-4: Find all phases using helper functions
|
|
492
634
|
with timer.measure("cmj_find_takeoff"):
|
|
493
|
-
takeoff_frame =
|
|
635
|
+
takeoff_frame = find_takeoff_frame(
|
|
494
636
|
velocities, peak_height_frame, fps, accelerations=accelerations
|
|
495
637
|
)
|
|
496
638
|
|
|
497
639
|
with timer.measure("cmj_find_lowest_point"):
|
|
498
|
-
lowest_point =
|
|
640
|
+
lowest_point = find_lowest_frame(velocities, positions, takeoff_frame, fps)
|
|
499
641
|
|
|
500
642
|
# Determine landing frame
|
|
501
643
|
with timer.measure("cmj_find_landing"):
|
|
@@ -509,7 +651,7 @@ def detect_cmj_phases(
|
|
|
509
651
|
)
|
|
510
652
|
# We still reference peak_height_frame from Hips, as Feet peak
|
|
511
653
|
# might be different/noisy but generally they align in time.
|
|
512
|
-
landing_frame =
|
|
654
|
+
landing_frame = find_landing_frame(
|
|
513
655
|
landing_accelerations,
|
|
514
656
|
landing_velocities,
|
|
515
657
|
peak_height_frame,
|
|
@@ -517,7 +659,7 @@ def detect_cmj_phases(
|
|
|
517
659
|
)
|
|
518
660
|
else:
|
|
519
661
|
# Use primary signal (Hips)
|
|
520
|
-
landing_frame =
|
|
662
|
+
landing_frame = find_landing_frame(
|
|
521
663
|
accelerations,
|
|
522
664
|
velocities,
|
|
523
665
|
peak_height_frame,
|
|
@@ -525,6 +667,6 @@ def detect_cmj_phases(
|
|
|
525
667
|
)
|
|
526
668
|
|
|
527
669
|
with timer.measure("cmj_find_standing_end"):
|
|
528
|
-
standing_end =
|
|
670
|
+
standing_end = find_standing_end(velocities, lowest_point, positions, accelerations)
|
|
529
671
|
|
|
530
672
|
return (standing_end, lowest_point, takeoff_frame, landing_frame)
|
|
@@ -262,15 +262,13 @@ def _get_tuned_parameters(
|
|
|
262
262
|
with timer.measure("parameter_auto_tuning"):
|
|
263
263
|
characteristics = analyze_video_sample(landmarks_sequence, video.fps, video.frame_count)
|
|
264
264
|
params = auto_tune_parameters(characteristics, quality_preset)
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
overrides.visibility_threshold,
|
|
273
|
-
)
|
|
265
|
+
params = apply_expert_overrides(
|
|
266
|
+
params,
|
|
267
|
+
overrides.smoothing_window if overrides else None,
|
|
268
|
+
overrides.velocity_threshold if overrides else None,
|
|
269
|
+
overrides.min_contact_frames if overrides else None,
|
|
270
|
+
overrides.visibility_threshold if overrides else None,
|
|
271
|
+
)
|
|
274
272
|
|
|
275
273
|
if verbose:
|
|
276
274
|
print_verbose_parameters(video, characteristics, quality_preset, params)
|
|
@@ -393,24 +391,6 @@ class CMJVideoConfig:
|
|
|
393
391
|
overrides: AnalysisOverrides | None = None
|
|
394
392
|
detection_confidence: float | None = None
|
|
395
393
|
tracking_confidence: float | None = None
|
|
396
|
-
verbose: bool = False
|
|
397
|
-
timer: Timer | None = None
|
|
398
|
-
pose_tracker: "MediaPipePoseTracker | None" = None
|
|
399
|
-
|
|
400
|
-
def to_kwargs(self) -> dict:
|
|
401
|
-
"""Convert config to kwargs dict for process_cmj_video."""
|
|
402
|
-
return {
|
|
403
|
-
"video_path": self.video_path,
|
|
404
|
-
"quality": self.quality,
|
|
405
|
-
"output_video": self.output_video,
|
|
406
|
-
"json_output": self.json_output,
|
|
407
|
-
"overrides": self.overrides,
|
|
408
|
-
"detection_confidence": self.detection_confidence,
|
|
409
|
-
"tracking_confidence": self.tracking_confidence,
|
|
410
|
-
"verbose": self.verbose,
|
|
411
|
-
"timer": self.timer,
|
|
412
|
-
"pose_tracker": self.pose_tracker,
|
|
413
|
-
}
|
|
414
394
|
|
|
415
395
|
|
|
416
396
|
@dataclass
|
|
@@ -529,23 +509,6 @@ def process_cmj_video(
|
|
|
529
509
|
return metrics
|
|
530
510
|
|
|
531
511
|
|
|
532
|
-
def process_cmj_video_from_config(
|
|
533
|
-
config: CMJVideoConfig,
|
|
534
|
-
) -> CMJMetrics:
|
|
535
|
-
"""Process a CMJ video using a configuration object.
|
|
536
|
-
|
|
537
|
-
This is a convenience wrapper around process_cmj_video that
|
|
538
|
-
accepts a CMJVideoConfig instead of individual parameters.
|
|
539
|
-
|
|
540
|
-
Args:
|
|
541
|
-
config: Configuration object containing all analysis parameters
|
|
542
|
-
|
|
543
|
-
Returns:
|
|
544
|
-
CMJMetrics object containing analysis results
|
|
545
|
-
"""
|
|
546
|
-
return process_cmj_video(**config.to_kwargs())
|
|
547
|
-
|
|
548
|
-
|
|
549
512
|
def process_cmj_videos_bulk(
|
|
550
513
|
configs: list[CMJVideoConfig],
|
|
551
514
|
max_workers: int = 4,
|
|
@@ -572,8 +535,17 @@ def _process_cmj_video_wrapper(config: CMJVideoConfig) -> CMJVideoResult:
|
|
|
572
535
|
start_time = time.perf_counter()
|
|
573
536
|
|
|
574
537
|
try:
|
|
575
|
-
|
|
576
|
-
|
|
538
|
+
metrics = process_cmj_video(
|
|
539
|
+
video_path=config.video_path,
|
|
540
|
+
quality=config.quality,
|
|
541
|
+
output_video=config.output_video,
|
|
542
|
+
json_output=config.json_output,
|
|
543
|
+
overrides=config.overrides,
|
|
544
|
+
detection_confidence=config.detection_confidence,
|
|
545
|
+
tracking_confidence=config.tracking_confidence,
|
|
546
|
+
verbose=False,
|
|
547
|
+
)
|
|
548
|
+
|
|
577
549
|
processing_time = time.perf_counter() - start_time
|
|
578
550
|
|
|
579
551
|
return CMJVideoResult(
|
|
@@ -8,12 +8,9 @@ import click
|
|
|
8
8
|
|
|
9
9
|
from ..core.auto_tuning import QualityPreset
|
|
10
10
|
from ..core.cli_utils import (
|
|
11
|
-
batch_processing_options,
|
|
12
11
|
collect_video_files,
|
|
13
12
|
common_output_options,
|
|
14
13
|
generate_batch_output_paths,
|
|
15
|
-
quality_option,
|
|
16
|
-
verbose_option,
|
|
17
14
|
)
|
|
18
15
|
from .api import AnalysisOverrides, process_cmj_video
|
|
19
16
|
from .kinematics import CMJMetrics
|
|
@@ -62,9 +59,52 @@ def _process_batch_videos(
|
|
|
62
59
|
@click.command(name="cmj-analyze")
|
|
63
60
|
@click.argument("video_path", nargs=-1, type=click.Path(exists=False), required=True)
|
|
64
61
|
@common_output_options
|
|
65
|
-
@
|
|
66
|
-
|
|
67
|
-
|
|
62
|
+
@click.option(
|
|
63
|
+
"--quality",
|
|
64
|
+
type=click.Choice(["fast", "balanced", "accurate"], case_sensitive=False),
|
|
65
|
+
default="balanced",
|
|
66
|
+
help=(
|
|
67
|
+
"Analysis quality preset: "
|
|
68
|
+
"fast (quick, less precise), "
|
|
69
|
+
"balanced (default, good for most cases), "
|
|
70
|
+
"accurate (research-grade, slower)"
|
|
71
|
+
),
|
|
72
|
+
show_default=True,
|
|
73
|
+
)
|
|
74
|
+
@click.option(
|
|
75
|
+
"--verbose",
|
|
76
|
+
"-v",
|
|
77
|
+
is_flag=True,
|
|
78
|
+
help="Show auto-selected parameters and analysis details",
|
|
79
|
+
)
|
|
80
|
+
# Batch processing options
|
|
81
|
+
@click.option(
|
|
82
|
+
"--batch",
|
|
83
|
+
is_flag=True,
|
|
84
|
+
help="Enable batch processing mode for multiple videos",
|
|
85
|
+
)
|
|
86
|
+
@click.option(
|
|
87
|
+
"--workers",
|
|
88
|
+
type=int,
|
|
89
|
+
default=4,
|
|
90
|
+
help="Number of parallel workers for batch processing (default: 4)",
|
|
91
|
+
show_default=True,
|
|
92
|
+
)
|
|
93
|
+
@click.option(
|
|
94
|
+
"--output-dir",
|
|
95
|
+
type=click.Path(),
|
|
96
|
+
help="Directory for debug video outputs (batch mode only)",
|
|
97
|
+
)
|
|
98
|
+
@click.option(
|
|
99
|
+
"--json-output-dir",
|
|
100
|
+
type=click.Path(),
|
|
101
|
+
help="Directory for JSON metrics outputs (batch mode only)",
|
|
102
|
+
)
|
|
103
|
+
@click.option(
|
|
104
|
+
"--csv-summary",
|
|
105
|
+
type=click.Path(),
|
|
106
|
+
help="Path for CSV summary export (batch mode only)",
|
|
107
|
+
)
|
|
68
108
|
# Expert parameters (hidden in help, but always available for advanced users)
|
|
69
109
|
@click.option(
|
|
70
110
|
"--smoothing-window",
|