kinemotion 0.28.0__py3-none-any.whl → 0.29.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.
@@ -0,0 +1,380 @@
1
+ """CMJ metrics physiological bounds for validation testing.
2
+
3
+ This module defines realistic physiological bounds for Counter Movement Jump (CMJ)
4
+ metrics based on biomechanical literature and real-world athlete performance.
5
+
6
+ These bounds are used to:
7
+ 1. Prevent false positives from measurement noise
8
+ 2. Catch real errors in video processing and phase detection
9
+ 3. Provide athlete-profile-appropriate validation
10
+ 4. Enable cross-validation of metric consistency
11
+
12
+ References:
13
+ - Nordez et al. (2009): CMJ depth-height relationships
14
+ - Cormie et al. (2011): Power generation characteristics
15
+ - Bogdanis (2012): Plyometric training effects
16
+ """
17
+
18
+ from dataclasses import dataclass
19
+ from enum import Enum
20
+
21
+
22
+ class AthleteProfile(Enum):
23
+ """Athlete performance categories for metric bounds."""
24
+
25
+ ELDERLY = "elderly" # 70+, deconditioned
26
+ UNTRAINED = "untrained" # Sedentary, no training
27
+ RECREATIONAL = "recreational" # Fitness class, moderate activity
28
+ TRAINED = "trained" # Regular athlete, 3-5 years training
29
+ ELITE = "elite" # Competitive athlete, college/professional level
30
+
31
+
32
+ @dataclass
33
+ class MetricBounds:
34
+ """Physiological bounds for a single metric.
35
+
36
+ Attributes:
37
+ absolute_min: Absolute minimum value (error threshold)
38
+ practical_min: Practical minimum for weakest athletes
39
+ recreational_min: Minimum for recreational athletes
40
+ recreational_max: Maximum for recreational athletes
41
+ elite_min: Minimum for elite athletes
42
+ elite_max: Maximum for elite athletes
43
+ absolute_max: Absolute maximum value (error threshold)
44
+ unit: Unit of measurement (e.g., "m", "s", "m/s", "degrees")
45
+ """
46
+
47
+ absolute_min: float
48
+ practical_min: float
49
+ recreational_min: float
50
+ recreational_max: float
51
+ elite_min: float
52
+ elite_max: float
53
+ absolute_max: float
54
+ unit: str
55
+
56
+ def contains(self, value: float, profile: AthleteProfile) -> bool:
57
+ """Check if value is within bounds for athlete profile."""
58
+ if profile == AthleteProfile.ELDERLY:
59
+ return self.practical_min <= value <= self.recreational_max
60
+ elif profile == AthleteProfile.UNTRAINED:
61
+ return self.practical_min <= value <= self.recreational_max
62
+ elif profile == AthleteProfile.RECREATIONAL:
63
+ return self.recreational_min <= value <= self.recreational_max
64
+ elif profile == AthleteProfile.TRAINED:
65
+ # Trained athletes: midpoint between recreational and elite
66
+ trained_min = (self.recreational_min + self.elite_min) / 2
67
+ trained_max = (self.recreational_max + self.elite_max) / 2
68
+ return trained_min <= value <= trained_max
69
+ elif profile == AthleteProfile.ELITE:
70
+ return self.elite_min <= value <= self.elite_max
71
+ return False
72
+
73
+ def is_physically_possible(self, value: float) -> bool:
74
+ """Check if value is within absolute physiological limits."""
75
+ return self.absolute_min <= value <= self.absolute_max
76
+
77
+
78
+ class CMJBounds:
79
+ """Collection of physiological bounds for all CMJ metrics."""
80
+
81
+ # FLIGHT TIME (seconds)
82
+ FLIGHT_TIME = MetricBounds(
83
+ absolute_min=0.08, # Frame rate resolution limit
84
+ practical_min=0.15, # Minimum effort jump
85
+ recreational_min=0.25, # Untrained ~10cm
86
+ recreational_max=0.70, # Recreational ~30-50cm
87
+ elite_min=0.65, # Trained ~60cm
88
+ elite_max=1.10, # Elite >100cm
89
+ absolute_max=1.30,
90
+ unit="s",
91
+ )
92
+
93
+ # JUMP HEIGHT (meters)
94
+ JUMP_HEIGHT = MetricBounds(
95
+ absolute_min=0.02,
96
+ practical_min=0.05,
97
+ recreational_min=0.15, # Untrained with effort
98
+ recreational_max=0.60, # Good recreational
99
+ elite_min=0.65,
100
+ elite_max=1.00,
101
+ absolute_max=1.30,
102
+ unit="m",
103
+ )
104
+
105
+ # COUNTERMOVEMENT DEPTH (meters)
106
+ COUNTERMOVEMENT_DEPTH = MetricBounds(
107
+ absolute_min=0.05,
108
+ practical_min=0.08, # Minimal squat
109
+ recreational_min=0.20, # Shallow to parallel
110
+ recreational_max=0.55, # Normal to deep squat
111
+ elite_min=0.40,
112
+ elite_max=0.75,
113
+ absolute_max=1.10, # Only extreme tall athletes
114
+ unit="m",
115
+ )
116
+
117
+ # CONCENTRIC DURATION / CONTACT TIME (seconds)
118
+ CONCENTRIC_DURATION = MetricBounds(
119
+ absolute_min=0.08,
120
+ practical_min=0.10, # Extreme plyometric
121
+ recreational_min=0.40, # Moderate propulsion
122
+ recreational_max=0.90, # Slow push-off
123
+ elite_min=0.25,
124
+ elite_max=0.50,
125
+ absolute_max=1.80,
126
+ unit="s",
127
+ )
128
+
129
+ # ECCENTRIC DURATION (seconds)
130
+ ECCENTRIC_DURATION = MetricBounds(
131
+ absolute_min=0.15,
132
+ practical_min=0.25,
133
+ recreational_min=0.35,
134
+ recreational_max=0.75,
135
+ elite_min=0.30,
136
+ elite_max=0.65,
137
+ absolute_max=1.30,
138
+ unit="s",
139
+ )
140
+
141
+ # TOTAL MOVEMENT TIME (seconds)
142
+ TOTAL_MOVEMENT_TIME = MetricBounds(
143
+ absolute_min=0.25,
144
+ practical_min=0.35,
145
+ recreational_min=0.75,
146
+ recreational_max=1.50,
147
+ elite_min=0.55,
148
+ elite_max=1.10,
149
+ absolute_max=2.20,
150
+ unit="s",
151
+ )
152
+
153
+ # PEAK ECCENTRIC VELOCITY (m/s, downward)
154
+ PEAK_ECCENTRIC_VELOCITY = MetricBounds(
155
+ absolute_min=0.10,
156
+ practical_min=0.20,
157
+ recreational_min=0.80,
158
+ recreational_max=2.00,
159
+ elite_min=2.00,
160
+ elite_max=3.50,
161
+ absolute_max=4.50,
162
+ unit="m/s",
163
+ )
164
+
165
+ # PEAK CONCENTRIC VELOCITY (m/s, upward)
166
+ PEAK_CONCENTRIC_VELOCITY = MetricBounds(
167
+ absolute_min=0.30,
168
+ practical_min=0.50,
169
+ recreational_min=1.80,
170
+ recreational_max=2.80,
171
+ elite_min=3.00,
172
+ elite_max=4.20,
173
+ absolute_max=5.00,
174
+ unit="m/s",
175
+ )
176
+
177
+
178
+ class TripleExtensionBounds:
179
+ """Physiological bounds for triple extension angles (degrees)."""
180
+
181
+ # HIP ANGLE at takeoff (close to 180° = full extension)
182
+ @staticmethod
183
+ def hip_angle_valid(angle: float | None, profile: AthleteProfile) -> bool:
184
+ """Check if hip angle is valid for profile."""
185
+ if angle is None:
186
+ return True # May not be detectable
187
+ if angle < 120 or angle > 195:
188
+ return False # Outside physiological limits
189
+ if profile == AthleteProfile.ELDERLY:
190
+ return 150 <= angle <= 175
191
+ elif profile in (AthleteProfile.UNTRAINED, AthleteProfile.RECREATIONAL):
192
+ return 160 <= angle <= 180
193
+ elif profile in (AthleteProfile.TRAINED, AthleteProfile.ELITE):
194
+ return 170 <= angle <= 185
195
+ return True
196
+
197
+ # KNEE ANGLE at takeoff (close to 180° = full extension)
198
+ @staticmethod
199
+ def knee_angle_valid(angle: float | None, profile: AthleteProfile) -> bool:
200
+ """Check if knee angle is valid for profile."""
201
+ if angle is None:
202
+ return True
203
+ if angle < 130 or angle > 200:
204
+ return False # Outside physiological limits
205
+ if profile == AthleteProfile.ELDERLY:
206
+ return 155 <= angle <= 175
207
+ elif profile in (AthleteProfile.UNTRAINED, AthleteProfile.RECREATIONAL):
208
+ return 165 <= angle <= 182
209
+ elif profile in (AthleteProfile.TRAINED, AthleteProfile.ELITE):
210
+ return 173 <= angle <= 190
211
+ return True
212
+
213
+ # ANKLE ANGLE at takeoff (120-155° = plantarflexion, 90° = neutral)
214
+ @staticmethod
215
+ def ankle_angle_valid(angle: float | None, profile: AthleteProfile) -> bool:
216
+ """Check if ankle angle is valid for profile."""
217
+ if angle is None:
218
+ return True # Often not detectable in side view
219
+ if angle < 90 or angle > 165:
220
+ return False # Outside physiological limits
221
+ if profile == AthleteProfile.ELDERLY:
222
+ return 100 <= angle <= 125
223
+ elif profile in (AthleteProfile.UNTRAINED, AthleteProfile.RECREATIONAL):
224
+ return 110 <= angle <= 140
225
+ elif profile in (AthleteProfile.TRAINED, AthleteProfile.ELITE):
226
+ return 125 <= angle <= 155
227
+ return True
228
+
229
+ # TRUNK TILT (forward lean from vertical, degrees)
230
+ @staticmethod
231
+ def trunk_tilt_valid(angle: float | None, profile: AthleteProfile) -> bool:
232
+ """Check if trunk tilt is valid for profile."""
233
+ if angle is None:
234
+ return True
235
+ if angle < -15 or angle > 60:
236
+ return False # Outside reasonable range
237
+ # Most athletes show 10-30° forward lean during takeoff
238
+ return -10 <= angle <= 45
239
+
240
+
241
+ class RSIBounds:
242
+ """Reactive Strength Index bounds."""
243
+
244
+ # Calculated as: RSI = flight_time / contact_time
245
+ MIN_VALID = 0.30 # Below this: invalid metrics
246
+ MAX_VALID = 4.00 # Above this: invalid metrics
247
+
248
+ ELDERLY_RANGE = (0.15, 0.30)
249
+ UNTRAINED_RANGE = (0.30, 0.80)
250
+ RECREATIONAL_RANGE = (0.80, 1.50)
251
+ TRAINED_RANGE = (1.50, 2.40)
252
+ ELITE_RANGE = (2.20, 3.50)
253
+
254
+ @staticmethod
255
+ def get_rsi_range(profile: AthleteProfile) -> tuple[float, float]:
256
+ """Get expected RSI range for athlete profile."""
257
+ if profile == AthleteProfile.ELDERLY:
258
+ return RSIBounds.ELDERLY_RANGE
259
+ elif profile == AthleteProfile.UNTRAINED:
260
+ return RSIBounds.UNTRAINED_RANGE
261
+ elif profile == AthleteProfile.RECREATIONAL:
262
+ return RSIBounds.RECREATIONAL_RANGE
263
+ elif profile == AthleteProfile.TRAINED:
264
+ return RSIBounds.TRAINED_RANGE
265
+ elif profile == AthleteProfile.ELITE:
266
+ return RSIBounds.ELITE_RANGE
267
+ return (RSIBounds.MIN_VALID, RSIBounds.MAX_VALID)
268
+
269
+ @staticmethod
270
+ def is_valid(rsi: float) -> bool:
271
+ """Check if RSI is within physiological bounds."""
272
+ return RSIBounds.MIN_VALID <= rsi <= RSIBounds.MAX_VALID
273
+
274
+ @staticmethod
275
+ def in_range_for_profile(rsi: float, profile: AthleteProfile) -> bool:
276
+ """Check if RSI is in expected range for profile."""
277
+ min_rsi, max_rsi = RSIBounds.get_rsi_range(profile)
278
+ return min_rsi <= rsi <= max_rsi
279
+
280
+
281
+ class MetricConsistency:
282
+ """Cross-validation tolerance for metric consistency checks."""
283
+
284
+ # Jump height from flight time: h = g*t²/8
285
+ # Allow 10% deviation for measurement noise
286
+ HEIGHT_FLIGHT_TIME_TOLERANCE = 0.10
287
+
288
+ # Peak velocity from jump height: v = sqrt(2*g*h)
289
+ # Allow 15% deviation (velocity harder to detect precisely)
290
+ VELOCITY_HEIGHT_TOLERANCE = 0.15
291
+
292
+ # Countermovement depth to jump height ratio
293
+ # Typically 0.5-1.2, flag if outside 0.3-1.5
294
+ DEPTH_HEIGHT_RATIO_MIN = 0.30
295
+ DEPTH_HEIGHT_RATIO_MAX = 1.50
296
+
297
+ # Contact time to countermovement depth ratio
298
+ # Should be roughly 1.0-1.5 s/m
299
+ CONTACT_DEPTH_RATIO_MIN = 0.50
300
+ CONTACT_DEPTH_RATIO_MAX = 2.50
301
+
302
+
303
+ # Athlete profile examples with expected metric ranges
304
+ ATHLETE_PROFILES = {
305
+ "elderly_deconditioned": {
306
+ "label": "Elderly/Deconditioned (70+, sedentary)",
307
+ "profile": AthleteProfile.ELDERLY,
308
+ "expected": {
309
+ "jump_height_m": (0.10, 0.18),
310
+ "flight_time_s": (0.14, 0.19),
311
+ "countermovement_depth_m": (0.12, 0.20),
312
+ "concentric_duration_s": (0.80, 1.20),
313
+ "eccentric_duration_s": (0.60, 0.95),
314
+ "peak_eccentric_velocity_ms": (0.4, 0.7),
315
+ "peak_concentric_velocity_ms": (1.0, 1.5),
316
+ "rsi": (0.15, 0.25),
317
+ "hip_angle_deg": (150, 165),
318
+ "knee_angle_deg": (155, 170),
319
+ "ankle_angle_deg": (105, 125),
320
+ },
321
+ },
322
+ "recreational": {
323
+ "label": "Recreational Athlete (fitness participant, 30-45 yrs)",
324
+ "profile": AthleteProfile.RECREATIONAL,
325
+ "expected": {
326
+ "jump_height_m": (0.35, 0.55),
327
+ "flight_time_s": (0.53, 0.67),
328
+ "countermovement_depth_m": (0.28, 0.45),
329
+ "concentric_duration_s": (0.45, 0.65),
330
+ "eccentric_duration_s": (0.40, 0.65),
331
+ "peak_eccentric_velocity_ms": (1.3, 1.9),
332
+ "peak_concentric_velocity_ms": (2.6, 3.3),
333
+ "rsi": (0.85, 1.25),
334
+ "hip_angle_deg": (168, 178),
335
+ "knee_angle_deg": (170, 182),
336
+ "ankle_angle_deg": (120, 138),
337
+ },
338
+ },
339
+ "elite_male": {
340
+ "label": "Elite Male Athlete (college/pro volleyball/basketball)",
341
+ "profile": AthleteProfile.ELITE,
342
+ "expected": {
343
+ "jump_height_m": (0.68, 0.88),
344
+ "flight_time_s": (0.74, 0.84),
345
+ "countermovement_depth_m": (0.42, 0.62),
346
+ "concentric_duration_s": (0.28, 0.42),
347
+ "eccentric_duration_s": (0.35, 0.55),
348
+ "peak_eccentric_velocity_ms": (2.1, 3.2),
349
+ "peak_concentric_velocity_ms": (3.6, 4.2),
350
+ "rsi": (1.85, 2.80),
351
+ "hip_angle_deg": (173, 185),
352
+ "knee_angle_deg": (176, 188),
353
+ "ankle_angle_deg": (132, 148),
354
+ },
355
+ },
356
+ }
357
+
358
+
359
+ def estimate_athlete_profile(metrics_dict: dict) -> AthleteProfile:
360
+ """Estimate athlete profile from metrics.
361
+
362
+ Uses jump height as primary classifier:
363
+ - <0.20m: Elderly
364
+ - 0.20-0.35m: Untrained
365
+ - 0.35-0.65m: Recreational
366
+ - 0.65-0.85m: Trained
367
+ - >0.85m: Elite
368
+ """
369
+ jump_height = metrics_dict.get("jump_height", 0)
370
+
371
+ if jump_height < 0.20:
372
+ return AthleteProfile.ELDERLY
373
+ elif jump_height < 0.35:
374
+ return AthleteProfile.UNTRAINED
375
+ elif jump_height < 0.65:
376
+ return AthleteProfile.RECREATIONAL
377
+ elif jump_height < 0.85:
378
+ return AthleteProfile.TRAINED
379
+ else:
380
+ return AthleteProfile.ELITE
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kinemotion
3
- Version: 0.28.0
3
+ Version: 0.29.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
@@ -67,7 +67,7 @@ Description-Content-Type: text/markdown
67
67
 
