kinemotion 0.10.6__py3-none-any.whl → 0.67.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of kinemotion might be problematic. Click here for more details.

Files changed (48) hide show
  1. kinemotion/__init__.py +31 -6
  2. kinemotion/api.py +39 -598
  3. kinemotion/cli.py +2 -0
  4. kinemotion/cmj/__init__.py +5 -0
  5. kinemotion/cmj/analysis.py +621 -0
  6. kinemotion/cmj/api.py +563 -0
  7. kinemotion/cmj/cli.py +324 -0
  8. kinemotion/cmj/debug_overlay.py +457 -0
  9. kinemotion/cmj/joint_angles.py +307 -0
  10. kinemotion/cmj/kinematics.py +360 -0
  11. kinemotion/cmj/metrics_validator.py +767 -0
  12. kinemotion/cmj/validation_bounds.py +341 -0
  13. kinemotion/core/__init__.py +28 -0
  14. kinemotion/core/auto_tuning.py +71 -37
  15. kinemotion/core/cli_utils.py +60 -0
  16. kinemotion/core/debug_overlay_utils.py +385 -0
  17. kinemotion/core/determinism.py +83 -0
  18. kinemotion/core/experimental.py +103 -0
  19. kinemotion/core/filtering.py +9 -6
  20. kinemotion/core/formatting.py +75 -0
  21. kinemotion/core/metadata.py +231 -0
  22. kinemotion/core/model_downloader.py +172 -0
  23. kinemotion/core/pipeline_utils.py +433 -0
  24. kinemotion/core/pose.py +298 -141
  25. kinemotion/core/pose_landmarks.py +67 -0
  26. kinemotion/core/quality.py +393 -0
  27. kinemotion/core/smoothing.py +250 -154
  28. kinemotion/core/timing.py +247 -0
  29. kinemotion/core/types.py +42 -0
  30. kinemotion/core/validation.py +201 -0
  31. kinemotion/core/video_io.py +135 -50
  32. kinemotion/dropjump/__init__.py +1 -1
  33. kinemotion/dropjump/analysis.py +367 -182
  34. kinemotion/dropjump/api.py +665 -0
  35. kinemotion/dropjump/cli.py +156 -466
  36. kinemotion/dropjump/debug_overlay.py +136 -206
  37. kinemotion/dropjump/kinematics.py +232 -255
  38. kinemotion/dropjump/metrics_validator.py +240 -0
  39. kinemotion/dropjump/validation_bounds.py +157 -0
  40. kinemotion/models/__init__.py +0 -0
  41. kinemotion/models/pose_landmarker_lite.task +0 -0
  42. kinemotion-0.67.0.dist-info/METADATA +726 -0
  43. kinemotion-0.67.0.dist-info/RECORD +47 -0
  44. {kinemotion-0.10.6.dist-info → kinemotion-0.67.0.dist-info}/WHEEL +1 -1
  45. kinemotion-0.10.6.dist-info/METADATA +0 -561
  46. kinemotion-0.10.6.dist-info/RECORD +0 -20
  47. {kinemotion-0.10.6.dist-info → kinemotion-0.67.0.dist-info}/entry_points.txt +0 -0
  48. {kinemotion-0.10.6.dist-info → kinemotion-0.67.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,240 @@
1
+ """Drop jump metrics validation using physiological bounds.
2
+
3
+ Comprehensive validation of Drop Jump metrics against biomechanical bounds,
4
+ consistency checks, and cross-validation of RSI calculation.
5
+
6
+ Provides severity levels (ERROR, WARNING, INFO) for different categories
7
+ of metric issues.
8
+ """
9
+
10
+ from dataclasses import dataclass
11
+
12
+ from kinemotion.core.types import MetricsDict
13
+ from kinemotion.core.validation import (
14
+ MetricsValidator,
15
+ ValidationResult,
16
+ )
17
+ from kinemotion.dropjump.validation_bounds import (
18
+ DropJumpBounds,
19
+ estimate_athlete_profile,
20
+ )
21
+
22
+
23
+ @dataclass
24
+ class DropJumpValidationResult(ValidationResult):
25
+ """Drop jump-specific validation result."""
26
+
27
+ rsi: float | None = None
28
+ contact_flight_ratio: float | None = None
29
+ height_kinematic_trajectory_consistency: float | None = None # % error
30
+
31
+ def to_dict(self) -> dict:
32
+ """Convert validation result to JSON-serializable dictionary.
33
+
34
+ Returns:
35
+ Dictionary with status, issues, and consistency metrics.
36
+ """
37
+ return {
38
+ "status": self.status,
39
+ "issues": [
40
+ {
41
+ "severity": issue.severity.value,
42
+ "metric": issue.metric,
43
+ "message": issue.message,
44
+ "value": issue.value,
45
+ "bounds": issue.bounds,
46
+ }
47
+ for issue in self.issues
48
+ ],
49
+ "athlete_profile": (self.athlete_profile.value if self.athlete_profile else None),
50
+ "rsi": self.rsi,
51
+ "contact_flight_ratio": self.contact_flight_ratio,
52
+ "height_kinematic_trajectory_consistency_percent": (
53
+ self.height_kinematic_trajectory_consistency
54
+ ),
55
+ }
56
+
57
+
58
+ class DropJumpMetricsValidator(MetricsValidator):
59
+ """Comprehensive drop jump metrics validator."""
60
+
61
+ def validate(self, metrics: MetricsDict) -> DropJumpValidationResult:
62
+ """Validate drop jump metrics comprehensively.
63
+
64
+ Args:
65
+ metrics: Dictionary with drop jump metric values
66
+
67
+ Returns:
68
+ DropJumpValidationResult with all issues and status
69
+ """
70
+ result = DropJumpValidationResult()
71
+
72
+ # Estimate athlete profile if not provided
73
+ if self.assumed_profile:
74
+ result.athlete_profile = self.assumed_profile
75
+ else:
76
+ result.athlete_profile = estimate_athlete_profile(metrics)
77
+
78
+ # Extract metric values (handle nested "data" structure)
79
+ data = metrics.get("data", metrics) # Support both structures
80
+
81
+ contact_time_ms = data.get("ground_contact_time_ms")
82
+ flight_time_ms = data.get("flight_time_ms")
83
+ jump_height_m = data.get("jump_height_m")
84
+ jump_height_kinematic_m = data.get("jump_height_kinematic_m")
85
+ jump_height_trajectory_m = data.get("jump_height_trajectory_m")
86
+
87
+ # Validate individual metrics
88
+ if contact_time_ms is not None:
89
+ self._check_contact_time(contact_time_ms, result)
90
+
91
+ if flight_time_ms is not None:
92
+ self._check_flight_time(flight_time_ms, result)
93
+
94
+ if jump_height_m is not None:
95
+ self._check_jump_height(jump_height_m, result)
96
+
97
+ # Cross-validation
98
+ if contact_time_ms is not None and flight_time_ms is not None:
99
+ self._check_rsi(contact_time_ms, flight_time_ms, result)
100
+
101
+ # Dual height validation (kinematic vs trajectory)
102
+ if jump_height_kinematic_m is not None and jump_height_trajectory_m is not None:
103
+ self._check_dual_height_consistency(
104
+ jump_height_kinematic_m, jump_height_trajectory_m, result
105
+ )
106
+
107
+ # Finalize status
108
+ result.finalize_status()
109
+
110
+ return result
111
+
112
+ def _check_contact_time(
113
+ self, contact_time_ms: float, result: DropJumpValidationResult
114
+ ) -> None:
115
+ """Validate contact time."""
116
+ contact_time_s = contact_time_ms / 1000.0
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
+ )
135
+
136
+ def _check_flight_time(self, flight_time_ms: float, result: DropJumpValidationResult) -> None:
137
+ """Validate flight time."""
138
+ flight_time_s = flight_time_ms / 1000.0
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
+ )
155
+
156
+ def _check_jump_height(self, jump_height_m: float, result: DropJumpValidationResult) -> None:
157
+ """Validate jump height."""
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
+ )
174
+
175
+ def _check_rsi(
176
+ self,
177
+ contact_time_ms: float,
178
+ flight_time_ms: float,
179
+ result: DropJumpValidationResult,
180
+ ) -> None:
181
+ """Validate RSI and cross-check consistency."""
182
+ contact_time_s = contact_time_ms / 1000.0
183
+ flight_time_s = flight_time_ms / 1000.0
184
+
185
+ if contact_time_s > 0 and flight_time_s > 0:
186
+ rsi = flight_time_s / contact_time_s
187
+ result.rsi = rsi
188
+ result.contact_flight_ratio = contact_time_s / flight_time_s
189
+
190
+ bounds = DropJumpBounds.RSI
191
+
192
+ if not bounds.is_physically_possible(rsi):
193
+ result.add_error(
194
+ "rsi",
195
+ f"RSI {rsi:.2f} physically impossible",
196
+ value=rsi,
197
+ bounds=(bounds.absolute_min, bounds.absolute_max),
198
+ )
199
+ elif result.athlete_profile and not bounds.contains(rsi, result.athlete_profile):
200
+ result.add_warning(
201
+ "rsi",
202
+ f"RSI {rsi:.2f} unusual for {result.athlete_profile.value} athlete",
203
+ value=rsi,
204
+ )
205
+
206
+ def _check_dual_height_consistency(
207
+ self,
208
+ jump_height_kinematic_m: float,
209
+ jump_height_trajectory_m: float,
210
+ result: DropJumpValidationResult,
211
+ ) -> None:
212
+ """Validate consistency between kinematic and trajectory-based heights.
213
+
214
+ Kinematic height (h = g*t²/8) comes from flight time (objective).
215
+ Trajectory height comes from position tracking (subject to landmark
216
+ detection noise).
217
+
218
+ Expected correlation: r > 0.95, absolute difference < 5% for quality video.
219
+ """
220
+ if jump_height_kinematic_m <= 0 or jump_height_trajectory_m <= 0:
221
+ return # Skip if either value is missing or invalid
222
+
223
+ # Calculate percentage difference
224
+ avg_height = (jump_height_kinematic_m + jump_height_trajectory_m) / 2.0
225
+ if avg_height > 0:
226
+ abs_diff = abs(jump_height_kinematic_m - jump_height_trajectory_m)
227
+ percent_error = (abs_diff / avg_height) * 100.0
228
+ result.height_kinematic_trajectory_consistency = percent_error
229
+
230
+ # Allow 10% tolerance for typical video processing noise
231
+ if percent_error > 10.0:
232
+ result.add_warning(
233
+ "height_consistency",
234
+ f"Kinematic ({jump_height_kinematic_m:.3f}m) and trajectory "
235
+ f"({jump_height_trajectory_m:.3f}m) heights differ by "
236
+ f"{percent_error:.1f}%. May indicate landmark detection "
237
+ "issues or video quality problems.",
238
+ value=percent_error,
239
+ bounds=(0, 10),
240
+ )
@@ -0,0 +1,157 @@
1
+ """Drop jump metrics physiological bounds for validation testing.
2
+
3
+ This module defines realistic physiological bounds for Drop Jump metrics
4
+ based on biomechanical literature and real-world athlete performance.
5
+
6
+ Drop jump metrics differ from CMJ:
7
+ - Contact time (ground interaction during landing)
8
+ - Flight time (time in air after landing)
9
+ - RSI (Reactive Strength Index) = flight_time / contact_time
10
+ - Jump height (calculated from flight time)
11
+
12
+ References:
13
+ - Komi & Bosco (1978): Drop jump RSI and elastic properties
14
+ - Flanagan & Comyns (2008): RSI reliability and athlete assessment
15
+ - Covens et al. (2019): Drop jump kinetics across athletes
16
+ """
17
+
18
+ from kinemotion.core.types import MetricsDict
19
+ from kinemotion.core.validation import AthleteProfile, MetricBounds
20
+
21
+
22
+ class DropJumpBounds:
23
+ """Collection of physiological bounds for all drop jump metrics."""
24
+
25
+ # GROUND CONTACT TIME (seconds, landing interaction)
26
+ CONTACT_TIME = MetricBounds(
27
+ absolute_min=0.08, # Physiological minimum: neural delay + deceleration
28
+ practical_min=0.15, # Extreme plyometric
29
+ recreational_min=0.35, # Typical landing
30
+ recreational_max=0.70, # Slower absorption
31
+ elite_min=0.20,
32
+ elite_max=0.50,
33
+ absolute_max=1.50,
34
+ unit="s",
35
+ )
36
+
37
+ # FLIGHT TIME (seconds, after landing)
38
+ FLIGHT_TIME = MetricBounds(
39
+ absolute_min=0.30,
40
+ practical_min=0.40, # Minimal jump
41
+ recreational_min=0.50,
42
+ recreational_max=0.85,
43
+ elite_min=0.65,
44
+ elite_max=1.10,
45
+ absolute_max=1.40,
46
+ unit="s",
47
+ )
48
+
49
+ # JUMP HEIGHT (meters, calculated from flight time)
50
+ JUMP_HEIGHT = MetricBounds(
51
+ absolute_min=0.05,
52
+ practical_min=0.10,
53
+ recreational_min=0.25,
54
+ recreational_max=0.65,
55
+ elite_min=0.50,
56
+ elite_max=1.00,
57
+ absolute_max=1.30,
58
+ unit="m",
59
+ )
60
+
61
+ # REACTIVE STRENGTH INDEX (RSI) = flight_time / contact_time (ratio, no unit)
62
+ RSI = MetricBounds(
63
+ absolute_min=0.30, # Very poor reactive ability
64
+ practical_min=0.50,
65
+ recreational_min=0.70,
66
+ recreational_max=1.80,
67
+ elite_min=1.50, # Elite: fast contact, long flight
68
+ elite_max=3.50,
69
+ absolute_max=5.00,
70
+ unit="ratio",
71
+ )
72
+
73
+
74
+ def _score_jump_height(jump_height: float) -> float:
75
+ """Convert jump height to athlete profile score (0-4).
76
+
77
+ Args:
78
+ jump_height: Jump height in meters
79
+
80
+ Returns:
81
+ Score from 0 (elderly) to 4 (elite)
82
+ """
83
+ thresholds = [(0.25, 0), (0.35, 1), (0.50, 2), (0.70, 3)]
84
+ for threshold, score in thresholds:
85
+ if jump_height < threshold:
86
+ return float(score)
87
+ return 4.0 # Elite
88
+
89
+
90
+ def _score_contact_time(contact_time_s: float) -> float:
91
+ """Convert contact time to athlete profile score (0-4).
92
+
93
+ Args:
94
+ contact_time_s: Ground contact time in seconds
95
+
96
+ Returns:
97
+ Score from 0 (elderly) to 4 (elite)
98
+ """
99
+ thresholds = [(0.60, 0), (0.50, 1), (0.45, 2), (0.40, 3)]
100
+ for threshold, score in thresholds:
101
+ if contact_time_s > threshold:
102
+ return float(score)
103
+ return 4.0 # Elite
104
+
105
+
106
+ def _classify_combined_score(combined_score: float) -> AthleteProfile:
107
+ """Classify combined score into athlete profile.
108
+
109
+ Args:
110
+ combined_score: Weighted score from height and contact time
111
+
112
+ Returns:
113
+ Athlete profile classification
114
+ """
115
+ thresholds = [
116
+ (1.0, AthleteProfile.ELDERLY),
117
+ (1.7, AthleteProfile.UNTRAINED),
118
+ (2.7, AthleteProfile.RECREATIONAL),
119
+ (3.7, AthleteProfile.TRAINED),
120
+ ]
121
+ for threshold, profile in thresholds:
122
+ if combined_score < threshold:
123
+ return profile
124
+ return AthleteProfile.ELITE
125
+
126
+
127
+ def estimate_athlete_profile(metrics: MetricsDict, gender: str | None = None) -> AthleteProfile:
128
+ """Estimate athlete profile from drop jump metrics.
129
+
130
+ Uses jump_height and contact_time to classify athlete level.
131
+
132
+ NOTE: Bounds are calibrated for adult males. Female athletes typically achieve
133
+ 60-70% of male heights due to lower muscle mass and strength. If analyzing
134
+ female athletes, interpret results one level lower than classification suggests.
135
+
136
+ Args:
137
+ metrics: Dictionary with drop jump metric values
138
+ gender: Optional gender for context ("M"/"F"). Currently informational only.
139
+
140
+ Returns:
141
+ Estimated AthleteProfile (ELDERLY, UNTRAINED, RECREATIONAL, TRAINED, or ELITE)
142
+ """
143
+ jump_height = metrics.get("data", {}).get("jump_height_m")
144
+ contact_time = metrics.get("data", {}).get("ground_contact_time_ms")
145
+
146
+ if jump_height is None or contact_time is None:
147
+ return AthleteProfile.RECREATIONAL
148
+
149
+ contact_time_s = contact_time / 1000.0
150
+
151
+ # Calculate weighted combination: height (70%) + contact time (30%)
152
+ # Height is more reliable indicator across populations
153
+ height_score = _score_jump_height(jump_height)
154
+ contact_score = _score_contact_time(contact_time_s)
155
+ combined_score = (height_score * 0.70) + (contact_score * 0.30)
156
+
157
+ return _classify_combined_score(combined_score)
File without changes