kinemotion 0.28.0__py3-none-any.whl → 0.29.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 +1 -1
- kinemotion/cmj/analysis.py +43 -10
- kinemotion/cmj/joint_angles.py +23 -6
- kinemotion/core/cmj_metrics_validator.py +717 -0
- kinemotion/core/cmj_validation_bounds.py +380 -0
- {kinemotion-0.28.0.dist-info → kinemotion-0.29.0.dist-info}/METADATA +111 -49
- {kinemotion-0.28.0.dist-info → kinemotion-0.29.0.dist-info}/RECORD +10 -8
- {kinemotion-0.28.0.dist-info → kinemotion-0.29.0.dist-info}/WHEEL +0 -0
- {kinemotion-0.28.0.dist-info → kinemotion-0.29.0.dist-info}/entry_points.txt +0 -0
- {kinemotion-0.28.0.dist-info → kinemotion-0.29.0.dist-info}/licenses/LICENSE +0 -0
kinemotion/__init__.py
CHANGED
kinemotion/cmj/analysis.py
CHANGED
|
@@ -417,17 +417,32 @@ def find_interpolated_takeoff_landing(
|
|
|
417
417
|
def _find_takeoff_frame(
|
|
418
418
|
velocities: np.ndarray, peak_height_frame: int, fps: float
|
|
419
419
|
) -> float:
|
|
420
|
-
"""Find takeoff frame as peak upward velocity before peak height.
|
|
420
|
+
"""Find takeoff frame as peak upward velocity before peak height.
|
|
421
|
+
|
|
422
|
+
Robust detection: When velocities are nearly identical (flat), detects
|
|
423
|
+
the transition point rather than using argmin which is unstable.
|
|
424
|
+
"""
|
|
421
425
|
takeoff_search_start = max(0, peak_height_frame - int(fps * 0.35))
|
|
422
426
|
takeoff_search_end = peak_height_frame - 2
|
|
423
427
|
|
|
424
428
|
takeoff_velocities = velocities[takeoff_search_start:takeoff_search_end]
|
|
425
429
|
|
|
426
|
-
if len(takeoff_velocities)
|
|
430
|
+
if len(takeoff_velocities) == 0:
|
|
431
|
+
return float(peak_height_frame - int(fps * 0.3))
|
|
432
|
+
|
|
433
|
+
# Check if velocities are suspiciously identical (flat derivative = ambiguous)
|
|
434
|
+
vel_min = np.min(takeoff_velocities)
|
|
435
|
+
vel_max = np.max(takeoff_velocities)
|
|
436
|
+
vel_range = vel_max - vel_min
|
|
437
|
+
|
|
438
|
+
if vel_range < 1e-6:
|
|
439
|
+
# Velocities are essentially identical - algorithm is ambiguous
|
|
440
|
+
# Return the midpoint of the search window as a stable estimate
|
|
441
|
+
return float((takeoff_search_start + takeoff_search_end) / 2.0)
|
|
442
|
+
else:
|
|
443
|
+
# Velocities have variation - use argmin as before
|
|
427
444
|
peak_vel_idx = int(np.argmin(takeoff_velocities))
|
|
428
445
|
return float(takeoff_search_start + peak_vel_idx)
|
|
429
|
-
else:
|
|
430
|
-
return float(peak_height_frame - int(fps * 0.3))
|
|
431
446
|
|
|
432
447
|
|
|
433
448
|
def _find_lowest_frame(
|
|
@@ -454,17 +469,35 @@ def _find_lowest_frame(
|
|
|
454
469
|
def _find_landing_frame(
|
|
455
470
|
accelerations: np.ndarray, peak_height_frame: int, fps: float
|
|
456
471
|
) -> float:
|
|
457
|
-
"""Find landing frame after peak height.
|
|
472
|
+
"""Find landing frame after peak height after takeoff.
|
|
473
|
+
|
|
474
|
+
Detects landing by finding the minimum acceleration (impact) in a search
|
|
475
|
+
window after peak height. The window is extended to 1.0s to ensure all
|
|
476
|
+
realistic flight times are captured.
|
|
477
|
+
|
|
478
|
+
Robust detection: When accelerations are nearly flat, skips the impact
|
|
479
|
+
frames near peak height and looks for the actual landing signal.
|
|
480
|
+
"""
|
|
458
481
|
landing_search_start = peak_height_frame
|
|
459
|
-
|
|
482
|
+
# Search window extended to 1.0s to accommodate all realistic flight times
|
|
483
|
+
# (recreational: 0.25-0.65s, elite: 0.65-0.95s, max: 1.1s)
|
|
484
|
+
landing_search_end = min(len(accelerations), peak_height_frame + int(fps * 1.0))
|
|
460
485
|
landing_accelerations = accelerations[landing_search_start:landing_search_end]
|
|
461
486
|
|
|
462
|
-
if len(landing_accelerations)
|
|
463
|
-
landing_idx = int(np.argmin(landing_accelerations))
|
|
464
|
-
return float(landing_search_start + landing_idx)
|
|
465
|
-
else:
|
|
487
|
+
if len(landing_accelerations) == 0:
|
|
466
488
|
return float(peak_height_frame + int(fps * 0.3))
|
|
467
489
|
|
|
490
|
+
# Skip the first 2 frames after peak (often have unreliable acceleration)
|
|
491
|
+
# This avoids locking onto peak height acceleration instead of impact
|
|
492
|
+
skip_frames = 2
|
|
493
|
+
if len(landing_accelerations) > skip_frames:
|
|
494
|
+
landing_accelerations_filtered = landing_accelerations[skip_frames:]
|
|
495
|
+
landing_idx = int(np.argmin(landing_accelerations_filtered)) + skip_frames
|
|
496
|
+
else:
|
|
497
|
+
landing_idx = int(np.argmin(landing_accelerations))
|
|
498
|
+
|
|
499
|
+
return float(landing_search_start + landing_idx)
|
|
500
|
+
|
|
468
501
|
|
|
469
502
|
def _find_standing_end(velocities: np.ndarray, lowest_point: float) -> float | None:
|
|
470
503
|
"""Find end of standing phase before lowest point."""
|
kinemotion/cmj/joint_angles.py
CHANGED
|
@@ -68,11 +68,19 @@ def calculate_ankle_angle(
|
|
|
68
68
|
"""
|
|
69
69
|
Calculate ankle angle (dorsiflexion/plantarflexion).
|
|
70
70
|
|
|
71
|
-
Angle formed by:
|
|
71
|
+
Angle formed by: foot_index -> ankle -> knee (primary)
|
|
72
|
+
Falls back to heel -> ankle -> knee if foot_index visibility < 0.5
|
|
73
|
+
|
|
74
|
+
Measurements:
|
|
72
75
|
- 90° = neutral (foot perpendicular to shin)
|
|
73
76
|
- < 90° = dorsiflexion (toes up)
|
|
74
77
|
- > 90° = plantarflexion (toes down)
|
|
75
78
|
|
|
79
|
+
Technical note:
|
|
80
|
+
- foot_index (toe tip) is used for accurate plantarflexion measurement
|
|
81
|
+
- Heel is relatively static during push-off; toes (foot_index) actively plantarflex
|
|
82
|
+
- Expected range during CMJ: 80° (standing) -> 130°+ (plantarflex at takeoff)
|
|
83
|
+
|
|
76
84
|
Args:
|
|
77
85
|
landmarks: Pose landmarks dictionary
|
|
78
86
|
side: Which side to measure ("left" or "right")
|
|
@@ -82,23 +90,32 @@ def calculate_ankle_angle(
|
|
|
82
90
|
"""
|
|
83
91
|
prefix = "left_" if side == "left" else "right_"
|
|
84
92
|
|
|
93
|
+
foot_index_key = f"{prefix}foot_index"
|
|
85
94
|
heel_key = f"{prefix}heel"
|
|
86
95
|
ankle_key = f"{prefix}ankle"
|
|
87
96
|
knee_key = f"{prefix}knee"
|
|
88
97
|
|
|
89
|
-
# Check visibility
|
|
90
|
-
if heel_key not in landmarks or landmarks[heel_key][2] < 0.3:
|
|
91
|
-
return None
|
|
98
|
+
# Check ankle and knee visibility (required)
|
|
92
99
|
if ankle_key not in landmarks or landmarks[ankle_key][2] < 0.3:
|
|
93
100
|
return None
|
|
94
101
|
if knee_key not in landmarks or landmarks[knee_key][2] < 0.3:
|
|
95
102
|
return None
|
|
96
103
|
|
|
97
|
-
heel = (landmarks[heel_key][0], landmarks[heel_key][1])
|
|
98
104
|
ankle = (landmarks[ankle_key][0], landmarks[ankle_key][1])
|
|
99
105
|
knee = (landmarks[knee_key][0], landmarks[knee_key][1])
|
|
100
106
|
|
|
101
|
-
|
|
107
|
+
# Try foot_index first (primary: toe tip for plantarflexion accuracy)
|
|
108
|
+
if foot_index_key in landmarks and landmarks[foot_index_key][2] > 0.5:
|
|
109
|
+
foot_point = (landmarks[foot_index_key][0], landmarks[foot_index_key][1])
|
|
110
|
+
return calculate_angle_3_points(foot_point, ankle, knee)
|
|
111
|
+
|
|
112
|
+
# Fallback to heel if foot_index visibility is insufficient
|
|
113
|
+
if heel_key in landmarks and landmarks[heel_key][2] > 0.3:
|
|
114
|
+
foot_point = (landmarks[heel_key][0], landmarks[heel_key][1])
|
|
115
|
+
return calculate_angle_3_points(foot_point, ankle, knee)
|
|
116
|
+
|
|
117
|
+
# No valid foot landmark available
|
|
118
|
+
return None
|
|
102
119
|
|
|
103
120
|
|
|
104
121
|
def calculate_knee_angle(
|