68
68
  - **Ground contact detection** based on foot velocity and position
69
69
  - **Automatic drop jump detection** - identifies box → drop → landing → jump phases
70
- - **Metrics**: Ground contact time, flight time, jump height (with drop height calibration)
70
+ - **Metrics**: Ground contact time, flight time, jump height (calculated from flight time)
71
71
  - **Reactive strength index** calculations
72
72
 
73
73
  ### Counter Movement Jump (CMJ) Analysis
@@ -267,27 +267,33 @@ kinemotion cmj-analyze videos/*.mp4 --batch --workers 4 \
267
267
  --csv-summary summary.csv
268
268
  ```
269
269
 
270
- ### Quality Indicators & Confidence Scores
270
+ ### Quality Assessment
271
271
 
272
- All analysis outputs include automatic quality assessment to help you know when to trust results:
272
+ All analysis outputs include automatic quality assessment in the metadata section to help you know when to trust results:
273
273
 
274
274
  ```json
275
275
  {
276
- "jump_height_m": 0.352,
277
- "flight_time_s": 0.534,
278
- "confidence": "high",
279
- "quality_score": 87.3,
280
- "quality_indicators": {
281
- "avg_visibility": 0.89,
282
- "min_visibility": 0.82,
283
- "tracking_stable": true,
284
- "phase_detection_clear": true,
285
- "outliers_detected": 2,
286
- "outlier_percentage": 1.5,
287
- "position_variance": 0.0008,
288
- "fps": 60.0
276
+ "data": {
277
+ "jump_height_m": 0.352,
278
+ "flight_time_ms": 534.2
289
279
  },
290
- "warnings": []
280
+ "metadata": {
281
+ "quality": {
282
+ "confidence": "high",
283
+ "quality_score": 87.3,
284
+ "quality_indicators": {
285
+ "avg_visibility": 0.89,
286
+ "min_visibility": 0.82,
287
+ "tracking_stable": true,
288
+ "phase_detection_clear": true,
289
+ "outliers_detected": 2,
290
+ "outlier_percentage": 1.5,
291
+ "position_variance": 0.0008,
292
+ "fps": 60.0
293
+ },
294
+ "warnings": []
295
+ }
296
+ }
291
297
  }
292
298
  ```
293
299
 
@@ -310,9 +316,9 @@ All analysis outputs include automatic quality assessment to help you know when
310
316
  ```python
311
317
  # Only use high-confidence results
312
318
  metrics = process_cmj_video("video.mp4")
313
- if metrics.quality_assessment.confidence == "high":
319
+ if metrics.quality_assessment is not None and metrics.quality_assessment.confidence == "high":
314
320
  print(f"Reliable jump height: {metrics.jump_height:.3f}m")
315
- else:
321
+ elif metrics.quality_assessment is not None:
316
322
  print(f"Low quality - warnings: {metrics.quality_assessment.warnings}")
317
323
  ```
318
324
 
@@ -414,14 +420,9 @@ Kinemotion automatically optimizes parameters based on your video:
414
420
  - **Quality-based adjustments**: Adapts smoothing based on MediaPipe tracking confidence
415
421
  - **Always enabled**: Outlier rejection, curvature analysis, drop start detection
416
422
 
417
- ### Required Parameters
423
+ ### Parameters
418
424
 
419
- - `--drop-height <float>` **\[REQUIRED\]**
420
- - Height of drop box/platform in meters (e.g., 0.40 for 40cm)
421
- - Used for accurate calibration of jump height measurements
422
- - Measure your box height accurately for best results
423
-
424
- ### Optional Parameters
425
+ All parameters are optional. Kinemotion uses intelligent auto-tuning to select optimal settings based on video characteristics.
425
426
 
426
427
  - `--quality [fast|balanced|accurate]` (default: balanced)
427
428
 
@@ -460,32 +461,95 @@ For advanced users who need manual control:
460
461
 
461
462
  ## Output Format
462
463
 
463
- ### JSON Metrics
464
+ ### Drop Jump JSON Output
464
465
 
465
466
  ```json
466
467
  {
467
- "ground_contact_time_ms": 245.67,
468
- "flight_time_ms": 456.78,
469
- "jump_height_m": 0.339,
470
- "jump_height_kinematic_m": 0.256,
471
- "jump_height_trajectory_normalized": 0.0845,
472
- "contact_start_frame": 45,
473
- "contact_end_frame": 67,
474
- "flight_start_frame": 68,
475
- "flight_end_frame": 95,
476
- "peak_height_frame": 82
468
+ "data": {
469
+ "ground_contact_time_ms": 245.67,
470
+ "flight_time_ms": 456.78,
471
+ "jump_height_m": 0.339,
472
+ "jump_height_kinematic_m": 0.339,
473
+ "jump_height_trajectory_normalized": 0.0845,
474
+ "contact_start_frame": 45,
475
+ "contact_end_frame": 67,
476
+ "flight_start_frame": 68,
477
+ "flight_end_frame": 95,
478
+ "peak_height_frame": 82,
479
+ "contact_start_frame_precise": 45.234,
480
+ "contact_end_frame_precise": 67.891,
481
+ "flight_start_frame_precise": 68.123,
482
+ "flight_end_frame_precise": 94.567
483
+ },
484
+ "metadata": {
485
+ "quality": { },
486
+ "processing_info": { }
487
+ }
477
488
  }
478
489
  ```
479
490
 
480
- **Fields**:
491
+ **Data Fields**:
481
492
 
482
- - `jump_height_m`: Primary jump height measurement (calibrated if --drop-height provided, otherwise corrected kinematic)
483
- - `jump_height_kinematic_m`: Kinematic estimate from flight time: h = (g × t²) / 8
493
+ - `ground_contact_time_ms`: Duration of ground contact phase in milliseconds
494
+ - `flight_time_ms`: Duration of flight phase in milliseconds
495
+ - `jump_height_m`: Jump height calculated from flight time: h = g × t² / 8
496
+ - `jump_height_kinematic_m`: Kinematic estimate (same as `jump_height_m`)
484
497
  - `jump_height_trajectory_normalized`: Position-based measurement in normalized coordinates (0-1 range)
485
- - `contact_start_frame_precise`, `contact_end_frame_precise`: Sub-frame timing (fractional frames)
486
- - `flight_start_frame_precise`, `flight_end_frame_precise`: Sub-frame timing (fractional frames)
498
+ - `contact_start_frame`: Frame index where contact begins (integer, for visualization)
499
+ - `contact_end_frame`: Frame index where contact ends (integer, for visualization)
500
+ - `flight_start_frame`: Frame index where flight begins (integer, for visualization)
501
+ - `flight_end_frame`: Frame index where flight ends (integer, for visualization)
502
+ - `peak_height_frame`: Frame index at maximum jump height (integer, for visualization)
503
+ - `contact_start_frame_precise`: Sub-frame precise timing for contact start (fractional, for calculations)
504
+ - `contact_end_frame_precise`: Sub-frame precise timing for contact end (fractional, for calculations)
505
+ - `flight_start_frame_precise`: Sub-frame precise timing for flight start (fractional, for calculations)
506
+ - `flight_end_frame_precise`: Sub-frame precise timing for flight end (fractional, for calculations)
507
+
508
+ **Note**: Integer frame indices are provided for visualization in debug videos. Precise fractional frames are used for all timing calculations and provide sub-frame accuracy (±10ms at 30fps).
509
+
510
+ ### CMJ JSON Output
511
+
512
+ ```json
513
+ {
514
+ "data": {
515
+ "jump_height_m": 0.352,
516
+ "flight_time_ms": 534.2,
517
+ "countermovement_depth_m": 0.285,
518
+ "eccentric_duration_ms": 612.5,
519
+ "concentric_duration_ms": 321.8,
520
+ "total_movement_time_ms": 934.3,
521
+ "peak_eccentric_velocity_m_s": -2.145,
522
+ "peak_concentric_velocity_m_s": 3.789,
523
+ "transition_time_ms": 125.4,
524
+ "standing_start_frame": 12.5,
525
+ "lowest_point_frame": 45.2,
526
+ "takeoff_frame": 67.8,
527
+ "landing_frame": 102.3,
528
+ "tracking_method": "foot"
529
+ },
530
+ "metadata": {
531
+ "quality": { },
532
+ "processing_info": { }
533
+ }
534
+ }
535
+ ```
487
536
 
488
- **Note**: Integer frame indices (e.g., `contact_start_frame`) are provided for visualization in debug videos. Precise fractional frames (e.g., `contact_start_frame_precise`) are used for all timing calculations and provide higher accuracy.
537
+ **Data Fields**:
538
+
539
+ - `jump_height_m`: Jump height calculated from flight time: h = g × t² / 8
540
+ - `flight_time_ms`: Duration of flight phase in milliseconds
541
+ - `countermovement_depth_m`: Maximum downward displacement during eccentric (descent) phase
542
+ - `eccentric_duration_ms`: Time from start of countermovement to lowest point
543
+ - `concentric_duration_ms`: Time from lowest point to takeoff
544
+ - `total_movement_time_ms`: Total time from countermovement start to takeoff
545
+ - `peak_eccentric_velocity_m_s`: Maximum downward velocity during descent (negative value)
546
+ - `peak_concentric_velocity_m_s`: Maximum upward velocity during propulsion (positive value)
547
+ - `transition_time_ms`: Duration at lowest point (amortization phase between descent and propulsion)
548
+ - `standing_start_frame`: Frame where standing phase ends and countermovement begins
549
+ - `lowest_point_frame`: Frame at the lowest point of the countermovement
550
+ - `takeoff_frame`: Frame where athlete leaves ground
551
+ - `landing_frame`: Frame where athlete lands after jump
552
+ - `tracking_method`: Tracking method used - "foot" (foot landmarks) or "com" (center of mass estimation)
489
553
 
490
554
  ### Debug Video
491
555
 
@@ -542,8 +606,7 @@ The debug video includes:
542
606
 
543
607
  **Solutions**:
544
608
 
545
- 1. **Use calibration**: For drop jumps, add `--drop-height` parameter with box height in meters (e.g., `--drop-height 0.40`)
546
- - Theoretically improves accuracy (⚠️ unvalidated)
609
+ 1. **Check video quality**: Ensure video frame rate is adequate (30fps or higher recommended)
547
610
  1. **Verify flight time detection**: Check `flight_start_frame` and `flight_end_frame` in JSON
548
611
  1. **Compare measurements**: JSON output includes both `jump_height_m` (primary) and `jump_height_kinematic_m` (kinematic-only)
549
612
  1. **Check for drop jump detection**: If doing a drop jump, ensure first phase is elevated enough (>5% of frame height)
@@ -581,8 +644,7 @@ The debug video includes:
581
644
  1. **Metric Calculation**:
582
645
  - Ground contact time = contact phase duration (using fractional frames)
583
646
  - Flight time = flight phase duration (using fractional frames)
584
- - Jump height = calibrated position-based measurement (if --drop-height provided)
585
- - Fallback: kinematic estimate (g × t²) / 8 with optional empirical correction factor (⚠️ unvalidated)
647
+ - Jump height = kinematic estimate from flight time: (g × ) / 8
586
648
 
587
649
  ## Development
588
650
 
@@ -593,7 +655,7 @@ This project enforces strict code quality standards:
593
655
  - **Type safety**: Full pyright strict mode compliance with complete type annotations
594
656
  - **Linting**: Comprehensive ruff checks (pycodestyle, pyflakes, isort, pep8-naming, etc.)
595
657
  - **Formatting**: Black code style
596
- - **Testing**: pytest with 61 unit tests
658
+ - **Testing**: pytest with 261 comprehensive tests (74.27% coverage)
597
659
  - **PEP 561 compliant**: Includes py.typed marker for type checking support
598
660
 
599
661
  ### Development Commands
@@ -1,15 +1,17 @@
1
- kinemotion/__init__.py,sha256=vAEIg-oDX1ZkQMnWgXd__tekaA5KUcEvdJSAGWS8VUY,722
1
+ kinemotion/__init__.py,sha256=sxdDOekOrIgjxm842gy-6zfq7OWmGl9ShJtXCm4JI7c,723
2
2
  kinemotion/api.py,sha256=tbkjXsfe0N0Bmik6XRIOYM7Nom4QqeQJSpDx7IoiuSA,38177
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=4HYGn4VDIB6oExAees-VcPfpNgWOltpgwjyNTU7YAb4,18263
5
+ kinemotion/cmj/analysis.py,sha256=il7-sfM89ZetxLhmw9boViaP4E8Y3mlS_XI-B5txmMs,19795
6
6
  kinemotion/cmj/cli.py,sha256=12FEfWrseG4kCUbgHHdBPkWp6zzVQ0VAzfgNJotArmM,10792
7
7
  kinemotion/cmj/debug_overlay.py,sha256=D-y2FQKI01KY0WXFKTKg6p9Qj3AkXCE7xjau3Ais080,15886
8
- kinemotion/cmj/joint_angles.py,sha256=8ucpDGPvbt4iX3tx9eVxJEUv0laTm2Y58_--VzJCogE,9113
8
+ kinemotion/cmj/joint_angles.py,sha256=HmheIEiKcQz39cRezk4h-htorOhGNPsqKIR9RsAEKts,9960
9
9
  kinemotion/cmj/kinematics.py,sha256=-iBFg2AkQR4LaThCQzO09fx6qJed27ZfMDQJgE7Si4k,9772
10
10
  kinemotion/core/__init__.py,sha256=HsqolRa60cW3vrG8F9Lvr9WvWcs5hCmsTzSgo7imi-4,1278
11
11
  kinemotion/core/auto_tuning.py,sha256=j6cul_qC6k0XyryCG93C1AWH2MKPj3UBMzuX02xaqfI,11235
12
12
  kinemotion/core/cli_utils.py,sha256=Pq1JF7yvK1YbH0tOUWKjplthCbWsJQt4Lv7esPYH4FM,7254
13
+ kinemotion/core/cmj_metrics_validator.py,sha256=Jfh8oxhxz5BCBIPdeMRHa60tZsliDkN10RiLQYkmck4,27262
14
+ kinemotion/core/cmj_validation_bounds.py,sha256=WBMuJx6ewb-rYan3xmQu32m7bs9h8J5isa4LduZuZkI,13507
13
15
  kinemotion/core/debug_overlay_utils.py,sha256=TyUb5okv5qw8oeaX3jsUO_kpwf1NnaHEAOTm-8LwTno,4587
14
16
  kinemotion/core/filtering.py,sha256=f-m-aA59e4WqE6u-9MA51wssu7rI-Y_7n1cG8IWdeRQ,11241
15
17
  kinemotion/core/formatting.py,sha256=G_3eqgOtym9RFOZVEwCxye4A2cyrmgvtQ214vIshowU,2480
@@ -24,8 +26,8 @@ kinemotion/dropjump/cli.py,sha256=ZyroaYPwz8TgfL39Wcaj6m68Awl6lYXC75ttaflU-c0,16
24
26
  kinemotion/dropjump/debug_overlay.py,sha256=LkPw6ucb7beoYWS4L-Lvjs1KLCm5wAWDAfiznUeV2IQ,5668
25
27
  kinemotion/dropjump/kinematics.py,sha256=Yr3G7AQwtYy1dxyeOAYfqqgd4pzoZwWQAhZzxI5RbnE,16658
26
28
  kinemotion/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
- kinemotion-0.28.0.dist-info/METADATA,sha256=RIUN7r__qFVHSNzj2CglERzONmcmLiIYrDZLkpztZu8,23244
28
- kinemotion-0.28.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
29
- kinemotion-0.28.0.dist-info/entry_points.txt,sha256=zaqnAnjLvcdrk1Qvj5nvXZCZ2gp0prS7it1zTJygcIY,50
30
- kinemotion-0.28.0.dist-info/licenses/LICENSE,sha256=KZajvqsHw0NoOHOi2q0FZ4NBe9HdV6oey-IPYAtHXfg,1088
31
- kinemotion-0.28.0.dist-info/RECORD,,
29
+ kinemotion-0.29.1.dist-info/METADATA,sha256=fOQmDQ0rv47YYfH_wXRU6wUDk2gDnSfS__nHTOlTKXU,25810
30
+ kinemotion-0.29.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
31
+ kinemotion-0.29.1.dist-info/entry_points.txt,sha256=zaqnAnjLvcdrk1Qvj5nvXZCZ2gp0prS7it1zTJygcIY,50
32
+ kinemotion-0.29.1.dist-info/licenses/LICENSE,sha256=KZajvqsHw0NoOHOi2q0FZ4NBe9HdV6oey-IPYAtHXfg,1088
33
+ kinemotion-0.29.1.dist-info/RECORD,,