kinemotion 0.71.0__py3-none-any.whl → 0.71.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.
- kinemotion/cmj/analysis.py +2 -4
- kinemotion/cmj/api.py +9 -7
- kinemotion/cmj/debug_overlay.py +154 -286
- kinemotion/cmj/joint_angles.py +96 -31
- kinemotion/cmj/metrics_validator.py +22 -29
- kinemotion/cmj/validation_bounds.py +1 -18
- kinemotion/core/__init__.py +0 -2
- kinemotion/core/auto_tuning.py +91 -99
- kinemotion/core/debug_overlay_utils.py +142 -15
- kinemotion/core/experimental.py +55 -51
- kinemotion/core/filtering.py +15 -11
- kinemotion/core/overlay_constants.py +61 -0
- kinemotion/core/pose.py +47 -98
- kinemotion/core/smoothing.py +65 -51
- kinemotion/core/types.py +15 -0
- kinemotion/core/validation.py +6 -7
- kinemotion/core/video_io.py +14 -9
- kinemotion/dropjump/__init__.py +2 -2
- kinemotion/dropjump/analysis.py +67 -44
- kinemotion/dropjump/api.py +5 -17
- kinemotion/dropjump/cli.py +62 -78
- kinemotion/dropjump/debug_overlay.py +124 -65
- kinemotion/dropjump/validation_bounds.py +1 -1
- {kinemotion-0.71.0.dist-info → kinemotion-0.71.1.dist-info}/METADATA +1 -1
- kinemotion-0.71.1.dist-info/RECORD +50 -0
- kinemotion-0.71.0.dist-info/RECORD +0 -49
- {kinemotion-0.71.0.dist-info → kinemotion-0.71.1.dist-info}/WHEEL +0 -0
- {kinemotion-0.71.0.dist-info → kinemotion-0.71.1.dist-info}/entry_points.txt +0 -0
- {kinemotion-0.71.0.dist-info → kinemotion-0.71.1.dist-info}/licenses/LICENSE +0 -0
kinemotion/core/smoothing.py
CHANGED
|
@@ -181,6 +181,18 @@ def _smooth_landmarks_core( # NOSONAR(S1172) - polyorder used via closure
|
|
|
181
181
|
return smoothed_sequence
|
|
182
182
|
|
|
183
183
|
|
|
184
|
+
def _ensure_odd_window_length(window_length: int) -> int:
|
|
185
|
+
"""Ensure window_length is odd (required for Savitzky-Golay filter).
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
window_length: Desired window length
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Odd window length (increments by 1 if even)
|
|
192
|
+
"""
|
|
193
|
+
return window_length + 1 if window_length % 2 == 0 else window_length
|
|
194
|
+
|
|
195
|
+
|
|
184
196
|
def smooth_landmarks(
|
|
185
197
|
landmark_sequence: LandmarkSequence,
|
|
186
198
|
window_length: int = 5,
|
|
@@ -200,9 +212,7 @@ def smooth_landmarks(
|
|
|
200
212
|
if len(landmark_sequence) < window_length:
|
|
201
213
|
return landmark_sequence
|
|
202
214
|
|
|
203
|
-
|
|
204
|
-
if window_length % 2 == 0:
|
|
205
|
-
window_length += 1
|
|
215
|
+
window_length = _ensure_odd_window_length(window_length)
|
|
206
216
|
|
|
207
217
|
def savgol_smoother(
|
|
208
218
|
x_coords: list[float], y_coords: list[float], _valid_frames: list[int]
|
|
@@ -231,61 +241,87 @@ def compute_velocity(positions: np.ndarray, fps: float, smooth_window: int = 3)
|
|
|
231
241
|
|
|
232
242
|
# Smooth velocity if we have enough data
|
|
233
243
|
if len(velocity) >= smooth_window and smooth_window > 1:
|
|
234
|
-
|
|
235
|
-
smooth_window += 1
|
|
244
|
+
smooth_window = _ensure_odd_window_length(smooth_window)
|
|
236
245
|
for dim in range(velocity.shape[1]):
|
|
237
246
|
velocity[:, dim] = savgol_filter(velocity[:, dim], smooth_window, 1)
|
|
238
247
|
|
|
239
248
|
return velocity
|
|
240
249
|
|
|
241
250
|
|
|
242
|
-
def
|
|
251
|
+
def _compute_derivative(
|
|
243
252
|
positions: np.ndarray,
|
|
253
|
+
deriv_order: int,
|
|
244
254
|
window_length: int = 5,
|
|
245
255
|
polyorder: int = 2,
|
|
246
256
|
) -> np.ndarray:
|
|
247
257
|
"""
|
|
248
|
-
Compute
|
|
249
|
-
|
|
250
|
-
Uses Savitzky-Golay filter to compute the derivative directly, which provides
|
|
251
|
-
a much smoother and more accurate velocity estimate than frame-to-frame differences.
|
|
258
|
+
Compute nth derivative using Savitzky-Golay filter.
|
|
252
259
|
|
|
253
|
-
This
|
|
254
|
-
|
|
255
|
-
2. Analytically computes the derivative of that polynomial
|
|
256
|
-
3. Returns smooth velocity values
|
|
260
|
+
This unified function handles both velocity (first derivative) and
|
|
261
|
+
acceleration (second derivative) computation with a single implementation.
|
|
257
262
|
|
|
258
263
|
Args:
|
|
259
264
|
positions: 1D array of position values (e.g., foot y-positions)
|
|
265
|
+
deriv_order: Order of derivative (1 for velocity, 2 for acceleration)
|
|
260
266
|
window_length: Window size for smoothing (must be odd, >= polyorder + 2)
|
|
261
267
|
polyorder: Polynomial order for Savitzky-Golay filter (typically 2 or 3)
|
|
262
268
|
|
|
263
269
|
Returns:
|
|
264
|
-
Array of
|
|
270
|
+
Array of derivative values
|
|
265
271
|
"""
|
|
266
272
|
if len(positions) < window_length:
|
|
267
273
|
# Fallback to simple differences for short sequences
|
|
268
|
-
|
|
274
|
+
if deriv_order == 1:
|
|
275
|
+
return np.abs(np.diff(positions, prepend=positions[0]))
|
|
276
|
+
# Second derivative fallback
|
|
277
|
+
velocity = np.diff(positions, prepend=positions[0])
|
|
278
|
+
return np.diff(velocity, prepend=velocity[0])
|
|
269
279
|
|
|
270
|
-
|
|
271
|
-
if window_length % 2 == 0:
|
|
272
|
-
window_length += 1
|
|
280
|
+
window_length = _ensure_odd_window_length(window_length)
|
|
273
281
|
|
|
274
282
|
# Compute derivative using Savitzky-Golay filter
|
|
275
|
-
#
|
|
276
|
-
# delta=1.0: frame spacing (velocity per frame)
|
|
283
|
+
# delta=1.0: frame spacing
|
|
277
284
|
# mode='interp': interpolate at boundaries
|
|
278
|
-
|
|
285
|
+
result = savgol_filter(
|
|
279
286
|
positions,
|
|
280
287
|
window_length,
|
|
281
288
|
polyorder,
|
|
282
|
-
deriv=
|
|
283
|
-
delta=1.0,
|
|
289
|
+
deriv=deriv_order,
|
|
290
|
+
delta=1.0,
|
|
284
291
|
mode="interp",
|
|
285
292
|
)
|
|
286
293
|
|
|
287
|
-
# Return absolute velocity (
|
|
288
|
-
return np.abs(
|
|
294
|
+
# Return absolute values for velocity (first derivative)
|
|
295
|
+
return np.abs(result) if deriv_order == 1 else result
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def compute_velocity_from_derivative(
|
|
299
|
+
positions: np.ndarray,
|
|
300
|
+
window_length: int = 5,
|
|
301
|
+
polyorder: int = 2,
|
|
302
|
+
) -> np.ndarray:
|
|
303
|
+
"""
|
|
304
|
+
Compute velocity as derivative of smoothed position trajectory.
|
|
305
|
+
|
|
306
|
+
Uses Savitzky-Golay filter to compute the derivative directly, which provides
|
|
307
|
+
a much smoother and more accurate velocity estimate than frame-to-frame differences.
|
|
308
|
+
|
|
309
|
+
This method:
|
|
310
|
+
1. Fits a polynomial to the position data in a sliding window
|
|
311
|
+
2. Analytically computes the derivative of that polynomial
|
|
312
|
+
3. Returns smooth velocity values
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
positions: 1D array of position values (e.g., foot y-positions)
|
|
316
|
+
window_length: Window size for smoothing (must be odd, >= polyorder + 2)
|
|
317
|
+
polyorder: Polynomial order for Savitzky-Golay filter (typically 2 or 3)
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
Array of absolute velocity values (magnitude of derivative)
|
|
321
|
+
"""
|
|
322
|
+
return _compute_derivative(
|
|
323
|
+
positions, deriv_order=1, window_length=window_length, polyorder=polyorder
|
|
324
|
+
)
|
|
289
325
|
|
|
290
326
|
|
|
291
327
|
def compute_acceleration_from_derivative(
|
|
@@ -314,30 +350,10 @@ def compute_acceleration_from_derivative(
|
|
|
314
350
|
Returns:
|
|
315
351
|
Array of acceleration values (second derivative of position)
|
|
316
352
|
"""
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
velocity = np.diff(positions, prepend=positions[0])
|
|
320
|
-
return np.diff(velocity, prepend=velocity[0])
|
|
321
|
-
|
|
322
|
-
# Ensure window_length is odd
|
|
323
|
-
if window_length % 2 == 0:
|
|
324
|
-
window_length += 1
|
|
325
|
-
|
|
326
|
-
# Compute second derivative using Savitzky-Golay filter
|
|
327
|
-
# deriv=2: compute second derivative (acceleration/curvature)
|
|
328
|
-
# delta=1.0: frame spacing
|
|
329
|
-
# mode='interp': interpolate at boundaries
|
|
330
|
-
acceleration = savgol_filter(
|
|
331
|
-
positions,
|
|
332
|
-
window_length,
|
|
333
|
-
polyorder,
|
|
334
|
-
deriv=2, # Second derivative
|
|
335
|
-
delta=1.0, # Frame spacing
|
|
336
|
-
mode="interp",
|
|
353
|
+
return _compute_derivative(
|
|
354
|
+
positions, deriv_order=2, window_length=window_length, polyorder=polyorder
|
|
337
355
|
)
|
|
338
356
|
|
|
339
|
-
return acceleration
|
|
340
|
-
|
|
341
357
|
|
|
342
358
|
def smooth_landmarks_advanced(
|
|
343
359
|
landmark_sequence: LandmarkSequence,
|
|
@@ -376,9 +392,7 @@ def smooth_landmarks_advanced(
|
|
|
376
392
|
if len(landmark_sequence) < window_length:
|
|
377
393
|
return landmark_sequence
|
|
378
394
|
|
|
379
|
-
|
|
380
|
-
if window_length % 2 == 0:
|
|
381
|
-
window_length += 1
|
|
395
|
+
window_length = _ensure_odd_window_length(window_length)
|
|
382
396
|
|
|
383
397
|
def advanced_smoother(
|
|
384
398
|
x_coords: list[float], y_coords: list[float], _valid_frames: list[int]
|
kinemotion/core/types.py
CHANGED
|
@@ -29,6 +29,19 @@ LandmarkSequence: TypeAlias = list[LandmarkFrame]
|
|
|
29
29
|
# - Wrapper structures: e.g. {"data": {...actual metrics...}}
|
|
30
30
|
MetricsDict: TypeAlias = dict[str, Any]
|
|
31
31
|
|
|
32
|
+
# MediaPipe foot landmark names used for position and visibility tracking
|
|
33
|
+
FOOT_KEYS: tuple[str, ...] = (
|
|
34
|
+
"left_ankle",
|
|
35
|
+
"right_ankle",
|
|
36
|
+
"left_heel",
|
|
37
|
+
"right_heel",
|
|
38
|
+
"left_foot_index",
|
|
39
|
+
"right_foot_index",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# MediaPipe hip landmark names used for position tracking
|
|
43
|
+
HIP_KEYS: tuple[str, ...] = ("left_hip", "right_hip")
|
|
44
|
+
|
|
32
45
|
__all__ = [
|
|
33
46
|
"FloatArray",
|
|
34
47
|
"Float64Array",
|
|
@@ -39,4 +52,6 @@ __all__ = [
|
|
|
39
52
|
"LandmarkFrame",
|
|
40
53
|
"LandmarkSequence",
|
|
41
54
|
"MetricsDict",
|
|
55
|
+
"FOOT_KEYS",
|
|
56
|
+
"HIP_KEYS",
|
|
42
57
|
]
|
kinemotion/core/validation.py
CHANGED
|
@@ -78,19 +78,18 @@ class MetricBounds:
|
|
|
78
78
|
|
|
79
79
|
def contains(self, value: float, profile: AthleteProfile) -> bool:
|
|
80
80
|
"""Check if value is within bounds for athlete profile."""
|
|
81
|
-
|
|
81
|
+
# ELDERLY and UNTRAINED use same bounds (practical to recreational)
|
|
82
|
+
if profile in (AthleteProfile.ELDERLY, AthleteProfile.UNTRAINED):
|
|
82
83
|
return self.practical_min <= value <= self.recreational_max
|
|
83
|
-
|
|
84
|
-
return self.practical_min <= value <= self.recreational_max
|
|
85
|
-
elif profile == AthleteProfile.RECREATIONAL:
|
|
84
|
+
if profile == AthleteProfile.RECREATIONAL:
|
|
86
85
|
return self.recreational_min <= value <= self.recreational_max
|
|
87
|
-
|
|
86
|
+
if profile == AthleteProfile.ELITE:
|
|
87
|
+
return self.elite_min <= value <= self.elite_max
|
|
88
|
+
if profile == AthleteProfile.TRAINED:
|
|
88
89
|
# Trained athletes: midpoint between recreational and elite
|
|
89
90
|
trained_min = (self.recreational_min + self.elite_min) / 2
|
|
90
91
|
trained_max = (self.recreational_max + self.elite_max) / 2
|
|
91
92
|
return trained_min <= value <= trained_max
|
|
92
|
-
elif profile == AthleteProfile.ELITE:
|
|
93
|
-
return self.elite_min <= value <= self.elite_max
|
|
94
93
|
return False
|
|
95
94
|
|
|
96
95
|
def is_physically_possible(self, value: float) -> bool:
|
kinemotion/core/video_io.py
CHANGED
|
@@ -18,6 +18,17 @@ class VideoProcessor:
|
|
|
18
18
|
No dimensions are hardcoded - all dimensions are extracted from actual frame data.
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
|
+
# Mapping of rotation angles to OpenCV rotation operations
|
|
22
|
+
# Keys are normalized angles (equivalent angles grouped)
|
|
23
|
+
_ROTATION_OPS: dict[int, int] = {
|
|
24
|
+
-90: cv2.ROTATE_90_CLOCKWISE,
|
|
25
|
+
270: cv2.ROTATE_90_CLOCKWISE,
|
|
26
|
+
90: cv2.ROTATE_90_COUNTERCLOCKWISE,
|
|
27
|
+
-270: cv2.ROTATE_90_COUNTERCLOCKWISE,
|
|
28
|
+
180: cv2.ROTATE_180,
|
|
29
|
+
-180: cv2.ROTATE_180,
|
|
30
|
+
}
|
|
31
|
+
|
|
21
32
|
def __init__(self, video_path: str, timer: Timer | None = None) -> None:
|
|
22
33
|
"""
|
|
23
34
|
Initialize video processor.
|
|
@@ -216,15 +227,9 @@ class VideoProcessor:
|
|
|
216
227
|
|
|
217
228
|
# Apply rotation if video has rotation metadata
|
|
218
229
|
with self.timer.measure("frame_rotation"):
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
frame = cv2.rotate(frame,
|
|
222
|
-
elif self.rotation == 90 or self.rotation == -270:
|
|
223
|
-
# 90 degrees = rotate 90 degrees counter-clockwise
|
|
224
|
-
frame = cv2.rotate(frame, cv2.ROTATE_90_COUNTERCLOCKWISE)
|
|
225
|
-
elif self.rotation == 180 or self.rotation == -180:
|
|
226
|
-
# 180 degrees rotation
|
|
227
|
-
frame = cv2.rotate(frame, cv2.ROTATE_180)
|
|
230
|
+
rotation_op = self._ROTATION_OPS.get(self.rotation)
|
|
231
|
+
if rotation_op is not None:
|
|
232
|
+
frame = cv2.rotate(frame, rotation_op)
|
|
228
233
|
|
|
229
234
|
self._frame_index += 1
|
|
230
235
|
return frame
|
kinemotion/dropjump/__init__.py
CHANGED
|
@@ -9,7 +9,7 @@ from .analysis import (
|
|
|
9
9
|
find_interpolated_phase_transitions_with_curvature,
|
|
10
10
|
refine_transition_with_curvature,
|
|
11
11
|
)
|
|
12
|
-
from .debug_overlay import
|
|
12
|
+
from .debug_overlay import DropJumpDebugOverlayRenderer
|
|
13
13
|
from .kinematics import DropJumpMetrics, calculate_drop_jump_metrics
|
|
14
14
|
|
|
15
15
|
__all__ = [
|
|
@@ -25,5 +25,5 @@ __all__ = [
|
|
|
25
25
|
"DropJumpMetrics",
|
|
26
26
|
"calculate_drop_jump_metrics",
|
|
27
27
|
# Debug overlay
|
|
28
|
-
"
|
|
28
|
+
"DropJumpDebugOverlayRenderer",
|
|
29
29
|
]
|
kinemotion/dropjump/analysis.py
CHANGED
|
@@ -11,7 +11,7 @@ from ..core.smoothing import (
|
|
|
11
11
|
interpolate_threshold_crossing,
|
|
12
12
|
)
|
|
13
13
|
from ..core.timing import NULL_TIMER, Timer
|
|
14
|
-
from ..core.types import BoolArray, FloatArray
|
|
14
|
+
from ..core.types import FOOT_KEYS, BoolArray, FloatArray
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class ContactState(Enum):
|
|
@@ -424,6 +424,57 @@ def find_contact_phases(
|
|
|
424
424
|
return phases
|
|
425
425
|
|
|
426
426
|
|
|
427
|
+
def _interpolate_phase_boundary(
|
|
428
|
+
boundary_idx: int,
|
|
429
|
+
state: ContactState,
|
|
430
|
+
velocities: FloatArray,
|
|
431
|
+
velocity_threshold: float,
|
|
432
|
+
is_start: bool,
|
|
433
|
+
) -> float:
|
|
434
|
+
"""Interpolate phase boundary with sub-frame precision.
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
boundary_idx: Index of the boundary frame
|
|
438
|
+
state: Contact state of the phase
|
|
439
|
+
velocities: Velocity array
|
|
440
|
+
velocity_threshold: Threshold value for crossing detection
|
|
441
|
+
is_start: True for phase start, False for phase end
|
|
442
|
+
|
|
443
|
+
Returns:
|
|
444
|
+
Fractional frame index, or float(boundary_idx) if no interpolation.
|
|
445
|
+
"""
|
|
446
|
+
n_velocities = len(velocities)
|
|
447
|
+
|
|
448
|
+
if is_start:
|
|
449
|
+
# For start boundary, look at velocity before and at the boundary
|
|
450
|
+
if boundary_idx <= 0 or boundary_idx >= n_velocities:
|
|
451
|
+
return float(boundary_idx)
|
|
452
|
+
vel_before = velocities[boundary_idx - 1]
|
|
453
|
+
vel_at = velocities[boundary_idx]
|
|
454
|
+
# Check threshold crossing based on state
|
|
455
|
+
is_crossing = (
|
|
456
|
+
state == ContactState.ON_GROUND and vel_before > velocity_threshold > vel_at
|
|
457
|
+
) or (state == ContactState.IN_AIR and vel_before < velocity_threshold < vel_at)
|
|
458
|
+
if is_crossing:
|
|
459
|
+
offset = interpolate_threshold_crossing(vel_before, vel_at, velocity_threshold)
|
|
460
|
+
return (boundary_idx - 1) + offset
|
|
461
|
+
return float(boundary_idx)
|
|
462
|
+
|
|
463
|
+
# For end boundary, look at velocity at and after the boundary
|
|
464
|
+
if boundary_idx + 1 >= n_velocities:
|
|
465
|
+
return float(boundary_idx)
|
|
466
|
+
vel_at = velocities[boundary_idx]
|
|
467
|
+
vel_after = velocities[boundary_idx + 1]
|
|
468
|
+
# Check threshold crossing based on state
|
|
469
|
+
is_crossing = (
|
|
470
|
+
state == ContactState.ON_GROUND and vel_at < velocity_threshold < vel_after
|
|
471
|
+
) or (state == ContactState.IN_AIR and vel_at > velocity_threshold > vel_after)
|
|
472
|
+
if is_crossing:
|
|
473
|
+
offset = interpolate_threshold_crossing(vel_at, vel_after, velocity_threshold)
|
|
474
|
+
return boundary_idx + offset
|
|
475
|
+
return float(boundary_idx)
|
|
476
|
+
|
|
477
|
+
|
|
427
478
|
def _interpolate_phase_start(
|
|
428
479
|
start_idx: int,
|
|
429
480
|
state: ContactState,
|
|
@@ -435,21 +486,9 @@ def _interpolate_phase_start(
|
|
|
435
486
|
Returns:
|
|
436
487
|
Fractional start frame, or float(start_idx) if no interpolation.
|
|
437
488
|
"""
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
vel_before = velocities[start_idx - 1]
|
|
442
|
-
vel_at = velocities[start_idx]
|
|
443
|
-
|
|
444
|
-
# Check threshold crossing based on state
|
|
445
|
-
is_landing = state == ContactState.ON_GROUND and vel_before > velocity_threshold > vel_at
|
|
446
|
-
is_takeoff = state == ContactState.IN_AIR and vel_before < velocity_threshold < vel_at
|
|
447
|
-
|
|
448
|
-
if is_landing or is_takeoff:
|
|
449
|
-
offset = interpolate_threshold_crossing(vel_before, vel_at, velocity_threshold)
|
|
450
|
-
return (start_idx - 1) + offset
|
|
451
|
-
|
|
452
|
-
return float(start_idx)
|
|
489
|
+
return _interpolate_phase_boundary(
|
|
490
|
+
start_idx, state, velocities, velocity_threshold, is_start=True
|
|
491
|
+
)
|
|
453
492
|
|
|
454
493
|
|
|
455
494
|
def _interpolate_phase_end(
|
|
@@ -457,28 +496,16 @@ def _interpolate_phase_end(
|
|
|
457
496
|
state: ContactState,
|
|
458
497
|
velocities: FloatArray,
|
|
459
498
|
velocity_threshold: float,
|
|
460
|
-
|
|
499
|
+
_max_idx: int,
|
|
461
500
|
) -> float:
|
|
462
501
|
"""Interpolate end boundary of a phase with sub-frame precision.
|
|
463
502
|
|
|
464
503
|
Returns:
|
|
465
504
|
Fractional end frame, or float(end_idx) if no interpolation.
|
|
466
505
|
"""
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
vel_at = velocities[end_idx]
|
|
471
|
-
vel_after = velocities[end_idx + 1]
|
|
472
|
-
|
|
473
|
-
# Check threshold crossing based on state
|
|
474
|
-
is_takeoff = state == ContactState.ON_GROUND and vel_at < velocity_threshold < vel_after
|
|
475
|
-
is_landing = state == ContactState.IN_AIR and vel_at > velocity_threshold > vel_after
|
|
476
|
-
|
|
477
|
-
if is_takeoff or is_landing:
|
|
478
|
-
offset = interpolate_threshold_crossing(vel_at, vel_after, velocity_threshold)
|
|
479
|
-
return end_idx + offset
|
|
480
|
-
|
|
481
|
-
return float(end_idx)
|
|
506
|
+
return _interpolate_phase_boundary(
|
|
507
|
+
end_idx, state, velocities, velocity_threshold, is_start=False
|
|
508
|
+
)
|
|
482
509
|
|
|
483
510
|
|
|
484
511
|
def find_interpolated_phase_transitions(
|
|
@@ -747,19 +774,10 @@ def compute_average_foot_position(
|
|
|
747
774
|
Returns:
|
|
748
775
|
(x, y) average foot position in normalized coordinates
|
|
749
776
|
"""
|
|
750
|
-
foot_keys = [
|
|
751
|
-
"left_ankle",
|
|
752
|
-
"right_ankle",
|
|
753
|
-
"left_heel",
|
|
754
|
-
"right_heel",
|
|
755
|
-
"left_foot_index",
|
|
756
|
-
"right_foot_index",
|
|
757
|
-
]
|
|
758
|
-
|
|
759
777
|
x_positions = []
|
|
760
778
|
y_positions = []
|
|
761
779
|
|
|
762
|
-
for key in
|
|
780
|
+
for key in FOOT_KEYS:
|
|
763
781
|
if key in landmarks:
|
|
764
782
|
x, y, visibility = landmarks[key]
|
|
765
783
|
if visibility > 0.5: # Only use visible landmarks
|
|
@@ -783,8 +801,13 @@ def _calculate_average_visibility(
|
|
|
783
801
|
Returns:
|
|
784
802
|
Average visibility of foot landmarks (0.0 if none visible)
|
|
785
803
|
"""
|
|
786
|
-
|
|
787
|
-
|
|
804
|
+
# Only use ankles and heels for visibility (foot_index can be noisy)
|
|
805
|
+
visibility_keys = ("left_ankle", "right_ankle", "left_heel", "right_heel")
|
|
806
|
+
foot_vis = [
|
|
807
|
+
frame_landmarks[key][2]
|
|
808
|
+
for key in FOOT_KEYS
|
|
809
|
+
if key in frame_landmarks and key in visibility_keys
|
|
810
|
+
]
|
|
788
811
|
return float(np.mean(foot_vis)) if foot_vis else 0.0
|
|
789
812
|
|
|
790
813
|
|
kinemotion/dropjump/api.py
CHANGED
|
@@ -48,7 +48,7 @@ from .analysis import (
|
|
|
48
48
|
detect_ground_contact,
|
|
49
49
|
find_contact_phases,
|
|
50
50
|
)
|
|
51
|
-
from .debug_overlay import
|
|
51
|
+
from .debug_overlay import DropJumpDebugOverlayRenderer
|
|
52
52
|
from .kinematics import DropJumpMetrics, calculate_drop_jump_metrics
|
|
53
53
|
from .metrics_validator import DropJumpMetricsValidator
|
|
54
54
|
|
|
@@ -305,7 +305,6 @@ def _tune_and_smooth(
|
|
|
305
305
|
characteristics = analyze_video_sample(landmarks_sequence, video_fps, frame_count)
|
|
306
306
|
params = auto_tune_parameters(characteristics, quality_preset)
|
|
307
307
|
|
|
308
|
-
# Apply overrides if provided
|
|
309
308
|
if overrides:
|
|
310
309
|
params = apply_expert_overrides(
|
|
311
310
|
params,
|
|
@@ -314,14 +313,6 @@ def _tune_and_smooth(
|
|
|
314
313
|
overrides.min_contact_frames,
|
|
315
314
|
overrides.visibility_threshold,
|
|
316
315
|
)
|
|
317
|
-
else:
|
|
318
|
-
params = apply_expert_overrides(
|
|
319
|
-
params,
|
|
320
|
-
None,
|
|
321
|
-
None,
|
|
322
|
-
None,
|
|
323
|
-
None,
|
|
324
|
-
)
|
|
325
316
|
|
|
326
317
|
smoothed_landmarks = apply_smoothing(landmarks_sequence, params, verbose, timer)
|
|
327
318
|
|
|
@@ -440,16 +431,13 @@ def _generate_debug_video(
|
|
|
440
431
|
timer = timer or NULL_TIMER
|
|
441
432
|
debug_h, debug_w = frames[0].shape[:2]
|
|
442
433
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
else:
|
|
446
|
-
debug_fps = video_fps
|
|
447
|
-
|
|
434
|
+
# Calculate debug FPS: cap at 30 for high-fps videos, use step if frame-sparse
|
|
435
|
+
debug_fps = min(video_fps, 30.0)
|
|
448
436
|
if len(frames) < len(smoothed_landmarks):
|
|
449
437
|
step = max(1, int(video_fps / 30.0))
|
|
450
438
|
debug_fps = video_fps / step
|
|
451
439
|
|
|
452
|
-
def _render_frames(renderer:
|
|
440
|
+
def _render_frames(renderer: DropJumpDebugOverlayRenderer) -> None:
|
|
453
441
|
for frame, idx in zip(frames, frame_indices, strict=True):
|
|
454
442
|
annotated = renderer.render_frame(
|
|
455
443
|
frame,
|
|
@@ -461,7 +449,7 @@ def _generate_debug_video(
|
|
|
461
449
|
)
|
|
462
450
|
renderer.write_frame(annotated)
|
|
463
451
|
|
|
464
|
-
renderer_context =
|
|
452
|
+
renderer_context = DropJumpDebugOverlayRenderer(
|
|
465
453
|
output_video,
|
|
466
454
|
debug_w,
|
|
467
455
|
debug_h,
|