kinemotion 0.34.0__py3-none-any.whl → 0.35.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/api.py +2 -2
- kinemotion/cmj/analysis.py +21 -101
- kinemotion/{core/cmj_metrics_validator.py → cmj/metrics_validator.py} +27 -123
- kinemotion/{core/cmj_validation_bounds.py → cmj/validation_bounds.py} +1 -58
- kinemotion/core/cli_utils.py +26 -0
- kinemotion/core/experimental.py +103 -0
- kinemotion/core/filtering.py +7 -0
- kinemotion/core/smoothing.py +34 -0
- kinemotion/core/validation.py +198 -0
- kinemotion/dropjump/__init__.py +1 -1
- kinemotion/dropjump/analysis.py +23 -34
- kinemotion/{core/dropjump_metrics_validator.py → dropjump/metrics_validator.py} +20 -114
- kinemotion/{core/dropjump_validation_bounds.py → dropjump/validation_bounds.py} +1 -58
- {kinemotion-0.34.0.dist-info → kinemotion-0.35.0.dist-info}/METADATA +1 -1
- {kinemotion-0.34.0.dist-info → kinemotion-0.35.0.dist-info}/RECORD +18 -16
- {kinemotion-0.34.0.dist-info → kinemotion-0.35.0.dist-info}/WHEEL +0 -0
- {kinemotion-0.34.0.dist-info → kinemotion-0.35.0.dist-info}/entry_points.txt +0 -0
- {kinemotion-0.34.0.dist-info → kinemotion-0.35.0.dist-info}/licenses/LICENSE +0 -0
kinemotion/api.py
CHANGED
|
@@ -11,6 +11,7 @@ import numpy as np
|
|
|
11
11
|
from .cmj.analysis import detect_cmj_phases
|
|
12
12
|
from .cmj.debug_overlay import CMJDebugOverlayRenderer
|
|
13
13
|
from .cmj.kinematics import CMJMetrics, calculate_cmj_metrics
|
|
14
|
+
from .cmj.metrics_validator import CMJMetricsValidator
|
|
14
15
|
from .core.auto_tuning import (
|
|
15
16
|
AnalysisParameters,
|
|
16
17
|
QualityPreset,
|
|
@@ -18,8 +19,6 @@ from .core.auto_tuning import (
|
|
|
18
19
|
analyze_video_sample,
|
|
19
20
|
auto_tune_parameters,
|
|
20
21
|
)
|
|
21
|
-
from .core.cmj_metrics_validator import CMJMetricsValidator
|
|
22
|
-
from .core.dropjump_metrics_validator import DropJumpMetricsValidator
|
|
23
22
|
from .core.filtering import reject_outliers
|
|
24
23
|
from .core.metadata import (
|
|
25
24
|
AlgorithmConfig,
|
|
@@ -44,6 +43,7 @@ from .dropjump.analysis import (
|
|
|
44
43
|
)
|
|
45
44
|
from .dropjump.debug_overlay import DebugOverlayRenderer
|
|
46
45
|
from .dropjump.kinematics import DropJumpMetrics, calculate_drop_jump_metrics
|
|
46
|
+
from .dropjump.metrics_validator import DropJumpMetricsValidator
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
def _parse_quality_preset(quality: str) -> QualityPreset:
|
kinemotion/cmj/analysis.py
CHANGED
|
@@ -5,6 +5,7 @@ from enum import Enum
|
|
|
5
5
|
import numpy as np
|
|
6
6
|
from scipy.signal import savgol_filter
|
|
7
7
|
|
|
8
|
+
from ..core.experimental import unused
|
|
8
9
|
from ..core.smoothing import compute_acceleration_from_derivative
|
|
9
10
|
|
|
10
11
|
|
|
@@ -52,6 +53,10 @@ class CMJPhase(Enum):
|
|
|
52
53
|
UNKNOWN = "unknown"
|
|
53
54
|
|
|
54
55
|
|
|
56
|
+
@unused(
|
|
57
|
+
reason="Alternative implementation not called by pipeline",
|
|
58
|
+
since="0.34.0",
|
|
59
|
+
)
|
|
55
60
|
def find_standing_phase(
|
|
56
61
|
positions: np.ndarray,
|
|
57
62
|
velocities: np.ndarray,
|
|
@@ -101,6 +106,10 @@ def find_standing_phase(
|
|
|
101
106
|
return None
|
|
102
107
|
|
|
103
108
|
|
|
109
|
+
@unused(
|
|
110
|
+
reason="Alternative implementation not called by pipeline",
|
|
111
|
+
since="0.34.0",
|
|
112
|
+
)
|
|
104
113
|
def find_countermovement_start(
|
|
105
114
|
velocities: np.ndarray,
|
|
106
115
|
countermovement_threshold: float = 0.015,
|
|
@@ -184,99 +193,6 @@ def find_lowest_point(
|
|
|
184
193
|
return lowest_frame
|
|
185
194
|
|
|
186
195
|
|
|
187
|
-
def refine_transition_with_curvature(
|
|
188
|
-
positions: np.ndarray,
|
|
189
|
-
velocities: np.ndarray,
|
|
190
|
-
initial_frame: int,
|
|
191
|
-
transition_type: str,
|
|
192
|
-
search_radius: int = 3,
|
|
193
|
-
window_length: int = 5,
|
|
194
|
-
polyorder: int = 2,
|
|
195
|
-
) -> float:
|
|
196
|
-
"""
|
|
197
|
-
Refine transition frame using trajectory curvature (acceleration patterns).
|
|
198
|
-
|
|
199
|
-
Uses acceleration (second derivative) to identify characteristic patterns:
|
|
200
|
-
- Landing: Large acceleration spike (impact deceleration)
|
|
201
|
-
- Takeoff: Acceleration change (transition from static to flight)
|
|
202
|
-
|
|
203
|
-
Args:
|
|
204
|
-
positions: Array of vertical positions
|
|
205
|
-
velocities: Array of vertical velocities
|
|
206
|
-
initial_frame: Initial estimate of transition frame
|
|
207
|
-
transition_type: Type of transition ("takeoff" or "landing")
|
|
208
|
-
search_radius: Frames to search around initial estimate (±radius)
|
|
209
|
-
window_length: Window size for acceleration calculation
|
|
210
|
-
polyorder: Polynomial order for Savitzky-Golay filter
|
|
211
|
-
|
|
212
|
-
Returns:
|
|
213
|
-
Refined fractional frame index.
|
|
214
|
-
"""
|
|
215
|
-
# Compute acceleration using second derivative
|
|
216
|
-
acceleration = compute_acceleration_from_derivative(
|
|
217
|
-
positions, window_length=window_length, polyorder=polyorder
|
|
218
|
-
)
|
|
219
|
-
|
|
220
|
-
# Define search window
|
|
221
|
-
search_start = max(0, initial_frame - search_radius)
|
|
222
|
-
search_end = min(len(positions), initial_frame + search_radius + 1)
|
|
223
|
-
|
|
224
|
-
if search_start >= search_end:
|
|
225
|
-
return float(initial_frame)
|
|
226
|
-
|
|
227
|
-
search_accel = acceleration[search_start:search_end]
|
|
228
|
-
|
|
229
|
-
if transition_type == "landing":
|
|
230
|
-
# Landing: Find maximum absolute acceleration (impact)
|
|
231
|
-
peak_idx = int(np.argmax(np.abs(search_accel)))
|
|
232
|
-
elif transition_type == "takeoff":
|
|
233
|
-
# Takeoff: Find maximum acceleration change
|
|
234
|
-
accel_change = np.abs(np.diff(search_accel))
|
|
235
|
-
if len(accel_change) > 0:
|
|
236
|
-
peak_idx = int(np.argmax(accel_change))
|
|
237
|
-
else:
|
|
238
|
-
peak_idx = 0
|
|
239
|
-
else:
|
|
240
|
-
return float(initial_frame)
|
|
241
|
-
|
|
242
|
-
curvature_frame = search_start + peak_idx
|
|
243
|
-
|
|
244
|
-
# Blend curvature-based estimate with velocity-based estimate
|
|
245
|
-
# 70% curvature, 30% velocity
|
|
246
|
-
blended_frame = 0.7 * curvature_frame + 0.3 * initial_frame
|
|
247
|
-
|
|
248
|
-
return float(blended_frame)
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
def interpolate_threshold_crossing(
|
|
252
|
-
vel_before: float,
|
|
253
|
-
vel_after: float,
|
|
254
|
-
velocity_threshold: float,
|
|
255
|
-
) -> float:
|
|
256
|
-
"""
|
|
257
|
-
Find fractional offset where velocity crosses threshold between two frames.
|
|
258
|
-
|
|
259
|
-
Uses linear interpolation assuming velocity changes linearly between frames.
|
|
260
|
-
|
|
261
|
-
Args:
|
|
262
|
-
vel_before: Velocity at frame boundary N (absolute value)
|
|
263
|
-
vel_after: Velocity at frame boundary N+1 (absolute value)
|
|
264
|
-
velocity_threshold: Threshold value
|
|
265
|
-
|
|
266
|
-
Returns:
|
|
267
|
-
Fractional offset from frame N (0.0 to 1.0)
|
|
268
|
-
"""
|
|
269
|
-
# Handle edge cases
|
|
270
|
-
if abs(vel_after - vel_before) < 1e-9: # Velocity not changing
|
|
271
|
-
return 0.5
|
|
272
|
-
|
|
273
|
-
# Linear interpolation
|
|
274
|
-
t = (velocity_threshold - vel_before) / (vel_after - vel_before)
|
|
275
|
-
|
|
276
|
-
# Clamp to [0, 1] range
|
|
277
|
-
return float(max(0.0, min(1.0, t)))
|
|
278
|
-
|
|
279
|
-
|
|
280
196
|
def find_cmj_takeoff_from_velocity_peak(
|
|
281
197
|
positions: np.ndarray,
|
|
282
198
|
velocities: np.ndarray,
|
|
@@ -372,6 +288,10 @@ def find_cmj_landing_from_position_peak(
|
|
|
372
288
|
return float(landing_frame)
|
|
373
289
|
|
|
374
290
|
|
|
291
|
+
@unused(
|
|
292
|
+
reason="Experimental alternative superseded by backward search algorithm",
|
|
293
|
+
since="0.34.0",
|
|
294
|
+
)
|
|
375
295
|
def find_interpolated_takeoff_landing(
|
|
376
296
|
positions: np.ndarray,
|
|
377
297
|
velocities: np.ndarray,
|
|
@@ -417,7 +337,7 @@ def find_interpolated_takeoff_landing(
|
|
|
417
337
|
return (takeoff_frame, landing_frame)
|
|
418
338
|
|
|
419
339
|
|
|
420
|
-
def
|
|
340
|
+
def find_takeoff_frame(
|
|
421
341
|
velocities: np.ndarray, peak_height_frame: int, fps: float
|
|
422
342
|
) -> float:
|
|
423
343
|
"""Find takeoff frame as peak upward velocity before peak height.
|
|
@@ -448,7 +368,7 @@ def _find_takeoff_frame(
|
|
|
448
368
|
return float(takeoff_search_start + peak_vel_idx)
|
|
449
369
|
|
|
450
370
|
|
|
451
|
-
def
|
|
371
|
+
def find_lowest_frame(
|
|
452
372
|
velocities: np.ndarray, positions: np.ndarray, takeoff_frame: float, fps: float
|
|
453
373
|
) -> float:
|
|
454
374
|
"""Find lowest point frame before takeoff."""
|
|
@@ -469,7 +389,7 @@ def _find_lowest_frame(
|
|
|
469
389
|
return float(int(takeoff_frame) - int(fps * 0.2))
|
|
470
390
|
|
|
471
391
|
|
|
472
|
-
def
|
|
392
|
+
def find_landing_frame(
|
|
473
393
|
accelerations: np.ndarray, peak_height_frame: int, fps: float
|
|
474
394
|
) -> float:
|
|
475
395
|
"""Find landing frame after peak height after takeoff.
|
|
@@ -502,7 +422,7 @@ def _find_landing_frame(
|
|
|
502
422
|
return float(landing_search_start + landing_idx)
|
|
503
423
|
|
|
504
424
|
|
|
505
|
-
def
|
|
425
|
+
def find_standing_end(velocities: np.ndarray, lowest_point: float) -> float | None:
|
|
506
426
|
"""Find end of standing phase before lowest point."""
|
|
507
427
|
if lowest_point <= 20:
|
|
508
428
|
return None
|
|
@@ -556,9 +476,9 @@ def detect_cmj_phases(
|
|
|
556
476
|
return None # Peak too early, invalid
|
|
557
477
|
|
|
558
478
|
# Step 2-4: Find all phases using helper functions
|
|
559
|
-
takeoff_frame =
|
|
560
|
-
lowest_point =
|
|
561
|
-
landing_frame =
|
|
562
|
-
standing_end =
|
|
479
|
+
takeoff_frame = find_takeoff_frame(velocities, peak_height_frame, fps)
|
|
480
|
+
lowest_point = find_lowest_frame(velocities, positions, takeoff_frame, fps)
|
|
481
|
+
landing_frame = find_landing_frame(accelerations, peak_height_frame, fps)
|
|
482
|
+
standing_end = find_standing_end(velocities, lowest_point)
|
|
563
483
|
|
|
564
484
|
return (standing_end, lowest_point, takeoff_frame, landing_frame)
|
|
@@ -7,120 +7,33 @@ Provides severity levels (ERROR, WARNING, INFO) for different categories
|
|
|
7
7
|
of metric issues.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
from dataclasses import dataclass
|
|
11
|
-
from enum import Enum
|
|
10
|
+
from dataclasses import dataclass
|
|
12
11
|
|
|
13
|
-
from kinemotion.
|
|
14
|
-
AthleteProfile,
|
|
12
|
+
from kinemotion.cmj.validation_bounds import (
|
|
15
13
|
CMJBounds,
|
|
16
|
-
MetricBounds,
|
|
17
14
|
MetricConsistency,
|
|
18
15
|
RSIBounds,
|
|
19
16
|
TripleExtensionBounds,
|
|
20
17
|
estimate_athlete_profile,
|
|
21
18
|
)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
WARNING = "WARNING" # Metrics valid but unusual, needs review
|
|
29
|
-
INFO = "INFO" # Normal variation, informational only
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
@dataclass
|
|
33
|
-
class ValidationIssue:
|
|
34
|
-
"""Single validation issue."""
|
|
35
|
-
|
|
36
|
-
severity: ValidationSeverity
|
|
37
|
-
metric: str
|
|
38
|
-
message: str
|
|
39
|
-
value: float | None = None
|
|
40
|
-
bounds: tuple[float, float] | None = None
|
|
19
|
+
from kinemotion.core.validation import (
|
|
20
|
+
AthleteProfile,
|
|
21
|
+
MetricBounds,
|
|
22
|
+
MetricsValidator,
|
|
23
|
+
ValidationResult,
|
|
24
|
+
)
|
|
41
25
|
|
|
42
26
|
|
|
43
27
|
@dataclass
|
|
44
|
-
class ValidationResult:
|
|
45
|
-
"""
|
|
28
|
+
class CMJValidationResult(ValidationResult):
|
|
29
|
+
"""CMJ-specific validation result."""
|
|
46
30
|
|
|
47
|
-
issues: list[ValidationIssue] = field(default_factory=list)
|
|
48
|
-
status: str = "PASS" # "PASS", "PASS_WITH_WARNINGS", "FAIL"
|
|
49
|
-
athlete_profile: AthleteProfile | None = None
|
|
50
31
|
rsi: float | None = None
|
|
51
32
|
height_flight_time_consistency: float | None = None # % error
|
|
52
33
|
velocity_height_consistency: float | None = None # % error
|
|
53
34
|
depth_height_ratio: float | None = None
|
|
54
35
|
contact_depth_ratio: float | None = None
|
|
55
36
|
|
|
56
|
-
def add_error(
|
|
57
|
-
self,
|
|
58
|
-
metric: str,
|
|
59
|
-
message: str,
|
|
60
|
-
value: float | None = None,
|
|
61
|
-
bounds: tuple[float, float] | None = None,
|
|
62
|
-
) -> None:
|
|
63
|
-
"""Add error-level issue."""
|
|
64
|
-
self.issues.append(
|
|
65
|
-
ValidationIssue(
|
|
66
|
-
severity=ValidationSeverity.ERROR,
|
|
67
|
-
metric=metric,
|
|
68
|
-
message=message,
|
|
69
|
-
value=value,
|
|
70
|
-
bounds=bounds,
|
|
71
|
-
)
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
def add_warning(
|
|
75
|
-
self,
|
|
76
|
-
metric: str,
|
|
77
|
-
message: str,
|
|
78
|
-
value: float | None = None,
|
|
79
|
-
bounds: tuple[float, float] | None = None,
|
|
80
|
-
) -> None:
|
|
81
|
-
"""Add warning-level issue."""
|
|
82
|
-
self.issues.append(
|
|
83
|
-
ValidationIssue(
|
|
84
|
-
severity=ValidationSeverity.WARNING,
|
|
85
|
-
metric=metric,
|
|
86
|
-
message=message,
|
|
87
|
-
value=value,
|
|
88
|
-
bounds=bounds,
|
|
89
|
-
)
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
def add_info(
|
|
93
|
-
self,
|
|
94
|
-
metric: str,
|
|
95
|
-
message: str,
|
|
96
|
-
value: float | None = None,
|
|
97
|
-
) -> None:
|
|
98
|
-
"""Add info-level issue."""
|
|
99
|
-
self.issues.append(
|
|
100
|
-
ValidationIssue(
|
|
101
|
-
severity=ValidationSeverity.INFO,
|
|
102
|
-
metric=metric,
|
|
103
|
-
message=message,
|
|
104
|
-
value=value,
|
|
105
|
-
)
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
def finalize_status(self) -> None:
|
|
109
|
-
"""Determine final pass/fail status based on issues."""
|
|
110
|
-
has_errors = any(
|
|
111
|
-
issue.severity == ValidationSeverity.ERROR for issue in self.issues
|
|
112
|
-
)
|
|
113
|
-
has_warnings = any(
|
|
114
|
-
issue.severity == ValidationSeverity.WARNING for issue in self.issues
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
if has_errors:
|
|
118
|
-
self.status = "FAIL"
|
|
119
|
-
elif has_warnings:
|
|
120
|
-
self.status = "PASS_WITH_WARNINGS"
|
|
121
|
-
else:
|
|
122
|
-
self.status = "PASS"
|
|
123
|
-
|
|
124
37
|
def to_dict(self) -> dict:
|
|
125
38
|
"""Convert validation result to JSON-serializable dictionary.
|
|
126
39
|
|
|
@@ -152,28 +65,19 @@ class ValidationResult:
|
|
|
152
65
|
}
|
|
153
66
|
|
|
154
67
|
|
|
155
|
-
class CMJMetricsValidator:
|
|
68
|
+
class CMJMetricsValidator(MetricsValidator):
|
|
156
69
|
"""Comprehensive CMJ metrics validator."""
|
|
157
70
|
|
|
158
|
-
def
|
|
159
|
-
"""Initialize validator.
|
|
160
|
-
|
|
161
|
-
Args:
|
|
162
|
-
assumed_profile: If provided, validate against this specific profile.
|
|
163
|
-
Otherwise, estimate from metrics.
|
|
164
|
-
"""
|
|
165
|
-
self.assumed_profile = assumed_profile
|
|
166
|
-
|
|
167
|
-
def validate(self, metrics: dict) -> ValidationResult:
|
|
71
|
+
def validate(self, metrics: dict) -> CMJValidationResult:
|
|
168
72
|
"""Validate CMJ metrics comprehensively.
|
|
169
73
|
|
|
170
74
|
Args:
|
|
171
75
|
metrics: Dictionary with CMJ metric values
|
|
172
76
|
|
|
173
77
|
Returns:
|
|
174
|
-
|
|
78
|
+
CMJValidationResult with all issues and status
|
|
175
79
|
"""
|
|
176
|
-
result =
|
|
80
|
+
result = CMJValidationResult()
|
|
177
81
|
|
|
178
82
|
# Estimate athlete profile if not provided
|
|
179
83
|
if self.assumed_profile:
|
|
@@ -209,7 +113,7 @@ class CMJMetricsValidator:
|
|
|
209
113
|
return result
|
|
210
114
|
|
|
211
115
|
def _check_flight_time(
|
|
212
|
-
self, metrics: dict, result:
|
|
116
|
+
self, metrics: dict, result: CMJValidationResult, profile: AthleteProfile
|
|
213
117
|
) -> None:
|
|
214
118
|
"""Validate flight time."""
|
|
215
119
|
flight_time = metrics.get("flight_time")
|
|
@@ -252,7 +156,7 @@ class CMJMetricsValidator:
|
|
|
252
156
|
)
|
|
253
157
|
|
|
254
158
|
def _check_jump_height(
|
|
255
|
-
self, metrics: dict, result:
|
|
159
|
+
self, metrics: dict, result: CMJValidationResult, profile: AthleteProfile
|
|
256
160
|
) -> None:
|
|
257
161
|
"""Validate jump height."""
|
|
258
162
|
jump_height = metrics.get("jump_height")
|
|
@@ -294,7 +198,7 @@ class CMJMetricsValidator:
|
|
|
294
198
|
)
|
|
295
199
|
|
|
296
200
|
def _check_countermovement_depth(
|
|
297
|
-
self, metrics: dict, result:
|
|
201
|
+
self, metrics: dict, result: CMJValidationResult, profile: AthleteProfile
|
|
298
202
|
) -> None:
|
|
299
203
|
"""Validate countermovement depth."""
|
|
300
204
|
depth = metrics.get("countermovement_depth")
|
|
@@ -336,7 +240,7 @@ class CMJMetricsValidator:
|
|
|
336
240
|
)
|
|
337
241
|
|
|
338
242
|
def _check_concentric_duration(
|
|
339
|
-
self, metrics: dict, result:
|
|
243
|
+
self, metrics: dict, result: CMJValidationResult, profile: AthleteProfile
|
|
340
244
|
) -> None:
|
|
341
245
|
"""Validate concentric duration (contact time)."""
|
|
342
246
|
duration = metrics.get("concentric_duration")
|
|
@@ -379,7 +283,7 @@ class CMJMetricsValidator:
|
|
|
379
283
|
)
|
|
380
284
|
|
|
381
285
|
def _check_eccentric_duration(
|
|
382
|
-
self, metrics: dict, result:
|
|
286
|
+
self, metrics: dict, result: CMJValidationResult, profile: AthleteProfile
|
|
383
287
|
) -> None:
|
|
384
288
|
"""Validate eccentric duration."""
|
|
385
289
|
duration = metrics.get("eccentric_duration")
|
|
@@ -413,7 +317,7 @@ class CMJMetricsValidator:
|
|
|
413
317
|
)
|
|
414
318
|
|
|
415
319
|
def _check_peak_velocities(
|
|
416
|
-
self, metrics: dict, result:
|
|
320
|
+
self, metrics: dict, result: CMJValidationResult, profile: AthleteProfile
|
|
417
321
|
) -> None:
|
|
418
322
|
"""Validate peak eccentric and concentric velocities."""
|
|
419
323
|
# Eccentric
|
|
@@ -483,7 +387,7 @@ class CMJMetricsValidator:
|
|
|
483
387
|
)
|
|
484
388
|
|
|
485
389
|
def _check_flight_time_height_consistency(
|
|
486
|
-
self, metrics: dict, result:
|
|
390
|
+
self, metrics: dict, result: CMJValidationResult
|
|
487
391
|
) -> None:
|
|
488
392
|
"""Verify jump height is consistent with flight time."""
|
|
489
393
|
flight_time = metrics.get("flight_time")
|
|
@@ -517,7 +421,7 @@ class CMJMetricsValidator:
|
|
|
517
421
|
)
|
|
518
422
|
|
|
519
423
|
def _check_velocity_height_consistency(
|
|
520
|
-
self, metrics: dict, result:
|
|
424
|
+
self, metrics: dict, result: CMJValidationResult
|
|
521
425
|
) -> None:
|
|
522
426
|
"""Verify peak velocity is consistent with jump height."""
|
|
523
427
|
velocity = metrics.get("peak_concentric_velocity")
|
|
@@ -554,7 +458,7 @@ class CMJMetricsValidator:
|
|
|
554
458
|
)
|
|
555
459
|
|
|
556
460
|
def _check_rsi_validity(
|
|
557
|
-
self, metrics: dict, result:
|
|
461
|
+
self, metrics: dict, result: CMJValidationResult, profile: AthleteProfile
|
|
558
462
|
) -> None:
|
|
559
463
|
"""Validate Reactive Strength Index."""
|
|
560
464
|
flight_time = metrics.get("flight_time")
|
|
@@ -606,7 +510,7 @@ class CMJMetricsValidator:
|
|
|
606
510
|
)
|
|
607
511
|
|
|
608
512
|
def _check_depth_height_ratio(
|
|
609
|
-
self, metrics: dict, result:
|
|
513
|
+
self, metrics: dict, result: CMJValidationResult
|
|
610
514
|
) -> None:
|
|
611
515
|
"""Check countermovement depth to jump height ratio."""
|
|
612
516
|
depth = metrics.get("countermovement_depth")
|
|
@@ -650,7 +554,7 @@ class CMJMetricsValidator:
|
|
|
650
554
|
)
|
|
651
555
|
|
|
652
556
|
def _check_contact_depth_ratio(
|
|
653
|
-
self, metrics: dict, result:
|
|
557
|
+
self, metrics: dict, result: CMJValidationResult
|
|
654
558
|
) -> None:
|
|
655
559
|
"""Check contact time to countermovement depth ratio."""
|
|
656
560
|
contact = metrics.get("concentric_duration")
|
|
@@ -691,7 +595,7 @@ class CMJMetricsValidator:
|
|
|
691
595
|
)
|
|
692
596
|
|
|
693
597
|
def _check_triple_extension(
|
|
694
|
-
self, metrics: dict, result:
|
|
598
|
+
self, metrics: dict, result: CMJValidationResult, profile: AthleteProfile
|
|
695
599
|
) -> None:
|
|
696
600
|
"""Validate triple extension angles."""
|
|
697
601
|
angles = metrics.get("triple_extension")
|
|
@@ -750,7 +654,7 @@ class CMJMetricsValidator:
|
|
|
750
654
|
self._check_joint_compensation_pattern(angles, result, profile)
|
|
751
655
|
|
|
752
656
|
def _check_joint_compensation_pattern(
|
|
753
|
-
self, angles: dict, result:
|
|
657
|
+
self, angles: dict, result: CMJValidationResult, profile: AthleteProfile
|
|
754
658
|
) -> None:
|
|
755
659
|
"""Detect compensatory joint patterns in triple extension.
|
|
756
660
|
|
|
@@ -15,64 +15,7 @@ References:
|
|
|
15
15
|
- Bogdanis (2012): Plyometric training effects
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
-
from
|
|
19
|
-
from enum import Enum
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class AthleteProfile(Enum):
|
|
23
|
-
"""Athlete performance categories for metric bounds."""
|
|
24
|
-
|
|
25
|
-
ELDERLY = "elderly" # 70+, deconditioned
|
|
26
|
-
UNTRAINED = "untrained" # Sedentary, no training
|
|
27
|
-
RECREATIONAL = "recreational" # Fitness class, moderate activity
|
|
28
|
-
TRAINED = "trained" # Regular athlete, 3-5 years training
|
|
29
|
-
ELITE = "elite" # Competitive athlete, college/professional level
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
@dataclass
|
|
33
|
-
class MetricBounds:
|
|
34
|
-
"""Physiological bounds for a single metric.
|
|
35
|
-
|
|
36
|
-
Attributes:
|
|
37
|
-
absolute_min: Absolute minimum value (error threshold)
|
|
38
|
-
practical_min: Practical minimum for weakest athletes
|
|
39
|
-
recreational_min: Minimum for recreational athletes
|
|
40
|
-
recreational_max: Maximum for recreational athletes
|
|
41
|
-
elite_min: Minimum for elite athletes
|
|
42
|
-
elite_max: Maximum for elite athletes
|
|
43
|
-
absolute_max: Absolute maximum value (error threshold)
|
|
44
|
-
unit: Unit of measurement (e.g., "m", "s", "m/s", "degrees")
|
|
45
|
-
"""
|
|
46
|
-
|
|
47
|
-
absolute_min: float
|
|
48
|
-
practical_min: float
|
|
49
|
-
recreational_min: float
|
|
50
|
-
recreational_max: float
|
|
51
|
-
elite_min: float
|
|
52
|
-
elite_max: float
|
|
53
|
-
absolute_max: float
|
|
54
|
-
unit: str
|
|
55
|
-
|
|
56
|
-
def contains(self, value: float, profile: AthleteProfile) -> bool:
|
|
57
|
-
"""Check if value is within bounds for athlete profile."""
|
|
58
|
-
if profile == AthleteProfile.ELDERLY:
|
|
59
|
-
return self.practical_min <= value <= self.recreational_max
|
|
60
|
-
elif profile == AthleteProfile.UNTRAINED:
|
|
61
|
-
return self.practical_min <= value <= self.recreational_max
|
|
62
|
-
elif profile == AthleteProfile.RECREATIONAL:
|
|
63
|
-
return self.recreational_min <= value <= self.recreational_max
|
|
64
|
-
elif profile == AthleteProfile.TRAINED:
|
|
65
|
-
# Trained athletes: midpoint between recreational and elite
|
|
66
|
-
trained_min = (self.recreational_min + self.elite_min) / 2
|
|
67
|
-
trained_max = (self.recreational_max + self.elite_max) / 2
|
|
68
|
-
return trained_min <= value <= trained_max
|
|
69
|
-
elif profile == AthleteProfile.ELITE:
|
|
70
|
-
return self.elite_min <= value <= self.elite_max
|
|
71
|
-
return False
|
|
72
|
-
|
|
73
|
-
def is_physically_possible(self, value: float) -> bool:
|
|
74
|
-
"""Check if value is within absolute physiological limits."""
|
|
75
|
-
return self.absolute_min <= value <= self.absolute_max
|
|
18
|
+
from kinemotion.core.validation import AthleteProfile, MetricBounds
|
|
76
19
|
|
|
77
20
|
|
|
78
21
|
class CMJBounds:
|
kinemotion/core/cli_utils.py
CHANGED
|
@@ -6,6 +6,7 @@ from typing import Any, Protocol
|
|
|
6
6
|
import click
|
|
7
7
|
|
|
8
8
|
from .auto_tuning import AnalysisParameters, QualityPreset, VideoCharacteristics
|
|
9
|
+
from .experimental import unused
|
|
9
10
|
from .pose import PoseTracker
|
|
10
11
|
from .smoothing import smooth_landmarks, smooth_landmarks_advanced
|
|
11
12
|
from .video_io import VideoProcessor
|
|
@@ -22,6 +23,11 @@ class ExpertParameters(Protocol):
|
|
|
22
23
|
visibility_threshold: float | None
|
|
23
24
|
|
|
24
25
|
|
|
26
|
+
@unused(
|
|
27
|
+
reason="Not called by analysis pipeline - remnant from CLI refactoring",
|
|
28
|
+
remove_in="1.0.0",
|
|
29
|
+
since="0.34.0",
|
|
30
|
+
)
|
|
25
31
|
def determine_initial_confidence(
|
|
26
32
|
quality_preset: QualityPreset,
|
|
27
33
|
expert_params: ExpertParameters,
|
|
@@ -54,6 +60,11 @@ def determine_initial_confidence(
|
|
|
54
60
|
return initial_detection_conf, initial_tracking_conf
|
|
55
61
|
|
|
56
62
|
|
|
63
|
+
@unused(
|
|
64
|
+
reason="Not called by analysis pipeline - remnant from CLI refactoring",
|
|
65
|
+
remove_in="1.0.0",
|
|
66
|
+
since="0.34.0",
|
|
67
|
+
)
|
|
57
68
|
def track_all_frames(video: VideoProcessor, tracker: PoseTracker) -> tuple[list, list]:
|
|
58
69
|
"""Track pose landmarks in all video frames.
|
|
59
70
|
|
|
@@ -84,6 +95,11 @@ def track_all_frames(video: VideoProcessor, tracker: PoseTracker) -> tuple[list,
|
|
|
84
95
|
return frames, landmarks_sequence
|
|
85
96
|
|
|
86
97
|
|
|
98
|
+
@unused(
|
|
99
|
+
reason="Not called by analysis pipeline - remnant from CLI refactoring",
|
|
100
|
+
remove_in="1.0.0",
|
|
101
|
+
since="0.34.0",
|
|
102
|
+
)
|
|
87
103
|
def apply_expert_param_overrides(
|
|
88
104
|
params: AnalysisParameters, expert_params: ExpertParameters
|
|
89
105
|
) -> AnalysisParameters:
|
|
@@ -107,6 +123,11 @@ def apply_expert_param_overrides(
|
|
|
107
123
|
return params
|
|
108
124
|
|
|
109
125
|
|
|
126
|
+
@unused(
|
|
127
|
+
reason="Not called by analysis pipeline - remnant from CLI refactoring",
|
|
128
|
+
remove_in="1.0.0",
|
|
129
|
+
since="0.34.0",
|
|
130
|
+
)
|
|
110
131
|
def print_auto_tuned_params(
|
|
111
132
|
video: VideoProcessor,
|
|
112
133
|
quality_preset: QualityPreset,
|
|
@@ -161,6 +182,11 @@ def print_auto_tuned_params(
|
|
|
161
182
|
click.echo("=" * 60 + "\n", err=True)
|
|
162
183
|
|
|
163
184
|
|
|
185
|
+
@unused(
|
|
186
|
+
reason="Not called by analysis pipeline - remnant from CLI refactoring",
|
|
187
|
+
remove_in="1.0.0",
|
|
188
|
+
since="0.34.0",
|
|
189
|
+
)
|
|
164
190
|
def smooth_landmark_sequence(
|
|
165
191
|
landmarks_sequence: list, params: AnalysisParameters
|
|
166
192
|
) -> list:
|