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
|
@@ -10,10 +10,10 @@ from ..core.smoothing import compute_acceleration_from_derivative
|
|
|
10
10
|
from ..core.timing import NULL_TIMER, Timer
|
|
11
11
|
from .analysis import (
|
|
12
12
|
ContactState,
|
|
13
|
-
|
|
14
|
-
_find_interpolated_phase_transitions_with_curvature, # pyright: ignore[reportPrivateUsage]
|
|
15
|
-
_find_landing_from_acceleration, # pyright: ignore[reportPrivateUsage]
|
|
13
|
+
detect_drop_start,
|
|
16
14
|
find_contact_phases,
|
|
15
|
+
find_interpolated_phase_transitions_with_curvature,
|
|
16
|
+
find_landing_from_acceleration,
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
if TYPE_CHECKING:
|
|
@@ -159,7 +159,7 @@ def _determine_drop_start_frame(
|
|
|
159
159
|
"""
|
|
160
160
|
if drop_start_frame is None:
|
|
161
161
|
# Auto-detect where drop jump actually starts (skip initial stationary period)
|
|
162
|
-
return
|
|
162
|
+
return detect_drop_start(
|
|
163
163
|
foot_y_positions,
|
|
164
164
|
fps,
|
|
165
165
|
min_stationary_duration=0.5,
|
|
@@ -226,106 +226,14 @@ def _compute_robust_phase_position(
|
|
|
226
226
|
return float(np.median(window_positions))
|
|
227
227
|
|
|
228
228
|
|
|
229
|
-
def _detect_drop_jump_air_first_pattern(
|
|
230
|
-
air_phases_indexed: list[tuple[int, int, int]],
|
|
231
|
-
ground_phases: list[tuple[int, int, int]],
|
|
232
|
-
) -> tuple[int, int] | None:
|
|
233
|
-
"""Detect drop jump using air-first pattern (box + drop classified as IN_AIR).
|
|
234
|
-
|
|
235
|
-
Pattern: IN_AIR(box+drop) → ON_GROUND(contact) → IN_AIR(flight) → ON_GROUND(land)
|
|
236
|
-
|
|
237
|
-
Args:
|
|
238
|
-
air_phases_indexed: Air phases with indices
|
|
239
|
-
ground_phases: Ground phases with indices
|
|
240
|
-
|
|
241
|
-
Returns:
|
|
242
|
-
(contact_start, contact_end) if drop jump detected, None otherwise
|
|
243
|
-
"""
|
|
244
|
-
if not air_phases_indexed or len(ground_phases) < 2:
|
|
245
|
-
return None
|
|
246
|
-
|
|
247
|
-
_, _, first_air_idx = air_phases_indexed[0]
|
|
248
|
-
first_ground_start, first_ground_end, first_ground_idx = ground_phases[0]
|
|
249
|
-
|
|
250
|
-
# Drop jump: first phase is IN_AIR (index 0), second phase is ground (index 1)
|
|
251
|
-
if first_air_idx != 0 or first_ground_idx != 1:
|
|
252
|
-
return None
|
|
253
|
-
|
|
254
|
-
# Check for flight phase after contact
|
|
255
|
-
air_after_contact = [i for _, _, i in air_phases_indexed if i > first_ground_idx]
|
|
256
|
-
if not air_after_contact:
|
|
257
|
-
return None
|
|
258
|
-
|
|
259
|
-
return first_ground_start, first_ground_end
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
def _detect_drop_jump_height_pattern(
|
|
263
|
-
air_phases_indexed: list[tuple[int, int, int]],
|
|
264
|
-
ground_phases: list[tuple[int, int, int]],
|
|
265
|
-
foot_y_positions: NDArray[np.float64],
|
|
266
|
-
) -> tuple[int, int] | None:
|
|
267
|
-
"""Detect drop jump using height comparison (box detected as ground).
|
|
268
|
-
|
|
269
|
-
Legacy detection: first ground is on elevated box (lower y value).
|
|
270
|
-
|
|
271
|
-
Args:
|
|
272
|
-
air_phases_indexed: Air phases with indices
|
|
273
|
-
ground_phases: Ground phases with indices
|
|
274
|
-
foot_y_positions: Vertical position array
|
|
275
|
-
|
|
276
|
-
Returns:
|
|
277
|
-
(contact_start, contact_end) if drop jump detected, None otherwise
|
|
278
|
-
"""
|
|
279
|
-
if not air_phases_indexed or len(ground_phases) < 2:
|
|
280
|
-
return None
|
|
281
|
-
|
|
282
|
-
_, _, first_air_idx = air_phases_indexed[0]
|
|
283
|
-
first_ground_start, first_ground_end, first_ground_idx = ground_phases[0]
|
|
284
|
-
|
|
285
|
-
# This pattern: first ground is before first air (athlete on box)
|
|
286
|
-
if first_ground_idx >= first_air_idx:
|
|
287
|
-
return None
|
|
288
|
-
|
|
289
|
-
ground_after_air = [
|
|
290
|
-
(start, end, idx) for start, end, idx in ground_phases if idx > first_air_idx
|
|
291
|
-
]
|
|
292
|
-
if not ground_after_air:
|
|
293
|
-
return None
|
|
294
|
-
|
|
295
|
-
first_ground_y = _compute_robust_phase_position(
|
|
296
|
-
foot_y_positions, first_ground_start, first_ground_end
|
|
297
|
-
)
|
|
298
|
-
second_ground_start, second_ground_end, _ = ground_after_air[0]
|
|
299
|
-
second_ground_y = _compute_robust_phase_position(
|
|
300
|
-
foot_y_positions, second_ground_start, second_ground_end
|
|
301
|
-
)
|
|
302
|
-
|
|
303
|
-
# If second ground is significantly lower (>7% of frame), it's a drop jump
|
|
304
|
-
height_diff = second_ground_y - first_ground_y
|
|
305
|
-
if height_diff <= 0.07:
|
|
306
|
-
return None
|
|
307
|
-
|
|
308
|
-
return second_ground_start, second_ground_end
|
|
309
|
-
|
|
310
|
-
|
|
311
229
|
def _identify_main_contact_phase(
|
|
312
|
-
phases: list[tuple[int, int, ContactState]],
|
|
230
|
+
phases: list[tuple[int, int, ContactState]],
|
|
313
231
|
ground_phases: list[tuple[int, int, int]],
|
|
314
232
|
air_phases_indexed: list[tuple[int, int, int]],
|
|
315
233
|
foot_y_positions: NDArray[np.float64],
|
|
316
234
|
) -> tuple[int, int, bool]:
|
|
317
235
|
"""Identify the main contact phase and determine if it's a drop jump.
|
|
318
236
|
|
|
319
|
-
Drop jump detection strategy:
|
|
320
|
-
1. With position-based filtering, box period is classified as IN_AIR
|
|
321
|
-
2. Pattern: IN_AIR(box+drop) → ON_GROUND(contact) → IN_AIR(flight) → ON_GROUND(land)
|
|
322
|
-
3. The FIRST ground phase is the contact phase (before the flight)
|
|
323
|
-
4. The LAST ground phase is the landing (after the flight)
|
|
324
|
-
|
|
325
|
-
The key differentiator from regular jump:
|
|
326
|
-
- Drop jump: starts with IN_AIR, has 2+ ground phases with air between them
|
|
327
|
-
- Regular jump: starts with ON_GROUND, may have multiple phases
|
|
328
|
-
|
|
329
237
|
Args:
|
|
330
238
|
phases: All phase tuples
|
|
331
239
|
ground_phases: Ground phases with indices
|
|
@@ -335,21 +243,46 @@ def _identify_main_contact_phase(
|
|
|
335
243
|
Returns:
|
|
336
244
|
Tuple of (contact_start, contact_end, is_drop_jump)
|
|
337
245
|
"""
|
|
338
|
-
#
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
246
|
+
# Initialize with first ground phase as fallback
|
|
247
|
+
contact_start, contact_end = ground_phases[0][0], ground_phases[0][1]
|
|
248
|
+
is_drop_jump = False
|
|
249
|
+
|
|
250
|
+
# Detect if this is a drop jump or regular jump
|
|
251
|
+
if air_phases_indexed and len(ground_phases) >= 2:
|
|
252
|
+
first_ground_start, first_ground_end, first_ground_idx = ground_phases[0]
|
|
253
|
+
first_air_idx = air_phases_indexed[0][2]
|
|
254
|
+
|
|
255
|
+
# Find ground phase after first air phase
|
|
256
|
+
ground_after_air = [
|
|
257
|
+
(start, end, idx) for start, end, idx in ground_phases if idx > first_air_idx
|
|
258
|
+
]
|
|
259
|
+
|
|
260
|
+
if ground_after_air and first_ground_idx < first_air_idx:
|
|
261
|
+
# Check if first ground is at higher elevation (lower y) than
|
|
262
|
+
# ground after air using robust temporal averaging
|
|
263
|
+
first_ground_y = _compute_robust_phase_position(
|
|
264
|
+
foot_y_positions, first_ground_start, first_ground_end
|
|
265
|
+
)
|
|
266
|
+
second_ground_start, second_ground_end, _ = ground_after_air[0]
|
|
267
|
+
second_ground_y = _compute_robust_phase_position(
|
|
268
|
+
foot_y_positions, second_ground_start, second_ground_end
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# If first ground is significantly higher (>7% of frame), it's a drop jump
|
|
272
|
+
# Increased from 0.05 to 0.07 with 11-frame temporal averaging
|
|
273
|
+
# for reproducibility (balances detection sensitivity with noise robustness)
|
|
274
|
+
# Note: MediaPipe has inherent non-determinism (Google issue #3945)
|
|
275
|
+
if second_ground_y - first_ground_y > 0.07:
|
|
276
|
+
is_drop_jump = True
|
|
277
|
+
contact_start, contact_end = second_ground_start, second_ground_end
|
|
278
|
+
|
|
279
|
+
if not is_drop_jump:
|
|
280
|
+
# Regular jump: use longest ground contact phase
|
|
281
|
+
contact_start, contact_end = max(
|
|
282
|
+
[(s, e) for s, e, _ in ground_phases], key=lambda p: p[1] - p[0]
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
return contact_start, contact_end, is_drop_jump
|
|
353
286
|
|
|
354
287
|
|
|
355
288
|
def _find_precise_phase_timing(
|
|
@@ -384,30 +317,6 @@ def _find_precise_phase_timing(
|
|
|
384
317
|
return contact_start_frac, contact_end_frac
|
|
385
318
|
|
|
386
319
|
|
|
387
|
-
def _find_landing_from_phases(
|
|
388
|
-
phases: list[tuple[int, int, ContactState]],
|
|
389
|
-
flight_start: int,
|
|
390
|
-
) -> int | None:
|
|
391
|
-
"""Find landing frame from phase detection.
|
|
392
|
-
|
|
393
|
-
Looks for the first ON_GROUND phase that starts after the flight_start frame.
|
|
394
|
-
This represents the first ground contact after the reactive jump.
|
|
395
|
-
|
|
396
|
-
Args:
|
|
397
|
-
phases: List of (start, end, state) phase tuples
|
|
398
|
-
flight_start: Frame where flight begins (takeoff)
|
|
399
|
-
|
|
400
|
-
Returns:
|
|
401
|
-
Landing frame (start of landing phase), or None if not found
|
|
402
|
-
"""
|
|
403
|
-
for start, _, state in phases:
|
|
404
|
-
if state == ContactState.ON_GROUND and start > flight_start:
|
|
405
|
-
# Found the landing phase - return its start frame
|
|
406
|
-
return start
|
|
407
|
-
|
|
408
|
-
return None
|
|
409
|
-
|
|
410
|
-
|
|
411
320
|
def _analyze_flight_phase(
|
|
412
321
|
metrics: DropJumpMetrics,
|
|
413
322
|
phases: list[tuple[int, int, ContactState]],
|
|
@@ -436,20 +345,22 @@ def _analyze_flight_phase(
|
|
|
436
345
|
# Find takeoff frame (end of ground contact)
|
|
437
346
|
flight_start = contact_end
|
|
438
347
|
|
|
439
|
-
#
|
|
440
|
-
|
|
441
|
-
|
|
348
|
+
# Compute accelerations for landing detection
|
|
349
|
+
accelerations = compute_acceleration_from_derivative(
|
|
350
|
+
foot_y_positions, window_length=smoothing_window, polyorder=polyorder
|
|
351
|
+
)
|
|
442
352
|
|
|
443
|
-
#
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
flight_end = _find_landing_from_acceleration(
|
|
449
|
-
foot_y_positions, accelerations, flight_start, fps
|
|
450
|
-
)
|
|
353
|
+
# Use acceleration-based landing detection (like CMJ)
|
|
354
|
+
# This finds the actual ground impact, not just when velocity drops
|
|
355
|
+
flight_end = find_landing_from_acceleration(
|
|
356
|
+
foot_y_positions, accelerations, flight_start, fps, search_duration=0.7
|
|
357
|
+
)
|
|
451
358
|
|
|
452
|
-
#
|
|
359
|
+
# Store integer frame indices
|
|
360
|
+
metrics.flight_start_frame = flight_start
|
|
361
|
+
metrics.flight_end_frame = flight_end
|
|
362
|
+
|
|
363
|
+
# Find precise sub-frame timing for takeoff
|
|
453
364
|
flight_start_frac = float(flight_start)
|
|
454
365
|
flight_end_frac = float(flight_end)
|
|
455
366
|
|
|
@@ -462,20 +373,6 @@ def _analyze_flight_phase(
|
|
|
462
373
|
flight_start_frac = end_frac
|
|
463
374
|
break
|
|
464
375
|
|
|
465
|
-
# Find interpolated landing (start of landing ON_GROUND phase)
|
|
466
|
-
for start_frac, _, state in interpolated_phases:
|
|
467
|
-
if state == ContactState.ON_GROUND and int(start_frac) >= flight_end - 2:
|
|
468
|
-
flight_end_frac = start_frac
|
|
469
|
-
break
|
|
470
|
-
|
|
471
|
-
# Refine landing frame using floor of interpolated value
|
|
472
|
-
# This compensates for velocity-based detection being ~1-2 frames late
|
|
473
|
-
refined_flight_end = int(np.floor(flight_end_frac))
|
|
474
|
-
|
|
475
|
-
# Store integer frame indices (refined using interpolated values)
|
|
476
|
-
metrics.flight_start_frame = flight_start
|
|
477
|
-
metrics.flight_end_frame = refined_flight_end
|
|
478
|
-
|
|
479
376
|
# Calculate flight time
|
|
480
377
|
flight_frames_precise = flight_end_frac - flight_start_frac
|
|
481
378
|
metrics.flight_time = flight_frames_precise / fps
|
|
@@ -559,7 +456,7 @@ def calculate_drop_jump_metrics(
|
|
|
559
456
|
# Find contact phases
|
|
560
457
|
with timer.measure("dj_find_phases"):
|
|
561
458
|
phases = find_contact_phases(contact_states)
|
|
562
|
-
interpolated_phases =
|
|
459
|
+
interpolated_phases = find_interpolated_phase_transitions_with_curvature(
|
|
563
460
|
foot_y_positions,
|
|
564
461
|
contact_states,
|
|
565
462
|
velocity_threshold,
|
|
@@ -600,22 +497,15 @@ def calculate_drop_jump_metrics(
|
|
|
600
497
|
phases, ground_phases, air_phases_indexed, foot_y_positions
|
|
601
498
|
)
|
|
602
499
|
|
|
603
|
-
#
|
|
500
|
+
# Store integer frame indices
|
|
501
|
+
metrics.contact_start_frame = contact_start
|
|
502
|
+
metrics.contact_end_frame = contact_end
|
|
503
|
+
|
|
504
|
+
# Find precise timing for contact phase
|
|
604
505
|
contact_start_frac, contact_end_frac = _find_precise_phase_timing(
|
|
605
506
|
contact_start, contact_end, interpolated_phases
|
|
606
507
|
)
|
|
607
508
|
|
|
608
|
-
# Refine contact_start using floor of interpolated value
|
|
609
|
-
# This compensates for velocity-based detection being ~1-2 frames late
|
|
610
|
-
# because velocity settles AFTER initial impact. Using floor() biases
|
|
611
|
-
# toward earlier detection, matching the moment of first ground contact.
|
|
612
|
-
refined_contact_start = int(np.floor(contact_start_frac))
|
|
613
|
-
|
|
614
|
-
# Store integer frame indices (refined start, raw end)
|
|
615
|
-
# Contact end (takeoff) uses raw value as velocity-based detection is accurate
|
|
616
|
-
metrics.contact_start_frame = refined_contact_start
|
|
617
|
-
metrics.contact_end_frame = contact_end
|
|
618
|
-
|
|
619
509
|
# Calculate ground contact time
|
|
620
510
|
contact_frames_precise = contact_end_frac - contact_start_frac
|
|
621
511
|
metrics.ground_contact_time = contact_frames_precise / fps
|
|
@@ -14,7 +14,7 @@ from kinemotion.core.validation import (
|
|
|
14
14
|
MetricsValidator,
|
|
15
15
|
ValidationResult,
|
|
16
16
|
)
|
|
17
|
-
from kinemotion.
|
|
17
|
+
from kinemotion.dropjump.validation_bounds import (
|
|
18
18
|
DropJumpBounds,
|
|
19
19
|
estimate_athlete_profile,
|
|
20
20
|
)
|
|
@@ -114,37 +114,63 @@ class DropJumpMetricsValidator(MetricsValidator):
|
|
|
114
114
|
) -> None:
|
|
115
115
|
"""Validate contact time."""
|
|
116
116
|
contact_time_s = contact_time_ms / 1000.0
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
117
|
+
bounds = DropJumpBounds.CONTACT_TIME
|
|
118
|
+
|
|
119
|
+
if not bounds.is_physically_possible(contact_time_s):
|
|
120
|
+
result.add_error(
|
|
121
|
+
"contact_time",
|
|
122
|
+
f"Contact time {contact_time_s:.3f}s physically impossible",
|
|
123
|
+
value=contact_time_s,
|
|
124
|
+
bounds=(bounds.absolute_min, bounds.absolute_max),
|
|
125
|
+
)
|
|
126
|
+
elif result.athlete_profile and not bounds.contains(
|
|
127
|
+
contact_time_s, result.athlete_profile
|
|
128
|
+
):
|
|
129
|
+
profile_name = result.athlete_profile.value
|
|
130
|
+
result.add_warning(
|
|
131
|
+
"contact_time",
|
|
132
|
+
f"Contact time {contact_time_s:.3f}s unusual for {profile_name} athlete",
|
|
133
|
+
value=contact_time_s,
|
|
134
|
+
)
|
|
125
135
|
|
|
126
136
|
def _check_flight_time(self, flight_time_ms: float, result: DropJumpValidationResult) -> None:
|
|
127
137
|
"""Validate flight time."""
|
|
128
138
|
flight_time_s = flight_time_ms / 1000.0
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
139
|
+
bounds = DropJumpBounds.FLIGHT_TIME
|
|
140
|
+
|
|
141
|
+
if not bounds.is_physically_possible(flight_time_s):
|
|
142
|
+
result.add_error(
|
|
143
|
+
"flight_time",
|
|
144
|
+
f"Flight time {flight_time_s:.3f}s physically impossible",
|
|
145
|
+
value=flight_time_s,
|
|
146
|
+
bounds=(bounds.absolute_min, bounds.absolute_max),
|
|
147
|
+
)
|
|
148
|
+
elif result.athlete_profile and not bounds.contains(flight_time_s, result.athlete_profile):
|
|
149
|
+
profile_name = result.athlete_profile.value
|
|
150
|
+
result.add_warning(
|
|
151
|
+
"flight_time",
|
|
152
|
+
f"Flight time {flight_time_s:.3f}s unusual for {profile_name} athlete",
|
|
153
|
+
value=flight_time_s,
|
|
154
|
+
)
|
|
137
155
|
|
|
138
156
|
def _check_jump_height(self, jump_height_m: float, result: DropJumpValidationResult) -> None:
|
|
139
157
|
"""Validate jump height."""
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
158
|
+
bounds = DropJumpBounds.JUMP_HEIGHT
|
|
159
|
+
|
|
160
|
+
if not bounds.is_physically_possible(jump_height_m):
|
|
161
|
+
result.add_error(
|
|
162
|
+
"jump_height",
|
|
163
|
+
f"Jump height {jump_height_m:.3f}m physically impossible",
|
|
164
|
+
value=jump_height_m,
|
|
165
|
+
bounds=(bounds.absolute_min, bounds.absolute_max),
|
|
166
|
+
)
|
|
167
|
+
elif result.athlete_profile and not bounds.contains(jump_height_m, result.athlete_profile):
|
|
168
|
+
profile_name = result.athlete_profile.value
|
|
169
|
+
result.add_warning(
|
|
170
|
+
"jump_height",
|
|
171
|
+
f"Jump height {jump_height_m:.3f}m unusual for {profile_name} athlete",
|
|
172
|
+
value=jump_height_m,
|
|
173
|
+
)
|
|
148
174
|
|
|
149
175
|
def _check_rsi(
|
|
150
176
|
self,
|
|
@@ -124,7 +124,7 @@ def _classify_combined_score(combined_score: float) -> AthleteProfile:
|
|
|
124
124
|
return AthleteProfile.ELITE
|
|
125
125
|
|
|
126
126
|
|
|
127
|
-
def estimate_athlete_profile(metrics: MetricsDict,
|
|
127
|
+
def estimate_athlete_profile(metrics: MetricsDict, gender: str | None = None) -> AthleteProfile:
|
|
128
128
|
"""Estimate athlete profile from drop jump metrics.
|
|
129
129
|
|
|
130
130
|
Uses jump_height and contact_time to classify athlete level.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kinemotion
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.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
|
|
@@ -51,7 +51,6 @@ Description-Content-Type: text/markdown
|
|
|
51
51
|
|
|
52
52
|
- **Drop Jump**: Ground contact time, flight time, reactive strength index
|
|
53
53
|
- **Counter Movement Jump (CMJ)**: Jump height, flight time, countermovement depth, triple extension biomechanics
|
|
54
|
-
- **Squat Jump (SJ)**: Pure concentric power, force production, requires athlete mass
|
|
55
54
|
|
|
56
55
|
## Features
|
|
57
56
|
|
|
@@ -84,14 +83,6 @@ Description-Content-Type: text/markdown
|
|
|
84
83
|
- **Metrics**: Jump height, flight time, countermovement depth, eccentric/concentric durations
|
|
85
84
|
- **Validated accuracy**: 50.6cm jump (±1 frame precision)
|
|
86
85
|
|
|
87
|
-
### Squat Jump (SJ) Analysis
|
|
88
|
-
|
|
89
|
-
- **Static squat start** - pure concentric power test (no countermovement)
|
|
90
|
-
- **Power/Force calculations** - Sayers regression (R² = 0.87, \<1% error vs force plates)
|
|
91
|
-
- **Mass required** - athlete body weight needed for kinetic calculations
|
|
92
|
-
- **Metrics**: Jump height, flight time, squat hold/concentric durations, peak/mean power, peak force
|
|
93
|
-
- **Phase detection**: Squat hold → concentric → flight → landing
|
|
94
|
-
|
|
95
86
|
## ⚠️ Validation Status
|
|
96
87
|
|
|
97
88
|
**Current Status:** Pre-validation (not validated against force plates or motion capture systems)
|
|
@@ -237,22 +228,7 @@ kinemotion cmj-analyze video.mp4
|
|
|
237
228
|
kinemotion cmj-analyze video.mp4 --output debug.mp4
|
|
238
229
|
```
|
|
239
230
|
|
|
240
|
-
###
|
|
241
|
-
|
|
242
|
-
Analyzes pure concentric power production:
|
|
243
|
-
|
|
244
|
-
```bash
|
|
245
|
-
# Mass is required for power/force calculations
|
|
246
|
-
kinemotion sj-analyze video.mp4 --mass 75.0
|
|
247
|
-
|
|
248
|
-
# Complete analysis with all outputs
|
|
249
|
-
kinemotion sj-analyze video.mp4 --mass 75.0 \
|
|
250
|
-
--output debug.mp4 \
|
|
251
|
-
--json-output results.json \
|
|
252
|
-
--verbose
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
### Common Options (All Jump Types)
|
|
231
|
+
### Common Options (Both Jump Types)
|
|
256
232
|
|
|
257
233
|
```bash
|
|
258
234
|
# Save metrics to JSON
|
|
@@ -414,31 +390,6 @@ metrics = process_cmj_video("video.mp4", output_video="debug.mp4")
|
|
|
414
390
|
# - Phase-coded visualization
|
|
415
391
|
```
|
|
416
392
|
|
|
417
|
-
### Squat Jump (SJ) API
|
|
418
|
-
|
|
419
|
-
```python
|
|
420
|
-
from kinemotion import process_sj_video
|
|
421
|
-
|
|
422
|
-
# Mass is required for power/force calculations
|
|
423
|
-
metrics = process_sj_video(
|
|
424
|
-
video_path="athlete_sj.mp4",
|
|
425
|
-
mass_kg=75.0, # Required: athlete body mass
|
|
426
|
-
quality="balanced",
|
|
427
|
-
verbose=True
|
|
428
|
-
)
|
|
429
|
-
|
|
430
|
-
# Access results
|
|
431
|
-
print(f"Jump height: {metrics.jump_height:.3f}m")
|
|
432
|
-
print(f"Squat hold: {metrics.squat_hold_duration*1000:.1f}ms")
|
|
433
|
-
print(f"Concentric: {metrics.concentric_duration*1000:.1f}ms")
|
|
434
|
-
|
|
435
|
-
# Power/force (only available if mass provided)
|
|
436
|
-
if metrics.peak_power:
|
|
437
|
-
print(f"Peak power: {metrics.peak_power:.0f}W")
|
|
438
|
-
print(f"Mean power: {metrics.mean_power:.0f}W")
|
|
439
|
-
print(f"Peak force: {metrics.peak_force:.0f}N")
|
|
440
|
-
```
|
|
441
|
-
|
|
442
393
|
### CSV Export Example
|
|
443
394
|
|
|
444
395
|
```python
|
|
@@ -625,9 +576,9 @@ The debug video includes:
|
|
|
625
576
|
**Solutions**:
|
|
626
577
|
|
|
627
578
|
1. **Check video quality**: Ensure the athlete is clearly visible in profile view
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
579
|
+
1. **Increase smoothing**: Use `--smoothing-window 7` or higher
|
|
580
|
+
1. **Adjust detection confidence**: Try `--detection-confidence 0.6` or `--tracking-confidence 0.6`
|
|
581
|
+
1. **Generate debug video**: Use `--output` to visualize what's being tracked
|
|
631
582
|
|
|
632
583
|
### No Pose Detected
|
|
633
584
|
|
|
@@ -636,9 +587,9 @@ The debug video includes:
|
|
|
636
587
|
**Solutions**:
|
|
637
588
|
|
|
638
589
|
1. **Verify video format**: OpenCV must be able to read the video
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
590
|
+
1. **Check framing**: Ensure full body is visible in side view
|
|
591
|
+
1. **Lower confidence thresholds**: Try `--detection-confidence 0.3 --tracking-confidence 0.3`
|
|
592
|
+
1. **Test video playback**: Verify video opens correctly with standard video players
|
|
642
593
|
|
|
643
594
|
### Incorrect Contact Detection
|
|
644
595
|
|
|
@@ -647,11 +598,11 @@ The debug video includes:
|
|
|
647
598
|
**Solutions**:
|
|
648
599
|
|
|
649
600
|
1. **Generate debug video**: Visualize contact states to diagnose the issue
|
|
650
|
-
|
|
601
|
+
1. **Adjust velocity threshold**:
|
|
651
602
|
- If missing contacts: decrease to `--velocity-threshold 0.01`
|
|
652
603
|
- If false contacts: increase to `--velocity-threshold 0.03`
|
|
653
|
-
|
|
654
|
-
|
|
604
|
+
1. **Adjust minimum frames**: `--min-contact-frames 5` for longer required contact
|
|
605
|
+
1. **Check visibility**: Lower `--visibility-threshold 0.3` if feet are partially obscured
|
|
655
606
|
|
|
656
607
|
### Jump Height Seems Wrong
|
|
657
608
|
|
|
@@ -660,9 +611,9 @@ The debug video includes:
|
|
|
660
611
|
**Solutions**:
|
|
661
612
|
|
|
662
613
|
1. **Check video quality**: Ensure video frame rate is adequate (30fps or higher recommended)
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
614
|
+
1. **Verify flight time detection**: Check `flight_start_frame` and `flight_end_frame` in JSON
|
|
615
|
+
1. **Compare measurements**: JSON output includes both `jump_height_m` (primary) and `jump_height_kinematic_m` (kinematic-only)
|
|
616
|
+
1. **Check for drop jump detection**: If doing a drop jump, ensure first phase is elevated enough (>5% of frame height)
|
|
666
617
|
|
|
667
618
|
### Video Codec Issues
|
|
668
619
|
|
|
@@ -671,30 +622,30 @@ The debug video includes:
|
|
|
671
622
|
**Solutions**:
|
|
672
623
|
|
|
673
624
|
1. **Install additional codecs**: Ensure OpenCV has proper video codec support
|
|
674
|
-
|
|
675
|
-
|
|
625
|
+
1. **Try different output format**: Use `.avi` extension instead of `.mp4`
|
|
626
|
+
1. **Check output path**: Ensure write permissions for output directory
|
|
676
627
|
|
|
677
628
|
## How It Works
|
|
678
629
|
|
|
679
630
|
1. **Pose Tracking**: MediaPipe extracts 2D pose landmarks (foot points: ankles, heels, foot indices) from each frame
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
631
|
+
1. **Position Calculation**: Averages ankle, heel, and foot index positions to determine foot location
|
|
632
|
+
1. **Smoothing**: Savitzky-Golay filter reduces tracking jitter while preserving motion dynamics
|
|
633
|
+
1. **Contact Detection**: Analyzes vertical position velocity to identify ground contact vs. flight phases
|
|
634
|
+
1. **Phase Identification**: Finds continuous ground contact and flight periods
|
|
684
635
|
- Automatically detects drop jumps vs regular jumps
|
|
685
636
|
- For drop jumps: identifies box → drop → ground contact → jump sequence
|
|
686
|
-
|
|
637
|
+
1. **Sub-Frame Interpolation**: Estimates exact transition times between frames
|
|
687
638
|
- Uses Savitzky-Golay derivative for smooth velocity calculation
|
|
688
639
|
- Linear interpolation of velocity to find threshold crossings
|
|
689
640
|
- Achieves sub-millisecond timing precision (at 30fps: ±10ms vs ±33ms)
|
|
690
641
|
- Reduces timing error by 60-70% for contact and flight measurements
|
|
691
642
|
- Smoother velocity curves eliminate false threshold crossings
|
|
692
|
-
|
|
643
|
+
1. **Trajectory Curvature Analysis**: Refines transitions using acceleration patterns
|
|
693
644
|
- Computes second derivative (acceleration) from position trajectory
|
|
694
645
|
- Detects landing impact by acceleration spike
|
|
695
646
|
- Identifies takeoff by acceleration change patterns
|
|
696
647
|
- Provides independent validation and refinement of velocity-based detection
|
|
697
|
-
|
|
648
|
+
1. **Metric Calculation**:
|
|
698
649
|
- Ground contact time = contact phase duration (using fractional frames)
|
|
699
650
|
- Flight time = flight phase duration (using fractional frames)
|
|
700
651
|
- Jump height = kinematic estimate from flight time: (g × t²) / 8
|
|
@@ -744,9 +695,9 @@ uv run ruff check && uv run pyright && uv run pytest
|
|
|
744
695
|
Before committing code, ensure all checks pass:
|
|
745
696
|
|
|
746
697
|
1. Format with Black
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
698
|
+
1. Fix linting issues with ruff
|
|
699
|
+
1. Ensure type safety with pyright
|
|
700
|
+
1. Run all tests with pytest
|
|
750
701
|
|
|
751
702
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines and requirements, or [CLAUDE.md](CLAUDE.md) for detailed development guidelines.
|
|
752
703
|
|