kinemotion 0.38.1__py3-none-any.whl → 0.39.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.

kinemotion/api.py CHANGED
@@ -8,7 +8,7 @@ from pathlib import Path
8
8
 
9
9
  import numpy as np
10
10
 
11
- from .cmj.analysis import detect_cmj_phases
11
+ from .cmj.analysis import compute_average_hip_position, detect_cmj_phases
12
12
  from .cmj.debug_overlay import CMJDebugOverlayRenderer
13
13
  from .cmj.kinematics import CMJMetrics, calculate_cmj_metrics
14
14
  from .cmj.metrics_validator import CMJMetricsValidator
@@ -263,11 +263,13 @@ def _calculate_foot_visibility(frame_landmarks: dict) -> float:
263
263
 
264
264
  def _extract_vertical_positions(
265
265
  smoothed_landmarks: list,
266
+ target: str = "foot",
266
267
  ) -> tuple[np.ndarray, np.ndarray]:
267
- """Extract vertical foot positions and visibilities from smoothed landmarks.
268
+ """Extract vertical positions and visibilities from smoothed landmarks.
268
269
 
269
270
  Args:
270
271
  smoothed_landmarks: Smoothed landmark sequence
272
+ target: Tracking target "foot" or "hip" (default: "foot")
271
273
 
272
274
  Returns:
273
275
  Tuple of (vertical_positions, visibilities) as numpy arrays
