kinemotion 0.10.6__py3-none-any.whl → 0.67.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.

Files changed (48) hide show
  1. kinemotion/__init__.py +31 -6
  2. kinemotion/api.py +39 -598
  3. kinemotion/cli.py +2 -0
  4. kinemotion/cmj/__init__.py +5 -0
  5. kinemotion/cmj/analysis.py +621 -0
  6. kinemotion/cmj/api.py +563 -0
  7. kinemotion/cmj/cli.py +324 -0
  8. kinemotion/cmj/debug_overlay.py +457 -0
  9. kinemotion/cmj/joint_angles.py +307 -0
  10. kinemotion/cmj/kinematics.py +360 -0
  11. kinemotion/cmj/metrics_validator.py +767 -0
  12. kinemotion/cmj/validation_bounds.py +341 -0
  13. kinemotion/core/__init__.py +28 -0
  14. kinemotion/core/auto_tuning.py +71 -37
  15. kinemotion/core/cli_utils.py +60 -0
  16. kinemotion/core/debug_overlay_utils.py +385 -0
  17. kinemotion/core/determinism.py +83 -0
  18. kinemotion/core/experimental.py +103 -0
  19. kinemotion/core/filtering.py +9 -6
  20. kinemotion/core/formatting.py +75 -0
  21. kinemotion/core/metadata.py +231 -0
  22. kinemotion/core/model_downloader.py +172 -0
  23. kinemotion/core/pipeline_utils.py +433 -0
  24. kinemotion/core/pose.py +298 -141
  25. kinemotion/core/pose_landmarks.py +67 -0
  26. kinemotion/core/quality.py +393 -0
  27. kinemotion/core/smoothing.py +250 -154
  28. kinemotion/core/timing.py +247 -0
  29. kinemotion/core/types.py +42 -0
  30. kinemotion/core/validation.py +201 -0
  31. kinemotion/core/video_io.py +135 -50
  32. kinemotion/dropjump/__init__.py +1 -1
  33. kinemotion/dropjump/analysis.py +367 -182
  34. kinemotion/dropjump/api.py +665 -0
  35. kinemotion/dropjump/cli.py +156 -466
  36. kinemotion/dropjump/debug_overlay.py +136 -206
  37. kinemotion/dropjump/kinematics.py +232 -255
  38. kinemotion/dropjump/metrics_validator.py +240 -0
  39. kinemotion/dropjump/validation_bounds.py +157 -0
  40. kinemotion/models/__init__.py +0 -0
  41. kinemotion/models/pose_landmarker_lite.task +0 -0
  42. kinemotion-0.67.0.dist-info/METADATA +726 -0
  43. kinemotion-0.67.0.dist-info/RECORD +47 -0
  44. {kinemotion-0.10.6.dist-info → kinemotion-0.67.0.dist-info}/WHEEL +1 -1
  45. kinemotion-0.10.6.dist-info/METADATA +0 -561
  46. kinemotion-0.10.6.dist-info/RECORD +0 -20
  47. {kinemotion-0.10.6.dist-info → kinemotion-0.67.0.dist-info}/entry_points.txt +0 -0
  48. {kinemotion-0.10.6.dist-info → kinemotion-0.67.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,5 +1,7 @@
1
1
  """Landmark smoothing utilities to reduce jitter in pose tracking."""
2
2
 
3
+ from collections.abc import Callable
4
+
3
5
  import numpy as np
4
6
  from scipy.signal import savgol_filter
5
7
 
@@ -7,102 +9,212 @@ from .filtering import (
7
9
  bilateral_temporal_filter,
8
10
  reject_outliers,
9
11
  )
12
+ from .timing import NULL_TIMER, Timer
13
+ from .types import FloatArray, LandmarkCoord, LandmarkSequence
10
14
 
15
+ # Type alias for smoothing function callback
16
+ SmootherFn = Callable[[list[float], list[float], list[int]], tuple[FloatArray, FloatArray]]
11
17
 
12
- def smooth_landmarks(
13
- landmark_sequence: list[dict[str, tuple[float, float, float]] | None],
14
- window_length: int = 5,
15
- polyorder: int = 2,
16
- ) -> list[dict[str, tuple[float, float, float]] | None]:
18
+
19
+ def _extract_landmark_coordinates(
20
+ landmark_sequence: LandmarkSequence,
21
+ landmark_name: str,
22
+ ) -> tuple[list[float], list[float], list[int]]:
17
23
  """
18
- Smooth landmark trajectories using Savitzky-Golay filter.
24
+ Extract x, y coordinates and valid frame indices for a specific landmark.
19
25
 
20
26
  Args:
21
27
  landmark_sequence: List of landmark dictionaries from each frame
22
- window_length: Length of filter window (must be odd, >= polyorder + 2)
23
- polyorder: Order of polynomial used to fit samples
28
+ landmark_name: Name of the landmark to extract
24
29
 
25
30
  Returns:
26
- Smoothed landmark sequence with same structure as input
31
+ Tuple of (x_coords, y_coords, valid_frames)
27
32
  """
28
- if len(landmark_sequence) < window_length:
29
- # Not enough frames to smooth effectively
30
- return landmark_sequence
33
+ x_coords: list[float] = []
34
+ y_coords: list[float] = []
35
+ valid_frames: list[int] = []
36
+
37
+ for i, frame_landmarks in enumerate(landmark_sequence):
38
+ if frame_landmarks is not None and landmark_name in frame_landmarks:
39
+ x, y, _ = frame_landmarks[landmark_name] # vis not used
40
+ x_coords.append(x)
41
+ y_coords.append(y)
42
+ valid_frames.append(i)
43
+
44
+ return x_coords, y_coords, valid_frames
31
45
 
32
- # Ensure window_length is odd
33
- if window_length % 2 == 0:
34
- window_length += 1
35
46
 
36
- # Extract landmark names from first valid frame
37
- landmark_names = None
47
+ def _get_landmark_names(
48
+ landmark_sequence: LandmarkSequence,
49
+ ) -> list[str] | None:
50
+ """
51
+ Extract landmark names from first valid frame.
52
+
53
+ Args:
54
+ landmark_sequence: List of landmark dictionaries from each frame
55
+
56
+ Returns:
57
+ List of landmark names or None if no valid frame found
58
+ """
38
59
  for frame_landmarks in landmark_sequence:
39
60
  if frame_landmarks is not None:
40
- landmark_names = list(frame_landmarks.keys())
41
- break
61
+ return list(frame_landmarks.keys())
62
+ return None
63
+
42
64
 
65
+ def _fill_missing_frames(
66
+ smoothed_sequence: LandmarkSequence,
67
+ landmark_sequence: LandmarkSequence,
68
+ ) -> None:
69
+ """
70
+ Fill in any missing frames in smoothed sequence with original data.
71
+
72
+ Args:
73
+ smoothed_sequence: Smoothed sequence (modified in place)
74
+ landmark_sequence: Original sequence
75
+ """
76
+ for i in range(len(landmark_sequence)):
77
+ if i >= len(smoothed_sequence) or not smoothed_sequence[i]:
78
+ if i < len(smoothed_sequence):
79
+ smoothed_sequence[i] = landmark_sequence[i]
80
+ else:
81
+ smoothed_sequence.append(landmark_sequence[i])
82
+
83
+
84
+ def _store_smoothed_landmarks(
85
+ smoothed_sequence: LandmarkSequence,
86
+ landmark_sequence: LandmarkSequence,
87
+ landmark_name: str,
88
+ x_smooth: FloatArray,
89
+ y_smooth: FloatArray,
90
+ valid_frames: list[int],
91
+ ) -> None:
92
+ """
93
+ Store smoothed landmark values back into the sequence.
94
+
95
+ Args:
96
+ smoothed_sequence: Sequence to store smoothed values into (modified in place)
97
+ landmark_sequence: Original sequence (for visibility values)
98
+ landmark_name: Name of the landmark being smoothed
99
+ x_smooth: Smoothed x coordinates
100
+ y_smooth: Smoothed y coordinates
101
+ valid_frames: Frame indices corresponding to smoothed values
102
+ """
103
+ for idx, frame_idx in enumerate(valid_frames):
104
+ if frame_idx >= len(smoothed_sequence):
105
+ empty_frames: list[dict[str, LandmarkCoord]] = [{}] * (
106
+ frame_idx - len(smoothed_sequence) + 1
107
+ )
108
+ smoothed_sequence.extend(empty_frames)
109
+
110
+ # Ensure smoothed_sequence[frame_idx] is a dict, not None
111
+ if smoothed_sequence[frame_idx] is None:
112
+ smoothed_sequence[frame_idx] = {}
113
+
114
+ # Type narrowing: after the check above, we know it's a dict
115
+ frame_dict = smoothed_sequence[frame_idx]
116
+ assert frame_dict is not None # for type checker
117
+
118
+ if landmark_name not in frame_dict and landmark_sequence[frame_idx] is not None:
119
+ # Keep original visibility
120
+ orig_landmarks = landmark_sequence[frame_idx]
121
+ assert orig_landmarks is not None # for type checker
122
+ orig_vis = orig_landmarks[landmark_name][2]
123
+ frame_dict[landmark_name] = (
124
+ float(x_smooth[idx]),
125
+ float(y_smooth[idx]),
126
+ orig_vis,
127
+ )
128
+
129
+
130
+ def _smooth_landmarks_core( # NOSONAR(S1172) - polyorder used via closure
131
+ # capture in smoother_fn
132
+ landmark_sequence: LandmarkSequence,
133
+ window_length: int,
134
+ polyorder: int,
135
+ smoother_fn: SmootherFn,
136
+ ) -> LandmarkSequence:
137
+ """
138
+ Core smoothing logic shared by both standard and advanced smoothing.
139
+
140
+ Args:
141
+ landmark_sequence: List of landmark dictionaries from each frame
142
+ window_length: Length of filter window (must be odd)
143
+ polyorder: Order of polynomial used to fit samples (captured by
144
+ smoother_fn closure)
145
+ smoother_fn: Function that takes (x_coords, y_coords, valid_frames)
146
+ and returns (x_smooth, y_smooth)
147
+
148
+ Returns:
149
+ Smoothed landmark sequence
150
+ """
151
+ landmark_names = _get_landmark_names(landmark_sequence)
43
152
  if landmark_names is None:
44
153
  return landmark_sequence
45
154
 
46
- # Build arrays for each landmark coordinate
47
- smoothed_sequence: list[dict[str, tuple[float, float, float]] | None] = []
155
+ smoothed_sequence: LandmarkSequence = []
48
156
 
49
157
  for landmark_name in landmark_names:
50
- # Extract x, y coordinates for this landmark across all frames
51
- x_coords = []
52
- y_coords = []
53
- valid_frames = []
54
-
55
- for i, frame_landmarks in enumerate(landmark_sequence):
56
- if frame_landmarks is not None and landmark_name in frame_landmarks:
57
- x, y, _ = frame_landmarks[landmark_name] # vis not used
58
- x_coords.append(x)
59
- y_coords.append(y)
60
- valid_frames.append(i)
158
+ x_coords, y_coords, valid_frames = _extract_landmark_coordinates(
159
+ landmark_sequence, landmark_name
160
+ )
61
161
 
62
162
  if len(x_coords) < window_length:
63
163
  continue
64
164
 
65
- # Apply Savitzky-Golay filter
66
- x_smooth = savgol_filter(x_coords, window_length, polyorder)
67
- y_smooth = savgol_filter(y_coords, window_length, polyorder)
165
+ # Apply smoothing function
166
+ x_smooth, y_smooth = smoother_fn(x_coords, y_coords, valid_frames)
68
167
 
69
168
  # Store smoothed values back
70
- for idx, frame_idx in enumerate(valid_frames):
71
- if frame_idx >= len(smoothed_sequence):
72
- smoothed_sequence.extend(
73
- [{}] * (frame_idx - len(smoothed_sequence) + 1)
74
- )
75
-
76
- # Ensure smoothed_sequence[frame_idx] is a dict, not None
77
- if smoothed_sequence[frame_idx] is None:
78
- smoothed_sequence[frame_idx] = {}
79
-
80
- if (
81
- landmark_name not in smoothed_sequence[frame_idx]
82
- and landmark_sequence[frame_idx] is not None
83
- ):
84
- # Keep original visibility
85
- orig_vis = landmark_sequence[frame_idx][landmark_name][2]
86
- smoothed_sequence[frame_idx][landmark_name] = (
87
- float(x_smooth[idx]),
88
- float(y_smooth[idx]),
89
- orig_vis,
90
- )
169
+ _store_smoothed_landmarks(
170
+ smoothed_sequence,
171
+ landmark_sequence,
172
+ landmark_name,
173
+ x_smooth,
174
+ y_smooth,
175
+ valid_frames,
176
+ )
91
177
 
92
178
  # Fill in any missing frames with original data
93
- for i in range(len(landmark_sequence)):
94
- if i >= len(smoothed_sequence) or not smoothed_sequence[i]:
95
- if i < len(smoothed_sequence):
96
- smoothed_sequence[i] = landmark_sequence[i]
97
- else:
98
- smoothed_sequence.append(landmark_sequence[i])
179
+ _fill_missing_frames(smoothed_sequence, landmark_sequence)
99
180
 
100
181
  return smoothed_sequence
101
182
 
102
183
 
103
- def compute_velocity(
104
- positions: np.ndarray, fps: float, smooth_window: int = 3
105
- ) -> np.ndarray:
184
+ def smooth_landmarks(
185
+ landmark_sequence: LandmarkSequence,
186
+ window_length: int = 5,
187
+ polyorder: int = 2,
188
+ ) -> LandmarkSequence:
189
+ """
190
+ Smooth landmark trajectories using Savitzky-Golay filter.
191
+
192
+ Args:
193
+ landmark_sequence: List of landmark dictionaries from each frame
194
+ window_length: Length of filter window (must be odd, >= polyorder + 2)
195
+ polyorder: Order of polynomial used to fit samples
196
+
197
+ Returns:
198
+ Smoothed landmark sequence with same structure as input
199
+ """
200
+ if len(landmark_sequence) < window_length:
201
+ return landmark_sequence
202
+
203
+ # Ensure window_length is odd
204
+ if window_length % 2 == 0:
205
+ window_length += 1
206
+
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)
212
+ return x_smooth, y_smooth
213
+
214
+ return _smooth_landmarks_core(landmark_sequence, window_length, polyorder, savgol_smoother)
215
+
216
+
217
+ def compute_velocity(positions: np.ndarray, fps: float, smooth_window: int = 3) -> np.ndarray:
106
218
  """
107
219
  Compute velocity from position data.
108
220
 
@@ -228,7 +340,7 @@ def compute_acceleration_from_derivative(
228
340
 
229
341
 
230
342
  def smooth_landmarks_advanced(
231
- landmark_sequence: list[dict[str, tuple[float, float, float]] | None],
343
+ landmark_sequence: LandmarkSequence,
232
344
  window_length: int = 5,
233
345
  polyorder: int = 2,
234
346
  use_outlier_rejection: bool = True,
@@ -236,7 +348,8 @@ def smooth_landmarks_advanced(
236
348
  ransac_threshold: float = 0.02,
237
349
  bilateral_sigma_spatial: float = 3.0,
238
350
  bilateral_sigma_intensity: float = 0.02,
239
- ) -> list[dict[str, tuple[float, float, float]] | None]:
351
+ timer: Timer | None = None,
352
+ ) -> LandmarkSequence:
240
353
  """
