kinemotion 0.76.0__py3-none-any.whl → 0.76.1__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/countermovement_jump/metrics_validator.py +4 -6
- kinemotion/squat_jump/analysis.py +64 -29
- kinemotion/squat_jump/debug_overlay.py +97 -149
- kinemotion/squat_jump/kinematics.py +6 -12
- kinemotion/squat_jump/metrics_validator.py +8 -16
- {kinemotion-0.76.0.dist-info → kinemotion-0.76.1.dist-info}/METADATA +1 -1
- {kinemotion-0.76.0.dist-info → kinemotion-0.76.1.dist-info}/RECORD +10 -10
- {kinemotion-0.76.0.dist-info → kinemotion-0.76.1.dist-info}/WHEEL +0 -0
- {kinemotion-0.76.0.dist-info → kinemotion-0.76.1.dist-info}/entry_points.txt +0 -0
- {kinemotion-0.76.0.dist-info → kinemotion-0.76.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -124,8 +124,8 @@ class CMJMetricsValidator(MetricsValidator):
|
|
|
124
124
|
self._check_flight_time(data, result, profile)
|
|
125
125
|
self._check_jump_height(data, result, profile)
|
|
126
126
|
self._check_countermovement_depth(data, result, profile)
|
|
127
|
-
self._check_concentric_duration(data, result
|
|
128
|
-
self._check_eccentric_duration(data, result
|
|
127
|
+
self._check_concentric_duration(data, result)
|
|
128
|
+
self._check_eccentric_duration(data, result)
|
|
129
129
|
self._check_peak_velocities(data, result, profile)
|
|
130
130
|
|
|
131
131
|
# CROSS-VALIDATION CHECKS
|
|
@@ -220,7 +220,7 @@ class CMJMetricsValidator(MetricsValidator):
|
|
|
220
220
|
)
|
|
221
221
|
|
|
222
222
|
def _check_concentric_duration(
|
|
223
|
-
self, metrics: MetricsDict, result: CMJValidationResult
|
|
223
|
+
self, metrics: MetricsDict, result: CMJValidationResult
|
|
224
224
|
) -> None:
|
|
225
225
|
"""Validate concentric duration (contact time)."""
|
|
226
226
|
duration_raw = self._get_metric_value(
|
|
@@ -256,9 +256,7 @@ class CMJMetricsValidator(MetricsValidator):
|
|
|
256
256
|
value=duration,
|
|
257
257
|
)
|
|
258
258
|
|
|
259
|
-
def _check_eccentric_duration(
|
|
260
|
-
self, metrics: MetricsDict, result: CMJValidationResult, profile: AthleteProfile
|
|
261
|
-
) -> None:
|
|
259
|
+
def _check_eccentric_duration(self, metrics: MetricsDict, result: CMJValidationResult) -> None:
|
|
262
260
|
"""Validate eccentric duration."""
|
|
263
261
|
duration_raw = self._get_metric_value(
|
|
264
262
|
metrics, "eccentric_duration_ms", "eccentric_duration"
|
|
@@ -21,7 +21,6 @@ class SJPhase(Enum):
|
|
|
21
21
|
def detect_sj_phases(
|
|
22
22
|
positions: FloatArray,
|
|
23
23
|
fps: float,
|
|
24
|
-
squat_hold_threshold: float = 0.02,
|
|
25
24
|
velocity_threshold: float = 0.1,
|
|
26
25
|
window_length: int = 5,
|
|
27
26
|
polyorder: int = 2,
|
|
@@ -38,7 +37,6 @@ def detect_sj_phases(
|
|
|
38
37
|
Args:
|
|
39
38
|
positions: 1D array of vertical positions (normalized coordinates)
|
|
40
39
|
fps: Video frames per second
|
|
41
|
-
squat_hold_threshold: Threshold for detecting squat hold phase (m)
|
|
42
40
|
velocity_threshold: Threshold for detecting flight phase (m/s)
|
|
43
41
|
window_length: Window size for velocity smoothing
|
|
44
42
|
polyorder: Polynomial order for smoothing
|
|
@@ -183,6 +181,26 @@ def detect_squat_start(
|
|
|
183
181
|
return None
|
|
184
182
|
|
|
185
183
|
|
|
184
|
+
def _find_takeoff_threshold_crossing(
|
|
185
|
+
velocities: FloatArray,
|
|
186
|
+
search_start: int,
|
|
187
|
+
search_end: int,
|
|
188
|
+
velocity_threshold: float,
|
|
189
|
+
min_duration_frames: int,
|
|
190
|
+
) -> int | None:
|
|
191
|
+
"""Find first frame where velocity exceeds threshold for minimum duration."""
|
|
192
|
+
above_threshold = velocities[search_start:search_end] <= -velocity_threshold
|
|
193
|
+
if not np.any(above_threshold):
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
threshold_indices = np.nonzero(above_threshold)[0]
|
|
197
|
+
for idx in threshold_indices:
|
|
198
|
+
if idx + min_duration_frames < len(above_threshold):
|
|
199
|
+
if np.all(above_threshold[idx : idx + min_duration_frames]):
|
|
200
|
+
return search_start + idx
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
|
|
186
204
|
def detect_takeoff(
|
|
187
205
|
positions: FloatArray,
|
|
188
206
|
velocities: FloatArray,
|
|
@@ -234,19 +252,43 @@ def detect_takeoff(
|
|
|
234
252
|
# Verify velocity exceeds threshold
|
|
235
253
|
if velocities[takeoff_frame] > -velocity_threshold:
|
|
236
254
|
# Velocity not high enough - actual takeoff may be later
|
|
237
|
-
# Look for frames where velocity exceeds threshold
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
threshold_indices = np.where(above_threshold)[0]
|
|
242
|
-
for idx in threshold_indices:
|
|
243
|
-
if idx + min_duration_frames < len(above_threshold):
|
|
244
|
-
if np.all(above_threshold[idx : idx + min_duration_frames]):
|
|
245
|
-
return search_start + idx
|
|
255
|
+
# Look for frames where velocity exceeds threshold with duration filter
|
|
256
|
+
return _find_takeoff_threshold_crossing(
|
|
257
|
+
velocities, search_start, search_end, velocity_threshold, min_duration_frames
|
|
258
|
+
)
|
|
246
259
|
|
|
247
260
|
return takeoff_frame if velocities[takeoff_frame] <= -velocity_threshold else None
|
|
248
261
|
|
|
249
262
|
|
|
263
|
+
def _detect_impact_landing(
|
|
264
|
+
accelerations: FloatArray,
|
|
265
|
+
search_start: int,
|
|
266
|
+
search_end: int,
|
|
267
|
+
) -> int | None:
|
|
268
|
+
"""Detect landing by finding the maximum acceleration spike."""
|
|
269
|
+
landing_accelerations = accelerations[search_start:search_end]
|
|
270
|
+
if len(landing_accelerations) == 0:
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
# Find maximum acceleration spike (impact)
|
|
274
|
+
landing_idx = int(np.argmax(landing_accelerations))
|
|
275
|
+
return search_start + landing_idx
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _refine_landing_by_velocity(
|
|
279
|
+
velocities: FloatArray,
|
|
280
|
+
landing_frame: int,
|
|
281
|
+
) -> int:
|
|
282
|
+
"""Refine landing frame by looking for positive (downward) velocity."""
|
|
283
|
+
if landing_frame < len(velocities) and velocities[landing_frame] < 0:
|
|
284
|
+
# Velocity still upward - landing might not be detected yet
|
|
285
|
+
# Look ahead for where velocity becomes positive
|
|
286
|
+
for i in range(landing_frame, min(landing_frame + 10, len(velocities))):
|
|
287
|
+
if velocities[i] >= 0:
|
|
288
|
+
return i
|
|
289
|
+
return landing_frame
|
|
290
|
+
|
|
291
|
+
|
|
250
292
|
def detect_landing(
|
|
251
293
|
positions: FloatArray,
|
|
252
294
|
velocities: FloatArray,
|
|
@@ -270,6 +312,8 @@ def detect_landing(
|
|
|
270
312
|
velocity_threshold: Maximum velocity threshold for landing detection
|
|
271
313
|
min_flight_frames: Minimum frames in flight before landing
|
|
272
314
|
landing_search_window_s: Time window to search for landing after peak (seconds)
|
|
315
|
+
window_length: Window size for velocity smoothing
|
|
316
|
+
polyorder: Polynomial order for smoothing
|
|
273
317
|
|
|
274
318
|
Returns:
|
|
275
319
|
Frame index where landing occurs, or None if not detected
|
|
@@ -294,8 +338,7 @@ def detect_landing(
|
|
|
294
338
|
|
|
295
339
|
# Find peak height (minimum position value in normalized coords = highest point)
|
|
296
340
|
flight_positions = positions[search_start:search_end]
|
|
297
|
-
|
|
298
|
-
peak_frame = search_start + peak_idx
|
|
341
|
+
peak_frame = search_start + int(np.argmin(flight_positions))
|
|
299
342
|
|
|
300
343
|
# After peak, look for landing using impact detection
|
|
301
344
|
landing_search_start = peak_frame + min_flight_frames
|
|
@@ -312,8 +355,12 @@ def detect_landing(
|
|
|
312
355
|
landing_window = window_length
|
|
313
356
|
if landing_window % 2 == 0:
|
|
314
357
|
landing_window += 1
|
|
358
|
+
# Use polyorder for smoothing (must be at least 2 for deriv=2)
|
|
359
|
+
eff_polyorder = max(2, polyorder)
|
|
315
360
|
accelerations = np.abs(
|
|
316
|
-
savgol_filter(
|
|
361
|
+
savgol_filter(
|
|
362
|
+
positions, landing_window, eff_polyorder, deriv=2, delta=1.0, mode="interp"
|
|
363
|
+
)
|
|
317
364
|
)
|
|
318
365
|
else:
|
|
319
366
|
# Fallback for short sequences
|
|
@@ -321,22 +368,10 @@ def detect_landing(
|
|
|
321
368
|
accelerations = np.abs(np.diff(velocities_abs, prepend=velocities_abs[0]))
|
|
322
369
|
|
|
323
370
|
# Find impact: maximum positive acceleration (deceleration spike)
|
|
324
|
-
|
|
371
|
+
landing_frame = _detect_impact_landing(accelerations, landing_search_start, landing_search_end)
|
|
325
372
|
|
|
326
|
-
if
|
|
373
|
+
if landing_frame is None:
|
|
327
374
|
return None
|
|
328
375
|
|
|
329
|
-
# Find maximum acceleration spike (impact)
|
|
330
|
-
landing_idx = int(np.argmax(landing_accelerations))
|
|
331
|
-
landing_frame = landing_search_start + landing_idx
|
|
332
|
-
|
|
333
376
|
# Additional verification: velocity should be positive (downward) at landing
|
|
334
|
-
|
|
335
|
-
# Velocity still upward - landing might not be detected yet
|
|
336
|
-
# Look ahead for where velocity becomes positive
|
|
337
|
-
for i in range(landing_frame, min(landing_frame + 10, len(velocities))):
|
|
338
|
-
if velocities[i] >= 0:
|
|
339
|
-
landing_frame = i
|
|
340
|
-
break
|
|
341
|
-
|
|
342
|
-
return landing_frame
|
|
377
|
+
return _refine_landing_by_velocity(velocities, landing_frame)
|
|
@@ -1,83 +1,43 @@
|
|
|
1
1
|
"""Debug overlay visualization for Squat Jump analysis."""
|
|
2
2
|
|
|
3
|
-
from typing import Any
|
|
4
|
-
|
|
5
3
|
import cv2
|
|
6
4
|
import numpy as np
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
from ..core.debug_overlay_utils import BaseDebugOverlayRenderer
|
|
7
|
+
from ..core.overlay_constants import (
|
|
8
|
+
CYAN,
|
|
9
|
+
GREEN,
|
|
10
|
+
PHASE_LABEL_LINE_HEIGHT,
|
|
11
|
+
PHASE_LABEL_START_Y,
|
|
12
|
+
RED,
|
|
13
|
+
WHITE,
|
|
14
|
+
Color,
|
|
15
|
+
LandmarkDict,
|
|
16
|
+
)
|
|
17
|
+
from .analysis import SJPhase
|
|
18
|
+
from .kinematics import SJMetrics
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SquatJumpDebugOverlayRenderer(BaseDebugOverlayRenderer):
|
|
10
22
|
"""Debug overlay renderer for Squat Jump analysis results."""
|
|
11
23
|
|
|
12
|
-
def
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"""Initialize debug overlay renderer.
|
|
23
|
-
|
|
24
|
-
Args:
|
|
25
|
-
output_path: Path to output video file
|
|
26
|
-
input_width: Width of input frames
|
|
27
|
-
input_height: Height of input frames
|
|
28
|
-
output_width: Width of output video
|
|
29
|
-
output_height: Height of output video
|
|
30
|
-
fps: Frames per second for output video
|
|
31
|
-
timer: Optional timer for performance profiling
|
|
32
|
-
"""
|
|
33
|
-
self.output_path = output_path
|
|
34
|
-
self.input_width = input_width
|
|
35
|
-
self.input_height = input_height
|
|
36
|
-
self.output_width = output_width
|
|
37
|
-
self.output_height = output_height
|
|
38
|
-
self.fps = fps
|
|
39
|
-
self.timer = timer
|
|
40
|
-
|
|
41
|
-
self.writer = None
|
|
42
|
-
self.frame_count = 0
|
|
43
|
-
|
|
44
|
-
def __enter__(self):
|
|
45
|
-
"""Enter context manager and initialize video writer."""
|
|
46
|
-
fourcc = cv2.VideoWriter_fourcc(*"mp4v") # type: ignore[attr-defined]
|
|
47
|
-
self.writer = cv2.VideoWriter(
|
|
48
|
-
self.output_path,
|
|
49
|
-
fourcc,
|
|
50
|
-
self.fps,
|
|
51
|
-
(self.output_width, self.output_height),
|
|
52
|
-
)
|
|
53
|
-
return self
|
|
54
|
-
|
|
55
|
-
def __exit__(
|
|
56
|
-
self,
|
|
57
|
-
exc_type: type[BaseException] | None,
|
|
58
|
-
exc_val: BaseException | None,
|
|
59
|
-
exc_tb: Any,
|
|
60
|
-
) -> None:
|
|
61
|
-
"""Exit context manager and release video writer."""
|
|
62
|
-
if self.writer:
|
|
63
|
-
self.writer.release()
|
|
64
|
-
|
|
65
|
-
def write_frame(self, frame: np.ndarray) -> None:
|
|
66
|
-
"""Write a frame to the output video.
|
|
67
|
-
|
|
68
|
-
Args:
|
|
69
|
-
frame: Annotated frame to write
|
|
70
|
-
"""
|
|
71
|
-
if self.writer:
|
|
72
|
-
self.writer.write(frame)
|
|
73
|
-
self.frame_count += 1
|
|
24
|
+
def _get_phase_color(self, phase: SJPhase) -> Color:
|
|
25
|
+
"""Get color based on jump phase."""
|
|
26
|
+
phase_colors = {
|
|
27
|
+
SJPhase.SQUAT_HOLD: (255, 255, 0), # Yellow
|
|
28
|
+
SJPhase.CONCENTRIC: (0, 165, 255), # Orange
|
|
29
|
+
SJPhase.FLIGHT: RED,
|
|
30
|
+
SJPhase.LANDING: GREEN,
|
|
31
|
+
SJPhase.UNKNOWN: WHITE,
|
|
32
|
+
}
|
|
33
|
+
return phase_colors.get(phase, WHITE)
|
|
74
34
|
|
|
75
35
|
def render_frame(
|
|
76
36
|
self,
|
|
77
37
|
frame: np.ndarray,
|
|
78
|
-
landmarks:
|
|
38
|
+
landmarks: LandmarkDict | None,
|
|
79
39
|
frame_index: int,
|
|
80
|
-
metrics:
|
|
40
|
+
metrics: SJMetrics | None = None,
|
|
81
41
|
) -> np.ndarray:
|
|
82
42
|
"""Render debug overlay on a single frame.
|
|
83
43
|
|
|
@@ -93,36 +53,53 @@ class SquatJumpDebugOverlayRenderer:
|
|
|
93
53
|
# Create a copy to avoid modifying the original
|
|
94
54
|
annotated_frame = frame.copy()
|
|
95
55
|
|
|
96
|
-
#
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
56
|
+
# Determine current phase
|
|
57
|
+
current_phase = SJPhase.UNKNOWN
|
|
58
|
+
if metrics:
|
|
59
|
+
if (
|
|
60
|
+
metrics.squat_hold_start_frame is not None
|
|
61
|
+
and metrics.concentric_start_frame is not None
|
|
62
|
+
and metrics.squat_hold_start_frame <= frame_index < metrics.concentric_start_frame
|
|
63
|
+
):
|
|
64
|
+
current_phase = SJPhase.SQUAT_HOLD
|
|
65
|
+
elif (
|
|
66
|
+
metrics.concentric_start_frame is not None
|
|
67
|
+
and metrics.takeoff_frame is not None
|
|
68
|
+
and metrics.concentric_start_frame <= frame_index < metrics.takeoff_frame
|
|
69
|
+
):
|
|
70
|
+
current_phase = SJPhase.CONCENTRIC
|
|
71
|
+
elif (
|
|
72
|
+
metrics.takeoff_frame is not None
|
|
73
|
+
and metrics.landing_frame is not None
|
|
74
|
+
and metrics.takeoff_frame <= frame_index < metrics.landing_frame
|
|
75
|
+
):
|
|
76
|
+
current_phase = SJPhase.FLIGHT
|
|
77
|
+
elif (
|
|
78
|
+
metrics.landing_frame is not None
|
|
79
|
+
and metrics.landing_frame <= frame_index < metrics.landing_frame + 15
|
|
80
|
+
):
|
|
81
|
+
current_phase = SJPhase.LANDING
|
|
82
|
+
|
|
83
|
+
# Draw skeleton and landmarks
|
|
111
84
|
if landmarks:
|
|
112
|
-
self.
|
|
85
|
+
self._draw_skeleton(annotated_frame, landmarks)
|
|
113
86
|
|
|
114
|
-
#
|
|
87
|
+
# Draw frame information
|
|
88
|
+
self._draw_frame_info(annotated_frame, frame_index, current_phase)
|
|
89
|
+
|
|
90
|
+
# Draw metrics if available
|
|
115
91
|
if metrics:
|
|
116
|
-
self._draw_metrics(annotated_frame, metrics)
|
|
92
|
+
self._draw_metrics(annotated_frame, metrics, frame_index)
|
|
117
93
|
|
|
118
94
|
return annotated_frame
|
|
119
95
|
|
|
120
|
-
def _draw_frame_info(self, frame: np.ndarray, frame_index: int) -> None:
|
|
96
|
+
def _draw_frame_info(self, frame: np.ndarray, frame_index: int, phase: SJPhase) -> None:
|
|
121
97
|
"""Draw frame information overlay.
|
|
122
98
|
|
|
123
99
|
Args:
|
|
124
100
|
frame: Frame to draw on
|
|
125
101
|
frame_index: Current frame index
|
|
102
|
+
phase: Current jump phase
|
|
126
103
|
"""
|
|
127
104
|
# Draw frame counter
|
|
128
105
|
cv2.putText(
|
|
@@ -131,85 +108,56 @@ class SquatJumpDebugOverlayRenderer:
|
|
|
131
108
|
(10, 30),
|
|
132
109
|
cv2.FONT_HERSHEY_SIMPLEX,
|
|
133
110
|
0.7,
|
|
134
|
-
|
|
111
|
+
WHITE,
|
|
135
112
|
2,
|
|
136
113
|
cv2.LINE_AA,
|
|
137
114
|
)
|
|
138
115
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
116
|
+
# Draw phase label
|
|
117
|
+
phase_color = self._get_phase_color(phase)
|
|
118
|
+
cv2.putText(
|
|
119
|
+
frame,
|
|
120
|
+
f"Phase: {phase.value.replace('_', ' ').upper()}",
|
|
121
|
+
(10, 70),
|
|
122
|
+
cv2.FONT_HERSHEY_SIMPLEX,
|
|
123
|
+
0.8,
|
|
124
|
+
phase_color,
|
|
125
|
+
2,
|
|
126
|
+
cv2.LINE_AA,
|
|
127
|
+
)
|
|
149
128
|
|
|
150
|
-
def _draw_metrics(self, frame: np.ndarray, metrics:
|
|
129
|
+
def _draw_metrics(self, frame: np.ndarray, metrics: SJMetrics, frame_index: int) -> None:
|
|
151
130
|
"""Draw metrics information on frame.
|
|
152
131
|
|
|
153
132
|
Args:
|
|
154
133
|
frame: Frame to draw on
|
|
155
134
|
metrics: Metrics object with analysis results
|
|
135
|
+
frame_index: Current frame index
|
|
156
136
|
"""
|
|
157
|
-
#
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
)
|
|
174
|
-
y_offset += 30
|
|
175
|
-
|
|
176
|
-
if hasattr(metrics, "flight_time"):
|
|
177
|
-
text = f"Flight Time: {metrics.flight_time * 1000:.1f} ms"
|
|
178
|
-
cv2.putText(
|
|
179
|
-
frame,
|
|
180
|
-
text,
|
|
181
|
-
(10, y_offset),
|
|
182
|
-
cv2.FONT_HERSHEY_SIMPLEX,
|
|
183
|
-
0.6,
|
|
184
|
-
(255, 255, 255),
|
|
185
|
-
2,
|
|
186
|
-
cv2.LINE_AA,
|
|
187
|
-
)
|
|
188
|
-
y_offset += 30
|
|
189
|
-
|
|
190
|
-
if hasattr(metrics, "squat_hold_duration"):
|
|
191
|
-
text = f"Squat Hold: {metrics.squat_hold_duration * 1000:.1f} ms"
|
|
192
|
-
cv2.putText(
|
|
193
|
-
frame,
|
|
194
|
-
text,
|
|
195
|
-
(10, y_offset),
|
|
196
|
-
cv2.FONT_HERSHEY_SIMPLEX,
|
|
197
|
-
0.6,
|
|
198
|
-
(255, 255, 255),
|
|
199
|
-
2,
|
|
200
|
-
cv2.LINE_AA,
|
|
201
|
-
)
|
|
202
|
-
y_offset += 30
|
|
203
|
-
|
|
204
|
-
if hasattr(metrics, "concentric_duration"):
|
|
205
|
-
text = f"Concentric: {metrics.concentric_duration * 1000:.1f} ms"
|
|
137
|
+
# Only show summary metrics after takeoff or at the end
|
|
138
|
+
if metrics.takeoff_frame is None or frame_index < metrics.takeoff_frame:
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
y_offset = PHASE_LABEL_START_Y + 100
|
|
142
|
+
|
|
143
|
+
# Display key metrics
|
|
144
|
+
metric_items: list[tuple[str, Color]] = [
|
|
145
|
+
(f"Jump Height: {metrics.jump_height:.3f} m", WHITE),
|
|
146
|
+
(f"Flight Time: {metrics.flight_time * 1000:.1f} ms", RED),
|
|
147
|
+
(f"Concentric: {metrics.concentric_duration * 1000:.1f} ms", CYAN),
|
|
148
|
+
]
|
|
149
|
+
if metrics.peak_power is not None:
|
|
150
|
+
metric_items.append((f"Peak Power: {metrics.peak_power:.0f} W", GREEN))
|
|
151
|
+
|
|
152
|
+
for text, color in metric_items:
|
|
206
153
|
cv2.putText(
|
|
207
154
|
frame,
|
|
208
155
|
text,
|
|
209
156
|
(10, y_offset),
|
|
210
157
|
cv2.FONT_HERSHEY_SIMPLEX,
|
|
211
158
|
0.6,
|
|
212
|
-
|
|
159
|
+
color,
|
|
213
160
|
2,
|
|
214
161
|
cv2.LINE_AA,
|
|
215
162
|
)
|
|
163
|
+
y_offset += PHASE_LABEL_LINE_HEIGHT
|
|
@@ -165,13 +165,11 @@ def calculate_sj_metrics(
|
|
|
165
165
|
peak_concentric_velocity = 0.0
|
|
166
166
|
|
|
167
167
|
# Calculate power and force if mass is provided
|
|
168
|
-
peak_power =
|
|
168
|
+
peak_power = _calculate_peak_power(velocities, concentric_start, takeoff_frame, mass_kg)
|
|
169
|
+
mean_power = _calculate_mean_power(
|
|
169
170
|
positions, velocities, concentric_start, takeoff_frame, fps, mass_kg
|
|
170
171
|
)
|
|
171
|
-
|
|
172
|
-
positions, velocities, concentric_start, takeoff_frame, fps, mass_kg
|
|
173
|
-
)
|
|
174
|
-
peak_force = calculate_peak_force(
|
|
172
|
+
peak_force = _calculate_peak_force(
|
|
175
173
|
positions, velocities, concentric_start, takeoff_frame, fps, mass_kg
|
|
176
174
|
)
|
|
177
175
|
|
|
@@ -193,12 +191,10 @@ def calculate_sj_metrics(
|
|
|
193
191
|
)
|
|
194
192
|
|
|
195
193
|
|
|
196
|
-
def
|
|
197
|
-
positions: FloatArray,
|
|
194
|
+
def _calculate_peak_power(
|
|
198
195
|
velocities: FloatArray,
|
|
199
196
|
concentric_start: int,
|
|
200
197
|
takeoff_frame: int,
|
|
201
|
-
fps: float,
|
|
202
198
|
mass_kg: float | None,
|
|
203
199
|
) -> float | None:
|
|
204
200
|
"""Calculate peak power using Sayers et al. (1999) regression equation.
|
|
@@ -212,11 +208,9 @@ def calculate_peak_power(
|
|
|
212
208
|
- Superior to Lewis formula (73% error) and Harman equation
|
|
213
209
|
|
|
214
210
|
Args:
|
|
215
|
-
positions: 1D array of vertical positions (not used in regression)
|
|
216
211
|
velocities: 1D array of vertical velocities
|
|
217
212
|
concentric_start: Frame index where concentric phase begins
|
|
218
213
|
takeoff_frame: Frame index where takeoff occurs
|
|
219
|
-
fps: Video frames per second
|
|
220
214
|
mass_kg: Athlete mass in kilograms
|
|
221
215
|
|
|
222
216
|
Returns:
|
|
@@ -246,7 +240,7 @@ def calculate_peak_power(
|
|
|
246
240
|
return float(peak_power)
|
|
247
241
|
|
|
248
242
|
|
|
249
|
-
def
|
|
243
|
+
def _calculate_mean_power(
|
|
250
244
|
positions: FloatArray,
|
|
251
245
|
velocities: FloatArray,
|
|
252
246
|
concentric_start: int,
|
|
@@ -297,7 +291,7 @@ def calculate_mean_power(
|
|
|
297
291
|
return float(mean_power)
|
|
298
292
|
|
|
299
293
|
|
|
300
|
-
def
|
|
294
|
+
def _calculate_peak_force(
|
|
301
295
|
positions: FloatArray,
|
|
302
296
|
velocities: FloatArray,
|
|
303
297
|
concentric_start: int,
|
|
@@ -114,11 +114,11 @@ class SJMetricsValidator(MetricsValidator):
|
|
|
114
114
|
# PRIMARY BOUNDS CHECKS
|
|
115
115
|
self._check_flight_time(data, result, profile)
|
|
116
116
|
self._check_jump_height(data, result, profile)
|
|
117
|
-
self._check_squat_hold_duration(data, result
|
|
118
|
-
self._check_concentric_duration(data, result
|
|
117
|
+
self._check_squat_hold_duration(data, result)
|
|
118
|
+
self._check_concentric_duration(data, result)
|
|
119
119
|
self._check_peak_concentric_velocity(data, result, profile)
|
|
120
|
-
self._check_power_metrics(data, result
|
|
121
|
-
self._check_force_metrics(data, result
|
|
120
|
+
self._check_power_metrics(data, result)
|
|
121
|
+
self._check_force_metrics(data, result)
|
|
122
122
|
|
|
123
123
|
# CROSS-VALIDATION CHECKS
|
|
124
124
|
self._check_flight_time_height_consistency(data, result)
|
|
@@ -183,9 +183,7 @@ class SJMetricsValidator(MetricsValidator):
|
|
|
183
183
|
format_str="{value:.3f}m",
|
|
184
184
|
)
|
|
185
185
|
|
|
186
|
-
def _check_squat_hold_duration(
|
|
187
|
-
self, metrics: MetricsDict, result: SJValidationResult, profile: AthleteProfile
|
|
188
|
-
) -> None:
|
|
186
|
+
def _check_squat_hold_duration(self, metrics: MetricsDict, result: SJValidationResult) -> None:
|
|
189
187
|
"""Validate squat hold duration."""
|
|
190
188
|
duration_raw = self._get_metric_value(
|
|
191
189
|
metrics, "squat_hold_duration_ms", "squat_hold_duration"
|
|
@@ -210,9 +208,7 @@ class SJMetricsValidator(MetricsValidator):
|
|
|
210
208
|
value=duration,
|
|
211
209
|
)
|
|
212
210
|
|
|
213
|
-
def _check_concentric_duration(
|
|
214
|
-
self, metrics: MetricsDict, result: SJValidationResult, profile: AthleteProfile
|
|
215
|
-
) -> None:
|
|
211
|
+
def _check_concentric_duration(self, metrics: MetricsDict, result: SJValidationResult) -> None:
|
|
216
212
|
"""Validate concentric duration."""
|
|
217
213
|
duration_raw = self._get_metric_value(
|
|
218
214
|
metrics, "concentric_duration_ms", "concentric_duration"
|
|
@@ -268,9 +264,7 @@ class SJMetricsValidator(MetricsValidator):
|
|
|
268
264
|
format_str="{value:.3f} m/s",
|
|
269
265
|
)
|
|
270
266
|
|
|
271
|
-
def _check_power_metrics(
|
|
272
|
-
self, metrics: MetricsDict, result: SJValidationResult, profile: AthleteProfile
|
|
273
|
-
) -> None:
|
|
267
|
+
def _check_power_metrics(self, metrics: MetricsDict, result: SJValidationResult) -> None:
|
|
274
268
|
"""Validate power metrics (peak and mean)."""
|
|
275
269
|
power_checks = [
|
|
276
270
|
("peak_power", "peak_power_w", SJBounds.PEAK_POWER, ""),
|
|
@@ -305,9 +299,7 @@ class SJMetricsValidator(MetricsValidator):
|
|
|
305
299
|
value=power,
|
|
306
300
|
)
|
|
307
301
|
|
|
308
|
-
def _check_force_metrics(
|
|
309
|
-
self, metrics: MetricsDict, result: SJValidationResult, profile: AthleteProfile
|
|
310
|
-
) -> None:
|
|
302
|
+
def _check_force_metrics(self, metrics: MetricsDict, result: SJValidationResult) -> None:
|
|
311
303
|
"""Validate force metrics."""
|
|
312
304
|
peak_force = self._get_metric_value(metrics, "peak_force_n", "peak_force")
|
|
313
305
|
if peak_force is None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kinemotion
|
|
3
|
-
Version: 0.76.
|
|
3
|
+
Version: 0.76.1
|
|
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
|
|
@@ -29,7 +29,7 @@ kinemotion/countermovement_jump/cli.py,sha256=m727IOg5BuixgNraCXc2sjW5jGrxrg7RKv
|
|
|
29
29
|
kinemotion/countermovement_jump/debug_overlay.py,sha256=vF5Apiz8zDRpgrVzf52manLW99m1kHQAPSdUkar5rPs,11474
|
|
30
30
|
kinemotion/countermovement_jump/joint_angles.py,sha256=by5M4LDtUfd2_Z9DmcgUl0nsvarsBYjgsE8KWWYcn08,11255
|
|
31
31
|
kinemotion/countermovement_jump/kinematics.py,sha256=KwA8uSj3g1SeNf0NXMSHsp3gIw6Gfa-6QWIwdYdRXYw,13362
|
|
32
|
-
kinemotion/countermovement_jump/metrics_validator.py,sha256=
|
|
32
|
+
kinemotion/countermovement_jump/metrics_validator.py,sha256=ma1XSLT-RIDrcjYmgfixf244TwbiosRzN7oFr4yWCXg,24609
|
|
33
33
|
kinemotion/countermovement_jump/validation_bounds.py,sha256=-0iXDhH-RntiGZi_Co22V6qtA5D-hLzkrPkVcfoNd2U,11343
|
|
34
34
|
kinemotion/drop_jump/__init__.py,sha256=yBbEbPdY6sqozWtTvfbvuUZnrVWSSjBp61xK34M29F4,878
|
|
35
35
|
kinemotion/drop_jump/analysis.py,sha256=5jlRAjS1kN091FfxNcrkh2gh3PnDqAbnJHFXZrm3hro,34870
|
|
@@ -44,16 +44,16 @@ kinemotion/models/pose_landmarker_lite.task,sha256=WZKeHR7pUodzXd2DOxnPSsRtKbx6_
|
|
|
44
44
|
kinemotion/models/rtmpose-s_simcc-body7_pt-body7-halpe26_700e-256x192-7f134165_20230605.onnx,sha256=dfZTq8kbhv8RxWiXS0HUIJNCUpxYTBN45dFIorPflEs,133
|
|
45
45
|
kinemotion/models/yolox_tiny_8xb8-300e_humanart-6f3252f9.onnx,sha256=UsutHVQ6GP3X5pCcp52EN8q7o2J3d-TnxZqlF48kY6I,133
|
|
46
46
|
kinemotion/squat_jump/__init__.py,sha256=h6ubO3BUANxqjKMdN-KtlN6m77HARAP25PLzHw9k-Lk,99
|
|
47
|
-
kinemotion/squat_jump/analysis.py,sha256=
|
|
47
|
+
kinemotion/squat_jump/analysis.py,sha256=Iwmss8EHVylN6Hace53fmGTDSxpUiEBh4AiO5YYIWc8,13500
|
|
48
48
|
kinemotion/squat_jump/api.py,sha256=YMbq2BQzB_SnZ_Z-2KnR_OO2xuua-Zg0hP29Ghbk_d4,20111
|
|
49
49
|
kinemotion/squat_jump/cli.py,sha256=09Q9O4_sHxw6QWDyPiynDQZSMixTO32NrJ5PTXTJNIk,9806
|
|
50
|
-
kinemotion/squat_jump/debug_overlay.py,sha256=
|
|
51
|
-
kinemotion/squat_jump/kinematics.py,sha256=
|
|
52
|
-
kinemotion/squat_jump/metrics_validator.py,sha256=
|
|
50
|
+
kinemotion/squat_jump/debug_overlay.py,sha256=IZij8XQvWnmxfDhOZZiLIQ-0xuICx6lDYqcdS7TA3Kw,5280
|
|
51
|
+
kinemotion/squat_jump/kinematics.py,sha256=RU5JobjkSV6Bxs3ope-4fvbeLBITFb_dv9uvHQY3fAE,12052
|
|
52
|
+
kinemotion/squat_jump/metrics_validator.py,sha256=euqd3dYrDCqdifVhs0RPc-UUprkyR-PzTg-D_rIfmI4,15914
|
|
53
53
|
kinemotion/squat_jump/validation_bounds.py,sha256=q01eQ8Eg01Y5UV3KlvZS1S9iY628OVPUwLoukHZvQOs,7276
|
|
54
54
|
kinemotion/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
55
|
-
kinemotion-0.76.
|
|
56
|
-
kinemotion-0.76.
|
|
57
|
-
kinemotion-0.76.
|
|
58
|
-
kinemotion-0.76.
|
|
59
|
-
kinemotion-0.76.
|
|
55
|
+
kinemotion-0.76.1.dist-info/METADATA,sha256=ddJE2glHyWt9apLoYSBLt7Lpq9PZIDEjgjVVzxc5XBY,27690
|
|
56
|
+
kinemotion-0.76.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
57
|
+
kinemotion-0.76.1.dist-info/entry_points.txt,sha256=zaqnAnjLvcdrk1Qvj5nvXZCZ2gp0prS7it1zTJygcIY,50
|
|
58
|
+
kinemotion-0.76.1.dist-info/licenses/LICENSE,sha256=KZajvqsHw0NoOHOi2q0FZ4NBe9HdV6oey-IPYAtHXfg,1088
|
|
59
|
+
kinemotion-0.76.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|