kinemotion 0.62.0__py3-none-any.whl → 0.64.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.

@@ -8,11 +8,12 @@ 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
12
 
12
13
 
13
14
  def compute_signed_velocity(
14
- positions: np.ndarray, window_length: int = 5, polyorder: int = 2
15
- ) -> np.ndarray:
15
+ positions: FloatArray, window_length: int = 5, polyorder: int = 2
16
+ ) -> FloatArray:
16
17
  """
17
18
  Compute SIGNED velocity for CMJ phase detection.
18
19
 
@@ -59,8 +60,8 @@ class CMJPhase(Enum):
59
60
  since="0.34.0",
60
61
  )
61
62
  def find_standing_phase(
62
- positions: np.ndarray,
63
- velocities: np.ndarray,
63
+ positions: FloatArray,
64
+ velocities: FloatArray,
64
65
  fps: float,
65
66
  min_standing_duration: float = 0.5,
66
67
  velocity_threshold: float = 0.01,
@@ -112,7 +113,7 @@ def find_standing_phase(
112
113
  since="0.34.0",
113
114
  )
114
115
  def find_countermovement_start(
115
- velocities: np.ndarray,
116
+ velocities: FloatArray,
116
117
  countermovement_threshold: float = 0.015,
117
118
  min_eccentric_frames: int = 3,
118
119
  standing_start: int | None = None,
@@ -152,8 +153,8 @@ def find_countermovement_start(
152
153
 
153
154
 
154
155
  def find_lowest_point(
155
- positions: np.ndarray,
156
- velocities: np.ndarray,
156
+ positions: FloatArray,
157
+ velocities: FloatArray,
157
158
  min_search_frame: int = 80,
158
159
  ) -> int:
159
160
  """
