kinemotion 0.76.3__py3-none-any.whl → 2.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 +271 -176
- 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-2.0.0.dist-info}/METADATA +26 -75
- kinemotion-2.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-2.0.0.dist-info}/WHEEL +0 -0
- {kinemotion-0.76.3.dist-info → kinemotion-2.0.0.dist-info}/entry_points.txt +0 -0
- {kinemotion-0.76.3.dist-info → kinemotion-2.0.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
|
14
|
+
from ..core.types import BoolArray, FloatArray
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class ContactState(Enum):
|
|
@@ -27,7 +27,7 @@ class ContactState(Enum):
|
|
|
27
27
|
remove_in="1.0.0",
|
|
28
28
|
since="0.34.0",
|
|
29
29
|
)
|
|
30
|
-
def
|
|
30
|
+
def calculate_adaptive_threshold(
|
|
31
31
|
positions: FloatArray,
|
|
32
32
|
fps: float,
|
|
33
33
|
baseline_duration: float = 3.0,
|
|
@@ -188,7 +188,7 @@ def _find_drop_from_baseline(
|
|
|
188
188
|
return 0
|
|
189
189
|
|
|
190
190
|
|
|
191
|
-
def
|
|
191
|
+
def detect_drop_start(
|
|
192
192
|
positions: FloatArray,
|
|
193
193
|
fps: float,
|
|
194
194
|
min_stationary_duration: float = 1.0,
|
|
@@ -314,8 +314,6 @@ def _assign_contact_states(
|
|
|
314
314
|
) -> list[ContactState]:
|
|
315
315
|
"""Assign contact states based on contact frames and visibility.
|
|
316
316
|
|
|
317
|
-
Vectorized implementation for 2-3x speedup over loop-based version.
|
|
318
|
-
|
|
319
317
|
Args:
|
|
320
318
|
n_frames: Total number of frames
|
|
321
319
|
contact_frames: Set of frames with confirmed contact
|
|
@@ -325,75 +323,15 @@ def _assign_contact_states(
|
|
|
325
323
|
Returns:
|
|
326
324
|
List of ContactState for each frame
|
|
327
325
|
"""
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
# Mark ON_GROUND where visibility is sufficient
|
|
335
|
-
if contact_frames:
|
|
336
|
-
contact_array = np.fromiter(contact_frames, dtype=int)
|
|
337
|
-
# Filter to valid indices only
|
|
338
|
-
valid_mask = (contact_array >= 0) & (contact_array < n_frames)
|
|
339
|
-
valid_contacts = contact_array[valid_mask]
|
|
340
|
-
|
|
341
|
-
# Only mark ON_GROUND for frames with good visibility
|
|
342
|
-
if visibilities is not None:
|
|
343
|
-
good_visibility = visibilities[valid_contacts] >= visibility_threshold
|
|
344
|
-
states[valid_contacts[good_visibility]] = 1
|
|
326
|
+
states = []
|
|
327
|
+
for i in range(n_frames):
|
|
328
|
+
if visibilities is not None and visibilities[i] < visibility_threshold:
|
|
329
|
+
states.append(ContactState.UNKNOWN)
|
|
330
|
+
elif i in contact_frames:
|
|
331
|
+
states.append(ContactState.ON_GROUND)
|
|
345
332
|
else:
|
|
346
|
-
states
|
|
347
|
-
|
|
348
|
-
# Mark UNKNOWN last (highest priority - overrides ON_GROUND)
|
|
349
|
-
if visibilities is not None:
|
|
350
|
-
unknown_mask = visibilities < visibility_threshold
|
|
351
|
-
states[unknown_mask] = 2
|
|
352
|
-
|
|
353
|
-
# Convert integer indices back to ContactState
|
|
354
|
-
return [_state_order[s] for s in states]
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
def _compute_near_ground_mask(
|
|
358
|
-
foot_positions: FloatArray,
|
|
359
|
-
height_tolerance: float = 0.35,
|
|
360
|
-
) -> BoolArray:
|
|
361
|
-
"""Compute mask for frames where feet are near ground level.
|
|
362
|
-
|
|
363
|
-
Uses position-based filtering to identify frames near ground baseline.
|
|
364
|
-
In normalized coordinates: y=1 is bottom (ground), y=0 is top.
|
|
365
|
-
|
|
366
|
-
The ground baseline is established as the 90th percentile of positions,
|
|
367
|
-
which represents the typical ground level while handling outliers.
|
|
368
|
-
|
|
369
|
-
The tolerance is set at 35% of the position range by default, which is
|
|
370
|
-
generous enough to capture the full reactive contact phase (where athletes
|
|
371
|
-
maintain an athletic stance) while still filtering out the jump apex
|
|
372
|
-
(where y is much lower than ground level).
|
|
373
|
-
|
|
374
|
-
Args:
|
|
375
|
-
foot_positions: Array of foot y-positions (normalized, 0-1)
|
|
376
|
-
height_tolerance: Fraction of position range allowed above ground (default 35%)
|
|
377
|
-
|
|
378
|
-
Returns:
|
|
379
|
-
Boolean array where True indicates frame is near ground level
|
|
380
|
-
"""
|
|
381
|
-
# Ground baseline: 90th percentile (where feet are typically on ground)
|
|
382
|
-
# Using 90th instead of 95th to be less sensitive to final landing positions
|
|
383
|
-
ground_baseline = float(np.percentile(foot_positions, 90))
|
|
384
|
-
|
|
385
|
-
# Compute position range for tolerance calculation
|
|
386
|
-
position_range = float(np.max(foot_positions) - np.min(foot_positions))
|
|
387
|
-
|
|
388
|
-
# Minimum absolute tolerance to handle small movements
|
|
389
|
-
min_tolerance = 0.03 # 3% of normalized range
|
|
390
|
-
|
|
391
|
-
# Height tolerance: percentage of position range or minimum
|
|
392
|
-
tolerance = max(position_range * height_tolerance, min_tolerance)
|
|
393
|
-
|
|
394
|
-
# Frames are near ground if y >= ground_baseline - tolerance
|
|
395
|
-
# (Remember: higher y = closer to ground in normalized coords)
|
|
396
|
-
return foot_positions >= (ground_baseline - tolerance)
|
|
333
|
+
states.append(ContactState.IN_AIR)
|
|
334
|
+
return states
|
|
397
335
|
|
|
398
336
|
|
|
399
337
|
def detect_ground_contact(
|
|
@@ -405,14 +343,13 @@ def detect_ground_contact(
|
|
|
405
343
|
window_length: int = 5,
|
|
406
344
|
polyorder: int = 2,
|
|
407
345
|
timer: Timer | None = None,
|
|
408
|
-
height_tolerance: float = 0.35,
|
|
409
346
|
) -> list[ContactState]:
|
|
410
347
|
"""
|
|
411
|
-
Detect when feet are in contact with ground based on vertical motion
|
|
348
|
+
Detect when feet are in contact with ground based on vertical motion.
|
|
412
349
|
|
|
413
|
-
Uses derivative-based velocity calculation via Savitzky-
|
|
414
|
-
accurate velocity estimates.
|
|
415
|
-
|
|
350
|
+
Uses derivative-based velocity calculation via Savitzky-Goyal filter for smooth,
|
|
351
|
+
accurate velocity estimates. This is consistent with the velocity calculation used
|
|
352
|
+
throughout the pipeline for sub-frame interpolation and curvature analysis.
|
|
416
353
|
|
|
417
354
|
Args:
|
|
418
355
|
foot_positions: Array of foot y-positions (normalized, 0-1, where 1 is bottom)
|
|
@@ -423,7 +360,6 @@ def detect_ground_contact(
|
|
|
423
360
|
window_length: Window size for velocity derivative calculation (must be odd)
|
|
424
361
|
polyorder: Polynomial order for Savitzky-Golay filter (default: 2)
|
|
425
362
|
timer: Optional Timer for measuring operations
|
|
426
|
-
height_tolerance: Fraction of position range to allow above ground baseline (default 35%)
|
|
427
363
|
|
|
428
364
|
Returns:
|
|
429
365
|
List of ContactState for each frame
|
|
@@ -443,14 +379,6 @@ def detect_ground_contact(
|
|
|
443
379
|
# Detect stationary frames based on velocity threshold
|
|
444
380
|
is_stationary = np.abs(velocities) < velocity_threshold
|
|
445
381
|
|
|
446
|
-
# Position-based filtering to prevent false ON_GROUND at jump apex
|
|
447
|
-
# In normalized coords: y=1 is bottom (ground), y=0 is top
|
|
448
|
-
# Ground baseline is the 95th percentile (handles outliers)
|
|
449
|
-
is_near_ground = _compute_near_ground_mask(foot_positions, height_tolerance)
|
|
450
|
-
|
|
451
|
-
# Both conditions must be true: low velocity AND near ground
|
|
452
|
-
is_stationary = is_stationary & is_near_ground
|
|
453
|
-
|
|
454
382
|
# Apply visibility filter
|
|
455
383
|
is_stationary = _filter_stationary_with_visibility(
|
|
456
384
|
is_stationary, visibilities, visibility_threshold
|
|
@@ -496,57 +424,6 @@ def find_contact_phases(
|
|
|
496
424
|
return phases
|
|
497
425
|
|
|
498
426
|
|
|
499
|
-
def _interpolate_phase_boundary(
|
|
500
|
-
boundary_idx: int,
|
|
501
|
-
state: ContactState,
|
|
502
|
-
velocities: FloatArray,
|
|
503
|
-
velocity_threshold: float,
|
|
504
|
-
is_start: bool,
|
|
505
|
-
) -> float:
|
|
506
|
-
"""Interpolate phase boundary with sub-frame precision.
|
|
507
|
-
|
|
508
|
-
Args:
|
|
509
|
-
boundary_idx: Index of the boundary frame
|
|
510
|
-
state: Contact state of the phase
|
|
511
|
-
velocities: Velocity array
|
|
512
|
-
velocity_threshold: Threshold value for crossing detection
|
|
513
|
-
is_start: True for phase start, False for phase end
|
|
514
|
-
|
|
515
|
-
Returns:
|
|
516
|
-
Fractional frame index, or float(boundary_idx) if no interpolation.
|
|
517
|
-
"""
|
|
518
|
-
n_velocities = len(velocities)
|
|
519
|
-
|
|
520
|
-
if is_start:
|
|
521
|
-
# For start boundary, look at velocity before and at the boundary
|
|
522
|
-
if boundary_idx <= 0 or boundary_idx >= n_velocities:
|
|
523
|
-
return float(boundary_idx)
|
|
524
|
-
vel_before = velocities[boundary_idx - 1]
|
|
525
|
-
vel_at = velocities[boundary_idx]
|
|
526
|
-
# Check threshold crossing based on state
|
|
527
|
-
is_crossing = (
|
|
528
|
-
state == ContactState.ON_GROUND and vel_before > velocity_threshold > vel_at
|
|
529
|
-
) or (state == ContactState.IN_AIR and vel_before < velocity_threshold < vel_at)
|
|
530
|
-
if is_crossing:
|
|
531
|
-
offset = interpolate_threshold_crossing(vel_before, vel_at, velocity_threshold)
|
|
532
|
-
return (boundary_idx - 1) + offset
|
|
533
|
-
return float(boundary_idx)
|
|
534
|
-
|
|
535
|
-
# For end boundary, look at velocity at and after the boundary
|
|
536
|
-
if boundary_idx + 1 >= n_velocities:
|
|
537
|
-
return float(boundary_idx)
|
|
538
|
-
vel_at = velocities[boundary_idx]
|
|
539
|
-
vel_after = velocities[boundary_idx + 1]
|
|
540
|
-
# Check threshold crossing based on state
|
|
541
|
-
is_crossing = (
|
|
542
|
-
state == ContactState.ON_GROUND and vel_at < velocity_threshold < vel_after
|
|
543
|
-
) or (state == ContactState.IN_AIR and vel_at > velocity_threshold > vel_after)
|
|
544
|
-
if is_crossing:
|
|
545
|
-
offset = interpolate_threshold_crossing(vel_at, vel_after, velocity_threshold)
|
|
546
|
-
return boundary_idx + offset
|
|
547
|
-
return float(boundary_idx)
|
|
548
|
-
|
|
549
|
-
|
|
550
427
|
def _interpolate_phase_start(
|
|
551
428
|
start_idx: int,
|
|
552
429
|
state: ContactState,
|
|
@@ -558,9 +435,21 @@ def _interpolate_phase_start(
|
|
|
558
435
|
Returns:
|
|
559
436
|
Fractional start frame, or float(start_idx) if no interpolation.
|
|
560
437
|
"""
|
|
561
|
-
|
|
562
|
-
start_idx
|
|
563
|
-
|
|
438
|
+
if start_idx <= 0 or start_idx >= len(velocities):
|
|
439
|
+
return float(start_idx)
|
|
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)
|
|
564
453
|
|
|
565
454
|
|
|
566
455
|
def _interpolate_phase_end(
|
|
@@ -568,19 +457,31 @@ def _interpolate_phase_end(
|
|
|
568
457
|
state: ContactState,
|
|
569
458
|
velocities: FloatArray,
|
|
570
459
|
velocity_threshold: float,
|
|
571
|
-
|
|
460
|
+
max_idx: int,
|
|
572
461
|
) -> float:
|
|
573
462
|
"""Interpolate end boundary of a phase with sub-frame precision.
|
|
574
463
|
|
|
575
464
|
Returns:
|
|
576
465
|
Fractional end frame, or float(end_idx) if no interpolation.
|
|
577
466
|
"""
|
|
578
|
-
|
|
579
|
-
end_idx
|
|
580
|
-
|
|
467
|
+
if end_idx >= max_idx - 1 or end_idx + 1 >= len(velocities):
|
|
468
|
+
return float(end_idx)
|
|
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
|
|
581
480
|
|
|
481
|
+
return float(end_idx)
|
|
582
482
|
|
|
583
|
-
|
|
483
|
+
|
|
484
|
+
def find_interpolated_phase_transitions(
|
|
584
485
|
foot_positions: FloatArray,
|
|
585
486
|
contact_states: list[ContactState],
|
|
586
487
|
velocity_threshold: float,
|
|
@@ -622,7 +523,7 @@ def _find_interpolated_phase_transitions(
|
|
|
622
523
|
return interpolated_phases
|
|
623
524
|
|
|
624
525
|
|
|
625
|
-
def
|
|
526
|
+
def refine_transition_with_curvature(
|
|
626
527
|
foot_positions: FloatArray,
|
|
627
528
|
estimated_frame: float,
|
|
628
529
|
transition_type: str,
|
|
@@ -697,49 +598,7 @@ def _refine_transition_with_curvature(
|
|
|
697
598
|
return refined_frame
|
|
698
599
|
|
|
699
600
|
|
|
700
|
-
def
|
|
701
|
-
foot_positions: FloatArray,
|
|
702
|
-
start_frac: float,
|
|
703
|
-
end_frac: float,
|
|
704
|
-
start_type: str,
|
|
705
|
-
end_type: str,
|
|
706
|
-
smoothing_window: int,
|
|
707
|
-
polyorder: int,
|
|
708
|
-
) -> tuple[float, float]:
|
|
709
|
-
"""Refine phase boundary frames using curvature analysis.
|
|
710
|
-
|
|
711
|
-
Args:
|
|
712
|
-
foot_positions: Array of foot y-positions (normalized, 0-1)
|
|
713
|
-
start_frac: Start frame (fractional)
|
|
714
|
-
end_frac: End frame (fractional)
|
|
715
|
-
start_type: Transition type for start ("landing" or "takeoff")
|
|
716
|
-
end_type: Transition type for end ("landing" or "takeoff")
|
|
717
|
-
smoothing_window: Window size for acceleration computation
|
|
718
|
-
polyorder: Polynomial order for Savitzky-Golay filter
|
|
719
|
-
|
|
720
|
-
Returns:
|
|
721
|
-
Tuple of (refined_start, refined_end) fractional frame indices
|
|
722
|
-
"""
|
|
723
|
-
refined_start = _refine_transition_with_curvature(
|
|
724
|
-
foot_positions,
|
|
725
|
-
start_frac,
|
|
726
|
-
start_type,
|
|
727
|
-
search_window=3,
|
|
728
|
-
smoothing_window=smoothing_window,
|
|
729
|
-
polyorder=polyorder,
|
|
730
|
-
)
|
|
731
|
-
refined_end = _refine_transition_with_curvature(
|
|
732
|
-
foot_positions,
|
|
733
|
-
end_frac,
|
|
734
|
-
end_type,
|
|
735
|
-
search_window=3,
|
|
736
|
-
smoothing_window=smoothing_window,
|
|
737
|
-
polyorder=polyorder,
|
|
738
|
-
)
|
|
739
|
-
return refined_start, refined_end
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
def _find_interpolated_phase_transitions_with_curvature(
|
|
601
|
+
def find_interpolated_phase_transitions_with_curvature(
|
|
743
602
|
foot_positions: FloatArray,
|
|
744
603
|
contact_states: list[ContactState],
|
|
745
604
|
velocity_threshold: float,
|
|
@@ -767,7 +626,7 @@ def _find_interpolated_phase_transitions_with_curvature(
|
|
|
767
626
|
List of (start_frame, end_frame, state) tuples with fractional frame indices
|
|
768
627
|
"""
|
|
769
628
|
# Get interpolated phases using velocity
|
|
770
|
-
interpolated_phases =
|
|
629
|
+
interpolated_phases = find_interpolated_phase_transitions(
|
|
771
630
|
foot_positions, contact_states, velocity_threshold, smoothing_window
|
|
772
631
|
)
|
|
773
632
|
|
|
@@ -778,63 +637,77 @@ def _find_interpolated_phase_transitions_with_curvature(
|
|
|
778
637
|
refined_phases: list[tuple[float, float, ContactState]] = []
|
|
779
638
|
|
|
780
639
|
for start_frac, end_frac, state in interpolated_phases:
|
|
640
|
+
refined_start = start_frac
|
|
641
|
+
refined_end = end_frac
|
|
642
|
+
|
|
781
643
|
if state == ContactState.ON_GROUND:
|
|
782
|
-
#
|
|
783
|
-
refined_start
|
|
644
|
+
# Refine landing (start of ground contact)
|
|
645
|
+
refined_start = refine_transition_with_curvature(
|
|
784
646
|
foot_positions,
|
|
785
647
|
start_frac,
|
|
786
|
-
end_frac,
|
|
787
648
|
"landing",
|
|
649
|
+
search_window=3,
|
|
650
|
+
smoothing_window=smoothing_window,
|
|
651
|
+
polyorder=polyorder,
|
|
652
|
+
)
|
|
653
|
+
# Refine takeoff (end of ground contact)
|
|
654
|
+
refined_end = refine_transition_with_curvature(
|
|
655
|
+
foot_positions,
|
|
656
|
+
end_frac,
|
|
788
657
|
"takeoff",
|
|
789
|
-
|
|
790
|
-
|
|
658
|
+
search_window=3,
|
|
659
|
+
smoothing_window=smoothing_window,
|
|
660
|
+
polyorder=polyorder,
|
|
791
661
|
)
|
|
662
|
+
|
|
792
663
|
elif state == ContactState.IN_AIR:
|
|
793
|
-
#
|
|
794
|
-
refined_start
|
|
664
|
+
# For flight phases, takeoff is at start, landing is at end
|
|
665
|
+
refined_start = refine_transition_with_curvature(
|
|
795
666
|
foot_positions,
|
|
796
667
|
start_frac,
|
|
797
|
-
end_frac,
|
|
798
668
|
"takeoff",
|
|
669
|
+
search_window=3,
|
|
670
|
+
smoothing_window=smoothing_window,
|
|
671
|
+
polyorder=polyorder,
|
|
672
|
+
)
|
|
673
|
+
refined_end = refine_transition_with_curvature(
|
|
674
|
+
foot_positions,
|
|
675
|
+
end_frac,
|
|
799
676
|
"landing",
|
|
800
|
-
|
|
801
|
-
|
|
677
|
+
search_window=3,
|
|
678
|
+
smoothing_window=smoothing_window,
|
|
679
|
+
polyorder=polyorder,
|
|
802
680
|
)
|
|
803
|
-
else:
|
|
804
|
-
refined_start, refined_end = start_frac, end_frac
|
|
805
681
|
|
|
806
682
|
refined_phases.append((refined_start, refined_end, state))
|
|
807
683
|
|
|
808
684
|
return refined_phases
|
|
809
685
|
|
|
810
686
|
|
|
811
|
-
def
|
|
687
|
+
def find_landing_from_acceleration(
|
|
812
688
|
positions: FloatArray,
|
|
813
689
|
accelerations: FloatArray,
|
|
814
690
|
takeoff_frame: int,
|
|
815
691
|
fps: float,
|
|
816
|
-
search_duration: float =
|
|
692
|
+
search_duration: float = 0.7,
|
|
817
693
|
) -> int:
|
|
818
694
|
"""
|
|
819
|
-
Find landing frame
|
|
695
|
+
Find landing frame by detecting impact acceleration after takeoff.
|
|
820
696
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
For drop jumps, landing is defined as the first ground contact after the
|
|
825
|
-
reactive jump, when feet return to approximately the same level as takeoff.
|
|
697
|
+
Detects the moment of initial ground contact, characterized by a sharp
|
|
698
|
+
deceleration (positive acceleration spike) as downward velocity is arrested.
|
|
826
699
|
|
|
827
700
|
Args:
|
|
828
|
-
positions: Array of vertical positions (normalized 0-1
|
|
701
|
+
positions: Array of vertical positions (normalized 0-1)
|
|
829
702
|
accelerations: Array of accelerations (second derivative)
|
|
830
703
|
takeoff_frame: Frame at takeoff (end of ground contact)
|
|
831
704
|
fps: Video frame rate
|
|
832
|
-
search_duration: Duration in seconds to search for landing (default:
|
|
705
|
+
search_duration: Duration in seconds to search for landing (default: 0.7s)
|
|
833
706
|
|
|
834
707
|
Returns:
|
|
835
708
|
Landing frame index (integer)
|
|
836
709
|
"""
|
|
837
|
-
#
|
|
710
|
+
# Find peak height (minimum y value = highest point)
|
|
838
711
|
search_start = takeoff_frame
|
|
839
712
|
search_end = min(len(positions), takeoff_frame + int(fps * search_duration))
|
|
840
713
|
|
|
@@ -842,91 +715,61 @@ def _find_landing_from_acceleration(
|
|
|
842
715
|
return min(len(positions) - 1, takeoff_frame + int(fps * 0.3))
|
|
843
716
|
|
|
844
717
|
flight_positions = positions[search_start:search_end]
|
|
845
|
-
|
|
846
|
-
# Find peak height (minimum y value = highest point)
|
|
847
718
|
peak_idx = int(np.argmin(flight_positions))
|
|
848
719
|
peak_frame = search_start + peak_idx
|
|
849
720
|
|
|
850
|
-
#
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
# returns to within 5% of takeoff level (or 95% of the way back)
|
|
855
|
-
landing_threshold = takeoff_position - 0.05 * (takeoff_position - positions[peak_frame])
|
|
721
|
+
# After peak, look for landing (impact with ground)
|
|
722
|
+
# Landing is detected by maximum positive acceleration (deceleration on impact)
|
|
723
|
+
landing_search_start = peak_frame + 2
|
|
724
|
+
landing_search_end = min(len(accelerations), landing_search_start + int(fps * 0.6))
|
|
856
725
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
for i in range(peak_frame + 2, min(len(positions), search_end)):
|
|
860
|
-
if positions[i] >= landing_threshold:
|
|
861
|
-
landing_frame = i
|
|
862
|
-
break
|
|
726
|
+
if landing_search_end <= landing_search_start:
|
|
727
|
+
return min(len(positions) - 1, peak_frame + int(fps * 0.2))
|
|
863
728
|
|
|
864
|
-
#
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
# Look for significant acceleration in a small window around the position-based landing
|
|
870
|
-
refine_start = max(peak_frame + 2, landing_frame - int(fps * 0.1))
|
|
871
|
-
refine_end = min(len(accelerations), landing_frame + int(fps * 0.1))
|
|
872
|
-
|
|
873
|
-
if refine_end > refine_start:
|
|
874
|
-
window_accelerations = accelerations[refine_start:refine_end]
|
|
875
|
-
# Check if there's a significant acceleration spike (> 3x median)
|
|
876
|
-
median_acc = float(np.median(np.abs(window_accelerations)))
|
|
877
|
-
max_acc_idx = int(np.argmax(np.abs(window_accelerations)))
|
|
878
|
-
max_acc = float(np.abs(window_accelerations[max_acc_idx]))
|
|
879
|
-
|
|
880
|
-
if median_acc > 0 and max_acc > 3 * median_acc:
|
|
881
|
-
# Use acceleration-refined landing frame
|
|
882
|
-
landing_frame = refine_start + max_acc_idx
|
|
729
|
+
# Find impact: maximum negative acceleration after peak (deceleration on impact)
|
|
730
|
+
# The impact creates a large upward force (negative acceleration in Y-down)
|
|
731
|
+
landing_accelerations = accelerations[landing_search_start:landing_search_end]
|
|
732
|
+
impact_idx = int(np.argmin(landing_accelerations))
|
|
733
|
+
landing_frame = landing_search_start + impact_idx
|
|
883
734
|
|
|
884
735
|
return landing_frame
|
|
885
736
|
|
|
886
737
|
|
|
887
738
|
def compute_average_foot_position(
|
|
888
739
|
landmarks: dict[str, tuple[float, float, float]],
|
|
889
|
-
visibility_threshold: float = 0.5,
|
|
890
740
|
) -> tuple[float, float]:
|
|
891
741
|
"""
|
|
892
742
|
Compute average foot position from ankle and foot landmarks.
|
|
893
743
|
|
|
894
|
-
Uses tiered visibility approach to avoid returning center (0.5, 0.5)
|
|
895
|
-
which can cause false phase transitions in contact detection.
|
|
896
|
-
|
|
897
744
|
Args:
|
|
898
745
|
landmarks: Dictionary of landmark positions
|
|
899
|
-
visibility_threshold: Minimum visibility to include landmark (default: 0.5)
|
|
900
746
|
|
|
901
747
|
Returns:
|
|
902
748
|
(x, y) average foot position in normalized coordinates
|
|
903
749
|
"""
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
750
|
+
foot_keys = [
|
|
751
|
+
"left_ankle",
|
|
752
|
+
"right_ankle",
|
|
753
|
+
"left_heel",
|
|
754
|
+
"right_heel",
|
|
755
|
+
"left_foot_index",
|
|
756
|
+
"right_foot_index",
|
|
757
|
+
]
|
|
910
758
|
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
return (0.5, 0.5)
|
|
759
|
+
x_positions = []
|
|
760
|
+
y_positions = []
|
|
914
761
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
762
|
+
for key in foot_keys:
|
|
763
|
+
if key in landmarks:
|
|
764
|
+
x, y, visibility = landmarks[key]
|
|
765
|
+
if visibility > 0.5: # Only use visible landmarks
|
|
766
|
+
x_positions.append(x)
|
|
767
|
+
y_positions.append(y)
|
|
920
768
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
if low_vis:
|
|
924
|
-
xs, ys = zip(*low_vis, strict=False)
|
|
925
|
-
return (float(np.mean(xs)), float(np.mean(ys)))
|
|
769
|
+
if not x_positions:
|
|
770
|
+
return (0.5, 0.5) # Default to center if no visible feet
|
|
926
771
|
|
|
927
|
-
|
|
928
|
-
best = max(foot_data, key=lambda t: t[2])
|
|
929
|
-
return (best[0], best[1])
|
|
772
|
+
return (float(np.mean(x_positions)), float(np.mean(y_positions)))
|
|
930
773
|
|
|
931
774
|
|
|
932
775
|
def _calculate_average_visibility(
|
|
@@ -940,13 +783,8 @@ def _calculate_average_visibility(
|
|
|
940
783
|
Returns:
|
|
941
784
|
Average visibility of foot landmarks (0.0 if none visible)
|
|
942
785
|
"""
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
foot_vis = [
|
|
946
|
-
frame_landmarks[key][2]
|
|
947
|
-
for key in FOOT_KEYS
|
|
948
|
-
if key in frame_landmarks and key in visibility_keys
|
|
949
|
-
]
|
|
786
|
+
foot_keys = ["left_ankle", "right_ankle", "left_heel", "right_heel"]
|
|
787
|
+
foot_vis = [frame_landmarks[key][2] for key in foot_keys if key in frame_landmarks]
|
|
950
788
|
return float(np.mean(foot_vis)) if foot_vis else 0.0
|
|
951
789
|
|
|
952
790
|
|
|
@@ -954,7 +792,7 @@ def _calculate_average_visibility(
|
|
|
954
792
|
reason="Alternative implementation not called by pipeline",
|
|
955
793
|
since="0.34.0",
|
|
956
794
|
)
|
|
957
|
-
def
|
|
795
|
+
def extract_foot_positions_and_visibilities(
|
|
958
796
|
smoothed_landmarks: list[dict[str, tuple[float, float, float]] | None],
|
|
959
797
|
) -> tuple[np.ndarray, np.ndarray]:
|
|
960
798
|
"""
|