kinemotion 0.35.1__py3-none-any.whl → 0.36.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 +1 -1
- kinemotion/cmj/metrics_validator.py +107 -37
- kinemotion/cmj/validation_bounds.py +4 -1
- {kinemotion-0.35.1.dist-info → kinemotion-0.36.0.dist-info}/METADATA +1 -1
- {kinemotion-0.35.1.dist-info → kinemotion-0.36.0.dist-info}/RECORD +8 -8
- {kinemotion-0.35.1.dist-info → kinemotion-0.36.0.dist-info}/WHEEL +0 -0
- {kinemotion-0.35.1.dist-info → kinemotion-0.36.0.dist-info}/entry_points.txt +0 -0
- {kinemotion-0.35.1.dist-info → kinemotion-0.36.0.dist-info}/licenses/LICENSE +0 -0
kinemotion/api.py
CHANGED
|
@@ -1060,7 +1060,7 @@ def process_cmj_video(
|
|
|
1060
1060
|
|
|
1061
1061
|
# Validate metrics against physiological bounds
|
|
1062
1062
|
validator = CMJMetricsValidator()
|
|
1063
|
-
validation_result = validator.validate(metrics.to_dict()
|
|
1063
|
+
validation_result = validator.validate(metrics.to_dict()) # type: ignore[arg-type]
|
|
1064
1064
|
metrics.validation_result = validation_result
|
|
1065
1065
|
|
|
1066
1066
|
if verbose and validation_result.issues:
|
|
@@ -68,6 +68,22 @@ class CMJValidationResult(ValidationResult):
|
|
|
68
68
|
class CMJMetricsValidator(MetricsValidator):
|
|
69
69
|
"""Comprehensive CMJ metrics validator."""
|
|
70
70
|
|
|
71
|
+
@staticmethod
|
|
72
|
+
def _get_metric_value(
|
|
73
|
+
data: dict, key_with_suffix: str, key_without_suffix: str
|
|
74
|
+
) -> float | None:
|
|
75
|
+
"""Get metric value, supporting both suffixed and legacy key formats.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
data: Dictionary containing metrics
|
|
79
|
+
key_with_suffix: Key with unit suffix (e.g., "flight_time_ms")
|
|
80
|
+
key_without_suffix: Legacy key without suffix (e.g., "flight_time")
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Metric value or None if not found
|
|
84
|
+
"""
|
|
85
|
+
return data.get(key_with_suffix) or data.get(key_without_suffix)
|
|
86
|
+
|
|
71
87
|
def validate(self, metrics: dict) -> CMJValidationResult:
|
|
72
88
|
"""Validate CMJ metrics comprehensively.
|
|
73
89
|
|
|
@@ -87,25 +103,28 @@ class CMJMetricsValidator(MetricsValidator):
|
|
|
87
103
|
|
|
88
104
|
profile = result.athlete_profile
|
|
89
105
|
|
|
106
|
+
# Extract metric values (handle nested "data" structure)
|
|
107
|
+
data = metrics.get("data", metrics) # Support both structures
|
|
108
|
+
|
|
90
109
|
# PRIMARY BOUNDS CHECKS
|
|
91
|
-
self._check_flight_time(
|
|
92
|
-
self._check_jump_height(
|
|
93
|
-
self._check_countermovement_depth(
|
|
94
|
-
self._check_concentric_duration(
|
|
95
|
-
self._check_eccentric_duration(
|
|
96
|
-
self._check_peak_velocities(
|
|
110
|
+
self._check_flight_time(data, result, profile)
|
|
111
|
+
self._check_jump_height(data, result, profile)
|
|
112
|
+
self._check_countermovement_depth(data, result, profile)
|
|
113
|
+
self._check_concentric_duration(data, result, profile)
|
|
114
|
+
self._check_eccentric_duration(data, result, profile)
|
|
115
|
+
self._check_peak_velocities(data, result, profile)
|
|
97
116
|
|
|
98
117
|
# CROSS-VALIDATION CHECKS
|
|
99
|
-
self._check_flight_time_height_consistency(
|
|
100
|
-
self._check_velocity_height_consistency(
|
|
101
|
-
self._check_rsi_validity(
|
|
118
|
+
self._check_flight_time_height_consistency(data, result)
|
|
119
|
+
self._check_velocity_height_consistency(data, result)
|
|
120
|
+
self._check_rsi_validity(data, result, profile)
|
|
102
121
|
|
|
103
122
|
# CONSISTENCY CHECKS
|
|
104
|
-
self._check_depth_height_ratio(
|
|
105
|
-
self._check_contact_depth_ratio(
|
|
123
|
+
self._check_depth_height_ratio(data, result)
|
|
124
|
+
self._check_contact_depth_ratio(data, result)
|
|
106
125
|
|
|
107
126
|
# TRIPLE EXTENSION ANGLES
|
|
108
|
-
self._check_triple_extension(
|
|
127
|
+
self._check_triple_extension(data, result, profile)
|
|
109
128
|
|
|
110
129
|
# Finalize status
|
|
111
130
|
result.finalize_status()
|
|
@@ -116,10 +135,18 @@ class CMJMetricsValidator(MetricsValidator):
|
|
|
116
135
|
self, metrics: dict, result: CMJValidationResult, profile: AthleteProfile
|
|
117
136
|
) -> None:
|
|
118
137
|
"""Validate flight time."""
|
|
119
|
-
|
|
120
|
-
|
|
138
|
+
flight_time_raw = self._get_metric_value(
|
|
139
|
+
metrics, "flight_time_ms", "flight_time"
|
|
140
|
+
)
|
|
141
|
+
if flight_time_raw is None:
|
|
121
142
|
return
|
|
122
143
|
|
|
144
|
+
# If value is in seconds (legacy), use as-is; if in ms, convert
|
|
145
|
+
if flight_time_raw < 10: # Likely in seconds
|
|
146
|
+
flight_time = flight_time_raw
|
|
147
|
+
else: # In milliseconds
|
|
148
|
+
flight_time = flight_time_raw / 1000.0
|
|
149
|
+
|
|
123
150
|
bounds = CMJBounds.FLIGHT_TIME
|
|
124
151
|
|
|
125
152
|
if not bounds.is_physically_possible(flight_time):
|
|
@@ -159,7 +186,7 @@ class CMJMetricsValidator(MetricsValidator):
|
|
|
159
186
|
self, metrics: dict, result: CMJValidationResult, profile: AthleteProfile
|
|
160
187
|
) -> None:
|
|
161
188
|
"""Validate jump height."""
|
|
162
|
-
jump_height =
|
|
189
|
+
jump_height = self._get_metric_value(metrics, "jump_height_m", "jump_height")
|
|
163
190
|
if jump_height is None:
|
|
164
191
|
return
|
|
165
192
|
|
|
@@ -201,7 +228,9 @@ class CMJMetricsValidator(MetricsValidator):
|
|
|
201
228
|
self, metrics: dict, result: CMJValidationResult, profile: AthleteProfile
|
|
202
229
|
) -> None:
|
|
203
230
|
"""Validate countermovement depth."""
|
|
204
|
-
depth =
|
|
231
|
+
depth = self._get_metric_value(
|
|
232
|
+
metrics, "countermovement_depth_m", "countermovement_depth"
|
|
233
|
+
)
|
|
205
234
|
if depth is None:
|
|
206
235
|
return
|
|
207
236
|
|
|
@@ -243,10 +272,19 @@ class CMJMetricsValidator(MetricsValidator):
|
|
|
243
272
|
self, metrics: dict, result: CMJValidationResult, profile: AthleteProfile
|
|
244
273
|
) -> None:
|
|
245
274
|
"""Validate concentric duration (contact time)."""
|
|
246
|
-
|
|
247
|
-
|
|
275
|
+
duration_raw = self._get_metric_value(
|
|
276
|
+
metrics, "concentric_duration_ms", "concentric_duration"
|
|
277
|
+
)
|
|
278
|
+
if duration_raw is None:
|
|
248
279
|
return
|
|
249
280
|
|
|
281
|
+
# If value is in seconds (legacy), convert to ms first
|
|
282
|
+
# Values >10 are assumed to be in ms, <10 assumed to be in seconds
|
|
283
|
+
if duration_raw < 10: # Likely in seconds
|
|
284
|
+
duration = duration_raw
|
|
285
|
+
else: # In milliseconds
|
|
286
|
+
duration = duration_raw / 1000.0
|
|
287
|
+
|
|
250
288
|
bounds = CMJBounds.CONCENTRIC_DURATION
|
|
251
289
|
|
|
252
290
|
if not bounds.is_physically_possible(duration):
|
|
@@ -286,10 +324,18 @@ class CMJMetricsValidator(MetricsValidator):
|
|
|
286
324
|
self, metrics: dict, result: CMJValidationResult, profile: AthleteProfile
|
|
287
325
|
) -> None:
|
|
288
326
|
"""Validate eccentric duration."""
|
|
289
|
-
|
|
290
|
-
|
|
327
|
+
duration_raw = self._get_metric_value(
|
|
328
|
+
metrics, "eccentric_duration_ms", "eccentric_duration"
|
|
329
|
+
)
|
|
330
|
+
if duration_raw is None:
|
|
291
331
|
return
|
|
292
332
|
|
|
333
|
+
# If value is in seconds (legacy), use as-is; if in ms, convert
|
|
334
|
+
if duration_raw < 10: # Likely in seconds
|
|
335
|
+
duration = duration_raw
|
|
336
|
+
else: # In milliseconds
|
|
337
|
+
duration = duration_raw / 1000.0
|
|
338
|
+
|
|
293
339
|
bounds = CMJBounds.ECCENTRIC_DURATION
|
|
294
340
|
|
|
295
341
|
if not bounds.is_physically_possible(duration):
|
|
@@ -321,7 +367,9 @@ class CMJMetricsValidator(MetricsValidator):
|
|
|
321
367
|
) -> None:
|
|
322
368
|
"""Validate peak eccentric and concentric velocities."""
|
|
323
369
|
# Eccentric
|
|
324
|
-
ecc_vel =
|
|
370
|
+
ecc_vel = self._get_metric_value(
|
|
371
|
+
metrics, "peak_eccentric_velocity_m_s", "peak_eccentric_velocity"
|
|
372
|
+
)
|
|
325
373
|
if ecc_vel is not None:
|
|
326
374
|
bounds = CMJBounds.PEAK_ECCENTRIC_VELOCITY
|
|
327
375
|
if not bounds.is_physically_possible(ecc_vel):
|
|
@@ -349,7 +397,9 @@ class CMJMetricsValidator(MetricsValidator):
|
|
|
349
397
|
)
|
|
350
398
|
|
|
351
399
|
# Concentric
|
|
352
|
-
con_vel =
|
|
400
|
+
con_vel = self._get_metric_value(
|
|
401
|
+
metrics, "peak_concentric_velocity_m_s", "peak_concentric_velocity"
|
|
402
|
+
)
|
|
353
403
|
if con_vel is not None:
|
|
354
404
|
bounds = CMJBounds.PEAK_CONCENTRIC_VELOCITY
|
|
355
405
|
if not bounds.is_physically_possible(con_vel):
|
|
@@ -390,12 +440,15 @@ class CMJMetricsValidator(MetricsValidator):
|
|
|
390
440
|
self, metrics: dict, result: CMJValidationResult
|
|
391
441
|
) -> None:
|
|
392
442
|
"""Verify jump height is consistent with flight time."""
|
|
393
|
-
|
|
394
|
-
jump_height = metrics.get("
|
|
443
|
+
flight_time_ms = metrics.get("flight_time_ms")
|
|
444
|
+
jump_height = metrics.get("jump_height_m")
|
|
395
445
|
|
|
396
|
-
if
|
|
446
|
+
if flight_time_ms is None or jump_height is None:
|
|
397
447
|
return
|
|
398
448
|
|
|
449
|
+
# Convert ms to seconds
|
|
450
|
+
flight_time = flight_time_ms / 1000.0
|
|
451
|
+
|
|
399
452
|
# h = g * t^2 / 8
|
|
400
453
|
g = 9.81
|
|
401
454
|
expected_height = (g * flight_time**2) / 8
|
|
@@ -424,8 +477,8 @@ class CMJMetricsValidator(MetricsValidator):
|
|
|
424
477
|
self, metrics: dict, result: CMJValidationResult
|
|
425
478
|
) -> None:
|
|
426
479
|
"""Verify peak velocity is consistent with jump height."""
|
|
427
|
-
velocity = metrics.get("
|
|
428
|
-
jump_height = metrics.get("
|
|
480
|
+
velocity = metrics.get("peak_concentric_velocity_m_s")
|
|
481
|
+
jump_height = metrics.get("jump_height_m")
|
|
429
482
|
|
|
430
483
|
if velocity is None or jump_height is None:
|
|
431
484
|
return
|
|
@@ -461,16 +514,31 @@ class CMJMetricsValidator(MetricsValidator):
|
|
|
461
514
|
self, metrics: dict, result: CMJValidationResult, profile: AthleteProfile
|
|
462
515
|
) -> None:
|
|
463
516
|
"""Validate Reactive Strength Index."""
|
|
464
|
-
|
|
465
|
-
|
|
517
|
+
flight_time_raw = self._get_metric_value(
|
|
518
|
+
metrics, "flight_time_ms", "flight_time"
|
|
519
|
+
)
|
|
520
|
+
concentric_duration_raw = self._get_metric_value(
|
|
521
|
+
metrics, "concentric_duration_ms", "concentric_duration"
|
|
522
|
+
)
|
|
466
523
|
|
|
467
524
|
if (
|
|
468
|
-
|
|
469
|
-
or
|
|
470
|
-
or
|
|
525
|
+
flight_time_raw is None
|
|
526
|
+
or concentric_duration_raw is None
|
|
527
|
+
or concentric_duration_raw == 0
|
|
471
528
|
):
|
|
472
529
|
return
|
|
473
530
|
|
|
531
|
+
# Convert to seconds if needed
|
|
532
|
+
if flight_time_raw < 10: # Likely in seconds
|
|
533
|
+
flight_time = flight_time_raw
|
|
534
|
+
else: # In milliseconds
|
|
535
|
+
flight_time = flight_time_raw / 1000.0
|
|
536
|
+
|
|
537
|
+
if concentric_duration_raw < 10: # Likely in seconds
|
|
538
|
+
concentric_duration = concentric_duration_raw
|
|
539
|
+
else: # In milliseconds
|
|
540
|
+
concentric_duration = concentric_duration_raw / 1000.0
|
|
541
|
+
|
|
474
542
|
rsi = flight_time / concentric_duration
|
|
475
543
|
result.rsi = rsi
|
|
476
544
|
|
|
@@ -513,8 +581,8 @@ class CMJMetricsValidator(MetricsValidator):
|
|
|
513
581
|
self, metrics: dict, result: CMJValidationResult
|
|
514
582
|
) -> None:
|
|
515
583
|
"""Check countermovement depth to jump height ratio."""
|
|
516
|
-
depth = metrics.get("
|
|
517
|
-
jump_height = metrics.get("
|
|
584
|
+
depth = metrics.get("countermovement_depth_m")
|
|
585
|
+
jump_height = metrics.get("jump_height_m")
|
|
518
586
|
|
|
519
587
|
if (
|
|
520
588
|
depth is None or jump_height is None or depth < 0.05
|
|
@@ -557,12 +625,14 @@ class CMJMetricsValidator(MetricsValidator):
|
|
|
557
625
|
self, metrics: dict, result: CMJValidationResult
|
|
558
626
|
) -> None:
|
|
559
627
|
"""Check contact time to countermovement depth ratio."""
|
|
560
|
-
|
|
561
|
-
depth = metrics.get("
|
|
628
|
+
contact_ms = metrics.get("concentric_duration_ms")
|
|
629
|
+
depth = metrics.get("countermovement_depth_m")
|
|
562
630
|
|
|
563
|
-
if
|
|
631
|
+
if contact_ms is None or depth is None or depth < 0.05:
|
|
564
632
|
return
|
|
565
633
|
|
|
634
|
+
# Convert ms to seconds for ratio calculation
|
|
635
|
+
contact = contact_ms / 1000.0
|
|
566
636
|
ratio = contact / depth
|
|
567
637
|
result.contact_depth_ratio = ratio
|
|
568
638
|
|
|
@@ -323,7 +323,10 @@ def estimate_athlete_profile(
|
|
|
323
323
|
Returns:
|
|
324
324
|
Estimated AthleteProfile
|
|
325
325
|
"""
|
|
326
|
-
|
|
326
|
+
# Support both nested "data" structure and flat structure
|
|
327
|
+
# Extract with unit suffix as used in serialization, or without suffix (legacy)
|
|
328
|
+
data = metrics_dict.get("data", metrics_dict)
|
|
329
|
+
jump_height = data.get("jump_height_m") or data.get("jump_height", 0)
|
|
327
330
|
|
|
328
331
|
if jump_height < 0.20:
|
|
329
332
|
return AthleteProfile.ELDERLY
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kinemotion
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.36.0
|
|
4
4
|
Summary: Video-based kinematic analysis for athletic performance
|
|
5
5
|
Project-URL: Homepage, https://github.com/feniix/kinemotion
|
|
6
6
|
Project-URL: Repository, https://github.com/feniix/kinemotion
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
kinemotion/__init__.py,sha256=sxdDOekOrIgjxm842gy-6zfq7OWmGl9ShJtXCm4JI7c,723
|
|
2
|
-
kinemotion/api.py,sha256=
|
|
2
|
+
kinemotion/api.py,sha256=nbDbyzhXIMA04tGqKPH8R0fR66zgtu14x6NgWroy_QU,39471
|
|
3
3
|
kinemotion/cli.py,sha256=cqYV_7URH0JUDy1VQ_EDLv63FmNO4Ns20m6s1XAjiP4,464
|
|
4
4
|
kinemotion/cmj/__init__.py,sha256=Ynv0-Oco4I3Y1Ubj25m3h9h2XFqeNwpAewXmAYOmwfU,127
|
|
5
5
|
kinemotion/cmj/analysis.py,sha256=OfNTMLPwZIRYbX-Yd8jgZ-7pqnHRz7L2bWAHVYFsQ60,18955
|
|
@@ -7,8 +7,8 @@ kinemotion/cmj/cli.py,sha256=Mj2h9It1jVjAauvtCxfLWTRijj7zbYhxZuebhw2Zz6w,10828
|
|
|
7
7
|
kinemotion/cmj/debug_overlay.py,sha256=fXmWoHhqMLGo4vTtB6Ezs3yLUDOLw63zLIgU2gFlJQU,15892
|
|
8
8
|
kinemotion/cmj/joint_angles.py,sha256=HmheIEiKcQz39cRezk4h-htorOhGNPsqKIR9RsAEKts,9960
|
|
9
9
|
kinemotion/cmj/kinematics.py,sha256=qRBe87NkX-7HQTQ8RoF-EpvfcffgP5vycJJRrxpHboc,10307
|
|
10
|
-
kinemotion/cmj/metrics_validator.py,sha256=
|
|
11
|
-
kinemotion/cmj/validation_bounds.py,sha256=
|
|
10
|
+
kinemotion/cmj/metrics_validator.py,sha256=V_fmlczYH06SBtwqESv-IfGi3wDsIy3RQbd7VwOyNo0,31359
|
|
11
|
+
kinemotion/cmj/validation_bounds.py,sha256=9ZTo68fl3ooyWjXXyTMRLpK9tFANa_rQf3oHhq7iQGE,11995
|
|
12
12
|
kinemotion/core/__init__.py,sha256=HsqolRa60cW3vrG8F9Lvr9WvWcs5hCmsTzSgo7imi-4,1278
|
|
13
13
|
kinemotion/core/auto_tuning.py,sha256=wtCUMOhBChVJNXfEeku3GCMW4qED6MF-O_mv2sPTiVQ,11324
|
|
14
14
|
kinemotion/core/cli_utils.py,sha256=zbnifPhD-OYofJioeYfJtshuWcl8OAEWtqCGVF4ctAI,7966
|
|
@@ -30,8 +30,8 @@ kinemotion/dropjump/kinematics.py,sha256=yB4ws4VG59SUGcw1J-uXfDFfCMXBdzRh5C4jo0o
|
|
|
30
30
|
kinemotion/dropjump/metrics_validator.py,sha256=sx4RodHpeiW8_PRB0GUJvkUWto1Ard1Dvrc9z8eKk7M,9351
|
|
31
31
|
kinemotion/dropjump/validation_bounds.py,sha256=5b4I3CKPybuvrbn-nP5yCcGF_sH4Vtyw3a5AWWvWnBk,4645
|
|
32
32
|
kinemotion/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
33
|
-
kinemotion-0.
|
|
34
|
-
kinemotion-0.
|
|
35
|
-
kinemotion-0.
|
|
36
|
-
kinemotion-0.
|
|
37
|
-
kinemotion-0.
|
|
33
|
+
kinemotion-0.36.0.dist-info/METADATA,sha256=oDRVIjwO8LiGTAOuFxGM5AVQAejcl5liHy3WM95eq3c,26020
|
|
34
|
+
kinemotion-0.36.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
35
|
+
kinemotion-0.36.0.dist-info/entry_points.txt,sha256=zaqnAnjLvcdrk1Qvj5nvXZCZ2gp0prS7it1zTJygcIY,50
|
|
36
|
+
kinemotion-0.36.0.dist-info/licenses/LICENSE,sha256=KZajvqsHw0NoOHOi2q0FZ4NBe9HdV6oey-IPYAtHXfg,1088
|
|
37
|
+
kinemotion-0.36.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|