@@ -195,8 +196,8 @@ def find_lowest_point(
195
196
 
196
197
 
197
198
  def find_cmj_takeoff_from_velocity_peak(
198
- positions: np.ndarray,
199
- velocities: np.ndarray,
199
+ positions: FloatArray,
200
+ velocities: FloatArray,
200
201
  lowest_point_frame: int,
201
202
  fps: float,
202
203
  ) -> float:
@@ -232,9 +233,9 @@ def find_cmj_takeoff_from_velocity_peak(
232
233
 
233
234
 
234
235
  def find_cmj_landing_from_position_peak(
235
- positions: np.ndarray,
236
- velocities: np.ndarray,
237
- accelerations: np.ndarray,
236
+ positions: FloatArray,
237
+ velocities: FloatArray,
238
+ accelerations: FloatArray,
238
239
  takeoff_frame: int,
239
240
  fps: float,
240
241
  ) -> float:
@@ -290,8 +291,8 @@ def find_cmj_landing_from_position_peak(
290
291
  since="0.34.0",
291
292
  )
292
293
  def find_interpolated_takeoff_landing(
293
- positions: np.ndarray,
294
- velocities: np.ndarray,
294
+ positions: FloatArray,
295
+ velocities: FloatArray,
295
296
  lowest_point_frame: int,
296
297
  window_length: int = 5,
297
298
  polyorder: int = 2,
@@ -334,7 +335,7 @@ def find_interpolated_takeoff_landing(
334
335
  return (takeoff_frame, landing_frame)
335
336
 
336
337
 
337
- def find_takeoff_frame(velocities: np.ndarray, peak_height_frame: int, fps: float) -> float:
338
+ def find_takeoff_frame(velocities: FloatArray, peak_height_frame: int, fps: float) -> float:
338
339
  """Find takeoff frame as peak upward velocity before peak height.
339
340
 
340
341
  Robust detection: When velocities are nearly identical (flat), detects
@@ -364,7 +365,7 @@ def find_takeoff_frame(velocities: np.ndarray, peak_height_frame: int, fps: floa
364
365
 
365
366
 
366
367
  def find_lowest_frame(
367
- velocities: np.ndarray, positions: np.ndarray, takeoff_frame: float, fps: float
368
+ velocities: FloatArray, positions: FloatArray, takeoff_frame: float, fps: float
368
369
  ) -> float:
369
370
  """Find lowest point frame before takeoff."""
370
371
  lowest_search_start = max(0, int(takeoff_frame) - int(fps * 0.4))
@@ -385,8 +386,8 @@ def find_lowest_frame(
385
386
 
386
387
 
387
388
  def find_landing_frame(
388
- accelerations: np.ndarray,
389
- velocities: np.ndarray,
389
+ accelerations: FloatArray,
390
+ velocities: FloatArray,
390
391
  peak_height_frame: int,
391
392
  fps: float,
392
393
  ) -> float:
@@ -455,8 +456,8 @@ def compute_average_hip_position(
455
456
  """
456
457
  hip_keys = ["left_hip", "right_hip"]
457
458
 
458
- x_positions = []
459
- y_positions = []
459
+ x_positions: list[float] = []
460
+ y_positions: list[float] = []
460
461
 
461
462
  for key in hip_keys:
462
463
  if key in landmarks:
@@ -472,10 +473,10 @@ def compute_average_hip_position(
472
473
 
473
474
 
474
475
  def find_standing_end(
475
- velocities: np.ndarray,
476
+ velocities: FloatArray,
476
477
  lowest_point: float,
477
- _positions: np.ndarray | None = None,
478
- accelerations: np.ndarray | None = None,
478
+ _positions: FloatArray | None = None,
479
+ accelerations: FloatArray | None = None,
479
480
  ) -> float | None:
480
481
  """
481
482
  Find end of standing phase before lowest point.
@@ -535,11 +536,11 @@ def find_standing_end(
535
536
 
536
537
 
537
538
  def detect_cmj_phases(
538
- positions: np.ndarray,
539
+ positions: FloatArray,
539
540
  fps: float,
540
541
  window_length: int = 5,
541
542
  polyorder: int = 2,
542
- landing_positions: np.ndarray | None = None,
543
+ landing_positions: FloatArray | None = None,
543
544
  timer: Timer | None = None,
544
545
  ) -> tuple[float | None, float, float, float] | None:
545
546
  """
@@ -4,9 +4,9 @@ from dataclasses import dataclass
4
4
  from typing import TYPE_CHECKING, TypedDict
5
5
 
6
6
  import numpy as np
7
- from numpy.typing import NDArray
8
7
 
9
8
  from ..core.formatting import format_float_metric
9
+ from ..core.types import FloatArray
10
10
 
11
11
  if TYPE_CHECKING:
12
12
  from ..core.cmj_metrics_validator import ValidationResult
@@ -136,7 +136,7 @@ class CMJMetrics:
136
136
 
137
137
 
138
138
  def _calculate_scale_factor(
139
- positions: NDArray[np.float64],
139
+ positions: FloatArray,
140
140
  takeoff_frame: float,
141
141
  landing_frame: float,
142
142
  jump_height: float,
@@ -169,7 +169,7 @@ def _calculate_scale_factor(
169
169
 
170
170
 
171
171
  def _calculate_countermovement_depth(
172
- positions: NDArray[np.float64],
172
+ positions: FloatArray,
173
173
  standing_start_frame: float | None,
174
174
  lowest_point_frame: float,
175
175
  scale_factor: float,
@@ -222,7 +222,7 @@ def _calculate_phase_durations(
222
222
 
223
223
 
224
224
  def _calculate_peak_velocities(
225
- velocities: NDArray[np.float64],
225
+ velocities: FloatArray,
226
226
  standing_start_frame: float | None,
227
227
  lowest_point_frame: float,
228
228
  takeoff_frame: float,
@@ -261,7 +261,7 @@ def _calculate_peak_velocities(
261
261
 
262
262
 
263
263
  def _calculate_transition_time(
264
- velocities: NDArray[np.float64],
264
+ velocities: FloatArray,
265
265
  lowest_point_frame: float,
266
266
  fps: float,
267
267
  ) -> float | None:
@@ -291,8 +291,8 @@ def _calculate_transition_time(
291
291
 
292
292
 
293
293
  def calculate_cmj_metrics(
294
- positions: NDArray[np.float64],
295
- velocities: NDArray[np.float64],
294
+ positions: FloatArray,
295
+ velocities: FloatArray,
296
296
  standing_start_frame: float | None,
297
297
  lowest_point_frame: float,
298
298
  takeoff_frame: float,
@@ -16,6 +16,7 @@ from kinemotion.cmj.validation_bounds import (
16
16
  TripleExtensionBounds,
17
17
  estimate_athlete_profile,
18
18
  )
19
+ from kinemotion.core.types import MetricsDict
19
20
  from kinemotion.core.validation import (
20
21
  AthleteProfile,
21
22
  MetricBounds,
@@ -80,7 +81,7 @@ class CMJMetricsValidator(MetricsValidator):
80
81
  """
81
82
  return data.get(key_with_suffix) or data.get(key_without_suffix)
82
83
 
83
- def validate(self, metrics: dict) -> CMJValidationResult:
84
+ def validate(self, metrics: MetricsDict) -> CMJValidationResult:
84
85
  """Validate CMJ metrics comprehensively.
85
86
 
86
87
  Args:
@@ -128,7 +129,7 @@ class CMJMetricsValidator(MetricsValidator):
128
129
  return result
129
130
 
130
131
  def _check_flight_time(
131
- self, metrics: dict, result: CMJValidationResult, profile: AthleteProfile
132
+ self, metrics: MetricsDict, result: CMJValidationResult, profile: AthleteProfile
132
133
  ) -> None:
133
134
  """Validate flight time."""
134
135
  flight_time_raw = self._get_metric_value(metrics, "flight_time_ms", "flight_time")
@@ -176,7 +177,7 @@ class CMJMetricsValidator(MetricsValidator):
176
177
  )
177
178
 
178
179
  def _check_jump_height(
179
- self, metrics: dict, result: CMJValidationResult, profile: AthleteProfile
180
+ self, metrics: MetricsDict, result: CMJValidationResult, profile: AthleteProfile
180
181
  ) -> None:
181
182
  """Validate jump height."""
182
183
  jump_height = self._get_metric_value(metrics, "jump_height_m", "jump_height")
@@ -217,7 +218,7 @@ class CMJMetricsValidator(MetricsValidator):
217
218
  )
218
219
 
219
220
  def _check_countermovement_depth(
220
- self, metrics: dict, result: CMJValidationResult, profile: AthleteProfile
221
+ self, metrics: MetricsDict, result: CMJValidationResult, profile: AthleteProfile
221
222
  ) -> None:
222
223
  """Validate countermovement depth."""
223
224
  depth = self._get_metric_value(metrics, "countermovement_depth_m", "countermovement_depth")
@@ -258,7 +259,7 @@ class CMJMetricsValidator(MetricsValidator):
258
259
  )
259
260
 
260
261
  def _check_concentric_duration(
261
- self, metrics: dict, result: CMJValidationResult, profile: AthleteProfile
262
+ self, metrics: MetricsDict, result: CMJValidationResult, profile: AthleteProfile
262
263
  ) -> None:
263
264
  """Validate concentric duration (contact time)."""
264
265
  duration_raw = self._get_metric_value(
@@ -308,7 +309,7 @@ class CMJMetricsValidator(MetricsValidator):
308
309
  )
309
310
 
310
311
  def _check_eccentric_duration(
311
- self, metrics: dict, result: CMJValidationResult, profile: AthleteProfile
312
+ self, metrics: MetricsDict, result: CMJValidationResult, profile: AthleteProfile
312
313
  ) -> None:
313
314
  """Validate eccentric duration."""
314
315
  duration_raw = self._get_metric_value(
@@ -349,7 +350,7 @@ class CMJMetricsValidator(MetricsValidator):
349
350
  )
350
351
 
351
352
  def _check_peak_velocities(
352
- self, metrics: dict, result: CMJValidationResult, profile: AthleteProfile
353
+ self, metrics: MetricsDict, result: CMJValidationResult, profile: AthleteProfile
353
354
  ) -> None:
354
355
  """Validate peak eccentric and concentric velocities."""
355
356
  # Eccentric
@@ -419,7 +420,7 @@ class CMJMetricsValidator(MetricsValidator):
419
420
  )
420
421
 
421
422
  def _check_flight_time_height_consistency(
422
- self, metrics: dict, result: CMJValidationResult
423
+ self, metrics: MetricsDict, result: CMJValidationResult
423
424
  ) -> None:
424
425
  """Verify jump height is consistent with flight time."""
425
426
  flight_time_ms = metrics.get("flight_time_ms")
@@ -455,7 +456,7 @@ class CMJMetricsValidator(MetricsValidator):
455
456
  )
456
457
 
457
458
  def _check_velocity_height_consistency(
458
- self, metrics: dict, result: CMJValidationResult
459
+ self, metrics: MetricsDict, result: CMJValidationResult
459
460
  ) -> None:
460
461
  """Verify peak velocity is consistent with jump height."""
461
462
  velocity = metrics.get("peak_concentric_velocity_m_s")
@@ -491,7 +492,7 @@ class CMJMetricsValidator(MetricsValidator):
491
492
  )
492
493
 
493
494
  def _check_rsi_validity(
494
- self, metrics: dict, result: CMJValidationResult, profile: AthleteProfile
495
+ self, metrics: MetricsDict, result: CMJValidationResult, profile: AthleteProfile
495
496
  ) -> None:
496
497
  """Validate Reactive Strength Index."""
497
498
  flight_time_raw = self._get_metric_value(metrics, "flight_time_ms", "flight_time")
@@ -555,7 +556,7 @@ class CMJMetricsValidator(MetricsValidator):
555
556
  bounds=(expected_min, expected_max),
556
557
  )
557
558
 
558
- def _check_depth_height_ratio(self, metrics: dict, result: CMJValidationResult) -> None:
559
+ def _check_depth_height_ratio(self, metrics: MetricsDict, result: CMJValidationResult) -> None:
559
560
  """Check countermovement depth to jump height ratio."""
560
561
  depth = metrics.get("countermovement_depth_m")
561
562
  jump_height = metrics.get("jump_height_m")
@@ -595,7 +596,9 @@ class CMJMetricsValidator(MetricsValidator):
595
596
  value=ratio,
596
597
  )
597
598
 
598
- def _check_contact_depth_ratio(self, metrics: dict, result: CMJValidationResult) -> None:
599
+ def _check_contact_depth_ratio(
600
+ self, metrics: MetricsDict, result: CMJValidationResult
601
+ ) -> None:
599
602
  """Check contact time to countermovement depth ratio."""
600
603
  contact_ms = metrics.get("concentric_duration_ms")
601
604
  depth = metrics.get("countermovement_depth_m")
@@ -636,7 +639,7 @@ class CMJMetricsValidator(MetricsValidator):
636
639
  )
637
640
 
638
641
  def _check_triple_extension(
639
- self, metrics: dict, result: CMJValidationResult, profile: AthleteProfile
642
+ self, metrics: MetricsDict, result: CMJValidationResult, profile: AthleteProfile
640
643
  ) -> None:
641
644
  """Validate triple extension angles."""
642
645
  angles = metrics.get("triple_extension")
@@ -15,6 +15,7 @@ References:
15
15
  - Bogdanis (2012): Plyometric training effects
16
16
  """
17
17
 
18
+ from kinemotion.core.types import MetricsDict
18
19
  from kinemotion.core.validation import AthleteProfile, MetricBounds
19
20
 
20
21
 
@@ -299,7 +300,9 @@ ATHLETE_PROFILES = {
299
300
  }
300
301
 
301
302
 
302
- def estimate_athlete_profile(metrics_dict: dict, gender: str | None = None) -> AthleteProfile:
303
+ def estimate_athlete_profile(
304
+ metrics_dict: MetricsDict, gender: str | None = None
305
+ ) -> AthleteProfile:
303
306
  """Estimate athlete profile from metrics.
304
307
 
305
308
  Uses jump height as primary classifier:
@@ -5,10 +5,10 @@ import shutil
5
5
  import subprocess
6
6
  import time
7
7
  from pathlib import Path
8
- from typing import Self
9
8
 
10
9
  import cv2
11
10
  import numpy as np
11
+ from typing_extensions import Self
12
12
 
13
13
  from .timing import NULL_TIMER, Timer
14
14
 
@@ -48,7 +48,7 @@ def create_video_writer(
48
48
 
49
49
  for codec in codecs_to_try:
50
50
  try:
51
- fourcc = cv2.VideoWriter_fourcc(*codec)
51
+ fourcc = cv2.VideoWriter_fourcc(*codec) # type: ignore[attr-defined]
52
52
  writer = cv2.VideoWriter(output_path, fourcc, fps, (display_width, display_height))
53
53
  if writer.isOpened():
54
54
  used_codec = codec
kinemotion/core/pose.py CHANGED
@@ -25,7 +25,7 @@ class PoseTracker:
25
25
  timer: Optional Timer for measuring operations
26
26
  """
27
27
  self.timer = timer or NULL_TIMER
28
- self.mp_pose = mp.solutions.pose
28
+ self.mp_pose = mp.solutions.pose # type: ignore[attr-defined]
29
29
  self.pose = self.mp_pose.Pose(
30
30
  static_image_mode=False, # Use tracking mode for better performance
31
31
  min_detection_confidence=min_detection_confidence,
@@ -1,6 +1,6 @@
1
1
  """Landmark smoothing utilities to reduce jitter in pose tracking."""
2
2
 
3
- from typing import TypeAlias
3
+ from collections.abc import Callable
4
4
 
5
5
  import numpy as np
6
6
  from scipy.signal import savgol_filter
@@ -10,11 +10,10 @@ from .filtering import (
10
10
  reject_outliers,
11
11
  )
12
12
  from .timing import NULL_TIMER, Timer
13
+ from .types import FloatArray, LandmarkCoord, LandmarkSequence
13
14
 
14
- # Type aliases for landmark data structures
15
- LandmarkCoord: TypeAlias = tuple[float, float, float] # (x, y, visibility)
16
- LandmarkFrame: TypeAlias = dict[str, LandmarkCoord] | None
17
- LandmarkSequence: TypeAlias = list[LandmarkFrame]
15
+ # Type alias for smoothing function callback
16
+ SmootherFn = Callable[[list[float], list[float], list[int]], tuple[FloatArray, FloatArray]]
18
17
 
19
18
 
20
19
  def _extract_landmark_coordinates(
@@ -31,9 +30,9 @@ def _extract_landmark_coordinates(
31
30
  Returns:
32
31
  Tuple of (x_coords, y_coords, valid_frames)
33
32
  """
34
- x_coords = []
35
- y_coords = []
36
- valid_frames = []
33
+ x_coords: list[float] = []
34
+ y_coords: list[float] = []
35
+ valid_frames: list[int] = []
37
36
 
38
37
  for i, frame_landmarks in enumerate(landmark_sequence):
39
38
  if frame_landmarks is not None and landmark_name in frame_landmarks:
@@ -86,8 +85,8 @@ def _store_smoothed_landmarks(
86
85
  smoothed_sequence: LandmarkSequence,
87
86
  landmark_sequence: LandmarkSequence,
88
87
  landmark_name: str,
89
- x_smooth: np.ndarray,
90
- y_smooth: np.ndarray,
88
+ x_smooth: FloatArray,
89
+ y_smooth: FloatArray,
91
90
  valid_frames: list[int],
92
91
  ) -> None:
93
92
  """
@@ -103,7 +102,10 @@ def _store_smoothed_landmarks(
103
102
  """
104
103
  for idx, frame_idx in enumerate(valid_frames):
105
104
  if frame_idx >= len(smoothed_sequence):
106
- smoothed_sequence.extend([{}] * (frame_idx - len(smoothed_sequence) + 1))
105
+ empty_frames: list[dict[str, LandmarkCoord]] = [{}] * (
106
+ frame_idx - len(smoothed_sequence) + 1
107
+ )
108
+ smoothed_sequence.extend(empty_frames)
107
109
 
108
110
  # Ensure smoothed_sequence[frame_idx] is a dict, not None
109
111
  if smoothed_sequence[frame_idx] is None:
@@ -130,7 +132,7 @@ def _smooth_landmarks_core( # NOSONAR(S1172) - polyorder used via closure
130
132
  landmark_sequence: LandmarkSequence,
131
133
  window_length: int,
132
134
  polyorder: int,
133
- smoother_fn, # type: ignore[no-untyped-def]
135
+ smoother_fn: SmootherFn,
134
136
  ) -> LandmarkSequence:
135
137
  """
136
138
  Core smoothing logic shared by both standard and advanced smoothing.
@@ -150,7 +152,7 @@ def _smooth_landmarks_core( # NOSONAR(S1172) - polyorder used via closure
150
152
  if landmark_names is None:
151
153
  return landmark_sequence
152
154
 
153
- smoothed_sequence: list[dict[str, tuple[float, float, float]] | None] = []
155
+ smoothed_sequence: LandmarkSequence = []
154
156
 
155
157
  for landmark_name in landmark_names:
156
158
  x_coords, y_coords, valid_frames = _extract_landmark_coordinates(
@@ -202,9 +204,11 @@ def smooth_landmarks(
202
204
  if window_length % 2 == 0:
203
205
  window_length += 1
204
206
 
205
- def savgol_smoother(x_coords, y_coords, _valid_frames): # type: ignore[no-untyped-def]
206
- x_smooth = savgol_filter(x_coords, window_length, polyorder)
207
- y_smooth = savgol_filter(y_coords, window_length, polyorder)
207
+ def savgol_smoother(
208
+ x_coords: list[float], y_coords: list[float], _valid_frames: list[int]
209
+ ) -> tuple[FloatArray, FloatArray]:
210
+ x_smooth: FloatArray = savgol_filter(x_coords, window_length, polyorder)
211
+ y_smooth: FloatArray = savgol_filter(y_coords, window_length, polyorder)
208
212
  return x_smooth, y_smooth
209
213
 
210
214
  return _smooth_landmarks_core(landmark_sequence, window_length, polyorder, savgol_smoother)
@@ -376,9 +380,11 @@ def smooth_landmarks_advanced(
376
380
  if window_length % 2 == 0:
377
381
  window_length += 1
378
382
 
379
- def advanced_smoother(x_coords, y_coords, _valid_frames): # type: ignore[no-untyped-def]
380
- x_array = np.array(x_coords)
381
- y_array = np.array(y_coords)
383
+ def advanced_smoother(
384
+ x_coords: list[float], y_coords: list[float], _valid_frames: list[int]
385
+ ) -> tuple[FloatArray, FloatArray]:
386
+ x_array: FloatArray = np.array(x_coords)
387
+ y_array: FloatArray = np.array(y_coords)
382
388
 
383
389
  # Step 1: Outlier rejection
384
390
  if use_outlier_rejection:
@@ -414,8 +420,8 @@ def smooth_landmarks_advanced(
414
420
  else:
415
421
  # Standard Savitzky-Golay
416
422
  with timer.measure("smoothing_savgol"):
417
- x_smooth = savgol_filter(x_array, window_length, polyorder)
418
- y_smooth = savgol_filter(y_array, window_length, polyorder)
423
+ x_smooth: FloatArray = savgol_filter(x_array, window_length, polyorder) # type: ignore[reportUnknownVariableType]
424
+ y_smooth: FloatArray = savgol_filter(y_array, window_length, polyorder) # type: ignore[reportUnknownVariableType]
419
425
 
420
426
  return x_smooth, y_smooth
421
427
 
@@ -0,0 +1,42 @@
1
+ """Central type definitions for the kinemotion package.
2
+
3
+ This module provides all type aliases used throughout the codebase to ensure
4
+ consistent typing and better IDE support.
5
+ """
6
+
7
+ from typing import Any, TypeAlias
8
+
9
+ import numpy as np
10
+ from numpy.typing import NDArray
11
+
12
+ # NumPy array types for various use cases
13
+ FloatArray: TypeAlias = NDArray[np.floating[Any]]
14
+ Float64Array: TypeAlias = NDArray[np.float64]
15
+ IntArray: TypeAlias = NDArray[np.integer[Any]]
16
+ UInt8Array: TypeAlias = NDArray[np.uint8]
17
+ BoolArray: TypeAlias = NDArray[np.bool_]
18
+
19
+ # MediaPipe landmark types
20
+ # Using dict-based representation since MediaPipe lacks proper type stubs
21
+ LandmarkCoord: TypeAlias = tuple[float, float, float] # (x, y, visibility)
22
+ LandmarkFrame: TypeAlias = dict[str, LandmarkCoord] | None
23
+ LandmarkSequence: TypeAlias = list[LandmarkFrame]
24
+
25
+ # Metrics dictionary type
26
+ # Uses Any because metrics can contain:
27
+ # - Simple values: float, int, str
28
+ # - Nested dicts: e.g. "triple_extension" contains angle data
29
+ # - Wrapper structures: e.g. {"data": {...actual metrics...}}
30
+ MetricsDict: TypeAlias = dict[str, Any]
31
+
32
+ __all__ = [
33
+ "FloatArray",
34
+ "Float64Array",
35
+ "IntArray",
36
+ "UInt8Array",
37
+ "BoolArray",
38
+ "LandmarkCoord",
39
+ "LandmarkFrame",
40
+ "LandmarkSequence",
41
+ "MetricsDict",
42
+ ]
@@ -11,6 +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
15
 
15
16
 
16
17
  class ContactState(Enum):
@@ -27,7 +28,7 @@ class ContactState(Enum):
27
28
  since="0.34.0",
28
29
  )
29
30
  def calculate_adaptive_threshold(
30
- positions: np.ndarray,
31
+ positions: FloatArray,
31
32
  fps: float,
32
33
  baseline_duration: float = 3.0,
33
34
  multiplier: float = 1.5,
@@ -110,7 +111,7 @@ def calculate_adaptive_threshold(
110
111
 
111
112
 
112
113
  def _find_stable_baseline(
113
- positions: np.ndarray,
114
+ positions: FloatArray,
114
115
  min_stable_frames: int,
115
116
  stability_threshold: float = 0.01,
116
117
  debug: bool = False,
@@ -149,7 +150,7 @@ def _find_stable_baseline(
149
150
 
150
151
 
151
152
  def _find_drop_from_baseline(
152
- positions: np.ndarray,
153
+ positions: FloatArray,
153
154
  baseline_start: int,
154
155
  baseline_position: float,
155
156
  stable_window: int,
@@ -188,7 +189,7 @@ def _find_drop_from_baseline(
188
189
 
189
190
 
190
191
  def detect_drop_start(
191
- positions: np.ndarray,
192
+ positions: FloatArray,
192
193
  fps: float,
193
194
  min_stationary_duration: float = 1.0,
194
195
  position_change_threshold: float = 0.02,
@@ -254,10 +255,10 @@ def detect_drop_start(
254
255
 
255
256
 
256
257
  def _filter_stationary_with_visibility(
257
- is_stationary: np.ndarray,
258
- visibilities: np.ndarray | None,
258
+ is_stationary: BoolArray,
259
+ visibilities: FloatArray | None,
259
260
  visibility_threshold: float,
260
- ) -> np.ndarray:
261
+ ) -> BoolArray:
261
262
  """Apply visibility filter to stationary flags.
262
263
 
263
264
  Args:
@@ -275,7 +276,7 @@ def _filter_stationary_with_visibility(
275
276
 
276
277
 
277
278
  def _find_contact_frames(
278
- is_stationary: np.ndarray,
279
+ is_stationary: BoolArray,
279
280
  min_contact_frames: int,
280
281
  ) -> set[int]:
281
282
  """Find frames with sustained contact using minimum duration filter.
@@ -308,7 +309,7 @@ def _find_contact_frames(
308
309
  def _assign_contact_states(
309
310
  n_frames: int,
310
311
  contact_frames: set[int],
311
- visibilities: np.ndarray | None,
312
+ visibilities: FloatArray | None,
312
313
  visibility_threshold: float,
313
314
  ) -> list[ContactState]:
314
315
  """Assign contact states based on contact frames and visibility.
@@ -334,11 +335,11 @@ def _assign_contact_states(
334
335
 
335
336
 
336
337
  def detect_ground_contact(
337
- foot_positions: np.ndarray,
338
+ foot_positions: FloatArray,
338
339
  velocity_threshold: float = 0.02,
339
340
  min_contact_frames: int = 3,
340
341
  visibility_threshold: float = 0.5,
341
- visibilities: np.ndarray | None = None,
342
+ visibilities: FloatArray | None = None,
342
343
  window_length: int = 5,
343
344
  polyorder: int = 2,
344
345
  timer: Timer | None = None,
@@ -426,7 +427,7 @@ def find_contact_phases(
426
427
  def _interpolate_phase_start(
427
428
  start_idx: int,
428
429
  state: ContactState,
429
- velocities: np.ndarray,
430
+ velocities: FloatArray,
430
431
  velocity_threshold: float,
431
432
  ) -> float:
432
433
  """Interpolate start boundary of a phase with sub-frame precision.
@@ -454,7 +455,7 @@ def _interpolate_phase_start(
454
455
  def _interpolate_phase_end(
455
456
  end_idx: int,
456
457
  state: ContactState,
457
- velocities: np.ndarray,
458
+ velocities: FloatArray,
458
459
  velocity_threshold: float,
459
460
  max_idx: int,
460
461
  ) -> float:
@@ -481,7 +482,7 @@ def _interpolate_phase_end(
481
482
 
482
483
 
483
484
  def find_interpolated_phase_transitions(
484
- foot_positions: np.ndarray,
485
+ foot_positions: FloatArray,
485
486
  contact_states: list[ContactState],
486
487
  velocity_threshold: float,
487
488
  smoothing_window: int = 5,
@@ -523,7 +524,7 @@ def find_interpolated_phase_transitions(
523
524
 
524
525
 
525
526
  def refine_transition_with_curvature(
526
- foot_positions: np.ndarray,
527
+ foot_positions: FloatArray,
527
528
  estimated_frame: float,
528
529
  transition_type: str,
529
530
  search_window: int = 3,
@@ -598,7 +599,7 @@ def refine_transition_with_curvature(
598
599
 
599
600
 
600
601
  def find_interpolated_phase_transitions_with_curvature(
601
- foot_positions: np.ndarray,
602
+ foot_positions: FloatArray,
602
603
  contact_states: list[ContactState],
603
604
  velocity_threshold: float,
604
605
  smoothing_window: int = 5,
@@ -684,8 +685,8 @@ def find_interpolated_phase_transitions_with_curvature(
684
685
 
685
686
 
686
687
  def find_landing_from_acceleration(
687
- positions: np.ndarray,
688
- accelerations: np.ndarray,
688
+ positions: FloatArray,
689
+ accelerations: FloatArray,
689
690
  takeoff_frame: int,
690
691
  fps: float,
691
692
  search_duration: float = 0.7,
@@ -9,6 +9,7 @@ of metric issues.
9
9
 
10
10
  from dataclasses import dataclass
11
11
 
12
+ from kinemotion.core.types import MetricsDict
12
13
  from kinemotion.core.validation import (
13
14
  MetricsValidator,
14
15
  ValidationResult,
@@ -57,7 +58,7 @@ class DropJumpValidationResult(ValidationResult):
57
58
  class DropJumpMetricsValidator(MetricsValidator):
58
59
  """Comprehensive drop jump metrics validator."""
59
60
 
60
- def validate(self, metrics: dict) -> DropJumpValidationResult:
61
+ def validate(self, metrics: MetricsDict) -> DropJumpValidationResult:
61
62
  """Validate drop jump metrics comprehensively.
62
63
 
63
64
  Args:
@@ -15,6 +15,7 @@ References:
15
15
  - Covens et al. (2019): Drop jump kinetics across athletes
16
16
  """
17
17
 
18
+ from kinemotion.core.types import MetricsDict
18
19
  from kinemotion.core.validation import AthleteProfile, MetricBounds
19
20
 
20
21
 
@@ -123,7 +124,7 @@ def _classify_combined_score(combined_score: float) -> AthleteProfile:
123
124
  return AthleteProfile.ELITE
124
125
 
125
126
 
126
- def estimate_athlete_profile(metrics: dict, gender: str | None = None) -> AthleteProfile:
127
+ def estimate_athlete_profile(metrics: MetricsDict, gender: str | None = None) -> AthleteProfile:
127
128
  """Estimate athlete profile from drop jump metrics.
128
129
 
129
130
  Uses jump_height and contact_time to classify athlete level.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kinemotion
3
- Version: 0.62.0
3
+ Version: 0.64.0
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
@@ -25,6 +25,7 @@ Requires-Dist: mediapipe>=0.10.9
25
25
  Requires-Dist: numpy>=1.26.0
26
26
  Requires-Dist: opencv-python>=4.9.0
27
27
  Requires-Dist: scipy>=1.11.0
28
+ Requires-Dist: typing-extensions>=4.15.0
28
29
  Description-Content-Type: text/markdown
29
30
 
30
31
  # Kinemotion
@@ -2,41 +2,42 @@ kinemotion/__init__.py,sha256=Ho_BUtsM0PBxBW1ye9RlUg0ZqBlgGudRI9bZTF7QKUI,966
2
2
  kinemotion/api.py,sha256=uG1e4bTnj2c-6cbZJEZ_LjMwFdaG32ba2KcK_XjE_NI,1040
3
3
  kinemotion/cli.py,sha256=cqYV_7URH0JUDy1VQ_EDLv63FmNO4Ns20m6s1XAjiP4,464
4
4
  kinemotion/cmj/__init__.py,sha256=SkAw9ka8Yd1Qfv9hcvk22m3EfucROzYrSNGNF5kDzho,113
5
- kinemotion/cmj/analysis.py,sha256=ZtKVfDj1-qoegXPklMO1GX8_OtD8pYbTyTCW4FCeaS8,22140
5
+ kinemotion/cmj/analysis.py,sha256=jM9ZX44h1__Cg2iIhAYRoo_5fPwIOeV5Q2FZ22rMvKY,22202
6
6
  kinemotion/cmj/api.py,sha256=Xu6PepQKQFzRWZMESaWvHyPV-dueCH1TQQeQdil1KiQ,16626
7
7
  kinemotion/cmj/cli.py,sha256=P2b77IIw6kqTSIkncxlShzhmjIwqMFBNd-pZxYP-TsI,9918
8
8
  kinemotion/cmj/debug_overlay.py,sha256=bX9aPLhXiLCCMZW9v8Y4OiOAaZO0i-UGr-Pl8HCsmbI,15810
9
9
  kinemotion/cmj/joint_angles.py,sha256=HmheIEiKcQz39cRezk4h-htorOhGNPsqKIR9RsAEKts,9960
10
- kinemotion/cmj/kinematics.py,sha256=5xAqBP_lwDGP4fcnMXozELj0XRzBcWYGmI0tsVMYbnw,13413
11
- kinemotion/cmj/metrics_validator.py,sha256=10Dx7-o5-ziQ9YXnzs98v_ZqJxi3ax3COwNY8M_KqqM,30835
12
- kinemotion/cmj/validation_bounds.py,sha256=1QXaX3uclU2ceZya90u5qVT1tWU4kGkUW0CQbvh317I,11989
10
+ kinemotion/cmj/kinematics.py,sha256=KwA8uSj3g1SeNf0NXMSHsp3gIw6Gfa-6QWIwdYdRXYw,13362
11
+ kinemotion/cmj/metrics_validator.py,sha256=3oFB331Xch2sRMTvqALiwOvsWkCUhrLQ7ZCZ4QhI2lA,30986
12
+ kinemotion/cmj/validation_bounds.py,sha256=Ry915JdInPXbqjaVGNY_urnDO1PAkCSJqHwNKRq-VkU,12048
13
13
  kinemotion/core/__init__.py,sha256=U2fnLUGXQ0jbwpXhdksYKDXbeQndEHjn9gwTAEJ9Av0,1451
14
14
  kinemotion/core/auto_tuning.py,sha256=lhAqPc-eLjMYx9BCvKdECE7TD2Dweb9KcifV6JHaXOE,11278
15
15
  kinemotion/core/cli_utils.py,sha256=sQPbT6XWWau-sm9yuN5c3eS5xNzoQGGXwSz6hQXtRvM,1859
16
- kinemotion/core/debug_overlay_utils.py,sha256=YlDmKns6x37H4yvulGGEUJ_D8G0bDZFTSbV8ig2hfFQ,8400
16
+ kinemotion/core/debug_overlay_utils.py,sha256=Ne8GqEWUySwwtkmAzbBs527mh2QsBW2hbgV9CbdJjxE,8441
17
17
  kinemotion/core/determinism.py,sha256=Frw-KAOvAxTL_XtxoWpXCjMbQPUKEAusK6JctlkeuRo,2509
18
18
  kinemotion/core/experimental.py,sha256=IK05AF4aZS15ke85hF3TWCqRIXU1AlD_XKzFz735Ua8,3640
19
19
  kinemotion/core/filtering.py,sha256=Oc__pV6iHEGyyovbqa5SUi-6v8QyvaRVwA0LRayM884,11355
20
20
  kinemotion/core/formatting.py,sha256=G_3eqgOtym9RFOZVEwCxye4A2cyrmgvtQ214vIshowU,2480
21
21
  kinemotion/core/metadata.py,sha256=bJAVa4nym__zx1hNowSZduMGKBSGOPxTbBQkjm6N0D0,7207
22
22
  kinemotion/core/pipeline_utils.py,sha256=q32c1AJ8KI4Ht-K3ZiI7ectQKtg8k_FLdLy6WPBPWkU,14927
23
- kinemotion/core/pose.py,sha256=b7RQF4prb40hb4Yr5ATBFsj3dvEX0ohkG4h65lqHf8E,8993
23
+ kinemotion/core/pose.py,sha256=55zqk02N6ibOCvVO5c73ZegRcDWDeKCnU_lKsdwvN4s,9023
24
24
  kinemotion/core/quality.py,sha256=VUkRL2N6B7lfIZ2pE9han_U68JwarmZz1U0ygHkgkhE,13022
25
- kinemotion/core/smoothing.py,sha256=-RPZzNjgtBQ-Ri0o-inkwIfx30IKo7ZTSnxXJ3Itn9w,15616
25
+ kinemotion/core/smoothing.py,sha256=ELMHL7pzSqYffjnLDBUMBJIgt1AwOssDInE8IiXBbig,15942
26
26
  kinemotion/core/timing.py,sha256=d1rjZc07Nbi5Jrio9AC-zeS0dNAlbPyNIydLz7X75Pk,7804
27
+ kinemotion/core/types.py,sha256=A_HclzKpf3By5DiJ0wY9B-dQJrIVAAhUfGab7qTSIL8,1279
27
28
  kinemotion/core/validation.py,sha256=0xVv-ftWveV60fJ97kmZMuy2Qqqb5aZLR50dDIrjnhg,6773
28
29
  kinemotion/core/video_io.py,sha256=vCwpWnlW2y29l48dFXokdehQn42w_IQvayxbVTjpXqQ,7863
29
30
  kinemotion/dropjump/__init__.py,sha256=tC3H3BrCg8Oj-db-Vrtx4PH_llR1Ppkd5jwaOjhQcLg,862
30
- kinemotion/dropjump/analysis.py,sha256=WQ2ol5fWAXA2y-0UDGXgF587qsOsKNgkWPQLXaNZMdU,28005
31
+ kinemotion/dropjump/analysis.py,sha256=YomuoJF_peyrBSpeT89Q5_sBgY0kEDyq7TFrtEnRLjs,28049
31
32
  kinemotion/dropjump/api.py,sha256=uidio49CXisyWKd287CnCrM51GusG9DWAIUKGH85fpM,20584
32
33
  kinemotion/dropjump/cli.py,sha256=gUef9nmyR5952h1WnfBGyCdFXQvzVTlCKYAjJGcO4sE,16819
33
34
  kinemotion/dropjump/debug_overlay.py,sha256=9RQYXPRf0q2wdy6y2Ak2R4tpRceDwC8aJrXZzkmh3Wo,5942
34
35
  kinemotion/dropjump/kinematics.py,sha256=dx4PuXKfKMKcsc_HX6sXj8rHXf9ksiZIOAIkJ4vBlY4,19637
35
- kinemotion/dropjump/metrics_validator.py,sha256=367-TFal2bVDU3zwoCDpYY_lnYjSdXOrvNIIxVnIWiE,9190
36
- kinemotion/dropjump/validation_bounds.py,sha256=MUMJhGV62peFuIHdPR1uulMS4bI-i_JerGh5T9HF8Wk,5071
36
+ kinemotion/dropjump/metrics_validator.py,sha256=lSfo4Lm5FHccl8ijUP6SA-kcSh50LS9hF8UIyWxcnW8,9243
37
+ kinemotion/dropjump/validation_bounds.py,sha256=x4yjcFxyvdMp5e7MkcoUosGLeGsxBh1Lft6h__AQ2G8,5124
37
38
  kinemotion/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
- kinemotion-0.62.0.dist-info/METADATA,sha256=-DwCYyCeH359z-cfaJawEIiQeNcMkT8gim-VdZv1R74,26020
39
- kinemotion-0.62.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
40
- kinemotion-0.62.0.dist-info/entry_points.txt,sha256=zaqnAnjLvcdrk1Qvj5nvXZCZ2gp0prS7it1zTJygcIY,50
41
- kinemotion-0.62.0.dist-info/licenses/LICENSE,sha256=KZajvqsHw0NoOHOi2q0FZ4NBe9HdV6oey-IPYAtHXfg,1088
42
- kinemotion-0.62.0.dist-info/RECORD,,
39
+ kinemotion-0.64.0.dist-info/METADATA,sha256=yyIEMoc-ce13yauhkU6v4lYI303kW4iACckeBDNhHvo,26061
40
+ kinemotion-0.64.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
41
+ kinemotion-0.64.0.dist-info/entry_points.txt,sha256=zaqnAnjLvcdrk1Qvj5nvXZCZ2gp0prS7it1zTJygcIY,50
42
+ kinemotion-0.64.0.dist-info/licenses/LICENSE,sha256=KZajvqsHw0NoOHOi2q0FZ4NBe9HdV6oey-IPYAtHXfg,1088
43
+ kinemotion-0.64.0.dist-info/RECORD,,