kinemotion 0.29.2__py3-none-any.whl → 0.30.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 +22 -0
- kinemotion/cmj/kinematics.py +12 -2
- kinemotion/core/cmj_metrics_validator.py +94 -0
- kinemotion/core/cmj_validation_bounds.py +15 -1
- kinemotion/core/dropjump_metrics_validator.py +343 -0
- kinemotion/core/dropjump_validation_bounds.py +196 -0
- kinemotion/dropjump/kinematics.py +12 -2
- {kinemotion-0.29.2.dist-info → kinemotion-0.30.0.dist-info}/METADATA +3 -2
- {kinemotion-0.29.2.dist-info → kinemotion-0.30.0.dist-info}/RECORD +12 -10
- {kinemotion-0.29.2.dist-info → kinemotion-0.30.0.dist-info}/WHEEL +0 -0
- {kinemotion-0.29.2.dist-info → kinemotion-0.30.0.dist-info}/entry_points.txt +0 -0
- {kinemotion-0.29.2.dist-info → kinemotion-0.30.0.dist-info}/licenses/LICENSE +0 -0
kinemotion/api.py
CHANGED
|
@@ -18,6 +18,8 @@ from .core.auto_tuning import (
|
|
|
18
18
|
analyze_video_sample,
|
|
19
19
|
auto_tune_parameters,
|
|
20
20
|
)
|
|
21
|
+
from .core.cmj_metrics_validator import CMJMetricsValidator
|
|
22
|
+
from .core.dropjump_metrics_validator import DropJumpMetricsValidator
|
|
21
23
|
from .core.filtering import reject_outliers
|
|
22
24
|
from .core.metadata import (
|
|
23
25
|
AlgorithmConfig,
|
|
@@ -602,6 +604,16 @@ def process_dropjump_video(
|
|
|
602
604
|
if verbose:
|
|
603
605
|
print("Analysis complete!")
|
|
604
606
|
|
|
607
|
+
# Validate metrics against physiological bounds
|
|
608
|
+
validator = DropJumpMetricsValidator()
|
|
609
|
+
validation_result = validator.validate(metrics.to_dict()) # type: ignore[arg-type]
|
|
610
|
+
metrics.validation_result = validation_result
|
|
611
|
+
|
|
612
|
+
if verbose and validation_result.issues:
|
|
613
|
+
print("\n⚠️ Validation Results:")
|
|
614
|
+
for issue in validation_result.issues:
|
|
615
|
+
print(f" [{issue.severity.value}] {issue.metric}: {issue.message}")
|
|
616
|
+
|
|
605
617
|
return metrics
|
|
606
618
|
|
|
607
619
|
|
|
@@ -1038,6 +1050,16 @@ def process_cmj_video(
|
|
|
1038
1050
|
print(f"Flight time: {metrics.flight_time*1000:.1f}ms")
|
|
1039
1051
|
print(f"Countermovement depth: {metrics.countermovement_depth:.3f}m")
|
|
1040
1052
|
|
|
1053
|
+
# Validate metrics against physiological bounds
|
|
1054
|
+
validator = CMJMetricsValidator()
|
|
1055
|
+
validation_result = validator.validate(metrics.to_dict()["data"]) # type: ignore[arg-type]
|
|
1056
|
+
metrics.validation_result = validation_result
|
|
1057
|
+
|
|
1058
|
+
if verbose and validation_result.issues:
|
|
1059
|
+
print("\n⚠️ Validation Results:")
|
|
1060
|
+
for issue in validation_result.issues:
|
|
1061
|
+
print(f" [{issue.severity.value}] {issue.metric}: {issue.message}")
|
|
1062
|
+
|
|
1041
1063
|
return metrics
|
|
1042
1064
|
|
|
1043
1065
|
|
kinemotion/cmj/kinematics.py
CHANGED
|
@@ -9,6 +9,7 @@ from numpy.typing import NDArray
|
|
|
9
9
|
from ..core.formatting import format_float_metric
|
|
10
10
|
|
|
11
11
|
if TYPE_CHECKING:
|
|
12
|
+
from ..core.cmj_metrics_validator import ValidationResult
|
|
12
13
|
from ..core.metadata import ResultMetadata
|
|
13
14
|
from ..core.quality import QualityAssessment
|
|
14
15
|
|
|
@@ -32,11 +33,12 @@ class CMJDataDict(TypedDict, total=False):
|
|
|
32
33
|
tracking_method: str
|
|
33
34
|
|
|
34
35
|
|
|
35
|
-
class CMJResultDict(TypedDict):
|
|
36
|
+
class CMJResultDict(TypedDict, total=False):
|
|
36
37
|
"""Type-safe dictionary for complete CMJ result with data and metadata."""
|
|
37
38
|
|
|
38
39
|
data: CMJDataDict
|
|
39
40
|
metadata: dict # ResultMetadata.to_dict()
|
|
41
|
+
validation: dict # ValidationResult.to_dict()
|
|
40
42
|
|
|
41
43
|
|
|
42
44
|
@dataclass
|
|
@@ -60,6 +62,7 @@ class CMJMetrics:
|
|
|
60
62
|
video_fps: Frames per second of the analyzed video
|
|
61
63
|
tracking_method: Method used for tracking ("foot" or "com")
|
|
62
64
|
quality_assessment: Optional quality assessment with confidence and warnings
|
|
65
|
+
validation_result: Optional validation result with physiological bounds checks
|
|
63
66
|
"""
|
|
64
67
|
|
|
65
68
|
jump_height: float
|
|
@@ -79,6 +82,7 @@ class CMJMetrics:
|
|
|
79
82
|
tracking_method: str
|
|
80
83
|
quality_assessment: "QualityAssessment | None" = None
|
|
81
84
|
result_metadata: "ResultMetadata | None" = None
|
|
85
|
+
validation_result: "ValidationResult | None" = None
|
|
82
86
|
|
|
83
87
|
def to_dict(self) -> CMJResultDict:
|
|
84
88
|
"""Convert metrics to JSON-serializable dictionary with data/metadata structure.
|
|
@@ -129,7 +133,13 @@ class CMJMetrics:
|
|
|
129
133
|
# No metadata available
|
|
130
134
|
metadata = {}
|
|
131
135
|
|
|
132
|
-
|
|
136
|
+
result: CMJResultDict = {"data": data, "metadata": metadata}
|
|
137
|
+
|
|
138
|
+
# Include validation results if available
|
|
139
|
+
if self.validation_result is not None:
|
|
140
|
+
result["validation"] = self.validation_result.to_dict()
|
|
141
|
+
|
|
142
|
+
return result
|
|
133
143
|
|
|
134
144
|
|
|
135
145
|
def calculate_cmj_metrics(
|
|
@@ -121,6 +121,36 @@ class ValidationResult:
|
|
|
121
121
|
else:
|
|
122
122
|
self.status = "PASS"
|
|
123
123
|
|
|
124
|
+
def to_dict(self) -> dict:
|
|
125
|
+
"""Convert validation result to JSON-serializable dictionary.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Dictionary with status, issues, and consistency metrics.
|
|
129
|
+
"""
|
|
130
|
+
return {
|
|
131
|
+
"status": self.status,
|
|
132
|
+
"issues": [
|
|
133
|
+
{
|
|
134
|
+
"severity": issue.severity.value,
|
|
135
|
+
"metric": issue.metric,
|
|
136
|
+
"message": issue.message,
|
|
137
|
+
"value": issue.value,
|
|
138
|
+
"bounds": issue.bounds,
|
|
139
|
+
}
|
|
140
|
+
for issue in self.issues
|
|
141
|
+
],
|
|
142
|
+
"athlete_profile": (
|
|
143
|
+
self.athlete_profile.value if self.athlete_profile else None
|
|
144
|
+
),
|
|
145
|
+
"rsi": self.rsi,
|
|
146
|
+
"height_flight_time_consistency_percent": (
|
|
147
|
+
self.height_flight_time_consistency
|
|
148
|
+
),
|
|
149
|
+
"velocity_height_consistency_percent": self.velocity_height_consistency,
|
|
150
|
+
"depth_height_ratio": self.depth_height_ratio,
|
|
151
|
+
"contact_depth_ratio": self.contact_depth_ratio,
|
|
152
|
+
}
|
|
153
|
+
|
|
124
154
|
|
|
125
155
|
class CMJMetricsValidator:
|
|
126
156
|
"""Comprehensive CMJ metrics validator."""
|
|
@@ -697,6 +727,70 @@ class CMJMetricsValidator:
|
|
|
697
727
|
value=ankle,
|
|
698
728
|
)
|
|
699
729
|
|
|
730
|
+
# Detect joint compensation patterns
|
|
731
|
+
self._check_joint_compensation_pattern(angles, result, profile)
|
|
732
|
+
|
|
733
|
+
def _check_joint_compensation_pattern(
|
|
734
|
+
self, angles: dict, result: ValidationResult, profile: AthleteProfile
|
|
735
|
+
) -> None:
|
|
736
|
+
"""Detect compensatory joint patterns in triple extension.
|
|
737
|
+
|
|
738
|
+
When one joint cannot achieve full extension, others may compensate.
|
|
739
|
+
Example: Limited hip extension (160°) with excessive knee extension (185°+)
|
|
740
|
+
suggests compensation rather than balanced movement quality.
|
|
741
|
+
|
|
742
|
+
This is a biomechanical quality indicator, not an error.
|
|
743
|
+
"""
|
|
744
|
+
hip = angles.get("hip_angle")
|
|
745
|
+
knee = angles.get("knee_angle")
|
|
746
|
+
ankle = angles.get("ankle_angle")
|
|
747
|
+
|
|
748
|
+
if hip is None or knee is None or ankle is None:
|
|
749
|
+
return # Need all three to detect patterns
|
|
750
|
+
|
|
751
|
+
# Get profile-specific bounds
|
|
752
|
+
if profile == AthleteProfile.ELDERLY:
|
|
753
|
+
hip_min, hip_max = 150, 175
|
|
754
|
+
knee_min, knee_max = 155, 175
|
|
755
|
+
ankle_min, ankle_max = 100, 125
|
|
756
|
+
elif profile in (AthleteProfile.UNTRAINED, AthleteProfile.RECREATIONAL):
|
|
757
|
+
hip_min, hip_max = 160, 180
|
|
758
|
+
knee_min, knee_max = 165, 182
|
|
759
|
+
ankle_min, ankle_max = 110, 140
|
|
760
|
+
elif profile in (AthleteProfile.TRAINED, AthleteProfile.ELITE):
|
|
761
|
+
hip_min, hip_max = 170, 185
|
|
762
|
+
knee_min, knee_max = 173, 190
|
|
763
|
+
ankle_min, ankle_max = 125, 155
|
|
764
|
+
else:
|
|
765
|
+
return
|
|
766
|
+
|
|
767
|
+
# Count how many joints are near their boundaries
|
|
768
|
+
joints_at_boundary = 0
|
|
769
|
+
boundary_threshold = 3.0 # degrees from limit
|
|
770
|
+
|
|
771
|
+
if hip <= hip_min + boundary_threshold or hip >= hip_max - boundary_threshold:
|
|
772
|
+
joints_at_boundary += 1
|
|
773
|
+
if (
|
|
774
|
+
knee <= knee_min + boundary_threshold
|
|
775
|
+
or knee >= knee_max - boundary_threshold
|
|
776
|
+
):
|
|
777
|
+
joints_at_boundary += 1
|
|
778
|
+
if (
|
|
779
|
+
ankle <= ankle_min + boundary_threshold
|
|
780
|
+
or ankle >= ankle_max - boundary_threshold
|
|
781
|
+
):
|
|
782
|
+
joints_at_boundary += 1
|
|
783
|
+
|
|
784
|
+
# If 2+ joints at boundaries, likely compensation pattern
|
|
785
|
+
if joints_at_boundary >= 2:
|
|
786
|
+
result.add_info(
|
|
787
|
+
"joint_compensation",
|
|
788
|
+
f"Multiple joints near extension limits (hip={hip:.0f}°, "
|
|
789
|
+
f"knee={knee:.0f}°, ankle={ankle:.0f}°). "
|
|
790
|
+
f"May indicate compensatory movement pattern.",
|
|
791
|
+
value=float(joints_at_boundary),
|
|
792
|
+
)
|
|
793
|
+
|
|
700
794
|
@staticmethod
|
|
701
795
|
def _get_profile_range(
|
|
702
796
|
profile: AthleteProfile, bounds: MetricBounds
|
|
@@ -356,7 +356,9 @@ ATHLETE_PROFILES = {
|
|
|
356
356
|
}
|
|
357
357
|
|
|
358
358
|
|
|
359
|
-
def estimate_athlete_profile(
|
|
359
|
+
def estimate_athlete_profile(
|
|
360
|
+
metrics_dict: dict, gender: str | None = None
|
|
361
|
+
) -> AthleteProfile:
|
|
360
362
|
"""Estimate athlete profile from metrics.
|
|
361
363
|
|
|
362
364
|
Uses jump height as primary classifier:
|
|
@@ -365,6 +367,18 @@ def estimate_athlete_profile(metrics_dict: dict) -> AthleteProfile:
|
|
|
365
367
|
- 0.35-0.65m: Recreational
|
|
366
368
|
- 0.65-0.85m: Trained
|
|
367
369
|
- >0.85m: Elite
|
|
370
|
+
|
|
371
|
+
NOTE: Bounds are calibrated for adult males. Female athletes typically achieve
|
|
372
|
+
60-70% of male heights due to lower muscle mass and strength. If analyzing
|
|
373
|
+
female athletes, interpret results one level lower than classification suggests.
|
|
374
|
+
Example: Female athlete with 0.45m jump = Recreational male = Trained female.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
metrics_dict: Dictionary with CMJ metric values
|
|
378
|
+
gender: Optional gender for context ("M"/"F"). Currently informational only.
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
Estimated AthleteProfile
|
|
368
382
|
"""
|
|
369
383
|
jump_height = metrics_dict.get("jump_height", 0)
|
|
370
384
|
|
|
@@ -0,0 +1,343 @@
|
|
|
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, field
|
|
11
|
+
from enum import Enum
|
|
12
|
+
|
|
13
|
+
from kinemotion.core.dropjump_validation_bounds import (
|
|
14
|
+
AthleteProfile,
|
|
15
|
+
DropJumpBounds,
|
|
16
|
+
estimate_athlete_profile,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ValidationSeverity(Enum):
|
|
21
|
+
"""Severity level for validation issues."""
|
|
22
|
+
|
|
23
|
+
ERROR = "ERROR" # Metrics invalid, likely data corruption
|
|
24
|
+
WARNING = "WARNING" # Metrics valid but unusual, needs review
|
|
25
|
+
INFO = "INFO" # Normal variation, informational only
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class ValidationIssue:
|
|
30
|
+
"""Single validation issue."""
|
|
31
|
+
|
|
32
|
+
severity: ValidationSeverity
|
|
33
|
+
metric: str
|
|
34
|
+
message: str
|
|
35
|
+
value: float | None = None
|
|
36
|
+
bounds: tuple[float, float] | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class ValidationResult:
|
|
41
|
+
"""Complete validation result for drop jump metrics."""
|
|
42
|
+
|
|
43
|
+
issues: list[ValidationIssue] = field(default_factory=list)
|
|
44
|
+
status: str = "PASS" # "PASS", "PASS_WITH_WARNINGS", "FAIL"
|
|
45
|
+
athlete_profile: AthleteProfile | None = None
|
|
46
|
+
rsi: float | None = None
|
|
47
|
+
contact_flight_ratio: float | None = None
|
|
48
|
+
height_kinematic_trajectory_consistency: float | None = None # % error
|
|
49
|
+
|
|
50
|
+
def add_error(
|
|
51
|
+
self,
|
|
52
|
+
metric: str,
|
|
53
|
+
message: str,
|
|
54
|
+
value: float | None = None,
|
|
55
|
+
bounds: tuple[float, float] | None = None,
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Add error-level issue."""
|
|
58
|
+
self.issues.append(
|
|
59
|
+
ValidationIssue(
|
|
60
|
+
severity=ValidationSeverity.ERROR,
|
|
61
|
+
metric=metric,
|
|
62
|
+
message=message,
|
|
63
|
+
value=value,
|
|
64
|
+
bounds=bounds,
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def add_warning(
|
|
69
|
+
self,
|
|
70
|
+
metric: str,
|
|
71
|
+
message: str,
|
|
72
|
+
value: float | None = None,
|
|
73
|
+
bounds: tuple[float, float] | None = None,
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Add warning-level issue."""
|
|
76
|
+
self.issues.append(
|
|
77
|
+
ValidationIssue(
|
|
78
|
+
severity=ValidationSeverity.WARNING,
|
|
79
|
+
metric=metric,
|
|
80
|
+
message=message,
|
|
81
|
+
value=value,
|
|
82
|
+
bounds=bounds,
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def add_info(
|
|
87
|
+
self,
|
|
88
|
+
metric: str,
|
|
89
|
+
message: str,
|
|
90
|
+
value: float | None = None,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Add info-level issue."""
|
|
93
|
+
self.issues.append(
|
|
94
|
+
ValidationIssue(
|
|
95
|
+
severity=ValidationSeverity.INFO,
|
|
96
|
+
metric=metric,
|
|
97
|
+
message=message,
|
|
98
|
+
value=value,
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def finalize_status(self) -> None:
|
|
103
|
+
"""Determine final pass/fail status based on issues."""
|
|
104
|
+
has_errors = any(
|
|
105
|
+
issue.severity == ValidationSeverity.ERROR for issue in self.issues
|
|
106
|
+
)
|
|
107
|
+
has_warnings = any(
|
|
108
|
+
issue.severity == ValidationSeverity.WARNING for issue in self.issues
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if has_errors:
|
|
112
|
+
self.status = "FAIL"
|
|
113
|
+
elif has_warnings:
|
|
114
|
+
self.status = "PASS_WITH_WARNINGS"
|
|
115
|
+
else:
|
|
116
|
+
self.status = "PASS"
|
|
117
|
+
|
|
118
|
+
def to_dict(self) -> dict:
|
|
119
|
+
"""Convert validation result to JSON-serializable dictionary.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Dictionary with status, issues, and consistency metrics.
|
|
123
|
+
"""
|
|
124
|
+
return {
|
|
125
|
+
"status": self.status,
|
|
126
|
+
"issues": [
|
|
127
|
+
{
|
|
128
|
+
"severity": issue.severity.value,
|
|
129
|
+
"metric": issue.metric,
|
|
130
|
+
"message": issue.message,
|
|
131
|
+
"value": issue.value,
|
|
132
|
+
"bounds": issue.bounds,
|
|
133
|
+
}
|
|
134
|
+
for issue in self.issues
|
|
135
|
+
],
|
|
136
|
+
"athlete_profile": (
|
|
137
|
+
self.athlete_profile.value if self.athlete_profile else None
|
|
138
|
+
),
|
|
139
|
+
"rsi": self.rsi,
|
|
140
|
+
"contact_flight_ratio": self.contact_flight_ratio,
|
|
141
|
+
"height_kinematic_trajectory_consistency_percent": (
|
|
142
|
+
self.height_kinematic_trajectory_consistency
|
|
143
|
+
),
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class DropJumpMetricsValidator:
|
|
148
|
+
"""Comprehensive drop jump metrics validator."""
|
|
149
|
+
|
|
150
|
+
def __init__(self, assumed_profile: AthleteProfile | None = None):
|
|
151
|
+
"""Initialize validator.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
assumed_profile: If provided, validate against this specific profile.
|
|
155
|
+
Otherwise, estimate from metrics.
|
|
156
|
+
"""
|
|
157
|
+
self.assumed_profile = assumed_profile
|
|
158
|
+
|
|
159
|
+
def validate(self, metrics: dict) -> ValidationResult:
|
|
160
|
+
"""Validate drop jump metrics comprehensively.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
metrics: Dictionary with drop jump metric values
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
ValidationResult with all issues and status
|
|
167
|
+
"""
|
|
168
|
+
result = ValidationResult()
|
|
169
|
+
|
|
170
|
+
# Estimate athlete profile if not provided
|
|
171
|
+
if self.assumed_profile:
|
|
172
|
+
result.athlete_profile = self.assumed_profile
|
|
173
|
+
else:
|
|
174
|
+
result.athlete_profile = estimate_athlete_profile(metrics)
|
|
175
|
+
|
|
176
|
+
# Extract metric values (handle nested "data" structure)
|
|
177
|
+
data = metrics.get("data", metrics) # Support both structures
|
|
178
|
+
|
|
179
|
+
contact_time_ms = data.get("ground_contact_time_ms")
|
|
180
|
+
flight_time_ms = data.get("flight_time_ms")
|
|
181
|
+
jump_height_m = data.get("jump_height_m")
|
|
182
|
+
jump_height_kinematic_m = data.get("jump_height_kinematic_m")
|
|
183
|
+
jump_height_trajectory_m = data.get("jump_height_trajectory_normalized")
|
|
184
|
+
|
|
185
|
+
# Validate individual metrics
|
|
186
|
+
if contact_time_ms is not None:
|
|
187
|
+
self._check_contact_time(contact_time_ms, result)
|
|
188
|
+
|
|
189
|
+
if flight_time_ms is not None:
|
|
190
|
+
self._check_flight_time(flight_time_ms, result)
|
|
191
|
+
|
|
192
|
+
if jump_height_m is not None:
|
|
193
|
+
self._check_jump_height(jump_height_m, result)
|
|
194
|
+
|
|
195
|
+
# Cross-validation
|
|
196
|
+
if contact_time_ms is not None and flight_time_ms is not None:
|
|
197
|
+
self._check_rsi(contact_time_ms, flight_time_ms, result)
|
|
198
|
+
|
|
199
|
+
# Dual height validation (kinematic vs trajectory)
|
|
200
|
+
if jump_height_kinematic_m is not None and jump_height_trajectory_m is not None:
|
|
201
|
+
self._check_dual_height_consistency(
|
|
202
|
+
jump_height_kinematic_m, jump_height_trajectory_m, result
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Finalize status
|
|
206
|
+
result.finalize_status()
|
|
207
|
+
|
|
208
|
+
return result
|
|
209
|
+
|
|
210
|
+
def _check_contact_time(
|
|
211
|
+
self, contact_time_ms: float, result: ValidationResult
|
|
212
|
+
) -> None:
|
|
213
|
+
"""Validate contact time."""
|
|
214
|
+
contact_time_s = contact_time_ms / 1000.0
|
|
215
|
+
bounds = DropJumpBounds.CONTACT_TIME
|
|
216
|
+
|
|
217
|
+
if not bounds.is_physically_possible(contact_time_s):
|
|
218
|
+
result.add_error(
|
|
219
|
+
"contact_time",
|
|
220
|
+
f"Contact time {contact_time_s:.3f}s physically impossible",
|
|
221
|
+
value=contact_time_s,
|
|
222
|
+
bounds=(bounds.absolute_min, bounds.absolute_max),
|
|
223
|
+
)
|
|
224
|
+
elif result.athlete_profile and not bounds.contains(
|
|
225
|
+
contact_time_s, result.athlete_profile
|
|
226
|
+
):
|
|
227
|
+
profile_name = result.athlete_profile.value
|
|
228
|
+
result.add_warning(
|
|
229
|
+
"contact_time",
|
|
230
|
+
f"Contact time {contact_time_s:.3f}s unusual for {profile_name} athlete",
|
|
231
|
+
value=contact_time_s,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
def _check_flight_time(
|
|
235
|
+
self, flight_time_ms: float, result: ValidationResult
|
|
236
|
+
) -> None:
|
|
237
|
+
"""Validate flight time."""
|
|
238
|
+
flight_time_s = flight_time_ms / 1000.0
|
|
239
|
+
bounds = DropJumpBounds.FLIGHT_TIME
|
|
240
|
+
|
|
241
|
+
if not bounds.is_physically_possible(flight_time_s):
|
|
242
|
+
result.add_error(
|
|
243
|
+
"flight_time",
|
|
244
|
+
f"Flight time {flight_time_s:.3f}s physically impossible",
|
|
245
|
+
value=flight_time_s,
|
|
246
|
+
bounds=(bounds.absolute_min, bounds.absolute_max),
|
|
247
|
+
)
|
|
248
|
+
elif result.athlete_profile and not bounds.contains(
|
|
249
|
+
flight_time_s, result.athlete_profile
|
|
250
|
+
):
|
|
251
|
+
profile_name = result.athlete_profile.value
|
|
252
|
+
result.add_warning(
|
|
253
|
+
"flight_time",
|
|
254
|
+
f"Flight time {flight_time_s:.3f}s unusual for {profile_name} athlete",
|
|
255
|
+
value=flight_time_s,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
def _check_jump_height(
|
|
259
|
+
self, jump_height_m: float, result: ValidationResult
|
|
260
|
+
) -> None:
|
|
261
|
+
"""Validate jump height."""
|
|
262
|
+
bounds = DropJumpBounds.JUMP_HEIGHT
|
|
263
|
+
|
|
264
|
+
if not bounds.is_physically_possible(jump_height_m):
|
|
265
|
+
result.add_error(
|
|
266
|
+
"jump_height",
|
|
267
|
+
f"Jump height {jump_height_m:.3f}m physically impossible",
|
|
268
|
+
value=jump_height_m,
|
|
269
|
+
bounds=(bounds.absolute_min, bounds.absolute_max),
|
|
270
|
+
)
|
|
271
|
+
elif result.athlete_profile and not bounds.contains(
|
|
272
|
+
jump_height_m, result.athlete_profile
|
|
273
|
+
):
|
|
274
|
+
profile_name = result.athlete_profile.value
|
|
275
|
+
result.add_warning(
|
|
276
|
+
"jump_height",
|
|
277
|
+
f"Jump height {jump_height_m:.3f}m unusual for {profile_name} athlete",
|
|
278
|
+
value=jump_height_m,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
def _check_rsi(
|
|
282
|
+
self, contact_time_ms: float, flight_time_ms: float, result: ValidationResult
|
|
283
|
+
) -> None:
|
|
284
|
+
"""Validate RSI and cross-check consistency."""
|
|
285
|
+
contact_time_s = contact_time_ms / 1000.0
|
|
286
|
+
flight_time_s = flight_time_ms / 1000.0
|
|
287
|
+
|
|
288
|
+
if contact_time_s > 0:
|
|
289
|
+
rsi = flight_time_s / contact_time_s
|
|
290
|
+
result.rsi = rsi
|
|
291
|
+
result.contact_flight_ratio = contact_time_s / flight_time_s
|
|
292
|
+
|
|
293
|
+
bounds = DropJumpBounds.RSI
|
|
294
|
+
|
|
295
|
+
if not bounds.is_physically_possible(rsi):
|
|
296
|
+
result.add_error(
|
|
297
|
+
"rsi",
|
|
298
|
+
f"RSI {rsi:.2f} physically impossible",
|
|
299
|
+
value=rsi,
|
|
300
|
+
bounds=(bounds.absolute_min, bounds.absolute_max),
|
|
301
|
+
)
|
|
302
|
+
elif result.athlete_profile and not bounds.contains(
|
|
303
|
+
rsi, result.athlete_profile
|
|
304
|
+
):
|
|
305
|
+
result.add_warning(
|
|
306
|
+
"rsi",
|
|
307
|
+
f"RSI {rsi:.2f} unusual for {result.athlete_profile.value} athlete",
|
|
308
|
+
value=rsi,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
def _check_dual_height_consistency(
|
|
312
|
+
self,
|
|
313
|
+
jump_height_kinematic_m: float,
|
|
314
|
+
jump_height_trajectory_m: float,
|
|
315
|
+
result: ValidationResult,
|
|
316
|
+
) -> None:
|
|
317
|
+
"""Validate consistency between kinematic and trajectory-based heights.
|
|
318
|
+
|
|
319
|
+
Kinematic height (h = g*t²/8) comes from flight time (objective).
|
|
320
|
+
Trajectory height comes from position tracking (subject to landmark detection noise).
|
|
321
|
+
|
|
322
|
+
Expected correlation: r > 0.95, absolute difference < 5% for quality video.
|
|
323
|
+
"""
|
|
324
|
+
if jump_height_kinematic_m <= 0 or jump_height_trajectory_m <= 0:
|
|
325
|
+
return # Skip if either value is missing or invalid
|
|
326
|
+
|
|
327
|
+
# Calculate percentage difference
|
|
328
|
+
avg_height = (jump_height_kinematic_m + jump_height_trajectory_m) / 2.0
|
|
329
|
+
if avg_height > 0:
|
|
330
|
+
abs_diff = abs(jump_height_kinematic_m - jump_height_trajectory_m)
|
|
331
|
+
percent_error = (abs_diff / avg_height) * 100.0
|
|
332
|
+
result.height_kinematic_trajectory_consistency = percent_error
|
|
333
|
+
|
|
334
|
+
# Allow 10% tolerance for typical video processing noise
|
|
335
|
+
if percent_error > 10.0:
|
|
336
|
+
result.add_warning(
|
|
337
|
+
"height_consistency",
|
|
338
|
+
f"Kinematic ({jump_height_kinematic_m:.3f}m) and trajectory "
|
|
339
|
+
f"({jump_height_trajectory_m:.3f}m) heights differ by {percent_error:.1f}%. "
|
|
340
|
+
f"May indicate landmark detection issues or video quality problems.",
|
|
341
|
+
value=percent_error,
|
|
342
|
+
bounds=(0, 10),
|
|
343
|
+
)
|
|
@@ -0,0 +1,196 @@
|
|
|
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 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., "s", "m", "ratio")
|
|
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 DropJumpBounds:
|
|
79
|
+
"""Collection of physiological bounds for all drop jump metrics."""
|
|
80
|
+
|
|
81
|
+
# GROUND CONTACT TIME (seconds, landing interaction)
|
|
82
|
+
CONTACT_TIME = MetricBounds(
|
|
83
|
+
absolute_min=0.08, # Physiological minimum: neural delay + deceleration
|
|
84
|
+
practical_min=0.15, # Extreme plyometric
|
|
85
|
+
recreational_min=0.35, # Typical landing
|
|
86
|
+
recreational_max=0.70, # Slower absorption
|
|
87
|
+
elite_min=0.20,
|
|
88
|
+
elite_max=0.50,
|
|
89
|
+
absolute_max=1.50,
|
|
90
|
+
unit="s",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# FLIGHT TIME (seconds, after landing)
|
|
94
|
+
FLIGHT_TIME = MetricBounds(
|
|
95
|
+
absolute_min=0.30,
|
|
96
|
+
practical_min=0.40, # Minimal jump
|
|
97
|
+
recreational_min=0.50,
|
|
98
|
+
recreational_max=0.85,
|
|
99
|
+
elite_min=0.65,
|
|
100
|
+
elite_max=1.10,
|
|
101
|
+
absolute_max=1.40,
|
|
102
|
+
unit="s",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# JUMP HEIGHT (meters, calculated from flight time)
|
|
106
|
+
JUMP_HEIGHT = MetricBounds(
|
|
107
|
+
absolute_min=0.05,
|
|
108
|
+
practical_min=0.10,
|
|
109
|
+
recreational_min=0.25,
|
|
110
|
+
recreational_max=0.65,
|
|
111
|
+
elite_min=0.50,
|
|
112
|
+
elite_max=1.00,
|
|
113
|
+
absolute_max=1.30,
|
|
114
|
+
unit="m",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# REACTIVE STRENGTH INDEX (RSI) = flight_time / contact_time (ratio, no unit)
|
|
118
|
+
RSI = MetricBounds(
|
|
119
|
+
absolute_min=0.30, # Very poor reactive ability
|
|
120
|
+
practical_min=0.50,
|
|
121
|
+
recreational_min=0.70,
|
|
122
|
+
recreational_max=1.80,
|
|
123
|
+
elite_min=1.50, # Elite: fast contact, long flight
|
|
124
|
+
elite_max=3.50,
|
|
125
|
+
absolute_max=5.00,
|
|
126
|
+
unit="ratio",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def estimate_athlete_profile(
|
|
131
|
+
metrics: dict, gender: str | None = None
|
|
132
|
+
) -> AthleteProfile:
|
|
133
|
+
"""Estimate athlete profile from drop jump metrics.
|
|
134
|
+
|
|
135
|
+
Uses jump_height and contact_time to classify athlete level.
|
|
136
|
+
|
|
137
|
+
NOTE: Bounds are calibrated for adult males. Female athletes typically achieve
|
|
138
|
+
60-70% of male heights due to lower muscle mass and strength. If analyzing
|
|
139
|
+
female athletes, interpret results one level lower than classification suggests.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
metrics: Dictionary with drop jump metric values
|
|
143
|
+
gender: Optional gender for context ("M"/"F"). Currently informational only.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Estimated AthleteProfile (ELDERLY, UNTRAINED, RECREATIONAL, TRAINED, or ELITE)
|
|
147
|
+
"""
|
|
148
|
+
jump_height = metrics.get("data", {}).get("jump_height_m")
|
|
149
|
+
contact_time = metrics.get("data", {}).get("ground_contact_time_ms")
|
|
150
|
+
|
|
151
|
+
if jump_height is None or contact_time is None:
|
|
152
|
+
return AthleteProfile.RECREATIONAL # Default
|
|
153
|
+
|
|
154
|
+
# Convert contact_time from ms to seconds
|
|
155
|
+
contact_time_s = contact_time / 1000.0
|
|
156
|
+
|
|
157
|
+
# Decision logic: Use weighted combination to avoid over-weighting single metrics
|
|
158
|
+
# Calculate profile scores based on each metric
|
|
159
|
+
height_score = 0.0
|
|
160
|
+
if jump_height < 0.25:
|
|
161
|
+
height_score = 0 # Elderly
|
|
162
|
+
elif jump_height < 0.35:
|
|
163
|
+
height_score = 1 # Untrained
|
|
164
|
+
elif jump_height < 0.50:
|
|
165
|
+
height_score = 2 # Recreational
|
|
166
|
+
elif jump_height < 0.70:
|
|
167
|
+
height_score = 3 # Trained
|
|
168
|
+
else:
|
|
169
|
+
height_score = 4 # Elite
|
|
170
|
+
|
|
171
|
+
contact_score = 0.0
|
|
172
|
+
if contact_time_s > 0.60:
|
|
173
|
+
contact_score = 0 # Elderly
|
|
174
|
+
elif contact_time_s > 0.50:
|
|
175
|
+
contact_score = 1 # Untrained
|
|
176
|
+
elif contact_time_s > 0.45:
|
|
177
|
+
contact_score = 2 # Recreational
|
|
178
|
+
elif contact_time_s > 0.40:
|
|
179
|
+
contact_score = 3 # Trained
|
|
180
|
+
else:
|
|
181
|
+
contact_score = 4 # Elite
|
|
182
|
+
|
|
183
|
+
# Weight height more heavily (70%) than contact time (30%)
|
|
184
|
+
# Height is more reliable indicator across populations
|
|
185
|
+
combined_score = (height_score * 0.70) + (contact_score * 0.30)
|
|
186
|
+
|
|
187
|
+
if combined_score < 1.0:
|
|
188
|
+
return AthleteProfile.ELDERLY
|
|
189
|
+
elif combined_score < 1.7:
|
|
190
|
+
return AthleteProfile.UNTRAINED
|
|
191
|
+
elif combined_score < 2.7:
|
|
192
|
+
return AthleteProfile.RECREATIONAL
|
|
193
|
+
elif combined_score < 3.7:
|
|
194
|
+
return AthleteProfile.TRAINED
|
|
195
|
+
else:
|
|
196
|
+
return AthleteProfile.ELITE
|
|
@@ -16,6 +16,7 @@ from .analysis import (
|
|
|
16
16
|
)
|
|
17
17
|
|
|
18
18
|
if TYPE_CHECKING:
|
|
19
|
+
from ..core.dropjump_metrics_validator import ValidationResult
|
|
19
20
|
from ..core.metadata import ResultMetadata
|
|
20
21
|
from ..core.quality import QualityAssessment
|
|
21
22
|
|
|
@@ -39,11 +40,12 @@ class DropJumpDataDict(TypedDict, total=False):
|
|
|
39
40
|
flight_end_frame_precise: float | None
|
|
40
41
|
|
|
41
42
|
|
|
42
|
-
class DropJumpResultDict(TypedDict):
|
|
43
|
+
class DropJumpResultDict(TypedDict, total=False):
|
|
43
44
|
"""Type-safe dictionary for complete drop jump result with data and metadata."""
|
|
44
45
|
|
|
45
46
|
data: DropJumpDataDict
|
|
46
47
|
metadata: dict # ResultMetadata.to_dict()
|
|
48
|
+
validation: dict # ValidationResult.to_dict()
|
|
47
49
|
|
|
48
50
|
|
|
49
51
|
class DropJumpMetrics:
|
|
@@ -69,6 +71,8 @@ class DropJumpMetrics:
|
|
|
69
71
|
self.quality_assessment: QualityAssessment | None = None
|
|
70
72
|
# Complete metadata
|
|
71
73
|
self.result_metadata: ResultMetadata | None = None
|
|
74
|
+
# Validation result
|
|
75
|
+
self.validation_result: ValidationResult | None = None
|
|
72
76
|
|
|
73
77
|
def _build_data_dict(self) -> DropJumpDataDict:
|
|
74
78
|
"""Build the data portion of the result dictionary.
|
|
@@ -125,11 +129,17 @@ class DropJumpMetrics:
|
|
|
125
129
|
Returns:
|
|
126
130
|
Dictionary with nested data and metadata structure.
|
|
127
131
|
"""
|
|
128
|
-
|
|
132
|
+
result: DropJumpResultDict = {
|
|
129
133
|
"data": self._build_data_dict(),
|
|
130
134
|
"metadata": self._build_metadata_dict(),
|
|
131
135
|
}
|
|
132
136
|
|
|
137
|
+
# Include validation results if available
|
|
138
|
+
if self.validation_result is not None:
|
|
139
|
+
result["validation"] = self.validation_result.to_dict()
|
|
140
|
+
|
|
141
|
+
return result
|
|
142
|
+
|
|
133
143
|
|
|
134
144
|
def _determine_drop_start_frame(
|
|
135
145
|
drop_start_frame: int | None,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kinemotion
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.30.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
|
|
@@ -36,6 +36,7 @@ Description-Content-Type: text/markdown
|
|
|
36
36
|
[](https://github.com/feniix/kinemotion/actions)
|
|
37
37
|
[](https://sonarcloud.io/summary/overall?id=feniix_kinemotion)
|
|
38
38
|
[](https://sonarcloud.io/summary/overall?id=feniix_kinemotion)
|
|
39
|
+
[](https://www.bestpractices.dev/projects/11506)
|
|
39
40
|
|
|
40
41
|
[](https://github.com/astral-sh/ruff)
|
|
41
42
|
[](https://github.com/microsoft/pyright)
|
|
@@ -695,7 +696,7 @@ Before committing code, ensure all checks pass:
|
|
|
695
696
|
1. Ensure type safety with pyright
|
|
696
697
|
1. Run all tests with pytest
|
|
697
698
|
|
|
698
|
-
See [CLAUDE.md](CLAUDE.md) for detailed development guidelines.
|
|
699
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines and requirements, or [CLAUDE.md](CLAUDE.md) for detailed development guidelines.
|
|
699
700
|
|
|
700
701
|
## Limitations
|
|
701
702
|
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
kinemotion/__init__.py,sha256=sxdDOekOrIgjxm842gy-6zfq7OWmGl9ShJtXCm4JI7c,723
|
|
2
|
-
kinemotion/api.py,sha256=
|
|
2
|
+
kinemotion/api.py,sha256=ozt6GLZtw2ZWki0tBlkpOQgcNH_M7GfrRPlc_Rg8ROw,39284
|
|
3
3
|
kinemotion/cli.py,sha256=cqYV_7URH0JUDy1VQ_EDLv63FmNO4Ns20m6s1XAjiP4,464
|
|
4
4
|
kinemotion/cmj/__init__.py,sha256=Ynv0-Oco4I3Y1Ubj25m3h9h2XFqeNwpAewXmAYOmwfU,127
|
|
5
5
|
kinemotion/cmj/analysis.py,sha256=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
8
|
kinemotion/cmj/joint_angles.py,sha256=HmheIEiKcQz39cRezk4h-htorOhGNPsqKIR9RsAEKts,9960
|
|
9
|
-
kinemotion/cmj/kinematics.py,sha256
|
|
9
|
+
kinemotion/cmj/kinematics.py,sha256=ax2RijtAWItZPNRmNr-CvC7bOSsZQw2qdCEnm5hUUpU,10247
|
|
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=
|
|
14
|
-
kinemotion/core/cmj_validation_bounds.py,sha256=
|
|
13
|
+
kinemotion/core/cmj_metrics_validator.py,sha256=bbOPTbFqDEZv3lDA8qejjCcMqXE7TYvHizCcWzHRW9Y,30902
|
|
14
|
+
kinemotion/core/cmj_validation_bounds.py,sha256=NXW0d4S8hDqSkzAiyX9UyWDnKsWf61ygEKTKE2a1uNc,14069
|
|
15
15
|
kinemotion/core/debug_overlay_utils.py,sha256=TyUb5okv5qw8oeaX3jsUO_kpwf1NnaHEAOTm-8LwTno,4587
|
|
16
|
+
kinemotion/core/dropjump_metrics_validator.py,sha256=yGcg8ub6Z791qj5BxCn9mHin608tfxvxxfIeTql8HcY,11967
|
|
17
|
+
kinemotion/core/dropjump_validation_bounds.py,sha256=Ow7T-0IK_qy0k9UZdiCuT_tttxklWAkuXFalQkF8Jbs,6927
|
|
16
18
|
kinemotion/core/filtering.py,sha256=f-m-aA59e4WqE6u-9MA51wssu7rI-Y_7n1cG8IWdeRQ,11241
|
|
17
19
|
kinemotion/core/formatting.py,sha256=G_3eqgOtym9RFOZVEwCxye4A2cyrmgvtQ214vIshowU,2480
|
|
18
20
|
kinemotion/core/metadata.py,sha256=PyGHL6sx7Hj21lyorg2VsWP9BGTj_y_-wWU6eKCEfJo,6817
|
|
@@ -24,10 +26,10 @@ kinemotion/dropjump/__init__.py,sha256=yc1XiZ9vfo5h_n7PKVSiX2TTgaIfGL7Y7SkQtiDZj
|
|
|
24
26
|
kinemotion/dropjump/analysis.py,sha256=BQ5NqSPNJjFQOb-W4bXSLvjCgWd-nvqx5NElyeqZJC4,29067
|
|
25
27
|
kinemotion/dropjump/cli.py,sha256=ZyroaYPwz8TgfL39Wcaj6m68Awl6lYXC75ttaflU-c0,16236
|
|
26
28
|
kinemotion/dropjump/debug_overlay.py,sha256=LkPw6ucb7beoYWS4L-Lvjs1KLCm5wAWDAfiznUeV2IQ,5668
|
|
27
|
-
kinemotion/dropjump/kinematics.py,sha256=
|
|
29
|
+
kinemotion/dropjump/kinematics.py,sha256=AfqIS8kaI3B8olPX9EY1QxQsuNmuJA5GRsI1EL4NHHg,17091
|
|
28
30
|
kinemotion/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
29
|
-
kinemotion-0.
|
|
30
|
-
kinemotion-0.
|
|
31
|
-
kinemotion-0.
|
|
32
|
-
kinemotion-0.
|
|
33
|
-
kinemotion-0.
|
|
31
|
+
kinemotion-0.30.0.dist-info/METADATA,sha256=AUhSzQW2siu-R4KfUyS5YRDy1i9eYzzYxOMdWZXF2ww,26020
|
|
32
|
+
kinemotion-0.30.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
33
|
+
kinemotion-0.30.0.dist-info/entry_points.txt,sha256=zaqnAnjLvcdrk1Qvj5nvXZCZ2gp0prS7it1zTJygcIY,50
|
|
34
|
+
kinemotion-0.30.0.dist-info/licenses/LICENSE,sha256=KZajvqsHw0NoOHOi2q0FZ4NBe9HdV6oey-IPYAtHXfg,1088
|
|
35
|
+
kinemotion-0.30.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|