kinemotion 0.20.2__tar.gz → 0.21.0__tar.gz
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-0.20.2 → kinemotion-0.21.0}/.pre-commit-config.yaml +2 -2
- {kinemotion-0.20.2 → kinemotion-0.21.0}/CHANGELOG.md +8 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/CLAUDE.md +66 -9
- {kinemotion-0.20.2 → kinemotion-0.21.0}/PKG-INFO +1 -1
- {kinemotion-0.20.2 → kinemotion-0.21.0}/pyproject.toml +5 -2
- {kinemotion-0.20.2 → kinemotion-0.21.0}/src/kinemotion/cmj/kinematics.py +25 -4
- {kinemotion-0.20.2 → kinemotion-0.21.0}/src/kinemotion/core/smoothing.py +19 -12
- {kinemotion-0.20.2 → kinemotion-0.21.0}/src/kinemotion/dropjump/kinematics.py +27 -5
- {kinemotion-0.20.2 → kinemotion-0.21.0}/uv.lock +356 -314
- {kinemotion-0.20.2 → kinemotion-0.21.0}/.dockerignore +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/.github/pull_request_template.md +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/.github/workflows/docs.yml +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/.github/workflows/release.yml +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/.github/workflows/test.yml +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/.gitignore +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/.readthedocs.yml +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/.tool-versions +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/CODE_OF_CONDUCT.md +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/CONTRIBUTING.md +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/Dockerfile +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/GEMINI.md +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/LICENSE +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/README.md +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/SECURITY.md +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/docs/README.md +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/docs/api/cmj.md +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/docs/api/core.md +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/docs/api/dropjump.md +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/docs/api/overview.md +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/docs/development/errors-findings.md +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/docs/development/validation-plan.md +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/docs/development/wallball-norep-detection.md +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/docs/guides/bulk-processing.md +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/docs/guides/camera-setup.md +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/docs/guides/cmj-guide.md +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/docs/index.md +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/docs/reference/parameters.md +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/docs/reference/pose-systems.md +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/docs/research/sports-biomechanics-pose-estimation.md +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/docs/technical/framerate.md +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/docs/technical/imu-metadata.md +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/docs/technical/real-time-analysis.md +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/docs/technical/triple-extension.md +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/docs/translations/es/camera-setup.md +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/examples/bulk/README.md +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/examples/bulk/bulk_processing.py +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/examples/bulk/simple_example.py +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/examples/programmatic_usage.py +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/mkdocs.yml +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/requirements-docs.txt +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/samples/cmjs/README.md +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/sonar-project.properties +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/src/kinemotion/__init__.py +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/src/kinemotion/api.py +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/src/kinemotion/cli.py +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/src/kinemotion/cmj/__init__.py +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/src/kinemotion/cmj/analysis.py +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/src/kinemotion/cmj/cli.py +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/src/kinemotion/cmj/debug_overlay.py +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/src/kinemotion/cmj/joint_angles.py +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/src/kinemotion/core/__init__.py +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/src/kinemotion/core/auto_tuning.py +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/src/kinemotion/core/cli_utils.py +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/src/kinemotion/core/debug_overlay_utils.py +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/src/kinemotion/core/filtering.py +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/src/kinemotion/core/pose.py +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/src/kinemotion/core/video_io.py +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/src/kinemotion/dropjump/__init__.py +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/src/kinemotion/dropjump/analysis.py +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/src/kinemotion/dropjump/cli.py +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/src/kinemotion/dropjump/debug_overlay.py +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/src/kinemotion/py.typed +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/tests/__init__.py +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/tests/test_adaptive_threshold.py +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/tests/test_api.py +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/tests/test_aspect_ratio.py +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/tests/test_cli_imports.py +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/tests/test_cmj_analysis.py +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/tests/test_cmj_kinematics.py +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/tests/test_com_estimation.py +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/tests/test_contact_detection.py +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/tests/test_filtering.py +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/tests/test_joint_angles.py +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/tests/test_kinematics.py +0 -0
- {kinemotion-0.20.2 → kinemotion-0.21.0}/tests/test_polyorder.py +0 -0
|
@@ -15,12 +15,12 @@ repos:
|
|
|
15
15
|
- id: mixed-line-ending
|
|
16
16
|
|
|
17
17
|
- repo: https://github.com/psf/black
|
|
18
|
-
rev: 25.
|
|
18
|
+
rev: 25.11.0
|
|
19
19
|
hooks:
|
|
20
20
|
- id: black
|
|
21
21
|
|
|
22
22
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
23
|
-
rev: v0.14.
|
|
23
|
+
rev: v0.14.4
|
|
24
24
|
hooks:
|
|
25
25
|
- id: ruff
|
|
26
26
|
args: [--fix, --exit-non-zero-on-fix]
|
|
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
<!-- version list -->
|
|
9
9
|
|
|
10
|
+
## v0.21.0 (2025-11-10)
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
- Add TypedDict and type aliases for improved type safety
|
|
15
|
+
([`053e010`](https://github.com/feniix/kinemotion/commit/053e010cf80e1c91d5900c39d49b1d7ac2ac6ab4))
|
|
16
|
+
|
|
17
|
+
|
|
10
18
|
## v0.20.2 (2025-11-10)
|
|
11
19
|
|
|
12
20
|
### Bug Fixes
|
|
@@ -19,7 +19,7 @@ uv run kinemotion cmj-analyze video.mp4
|
|
|
19
19
|
|
|
20
20
|
**Development:**
|
|
21
21
|
```bash
|
|
22
|
-
uv run pytest # Run all
|
|
22
|
+
uv run pytest # Run all 146 tests with coverage (57.57%)
|
|
23
23
|
uv run pytest --cov-report=html # Generate HTML coverage report
|
|
24
24
|
uv run ruff check --fix && uv run pyright # Lint + type check
|
|
25
25
|
```
|
|
@@ -64,7 +64,7 @@ src/kinemotion/
|
|
|
64
64
|
├── dropjump/ # Drop jump: cli, analysis, kinematics, debug_overlay
|
|
65
65
|
└── cmj/ # CMJ: cli, analysis, kinematics, joint_angles, debug_overlay
|
|
66
66
|
|
|
67
|
-
tests/ #
|
|
67
|
+
tests/ # 146 tests total (comprehensive coverage across all modules)
|
|
68
68
|
docs/ # CMJ_GUIDE, TRIPLE_EXTENSION, REAL_TIME_ANALYSIS, etc.
|
|
69
69
|
```
|
|
70
70
|
|
|
@@ -168,7 +168,7 @@ OpenCV vs NumPy ordering:
|
|
|
168
168
|
```bash
|
|
169
169
|
uv run ruff check --fix # Auto-fix linting
|
|
170
170
|
uv run pyright # Type check (strict)
|
|
171
|
-
uv run pytest # All
|
|
171
|
+
uv run pytest # All 146 tests with coverage
|
|
172
172
|
```
|
|
173
173
|
|
|
174
174
|
### Standards
|
|
@@ -177,22 +177,22 @@ uv run pytest # All 75 tests with coverage
|
|
|
177
177
|
- Ruff (100 char lines)
|
|
178
178
|
- Conventional Commits (see below)
|
|
179
179
|
- **Code duplication target: < 3%**
|
|
180
|
-
- **Test coverage: ≥ 50% (current:
|
|
180
|
+
- **Test coverage: ≥ 50% (current: 57.57% with branch coverage)**
|
|
181
181
|
|
|
182
182
|
### Coverage Analysis
|
|
183
183
|
|
|
184
|
-
Current coverage: **
|
|
184
|
+
Current coverage: **57.57%** (2225 statements, 752 branches)
|
|
185
185
|
|
|
186
186
|
**Well-tested modules (>80%):**
|
|
187
187
|
- Core filtering: 87.80%
|
|
188
188
|
- Core pose tracking: 88.46%
|
|
189
|
-
- Drop jump kinematics:
|
|
190
|
-
- CMJ kinematics:
|
|
189
|
+
- Drop jump kinematics: 85.71%
|
|
190
|
+
- CMJ kinematics: 95.65%
|
|
191
|
+
- CMJ joint angles: 100.00%
|
|
191
192
|
|
|
192
|
-
**
|
|
193
|
+
**Expected lower coverage (<30%):**
|
|
193
194
|
- CLI modules: 22-23% (expected - minimal integration tests)
|
|
194
195
|
- Debug overlays: 10-36% (visualization code)
|
|
195
|
-
- Joint angles: 6.20% (feature module)
|
|
196
196
|
|
|
197
197
|
View detailed report: `uv run pytest --cov-report=html && open htmlcov/index.html`
|
|
198
198
|
|
|
@@ -268,6 +268,63 @@ metrics = process_cmj_video("video.mp4", quality="balanced")
|
|
|
268
268
|
1. Convert NumPy types in `to_dict()`: `int()`, `float()`
|
|
269
269
|
2. Type all functions (pyright strict)
|
|
270
270
|
3. Handle None in optional fields
|
|
271
|
+
4. Use TypedDict for dictionary returns
|
|
272
|
+
5. Use type aliases for complex nested types
|
|
273
|
+
|
|
274
|
+
### Modern Type Hints (NumPy 2.x)
|
|
275
|
+
|
|
276
|
+
The project uses NumPy 2.x-compatible type hints:
|
|
277
|
+
|
|
278
|
+
**TypedDict for type-safe dictionaries:**
|
|
279
|
+
```python
|
|
280
|
+
from typing import TypedDict
|
|
281
|
+
|
|
282
|
+
class DropJumpMetricsDict(TypedDict, total=False):
|
|
283
|
+
"""Type-safe dictionary for drop jump metrics JSON output."""
|
|
284
|
+
ground_contact_time_ms: float | None
|
|
285
|
+
flight_time_ms: float | None
|
|
286
|
+
# ... IDE autocomplete and type checking!
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**Type aliases for cleaner signatures:**
|
|
290
|
+
```python
|
|
291
|
+
from typing import TypeAlias
|
|
292
|
+
|
|
293
|
+
LandmarkCoord: TypeAlias = tuple[float, float, float] # (x, y, visibility)
|
|
294
|
+
LandmarkFrame: TypeAlias = dict[str, LandmarkCoord] | None
|
|
295
|
+
LandmarkSequence: TypeAlias = list[LandmarkFrame]
|
|
296
|
+
|
|
297
|
+
def smooth_landmarks(landmark_sequence: LandmarkSequence) -> LandmarkSequence:
|
|
298
|
+
# Much cleaner than list[dict[str, tuple[float, float, float]] | None]!
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
**NDArray with dtype specificity:**
|
|
302
|
+
```python
|
|
303
|
+
from numpy.typing import NDArray
|
|
304
|
+
|
|
305
|
+
def calculate_metrics(
|
|
306
|
+
positions: NDArray[np.float64], # Explicit dtype for arrays
|
|
307
|
+
velocities: NDArray[np.float64],
|
|
308
|
+
) -> CMJMetrics:
|
|
309
|
+
...
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Dependencies
|
|
313
|
+
|
|
314
|
+
**Key versions:**
|
|
315
|
+
- Python: 3.12.7
|
|
316
|
+
- NumPy: 2.3.4 (benefits: better type hints, improved SciPy compatibility)
|
|
317
|
+
- pytest: 9.0.0 (features: per-test timing, strict mode)
|
|
318
|
+
- MediaPipe: 0.10.14
|
|
319
|
+
|
|
320
|
+
**Pytest 9 configuration** (`pyproject.toml`):
|
|
321
|
+
```toml
|
|
322
|
+
[tool.pytest]
|
|
323
|
+
minversion = "9.0"
|
|
324
|
+
testpaths = ["tests"]
|
|
325
|
+
console_output_style = "times" # Shows execution time per test
|
|
326
|
+
strict = true # Enables all strictness options
|
|
327
|
+
```
|
|
271
328
|
|
|
272
329
|
## Documentation
|
|
273
330
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kinemotion
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.21.0
|
|
4
4
|
Summary: Video-based kinematic analysis for athletic performance
|
|
5
5
|
Project-URL: Homepage, https://github.com/feniix/kinemotion
|
|
6
6
|
Project-URL: Repository, https://github.com/feniix/kinemotion
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "kinemotion"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.21.0"
|
|
4
4
|
description = "Video-based kinematic analysis for athletic performance"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10,<3.13"
|
|
@@ -62,8 +62,11 @@ dev = [
|
|
|
62
62
|
]
|
|
63
63
|
|
|
64
64
|
|
|
65
|
-
[tool.pytest
|
|
65
|
+
[tool.pytest]
|
|
66
|
+
minversion = "9.0"
|
|
66
67
|
testpaths = ["tests"]
|
|
68
|
+
console_output_style = "times"
|
|
69
|
+
strict = true
|
|
67
70
|
addopts = [
|
|
68
71
|
"--cov=src/kinemotion",
|
|
69
72
|
"--cov-report=term-missing",
|
|
@@ -1,9 +1,30 @@
|
|
|
1
1
|
"""Counter Movement Jump (CMJ) metrics calculation."""
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import TypedDict
|
|
5
5
|
|
|
6
6
|
import numpy as np
|
|
7
|
+
from numpy.typing import NDArray
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CMJMetricsDict(TypedDict, total=False):
|
|
11
|
+
"""Type-safe dictionary for CMJ metrics JSON output."""
|
|
12
|
+
|
|
13
|
+
jump_height_m: float
|
|
14
|
+
flight_time_s: float
|
|
15
|
+
countermovement_depth_m: float
|
|
16
|
+
eccentric_duration_s: float
|
|
17
|
+
concentric_duration_s: float
|
|
18
|
+
total_movement_time_s: float
|
|
19
|
+
peak_eccentric_velocity_m_s: float
|
|
20
|
+
peak_concentric_velocity_m_s: float
|
|
21
|
+
transition_time_s: float | None
|
|
22
|
+
standing_start_frame: float | None
|
|
23
|
+
lowest_point_frame: float
|
|
24
|
+
takeoff_frame: float
|
|
25
|
+
landing_frame: float
|
|
26
|
+
video_fps: float
|
|
27
|
+
tracking_method: str
|
|
7
28
|
|
|
8
29
|
|
|
9
30
|
@dataclass
|
|
@@ -44,7 +65,7 @@ class CMJMetrics:
|
|
|
44
65
|
video_fps: float
|
|
45
66
|
tracking_method: str
|
|
46
67
|
|
|
47
|
-
def to_dict(self) ->
|
|
68
|
+
def to_dict(self) -> CMJMetricsDict:
|
|
48
69
|
"""Convert metrics to JSON-serializable dictionary.
|
|
49
70
|
|
|
50
71
|
Returns:
|
|
@@ -78,8 +99,8 @@ class CMJMetrics:
|
|
|
78
99
|
|
|
79
100
|
|
|
80
101
|
def calculate_cmj_metrics(
|
|
81
|
-
positions: np.
|
|
82
|
-
velocities: np.
|
|
102
|
+
positions: NDArray[np.float64],
|
|
103
|
+
velocities: NDArray[np.float64],
|
|
83
104
|
standing_start_frame: float | None,
|
|
84
105
|
lowest_point_frame: float,
|
|
85
106
|
takeoff_frame: float,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Landmark smoothing utilities to reduce jitter in pose tracking."""
|
|
2
2
|
|
|
3
|
+
from typing import TypeAlias
|
|
4
|
+
|
|
3
5
|
import numpy as np
|
|
4
6
|
from scipy.signal import savgol_filter
|
|
5
7
|
|
|
@@ -8,9 +10,14 @@ from .filtering import (
|
|
|
8
10
|
reject_outliers,
|
|
9
11
|
)
|
|
10
12
|
|
|
13
|
+
# Type aliases for landmark data structures
|
|
14
|
+
LandmarkCoord: TypeAlias = tuple[float, float, float] # (x, y, visibility)
|
|
15
|
+
LandmarkFrame: TypeAlias = dict[str, LandmarkCoord] | None
|
|
16
|
+
LandmarkSequence: TypeAlias = list[LandmarkFrame]
|
|
17
|
+
|
|
11
18
|
|
|
12
19
|
def _extract_landmark_coordinates(
|
|
13
|
-
landmark_sequence:
|
|
20
|
+
landmark_sequence: LandmarkSequence,
|
|
14
21
|
landmark_name: str,
|
|
15
22
|
) -> tuple[list[float], list[float], list[int]]:
|
|
16
23
|
"""
|
|
@@ -38,7 +45,7 @@ def _extract_landmark_coordinates(
|
|
|
38
45
|
|
|
39
46
|
|
|
40
47
|
def _get_landmark_names(
|
|
41
|
-
landmark_sequence:
|
|
48
|
+
landmark_sequence: LandmarkSequence,
|
|
42
49
|
) -> list[str] | None:
|
|
43
50
|
"""
|
|
44
51
|
Extract landmark names from first valid frame.
|
|
@@ -56,8 +63,8 @@ def _get_landmark_names(
|
|
|
56
63
|
|
|
57
64
|
|
|
58
65
|
def _fill_missing_frames(
|
|
59
|
-
smoothed_sequence:
|
|
60
|
-
landmark_sequence:
|
|
66
|
+
smoothed_sequence: LandmarkSequence,
|
|
67
|
+
landmark_sequence: LandmarkSequence,
|
|
61
68
|
) -> None:
|
|
62
69
|
"""
|
|
63
70
|
Fill in any missing frames in smoothed sequence with original data.
|
|
@@ -75,8 +82,8 @@ def _fill_missing_frames(
|
|
|
75
82
|
|
|
76
83
|
|
|
77
84
|
def _store_smoothed_landmarks(
|
|
78
|
-
smoothed_sequence:
|
|
79
|
-
landmark_sequence:
|
|
85
|
+
smoothed_sequence: LandmarkSequence,
|
|
86
|
+
landmark_sequence: LandmarkSequence,
|
|
80
87
|
landmark_name: str,
|
|
81
88
|
x_smooth: np.ndarray,
|
|
82
89
|
y_smooth: np.ndarray,
|
|
@@ -118,11 +125,11 @@ def _store_smoothed_landmarks(
|
|
|
118
125
|
|
|
119
126
|
|
|
120
127
|
def _smooth_landmarks_core( # NOSONAR(S1172) - polyorder used via closure capture in smoother_fn
|
|
121
|
-
landmark_sequence:
|
|
128
|
+
landmark_sequence: LandmarkSequence,
|
|
122
129
|
window_length: int,
|
|
123
130
|
polyorder: int,
|
|
124
131
|
smoother_fn, # type: ignore[no-untyped-def]
|
|
125
|
-
) ->
|
|
132
|
+
) -> LandmarkSequence:
|
|
126
133
|
"""
|
|
127
134
|
Core smoothing logic shared by both standard and advanced smoothing.
|
|
128
135
|
|
|
@@ -170,10 +177,10 @@ def _smooth_landmarks_core( # NOSONAR(S1172) - polyorder used via closure captu
|
|
|
170
177
|
|
|
171
178
|
|
|
172
179
|
def smooth_landmarks(
|
|
173
|
-
landmark_sequence:
|
|
180
|
+
landmark_sequence: LandmarkSequence,
|
|
174
181
|
window_length: int = 5,
|
|
175
182
|
polyorder: int = 2,
|
|
176
|
-
) ->
|
|
183
|
+
) -> LandmarkSequence:
|
|
177
184
|
"""
|
|
178
185
|
Smooth landmark trajectories using Savitzky-Golay filter.
|
|
179
186
|
|
|
@@ -330,7 +337,7 @@ def compute_acceleration_from_derivative(
|
|
|
330
337
|
|
|
331
338
|
|
|
332
339
|
def smooth_landmarks_advanced(
|
|
333
|
-
landmark_sequence:
|
|
340
|
+
landmark_sequence: LandmarkSequence,
|
|
334
341
|
window_length: int = 5,
|
|
335
342
|
polyorder: int = 2,
|
|
336
343
|
use_outlier_rejection: bool = True,
|
|
@@ -338,7 +345,7 @@ def smooth_landmarks_advanced(
|
|
|
338
345
|
ransac_threshold: float = 0.02,
|
|
339
346
|
bilateral_sigma_spatial: float = 3.0,
|
|
340
347
|
bilateral_sigma_intensity: float = 0.02,
|
|
341
|
-
) ->
|
|
348
|
+
) -> LandmarkSequence:
|
|
342
349
|
"""
|
|
343
350
|
Advanced landmark smoothing with outlier rejection and bilateral filtering.
|
|
344
351
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"""Kinematic calculations for drop-jump metrics."""
|
|
2
2
|
|
|
3
|
+
from typing import TypedDict
|
|
4
|
+
|
|
3
5
|
import numpy as np
|
|
6
|
+
from numpy.typing import NDArray
|
|
4
7
|
|
|
5
8
|
from ..core.smoothing import compute_acceleration_from_derivative
|
|
6
9
|
from .analysis import (
|
|
@@ -12,6 +15,25 @@ from .analysis import (
|
|
|
12
15
|
)
|
|
13
16
|
|
|
14
17
|
|
|
18
|
+
class DropJumpMetricsDict(TypedDict, total=False):
|
|
19
|
+
"""Type-safe dictionary for drop jump metrics JSON output."""
|
|
20
|
+
|
|
21
|
+
ground_contact_time_ms: float | None
|
|
22
|
+
flight_time_ms: float | None
|
|
23
|
+
jump_height_m: float | None
|
|
24
|
+
jump_height_kinematic_m: float | None
|
|
25
|
+
jump_height_trajectory_normalized: float | None
|
|
26
|
+
contact_start_frame: int | None
|
|
27
|
+
contact_end_frame: int | None
|
|
28
|
+
flight_start_frame: int | None
|
|
29
|
+
flight_end_frame: int | None
|
|
30
|
+
peak_height_frame: int | None
|
|
31
|
+
contact_start_frame_precise: float | None
|
|
32
|
+
contact_end_frame_precise: float | None
|
|
33
|
+
flight_start_frame_precise: float | None
|
|
34
|
+
flight_end_frame_precise: float | None
|
|
35
|
+
|
|
36
|
+
|
|
15
37
|
class DropJumpMetrics:
|
|
16
38
|
"""Container for drop-jump analysis metrics."""
|
|
17
39
|
|
|
@@ -32,7 +54,7 @@ class DropJumpMetrics:
|
|
|
32
54
|
self.flight_start_frame_precise: float | None = None
|
|
33
55
|
self.flight_end_frame_precise: float | None = None
|
|
34
56
|
|
|
35
|
-
def to_dict(self) ->
|
|
57
|
+
def to_dict(self) -> DropJumpMetricsDict:
|
|
36
58
|
"""Convert metrics to dictionary for JSON output."""
|
|
37
59
|
return {
|
|
38
60
|
"ground_contact_time_ms": (
|
|
@@ -108,7 +130,7 @@ class DropJumpMetrics:
|
|
|
108
130
|
|
|
109
131
|
def _determine_drop_start_frame(
|
|
110
132
|
drop_start_frame: int | None,
|
|
111
|
-
foot_y_positions: np.
|
|
133
|
+
foot_y_positions: NDArray[np.float64],
|
|
112
134
|
fps: float,
|
|
113
135
|
smoothing_window: int,
|
|
114
136
|
) -> int:
|
|
@@ -170,7 +192,7 @@ def _identify_main_contact_phase(
|
|
|
170
192
|
phases: list[tuple[int, int, ContactState]],
|
|
171
193
|
ground_phases: list[tuple[int, int, int]],
|
|
172
194
|
air_phases_indexed: list[tuple[int, int, int]],
|
|
173
|
-
foot_y_positions: np.
|
|
195
|
+
foot_y_positions: NDArray[np.float64],
|
|
174
196
|
) -> tuple[int, int, bool]:
|
|
175
197
|
"""Identify the main contact phase and determine if it's a drop jump.
|
|
176
198
|
|
|
@@ -260,7 +282,7 @@ def _analyze_flight_phase(
|
|
|
260
282
|
phases: list[tuple[int, int, ContactState]],
|
|
261
283
|
interpolated_phases: list[tuple[float, float, ContactState]],
|
|
262
284
|
contact_end: int,
|
|
263
|
-
foot_y_positions: np.
|
|
285
|
+
foot_y_positions: NDArray[np.float64],
|
|
264
286
|
fps: float,
|
|
265
287
|
smoothing_window: int,
|
|
266
288
|
polyorder: int,
|
|
@@ -341,7 +363,7 @@ def _analyze_flight_phase(
|
|
|
341
363
|
|
|
342
364
|
def calculate_drop_jump_metrics(
|
|
343
365
|
contact_states: list[ContactState],
|
|
344
|
-
foot_y_positions: np.
|
|
366
|
+
foot_y_positions: NDArray[np.float64],
|
|
345
367
|
fps: float,
|
|
346
368
|
drop_start_frame: int | None = None,
|
|
347
369
|
velocity_threshold: float = 0.02,
|