@@ -277,9 +279,23 @@ def _extract_vertical_positions(
277
279
 
278
280
  for frame_landmarks in smoothed_landmarks:
279
281
  if frame_landmarks:
280
- _, foot_y = compute_average_foot_position(frame_landmarks)
281
- position_list.append(foot_y)
282
- visibilities_list.append(_calculate_foot_visibility(frame_landmarks))
282
+ if target == "hip":
283
+ _, y = compute_average_hip_position(frame_landmarks)
284
+ # For hips, we can use average visibility of hips if needed,
285
+ # but currently _calculate_foot_visibility is specific to feet.
286
+ # We'll stick to foot visibility for now as it indicates
287
+ # overall leg tracking quality, or we could implement
288
+ # _calculate_hip_visibility. For simplicity, we'll use foot
289
+ # visibility as a proxy for "body visibility" or just use 1.0
290
+ # since hips are usually visible if feet are. Actually, let's
291
+ # just use foot visibility for consistency in quality checks.
292
+ vis = _calculate_foot_visibility(frame_landmarks)
293
+ else:
294
+ _, y = compute_average_foot_position(frame_landmarks)
295
+ vis = _calculate_foot_visibility(frame_landmarks)
296
+
297
+ position_list.append(y)
298
+ visibilities_list.append(vis)
283
299
  else:
284
300
  position_list.append(position_list[-1] if position_list else 0.5)
285
301
  visibilities_list.append(0.0)
@@ -915,13 +931,21 @@ def process_cmj_video(
915
931
  # Apply smoothing
916
932
  smoothed_landmarks = _apply_smoothing(landmarks_sequence, params, verbose)
917
933
 
918
- # Extract foot positions
934
+ # Extract vertical positions
919
935
  if verbose:
920
- print("Extracting foot positions...")
936
+ print("Extracting vertical positions (Hip and Foot)...")
937
+
938
+ # Primary: Hips (for depth, velocity, general phases)
921
939
  vertical_positions, visibilities = _extract_vertical_positions(
922
- smoothed_landmarks
940
+ smoothed_landmarks, target="hip"
923
941
  )
924
- tracking_method = "foot"
942
+
943
+ # Secondary: Feet (for precise landing detection)
944
+ foot_positions, _ = _extract_vertical_positions(
945
+ smoothed_landmarks, target="foot"
946
+ )
947
+
948
+ tracking_method = "hip_hybrid"
925
949
 
926
950
  # Detect CMJ phases
927
951
  if verbose:
@@ -932,6 +956,7 @@ def process_cmj_video(
932
956
  video.fps,
933
957
  window_length=params.smoothing_window,
934
958
  polyorder=params.polyorder,
959
+ landing_positions=foot_positions, # Use feet for landing
935
960
  )
936
961
 
937
962
  if phases is None:
@@ -390,36 +390,90 @@ def find_lowest_frame(
390
390
 
391
391
 
392
392
  def find_landing_frame(
393
- accelerations: np.ndarray, peak_height_frame: int, fps: float
393
+ accelerations: np.ndarray,
394
+ velocities: np.ndarray,
395
+ peak_height_frame: int,
396
+ fps: float,
394
397
  ) -> float:
395
- """Find landing frame after peak height after takeoff.
398
+ """Find landing frame after peak height.
399
+
400
+ Robust detection strategy:
401
+ 1. Find peak downward velocity (maximum positive velocity) after peak height.
402
+ This corresponds to the moment just before or at initial ground contact.
403
+ 2. Look for maximum deceleration (impact) *after* the peak velocity.
404
+ This filters out mid-air tracking noise/flutter that can cause false
405
+ deceleration spikes while the athlete is still accelerating downward.
396
406
 
397
- Detects landing by finding the minimum acceleration (impact) in a search
398
- window after peak height. The window is extended to 1.0s to ensure all
399
- realistic flight times are captured.
407
+ Args:
408
+ accelerations: Vertical acceleration array (deriv=2)
409
+ velocities: Vertical velocity array (deriv=1)
410
+ peak_height_frame: Frame index of peak jump height
411
+ fps: Video frame rate
400
412
 
401
- Robust detection: When accelerations are nearly flat, skips the impact
402
- frames near peak height and looks for the actual landing signal.
413
+ Returns:
414
+ Frame index of landing impact.
403
415
  """
404
- landing_search_start = peak_height_frame
405
416
  # Search window extended to 1.0s to accommodate all realistic flight times
406
- # (recreational: 0.25-0.65s, elite: 0.65-0.95s, max: 1.1s)
407
- landing_search_end = min(len(accelerations), peak_height_frame + int(fps * 1.0))
417
+ search_end = min(len(accelerations), peak_height_frame + int(fps * 1.0))
418
+
419
+ # 1. Find peak downward velocity (max positive value)
420
+ # Search from peak height to end of window
421
+ vel_search_window = velocities[peak_height_frame:search_end]
422
+
423
+ if len(vel_search_window) == 0:
424
+ return float(peak_height_frame + int(fps * 0.3))
425
+
426
+ # Index relative to peak_height_frame
427
+ peak_vel_rel_idx = int(np.argmax(vel_search_window))
428
+ peak_vel_frame = peak_height_frame + peak_vel_rel_idx
429
+
430
+ # 2. Search for impact (min acceleration) starting from peak velocity
431
+ # We allow a small buffer (e.g., 1-2 frames) before peak velocity just in case
432
+ # peak velocity coincides with impact start due to smoothing
433
+ landing_search_start = max(peak_height_frame, peak_vel_frame - 2)
434
+ landing_search_end = search_end
435
+
408
436
  landing_accelerations = accelerations[landing_search_start:landing_search_end]
409
437
 
410
438
  if len(landing_accelerations) == 0:
439
+ # Fallback if window is empty
411
440
  return float(peak_height_frame + int(fps * 0.3))
412
441
 
413
- # Skip the first 2 frames after peak (often have unreliable acceleration)
414
- # This avoids locking onto peak height acceleration instead of impact
415
- skip_frames = 2
416
- if len(landing_accelerations) > skip_frames:
417
- landing_accelerations_filtered = landing_accelerations[skip_frames:]
418
- landing_idx = int(np.argmin(landing_accelerations_filtered)) + skip_frames
419
- else:
420
- landing_idx = int(np.argmin(landing_accelerations))
442
+ # Find minimum acceleration (maximum deceleration spike)
443
+ landing_rel_idx = int(np.argmin(landing_accelerations))
444
+ landing_frame = landing_search_start + landing_rel_idx
445
+
446
+ return float(landing_frame)
421
447
 
422
- return float(landing_search_start + landing_idx)
448
+
449
+ def compute_average_hip_position(
450
+ landmarks: dict[str, tuple[float, float, float]],
451
+ ) -> tuple[float, float]:
452
+ """
453
+ Compute average hip position from hip landmarks.
454
+
455
+ Args:
456
+ landmarks: Dictionary of landmark positions
457
+
458
+ Returns:
459
+ (x, y) average hip position in normalized coordinates
460
+ """
461
+ hip_keys = ["left_hip", "right_hip"]
462
+
463
+ x_positions = []
464
+ y_positions = []
465
+
466
+ for key in hip_keys:
467
+ if key in landmarks:
468
+ x, y, visibility = landmarks[key]
469
+ if visibility > 0.5: # Only use visible landmarks
470
+ x_positions.append(x)
471
+ y_positions.append(y)
472
+
473
+ if not x_positions:
474
+ return (0.5, 0.5) # Default to center if no visible hips
475
+
476
+ return (float(np.mean(x_positions)), float(np.mean(y_positions)))
423
477
 
424
478
 
425
479
  def find_standing_end(
@@ -490,6 +544,7 @@ def detect_cmj_phases(
490
544
  fps: float,
491
545
  window_length: int = 5,
492
546
  polyorder: int = 2,
547
+ landing_positions: np.ndarray | None = None,
493
548
  ) -> tuple[float | None, float, float, float] | None:
494
549
  """
495
550
  Detect all phases of a counter movement jump using a simplified, robust approach.
@@ -501,16 +556,18 @@ def detect_cmj_phases(
501
556
  4. Find landing (impact after peak height)
502
557
 
503
558
  Args:
504
- positions: Array of vertical positions (normalized 0-1)
559
+ positions: Array of vertical positions (normalized 0-1). Typically Hips/CoM.
505
560
  fps: Video frame rate
506
561
  window_length: Window size for derivative calculations
507
562
  polyorder: Polynomial order for Savitzky-Golay filter
563
+ landing_positions: Optional array of positions for landing detection
564
+ (e.g., Feet). If None, uses `positions` (Hips) for landing too.
508
565
 
509
566
  Returns:
510
567
  Tuple of (standing_end_frame, lowest_point_frame, takeoff_frame, landing_frame)
511
568
  with fractional precision, or None if phases cannot be detected.
512
569
  """
513
- # Compute SIGNED velocities and accelerations
570
+ # Compute SIGNED velocities and accelerations for primary signal (Hips)
514
571
  velocities = compute_signed_velocity(
515
572
  positions, window_length=window_length, polyorder=polyorder
516
573
  )
@@ -526,7 +583,32 @@ def detect_cmj_phases(
526
583
  # Step 2-4: Find all phases using helper functions
527
584
  takeoff_frame = find_takeoff_frame(velocities, peak_height_frame, fps)
528
585
  lowest_point = find_lowest_frame(velocities, positions, takeoff_frame, fps)
529
- landing_frame = find_landing_frame(accelerations, peak_height_frame, fps)
530
- standing_end = find_standing_end(velocities, lowest_point, positions, accelerations)
531
586
 
587
+ # Determine landing frame
588
+ if landing_positions is not None:
589
+ # Use specific landing signal (Feet) for landing detection
590
+ landing_velocities = compute_signed_velocity(
591
+ landing_positions, window_length=window_length, polyorder=polyorder
592
+ )
593
+ landing_accelerations = compute_acceleration_from_derivative(
594
+ landing_positions, window_length=window_length, polyorder=polyorder
595
+ )
596
+ # We still reference peak_height_frame from Hips, as Feet peak
597
+ # might be different/noisy but generally they align in time.
598
+ landing_frame = find_landing_frame(
599
+ landing_accelerations,
600
+ landing_velocities,
601
+ peak_height_frame,
602
+ fps,
603
+ )
604
+ else:
605
+ # Use primary signal (Hips)
606
+ landing_frame = find_landing_frame(
607
+ accelerations,
608
+ velocities,
609
+ peak_height_frame,
610
+ fps,
611
+ )
612
+
613
+ standing_end = find_standing_end(velocities, lowest_point, positions, accelerations)
532
614
  return (standing_end, lowest_point, takeoff_frame, landing_frame)
@@ -180,6 +180,24 @@ def calculate_cmj_metrics(
180
180
  g = 9.81 # gravity in m/s^2
181
181
  jump_height = (g * flight_time**2) / 8
182
182
 
183
+ # Determine scaling factor (meters per normalized unit)
184
+ # We use the flight phase displacement in normalized units compared to
185
+ # kinematic jump height
186
+ flight_start_idx = int(takeoff_frame)
187
+ flight_end_idx = int(landing_frame)
188
+ flight_positions = positions[flight_start_idx:flight_end_idx]
189
+
190
+ scale_factor = 0.0
191
+ if len(flight_positions) > 0:
192
+ # Peak height is minimum y value (highest point in frame)
193
+ peak_flight_pos = np.min(flight_positions)
194
+ takeoff_pos = positions[flight_start_idx]
195
+ # Displacement is upward (takeoff_pos - peak_pos) because y decreases upward
196
+ flight_displacement = takeoff_pos - peak_flight_pos
197
+
198
+ if flight_displacement > 0.001: # Avoid division by zero or noise
199
+ scale_factor = jump_height / flight_displacement
200
+
183
201
  # Calculate countermovement depth
184
202
  if standing_start_frame is not None:
185
203
  standing_position = positions[int(standing_start_frame)]
@@ -188,7 +206,10 @@ def calculate_cmj_metrics(
188
206
  standing_position = positions[0]
189
207
 
190
208
  lowest_position = positions[int(lowest_point_frame)]
191
- countermovement_depth = abs(standing_position - lowest_position)
209
+ # Depth in normalized units
210
+ depth_normalized = abs(standing_position - lowest_position)
211
+ # Convert to meters
212
+ countermovement_depth = depth_normalized * scale_factor
192
213
 
193
214
  # Calculate phase durations
194
215
  if standing_start_frame is not None:
@@ -201,8 +222,12 @@ def calculate_cmj_metrics(
201
222
 
202
223
  concentric_duration = (takeoff_frame - lowest_point_frame) / fps
203
224
 
225
+ # Velocity scaling factor: units/frame -> meters/second
226
+ # v_m_s = v_units_frame * fps * scale_factor
227
+ velocity_scale = scale_factor * fps
228
+
204
229
  # Calculate peak velocities
205
- # Eccentric phase: negative velocities (downward)
230
+ # Eccentric phase: Downward motion = Positive velocity in image coords
206
231
  if standing_start_frame is not None:
207
232
  eccentric_start_idx = int(standing_start_frame)
208
233
  else:
@@ -212,18 +237,26 @@ def calculate_cmj_metrics(
212
237
  eccentric_velocities = velocities[eccentric_start_idx:eccentric_end_idx]
213
238
 
214
239
  if len(eccentric_velocities) > 0:
215
- # Peak eccentric velocity is most negative value
216
- peak_eccentric_velocity = float(np.min(eccentric_velocities))
240
+ # Peak eccentric velocity is maximum positive value (fastest downward)
241
+ # We take max and ensure it's positive (it should be)
242
+ peak_eccentric_velocity = float(np.max(eccentric_velocities)) * velocity_scale
243
+ # If max is negative (weird), it means no downward motion detected
244
+ if peak_eccentric_velocity < 0:
245
+ peak_eccentric_velocity = 0.0
217
246
  else:
218
247
  peak_eccentric_velocity = 0.0
219
248
 
220
- # Concentric phase: positive velocities (upward)
249
+ # Concentric phase: Upward motion = Negative velocity in image coords
221
250
  concentric_start_idx = int(lowest_point_frame)
222
251
  concentric_end_idx = int(takeoff_frame)
223
252
  concentric_velocities = velocities[concentric_start_idx:concentric_end_idx]
224
253
 
225
254
  if len(concentric_velocities) > 0:
226
- peak_concentric_velocity = float(np.max(concentric_velocities))
255
+ # Peak concentric velocity is minimum value (most negative = fastest upward)
256
+ # We take abs to report magnitude
257
+ peak_concentric_velocity = (
258
+ abs(float(np.min(concentric_velocities))) * velocity_scale
259
+ )
227
260
  else:
228
261
  peak_concentric_velocity = 0.0
229
262
 
@@ -704,8 +704,8 @@ def find_landing_from_acceleration(
704
704
  """
705
705
  Find landing frame by detecting impact acceleration after takeoff.
706
706
 
707
- Similar to CMJ landing detection, looks for maximum positive acceleration
708
- (deceleration on ground impact) after the jump peak.
707
+ Detects the moment of initial ground contact, characterized by a sharp
708
+ deceleration (positive acceleration spike) as downward velocity is arrested.
709
709
 
710
710
  Args:
711
711
  positions: Array of vertical positions (normalized 0-1)
@@ -736,29 +736,11 @@ def find_landing_from_acceleration(
736
736
  if landing_search_end <= landing_search_start:
737
737
  return min(len(positions) - 1, peak_frame + int(fps * 0.2))
738
738
 
739
- # Find impact: maximum positive acceleration after peak
739
+ # Find impact: maximum negative acceleration after peak (deceleration on impact)
740
+ # The impact creates a large upward force (negative acceleration in Y-down)
740
741
  landing_accelerations = accelerations[landing_search_start:landing_search_end]
741
- impact_idx = int(np.argmax(landing_accelerations))
742
- impact_frame = landing_search_start + impact_idx
743
-
744
- # After acceleration peak, look for position stabilization (full ground contact)
745
- # Check where vertical position stops decreasing (athlete stops compressing)
746
- stabilization_search_start = impact_frame
747
- stabilization_search_end = min(len(positions), impact_frame + int(fps * 0.2))
748
-
749
- landing_frame = impact_frame
750
- if stabilization_search_end > stabilization_search_start + 3:
751
- # Find where position reaches maximum (lowest point) and starts stabilizing
752
- search_positions = positions[
753
- stabilization_search_start:stabilization_search_end
754
- ]
755
-
756
- # Look for the frame where position reaches its maximum (deepest landing)
757
- max_pos_idx = int(np.argmax(search_positions))
758
-
759
- # Landing is just after max position (athlete at deepest landing compression)
760
- landing_frame = stabilization_search_start + max_pos_idx
761
- landing_frame = min(len(positions) - 1, landing_frame)
742
+ impact_idx = int(np.argmin(landing_accelerations))
743
+ landing_frame = landing_search_start + impact_idx
762
744
 
763
745
  return landing_frame
764
746
 
@@ -28,6 +28,7 @@ class DropJumpDataDict(TypedDict, total=False):
28
28
  flight_time_ms: float | None
29
29
  jump_height_m: float | None
30
30
  jump_height_kinematic_m: float | None
31
+ jump_height_trajectory_m: float | None
31
32
  jump_height_trajectory_normalized: float | None
32
33
  contact_start_frame: int | None
33
34
  contact_end_frame: int | None
@@ -56,7 +57,10 @@ class DropJumpMetrics:
56
57
  self.flight_time: float | None = None
57
58
  self.jump_height: float | None = None
58
59
  self.jump_height_kinematic: float | None = None # From flight time
59
- self.jump_height_trajectory: float | None = None # From position tracking
60
+ # From position tracking (normalized)
61
+ self.jump_height_trajectory: float | None = None
62
+ # From position tracking (meters)
63
+ self.jump_height_trajectory_m: float | None = None
60
64
  self.drop_start_frame: int | None = None # Frame when athlete leaves box
61
65
  self.contact_start_frame: int | None = None
62
66
  self.contact_end_frame: int | None = None
@@ -90,6 +94,9 @@ class DropJumpMetrics:
90
94
  "jump_height_kinematic_m": format_float_metric(
91
95
  self.jump_height_kinematic, 1, 3
92
96
  ),
97
+ "jump_height_trajectory_m": format_float_metric(
98
+ self.jump_height_trajectory_m, 1, 3
99
+ ),
93
100
  "jump_height_trajectory_normalized": format_float_metric(
94
101
  self.jump_height_trajectory, 1, 4
95
102
  ),
@@ -408,6 +415,14 @@ def _analyze_flight_phase(
408
415
  height_normalized = float(takeoff_position - peak_position)
409
416
  metrics.jump_height_trajectory = height_normalized
410
417
 
418
+ # Calculate scale factor and metric height
419
+ # Scale factor = kinematic height / normalized height
420
+ if height_normalized > 0.001:
421
+ scale_factor = metrics.jump_height_kinematic / height_normalized
422
+ metrics.jump_height_trajectory_m = height_normalized * scale_factor
423
+ else:
424
+ metrics.jump_height_trajectory_m = 0.0
425
+
411
426
 
412
427
  def calculate_drop_jump_metrics(
413
428
  contact_states: list[ContactState],
@@ -83,7 +83,7 @@ class DropJumpMetricsValidator(MetricsValidator):
83
83
  flight_time_ms = data.get("flight_time_ms")
84
84
  jump_height_m = data.get("jump_height_m")
85
85
  jump_height_kinematic_m = data.get("jump_height_kinematic_m")
86
- jump_height_trajectory_m = data.get("jump_height_trajectory_normalized")
86
+ jump_height_trajectory_m = data.get("jump_height_trajectory_m")
87
87
 
88
88
  # Validate individual metrics
89
89
  if contact_time_ms is not None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kinemotion
3
- Version: 0.38.1
3
+ Version: 0.39.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
@@ -1,12 +1,12 @@
1
1
  kinemotion/__init__.py,sha256=wPItmyGJUOFM6GPRVhAEvRz0-ErI7e2qiUREYJ9EfPQ,943
2
- kinemotion/api.py,sha256=HdbdPxc6MTJO6lldYgfKGaF7SZuroKcWITS7rjhL8PA,39764
2
+ kinemotion/api.py,sha256=1MsBvkznlP_OaBZ37a0hG9uMTESv2iJMHR01tnPv5sA,41046
3
3
  kinemotion/cli.py,sha256=cqYV_7URH0JUDy1VQ_EDLv63FmNO4Ns20m6s1XAjiP4,464
4
4
  kinemotion/cmj/__init__.py,sha256=Ynv0-Oco4I3Y1Ubj25m3h9h2XFqeNwpAewXmAYOmwfU,127
5
- kinemotion/cmj/analysis.py,sha256=OfNTMLPwZIRYbX-Yd8jgZ-7pqnHRz7L2bWAHVYFsQ60,18955
5
+ kinemotion/cmj/analysis.py,sha256=qtULzp9uYzm5M0_Qu5YGJpuwjg9fz1VKAg6xg4NJxvM,21639
6
6
  kinemotion/cmj/cli.py,sha256=Mj2h9It1jVjAauvtCxfLWTRijj7zbYhxZuebhw2Zz6w,10828
7
7
  kinemotion/cmj/debug_overlay.py,sha256=fXmWoHhqMLGo4vTtB6Ezs3yLUDOLw63zLIgU2gFlJQU,15892
8
8
  kinemotion/cmj/joint_angles.py,sha256=HmheIEiKcQz39cRezk4h-htorOhGNPsqKIR9RsAEKts,9960
9
- kinemotion/cmj/kinematics.py,sha256=qRBe87NkX-7HQTQ8RoF-EpvfcffgP5vycJJRrxpHboc,10307
9
+ kinemotion/cmj/kinematics.py,sha256=Lq9m9MNQxnXv31VhKmXVrlM7rRkhi8PxW50N_CC8_8Y,11860
10
10
  kinemotion/cmj/metrics_validator.py,sha256=V_fmlczYH06SBtwqESv-IfGi3wDsIy3RQbd7VwOyNo0,31359
11
11
  kinemotion/cmj/validation_bounds.py,sha256=9ZTo68fl3ooyWjXXyTMRLpK9tFANa_rQf3oHhq7iQGE,11995
12
12
  kinemotion/core/__init__.py,sha256=HsqolRa60cW3vrG8F9Lvr9WvWcs5hCmsTzSgo7imi-4,1278
@@ -24,15 +24,15 @@ kinemotion/core/smoothing.py,sha256=GAfC-jxu1eqNyDjsUXqUBicKx9um5hrk49wz1FxfRNM,
24
24
  kinemotion/core/validation.py,sha256=LmKfSl4Ayw3DgwKD9IrhsPdzp5ia4drLsHA2UuU1SCM,6310
25
25
  kinemotion/core/video_io.py,sha256=fDdyYVIKqUSgCjBJa8l_S0SrDPDAhrWYfsDBNRuz1oM,7549
26
26
  kinemotion/dropjump/__init__.py,sha256=tC3H3BrCg8Oj-db-Vrtx4PH_llR1Ppkd5jwaOjhQcLg,862
27
- kinemotion/dropjump/analysis.py,sha256=B_N_51WoChyQ8I7yaeKeqj3vw7NufgV_3QL-FBZEtW4,28752
27
+ kinemotion/dropjump/analysis.py,sha256=MjxO-vps0nz_hXlnGk7cgq3jFenJYzsM0VVpHwnHXsM,27935
28
28
  kinemotion/dropjump/cli.py,sha256=n_Wfv3AC6YIgRPYhO3F2nTSai0NR7fh95nAoWjryQeY,16250
29
29
  kinemotion/dropjump/debug_overlay.py,sha256=LkPw6ucb7beoYWS4L-Lvjs1KLCm5wAWDAfiznUeV2IQ,5668
30
- kinemotion/dropjump/kinematics.py,sha256=GhCyXPjVcs7uNt7N9NbTlWzzmoOoMQ0EUdW4j2pomcw,18738
31
- kinemotion/dropjump/metrics_validator.py,sha256=sx4RodHpeiW8_PRB0GUJvkUWto1Ard1Dvrc9z8eKk7M,9351
30
+ kinemotion/dropjump/kinematics.py,sha256=kH-XM66wlOCYMpjvyb6_Qh5ZebyOfFZ47rmhgE1Tww4,19404
31
+ kinemotion/dropjump/metrics_validator.py,sha256=CrTlGup8q2kyPXtA6HNwm7_yq0AsBaDllG7RVZdXmYA,9342
32
32
  kinemotion/dropjump/validation_bounds.py,sha256=5b4I3CKPybuvrbn-nP5yCcGF_sH4Vtyw3a5AWWvWnBk,4645
33
33
  kinemotion/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
- kinemotion-0.38.1.dist-info/METADATA,sha256=TSCrEkzEwuhfVd9h0OHi4bHEJVwm5PAKQC3YBAdmYh4,26020
35
- kinemotion-0.38.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
36
- kinemotion-0.38.1.dist-info/entry_points.txt,sha256=zaqnAnjLvcdrk1Qvj5nvXZCZ2gp0prS7it1zTJygcIY,50
37
- kinemotion-0.38.1.dist-info/licenses/LICENSE,sha256=KZajvqsHw0NoOHOi2q0FZ4NBe9HdV6oey-IPYAtHXfg,1088
38
- kinemotion-0.38.1.dist-info/RECORD,,
34
+ kinemotion-0.39.0.dist-info/METADATA,sha256=ytvO76fvfntV_Vpjkc-4BezahvOO91a2I6X3PUM0eAk,26020
35
+ kinemotion-0.39.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
36
+ kinemotion-0.39.0.dist-info/entry_points.txt,sha256=zaqnAnjLvcdrk1Qvj5nvXZCZ2gp0prS7it1zTJygcIY,50
37
+ kinemotion-0.39.0.dist-info/licenses/LICENSE,sha256=KZajvqsHw0NoOHOi2q0FZ4NBe9HdV6oey-IPYAtHXfg,1088
38
+ kinemotion-0.39.0.dist-info/RECORD,,