241
354
  Advanced landmark smoothing with outlier rejection and bilateral filtering.
242
355
 
@@ -254,113 +367,96 @@ def smooth_landmarks_advanced(
254
367
  ransac_threshold: Threshold for RANSAC outlier detection
255
368
  bilateral_sigma_spatial: Spatial sigma for bilateral filter
256
369
  bilateral_sigma_intensity: Intensity sigma for bilateral filter
370
+ timer: Optional Timer for measuring operations
257
371
 
258
372
  Returns:
259
373
  Smoothed landmark sequence with same structure as input
260
374
  """
375
+ timer = timer or NULL_TIMER
261
376
  if len(landmark_sequence) < window_length:
262
- # Not enough frames to smooth effectively
263
377
  return landmark_sequence
264
378
 
265
379
  # Ensure window_length is odd
266
380
  if window_length % 2 == 0:
267
381
  window_length += 1
268
382
 
269
- # Extract landmark names from first valid frame
270
- landmark_names = None
271
- for frame_landmarks in landmark_sequence:
272
- if frame_landmarks is not None:
273
- landmark_names = list(frame_landmarks.keys())
274
- break
275
-
276
- if landmark_names is None:
277
- return landmark_sequence
278
-
279
- # Build arrays for each landmark coordinate
280
- smoothed_sequence: list[dict[str, tuple[float, float, float]] | None] = []
281
-
282
- for landmark_name in landmark_names:
283
- # Extract x, y coordinates for this landmark across all frames
284
- x_coords = []
285
- y_coords = []
286
- valid_frames = []
287
-
288
- for i, frame_landmarks in enumerate(landmark_sequence):
289
- if frame_landmarks is not None and landmark_name in frame_landmarks:
290
- x, y, _ = frame_landmarks[landmark_name] # vis not used
291
- x_coords.append(x)
292
- y_coords.append(y)
293
- valid_frames.append(i)
294
-
295
- if len(x_coords) < window_length:
296
- continue
297
-
298
- x_array = np.array(x_coords)
299
- 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)
300
388
 
301
389
  # Step 1: Outlier rejection
302
390
  if use_outlier_rejection:
303
- x_array, _ = reject_outliers(
304
- x_array,
305
- use_ransac=True,
306
- use_median=True,
307
- ransac_threshold=ransac_threshold,
308
- )
309
- y_array, _ = reject_outliers(
310
- y_array,
311
- use_ransac=True,
312
- use_median=True,
313
- ransac_threshold=ransac_threshold,
314
- )
391
+ with timer.measure("smoothing_outlier_rejection"):
392
+ x_array, _ = reject_outliers(
393
+ x_array,
394
+ use_ransac=True,
395
+ use_median=True,
396
+ ransac_threshold=ransac_threshold,
397
+ )
398
+ y_array, _ = reject_outliers(
399
+ y_array,
400
+ use_ransac=True,
401
+ use_median=True,
402
+ ransac_threshold=ransac_threshold,
403
+ )
315
404
 
316
405
  # Step 2: Smoothing (bilateral or Savitzky-Golay)
317
406
  if use_bilateral:
318
- x_smooth = bilateral_temporal_filter(
319
- x_array,
320
- window_size=window_length,
321
- sigma_spatial=bilateral_sigma_spatial,
322
- sigma_intensity=bilateral_sigma_intensity,
323
- )
324
- y_smooth = bilateral_temporal_filter(
325
- y_array,
326
- window_size=window_length,
327
- sigma_spatial=bilateral_sigma_spatial,
328
- sigma_intensity=bilateral_sigma_intensity,
329
- )
407
+ with timer.measure("smoothing_bilateral"):
408
+ x_smooth = bilateral_temporal_filter(
409
+ x_array,
410
+ window_size=window_length,
411
+ sigma_spatial=bilateral_sigma_spatial,
412
+ sigma_intensity=bilateral_sigma_intensity,
413
+ )
414
+ y_smooth = bilateral_temporal_filter(
415
+ y_array,
416
+ window_size=window_length,
417
+ sigma_spatial=bilateral_sigma_spatial,
418
+ sigma_intensity=bilateral_sigma_intensity,
419
+ )
330
420
  else:
331
421
  # Standard Savitzky-Golay
332
- x_smooth = savgol_filter(x_array, window_length, polyorder)
333
- y_smooth = savgol_filter(y_array, window_length, polyorder)
422
+ with timer.measure("smoothing_savgol"):
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]
334
425
 
335
- # Store smoothed values back
336
- for idx, frame_idx in enumerate(valid_frames):
337
- if frame_idx >= len(smoothed_sequence):
338
- smoothed_sequence.extend(
339
- [{}] * (frame_idx - len(smoothed_sequence) + 1)
340
- )
426
+ return x_smooth, y_smooth
341
427
 
342
- # Ensure smoothed_sequence[frame_idx] is a dict, not None
343
- if smoothed_sequence[frame_idx] is None:
344
- smoothed_sequence[frame_idx] = {}
345
-
346
- if (
347
- landmark_name not in smoothed_sequence[frame_idx]
348
- and landmark_sequence[frame_idx] is not None
349
- ):
350
- # Keep original visibility
351
- orig_vis = landmark_sequence[frame_idx][landmark_name][2]
352
- smoothed_sequence[frame_idx][landmark_name] = (
353
- float(x_smooth[idx]),
354
- float(y_smooth[idx]),
355
- orig_vis,
356
- )
428
+ return _smooth_landmarks_core(landmark_sequence, window_length, polyorder, advanced_smoother)
357
429
 
358
- # Fill in any missing frames with original data
359
- for i in range(len(landmark_sequence)):
360
- if i >= len(smoothed_sequence) or not smoothed_sequence[i]:
361
- if i < len(smoothed_sequence):
362
- smoothed_sequence[i] = landmark_sequence[i]
363
- else:
364
- smoothed_sequence.append(landmark_sequence[i])
365
430
 
366
- return smoothed_sequence
431
+ def interpolate_threshold_crossing(
432
+ vel_before: float,
433
+ vel_after: float,
434
+ velocity_threshold: float,
435
+ ) -> float:
436
+ """
437
+ Find fractional offset where velocity crosses threshold between two frames.
438
+
439
+ Uses linear interpolation assuming velocity changes linearly between frames.
440
+
441
+ Args:
442
+ vel_before: Velocity at frame boundary N (absolute value)
443
+ vel_after: Velocity at frame boundary N+1 (absolute value)
444
+ velocity_threshold: Threshold value
445
+
446
+ Returns:
447
+ Fractional offset from frame N (0.0 to 1.0)
448
+ """
449
+ # Handle edge cases
450
+ if abs(vel_after - vel_before) < 1e-9: # Velocity not changing
451
+ return 0.5
452
+
453
+ # Linear interpolation: at what fraction t does velocity equal threshold?
454
+ # vel(t) = vel_before + t * (vel_after - vel_before)
455
+ # Solve for t when vel(t) = threshold:
456
+ # threshold = vel_before + t * (vel_after - vel_before)
457
+ # t = (threshold - vel_before) / (vel_after - vel_before)
458
+
459
+ t = (velocity_threshold - vel_before) / (vel_after - vel_before)
460
+
461
+ # Clamp to [0, 1] range
462
+ return float(max(0.0, min(1.0, t)))