kinemotion 0.30.0__py3-none-any.whl → 0.31.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/api.py +13 -7
- kinemotion/cmj/analysis.py +6 -3
- kinemotion/cmj/cli.py +11 -6
- kinemotion/cmj/debug_overlay.py +3 -3
- kinemotion/cmj/kinematics.py +10 -5
- kinemotion/core/cli_utils.py +4 -2
- kinemotion/core/cmj_metrics_validator.py +40 -21
- kinemotion/core/dropjump_metrics_validator.py +8 -5
- kinemotion/core/metadata.py +2 -1
- kinemotion/core/quality.py +5 -4
- kinemotion/core/smoothing.py +4 -2
- kinemotion/core/video_io.py +3 -2
- kinemotion/dropjump/analysis.py +12 -7
- kinemotion/dropjump/cli.py +7 -4
- kinemotion/dropjump/kinematics.py +6 -3
- {kinemotion-0.30.0.dist-info → kinemotion-0.31.1.dist-info}/METADATA +1 -1
- kinemotion-0.31.1.dist-info/RECORD +35 -0
- {kinemotion-0.30.0.dist-info → kinemotion-0.31.1.dist-info}/WHEEL +1 -1
- kinemotion-0.30.0.dist-info/RECORD +0 -35
- {kinemotion-0.30.0.dist-info → kinemotion-0.31.1.dist-info}/entry_points.txt +0 -0
- {kinemotion-0.30.0.dist-info → kinemotion-0.31.1.dist-info}/licenses/LICENSE +0 -0
kinemotion/api.py
CHANGED
|
@@ -62,7 +62,8 @@ def _parse_quality_preset(quality: str) -> QualityPreset:
|
|
|
62
62
|
return QualityPreset(quality.lower())
|
|
63
63
|
except ValueError as e:
|
|
64
64
|
raise ValueError(
|
|
65
|
-
f"Invalid quality preset: {quality}.
|
|
65
|
+
f"Invalid quality preset: {quality}. "
|
|
66
|
+
"Must be 'fast', 'balanced', or 'accurate'"
|
|
66
67
|
) from e
|
|
67
68
|
|
|
68
69
|
|
|
@@ -501,7 +502,8 @@ def process_dropjump_video(
|
|
|
501
502
|
if verbose:
|
|
502
503
|
print("Assessing tracking quality...")
|
|
503
504
|
|
|
504
|
-
# Detect outliers for quality scoring (doesn't affect results, just
|
|
505
|
+
# Detect outliers for quality scoring (doesn't affect results, just
|
|
506
|
+
# for assessment)
|
|
505
507
|
_, outlier_mask = reject_outliers(
|
|
506
508
|
vertical_positions,
|
|
507
509
|
use_ransac=True,
|
|
@@ -626,9 +628,11 @@ def process_dropjump_videos_bulk(
|
|
|
626
628
|
Process multiple drop jump videos in parallel using ProcessPoolExecutor.
|
|
627
629
|
|
|
628
630
|
Args:
|
|
629
|
-
configs: List of DropJumpVideoConfig objects specifying video paths
|
|
631
|
+
configs: List of DropJumpVideoConfig objects specifying video paths
|
|
632
|
+
and parameters
|
|
630
633
|
max_workers: Maximum number of parallel workers (default: 4)
|
|
631
|
-
progress_callback: Optional callback function called after each video
|
|
634
|
+
progress_callback: Optional callback function called after each video
|
|
635
|
+
completes.
|
|
632
636
|
Receives DropJumpVideoResult object.
|
|
633
637
|
|
|
634
638
|
Returns:
|
|
@@ -953,7 +957,8 @@ def process_cmj_video(
|
|
|
953
957
|
if verbose:
|
|
954
958
|
print("Assessing tracking quality...")
|
|
955
959
|
|
|
956
|
-
# Detect outliers for quality scoring (doesn't affect results, just
|
|
960
|
+
# Detect outliers for quality scoring (doesn't affect results, just
|
|
961
|
+
# for assessment)
|
|
957
962
|
_, outlier_mask = reject_outliers(
|
|
958
963
|
vertical_positions,
|
|
959
964
|
use_ransac=True,
|
|
@@ -1047,7 +1052,7 @@ def process_cmj_video(
|
|
|
1047
1052
|
|
|
1048
1053
|
if verbose:
|
|
1049
1054
|
print(f"\nJump height: {metrics.jump_height:.3f}m")
|
|
1050
|
-
print(f"Flight time: {metrics.flight_time*1000:.1f}ms")
|
|
1055
|
+
print(f"Flight time: {metrics.flight_time * 1000:.1f}ms")
|
|
1051
1056
|
print(f"Countermovement depth: {metrics.countermovement_depth:.3f}m")
|
|
1052
1057
|
|
|
1053
1058
|
# Validate metrics against physiological bounds
|
|
@@ -1126,7 +1131,8 @@ def process_cmj_videos_bulk(
|
|
|
1126
1131
|
|
|
1127
1132
|
def _process_cmj_video_wrapper(config: CMJVideoConfig) -> CMJVideoResult:
|
|
1128
1133
|
"""
|
|
1129
|
-
Wrapper function for parallel CMJ processing. Must be picklable
|
|
1134
|
+
Wrapper function for parallel CMJ processing. Must be picklable
|
|
1135
|
+
(top-level function).
|
|
1130
1136
|
|
|
1131
1137
|
Args:
|
|
1132
1138
|
config: CMJVideoConfig object with processing parameters
|
kinemotion/cmj/analysis.py
CHANGED
|
@@ -62,7 +62,8 @@ def find_standing_phase(
|
|
|
62
62
|
"""
|
|
63
63
|
Find the end of standing phase (start of countermovement).
|
|
64
64
|
|
|
65
|
-
Looks for a period of low velocity (standing) followed by consistent
|
|
65
|
+
Looks for a period of low velocity (standing) followed by consistent
|
|
66
|
+
downward motion.
|
|
66
67
|
|
|
67
68
|
Args:
|
|
68
69
|
positions: Array of vertical positions (normalized 0-1)
|
|
@@ -109,11 +110,13 @@ def find_countermovement_start(
|
|
|
109
110
|
"""
|
|
110
111
|
Find the start of countermovement (eccentric phase).
|
|
111
112
|
|
|
112
|
-
Detects when velocity becomes consistently positive (downward motion in
|
|
113
|
+
Detects when velocity becomes consistently positive (downward motion in
|
|
114
|
+
normalized coords).
|
|
113
115
|
|
|
114
116
|
Args:
|
|
115
117
|
velocities: Array of SIGNED vertical velocities
|
|
116
|
-
countermovement_threshold: Velocity threshold for detecting downward
|
|
118
|
+
countermovement_threshold: Velocity threshold for detecting downward
|
|
119
|
+
motion (POSITIVE)
|
|
117
120
|
min_eccentric_frames: Minimum consecutive frames of downward motion
|
|
118
121
|
standing_start: Optional frame where standing phase ended
|
|
119
122
|
|
kinemotion/cmj/cli.py
CHANGED
|
@@ -177,7 +177,8 @@ def _process_batch_videos(
|
|
|
177
177
|
default=None,
|
|
178
178
|
help="[EXPERT] Override pose tracking confidence",
|
|
179
179
|
)
|
|
180
|
-
def cmj_analyze( # NOSONAR(S107) - Click CLI requires individual parameters
|
|
180
|
+
def cmj_analyze( # NOSONAR(S107) - Click CLI requires individual parameters
|
|
181
|
+
# for each option
|
|
181
182
|
video_path: tuple[str, ...],
|
|
182
183
|
output: str | None,
|
|
183
184
|
json_output: str | None,
|
|
@@ -197,10 +198,12 @@ def cmj_analyze( # NOSONAR(S107) - Click CLI requires individual parameters for
|
|
|
197
198
|
tracking_confidence: float | None,
|
|
198
199
|
) -> None:
|
|
199
200
|
"""
|
|
200
|
-
Analyze counter movement jump (CMJ) video(s) to estimate jump performance
|
|
201
|
+
Analyze counter movement jump (CMJ) video(s) to estimate jump performance
|
|
202
|
+
metrics.
|
|
201
203
|
|
|
202
|
-
Uses intelligent auto-tuning to select optimal parameters based on video
|
|
203
|
-
Parameters are automatically adjusted for frame rate,
|
|
204
|
+
Uses intelligent auto-tuning to select optimal parameters based on video
|
|
205
|
+
characteristics. Parameters are automatically adjusted for frame rate,
|
|
206
|
+
tracking quality, and analysis preset.
|
|
204
207
|
|
|
205
208
|
VIDEO_PATH: Path(s) to video file(s). Supports glob patterns in batch mode.
|
|
206
209
|
|
|
@@ -341,11 +344,13 @@ def _output_results(metrics: Any, json_output: str | None) -> None:
|
|
|
341
344
|
f"Total movement time: {metrics.total_movement_time * 1000:.1f} ms", err=True
|
|
342
345
|
)
|
|
343
346
|
click.echo(
|
|
344
|
-
f"Peak eccentric velocity: {abs(metrics.peak_eccentric_velocity):.3f}
|
|
347
|
+
f"Peak eccentric velocity: {abs(metrics.peak_eccentric_velocity):.3f} "
|
|
348
|
+
"m/s (downward)",
|
|
345
349
|
err=True,
|
|
346
350
|
)
|
|
347
351
|
click.echo(
|
|
348
|
-
f"Peak concentric velocity: {metrics.peak_concentric_velocity:.3f}
|
|
352
|
+
f"Peak concentric velocity: {metrics.peak_concentric_velocity:.3f} "
|
|
353
|
+
"m/s (upward)",
|
|
349
354
|
err=True,
|
|
350
355
|
)
|
|
351
356
|
if metrics.transition_time is not None:
|
kinemotion/cmj/debug_overlay.py
CHANGED
|
@@ -370,10 +370,10 @@ class CMJDebugOverlayRenderer(BaseDebugOverlayRenderer):
|
|
|
370
370
|
|
|
371
371
|
metrics_text = [
|
|
372
372
|
f"Jump Height: {metrics.jump_height:.3f}m",
|
|
373
|
-
f"Flight Time: {metrics.flight_time*1000:.0f}ms",
|
|
373
|
+
f"Flight Time: {metrics.flight_time * 1000:.0f}ms",
|
|
374
374
|
f"CM Depth: {metrics.countermovement_depth:.3f}m",
|
|
375
|
-
f"Ecc Duration: {metrics.eccentric_duration*1000:.0f}ms",
|
|
376
|
-
f"Con Duration: {metrics.concentric_duration*1000:.0f}ms",
|
|
375
|
+
f"Ecc Duration: {metrics.eccentric_duration * 1000:.0f}ms",
|
|
376
|
+
f"Con Duration: {metrics.concentric_duration * 1000:.0f}ms",
|
|
377
377
|
]
|
|
378
378
|
|
|
379
379
|
# Draw background
|
kinemotion/cmj/kinematics.py
CHANGED
|
@@ -48,12 +48,17 @@ class CMJMetrics:
|
|
|
48
48
|
Attributes:
|
|
49
49
|
jump_height: Maximum jump height in meters
|
|
50
50
|
flight_time: Time spent in the air in milliseconds
|
|
51
|
-
countermovement_depth: Vertical distance traveled during eccentric
|
|
52
|
-
|
|
51
|
+
countermovement_depth: Vertical distance traveled during eccentric
|
|
52
|
+
phase in meters
|
|
53
|
+
eccentric_duration: Time from countermovement start to lowest point in
|
|
54
|
+
milliseconds
|
|
53
55
|
concentric_duration: Time from lowest point to takeoff in milliseconds
|
|
54
|
-
total_movement_time: Total time from countermovement start to takeoff
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
total_movement_time: Total time from countermovement start to takeoff
|
|
57
|
+
in milliseconds
|
|
58
|
+
peak_eccentric_velocity: Maximum downward velocity during
|
|
59
|
+
countermovement in m/s
|
|
60
|
+
peak_concentric_velocity: Maximum upward velocity during propulsion in
|
|
61
|
+
m/s
|
|
57
62
|
transition_time: Duration at lowest point (amortization phase) in milliseconds
|
|
58
63
|
standing_start_frame: Frame where standing phase ends (countermovement begins)
|
|
59
64
|
lowest_point_frame: Frame at lowest point of countermovement
|
kinemotion/core/cli_utils.py
CHANGED
|
@@ -120,8 +120,10 @@ def print_auto_tuned_params(
|
|
|
120
120
|
video: Video processor
|
|
121
121
|
quality_preset: Quality preset
|
|
122
122
|
params: Auto-tuned parameters
|
|
123
|
-
characteristics: Optional video characteristics (for tracking quality
|
|
124
|
-
|
|
123
|
+
characteristics: Optional video characteristics (for tracking quality
|
|
124
|
+
display)
|
|
125
|
+
extra_params: Optional extra parameters to display (e.g.,
|
|
126
|
+
countermovement_threshold)
|
|
125
127
|
"""
|
|
126
128
|
click.echo("\n" + "=" * 60, err=True)
|
|
127
129
|
click.echo("AUTO-TUNED PARAMETERS", err=True)
|
|
@@ -236,7 +236,8 @@ class CMJMetricsValidator:
|
|
|
236
236
|
elif bounds.contains(flight_time, profile):
|
|
237
237
|
result.add_info(
|
|
238
238
|
"flight_time",
|
|
239
|
-
f"Flight time {flight_time:.3f}s within expected range for
|
|
239
|
+
f"Flight time {flight_time:.3f}s within expected range for "
|
|
240
|
+
f"{profile.value}",
|
|
240
241
|
value=flight_time,
|
|
241
242
|
)
|
|
242
243
|
else:
|
|
@@ -278,7 +279,8 @@ class CMJMetricsValidator:
|
|
|
278
279
|
elif bounds.contains(jump_height, profile):
|
|
279
280
|
result.add_info(
|
|
280
281
|
"jump_height",
|
|
281
|
-
f"Jump height {jump_height:.3f}m within expected range for
|
|
282
|
+
f"Jump height {jump_height:.3f}m within expected range for "
|
|
283
|
+
f"{profile.value}",
|
|
282
284
|
value=jump_height,
|
|
283
285
|
)
|
|
284
286
|
else:
|
|
@@ -319,7 +321,8 @@ class CMJMetricsValidator:
|
|
|
319
321
|
elif bounds.contains(depth, profile):
|
|
320
322
|
result.add_info(
|
|
321
323
|
"countermovement_depth",
|
|
322
|
-
f"Countermovement depth {depth:.3f}m within expected range for
|
|
324
|
+
f"Countermovement depth {depth:.3f}m within expected range for "
|
|
325
|
+
f"{profile.value}",
|
|
323
326
|
value=depth,
|
|
324
327
|
)
|
|
325
328
|
else:
|
|
@@ -353,14 +356,16 @@ class CMJMetricsValidator:
|
|
|
353
356
|
else:
|
|
354
357
|
result.add_error(
|
|
355
358
|
"concentric_duration",
|
|
356
|
-
f"Concentric duration {duration:.3f}s likely includes
|
|
359
|
+
f"Concentric duration {duration:.3f}s likely includes "
|
|
360
|
+
"standing phase",
|
|
357
361
|
value=duration,
|
|
358
362
|
bounds=(bounds.absolute_min, bounds.absolute_max),
|
|
359
363
|
)
|
|
360
364
|
elif bounds.contains(duration, profile):
|
|
361
365
|
result.add_info(
|
|
362
366
|
"concentric_duration",
|
|
363
|
-
f"Concentric duration {duration:.3f}s within expected range for
|
|
367
|
+
f"Concentric duration {duration:.3f}s within expected range for "
|
|
368
|
+
f"{profile.value}",
|
|
364
369
|
value=duration,
|
|
365
370
|
)
|
|
366
371
|
else:
|
|
@@ -393,7 +398,8 @@ class CMJMetricsValidator:
|
|
|
393
398
|
elif bounds.contains(duration, profile):
|
|
394
399
|
result.add_info(
|
|
395
400
|
"eccentric_duration",
|
|
396
|
-
f"Eccentric duration {duration:.3f}s within expected range for
|
|
401
|
+
f"Eccentric duration {duration:.3f}s within expected range for "
|
|
402
|
+
f"{profile.value}",
|
|
397
403
|
value=duration,
|
|
398
404
|
)
|
|
399
405
|
else:
|
|
@@ -424,7 +430,8 @@ class CMJMetricsValidator:
|
|
|
424
430
|
elif bounds.contains(ecc_vel, profile):
|
|
425
431
|
result.add_info(
|
|
426
432
|
"peak_eccentric_velocity",
|
|
427
|
-
f"Peak eccentric velocity {ecc_vel:.2f} m/s within range
|
|
433
|
+
f"Peak eccentric velocity {ecc_vel:.2f} m/s within range "
|
|
434
|
+
f"for {profile.value}",
|
|
428
435
|
value=ecc_vel,
|
|
429
436
|
)
|
|
430
437
|
else:
|
|
@@ -445,21 +452,24 @@ class CMJMetricsValidator:
|
|
|
445
452
|
if con_vel < bounds.absolute_min:
|
|
446
453
|
result.add_error(
|
|
447
454
|
"peak_concentric_velocity",
|
|
448
|
-
f"Peak concentric velocity {con_vel:.2f} m/s
|
|
455
|
+
f"Peak concentric velocity {con_vel:.2f} m/s "
|
|
456
|
+
"insufficient to leave ground",
|
|
449
457
|
value=con_vel,
|
|
450
458
|
bounds=(bounds.absolute_min, bounds.absolute_max),
|
|
451
459
|
)
|
|
452
460
|
else:
|
|
453
461
|
result.add_error(
|
|
454
462
|
"peak_concentric_velocity",
|
|
455
|
-
f"Peak concentric velocity {con_vel:.2f} m/s exceeds
|
|
463
|
+
f"Peak concentric velocity {con_vel:.2f} m/s exceeds "
|
|
464
|
+
"elite capability",
|
|
456
465
|
value=con_vel,
|
|
457
466
|
bounds=(bounds.absolute_min, bounds.absolute_max),
|
|
458
467
|
)
|
|
459
468
|
elif bounds.contains(con_vel, profile):
|
|
460
469
|
result.add_info(
|
|
461
470
|
"peak_concentric_velocity",
|
|
462
|
-
f"Peak concentric velocity {con_vel:.2f} m/s within range
|
|
471
|
+
f"Peak concentric velocity {con_vel:.2f} m/s within range "
|
|
472
|
+
f"for {profile.value}",
|
|
463
473
|
value=con_vel,
|
|
464
474
|
)
|
|
465
475
|
else:
|
|
@@ -492,15 +502,17 @@ class CMJMetricsValidator:
|
|
|
492
502
|
if error_pct > MetricConsistency.HEIGHT_FLIGHT_TIME_TOLERANCE:
|
|
493
503
|
result.add_error(
|
|
494
504
|
"height_flight_time_consistency",
|
|
495
|
-
f"Jump height {jump_height:.3f}m inconsistent with flight
|
|
496
|
-
f"(expected {expected_height:.3f}m,
|
|
505
|
+
f"Jump height {jump_height:.3f}m inconsistent with flight "
|
|
506
|
+
f"time {flight_time:.3f}s (expected {expected_height:.3f}m, "
|
|
507
|
+
f"error {error_pct * 100:.1f}%)",
|
|
497
508
|
value=error_pct,
|
|
498
509
|
bounds=(0, MetricConsistency.HEIGHT_FLIGHT_TIME_TOLERANCE),
|
|
499
510
|
)
|
|
500
511
|
else:
|
|
501
512
|
result.add_info(
|
|
502
513
|
"height_flight_time_consistency",
|
|
503
|
-
f"Jump height and flight time consistent
|
|
514
|
+
f"Jump height and flight time consistent "
|
|
515
|
+
f"(error {error_pct * 100:.1f}%)",
|
|
504
516
|
value=error_pct,
|
|
505
517
|
)
|
|
506
518
|
|
|
@@ -525,7 +537,7 @@ class CMJMetricsValidator:
|
|
|
525
537
|
error_msg = (
|
|
526
538
|
f"Peak velocity {velocity:.2f} m/s inconsistent with "
|
|
527
539
|
f"jump height {jump_height:.3f}m (expected {expected_velocity:.2f} "
|
|
528
|
-
f"m/s, error {error_pct*100:.1f}%)"
|
|
540
|
+
f"m/s, error {error_pct * 100:.1f}%)"
|
|
529
541
|
)
|
|
530
542
|
result.add_warning(
|
|
531
543
|
"velocity_height_consistency",
|
|
@@ -536,7 +548,8 @@ class CMJMetricsValidator:
|
|
|
536
548
|
else:
|
|
537
549
|
result.add_info(
|
|
538
550
|
"velocity_height_consistency",
|
|
539
|
-
f"Peak velocity and jump height consistent
|
|
551
|
+
f"Peak velocity and jump height consistent "
|
|
552
|
+
f"(error {error_pct * 100:.1f}%)",
|
|
540
553
|
value=error_pct,
|
|
541
554
|
)
|
|
542
555
|
|
|
@@ -577,14 +590,16 @@ class CMJMetricsValidator:
|
|
|
577
590
|
if expected_min <= rsi <= expected_max:
|
|
578
591
|
result.add_info(
|
|
579
592
|
"rsi",
|
|
580
|
-
f"RSI {rsi:.2f} within expected range
|
|
593
|
+
f"RSI {rsi:.2f} within expected range "
|
|
594
|
+
f"[{expected_min:.2f}-{expected_max:.2f}] "
|
|
581
595
|
f"for {profile.value}",
|
|
582
596
|
value=rsi,
|
|
583
597
|
)
|
|
584
598
|
else:
|
|
585
599
|
result.add_warning(
|
|
586
600
|
"rsi",
|
|
587
|
-
f"RSI {rsi:.2f} outside typical range
|
|
601
|
+
f"RSI {rsi:.2f} outside typical range "
|
|
602
|
+
f"[{expected_min:.2f}-{expected_max:.2f}] "
|
|
588
603
|
f"for {profile.value}",
|
|
589
604
|
value=rsi,
|
|
590
605
|
bounds=(expected_min, expected_max),
|
|
@@ -650,7 +665,8 @@ class CMJMetricsValidator:
|
|
|
650
665
|
if ratio < MetricConsistency.CONTACT_DEPTH_RATIO_MIN:
|
|
651
666
|
result.add_warning(
|
|
652
667
|
"contact_depth_ratio",
|
|
653
|
-
f"Contact time {ratio:.2f}s/m to depth ratio: Very fast for
|
|
668
|
+
f"Contact time {ratio:.2f}s/m to depth ratio: Very fast for "
|
|
669
|
+
"depth traversed",
|
|
654
670
|
value=ratio,
|
|
655
671
|
bounds=(
|
|
656
672
|
MetricConsistency.CONTACT_DEPTH_RATIO_MIN,
|
|
@@ -702,7 +718,8 @@ class CMJMetricsValidator:
|
|
|
702
718
|
if not TripleExtensionBounds.knee_angle_valid(knee, profile):
|
|
703
719
|
result.add_warning(
|
|
704
720
|
"knee_angle",
|
|
705
|
-
f"Knee angle {knee:.1f}° outside expected range for
|
|
721
|
+
f"Knee angle {knee:.1f}° outside expected range for "
|
|
722
|
+
f"{profile.value}",
|
|
706
723
|
value=knee,
|
|
707
724
|
)
|
|
708
725
|
else:
|
|
@@ -717,13 +734,15 @@ class CMJMetricsValidator:
|
|
|
717
734
|
if not TripleExtensionBounds.ankle_angle_valid(ankle, profile):
|
|
718
735
|
result.add_warning(
|
|
719
736
|
"ankle_angle",
|
|
720
|
-
f"Ankle angle {ankle:.1f}° outside expected range for
|
|
737
|
+
f"Ankle angle {ankle:.1f}° outside expected range for "
|
|
738
|
+
f"{profile.value}",
|
|
721
739
|
value=ankle,
|
|
722
740
|
)
|
|
723
741
|
else:
|
|
724
742
|
result.add_info(
|
|
725
743
|
"ankle_angle",
|
|
726
|
-
f"Ankle angle {ankle:.1f}° within expected range for
|
|
744
|
+
f"Ankle angle {ankle:.1f}° within expected range for "
|
|
745
|
+
f"{profile.value}",
|
|
727
746
|
value=ankle,
|
|
728
747
|
)
|
|
729
748
|
|
|
@@ -227,7 +227,8 @@ class DropJumpMetricsValidator:
|
|
|
227
227
|
profile_name = result.athlete_profile.value
|
|
228
228
|
result.add_warning(
|
|
229
229
|
"contact_time",
|
|
230
|
-
f"Contact time {contact_time_s:.3f}s unusual for
|
|
230
|
+
f"Contact time {contact_time_s:.3f}s unusual for "
|
|
231
|
+
f"{profile_name} athlete",
|
|
231
232
|
value=contact_time_s,
|
|
232
233
|
)
|
|
233
234
|
|
|
@@ -285,7 +286,7 @@ class DropJumpMetricsValidator:
|
|
|
285
286
|
contact_time_s = contact_time_ms / 1000.0
|
|
286
287
|
flight_time_s = flight_time_ms / 1000.0
|
|
287
288
|
|
|
288
|
-
if contact_time_s > 0:
|
|
289
|
+
if contact_time_s > 0 and flight_time_s > 0:
|
|
289
290
|
rsi = flight_time_s / contact_time_s
|
|
290
291
|
result.rsi = rsi
|
|
291
292
|
result.contact_flight_ratio = contact_time_s / flight_time_s
|
|
@@ -317,7 +318,8 @@ class DropJumpMetricsValidator:
|
|
|
317
318
|
"""Validate consistency between kinematic and trajectory-based heights.
|
|
318
319
|
|
|
319
320
|
Kinematic height (h = g*t²/8) comes from flight time (objective).
|
|
320
|
-
Trajectory height comes from position tracking (subject to landmark
|
|
321
|
+
Trajectory height comes from position tracking (subject to landmark
|
|
322
|
+
detection noise).
|
|
321
323
|
|
|
322
324
|
Expected correlation: r > 0.95, absolute difference < 5% for quality video.
|
|
323
325
|
"""
|
|
@@ -336,8 +338,9 @@ class DropJumpMetricsValidator:
|
|
|
336
338
|
result.add_warning(
|
|
337
339
|
"height_consistency",
|
|
338
340
|
f"Kinematic ({jump_height_kinematic_m:.3f}m) and trajectory "
|
|
339
|
-
f"({jump_height_trajectory_m:.3f}m) heights differ by
|
|
340
|
-
f"May indicate landmark detection
|
|
341
|
+
f"({jump_height_trajectory_m:.3f}m) heights differ by "
|
|
342
|
+
f"{percent_error:.1f}%. May indicate landmark detection "
|
|
343
|
+
"issues or video quality problems.",
|
|
341
344
|
value=percent_error,
|
|
342
345
|
bounds=(0, 10),
|
|
343
346
|
)
|
kinemotion/core/metadata.py
CHANGED
|
@@ -146,7 +146,8 @@ class AlgorithmConfig:
|
|
|
146
146
|
"""Complete algorithm configuration for reproducibility.
|
|
147
147
|
|
|
148
148
|
Attributes:
|
|
149
|
-
detection_method: Algorithm used ("backward_search" for CMJ,
|
|
149
|
+
detection_method: Algorithm used ("backward_search" for CMJ,
|
|
150
|
+
"forward_search" for drop)
|
|
150
151
|
tracking_method: Pose tracking method ("mediapipe_pose")
|
|
151
152
|
model_complexity: MediaPipe model complexity (0, 1, or 2)
|
|
152
153
|
smoothing: Smoothing configuration
|
kinemotion/core/quality.py
CHANGED
|
@@ -307,8 +307,9 @@ def _generate_warnings(
|
|
|
307
307
|
# Tracking stability warnings
|
|
308
308
|
if not indicators.tracking_stable:
|
|
309
309
|
warnings.append(
|
|
310
|
-
f"Unstable landmark tracking detected
|
|
311
|
-
"This may indicate
|
|
310
|
+
f"Unstable landmark tracking detected "
|
|
311
|
+
f"(variance {indicators.position_variance:.4f}). This may indicate "
|
|
312
|
+
"jitter or occlusion. Consider better lighting or camera position."
|
|
312
313
|
)
|
|
313
314
|
|
|
314
315
|
# Outlier warnings
|
|
@@ -349,8 +350,8 @@ def _generate_warnings(
|
|
|
349
350
|
# Overall confidence warning
|
|
350
351
|
if confidence == "low":
|
|
351
352
|
warnings.append(
|
|
352
|
-
"⚠️ LOW CONFIDENCE: Results may be unreliable. "
|
|
353
|
-
"
|
|
353
|
+
"⚠️ LOW CONFIDENCE: Results may be unreliable. Review quality "
|
|
354
|
+
"indicators and consider re-recording with better conditions."
|
|
354
355
|
)
|
|
355
356
|
elif confidence == "medium":
|
|
356
357
|
warnings.append(
|
kinemotion/core/smoothing.py
CHANGED
|
@@ -124,7 +124,8 @@ def _store_smoothed_landmarks(
|
|
|
124
124
|
)
|
|
125
125
|
|
|
126
126
|
|
|
127
|
-
def _smooth_landmarks_core( # NOSONAR(S1172) - polyorder used via closure
|
|
127
|
+
def _smooth_landmarks_core( # NOSONAR(S1172) - polyorder used via closure
|
|
128
|
+
# capture in smoother_fn
|
|
128
129
|
landmark_sequence: LandmarkSequence,
|
|
129
130
|
window_length: int,
|
|
130
131
|
polyorder: int,
|
|
@@ -136,7 +137,8 @@ def _smooth_landmarks_core( # NOSONAR(S1172) - polyorder used via closure captu
|
|
|
136
137
|
Args:
|
|
137
138
|
landmark_sequence: List of landmark dictionaries from each frame
|
|
138
139
|
window_length: Length of filter window (must be odd)
|
|
139
|
-
polyorder: Order of polynomial used to fit samples (captured by
|
|
140
|
+
polyorder: Order of polynomial used to fit samples (captured by
|
|
141
|
+
smoother_fn closure)
|
|
140
142
|
smoother_fn: Function that takes (x_coords, y_coords, valid_frames)
|
|
141
143
|
and returns (x_smooth, y_smooth)
|
|
142
144
|
|
kinemotion/core/video_io.py
CHANGED
|
@@ -46,8 +46,9 @@ class VideoProcessor:
|
|
|
46
46
|
self.width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
47
47
|
self.height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
48
48
|
|
|
49
|
-
# Extract rotation metadata from video (iPhones store rotation in
|
|
50
|
-
# OpenCV ignores rotation metadata, so we need to
|
|
49
|
+
# Extract rotation metadata from video (iPhones store rotation in
|
|
50
|
+
# side_data_list). OpenCV ignores rotation metadata, so we need to
|
|
51
|
+
# extract and apply it manually
|
|
51
52
|
self.rotation = 0 # Will be set by _extract_video_metadata()
|
|
52
53
|
|
|
53
54
|
# Extract codec information from video metadata
|
kinemotion/dropjump/analysis.py
CHANGED
|
@@ -98,7 +98,8 @@ def _find_stable_baseline(
|
|
|
98
98
|
"""Find first stable period and return baseline position.
|
|
99
99
|
|
|
100
100
|
Returns:
|
|
101
|
-
Tuple of (baseline_start_frame, baseline_position). Returns (-1, 0.0)
|
|
101
|
+
Tuple of (baseline_start_frame, baseline_position). Returns (-1, 0.0)
|
|
102
|
+
if not found.
|
|
102
103
|
"""
|
|
103
104
|
stable_window = min_stable_frames
|
|
104
105
|
|
|
@@ -159,8 +160,8 @@ def _find_drop_from_baseline(
|
|
|
159
160
|
f"{position_change_threshold:.4f}"
|
|
160
161
|
)
|
|
161
162
|
print(
|
|
162
|
-
f" avg_position: {avg_position:.4f} vs "
|
|
163
|
-
f"
|
|
163
|
+
f" avg_position: {avg_position:.4f} vs baseline: "
|
|
164
|
+
f"{baseline_position:.4f}"
|
|
164
165
|
)
|
|
165
166
|
|
|
166
167
|
return drop_frame
|
|
@@ -179,7 +180,8 @@ def detect_drop_start(
|
|
|
179
180
|
debug: bool = False,
|
|
180
181
|
) -> int:
|
|
181
182
|
"""
|
|
182
|
-
Detect when the drop jump actually starts by finding stable period then
|
|
183
|
+
Detect when the drop jump actually starts by finding stable period then
|
|
184
|
+
detecting drop.
|
|
183
185
|
|
|
184
186
|
Strategy:
|
|
185
187
|
1. Scan forward to find first STABLE period (low variance over N frames)
|
|
@@ -191,7 +193,8 @@ def detect_drop_start(
|
|
|
191
193
|
Args:
|
|
192
194
|
positions: Array of vertical positions (0-1 normalized, y increases downward)
|
|
193
195
|
fps: Video frame rate
|
|
194
|
-
min_stationary_duration: Minimum duration (seconds) of stable period
|
|
196
|
+
min_stationary_duration: Minimum duration (seconds) of stable period
|
|
197
|
+
(default: 1.0s)
|
|
195
198
|
position_change_threshold: Position change indicating start of drop
|
|
196
199
|
(default: 0.02 = 2% of frame)
|
|
197
200
|
smoothing_window: Window for computing position variance
|
|
@@ -832,9 +835,11 @@ def extract_foot_positions_and_visibilities(
|
|
|
832
835
|
smoothed_landmarks: list[dict[str, tuple[float, float, float]] | None],
|
|
833
836
|
) -> tuple[np.ndarray, np.ndarray]:
|
|
834
837
|
"""
|
|
835
|
-
Extract vertical positions and average visibilities from smoothed
|
|
838
|
+
Extract vertical positions and average visibilities from smoothed
|
|
839
|
+
landmarks.
|
|
836
840
|
|
|
837
|
-
This utility function eliminates code duplication between CLI and
|
|
841
|
+
This utility function eliminates code duplication between CLI and
|
|
842
|
+
programmatic usage.
|
|
838
843
|
|
|
839
844
|
Args:
|
|
840
845
|
smoothed_landmarks: Smoothed landmark sequence from tracking
|
kinemotion/dropjump/cli.py
CHANGED
|
@@ -133,7 +133,8 @@ class AnalysisParameters:
|
|
|
133
133
|
default=None,
|
|
134
134
|
help="[EXPERT] Override pose tracking confidence",
|
|
135
135
|
)
|
|
136
|
-
def dropjump_analyze( # NOSONAR(S107) - Click CLI requires individual
|
|
136
|
+
def dropjump_analyze( # NOSONAR(S107) - Click CLI requires individual
|
|
137
|
+
# parameters for each option
|
|
137
138
|
video_path: tuple[str, ...],
|
|
138
139
|
output: str | None,
|
|
139
140
|
json_output: str | None,
|
|
@@ -153,10 +154,12 @@ def dropjump_analyze( # NOSONAR(S107) - Click CLI requires individual parameter
|
|
|
153
154
|
tracking_confidence: float | None,
|
|
154
155
|
) -> None:
|
|
155
156
|
"""
|
|
156
|
-
Analyze drop-jump video(s) to estimate ground contact time, flight time,
|
|
157
|
+
Analyze drop-jump video(s) to estimate ground contact time, flight time,
|
|
158
|
+
and jump height.
|
|
157
159
|
|
|
158
|
-
Uses intelligent auto-tuning to select optimal parameters based on video
|
|
159
|
-
Parameters are automatically adjusted for frame rate,
|
|
160
|
+
Uses intelligent auto-tuning to select optimal parameters based on video
|
|
161
|
+
characteristics. Parameters are automatically adjusted for frame rate,
|
|
162
|
+
tracking quality, and analysis preset.
|
|
160
163
|
|
|
161
164
|
VIDEO_PATH: Path(s) to video file(s). Supports glob patterns in batch mode
|
|
162
165
|
(e.g., "videos/*.mp4").
|
|
@@ -235,7 +235,8 @@ def _identify_main_contact_phase(
|
|
|
235
235
|
]
|
|
236
236
|
|
|
237
237
|
if ground_after_air and first_ground_idx < first_air_idx:
|
|
238
|
-
# Check if first ground is at higher elevation (lower y) than
|
|
238
|
+
# Check if first ground is at higher elevation (lower y) than
|
|
239
|
+
# ground after air
|
|
239
240
|
first_ground_y = float(
|
|
240
241
|
np.mean(foot_y_positions[first_ground_start : first_ground_end + 1])
|
|
241
242
|
)
|
|
@@ -394,8 +395,10 @@ def calculate_drop_jump_metrics(
|
|
|
394
395
|
foot_y_positions: Vertical positions of feet (normalized 0-1)
|
|
395
396
|
fps: Video frame rate
|
|
396
397
|
drop_start_frame: Optional manual drop start frame
|
|
397
|
-
velocity_threshold: Velocity threshold used for contact detection
|
|
398
|
-
|
|
398
|
+
velocity_threshold: Velocity threshold used for contact detection
|
|
399
|
+
(for interpolation)
|
|
400
|
+
smoothing_window: Window size for velocity/acceleration smoothing
|
|
401
|
+
(must be odd)
|
|
399
402
|
polyorder: Polynomial order for Savitzky-Golay filter (default: 2)
|
|
400
403
|
use_curvature: Whether to use curvature analysis for refining transitions
|
|
401
404
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kinemotion
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.31.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
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
kinemotion/__init__.py,sha256=sxdDOekOrIgjxm842gy-6zfq7OWmGl9ShJtXCm4JI7c,723
|
|
2
|
+
kinemotion/api.py,sha256=IcvtjK38Eoh5u53yu99NaeChLDNvv7NSnlDcK8fhVS8,39349
|
|
3
|
+
kinemotion/cli.py,sha256=cqYV_7URH0JUDy1VQ_EDLv63FmNO4Ns20m6s1XAjiP4,464
|
|
4
|
+
kinemotion/cmj/__init__.py,sha256=Ynv0-Oco4I3Y1Ubj25m3h9h2XFqeNwpAewXmAYOmwfU,127
|
|
5
|
+
kinemotion/cmj/analysis.py,sha256=88zRNufM88Mc4p_YdnbXoSJM-pWAe4C9I7TqcH7QM9U,19815
|
|
6
|
+
kinemotion/cmj/cli.py,sha256=Mj2h9It1jVjAauvtCxfLWTRijj7zbYhxZuebhw2Zz6w,10828
|
|
7
|
+
kinemotion/cmj/debug_overlay.py,sha256=fXmWoHhqMLGo4vTtB6Ezs3yLUDOLw63zLIgU2gFlJQU,15892
|
|
8
|
+
kinemotion/cmj/joint_angles.py,sha256=HmheIEiKcQz39cRezk4h-htorOhGNPsqKIR9RsAEKts,9960
|
|
9
|
+
kinemotion/cmj/kinematics.py,sha256=qRBe87NkX-7HQTQ8RoF-EpvfcffgP5vycJJRrxpHboc,10307
|
|
10
|
+
kinemotion/core/__init__.py,sha256=HsqolRa60cW3vrG8F9Lvr9WvWcs5hCmsTzSgo7imi-4,1278
|
|
11
|
+
kinemotion/core/auto_tuning.py,sha256=j6cul_qC6k0XyryCG93C1AWH2MKPj3UBMzuX02xaqfI,11235
|
|
12
|
+
kinemotion/core/cli_utils.py,sha256=8xQvTiZFAQULGdpB9g-Mvf0doSgoXi1ZZfVY2T2zWss,7278
|
|
13
|
+
kinemotion/core/cmj_metrics_validator.py,sha256=VVRn56pG3GKna_EDv8jQ5msYNv4VOUR5fTcqpSUzdIA,31334
|
|
14
|
+
kinemotion/core/cmj_validation_bounds.py,sha256=NXW0d4S8hDqSkzAiyX9UyWDnKsWf61ygEKTKE2a1uNc,14069
|
|
15
|
+
kinemotion/core/debug_overlay_utils.py,sha256=TyUb5okv5qw8oeaX3jsUO_kpwf1NnaHEAOTm-8LwTno,4587
|
|
16
|
+
kinemotion/core/dropjump_metrics_validator.py,sha256=zTkWslex9qGQuJSEZltUXYyFLBtisPGGxwqpYpE_Mx4,12040
|
|
17
|
+
kinemotion/core/dropjump_validation_bounds.py,sha256=Ow7T-0IK_qy0k9UZdiCuT_tttxklWAkuXFalQkF8Jbs,6927
|
|
18
|
+
kinemotion/core/filtering.py,sha256=f-m-aA59e4WqE6u-9MA51wssu7rI-Y_7n1cG8IWdeRQ,11241
|
|
19
|
+
kinemotion/core/formatting.py,sha256=G_3eqgOtym9RFOZVEwCxye4A2cyrmgvtQ214vIshowU,2480
|
|
20
|
+
kinemotion/core/metadata.py,sha256=iz9YdkesHo-85TVBCoQVn7zkbrSde_fqjU79s_b-TZk,6829
|
|
21
|
+
kinemotion/core/pose.py,sha256=ztemdZ_ysVVK3gbXabm8qS_dr1VfJX9KZjmcO-Z-iNE,8532
|
|
22
|
+
kinemotion/core/quality.py,sha256=dPGQp08y8DdEUbUdjTThnUOUsALgF0D2sdz50cm6wLI,13098
|
|
23
|
+
kinemotion/core/smoothing.py,sha256=J-GjFP6xW9TO4teOqFd5523zTY2jYIY1Nd99cZvoM3s,14110
|
|
24
|
+
kinemotion/core/video_io.py,sha256=fDdyYVIKqUSgCjBJa8l_S0SrDPDAhrWYfsDBNRuz1oM,7549
|
|
25
|
+
kinemotion/dropjump/__init__.py,sha256=yc1XiZ9vfo5h_n7PKVSiX2TTgaIfGL7Y7SkQtiDZj_E,838
|
|
26
|
+
kinemotion/dropjump/analysis.py,sha256=Sz1lnAsQSp9fzkDy8AuqQtE47LKBqNwiLkaKzz4W_Wk,29099
|
|
27
|
+
kinemotion/dropjump/cli.py,sha256=n_Wfv3AC6YIgRPYhO3F2nTSai0NR7fh95nAoWjryQeY,16250
|
|
28
|
+
kinemotion/dropjump/debug_overlay.py,sha256=LkPw6ucb7beoYWS4L-Lvjs1KLCm5wAWDAfiznUeV2IQ,5668
|
|
29
|
+
kinemotion/dropjump/kinematics.py,sha256=IH6nCOwTuocQNX1VPS_am9vPpMRUUla0a0MjDhEiXnA,17129
|
|
30
|
+
kinemotion/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
31
|
+
kinemotion-0.31.1.dist-info/METADATA,sha256=bZB-v07C8eFhrmm0QArfwocLbYOxHkRIwLJkX_5ZQxM,26020
|
|
32
|
+
kinemotion-0.31.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
33
|
+
kinemotion-0.31.1.dist-info/entry_points.txt,sha256=zaqnAnjLvcdrk1Qvj5nvXZCZ2gp0prS7it1zTJygcIY,50
|
|
34
|
+
kinemotion-0.31.1.dist-info/licenses/LICENSE,sha256=KZajvqsHw0NoOHOi2q0FZ4NBe9HdV6oey-IPYAtHXfg,1088
|
|
35
|
+
kinemotion-0.31.1.dist-info/RECORD,,
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
kinemotion/__init__.py,sha256=sxdDOekOrIgjxm842gy-6zfq7OWmGl9ShJtXCm4JI7c,723
|
|
2
|
-
kinemotion/api.py,sha256=ozt6GLZtw2ZWki0tBlkpOQgcNH_M7GfrRPlc_Rg8ROw,39284
|
|
3
|
-
kinemotion/cli.py,sha256=cqYV_7URH0JUDy1VQ_EDLv63FmNO4Ns20m6s1XAjiP4,464
|
|
4
|
-
kinemotion/cmj/__init__.py,sha256=Ynv0-Oco4I3Y1Ubj25m3h9h2XFqeNwpAewXmAYOmwfU,127
|
|
5
|
-
kinemotion/cmj/analysis.py,sha256=il7-sfM89ZetxLhmw9boViaP4E8Y3mlS_XI-B5txmMs,19795
|
|
6
|
-
kinemotion/cmj/cli.py,sha256=12FEfWrseG4kCUbgHHdBPkWp6zzVQ0VAzfgNJotArmM,10792
|
|
7
|
-
kinemotion/cmj/debug_overlay.py,sha256=D-y2FQKI01KY0WXFKTKg6p9Qj3AkXCE7xjau3Ais080,15886
|
|
8
|
-
kinemotion/cmj/joint_angles.py,sha256=HmheIEiKcQz39cRezk4h-htorOhGNPsqKIR9RsAEKts,9960
|
|
9
|
-
kinemotion/cmj/kinematics.py,sha256=ax2RijtAWItZPNRmNr-CvC7bOSsZQw2qdCEnm5hUUpU,10247
|
|
10
|
-
kinemotion/core/__init__.py,sha256=HsqolRa60cW3vrG8F9Lvr9WvWcs5hCmsTzSgo7imi-4,1278
|
|
11
|
-
kinemotion/core/auto_tuning.py,sha256=j6cul_qC6k0XyryCG93C1AWH2MKPj3UBMzuX02xaqfI,11235
|
|
12
|
-
kinemotion/core/cli_utils.py,sha256=Pq1JF7yvK1YbH0tOUWKjplthCbWsJQt4Lv7esPYH4FM,7254
|
|
13
|
-
kinemotion/core/cmj_metrics_validator.py,sha256=bbOPTbFqDEZv3lDA8qejjCcMqXE7TYvHizCcWzHRW9Y,30902
|
|
14
|
-
kinemotion/core/cmj_validation_bounds.py,sha256=NXW0d4S8hDqSkzAiyX9UyWDnKsWf61ygEKTKE2a1uNc,14069
|
|
15
|
-
kinemotion/core/debug_overlay_utils.py,sha256=TyUb5okv5qw8oeaX3jsUO_kpwf1NnaHEAOTm-8LwTno,4587
|
|
16
|
-
kinemotion/core/dropjump_metrics_validator.py,sha256=yGcg8ub6Z791qj5BxCn9mHin608tfxvxxfIeTql8HcY,11967
|
|
17
|
-
kinemotion/core/dropjump_validation_bounds.py,sha256=Ow7T-0IK_qy0k9UZdiCuT_tttxklWAkuXFalQkF8Jbs,6927
|
|
18
|
-
kinemotion/core/filtering.py,sha256=f-m-aA59e4WqE6u-9MA51wssu7rI-Y_7n1cG8IWdeRQ,11241
|
|
19
|
-
kinemotion/core/formatting.py,sha256=G_3eqgOtym9RFOZVEwCxye4A2cyrmgvtQ214vIshowU,2480
|
|
20
|
-
kinemotion/core/metadata.py,sha256=PyGHL6sx7Hj21lyorg2VsWP9BGTj_y_-wWU6eKCEfJo,6817
|
|
21
|
-
kinemotion/core/pose.py,sha256=ztemdZ_ysVVK3gbXabm8qS_dr1VfJX9KZjmcO-Z-iNE,8532
|
|
22
|
-
kinemotion/core/quality.py,sha256=OC9nuf5IrQ9xURf3eA50VoNWOqkGwbjJpS90q2FDQzA,13082
|
|
23
|
-
kinemotion/core/smoothing.py,sha256=x4o3BnG6k8OaV3emgpoJDF84CE9k5RYR7BeSYH_-8Es,14092
|
|
24
|
-
kinemotion/core/video_io.py,sha256=SQBJSgAV8uOkAh96gNZRjd6XJG1G9dZzDc8kAZ_twy0,7538
|
|
25
|
-
kinemotion/dropjump/__init__.py,sha256=yc1XiZ9vfo5h_n7PKVSiX2TTgaIfGL7Y7SkQtiDZj_E,838
|
|
26
|
-
kinemotion/dropjump/analysis.py,sha256=BQ5NqSPNJjFQOb-W4bXSLvjCgWd-nvqx5NElyeqZJC4,29067
|
|
27
|
-
kinemotion/dropjump/cli.py,sha256=ZyroaYPwz8TgfL39Wcaj6m68Awl6lYXC75ttaflU-c0,16236
|
|
28
|
-
kinemotion/dropjump/debug_overlay.py,sha256=LkPw6ucb7beoYWS4L-Lvjs1KLCm5wAWDAfiznUeV2IQ,5668
|
|
29
|
-
kinemotion/dropjump/kinematics.py,sha256=AfqIS8kaI3B8olPX9EY1QxQsuNmuJA5GRsI1EL4NHHg,17091
|
|
30
|
-
kinemotion/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
31
|
-
kinemotion-0.30.0.dist-info/METADATA,sha256=AUhSzQW2siu-R4KfUyS5YRDy1i9eYzzYxOMdWZXF2ww,26020
|
|
32
|
-
kinemotion-0.30.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
33
|
-
kinemotion-0.30.0.dist-info/entry_points.txt,sha256=zaqnAnjLvcdrk1Qvj5nvXZCZ2gp0prS7it1zTJygcIY,50
|
|
34
|
-
kinemotion-0.30.0.dist-info/licenses/LICENSE,sha256=KZajvqsHw0NoOHOi2q0FZ4NBe9HdV6oey-IPYAtHXfg,1088
|
|
35
|
-
kinemotion-0.30.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|