kinemotion 0.70.1__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/__init__.py +4 -1
- kinemotion/cmj/analysis.py +79 -30
- kinemotion/cmj/api.py +16 -39
- kinemotion/cmj/cli.py +0 -21
- kinemotion/cmj/debug_overlay.py +154 -286
- kinemotion/cmj/joint_angles.py +96 -31
- kinemotion/cmj/metrics_validator.py +30 -51
- 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 +67 -499
- 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 +12 -44
- kinemotion/dropjump/cli.py +63 -105
- kinemotion/dropjump/debug_overlay.py +124 -65
- kinemotion/dropjump/validation_bounds.py +1 -1
- kinemotion/models/rtmpose-s_simcc-body7_pt-body7-halpe26_700e-256x192-7f134165_20230605.onnx +0 -0
- kinemotion/models/yolox_tiny_8xb8-300e_humanart-6f3252f9.onnx +0 -0
- {kinemotion-0.70.1.dist-info → kinemotion-0.71.1.dist-info}/METADATA +1 -5
- kinemotion-0.71.1.dist-info/RECORD +50 -0
- kinemotion/core/rtmpose_cpu.py +0 -626
- kinemotion/core/rtmpose_wrapper.py +0 -190
- kinemotion-0.70.1.dist-info/RECORD +0 -51
- {kinemotion-0.70.1.dist-info → kinemotion-0.71.1.dist-info}/WHEEL +0 -0
- {kinemotion-0.70.1.dist-info → kinemotion-0.71.1.dist-info}/entry_points.txt +0 -0
- {kinemotion-0.70.1.dist-info → kinemotion-0.71.1.dist-info}/licenses/LICENSE +0 -0
kinemotion/__init__.py
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
"""Kinemotion: Video-based kinematic analysis for athletic performance.
|
|
1
|
+
"""Kinemotion: Video-based kinematic analysis for athletic performance.
|
|
2
|
+
|
|
3
|
+
Supports Counter Movement Jump (CMJ) and Drop Jump analysis using MediaPipe pose estimation.
|
|
4
|
+
"""
|
|
2
5
|
|
|
3
6
|
from .api import (
|
|
4
7
|
CMJVideoConfig,
|
kinemotion/cmj/analysis.py
CHANGED
|
@@ -8,7 +8,7 @@ from scipy.signal import savgol_filter
|
|
|
8
8
|
from ..core.experimental import unused
|
|
9
9
|
from ..core.smoothing import compute_acceleration_from_derivative
|
|
10
10
|
from ..core.timing import NULL_TIMER, Timer
|
|
11
|
-
from ..core.types import FloatArray
|
|
11
|
+
from ..core.types import HIP_KEYS, FloatArray
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
def compute_signed_velocity(
|
|
@@ -335,12 +335,37 @@ def find_interpolated_takeoff_landing(
|
|
|
335
335
|
return (takeoff_frame, landing_frame)
|
|
336
336
|
|
|
337
337
|
|
|
338
|
-
def find_takeoff_frame(
|
|
339
|
-
|
|
338
|
+
def find_takeoff_frame(
|
|
339
|
+
velocities: FloatArray,
|
|
340
|
+
peak_height_frame: int,
|
|
341
|
+
fps: float,
|
|
342
|
+
accelerations: FloatArray | None = None,
|
|
343
|
+
) -> float:
|
|
344
|
+
"""Find takeoff frame using deceleration onset after peak velocity.
|
|
345
|
+
|
|
346
|
+
Two-step approach:
|
|
347
|
+
1. Find peak upward velocity (most negative) before peak height
|
|
348
|
+
2. Look forward to find when deceleration starts (gravity takes over)
|
|
349
|
+
|
|
350
|
+
This is more accurate than peak velocity alone because:
|
|
351
|
+
- Peak velocity occurs during final push (still on ground)
|
|
352
|
+
- Actual takeoff is when feet leave ground (deceleration begins)
|
|
353
|
+
|
|
354
|
+
Empirically validated against manual annotations:
|
|
355
|
+
- Peak velocity only: 0.67 frame MAE
|
|
356
|
+
- Deceleration onset: Improves Video 2 error from -2 to -1 frame
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
velocities: Vertical velocity array (negative = upward)
|
|
360
|
+
peak_height_frame: Frame index of peak jump height
|
|
361
|
+
fps: Video frame rate
|
|
362
|
+
accelerations: Optional acceleration array. If provided, uses
|
|
363
|
+
deceleration onset method. If None, falls back to peak velocity.
|
|
340
364
|
|
|
341
|
-
|
|
342
|
-
|
|
365
|
+
Returns:
|
|
366
|
+
Frame index of takeoff (fractional precision).
|
|
343
367
|
"""
|
|
368
|
+
# Step 1: Find peak upward velocity
|
|
344
369
|
takeoff_search_start = max(0, peak_height_frame - int(fps * 0.35))
|
|
345
370
|
takeoff_search_end = peak_height_frame - 2
|
|
346
371
|
|
|
@@ -349,19 +374,28 @@ def find_takeoff_frame(velocities: FloatArray, peak_height_frame: int, fps: floa
|
|
|
349
374
|
if len(takeoff_velocities) == 0:
|
|
350
375
|
return float(peak_height_frame - int(fps * 0.3))
|
|
351
376
|
|
|
352
|
-
# Check if velocities are suspiciously identical (flat
|
|
377
|
+
# Check if velocities are suspiciously identical (flat = ambiguous)
|
|
353
378
|
vel_min = np.min(takeoff_velocities)
|
|
354
379
|
vel_max = np.max(takeoff_velocities)
|
|
355
380
|
vel_range = vel_max - vel_min
|
|
356
381
|
|
|
357
382
|
if vel_range < 1e-6:
|
|
358
|
-
# Velocities
|
|
359
|
-
# Return the midpoint of the search window as a stable estimate
|
|
383
|
+
# Velocities essentially identical - use midpoint
|
|
360
384
|
return float((takeoff_search_start + takeoff_search_end) / 2.0)
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
385
|
+
|
|
386
|
+
peak_vel_idx = int(np.argmin(takeoff_velocities))
|
|
387
|
+
peak_vel_frame = takeoff_search_start + peak_vel_idx
|
|
388
|
+
|
|
389
|
+
# Step 2: If accelerations provided, find deceleration onset
|
|
390
|
+
if accelerations is not None:
|
|
391
|
+
# Look forward from peak velocity to find where deceleration starts
|
|
392
|
+
# Deceleration = positive acceleration (in normalized coords, up is negative)
|
|
393
|
+
for i in range(peak_vel_frame, peak_height_frame):
|
|
394
|
+
if accelerations[i] > 0:
|
|
395
|
+
return float(i)
|
|
396
|
+
|
|
397
|
+
# Fallback to peak velocity frame
|
|
398
|
+
return float(peak_vel_frame)
|
|
365
399
|
|
|
366
400
|
|
|
367
401
|
def find_lowest_frame(
|
|
@@ -385,20 +419,16 @@ def find_lowest_frame(
|
|
|
385
419
|
return float(int(takeoff_frame) - int(fps * 0.2))
|
|
386
420
|
|
|
387
421
|
|
|
388
|
-
def
|
|
422
|
+
def _find_landing_impact(
|
|
389
423
|
accelerations: FloatArray,
|
|
390
424
|
velocities: FloatArray,
|
|
391
425
|
peak_height_frame: int,
|
|
392
426
|
fps: float,
|
|
393
427
|
) -> float:
|
|
394
|
-
"""Find landing frame
|
|
428
|
+
"""Find landing frame using maximum deceleration (impact method).
|
|
395
429
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
This corresponds to the moment just before or at initial ground contact.
|
|
399
|
-
2. Look for maximum deceleration (impact) *after* the peak velocity.
|
|
400
|
-
This filters out mid-air tracking noise/flutter that can cause false
|
|
401
|
-
deceleration spikes while the athlete is still accelerating downward.
|
|
430
|
+
This is the original algorithm that detects the maximum deceleration spike,
|
|
431
|
+
which corresponds to peak impact force. Matches human visual annotations.
|
|
402
432
|
|
|
403
433
|
Args:
|
|
404
434
|
accelerations: Vertical acceleration array (deriv=2)
|
|
@@ -413,26 +443,21 @@ def find_landing_frame(
|
|
|
413
443
|
search_end = min(len(accelerations), peak_height_frame + int(fps * 1.0))
|
|
414
444
|
|
|
415
445
|
# 1. Find peak downward velocity (max positive value)
|
|
416
|
-
# Search from peak height to end of window
|
|
417
446
|
vel_search_window = velocities[peak_height_frame:search_end]
|
|
418
447
|
|
|
419
448
|
if len(vel_search_window) == 0:
|
|
420
449
|
return float(peak_height_frame + int(fps * 0.3))
|
|
421
450
|
|
|
422
|
-
# Index relative to peak_height_frame
|
|
423
451
|
peak_vel_rel_idx = int(np.argmax(vel_search_window))
|
|
424
452
|
peak_vel_frame = peak_height_frame + peak_vel_rel_idx
|
|
425
453
|
|
|
426
454
|
# 2. Search for impact (min acceleration) starting from peak velocity
|
|
427
|
-
# We allow a small buffer (e.g., 1-2 frames) before peak velocity just in case
|
|
428
|
-
# peak velocity coincides with impact start due to smoothing
|
|
429
455
|
landing_search_start = max(peak_height_frame, peak_vel_frame - 2)
|
|
430
456
|
landing_search_end = search_end
|
|
431
457
|
|
|
432
458
|
landing_accelerations = accelerations[landing_search_start:landing_search_end]
|
|
433
459
|
|
|
434
460
|
if len(landing_accelerations) == 0:
|
|
435
|
-
# Fallback if window is empty
|
|
436
461
|
return float(peak_height_frame + int(fps * 0.3))
|
|
437
462
|
|
|
438
463
|
# Find minimum acceleration (maximum deceleration spike)
|
|
@@ -442,6 +467,30 @@ def find_landing_frame(
|
|
|
442
467
|
return float(landing_frame)
|
|
443
468
|
|
|
444
469
|
|
|
470
|
+
def find_landing_frame(
|
|
471
|
+
accelerations: FloatArray,
|
|
472
|
+
velocities: FloatArray,
|
|
473
|
+
peak_height_frame: int,
|
|
474
|
+
fps: float,
|
|
475
|
+
) -> float:
|
|
476
|
+
"""Find landing frame after peak height using maximum deceleration.
|
|
477
|
+
|
|
478
|
+
Detects landing by finding the maximum deceleration spike, which corresponds
|
|
479
|
+
to the moment of foot-ground contact. Validated against manual annotations
|
|
480
|
+
with 0 frame mean absolute error.
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
accelerations: Vertical acceleration array (deriv=2)
|
|
484
|
+
velocities: Vertical velocity array (deriv=1)
|
|
485
|
+
peak_height_frame: Frame index of peak jump height
|
|
486
|
+
fps: Video frame rate
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
Frame index of landing.
|
|
490
|
+
"""
|
|
491
|
+
return _find_landing_impact(accelerations, velocities, peak_height_frame, fps)
|
|
492
|
+
|
|
493
|
+
|
|
445
494
|
def compute_average_hip_position(
|
|
446
495
|
landmarks: dict[str, tuple[float, float, float]],
|
|
447
496
|
) -> tuple[float, float]:
|
|
@@ -454,12 +503,10 @@ def compute_average_hip_position(
|
|
|
454
503
|
Returns:
|
|
455
504
|
(x, y) average hip position in normalized coordinates
|
|
456
505
|
"""
|
|
457
|
-
hip_keys = ["left_hip", "right_hip"]
|
|
458
|
-
|
|
459
506
|
x_positions: list[float] = []
|
|
460
507
|
y_positions: list[float] = []
|
|
461
508
|
|
|
462
|
-
for key in
|
|
509
|
+
for key in HIP_KEYS:
|
|
463
510
|
if key in landmarks:
|
|
464
511
|
x, y, visibility = landmarks[key]
|
|
465
512
|
if visibility > 0.5: # Only use visible landmarks
|
|
@@ -550,7 +597,7 @@ def detect_cmj_phases(
|
|
|
550
597
|
1. Find peak height (global minimum y)
|
|
551
598
|
2. Find takeoff (peak negative velocity before peak height)
|
|
552
599
|
3. Find lowest point (maximum y value before takeoff)
|
|
553
|
-
4. Find landing (
|
|
600
|
+
4. Find landing (maximum deceleration after peak height)
|
|
554
601
|
|
|
555
602
|
Args:
|
|
556
603
|
positions: Array of vertical positions (normalized 0-1). Typically Hips/CoM.
|
|
@@ -583,7 +630,9 @@ def detect_cmj_phases(
|
|
|
583
630
|
|
|
584
631
|
# Step 2-4: Find all phases using helper functions
|
|
585
632
|
with timer.measure("cmj_find_takeoff"):
|
|
586
|
-
takeoff_frame = find_takeoff_frame(
|
|
633
|
+
takeoff_frame = find_takeoff_frame(
|
|
634
|
+
velocities, peak_height_frame, fps, accelerations=accelerations
|
|
635
|
+
)
|
|
587
636
|
|
|
588
637
|
with timer.measure("cmj_find_lowest_point"):
|
|
589
638
|
lowest_point = find_lowest_frame(velocities, positions, takeoff_frame, fps)
|
kinemotion/cmj/api.py
CHANGED
|
@@ -220,7 +220,6 @@ def _run_pose_tracking(
|
|
|
220
220
|
detection_confidence: float | None,
|
|
221
221
|
tracking_confidence: float | None,
|
|
222
222
|
pose_tracker: "MediaPipePoseTracker | None",
|
|
223
|
-
pose_backend: str | None,
|
|
224
223
|
verbose: bool,
|
|
225
224
|
timer: Timer,
|
|
226
225
|
) -> tuple[list[NDArray[np.uint8]], list, list[int]]:
|
|
@@ -236,33 +235,13 @@ def _run_pose_tracking(
|
|
|
236
235
|
)
|
|
237
236
|
|
|
238
237
|
if pose_tracker is None:
|
|
239
|
-
if
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
tracker = PoseTrackerFactory.create(
|
|
247
|
-
backend=pose_backend,
|
|
248
|
-
min_detection_confidence=det_conf,
|
|
249
|
-
min_tracking_confidence=track_conf,
|
|
250
|
-
timer=timer,
|
|
251
|
-
)
|
|
252
|
-
init_time = time.perf_counter() - init_start
|
|
253
|
-
|
|
254
|
-
if verbose:
|
|
255
|
-
print(f"Using pose backend: {pose_backend}")
|
|
256
|
-
print(f" → {get_tracker_info(tracker)}")
|
|
257
|
-
print(f" → Initialized in {init_time * 1000:.1f} ms")
|
|
258
|
-
else:
|
|
259
|
-
if verbose:
|
|
260
|
-
print("Processing all frames with MediaPipe pose tracking...")
|
|
261
|
-
tracker = MediaPipePoseTracker(
|
|
262
|
-
min_detection_confidence=det_conf,
|
|
263
|
-
min_tracking_confidence=track_conf,
|
|
264
|
-
timer=timer,
|
|
265
|
-
)
|
|
238
|
+
if verbose:
|
|
239
|
+
print("Processing all frames with MediaPipe pose tracking...")
|
|
240
|
+
tracker = MediaPipePoseTracker(
|
|
241
|
+
min_detection_confidence=det_conf,
|
|
242
|
+
min_tracking_confidence=track_conf,
|
|
243
|
+
timer=timer,
|
|
244
|
+
)
|
|
266
245
|
should_close_tracker = True
|
|
267
246
|
else:
|
|
268
247
|
tracker = pose_tracker
|
|
@@ -283,13 +262,15 @@ def _get_tuned_parameters(
|
|
|
283
262
|
with timer.measure("parameter_auto_tuning"):
|
|
284
263
|
characteristics = analyze_video_sample(landmarks_sequence, video.fps, video.frame_count)
|
|
285
264
|
params = auto_tune_parameters(characteristics, quality_preset)
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
265
|
+
|
|
266
|
+
if overrides:
|
|
267
|
+
params = apply_expert_overrides(
|
|
268
|
+
params,
|
|
269
|
+
overrides.smoothing_window,
|
|
270
|
+
overrides.velocity_threshold,
|
|
271
|
+
overrides.min_contact_frames,
|
|
272
|
+
overrides.visibility_threshold,
|
|
273
|
+
)
|
|
293
274
|
|
|
294
275
|
if verbose:
|
|
295
276
|
print_verbose_parameters(video, characteristics, quality_preset, params)
|
|
@@ -412,7 +393,6 @@ class CMJVideoConfig:
|
|
|
412
393
|
overrides: AnalysisOverrides | None = None
|
|
413
394
|
detection_confidence: float | None = None
|
|
414
395
|
tracking_confidence: float | None = None
|
|
415
|
-
pose_backend: str | None = None
|
|
416
396
|
|
|
417
397
|
|
|
418
398
|
@dataclass
|
|
@@ -434,7 +414,6 @@ def process_cmj_video(
|
|
|
434
414
|
overrides: AnalysisOverrides | None = None,
|
|
435
415
|
detection_confidence: float | None = None,
|
|
436
416
|
tracking_confidence: float | None = None,
|
|
437
|
-
pose_backend: str | None = None,
|
|
438
417
|
verbose: bool = False,
|
|
439
418
|
timer: Timer | None = None,
|
|
440
419
|
pose_tracker: MediaPipePoseTracker | None = None,
|
|
@@ -481,7 +460,6 @@ def process_cmj_video(
|
|
|
481
460
|
detection_confidence,
|
|
482
461
|
tracking_confidence,
|
|
483
462
|
pose_tracker,
|
|
484
|
-
pose_backend,
|
|
485
463
|
verbose,
|
|
486
464
|
timer,
|
|
487
465
|
)
|
|
@@ -567,7 +545,6 @@ def _process_cmj_video_wrapper(config: CMJVideoConfig) -> CMJVideoResult:
|
|
|
567
545
|
overrides=config.overrides,
|
|
568
546
|
detection_confidence=config.detection_confidence,
|
|
569
547
|
tracking_confidence=config.tracking_confidence,
|
|
570
|
-
pose_backend=config.pose_backend,
|
|
571
548
|
verbose=False,
|
|
572
549
|
)
|
|
573
550
|
|
kinemotion/cmj/cli.py
CHANGED
|
@@ -27,7 +27,6 @@ class AnalysisParameters:
|
|
|
27
27
|
visibility_threshold: float | None = None
|
|
28
28
|
detection_confidence: float | None = None
|
|
29
29
|
tracking_confidence: float | None = None
|
|
30
|
-
pose_backend: str | None = None
|
|
31
30
|
|
|
32
31
|
|
|
33
32
|
def _process_batch_videos(
|
|
@@ -78,23 +77,6 @@ def _process_batch_videos(
|
|
|
78
77
|
is_flag=True,
|
|
79
78
|
help="Show auto-selected parameters and analysis details",
|
|
80
79
|
)
|
|
81
|
-
@click.option(
|
|
82
|
-
"--pose-backend",
|
|
83
|
-
type=click.Choice(
|
|
84
|
-
["auto", "mediapipe", "rtmpose-cpu", "rtmpose-cuda", "rtmpose-coreml"],
|
|
85
|
-
case_sensitive=False,
|
|
86
|
-
),
|
|
87
|
-
default="auto",
|
|
88
|
-
help=(
|
|
89
|
-
"Pose tracking backend: "
|
|
90
|
-
"auto (detect best), "
|
|
91
|
-
"mediapipe (baseline), "
|
|
92
|
-
"rtmpose-cpu (optimized CPU), "
|
|
93
|
-
"rtmpose-cuda (NVIDIA GPU), "
|
|
94
|
-
"rtmpose-coreml (Apple Silicon)"
|
|
95
|
-
),
|
|
96
|
-
show_default=True,
|
|
97
|
-
)
|
|
98
80
|
# Batch processing options
|
|
99
81
|
@click.option(
|
|
100
82
|
"--batch",
|
|
@@ -173,7 +155,6 @@ def cmj_analyze( # NOSONAR(S107) - Click CLI requires individual parameters
|
|
|
173
155
|
json_output: str | None,
|
|
174
156
|
quality: str,
|
|
175
157
|
verbose: bool,
|
|
176
|
-
pose_backend: str,
|
|
177
158
|
batch: bool,
|
|
178
159
|
workers: int,
|
|
179
160
|
output_dir: str | None,
|
|
@@ -237,7 +218,6 @@ def cmj_analyze( # NOSONAR(S107) - Click CLI requires individual parameters
|
|
|
237
218
|
visibility_threshold=visibility_threshold,
|
|
238
219
|
detection_confidence=detection_confidence,
|
|
239
220
|
tracking_confidence=tracking_confidence,
|
|
240
|
-
pose_backend=pose_backend,
|
|
241
221
|
)
|
|
242
222
|
|
|
243
223
|
if use_batch:
|
|
@@ -293,7 +273,6 @@ def _process_single(
|
|
|
293
273
|
overrides=overrides,
|
|
294
274
|
detection_confidence=expert_params.detection_confidence,
|
|
295
275
|
tracking_confidence=expert_params.tracking_confidence,
|
|
296
|
-
pose_backend=expert_params.pose_backend,
|
|
297
276
|
verbose=verbose,
|
|
298
277
|
)
|
|
299
278
|
|