kinemotion 0.76.3__py3-none-any.whl → 2.0.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 (53) hide show
  1. kinemotion/__init__.py +3 -18
  2. kinemotion/api.py +7 -27
  3. kinemotion/cli.py +2 -4
  4. kinemotion/{countermovement_jump → cmj}/analysis.py +158 -16
  5. kinemotion/{countermovement_jump → cmj}/api.py +18 -46
  6. kinemotion/{countermovement_jump → cmj}/cli.py +46 -6
  7. kinemotion/cmj/debug_overlay.py +457 -0
  8. kinemotion/{countermovement_jump → cmj}/joint_angles.py +31 -96
  9. kinemotion/{countermovement_jump → cmj}/metrics_validator.py +271 -176
  10. kinemotion/{countermovement_jump → cmj}/validation_bounds.py +18 -1
  11. kinemotion/core/__init__.py +2 -11
  12. kinemotion/core/auto_tuning.py +107 -149
  13. kinemotion/core/cli_utils.py +0 -74
  14. kinemotion/core/debug_overlay_utils.py +15 -142
  15. kinemotion/core/experimental.py +51 -55
  16. kinemotion/core/filtering.py +56 -116
  17. kinemotion/core/pipeline_utils.py +2 -2
  18. kinemotion/core/pose.py +98 -47
  19. kinemotion/core/quality.py +6 -4
  20. kinemotion/core/smoothing.py +51 -65
  21. kinemotion/core/types.py +0 -15
  22. kinemotion/core/validation.py +7 -76
  23. kinemotion/core/video_io.py +27 -41
  24. kinemotion/{drop_jump → dropjump}/__init__.py +8 -2
  25. kinemotion/{drop_jump → dropjump}/analysis.py +120 -282
  26. kinemotion/{drop_jump → dropjump}/api.py +33 -59
  27. kinemotion/{drop_jump → dropjump}/cli.py +136 -70
  28. kinemotion/dropjump/debug_overlay.py +182 -0
  29. kinemotion/{drop_jump → dropjump}/kinematics.py +65 -175
  30. kinemotion/{drop_jump → dropjump}/metrics_validator.py +51 -25
  31. kinemotion/{drop_jump → dropjump}/validation_bounds.py +1 -1
  32. kinemotion/models/rtmpose-s_simcc-body7_pt-body7-halpe26_700e-256x192-7f134165_20230605.onnx +3 -0
  33. kinemotion/models/yolox_tiny_8xb8-300e_humanart-6f3252f9.onnx +3 -0
  34. {kinemotion-0.76.3.dist-info → kinemotion-2.0.0.dist-info}/METADATA +26 -75
  35. kinemotion-2.0.0.dist-info/RECORD +49 -0
  36. kinemotion/core/overlay_constants.py +0 -61
  37. kinemotion/core/video_analysis_base.py +0 -132
  38. kinemotion/countermovement_jump/debug_overlay.py +0 -325
  39. kinemotion/drop_jump/debug_overlay.py +0 -241
  40. kinemotion/squat_jump/__init__.py +0 -5
  41. kinemotion/squat_jump/analysis.py +0 -377
  42. kinemotion/squat_jump/api.py +0 -610
  43. kinemotion/squat_jump/cli.py +0 -309
  44. kinemotion/squat_jump/debug_overlay.py +0 -163
  45. kinemotion/squat_jump/kinematics.py +0 -342
  46. kinemotion/squat_jump/metrics_validator.py +0 -438
  47. kinemotion/squat_jump/validation_bounds.py +0 -221
  48. kinemotion-0.76.3.dist-info/RECORD +0 -57
  49. /kinemotion/{countermovement_jump → cmj}/__init__.py +0 -0
  50. /kinemotion/{countermovement_jump → cmj}/kinematics.py +0 -0
  51. {kinemotion-0.76.3.dist-info → kinemotion-2.0.0.dist-info}/WHEEL +0 -0
  52. {kinemotion-0.76.3.dist-info → kinemotion-2.0.0.dist-info}/entry_points.txt +0 -0
  53. {kinemotion-0.76.3.dist-info → kinemotion-2.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -10,10 +10,10 @@ from ..core.smoothing import compute_acceleration_from_derivative
10
10
  from ..core.timing import NULL_TIMER, Timer
11
11
  from .analysis import (
12
12
  ContactState,
13
- _detect_drop_start, # pyright: ignore[reportPrivateUsage]
14
- _find_interpolated_phase_transitions_with_curvature, # pyright: ignore[reportPrivateUsage]
15
- _find_landing_from_acceleration, # pyright: ignore[reportPrivateUsage]
13
+ detect_drop_start,
16
14
  find_contact_phases,
15
+ find_interpolated_phase_transitions_with_curvature,
16
+ find_landing_from_acceleration,
17
17
  )
18
18
 
19
19
  if TYPE_CHECKING:
@@ -159,7 +159,7 @@ def _determine_drop_start_frame(
159
159
  """
160
160
  if drop_start_frame is None:
161
161
  # Auto-detect where drop jump actually starts (skip initial stationary period)
162
- return _detect_drop_start(
162
+ return detect_drop_start(
163
163
  foot_y_positions,
164
164
  fps,
165
165
  min_stationary_duration=0.5,
@@ -226,106 +226,14 @@ def _compute_robust_phase_position(
226
226
  return float(np.median(window_positions))
227
227
 
228
228
 
229
- def _detect_drop_jump_air_first_pattern(
230
- air_phases_indexed: list[tuple[int, int, int]],
231
- ground_phases: list[tuple[int, int, int]],
232
- ) -> tuple[int, int] | None:
233
- """Detect drop jump using air-first pattern (box + drop classified as IN_AIR).
234
-
235
- Pattern: IN_AIR(box+drop) → ON_GROUND(contact) → IN_AIR(flight) → ON_GROUND(land)
236
-
237
- Args:
238
- air_phases_indexed: Air phases with indices
239
- ground_phases: Ground phases with indices
240
-
241
- Returns:
242
- (contact_start, contact_end) if drop jump detected, None otherwise
243
- """
244
- if not air_phases_indexed or len(ground_phases) < 2:
245
- return None
246
-
247
- _, _, first_air_idx = air_phases_indexed[0]
248
- first_ground_start, first_ground_end, first_ground_idx = ground_phases[0]
249
-
250
- # Drop jump: first phase is IN_AIR (index 0), second phase is ground (index 1)
251
- if first_air_idx != 0 or first_ground_idx != 1:
252
- return None
253
-
254
- # Check for flight phase after contact
255
- air_after_contact = [i for _, _, i in air_phases_indexed if i > first_ground_idx]
256
- if not air_after_contact:
257
- return None
258
-
259
- return first_ground_start, first_ground_end
260
-
261
-
262
- def _detect_drop_jump_height_pattern(
263
- air_phases_indexed: list[tuple[int, int, int]],
264
- ground_phases: list[tuple[int, int, int]],
265
- foot_y_positions: NDArray[np.float64],
266
- ) -> tuple[int, int] | None:
267
- """Detect drop jump using height comparison (box detected as ground).
268
-
269
- Legacy detection: first ground is on elevated box (lower y value).
270
-
271
- Args:
272
- air_phases_indexed: Air phases with indices
273
- ground_phases: Ground phases with indices
274
- foot_y_positions: Vertical position array
275
-
276
- Returns:
277
- (contact_start, contact_end) if drop jump detected, None otherwise
278
- """
279
- if not air_phases_indexed or len(ground_phases) < 2:
280
- return None
281
-
282
- _, _, first_air_idx = air_phases_indexed[0]
283
- first_ground_start, first_ground_end, first_ground_idx = ground_phases[0]
284
-
285
- # This pattern: first ground is before first air (athlete on box)
286
- if first_ground_idx >= first_air_idx:
287
- return None
288
-
289
- ground_after_air = [
290
- (start, end, idx) for start, end, idx in ground_phases if idx > first_air_idx
291
- ]
292
- if not ground_after_air:
293
- return None
294
-
295
- first_ground_y = _compute_robust_phase_position(
296
- foot_y_positions, first_ground_start, first_ground_end
297
- )
298
- second_ground_start, second_ground_end, _ = ground_after_air[0]
299
- second_ground_y = _compute_robust_phase_position(
300
- foot_y_positions, second_ground_start, second_ground_end
301
- )
302
-
303
- # If second ground is significantly lower (>7% of frame), it's a drop jump
304
- height_diff = second_ground_y - first_ground_y
305
- if height_diff <= 0.07:
306
- return None
307
-
308
- return second_ground_start, second_ground_end
309
-
310
-
311
229
  def _identify_main_contact_phase(
312
- phases: list[tuple[int, int, ContactState]], # noqa: ARG001 # Used in caller for context
230
+ phases: list[tuple[int, int, ContactState]],
313
231
  ground_phases: list[tuple[int, int, int]],
314
232
  air_phases_indexed: list[tuple[int, int, int]],
315
233
  foot_y_positions: NDArray[np.float64],
316
234
  ) -> tuple[int, int, bool]:
317
235
  """Identify the main contact phase and determine if it's a drop jump.
318
236
 
319
- Drop jump detection strategy:
320
- 1. With position-based filtering, box period is classified as IN_AIR
321
- 2. Pattern: IN_AIR(box+drop) → ON_GROUND(contact) → IN_AIR(flight) → ON_GROUND(land)
322
- 3. The FIRST ground phase is the contact phase (before the flight)
323
- 4. The LAST ground phase is the landing (after the flight)
324
-
325
- The key differentiator from regular jump:
326
- - Drop jump: starts with IN_AIR, has 2+ ground phases with air between them
327
- - Regular jump: starts with ON_GROUND, may have multiple phases
328
-
329
237
  Args:
330
238
  phases: All phase tuples
331
239
  ground_phases: Ground phases with indices
@@ -335,21 +243,46 @@ def _identify_main_contact_phase(
335
243
  Returns:
336
244
  Tuple of (contact_start, contact_end, is_drop_jump)
337
245
  """
338
- # Try air-first detection pattern (most common for clean videos)
339
- result = _detect_drop_jump_air_first_pattern(air_phases_indexed, ground_phases)
340
- if result is not None:
341
- return result[0], result[1], True
342
-
343
- # Try height-based detection (fallback for box-as-ground videos)
344
- result = _detect_drop_jump_height_pattern(air_phases_indexed, ground_phases, foot_y_positions)
345
- if result is not None:
346
- return result[0], result[1], True
347
-
348
- # Regular jump: use longest ground contact phase
349
- contact_start, contact_end = max(
350
- [(s, e) for s, e, _ in ground_phases], key=lambda p: p[1] - p[0]
351
- )
352
- return contact_start, contact_end, False
246
+ # Initialize with first ground phase as fallback
247
+ contact_start, contact_end = ground_phases[0][0], ground_phases[0][1]
248
+ is_drop_jump = False
249
+
250
+ # Detect if this is a drop jump or regular jump
251
+ if air_phases_indexed and len(ground_phases) >= 2:
252
+ first_ground_start, first_ground_end, first_ground_idx = ground_phases[0]
253
+ first_air_idx = air_phases_indexed[0][2]
254
+
255
+ # Find ground phase after first air phase
256
+ ground_after_air = [
257
+ (start, end, idx) for start, end, idx in ground_phases if idx > first_air_idx
258
+ ]
259
+
260
+ if ground_after_air and first_ground_idx < first_air_idx:
261
+ # Check if first ground is at higher elevation (lower y) than
262
+ # ground after air using robust temporal averaging
263
+ first_ground_y = _compute_robust_phase_position(
264
+ foot_y_positions, first_ground_start, first_ground_end
265
+ )
266
+ second_ground_start, second_ground_end, _ = ground_after_air[0]
267
+ second_ground_y = _compute_robust_phase_position(
268
+ foot_y_positions, second_ground_start, second_ground_end
269
+ )
270
+
271
+ # If first ground is significantly higher (>7% of frame), it's a drop jump
272
+ # Increased from 0.05 to 0.07 with 11-frame temporal averaging
273
+ # for reproducibility (balances detection sensitivity with noise robustness)
274
+ # Note: MediaPipe has inherent non-determinism (Google issue #3945)
275
+ if second_ground_y - first_ground_y > 0.07:
276
+ is_drop_jump = True
277
+ contact_start, contact_end = second_ground_start, second_ground_end
278
+
279
+ if not is_drop_jump:
280
+ # Regular jump: use longest ground contact phase
281
+ contact_start, contact_end = max(
282
+ [(s, e) for s, e, _ in ground_phases], key=lambda p: p[1] - p[0]
283
+ )
284
+
285
+ return contact_start, contact_end, is_drop_jump
353
286
 
354
287
 
355
288
  def _find_precise_phase_timing(
@@ -384,30 +317,6 @@ def _find_precise_phase_timing(
384
317
  return contact_start_frac, contact_end_frac
385
318
 
386
319
 
387
- def _find_landing_from_phases(
388
- phases: list[tuple[int, int, ContactState]],
389
- flight_start: int,
390
- ) -> int | None:
391
- """Find landing frame from phase detection.
392
-
393
- Looks for the first ON_GROUND phase that starts after the flight_start frame.
394
- This represents the first ground contact after the reactive jump.
395
-
396
- Args:
397
- phases: List of (start, end, state) phase tuples
398
- flight_start: Frame where flight begins (takeoff)
399
-
400
- Returns:
401
- Landing frame (start of landing phase), or None if not found
402
- """
403
- for start, _, state in phases:
404
- if state == ContactState.ON_GROUND and start > flight_start:
405
- # Found the landing phase - return its start frame
406
- return start
407
-
408
- return None
409
-
410
-
411
320
  def _analyze_flight_phase(
412
321
  metrics: DropJumpMetrics,
413
322
  phases: list[tuple[int, int, ContactState]],
@@ -436,20 +345,22 @@ def _analyze_flight_phase(
436
345
  # Find takeoff frame (end of ground contact)
437
346
  flight_start = contact_end
438
347
 
439
- # Use phase detection for landing (more accurate than position-based)
440
- # Find the next ON_GROUND phase after the flight phase
441
- flight_end = _find_landing_from_phases(phases, flight_start)
348
+ # Compute accelerations for landing detection
349
+ accelerations = compute_acceleration_from_derivative(
350
+ foot_y_positions, window_length=smoothing_window, polyorder=polyorder
351
+ )
442
352
 
443
- # If phase detection fails, fall back to position-based detection
444
- if flight_end is None:
445
- accelerations = compute_acceleration_from_derivative(
446
- foot_y_positions, window_length=smoothing_window, polyorder=polyorder
447
- )
448
- flight_end = _find_landing_from_acceleration(
449
- foot_y_positions, accelerations, flight_start, fps
450
- )
353
+ # Use acceleration-based landing detection (like CMJ)
354
+ # This finds the actual ground impact, not just when velocity drops
355
+ flight_end = find_landing_from_acceleration(
356
+ foot_y_positions, accelerations, flight_start, fps, search_duration=0.7
357
+ )
451
358
 
452
- # Find precise sub-frame timing for takeoff and landing
359
+ # Store integer frame indices
360
+ metrics.flight_start_frame = flight_start
361
+ metrics.flight_end_frame = flight_end
362
+
363
+ # Find precise sub-frame timing for takeoff
453
364
  flight_start_frac = float(flight_start)
454
365
  flight_end_frac = float(flight_end)
455
366
 
@@ -462,20 +373,6 @@ def _analyze_flight_phase(
462
373
  flight_start_frac = end_frac
463
374
  break
464
375
 
465
- # Find interpolated landing (start of landing ON_GROUND phase)
466
- for start_frac, _, state in interpolated_phases:
467
- if state == ContactState.ON_GROUND and int(start_frac) >= flight_end - 2:
468
- flight_end_frac = start_frac
469
- break
470
-
471
- # Refine landing frame using floor of interpolated value
472
- # This compensates for velocity-based detection being ~1-2 frames late
473
- refined_flight_end = int(np.floor(flight_end_frac))
474
-
475
- # Store integer frame indices (refined using interpolated values)
476
- metrics.flight_start_frame = flight_start
477
- metrics.flight_end_frame = refined_flight_end
478
-
479
376
  # Calculate flight time
480
377
  flight_frames_precise = flight_end_frac - flight_start_frac
481
378
  metrics.flight_time = flight_frames_precise / fps
@@ -559,7 +456,7 @@ def calculate_drop_jump_metrics(
559
456
  # Find contact phases
560
457
  with timer.measure("dj_find_phases"):
561
458
  phases = find_contact_phases(contact_states)
562
- interpolated_phases = _find_interpolated_phase_transitions_with_curvature(
459
+ interpolated_phases = find_interpolated_phase_transitions_with_curvature(
563
460
  foot_y_positions,
564
461
  contact_states,
565
462
  velocity_threshold,
@@ -600,22 +497,15 @@ def calculate_drop_jump_metrics(
600
497
  phases, ground_phases, air_phases_indexed, foot_y_positions
601
498
  )
602
499
 
603
- # Find precise timing for contact phase (uses curvature refinement)
500
+ # Store integer frame indices
501
+ metrics.contact_start_frame = contact_start
502
+ metrics.contact_end_frame = contact_end
503
+
504
+ # Find precise timing for contact phase
604
505
  contact_start_frac, contact_end_frac = _find_precise_phase_timing(
605
506
  contact_start, contact_end, interpolated_phases
606
507
  )
607
508
 
608
- # Refine contact_start using floor of interpolated value
609
- # This compensates for velocity-based detection being ~1-2 frames late
610
- # because velocity settles AFTER initial impact. Using floor() biases
611
- # toward earlier detection, matching the moment of first ground contact.
612
- refined_contact_start = int(np.floor(contact_start_frac))
613
-
614
- # Store integer frame indices (refined start, raw end)
615
- # Contact end (takeoff) uses raw value as velocity-based detection is accurate
616
- metrics.contact_start_frame = refined_contact_start
617
- metrics.contact_end_frame = contact_end
618
-
619
509
  # Calculate ground contact time
620
510
  contact_frames_precise = contact_end_frac - contact_start_frac
621
511
  metrics.ground_contact_time = contact_frames_precise / fps
@@ -14,7 +14,7 @@ from kinemotion.core.validation import (
14
14
  MetricsValidator,
15
15
  ValidationResult,
16
16
  )
17
- from kinemotion.drop_jump.validation_bounds import (
17
+ from kinemotion.dropjump.validation_bounds import (
18
18
  DropJumpBounds,
19
19
  estimate_athlete_profile,
20
20
  )
@@ -114,37 +114,63 @@ class DropJumpMetricsValidator(MetricsValidator):
114
114
  ) -> None:
115
115
  """Validate contact time."""
116
116
  contact_time_s = contact_time_ms / 1000.0
117
- self._validate_metric_with_bounds(
118
- name="contact_time",
119
- value=contact_time_s,
120
- bounds=DropJumpBounds.CONTACT_TIME,
121
- profile=result.athlete_profile,
122
- result=result,
123
- format_str="{value:.3f}s",
124
- )
117
+ bounds = DropJumpBounds.CONTACT_TIME
118
+
119
+ if not bounds.is_physically_possible(contact_time_s):
120
+ result.add_error(
121
+ "contact_time",
122
+ f"Contact time {contact_time_s:.3f}s physically impossible",
123
+ value=contact_time_s,
124
+ bounds=(bounds.absolute_min, bounds.absolute_max),
125
+ )
126
+ elif result.athlete_profile and not bounds.contains(
127
+ contact_time_s, result.athlete_profile
128
+ ):
129
+ profile_name = result.athlete_profile.value
130
+ result.add_warning(
131
+ "contact_time",
132
+ f"Contact time {contact_time_s:.3f}s unusual for {profile_name} athlete",
133
+ value=contact_time_s,
134
+ )
125
135
 
126
136
  def _check_flight_time(self, flight_time_ms: float, result: DropJumpValidationResult) -> None:
127
137
  """Validate flight time."""
128
138
  flight_time_s = flight_time_ms / 1000.0
129
- self._validate_metric_with_bounds(
130
- name="flight_time",
131
- value=flight_time_s,
132
- bounds=DropJumpBounds.FLIGHT_TIME,
133
- profile=result.athlete_profile,
134
- result=result,
135
- format_str="{value:.3f}s",
136
- )
139
+ bounds = DropJumpBounds.FLIGHT_TIME
140
+
141
+ if not bounds.is_physically_possible(flight_time_s):
142
+ result.add_error(
143
+ "flight_time",
144
+ f"Flight time {flight_time_s:.3f}s physically impossible",
145
+ value=flight_time_s,
146
+ bounds=(bounds.absolute_min, bounds.absolute_max),
147
+ )
148
+ elif result.athlete_profile and not bounds.contains(flight_time_s, result.athlete_profile):
149
+ profile_name = result.athlete_profile.value
150
+ result.add_warning(
151
+ "flight_time",
152
+ f"Flight time {flight_time_s:.3f}s unusual for {profile_name} athlete",
153
+ value=flight_time_s,
154
+ )
137
155
 
138
156
  def _check_jump_height(self, jump_height_m: float, result: DropJumpValidationResult) -> None:
139
157
  """Validate jump height."""
140
- self._validate_metric_with_bounds(
141
- name="jump_height",
142
- value=jump_height_m,
143
- bounds=DropJumpBounds.JUMP_HEIGHT,
144
- profile=result.athlete_profile,
145
- result=result,
146
- format_str="{value:.3f}m",
147
- )
158
+ bounds = DropJumpBounds.JUMP_HEIGHT
159
+
160
+ if not bounds.is_physically_possible(jump_height_m):
161
+ result.add_error(
162
+ "jump_height",
163
+ f"Jump height {jump_height_m:.3f}m physically impossible",
164
+ value=jump_height_m,
165
+ bounds=(bounds.absolute_min, bounds.absolute_max),
166
+ )
167
+ elif result.athlete_profile and not bounds.contains(jump_height_m, result.athlete_profile):
168
+ profile_name = result.athlete_profile.value
169
+ result.add_warning(
170
+ "jump_height",
171
+ f"Jump height {jump_height_m:.3f}m unusual for {profile_name} athlete",
172
+ value=jump_height_m,
173
+ )
148
174
 
149
175
  def _check_rsi(
150
176
  self,
@@ -124,7 +124,7 @@ def _classify_combined_score(combined_score: float) -> AthleteProfile:
124
124
  return AthleteProfile.ELITE
125
125
 
126
126
 
127
- def estimate_athlete_profile(metrics: MetricsDict, _gender: str | None = None) -> AthleteProfile:
127
+ def estimate_athlete_profile(metrics: MetricsDict, gender: str | None = None) -> AthleteProfile:
128
128
  """Estimate athlete profile from drop jump metrics.
129
129
 
130
130
  Uses jump_height and contact_time to classify athlete level.
@@ -0,0 +1,3 @@
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:85c5d669aa0b0c7969799ea9e0620d3b8c1d3c2b6878243e98a1ef8351457986
3
+ size 22793379
@@ -0,0 +1,3 @@
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ceb11c07298f95c50d7c5abeb906d03340c85f23aa79e3e66966e7fb6c307250
3
+ size 20283006
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kinemotion
3
- Version: 0.76.3
3
+ Version: 2.0.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
@@ -51,7 +51,6 @@ Description-Content-Type: text/markdown
51
51
 
52
52
  - **Drop Jump**: Ground contact time, flight time, reactive strength index
53
53
  - **Counter Movement Jump (CMJ)**: Jump height, flight time, countermovement depth, triple extension biomechanics
54
- - **Squat Jump (SJ)**: Pure concentric power, force production, requires athlete mass
55
54
 
56
55
  ## Features
57
56
 
@@ -84,14 +83,6 @@ Description-Content-Type: text/markdown
84
83
  - **Metrics**: Jump height, flight time, countermovement depth, eccentric/concentric durations
85
84
  - **Validated accuracy**: 50.6cm jump (±1 frame precision)
86
85
 
87
- ### Squat Jump (SJ) Analysis
88
-
89
- - **Static squat start** - pure concentric power test (no countermovement)
90
- - **Power/Force calculations** - Sayers regression (R² = 0.87, \<1% error vs force plates)
91
- - **Mass required** - athlete body weight needed for kinetic calculations
92
- - **Metrics**: Jump height, flight time, squat hold/concentric durations, peak/mean power, peak force
93
- - **Phase detection**: Squat hold → concentric → flight → landing
94
-
95
86
  ## ⚠️ Validation Status
96
87
 
97
88
  **Current Status:** Pre-validation (not validated against force plates or motion capture systems)
@@ -237,22 +228,7 @@ kinemotion cmj-analyze video.mp4
237
228
  kinemotion cmj-analyze video.mp4 --output debug.mp4
238
229
  ```
239
230
 
240
- ### Analyzing Squat Jump (SJ)
241
-
242
- Analyzes pure concentric power production:
243
-
244
- ```bash
245
- # Mass is required for power/force calculations
246
- kinemotion sj-analyze video.mp4 --mass 75.0
247
-
248
- # Complete analysis with all outputs
249
- kinemotion sj-analyze video.mp4 --mass 75.0 \
250
- --output debug.mp4 \
251
- --json-output results.json \
252
- --verbose
253
- ```
254
-
255
- ### Common Options (All Jump Types)
231
+ ### Common Options (Both Jump Types)
256
232
 
257
233
  ```bash
258
234
  # Save metrics to JSON
@@ -414,31 +390,6 @@ metrics = process_cmj_video("video.mp4", output_video="debug.mp4")
414
390
  # - Phase-coded visualization
415
391
  ```
416
392
 
417
- ### Squat Jump (SJ) API
418
-
419
- ```python
420
- from kinemotion import process_sj_video
421
-
422
- # Mass is required for power/force calculations
423
- metrics = process_sj_video(
424
- video_path="athlete_sj.mp4",
425
- mass_kg=75.0, # Required: athlete body mass
426
- quality="balanced",
427
- verbose=True
428
- )
429
-
430
- # Access results
431
- print(f"Jump height: {metrics.jump_height:.3f}m")
432
- print(f"Squat hold: {metrics.squat_hold_duration*1000:.1f}ms")
433
- print(f"Concentric: {metrics.concentric_duration*1000:.1f}ms")
434
-
435
- # Power/force (only available if mass provided)
436
- if metrics.peak_power:
437
- print(f"Peak power: {metrics.peak_power:.0f}W")
438
- print(f"Mean power: {metrics.mean_power:.0f}W")
439
- print(f"Peak force: {metrics.peak_force:.0f}N")
440
- ```
441
-
442
393
  ### CSV Export Example
443
394
 
444
395
  ```python
@@ -625,9 +576,9 @@ The debug video includes:
625
576
  **Solutions**:
626
577
 
627
578
  1. **Check video quality**: Ensure the athlete is clearly visible in profile view
628
- 2. **Increase smoothing**: Use `--smoothing-window 7` or higher
629
- 3. **Adjust detection confidence**: Try `--detection-confidence 0.6` or `--tracking-confidence 0.6`
630
- 4. **Generate debug video**: Use `--output` to visualize what's being tracked
579
+ 1. **Increase smoothing**: Use `--smoothing-window 7` or higher
580
+ 1. **Adjust detection confidence**: Try `--detection-confidence 0.6` or `--tracking-confidence 0.6`
581
+ 1. **Generate debug video**: Use `--output` to visualize what's being tracked
631
582
 
632
583
  ### No Pose Detected
633
584
 
@@ -636,9 +587,9 @@ The debug video includes:
636
587
  **Solutions**:
637
588
 
638
589
  1. **Verify video format**: OpenCV must be able to read the video
639
- 2. **Check framing**: Ensure full body is visible in side view
640
- 3. **Lower confidence thresholds**: Try `--detection-confidence 0.3 --tracking-confidence 0.3`
641
- 4. **Test video playback**: Verify video opens correctly with standard video players
590
+ 1. **Check framing**: Ensure full body is visible in side view
591
+ 1. **Lower confidence thresholds**: Try `--detection-confidence 0.3 --tracking-confidence 0.3`
592
+ 1. **Test video playback**: Verify video opens correctly with standard video players
642
593
 
643
594
  ### Incorrect Contact Detection
644
595
 
@@ -647,11 +598,11 @@ The debug video includes:
647
598
  **Solutions**:
648
599
 
649
600
  1. **Generate debug video**: Visualize contact states to diagnose the issue
650
- 2. **Adjust velocity threshold**:
601
+ 1. **Adjust velocity threshold**:
651
602
  - If missing contacts: decrease to `--velocity-threshold 0.01`
652
603
  - If false contacts: increase to `--velocity-threshold 0.03`
653
- 3. **Adjust minimum frames**: `--min-contact-frames 5` for longer required contact
654
- 4. **Check visibility**: Lower `--visibility-threshold 0.3` if feet are partially obscured
604
+ 1. **Adjust minimum frames**: `--min-contact-frames 5` for longer required contact
605
+ 1. **Check visibility**: Lower `--visibility-threshold 0.3` if feet are partially obscured
655
606
 
656
607
  ### Jump Height Seems Wrong
657
608
 
@@ -660,9 +611,9 @@ The debug video includes:
660
611
  **Solutions**:
661
612
 
662
613
  1. **Check video quality**: Ensure video frame rate is adequate (30fps or higher recommended)
663
- 2. **Verify flight time detection**: Check `flight_start_frame` and `flight_end_frame` in JSON
664
- 3. **Compare measurements**: JSON output includes both `jump_height_m` (primary) and `jump_height_kinematic_m` (kinematic-only)
665
- 4. **Check for drop jump detection**: If doing a drop jump, ensure first phase is elevated enough (>5% of frame height)
614
+ 1. **Verify flight time detection**: Check `flight_start_frame` and `flight_end_frame` in JSON
615
+ 1. **Compare measurements**: JSON output includes both `jump_height_m` (primary) and `jump_height_kinematic_m` (kinematic-only)
616
+ 1. **Check for drop jump detection**: If doing a drop jump, ensure first phase is elevated enough (>5% of frame height)
666
617
 
667
618
  ### Video Codec Issues
668
619
 
@@ -671,30 +622,30 @@ The debug video includes:
671
622
  **Solutions**:
672
623
 
673
624
  1. **Install additional codecs**: Ensure OpenCV has proper video codec support
674
- 2. **Try different output format**: Use `.avi` extension instead of `.mp4`
675
- 3. **Check output path**: Ensure write permissions for output directory
625
+ 1. **Try different output format**: Use `.avi` extension instead of `.mp4`
626
+ 1. **Check output path**: Ensure write permissions for output directory
676
627
 
677
628
  ## How It Works
678
629
 
679
630
  1. **Pose Tracking**: MediaPipe extracts 2D pose landmarks (foot points: ankles, heels, foot indices) from each frame
680
- 2. **Position Calculation**: Averages ankle, heel, and foot index positions to determine foot location
681
- 3. **Smoothing**: Savitzky-Golay filter reduces tracking jitter while preserving motion dynamics
682
- 4. **Contact Detection**: Analyzes vertical position velocity to identify ground contact vs. flight phases
683
- 5. **Phase Identification**: Finds continuous ground contact and flight periods
631
+ 1. **Position Calculation**: Averages ankle, heel, and foot index positions to determine foot location
632
+ 1. **Smoothing**: Savitzky-Golay filter reduces tracking jitter while preserving motion dynamics
633
+ 1. **Contact Detection**: Analyzes vertical position velocity to identify ground contact vs. flight phases
634
+ 1. **Phase Identification**: Finds continuous ground contact and flight periods
684
635
  - Automatically detects drop jumps vs regular jumps
685
636
  - For drop jumps: identifies box → drop → ground contact → jump sequence
686
- 6. **Sub-Frame Interpolation**: Estimates exact transition times between frames
637
+ 1. **Sub-Frame Interpolation**: Estimates exact transition times between frames
687
638
  - Uses Savitzky-Golay derivative for smooth velocity calculation
688
639
  - Linear interpolation of velocity to find threshold crossings
689
640
  - Achieves sub-millisecond timing precision (at 30fps: ±10ms vs ±33ms)
690
641
  - Reduces timing error by 60-70% for contact and flight measurements
691
642
  - Smoother velocity curves eliminate false threshold crossings
692
- 7. **Trajectory Curvature Analysis**: Refines transitions using acceleration patterns
643
+ 1. **Trajectory Curvature Analysis**: Refines transitions using acceleration patterns
693
644
  - Computes second derivative (acceleration) from position trajectory
694
645
  - Detects landing impact by acceleration spike
695
646
  - Identifies takeoff by acceleration change patterns
696
647
  - Provides independent validation and refinement of velocity-based detection
697
- 8. **Metric Calculation**:
648
+ 1. **Metric Calculation**:
698
649
  - Ground contact time = contact phase duration (using fractional frames)
699
650
  - Flight time = flight phase duration (using fractional frames)
700
651
  - Jump height = kinematic estimate from flight time: (g × t²) / 8
@@ -744,9 +695,9 @@ uv run ruff check && uv run pyright && uv run pytest
744
695
  Before committing code, ensure all checks pass:
745
696
 
746
697
  1. Format with Black
747
- 2. Fix linting issues with ruff
748
- 3. Ensure type safety with pyright
749
- 4. Run all tests with pytest
698
+ 1. Fix linting issues with ruff
699
+ 1. Ensure type safety with pyright
700
+ 1. Run all tests with pytest
750
701
 
751
702
  See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines and requirements, or [CLAUDE.md](CLAUDE.md) for detailed development guidelines.
752
703