kinemotion 0.17.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 +31 -0
- kinemotion/api.py +946 -0
- kinemotion/cli.py +22 -0
- kinemotion/cmj/__init__.py +5 -0
- kinemotion/cmj/analysis.py +528 -0
- kinemotion/cmj/cli.py +543 -0
- kinemotion/cmj/debug_overlay.py +463 -0
- kinemotion/cmj/joint_angles.py +290 -0
- kinemotion/cmj/kinematics.py +191 -0
- kinemotion/core/__init__.py +40 -0
- kinemotion/core/auto_tuning.py +325 -0
- kinemotion/core/cli_utils.py +212 -0
- kinemotion/core/debug_overlay_utils.py +143 -0
- kinemotion/core/filtering.py +345 -0
- kinemotion/core/pose.py +259 -0
- kinemotion/core/smoothing.py +412 -0
- kinemotion/core/video_io.py +186 -0
- kinemotion/dropjump/__init__.py +29 -0
- kinemotion/dropjump/analysis.py +790 -0
- kinemotion/dropjump/cli.py +704 -0
- kinemotion/dropjump/debug_overlay.py +179 -0
- kinemotion/dropjump/kinematics.py +446 -0
- kinemotion/py.typed +0 -0
- kinemotion-0.17.0.dist-info/METADATA +529 -0
- kinemotion-0.17.0.dist-info/RECORD +28 -0
- kinemotion-0.17.0.dist-info/WHEEL +4 -0
- kinemotion-0.17.0.dist-info/entry_points.txt +2 -0
- kinemotion-0.17.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""Joint angle calculations for triple extension analysis."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def calculate_angle_3_points(
|
|
9
|
+
point1: tuple[float, float],
|
|
10
|
+
point2: tuple[float, float],
|
|
11
|
+
point3: tuple[float, float],
|
|
12
|
+
) -> float:
|
|
13
|
+
"""
|
|
14
|
+
Calculate angle at point2 formed by three points.
|
|
15
|
+
|
|
16
|
+
Uses the law of cosines to find the angle at the middle point.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
point1: First point (x, y) - e.g., foot
|
|
20
|
+
point2: Middle point (x, y) - e.g., knee (vertex of angle)
|
|
21
|
+
point3: Third point (x, y) - e.g., hip
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Angle in degrees (0-180)
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
>>> # Calculate knee angle
|
|
28
|
+
>>> ankle = (0.5, 0.8)
|
|
29
|
+
>>> knee = (0.5, 0.6)
|
|
30
|
+
>>> hip = (0.5, 0.4)
|
|
31
|
+
>>> angle = calculate_angle_3_points(ankle, knee, hip)
|
|
32
|
+
>>> # angle ≈ 180 (straight leg)
|
|
33
|
+
"""
|
|
34
|
+
# Convert points to numpy arrays
|
|
35
|
+
p1 = np.array(point1)
|
|
36
|
+
p2 = np.array(point2)
|
|
37
|
+
p3 = np.array(point3)
|
|
38
|
+
|
|
39
|
+
# Calculate vectors from point2 to point1 and point3
|
|
40
|
+
v1 = p1 - p2
|
|
41
|
+
v2 = p3 - p2
|
|
42
|
+
|
|
43
|
+
# Calculate angle using dot product
|
|
44
|
+
# cos(angle) = (v1 · v2) / (|v1| * |v2|)
|
|
45
|
+
dot_product = np.dot(v1, v2)
|
|
46
|
+
magnitude1 = np.linalg.norm(v1)
|
|
47
|
+
magnitude2 = np.linalg.norm(v2)
|
|
48
|
+
|
|
49
|
+
# Avoid division by zero
|
|
50
|
+
if magnitude1 < 1e-9 or magnitude2 < 1e-9:
|
|
51
|
+
return 0.0
|
|
52
|
+
|
|
53
|
+
# Calculate angle in radians, then convert to degrees
|
|
54
|
+
cos_angle = dot_product / (magnitude1 * magnitude2)
|
|
55
|
+
|
|
56
|
+
# Clamp to [-1, 1] to avoid numerical errors with arccos
|
|
57
|
+
cos_angle = np.clip(cos_angle, -1.0, 1.0)
|
|
58
|
+
|
|
59
|
+
angle_rad = math.acos(cos_angle)
|
|
60
|
+
angle_deg = math.degrees(angle_rad)
|
|
61
|
+
|
|
62
|
+
return float(angle_deg)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def calculate_ankle_angle(
|
|
66
|
+
landmarks: dict[str, tuple[float, float, float]], side: str = "right"
|
|
67
|
+
) -> float | None:
|
|
68
|
+
"""
|
|
69
|
+
Calculate ankle angle (dorsiflexion/plantarflexion).
|
|
70
|
+
|
|
71
|
+
Angle formed by: heel -> ankle -> knee
|
|
72
|
+
- 90° = neutral (foot perpendicular to shin)
|
|
73
|
+
- < 90° = dorsiflexion (toes up)
|
|
74
|
+
- > 90° = plantarflexion (toes down)
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
landmarks: Pose landmarks dictionary
|
|
78
|
+
side: Which side to measure ("left" or "right")
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Ankle angle in degrees, or None if landmarks not available
|
|
82
|
+
"""
|
|
83
|
+
prefix = "left_" if side == "left" else "right_"
|
|
84
|
+
|
|
85
|
+
heel_key = f"{prefix}heel"
|
|
86
|
+
ankle_key = f"{prefix}ankle"
|
|
87
|
+
knee_key = f"{prefix}knee"
|
|
88
|
+
|
|
89
|
+
# Check visibility threshold
|
|
90
|
+
if heel_key not in landmarks or landmarks[heel_key][2] < 0.3:
|
|
91
|
+
return None
|
|
92
|
+
if ankle_key not in landmarks or landmarks[ankle_key][2] < 0.3:
|
|
93
|
+
return None
|
|
94
|
+
if knee_key not in landmarks or landmarks[knee_key][2] < 0.3:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
heel = (landmarks[heel_key][0], landmarks[heel_key][1])
|
|
98
|
+
ankle = (landmarks[ankle_key][0], landmarks[ankle_key][1])
|
|
99
|
+
knee = (landmarks[knee_key][0], landmarks[knee_key][1])
|
|
100
|
+
|
|
101
|
+
return calculate_angle_3_points(heel, ankle, knee)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def calculate_knee_angle(
|
|
105
|
+
landmarks: dict[str, tuple[float, float, float]], side: str = "right"
|
|
106
|
+
) -> float | None:
|
|
107
|
+
"""
|
|
108
|
+
Calculate knee angle (flexion/extension).
|
|
109
|
+
|
|
110
|
+
Angle formed by: ankle -> knee -> hip
|
|
111
|
+
- 180° = full extension (straight leg)
|
|
112
|
+
- 90° = 90° flexion (deep squat)
|
|
113
|
+
- 0° = full flexion (not physiologically possible)
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
landmarks: Pose landmarks dictionary
|
|
117
|
+
side: Which side to measure ("left" or "right")
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Knee angle in degrees, or None if landmarks not available
|
|
121
|
+
"""
|
|
122
|
+
prefix = "left_" if side == "left" else "right_"
|
|
123
|
+
|
|
124
|
+
ankle_key = f"{prefix}ankle"
|
|
125
|
+
knee_key = f"{prefix}knee"
|
|
126
|
+
hip_key = f"{prefix}hip"
|
|
127
|
+
|
|
128
|
+
# Check visibility
|
|
129
|
+
if ankle_key not in landmarks or landmarks[ankle_key][2] < 0.3:
|
|
130
|
+
# Fallback: use foot_index if ankle not visible
|
|
131
|
+
foot_key = f"{prefix}foot_index"
|
|
132
|
+
if foot_key in landmarks and landmarks[foot_key][2] > 0.3:
|
|
133
|
+
ankle_key = foot_key
|
|
134
|
+
else:
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
if knee_key not in landmarks or landmarks[knee_key][2] < 0.3:
|
|
138
|
+
return None
|
|
139
|
+
if hip_key not in landmarks or landmarks[hip_key][2] < 0.3:
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
ankle = (landmarks[ankle_key][0], landmarks[ankle_key][1])
|
|
143
|
+
knee = (landmarks[knee_key][0], landmarks[knee_key][1])
|
|
144
|
+
hip = (landmarks[hip_key][0], landmarks[hip_key][1])
|
|
145
|
+
|
|
146
|
+
return calculate_angle_3_points(ankle, knee, hip)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def calculate_hip_angle(
|
|
150
|
+
landmarks: dict[str, tuple[float, float, float]], side: str = "right"
|
|
151
|
+
) -> float | None:
|
|
152
|
+
"""
|
|
153
|
+
Calculate hip angle (flexion/extension).
|
|
154
|
+
|
|
155
|
+
Angle formed by: knee -> hip -> shoulder
|
|
156
|
+
- 180° = standing upright (trunk and thigh aligned)
|
|
157
|
+
- 90° = 90° hip flexion (torso perpendicular to thigh)
|
|
158
|
+
- < 90° = deep flexion (squat position)
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
landmarks: Pose landmarks dictionary
|
|
162
|
+
side: Which side to measure ("left" or "right")
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Hip angle in degrees, or None if landmarks not available
|
|
166
|
+
"""
|
|
167
|
+
prefix = "left_" if side == "left" else "right_"
|
|
168
|
+
|
|
169
|
+
knee_key = f"{prefix}knee"
|
|
170
|
+
hip_key = f"{prefix}hip"
|
|
171
|
+
shoulder_key = f"{prefix}shoulder"
|
|
172
|
+
|
|
173
|
+
# Check visibility
|
|
174
|
+
if knee_key not in landmarks or landmarks[knee_key][2] < 0.3:
|
|
175
|
+
return None
|
|
176
|
+
if hip_key not in landmarks or landmarks[hip_key][2] < 0.3:
|
|
177
|
+
return None
|
|
178
|
+
if shoulder_key not in landmarks or landmarks[shoulder_key][2] < 0.3:
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
knee = (landmarks[knee_key][0], landmarks[knee_key][1])
|
|
182
|
+
hip = (landmarks[hip_key][0], landmarks[hip_key][1])
|
|
183
|
+
shoulder = (landmarks[shoulder_key][0], landmarks[shoulder_key][1])
|
|
184
|
+
|
|
185
|
+
return calculate_angle_3_points(knee, hip, shoulder)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def calculate_trunk_tilt(
|
|
189
|
+
landmarks: dict[str, tuple[float, float, float]], side: str = "right"
|
|
190
|
+
) -> float | None:
|
|
191
|
+
"""
|
|
192
|
+
Calculate trunk tilt angle relative to vertical.
|
|
193
|
+
|
|
194
|
+
Measures forward/backward lean of the torso.
|
|
195
|
+
- 0° = perfectly vertical
|
|
196
|
+
- Positive = leaning forward
|
|
197
|
+
- Negative = leaning backward
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
landmarks: Pose landmarks dictionary
|
|
201
|
+
side: Which side to measure ("left" or "right")
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Trunk tilt angle in degrees, or None if landmarks not available
|
|
205
|
+
"""
|
|
206
|
+
prefix = "left_" if side == "left" else "right_"
|
|
207
|
+
|
|
208
|
+
hip_key = f"{prefix}hip"
|
|
209
|
+
shoulder_key = f"{prefix}shoulder"
|
|
210
|
+
|
|
211
|
+
# Check visibility
|
|
212
|
+
if hip_key not in landmarks or landmarks[hip_key][2] < 0.3:
|
|
213
|
+
return None
|
|
214
|
+
if shoulder_key not in landmarks or landmarks[shoulder_key][2] < 0.3:
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
hip = np.array([landmarks[hip_key][0], landmarks[hip_key][1]])
|
|
218
|
+
shoulder = np.array([landmarks[shoulder_key][0], landmarks[shoulder_key][1]])
|
|
219
|
+
|
|
220
|
+
# Vector from hip to shoulder
|
|
221
|
+
trunk_vector = shoulder - hip
|
|
222
|
+
|
|
223
|
+
# Vertical reference (in normalized coords, vertical is along y-axis)
|
|
224
|
+
# Negative y direction is up in frame coordinates
|
|
225
|
+
vertical = np.array([0, -1])
|
|
226
|
+
|
|
227
|
+
# Calculate angle from vertical
|
|
228
|
+
dot_product = np.dot(trunk_vector, vertical)
|
|
229
|
+
magnitude_trunk = np.linalg.norm(trunk_vector)
|
|
230
|
+
|
|
231
|
+
if magnitude_trunk < 1e-9:
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
cos_angle = dot_product / magnitude_trunk
|
|
235
|
+
cos_angle = np.clip(cos_angle, -1.0, 1.0)
|
|
236
|
+
|
|
237
|
+
angle_rad = math.acos(cos_angle)
|
|
238
|
+
angle_deg = math.degrees(angle_rad)
|
|
239
|
+
|
|
240
|
+
# Determine if leaning forward or backward based on x-component
|
|
241
|
+
if trunk_vector[0] > 0: # Shoulder to the right of hip = leaning forward
|
|
242
|
+
return float(angle_deg)
|
|
243
|
+
else:
|
|
244
|
+
return float(-angle_deg)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def calculate_triple_extension(
|
|
248
|
+
landmarks: dict[str, tuple[float, float, float]], side: str = "right"
|
|
249
|
+
) -> dict[str, float | None] | None:
|
|
250
|
+
"""
|
|
251
|
+
Calculate all three joint angles for triple extension analysis.
|
|
252
|
+
|
|
253
|
+
Triple extension refers to simultaneous extension of ankle, knee, and hip joints
|
|
254
|
+
during the propulsive phase of jumping. This is a key indicator of proper technique.
|
|
255
|
+
|
|
256
|
+
NOTE: In side-view videos, ankle/knee may have low visibility from MediaPipe.
|
|
257
|
+
Returns partial results with None for unavailable angles.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
landmarks: Pose landmarks dictionary
|
|
261
|
+
side: Which side to measure ("left" or "right")
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
Dictionary with angle measurements:
|
|
265
|
+
- ankle_angle: Ankle angle or None
|
|
266
|
+
- knee_angle: Knee angle or None
|
|
267
|
+
- hip_angle: Hip angle or None
|
|
268
|
+
- trunk_tilt: Trunk lean angle or None
|
|
269
|
+
Returns None if NO angles can be calculated
|
|
270
|
+
|
|
271
|
+
Example:
|
|
272
|
+
>>> angles = calculate_triple_extension(landmarks, side="right")
|
|
273
|
+
>>> if angles and angles['knee_angle']:
|
|
274
|
+
... print(f"Knee: {angles['knee_angle']:.0f}°")
|
|
275
|
+
"""
|
|
276
|
+
ankle = calculate_ankle_angle(landmarks, side)
|
|
277
|
+
knee = calculate_knee_angle(landmarks, side)
|
|
278
|
+
hip = calculate_hip_angle(landmarks, side)
|
|
279
|
+
trunk = calculate_trunk_tilt(landmarks, side)
|
|
280
|
+
|
|
281
|
+
# Return results even if some are None (at least trunk should be available)
|
|
282
|
+
if ankle is None and knee is None and hip is None and trunk is None:
|
|
283
|
+
return None # No angles available at all
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
"ankle_angle": ankle,
|
|
287
|
+
"knee_angle": knee,
|
|
288
|
+
"hip_angle": hip,
|
|
289
|
+
"trunk_tilt": trunk,
|
|
290
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Counter Movement Jump (CMJ) metrics calculation."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class CMJMetrics:
|
|
11
|
+
"""Metrics for a counter movement jump analysis.
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
jump_height: Maximum jump height in meters
|
|
15
|
+
flight_time: Time spent in the air in seconds
|
|
16
|
+
countermovement_depth: Vertical distance traveled during eccentric phase in meters
|
|
17
|
+
eccentric_duration: Time from countermovement start to lowest point in seconds
|
|
18
|
+
concentric_duration: Time from lowest point to takeoff in seconds
|
|
19
|
+
total_movement_time: Total time from countermovement start to takeoff in seconds
|
|
20
|
+
peak_eccentric_velocity: Maximum downward velocity during countermovement in m/s
|
|
21
|
+
peak_concentric_velocity: Maximum upward velocity during propulsion in m/s
|
|
22
|
+
transition_time: Duration at lowest point (amortization phase) in seconds
|
|
23
|
+
standing_start_frame: Frame where standing phase ends (countermovement begins)
|
|
24
|
+
lowest_point_frame: Frame at lowest point of countermovement
|
|
25
|
+
takeoff_frame: Frame where athlete leaves ground
|
|
26
|
+
landing_frame: Frame where athlete lands
|
|
27
|
+
video_fps: Frames per second of the analyzed video
|
|
28
|
+
tracking_method: Method used for tracking ("foot" or "com")
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
jump_height: float
|
|
32
|
+
flight_time: float
|
|
33
|
+
countermovement_depth: float
|
|
34
|
+
eccentric_duration: float
|
|
35
|
+
concentric_duration: float
|
|
36
|
+
total_movement_time: float
|
|
37
|
+
peak_eccentric_velocity: float
|
|
38
|
+
peak_concentric_velocity: float
|
|
39
|
+
transition_time: float | None
|
|
40
|
+
standing_start_frame: float | None
|
|
41
|
+
lowest_point_frame: float
|
|
42
|
+
takeoff_frame: float
|
|
43
|
+
landing_frame: float
|
|
44
|
+
video_fps: float
|
|
45
|
+
tracking_method: str
|
|
46
|
+
|
|
47
|
+
def to_dict(self) -> dict[str, Any]:
|
|
48
|
+
"""Convert metrics to JSON-serializable dictionary.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Dictionary with all metrics, converting NumPy types to Python types.
|
|
52
|
+
"""
|
|
53
|
+
return {
|
|
54
|
+
"jump_height_m": float(self.jump_height),
|
|
55
|
+
"flight_time_s": float(self.flight_time),
|
|
56
|
+
"countermovement_depth_m": float(self.countermovement_depth),
|
|
57
|
+
"eccentric_duration_s": float(self.eccentric_duration),
|
|
58
|
+
"concentric_duration_s": float(self.concentric_duration),
|
|
59
|
+
"total_movement_time_s": float(self.total_movement_time),
|
|
60
|
+
"peak_eccentric_velocity_m_s": float(self.peak_eccentric_velocity),
|
|
61
|
+
"peak_concentric_velocity_m_s": float(self.peak_concentric_velocity),
|
|
62
|
+
"transition_time_s": (
|
|
63
|
+
float(self.transition_time)
|
|
64
|
+
if self.transition_time is not None
|
|
65
|
+
else None
|
|
66
|
+
),
|
|
67
|
+
"standing_start_frame": (
|
|
68
|
+
float(self.standing_start_frame)
|
|
69
|
+
if self.standing_start_frame is not None
|
|
70
|
+
else None
|
|
71
|
+
),
|
|
72
|
+
"lowest_point_frame": float(self.lowest_point_frame),
|
|
73
|
+
"takeoff_frame": float(self.takeoff_frame),
|
|
74
|
+
"landing_frame": float(self.landing_frame),
|
|
75
|
+
"video_fps": float(self.video_fps),
|
|
76
|
+
"tracking_method": self.tracking_method,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def calculate_cmj_metrics(
|
|
81
|
+
positions: np.ndarray,
|
|
82
|
+
velocities: np.ndarray,
|
|
83
|
+
standing_start_frame: float | None,
|
|
84
|
+
lowest_point_frame: float,
|
|
85
|
+
takeoff_frame: float,
|
|
86
|
+
landing_frame: float,
|
|
87
|
+
fps: float,
|
|
88
|
+
tracking_method: str = "foot",
|
|
89
|
+
) -> CMJMetrics:
|
|
90
|
+
"""Calculate all CMJ metrics from detected phases.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
positions: Array of vertical positions (normalized coordinates)
|
|
94
|
+
velocities: Array of vertical velocities
|
|
95
|
+
standing_start_frame: Frame where countermovement begins (fractional)
|
|
96
|
+
lowest_point_frame: Frame at lowest point (fractional)
|
|
97
|
+
takeoff_frame: Frame at takeoff (fractional)
|
|
98
|
+
landing_frame: Frame at landing (fractional)
|
|
99
|
+
fps: Video frames per second
|
|
100
|
+
tracking_method: Tracking method used ("foot" or "com")
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
CMJMetrics object with all calculated metrics.
|
|
104
|
+
"""
|
|
105
|
+
# Calculate flight time from takeoff to landing
|
|
106
|
+
flight_time = (landing_frame - takeoff_frame) / fps
|
|
107
|
+
|
|
108
|
+
# Calculate jump height from flight time using kinematic formula
|
|
109
|
+
# h = g * t^2 / 8 (where t is total flight time)
|
|
110
|
+
g = 9.81 # gravity in m/s^2
|
|
111
|
+
jump_height = (g * flight_time**2) / 8
|
|
112
|
+
|
|
113
|
+
# Calculate countermovement depth
|
|
114
|
+
if standing_start_frame is not None:
|
|
115
|
+
standing_position = positions[int(standing_start_frame)]
|
|
116
|
+
else:
|
|
117
|
+
# Use position at start of recording if standing not detected
|
|
118
|
+
standing_position = positions[0]
|
|
119
|
+
|
|
120
|
+
lowest_position = positions[int(lowest_point_frame)]
|
|
121
|
+
countermovement_depth = abs(standing_position - lowest_position)
|
|
122
|
+
|
|
123
|
+
# Calculate phase durations
|
|
124
|
+
if standing_start_frame is not None:
|
|
125
|
+
eccentric_duration = (lowest_point_frame - standing_start_frame) / fps
|
|
126
|
+
total_movement_time = (takeoff_frame - standing_start_frame) / fps
|
|
127
|
+
else:
|
|
128
|
+
# If no standing phase detected, measure from start
|
|
129
|
+
eccentric_duration = lowest_point_frame / fps
|
|
130
|
+
total_movement_time = takeoff_frame / fps
|
|
131
|
+
|
|
132
|
+
concentric_duration = (takeoff_frame - lowest_point_frame) / fps
|
|
133
|
+
|
|
134
|
+
# Calculate peak velocities
|
|
135
|
+
# Eccentric phase: negative velocities (downward)
|
|
136
|
+
if standing_start_frame is not None:
|
|
137
|
+
eccentric_start_idx = int(standing_start_frame)
|
|
138
|
+
else:
|
|
139
|
+
eccentric_start_idx = 0
|
|
140
|
+
|
|
141
|
+
eccentric_end_idx = int(lowest_point_frame)
|
|
142
|
+
eccentric_velocities = velocities[eccentric_start_idx:eccentric_end_idx]
|
|
143
|
+
|
|
144
|
+
if len(eccentric_velocities) > 0:
|
|
145
|
+
# Peak eccentric velocity is most negative value
|
|
146
|
+
peak_eccentric_velocity = float(np.min(eccentric_velocities))
|
|
147
|
+
else:
|
|
148
|
+
peak_eccentric_velocity = 0.0
|
|
149
|
+
|
|
150
|
+
# Concentric phase: positive velocities (upward)
|
|
151
|
+
concentric_start_idx = int(lowest_point_frame)
|
|
152
|
+
concentric_end_idx = int(takeoff_frame)
|
|
153
|
+
concentric_velocities = velocities[concentric_start_idx:concentric_end_idx]
|
|
154
|
+
|
|
155
|
+
if len(concentric_velocities) > 0:
|
|
156
|
+
peak_concentric_velocity = float(np.max(concentric_velocities))
|
|
157
|
+
else:
|
|
158
|
+
peak_concentric_velocity = 0.0
|
|
159
|
+
|
|
160
|
+
# Estimate transition time (amortization phase)
|
|
161
|
+
# Look for period around lowest point where velocity is near zero
|
|
162
|
+
transition_threshold = 0.005 # Very low velocity threshold
|
|
163
|
+
search_window = int(fps * 0.1) # Search within ±100ms
|
|
164
|
+
|
|
165
|
+
transition_start_idx = max(0, int(lowest_point_frame) - search_window)
|
|
166
|
+
transition_end_idx = min(len(velocities), int(lowest_point_frame) + search_window)
|
|
167
|
+
|
|
168
|
+
transition_frames = 0
|
|
169
|
+
for i in range(transition_start_idx, transition_end_idx):
|
|
170
|
+
if abs(velocities[i]) < transition_threshold:
|
|
171
|
+
transition_frames += 1
|
|
172
|
+
|
|
173
|
+
transition_time = transition_frames / fps if transition_frames > 0 else None
|
|
174
|
+
|
|
175
|
+
return CMJMetrics(
|
|
176
|
+
jump_height=jump_height,
|
|
177
|
+
flight_time=flight_time,
|
|
178
|
+
countermovement_depth=countermovement_depth,
|
|
179
|
+
eccentric_duration=eccentric_duration,
|
|
180
|
+
concentric_duration=concentric_duration,
|
|
181
|
+
total_movement_time=total_movement_time,
|
|
182
|
+
peak_eccentric_velocity=peak_eccentric_velocity,
|
|
183
|
+
peak_concentric_velocity=peak_concentric_velocity,
|
|
184
|
+
transition_time=transition_time,
|
|
185
|
+
standing_start_frame=standing_start_frame,
|
|
186
|
+
lowest_point_frame=lowest_point_frame,
|
|
187
|
+
takeoff_frame=takeoff_frame,
|
|
188
|
+
landing_frame=landing_frame,
|
|
189
|
+
video_fps=fps,
|
|
190
|
+
tracking_method=tracking_method,
|
|
191
|
+
)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Core functionality shared across all jump analysis types."""
|
|
2
|
+
|
|
3
|
+
from .filtering import (
|
|
4
|
+
adaptive_smooth_window,
|
|
5
|
+
bilateral_temporal_filter,
|
|
6
|
+
detect_outliers_median,
|
|
7
|
+
detect_outliers_ransac,
|
|
8
|
+
reject_outliers,
|
|
9
|
+
remove_outliers,
|
|
10
|
+
)
|
|
11
|
+
from .pose import PoseTracker, compute_center_of_mass
|
|
12
|
+
from .smoothing import (
|
|
13
|
+
compute_acceleration_from_derivative,
|
|
14
|
+
compute_velocity,
|
|
15
|
+
compute_velocity_from_derivative,
|
|
16
|
+
smooth_landmarks,
|
|
17
|
+
smooth_landmarks_advanced,
|
|
18
|
+
)
|
|
19
|
+
from .video_io import VideoProcessor
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
# Pose tracking
|
|
23
|
+
"PoseTracker",
|
|
24
|
+
"compute_center_of_mass",
|
|
25
|
+
# Smoothing
|
|
26
|
+
"smooth_landmarks",
|
|
27
|
+
"smooth_landmarks_advanced",
|
|
28
|
+
"compute_velocity",
|
|
29
|
+
"compute_velocity_from_derivative",
|
|
30
|
+
"compute_acceleration_from_derivative",
|
|
31
|
+
# Filtering
|
|
32
|
+
"detect_outliers_ransac",
|
|
33
|
+
"detect_outliers_median",
|
|
34
|
+
"remove_outliers",
|
|
35
|
+
"reject_outliers",
|
|
36
|
+
"adaptive_smooth_window",
|
|
37
|
+
"bilateral_temporal_filter",
|
|
38
|
+
# Video I/O
|
|
39
|
+
"VideoProcessor",
|
|
40
|
+
]
|