kinemotion 0.36.0__py3-none-any.whl → 0.36.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 CHANGED
@@ -417,6 +417,13 @@ def process_dropjump_video(
417
417
  if not Path(video_path).exists():
418
418
  raise FileNotFoundError(f"Video file not found: {video_path}")
419
419
 
420
+ # Set deterministic mode for drop jump reproducibility
421
+ # Note: MediaPipe has inherent non-determinism (Google issue #3945)
422
+ # This improves consistency but cannot eliminate all variation
423
+ from .core.determinism import set_deterministic_mode
424
+
425
+ set_deterministic_mode(seed=42)
426
+
420
427
  # Start timing
421
428
  start_time = time.time()
422
429
 
@@ -0,0 +1,85 @@
1
+ """Determinism utilities for reproducible analysis.
2
+
3
+ Provides functions to set random seeds for NumPy, Python's random module,
4
+ and TensorFlow (used by MediaPipe) to ensure deterministic behavior.
5
+ """
6
+
7
+ import hashlib
8
+ import os
9
+ import random
10
+ from pathlib import Path
11
+
12
+ import numpy as np
13
+
14
+
15
+ def get_video_hash_seed(video_path: str) -> int:
16
+ """Generate deterministic seed from video file path.
17
+
18
+ Uses video filename (not contents) to generate a consistent seed
19
+ for the same video across multiple runs.
20
+
21
+ Args:
22
+ video_path: Path to video file
23
+
24
+ Returns:
25
+ Integer seed value derived from filename
26
+ """
27
+ # Use filename only (not full path) for consistency
28
+ filename = Path(video_path).name
29
+ # Hash filename to get deterministic seed
30
+ hash_value = hashlib.md5(filename.encode()).hexdigest()
31
+ # Convert first 8 hex chars to integer
32
+ return int(hash_value[:8], 16)
33
+
34
+
35
+ def set_deterministic_mode(
36
+ seed: int | None = None, video_path: str | None = None
37
+ ) -> None:
38
+ """Set random seeds for reproducible analysis.
39
+
40
+ Sets seeds for:
41
+ - Python's random module
42
+ - NumPy random number generator
43
+ - TensorFlow (via environment variable for TFLite)
44
+
45
+ Args:
46
+ seed: Random seed value. If None and video_path provided,
47
+ generates seed from video filename.
48
+ video_path: Optional video path to generate deterministic seed
49
+
50
+ Note:
51
+ This should be called before any MediaPipe or analysis operations
52
+ to ensure deterministic pose detection and metric calculation.
53
+ """
54
+ # Generate seed from video if not provided
55
+ if seed is None and video_path is not None:
56
+ seed = get_video_hash_seed(video_path)
57
+ elif seed is None:
58
+ seed = 42 # Default
59
+
60
+ # Python random
61
+ random.seed(seed)
62
+
63
+ # NumPy random
64
+ np.random.seed(seed)
65
+
66
+ # TensorFlow/TFLite (used by MediaPipe)
67
+ # Set via environment variable before TF is initialized
68
+ os.environ["PYTHONHASHSEED"] = str(seed)
69
+ os.environ["TF_DETERMINISTIC_OPS"] = "1"
70
+
71
+ # Try to set TensorFlow seed if available
72
+ try:
73
+ import tensorflow as tf
74
+
75
+ tf.random.set_seed(seed)
76
+
77
+ # Disable GPU non-determinism if CUDA is available
78
+ try:
79
+ tf.config.experimental.enable_op_determinism()
80
+ except AttributeError:
81
+ # Older TensorFlow versions don't have this
82
+ pass
83
+ except ImportError:
84
+ # TensorFlow not directly available (only via MediaPipe's bundled version)
85
+ pass
kinemotion/core/pose.py CHANGED
@@ -22,6 +22,7 @@ class PoseTracker:
22
22
  """
23
23
  self.mp_pose = mp.solutions.pose
24
24
  self.pose = self.mp_pose.Pose(
25
+ static_image_mode=False, # Use tracking mode for better performance
25
26
  min_detection_confidence=min_detection_confidence,
26
27
  min_tracking_confidence=min_tracking_confidence,
27
28
  model_complexity=1,
@@ -202,6 +202,36 @@ def _filter_phases_after_drop(
202
202
  return filtered_phases, filtered_interpolated
203
203
 
204
204
 
205
+ def _compute_robust_phase_position(
206
+ foot_y_positions: NDArray[np.float64],
207
+ phase_start: int,
208
+ phase_end: int,
209
+ temporal_window: int = 11,
210
+ ) -> float:
211
+ """Compute robust position estimate using temporal averaging.
212
+
213
+ Uses median over a fixed temporal window to reduce sensitivity to
214
+ MediaPipe landmark noise, improving reproducibility.
215
+
216
+ Args:
217
+ foot_y_positions: Vertical position array
218
+ phase_start: Start frame of phase
219
+ phase_end: End frame of phase
220
+ temporal_window: Number of frames to average (default: 11)
221
+
222
+ Returns:
223
+ Robust position estimate using median
224
+ """
225
+ # Center the temporal window on the phase midpoint
226
+ phase_mid = (phase_start + phase_end) // 2
227
+ window_start = max(0, phase_mid - temporal_window // 2)
228
+ window_end = min(len(foot_y_positions), phase_mid + temporal_window // 2 + 1)
229
+
230
+ # Use median for robustness to outliers
231
+ window_positions = foot_y_positions[window_start:window_end]
232
+ return float(np.median(window_positions))
233
+
234
+
205
235
  def _identify_main_contact_phase(
206
236
  phases: list[tuple[int, int, ContactState]],
207
237
  ground_phases: list[tuple[int, int, int]],
@@ -237,17 +267,20 @@ def _identify_main_contact_phase(
237
267
 
238
268
  if ground_after_air and first_ground_idx < first_air_idx:
239
269
  # Check if first ground is at higher elevation (lower y) than
240
- # ground after air
241
- first_ground_y = float(
242
- np.mean(foot_y_positions[first_ground_start : first_ground_end + 1])
270
+ # ground after air using robust temporal averaging
271
+ first_ground_y = _compute_robust_phase_position(
272
+ foot_y_positions, first_ground_start, first_ground_end
243
273
  )
244
274
  second_ground_start, second_ground_end, _ = ground_after_air[0]
245
- second_ground_y = float(
246
- np.mean(foot_y_positions[second_ground_start : second_ground_end + 1])
275
+ second_ground_y = _compute_robust_phase_position(
276
+ foot_y_positions, second_ground_start, second_ground_end
247
277
  )
248
278
 
249
- # If first ground is significantly higher (>5% of frame), it's a drop jump
250
- if second_ground_y - first_ground_y > 0.05:
279
+ # If first ground is significantly higher (>7% of frame), it's a drop jump
280
+ # Increased from 0.05 to 0.07 with 11-frame temporal averaging
281
+ # for reproducibility (balances detection sensitivity with noise robustness)
282
+ # Note: MediaPipe has inherent non-determinism (Google issue #3945)
283
+ if second_ground_y - first_ground_y > 0.07:
251
284
  is_drop_jump = True
252
285
  contact_start, contact_end = second_ground_start, second_ground_end
253
286
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kinemotion
3
- Version: 0.36.0
3
+ Version: 0.36.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
@@ -1,5 +1,5 @@
1
1
  kinemotion/__init__.py,sha256=sxdDOekOrIgjxm842gy-6zfq7OWmGl9ShJtXCm4JI7c,723
2
- kinemotion/api.py,sha256=nbDbyzhXIMA04tGqKPH8R0fR66zgtu14x6NgWroy_QU,39471
2
+ kinemotion/api.py,sha256=HdbdPxc6MTJO6lldYgfKGaF7SZuroKcWITS7rjhL8PA,39764
3
3
  kinemotion/cli.py,sha256=cqYV_7URH0JUDy1VQ_EDLv63FmNO4Ns20m6s1XAjiP4,464
4
4
  kinemotion/cmj/__init__.py,sha256=Ynv0-Oco4I3Y1Ubj25m3h9h2XFqeNwpAewXmAYOmwfU,127
5
5
  kinemotion/cmj/analysis.py,sha256=OfNTMLPwZIRYbX-Yd8jgZ-7pqnHRz7L2bWAHVYFsQ60,18955
@@ -13,11 +13,12 @@ kinemotion/core/__init__.py,sha256=HsqolRa60cW3vrG8F9Lvr9WvWcs5hCmsTzSgo7imi-4,1
13
13
  kinemotion/core/auto_tuning.py,sha256=wtCUMOhBChVJNXfEeku3GCMW4qED6MF-O_mv2sPTiVQ,11324
14
14
  kinemotion/core/cli_utils.py,sha256=zbnifPhD-OYofJioeYfJtshuWcl8OAEWtqCGVF4ctAI,7966
15
15
  kinemotion/core/debug_overlay_utils.py,sha256=TyUb5okv5qw8oeaX3jsUO_kpwf1NnaHEAOTm-8LwTno,4587
16
+ kinemotion/core/determinism.py,sha256=NwVrHqJiVxxFHTBPVy8aDBJH2SLIcYIpdGFp7glblB8,2515
16
17
  kinemotion/core/experimental.py,sha256=IK05AF4aZS15ke85hF3TWCqRIXU1AlD_XKzFz735Ua8,3640
17
18
  kinemotion/core/filtering.py,sha256=GsC9BB71V07LJJHgS2lsaxUAtJsupcUiwtZFDgODh8c,11417
18
19
  kinemotion/core/formatting.py,sha256=G_3eqgOtym9RFOZVEwCxye4A2cyrmgvtQ214vIshowU,2480
19
20
  kinemotion/core/metadata.py,sha256=iz9YdkesHo-85TVBCoQVn7zkbrSde_fqjU79s_b-TZk,6829
20
- kinemotion/core/pose.py,sha256=ztemdZ_ysVVK3gbXabm8qS_dr1VfJX9KZjmcO-Z-iNE,8532
21
+ kinemotion/core/pose.py,sha256=MQa7ebbuvk_vxJzVlwARKvEaJOqSFJMRRap2dz0O__0,8613
21
22
  kinemotion/core/quality.py,sha256=dPGQp08y8DdEUbUdjTThnUOUsALgF0D2sdz50cm6wLI,13098
22
23
  kinemotion/core/smoothing.py,sha256=GAfC-jxu1eqNyDjsUXqUBicKx9um5hrk49wz1FxfRNM,15219
23
24
  kinemotion/core/validation.py,sha256=LmKfSl4Ayw3DgwKD9IrhsPdzp5ia4drLsHA2UuU1SCM,6310
@@ -26,12 +27,12 @@ kinemotion/dropjump/__init__.py,sha256=tC3H3BrCg8Oj-db-Vrtx4PH_llR1Ppkd5jwaOjhQc
26
27
  kinemotion/dropjump/analysis.py,sha256=B_N_51WoChyQ8I7yaeKeqj3vw7NufgV_3QL-FBZEtW4,28752
27
28
  kinemotion/dropjump/cli.py,sha256=n_Wfv3AC6YIgRPYhO3F2nTSai0NR7fh95nAoWjryQeY,16250
28
29
  kinemotion/dropjump/debug_overlay.py,sha256=LkPw6ucb7beoYWS4L-Lvjs1KLCm5wAWDAfiznUeV2IQ,5668
29
- kinemotion/dropjump/kinematics.py,sha256=yB4ws4VG59SUGcw1J-uXfDFfCMXBdzRh5C4jo0osXbs,17404
30
+ kinemotion/dropjump/kinematics.py,sha256=GhCyXPjVcs7uNt7N9NbTlWzzmoOoMQ0EUdW4j2pomcw,18738
30
31
  kinemotion/dropjump/metrics_validator.py,sha256=sx4RodHpeiW8_PRB0GUJvkUWto1Ard1Dvrc9z8eKk7M,9351
31
32
  kinemotion/dropjump/validation_bounds.py,sha256=5b4I3CKPybuvrbn-nP5yCcGF_sH4Vtyw3a5AWWvWnBk,4645
32
33
  kinemotion/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
- kinemotion-0.36.0.dist-info/METADATA,sha256=oDRVIjwO8LiGTAOuFxGM5AVQAejcl5liHy3WM95eq3c,26020
34
- kinemotion-0.36.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
35
- kinemotion-0.36.0.dist-info/entry_points.txt,sha256=zaqnAnjLvcdrk1Qvj5nvXZCZ2gp0prS7it1zTJygcIY,50
36
- kinemotion-0.36.0.dist-info/licenses/LICENSE,sha256=KZajvqsHw0NoOHOi2q0FZ4NBe9HdV6oey-IPYAtHXfg,1088
37
- kinemotion-0.36.0.dist-info/RECORD,,
34
+ kinemotion-0.36.1.dist-info/METADATA,sha256=uw4lnmICS3YUOtG7f_GXPuQFcpIx0F2H8kM-zBKO3eQ,26020
35
+ kinemotion-0.36.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
36
+ kinemotion-0.36.1.dist-info/entry_points.txt,sha256=zaqnAnjLvcdrk1Qvj5nvXZCZ2gp0prS7it1zTJygcIY,50
37
+ kinemotion-0.36.1.dist-info/licenses/LICENSE,sha256=KZajvqsHw0NoOHOi2q0FZ4NBe9HdV6oey-IPYAtHXfg,1088
38
+ kinemotion-0.36.1.dist-info/RECORD,,