kinemotion 0.11.7__tar.gz → 0.12.1__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.11.7 → kinemotion-0.12.1}/.pre-commit-config.yaml +1 -1
- {kinemotion-0.11.7 → kinemotion-0.12.1}/CHANGELOG.md +45 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/CLAUDE.md +2 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/PKG-INFO +1 -1
- {kinemotion-0.11.7 → kinemotion-0.12.1}/docs/CAMERA_SETUP.md +1 -1
- {kinemotion-0.11.7 → kinemotion-0.12.1}/docs/CAMERA_SETUP_ES.md +1 -1
- {kinemotion-0.11.7 → kinemotion-0.12.1}/docs/CMJ_GUIDE.md +6 -6
- {kinemotion-0.11.7 → kinemotion-0.12.1}/docs/REAL_TIME_ANALYSIS.md +4 -4
- {kinemotion-0.11.7 → kinemotion-0.12.1}/docs/TRIPLE_EXTENSION.md +10 -10
- {kinemotion-0.11.7 → kinemotion-0.12.1}/examples/bulk/bulk_processing.py +3 -10
- {kinemotion-0.11.7 → kinemotion-0.12.1}/examples/bulk/simple_example.py +5 -7
- {kinemotion-0.11.7 → kinemotion-0.12.1}/pyproject.toml +1 -1
- {kinemotion-0.11.7 → kinemotion-0.12.1}/src/kinemotion/api.py +6 -11
- {kinemotion-0.11.7 → kinemotion-0.12.1}/src/kinemotion/cmj/debug_overlay.py +9 -12
- {kinemotion-0.11.7 → kinemotion-0.12.1}/src/kinemotion/core/cli_utils.py +7 -101
- {kinemotion-0.11.7 → kinemotion-0.12.1}/src/kinemotion/core/debug_overlay_utils.py +1 -24
- {kinemotion-0.11.7 → kinemotion-0.12.1}/src/kinemotion/core/video_io.py +1 -5
- {kinemotion-0.11.7 → kinemotion-0.12.1}/src/kinemotion/dropjump/analysis.py +69 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/src/kinemotion/dropjump/cli.py +5 -26
- {kinemotion-0.11.7 → kinemotion-0.12.1}/src/kinemotion/dropjump/kinematics.py +34 -136
- {kinemotion-0.11.7 → kinemotion-0.12.1}/tests/test_api.py +15 -21
- {kinemotion-0.11.7 → kinemotion-0.12.1}/tests/test_kinematics.py +7 -5
- {kinemotion-0.11.7 → kinemotion-0.12.1}/uv.lock +1 -1
- {kinemotion-0.11.7 → kinemotion-0.12.1}/.dockerignore +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/.github/pull_request_template.md +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/.github/workflows/release.yml +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/.gitignore +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/.tool-versions +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/CODE_OF_CONDUCT.md +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/CONTRIBUTING.md +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/Dockerfile +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/GEMINI.md +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/LICENSE +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/README.md +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/SECURITY.md +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/docs/BULK_PROCESSING.md +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/docs/ERRORS_FINDINGS.md +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/docs/FRAMERATE.md +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/docs/IMU_METADATA_PRESERVATION.md +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/docs/PARAMETERS.md +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/docs/VALIDATION_PLAN.md +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/examples/bulk/README.md +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/examples/programmatic_usage.py +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/samples/cmjs/README.md +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/src/kinemotion/__init__.py +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/src/kinemotion/cli.py +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/src/kinemotion/cmj/__init__.py +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/src/kinemotion/cmj/analysis.py +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/src/kinemotion/cmj/cli.py +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/src/kinemotion/cmj/joint_angles.py +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/src/kinemotion/cmj/kinematics.py +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/src/kinemotion/core/__init__.py +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/src/kinemotion/core/auto_tuning.py +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/src/kinemotion/core/filtering.py +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/src/kinemotion/core/pose.py +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/src/kinemotion/core/smoothing.py +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/src/kinemotion/dropjump/__init__.py +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/src/kinemotion/dropjump/debug_overlay.py +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/src/kinemotion/py.typed +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/tests/__init__.py +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/tests/test_adaptive_threshold.py +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/tests/test_aspect_ratio.py +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/tests/test_cmj_analysis.py +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/tests/test_cmj_kinematics.py +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/tests/test_com_estimation.py +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/tests/test_contact_detection.py +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/tests/test_filtering.py +0 -0
- {kinemotion-0.11.7 → kinemotion-0.12.1}/tests/test_polyorder.py +0 -0
|
@@ -39,7 +39,7 @@ repos:
|
|
|
39
39
|
additional_dependencies:
|
|
40
40
|
- mdformat-gfm>=0.3.5 # GitHub Flavored Markdown
|
|
41
41
|
- mdformat-tables # Table formatting
|
|
42
|
-
exclude: ^CLAUDE\.md$
|
|
42
|
+
exclude: (^CLAUDE\.md$|^CHANGELOG\.md$)
|
|
43
43
|
|
|
44
44
|
- repo: https://github.com/compilerla/conventional-pre-commit
|
|
45
45
|
rev: v4.3.0
|
|
@@ -7,6 +7,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
<!-- version list -->
|
|
9
9
|
|
|
10
|
+
## v0.12.1 (2025-11-06)
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
- **core**: Remove unreachable duplicate return statement
|
|
15
|
+
([`294115d`](https://github.com/feniix/kinemotion/commit/294115da761b2851ecc4405a6503138851a56ad1))
|
|
16
|
+
|
|
17
|
+
- **examples**: Remove drop_height from API examples
|
|
18
|
+
([`f3da09e`](https://github.com/feniix/kinemotion/commit/f3da09ef4ab050b13b80b9fdd8c7734e4556647a))
|
|
19
|
+
|
|
20
|
+
### Refactoring
|
|
21
|
+
|
|
22
|
+
- **dropjump**: Remove unused calibration parameters
|
|
23
|
+
([`1a7572c`](https://github.com/feniix/kinemotion/commit/1a7572c83ff4e990e39dcb96ff61220adf40818e))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
## v0.12.0 (2025-11-06)
|
|
27
|
+
|
|
28
|
+
### Documentation
|
|
29
|
+
|
|
30
|
+
- Update claude.md
|
|
31
|
+
([`b4d93d9`](https://github.com/feniix/kinemotion/commit/b4d93d94259fbfe86101c256910fcfc07c8dfcc2))
|
|
32
|
+
|
|
33
|
+
### Features
|
|
34
|
+
|
|
35
|
+
- **dropjump**: Calculate jump height from flight time like CMJ
|
|
36
|
+
([`f7d96a2`](https://github.com/feniix/kinemotion/commit/f7d96a253b287d58215fd64bd1e598784cb098f4))
|
|
37
|
+
|
|
38
|
+
- **dropjump**: Improve landing detection with position stabilization
|
|
39
|
+
([`6d19938`](https://github.com/feniix/kinemotion/commit/6d199382485a80a975911c51444b2c18aa32c428))
|
|
40
|
+
|
|
41
|
+
### Refactoring
|
|
42
|
+
|
|
43
|
+
- **core**: Remove unused code and fix vulture warnings
|
|
44
|
+
([`16328e2`](https://github.com/feniix/kinemotion/commit/16328e299a0e15f7f0f0e87d133e1f662dc59d0b))
|
|
45
|
+
|
|
46
|
+
- **core**: Rename AutoTunedParams to AnalysisParameters for consistency
|
|
47
|
+
([`2b6e59b`](https://github.com/feniix/kinemotion/commit/2b6e59b832769224b600e23bf4141af5d6159169))
|
|
48
|
+
|
|
49
|
+
### Testing
|
|
50
|
+
|
|
51
|
+
- Update tests for kinematic-based height calculation
|
|
52
|
+
([`308469e`](https://github.com/feniix/kinemotion/commit/308469e978c53a971a4a20352cfffd72a3c9e6cd))
|
|
53
|
+
|
|
54
|
+
|
|
10
55
|
## v0.11.7 (2025-11-06)
|
|
11
56
|
|
|
12
57
|
### Bug Fixes
|
|
@@ -254,6 +254,8 @@ chore(release): 0.11.0 [skip ci]
|
|
|
254
254
|
feat!: change API signature for process_video
|
|
255
255
|
```
|
|
256
256
|
|
|
257
|
+
**Important**: Commit messages must never reference Claude or AI assistance. Keep messages professional and focused on the technical changes.
|
|
258
|
+
|
|
257
259
|
## MCP Servers
|
|
258
260
|
|
|
259
261
|
Configured in `.mcp.json`: web-search, sequential-thinking, context7, etc.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kinemotion
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12.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
|
|
@@ -12,7 +12,7 @@ Proper camera positioning is critical for accurate drop jump analysis. The curre
|
|
|
12
12
|
|
|
13
13
|
### Required Camera Position
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Camera must be positioned at a side view angle, perpendicular to the sagittal plane (90°).
|
|
16
16
|
|
|
17
17
|
#### Camera Positioning Diagram
|
|
18
18
|
|
|
@@ -12,7 +12,7 @@ El posicionamiento adecuado de la cámara es crítico para un análisis preciso
|
|
|
12
12
|
|
|
13
13
|
### Posición Requerida de la Cámara
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
La cámara debe posicionarse en ángulo de vista lateral, perpendicular al plano sagital (90°).
|
|
16
16
|
|
|
17
17
|
#### Diagrama de Posicionamiento de Cámara
|
|
18
18
|
|
|
@@ -61,7 +61,7 @@ print(f"Eccentric duration: {metrics.eccentric_duration*1000:.0f}ms")
|
|
|
61
61
|
|
|
62
62
|
### Movement Characteristics
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
1. **Countermovement Depth** (m) - Vertical distance during eccentric phase
|
|
65
65
|
|
|
66
66
|
- Represents how deep the athlete squats
|
|
67
67
|
- Typical range: 0.20-0.40m
|
|
@@ -90,7 +90,7 @@ print(f"Eccentric duration: {metrics.eccentric_duration*1000:.0f}ms")
|
|
|
90
90
|
|
|
91
91
|
### Velocity Profile
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
1. **Peak Eccentric Velocity** (m/s) - Maximum downward speed
|
|
94
94
|
|
|
95
95
|
- Indicates countermovement speed
|
|
96
96
|
- Typical range: 0.5-1.5 m/s
|
|
@@ -102,10 +102,10 @@ print(f"Eccentric duration: {metrics.eccentric_duration*1000:.0f}ms")
|
|
|
102
102
|
|
|
103
103
|
### Triple Extension (in debug video)
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
1. **Ankle Angle** - Dorsiflexion/plantarflexion
|
|
106
|
+
1. **Knee Angle** - Flexion/extension
|
|
107
|
+
1. **Hip Angle** - Flexion/extension
|
|
108
|
+
1. **Trunk Tilt** - Forward/backward lean
|
|
109
109
|
|
|
110
110
|
**Note**: Ankle/knee angles have limited visibility in lateral view videos (~20-30% of frames). Trunk angle is available throughout. See docs/TRIPLE_EXTENSION.md for details.
|
|
111
111
|
|
|
@@ -244,7 +244,7 @@ ______________________________________________________________________
|
|
|
244
244
|
|
|
245
245
|
**Test video**: 236 frames @ 29.58fps
|
|
246
246
|
|
|
247
|
-
```
|
|
247
|
+
```text
|
|
248
248
|
Processing time breakdown:
|
|
249
249
|
- MediaPipe tracking: ~5-6 seconds
|
|
250
250
|
- Smoothing: ~0.1 seconds
|
|
@@ -681,7 +681,7 @@ ______________________________________________________________________
|
|
|
681
681
|
|
|
682
682
|
## Recommendation Matrix
|
|
683
683
|
|
|
684
|
-
### Choose Offline (Current) If
|
|
684
|
+
### Choose Offline (Current) If
|
|
685
685
|
|
|
686
686
|
- ✅ Maximum accuracy required (research, validation)
|
|
687
687
|
- ✅ Processing pre-recorded videos
|
|
@@ -689,7 +689,7 @@ ______________________________________________________________________
|
|
|
689
689
|
- ✅ Want triple extension with full coverage
|
|
690
690
|
- ✅ Publication-quality data needed
|
|
691
691
|
|
|
692
|
-
### Choose Near Real-Time If
|
|
692
|
+
### Choose Near Real-Time If
|
|
693
693
|
|
|
694
694
|
- ✅ Need quick results (1-2 sec acceptable)
|
|
695
695
|
- ✅ Coaching/training applications
|
|
@@ -697,7 +697,7 @@ ______________________________________________________________________
|
|
|
697
697
|
- ✅ Want to maintain accuracy
|
|
698
698
|
- ✅ Building mobile/web app
|
|
699
699
|
|
|
700
|
-
### Choose True Real-Time If
|
|
700
|
+
### Choose True Real-Time If
|
|
701
701
|
|
|
702
702
|
- ⚠️ Instant feedback critical (\<100ms)
|
|
703
703
|
- ⚠️ Interactive applications (games, VR)
|
|
@@ -49,7 +49,7 @@ The CMJ debug video now includes **triple extension tracking** - real-time visua
|
|
|
49
49
|
|
|
50
50
|
**At Lowest Point (Countermovement Bottom):**
|
|
51
51
|
|
|
52
|
-
```
|
|
52
|
+
```text
|
|
53
53
|
Ankle: 70-90° (neutral to slight dorsiflexion)
|
|
54
54
|
Knee: 90-110° (moderate squat)
|
|
55
55
|
Hip: 90-110° (hip flexion)
|
|
@@ -58,7 +58,7 @@ Trunk: 0-20° (slight forward lean)
|
|
|
58
58
|
|
|
59
59
|
**At Takeoff (Leaving Ground):**
|
|
60
60
|
|
|
61
|
-
```
|
|
61
|
+
```text
|
|
62
62
|
Ankle: 110-130° (strong plantarflexion)
|
|
63
63
|
Knee: 160-180° (near full extension)
|
|
64
64
|
Hip: 170-180° (full extension)
|
|
@@ -67,7 +67,7 @@ Trunk: 0-10° (nearly vertical)
|
|
|
67
67
|
|
|
68
68
|
**During Flight:**
|
|
69
69
|
|
|
70
|
-
```
|
|
70
|
+
```text
|
|
71
71
|
All joints: ~180° (full extension)
|
|
72
72
|
```
|
|
73
73
|
|
|
@@ -139,23 +139,23 @@ All joints: ~180° (full extension)
|
|
|
139
139
|
|
|
140
140
|
### Poor Extension Patterns
|
|
141
141
|
|
|
142
|
-
|
|
142
|
+
#### Problem 1: Incomplete knee extension
|
|
143
143
|
|
|
144
|
-
```
|
|
144
|
+
```text
|
|
145
145
|
Takeoff: Ankle 120°, Knee 150°, Hip 175°
|
|
146
146
|
→ Power leak: Not fully utilizing leg strength
|
|
147
147
|
```
|
|
148
148
|
|
|
149
|
-
|
|
149
|
+
#### Problem 2: Sequential extension (not simultaneous)
|
|
150
150
|
|
|
151
|
-
```
|
|
151
|
+
```text
|
|
152
152
|
Early concentric: Hip 170°, Knee 120°, Ankle 80°
|
|
153
153
|
→ Poor coordination: Extending in sequence instead of together
|
|
154
154
|
```
|
|
155
155
|
|
|
156
|
-
|
|
156
|
+
#### Problem 3: Excessive trunk lean
|
|
157
157
|
|
|
158
|
-
```
|
|
158
|
+
```text
|
|
159
159
|
Takeoff: Trunk 30° forward
|
|
160
160
|
→ Sub-optimal: Reduces vertical force component
|
|
161
161
|
```
|
|
@@ -272,7 +272,7 @@ The triple extension feature has been tested with:
|
|
|
272
272
|
|
|
273
273
|
**Debug video shows:**
|
|
274
274
|
|
|
275
|
-
```
|
|
275
|
+
```text
|
|
276
276
|
Frame 140-155 (Concentric phase):
|
|
277
277
|
┌─────────────────────┐
|
|
278
278
|
│ TRIPLE EXTENSION │
|
|
@@ -18,9 +18,9 @@ def example_simple_bulk() -> None:
|
|
|
18
18
|
print("=" * 80)
|
|
19
19
|
|
|
20
20
|
video_configs = [
|
|
21
|
-
VideoConfig(video_path="video1.mp4"
|
|
22
|
-
VideoConfig(video_path="video2.mp4"
|
|
23
|
-
VideoConfig(video_path="video3.mp4"
|
|
21
|
+
VideoConfig(video_path="video1.mp4"),
|
|
22
|
+
VideoConfig(video_path="video2.mp4"),
|
|
23
|
+
VideoConfig(video_path="video3.mp4"),
|
|
24
24
|
]
|
|
25
25
|
|
|
26
26
|
# Process videos with 4 parallel workers
|
|
@@ -41,21 +41,18 @@ def example_advanced_configuration() -> None:
|
|
|
41
41
|
# Fast analysis for quick screening
|
|
42
42
|
VideoConfig(
|
|
43
43
|
video_path="athlete1_trial1.mp4",
|
|
44
|
-
drop_height=0.40,
|
|
45
44
|
quality="fast",
|
|
46
45
|
json_output="results/athlete1_trial1.json",
|
|
47
46
|
),
|
|
48
47
|
# Balanced analysis (default)
|
|
49
48
|
VideoConfig(
|
|
50
49
|
video_path="athlete1_trial2.mp4",
|
|
51
|
-
drop_height=0.40,
|
|
52
50
|
quality="balanced",
|
|
53
51
|
json_output="results/athlete1_trial2.json",
|
|
54
52
|
),
|
|
55
53
|
# Research-grade accurate analysis with debug video
|
|
56
54
|
VideoConfig(
|
|
57
55
|
video_path="athlete1_trial3.mp4",
|
|
58
|
-
drop_height=0.40,
|
|
59
56
|
quality="accurate",
|
|
60
57
|
output_video="debug/athlete1_trial3_debug.mp4",
|
|
61
58
|
json_output="results/athlete1_trial3.json",
|
|
@@ -101,7 +98,6 @@ def example_process_directory() -> list[VideoResult]:
|
|
|
101
98
|
dir_configs = [
|
|
102
99
|
VideoConfig(
|
|
103
100
|
video_path=str(video_file),
|
|
104
|
-
drop_height=0.40,
|
|
105
101
|
quality="balanced",
|
|
106
102
|
json_output=f"results/{video_file.stem}.json",
|
|
107
103
|
)
|
|
@@ -187,7 +183,6 @@ def example_custom_parameters() -> None:
|
|
|
187
183
|
# Low quality video - use more aggressive smoothing
|
|
188
184
|
VideoConfig(
|
|
189
185
|
video_path="low_quality.mp4",
|
|
190
|
-
drop_height=0.40,
|
|
191
186
|
smoothing_window=7, # More smoothing
|
|
192
187
|
velocity_threshold=0.025, # Higher threshold
|
|
193
188
|
quality="accurate",
|
|
@@ -195,7 +190,6 @@ def example_custom_parameters() -> None:
|
|
|
195
190
|
# High speed video - adjust for higher framerate
|
|
196
191
|
VideoConfig(
|
|
197
192
|
video_path="high_speed_120fps.mp4",
|
|
198
|
-
drop_height=0.40,
|
|
199
193
|
quality="accurate",
|
|
200
194
|
# Auto-tuning will handle FPS adjustments
|
|
201
195
|
),
|
|
@@ -290,7 +284,6 @@ def example_single_video() -> None:
|
|
|
290
284
|
# Process single video with verbose output
|
|
291
285
|
metrics = process_video(
|
|
292
286
|
video_path="sample.mp4",
|
|
293
|
-
drop_height=0.40,
|
|
294
287
|
quality="balanced",
|
|
295
288
|
output_video="sample_debug.mp4",
|
|
296
289
|
json_output="sample_results.json",
|
|
@@ -12,10 +12,9 @@ def process_single_video_example() -> None:
|
|
|
12
12
|
"""Process a single video - the simplest usage."""
|
|
13
13
|
print("Processing single video...")
|
|
14
14
|
|
|
15
|
-
# Process with just the
|
|
15
|
+
# Process with just the video path
|
|
16
16
|
metrics = process_video(
|
|
17
17
|
video_path="my_video.mp4",
|
|
18
|
-
drop_height=0.40, # 40cm drop box
|
|
19
18
|
verbose=True,
|
|
20
19
|
)
|
|
21
20
|
|
|
@@ -36,10 +35,10 @@ def process_multiple_videos_example() -> None:
|
|
|
36
35
|
|
|
37
36
|
# Configure videos to process
|
|
38
37
|
configs = [
|
|
39
|
-
VideoConfig("athlete1_jump1.mp4"
|
|
40
|
-
VideoConfig("athlete1_jump2.mp4"
|
|
41
|
-
VideoConfig("athlete1_jump3.mp4"
|
|
42
|
-
VideoConfig("athlete2_jump1.mp4",
|
|
38
|
+
VideoConfig("athlete1_jump1.mp4"),
|
|
39
|
+
VideoConfig("athlete1_jump2.mp4"),
|
|
40
|
+
VideoConfig("athlete1_jump3.mp4"),
|
|
41
|
+
VideoConfig("athlete2_jump1.mp4", quality="accurate"),
|
|
43
42
|
]
|
|
44
43
|
|
|
45
44
|
# Process all videos using 4 parallel workers
|
|
@@ -74,7 +73,6 @@ def process_with_outputs_example() -> None:
|
|
|
74
73
|
|
|
75
74
|
metrics = process_video(
|
|
76
75
|
video_path="my_video.mp4",
|
|
77
|
-
drop_height=0.40,
|
|
78
76
|
output_video="debug_output.mp4", # Save annotated video
|
|
79
77
|
json_output="results.json", # Save metrics as JSON
|
|
80
78
|
quality="accurate", # Use highest quality analysis
|
|
@@ -337,7 +337,6 @@ class VideoConfig:
|
|
|
337
337
|
"""Configuration for processing a single video."""
|
|
338
338
|
|
|
339
339
|
video_path: str
|
|
340
|
-
drop_height: float
|
|
341
340
|
quality: str = "balanced"
|
|
342
341
|
output_video: str | None = None
|
|
343
342
|
json_output: str | None = None
|
|
@@ -352,7 +351,6 @@ class VideoConfig:
|
|
|
352
351
|
|
|
353
352
|
def process_video(
|
|
354
353
|
video_path: str,
|
|
355
|
-
drop_height: float,
|
|
356
354
|
quality: str = "balanced",
|
|
357
355
|
output_video: str | None = None,
|
|
358
356
|
json_output: str | None = None,
|
|
@@ -368,9 +366,10 @@ def process_video(
|
|
|
368
366
|
"""
|
|
369
367
|
Process a single drop jump video and return metrics.
|
|
370
368
|
|
|
369
|
+
Jump height is calculated from flight time using kinematic formula (h = g*t²/8).
|
|
370
|
+
|
|
371
371
|
Args:
|
|
372
372
|
video_path: Path to the input video file
|
|
373
|
-
drop_height: Height of drop box/platform in meters (e.g., 0.40 for 40cm)
|
|
374
373
|
quality: Analysis quality preset ("fast", "balanced", or "accurate")
|
|
375
374
|
output_video: Optional path for debug video output
|
|
376
375
|
json_output: Optional path for JSON metrics output
|
|
@@ -459,15 +458,12 @@ def process_video(
|
|
|
459
458
|
# Calculate metrics
|
|
460
459
|
if verbose:
|
|
461
460
|
print("Calculating metrics...")
|
|
462
|
-
print(
|
|
463
|
-
f"Using drop height calibration: {drop_height}m ({drop_height*100:.0f}cm)"
|
|
464
|
-
)
|
|
465
461
|
|
|
466
462
|
metrics = calculate_drop_jump_metrics(
|
|
467
463
|
contact_states,
|
|
468
464
|
vertical_positions,
|
|
469
465
|
video.fps,
|
|
470
|
-
drop_height_m=
|
|
466
|
+
drop_height_m=None,
|
|
471
467
|
drop_start_frame=drop_start_frame,
|
|
472
468
|
velocity_threshold=params.velocity_threshold,
|
|
473
469
|
smoothing_window=params.smoothing_window,
|
|
@@ -513,9 +509,9 @@ def process_videos_bulk(
|
|
|
513
509
|
|
|
514
510
|
Example:
|
|
515
511
|
>>> configs = [
|
|
516
|
-
... VideoConfig("video1.mp4"
|
|
517
|
-
... VideoConfig("video2.mp4",
|
|
518
|
-
... VideoConfig("video3.mp4",
|
|
512
|
+
... VideoConfig("video1.mp4"),
|
|
513
|
+
... VideoConfig("video2.mp4", quality="accurate"),
|
|
514
|
+
... VideoConfig("video3.mp4", output_video="debug3.mp4"),
|
|
519
515
|
... ]
|
|
520
516
|
>>> results = process_videos_bulk(configs, max_workers=4)
|
|
521
517
|
>>> for result in results:
|
|
@@ -573,7 +569,6 @@ def _process_video_wrapper(config: VideoConfig) -> VideoResult:
|
|
|
573
569
|
try:
|
|
574
570
|
metrics = process_video(
|
|
575
571
|
video_path=config.video_path,
|
|
576
|
-
drop_height=config.drop_height,
|
|
577
572
|
quality=config.quality,
|
|
578
573
|
output_video=config.output_video,
|
|
579
574
|
json_output=config.json_output,
|
|
@@ -242,18 +242,15 @@ class CMJDebugOverlayRenderer(BaseDebugOverlayRenderer):
|
|
|
242
242
|
y_offset += 30
|
|
243
243
|
|
|
244
244
|
# Draw angle arcs at joints for visual feedback (only if angle is available)
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if
|
|
250
|
-
self._draw_angle_arc(
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
self._draw_angle_arc(
|
|
255
|
-
frame, landmarks, f"{side_used}_hip", angles["hip_angle"]
|
|
256
|
-
)
|
|
245
|
+
ankle_angle = angles.get("ankle_angle")
|
|
246
|
+
if ankle_angle is not None:
|
|
247
|
+
self._draw_angle_arc(frame, landmarks, f"{side_used}_ankle", ankle_angle)
|
|
248
|
+
knee_angle = angles.get("knee_angle")
|
|
249
|
+
if knee_angle is not None:
|
|
250
|
+
self._draw_angle_arc(frame, landmarks, f"{side_used}_knee", knee_angle)
|
|
251
|
+
hip_angle = angles.get("hip_angle")
|
|
252
|
+
if hip_angle is not None:
|
|
253
|
+
self._draw_angle_arc(frame, landmarks, f"{side_used}_hip", hip_angle)
|
|
257
254
|
|
|
258
255
|
def _draw_angle_arc(
|
|
259
256
|
self,
|
|
@@ -5,7 +5,7 @@ from typing import Any, Protocol
|
|
|
5
5
|
|
|
6
6
|
import click
|
|
7
7
|
|
|
8
|
-
from .auto_tuning import
|
|
8
|
+
from .auto_tuning import AnalysisParameters, QualityPreset, VideoCharacteristics
|
|
9
9
|
from .pose import PoseTracker
|
|
10
10
|
from .smoothing import smooth_landmarks, smooth_landmarks_advanced
|
|
11
11
|
from .video_io import VideoProcessor
|
|
@@ -85,8 +85,8 @@ def track_all_frames(video: VideoProcessor, tracker: PoseTracker) -> tuple[list,
|
|
|
85
85
|
|
|
86
86
|
|
|
87
87
|
def apply_expert_param_overrides(
|
|
88
|
-
params:
|
|
89
|
-
) ->
|
|
88
|
+
params: AnalysisParameters, expert_params: ExpertParameters
|
|
89
|
+
) -> AnalysisParameters:
|
|
90
90
|
"""Apply expert parameter overrides to auto-tuned parameters.
|
|
91
91
|
|
|
92
92
|
Args:
|
|
@@ -110,7 +110,7 @@ def apply_expert_param_overrides(
|
|
|
110
110
|
def print_auto_tuned_params(
|
|
111
111
|
video: VideoProcessor,
|
|
112
112
|
quality_preset: QualityPreset,
|
|
113
|
-
params:
|
|
113
|
+
params: AnalysisParameters,
|
|
114
114
|
characteristics: VideoCharacteristics | None = None,
|
|
115
115
|
extra_params: dict[str, Any] | None = None,
|
|
116
116
|
) -> None:
|
|
@@ -159,7 +159,9 @@ def print_auto_tuned_params(
|
|
|
159
159
|
click.echo("=" * 60 + "\n", err=True)
|
|
160
160
|
|
|
161
161
|
|
|
162
|
-
def smooth_landmark_sequence(
|
|
162
|
+
def smooth_landmark_sequence(
|
|
163
|
+
landmarks_sequence: list, params: AnalysisParameters
|
|
164
|
+
) -> list:
|
|
163
165
|
"""Apply smoothing to landmark sequence.
|
|
164
166
|
|
|
165
167
|
Args:
|
|
@@ -208,99 +210,3 @@ def common_output_options(func: Callable) -> Callable: # type: ignore[type-arg]
|
|
|
208
210
|
help="Path for JSON metrics output (default: stdout)",
|
|
209
211
|
)(func)
|
|
210
212
|
return func
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
def common_quality_options(func: Callable) -> Callable: # type: ignore[type-arg]
|
|
214
|
-
"""Add quality and verbose options to CLI command."""
|
|
215
|
-
func = click.option(
|
|
216
|
-
"--quality",
|
|
217
|
-
type=click.Choice(["fast", "balanced", "accurate"], case_sensitive=False),
|
|
218
|
-
default="balanced",
|
|
219
|
-
help=(
|
|
220
|
-
"Analysis quality preset: "
|
|
221
|
-
"fast (quick, less precise), "
|
|
222
|
-
"balanced (default, good for most cases), "
|
|
223
|
-
"accurate (research-grade, slower)"
|
|
224
|
-
),
|
|
225
|
-
show_default=True,
|
|
226
|
-
)(func)
|
|
227
|
-
func = click.option(
|
|
228
|
-
"--verbose",
|
|
229
|
-
"-v",
|
|
230
|
-
is_flag=True,
|
|
231
|
-
help="Show auto-selected parameters and analysis details",
|
|
232
|
-
)(func)
|
|
233
|
-
return func
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
def common_batch_options(func: Callable) -> Callable: # type: ignore[type-arg]
|
|
237
|
-
"""Add batch processing options to CLI command."""
|
|
238
|
-
func = click.option(
|
|
239
|
-
"--batch",
|
|
240
|
-
is_flag=True,
|
|
241
|
-
help="Enable batch processing mode for multiple videos",
|
|
242
|
-
)(func)
|
|
243
|
-
func = click.option(
|
|
244
|
-
"--workers",
|
|
245
|
-
type=int,
|
|
246
|
-
default=4,
|
|
247
|
-
help="Number of parallel workers for batch processing (default: 4)",
|
|
248
|
-
show_default=True,
|
|
249
|
-
)(func)
|
|
250
|
-
func = click.option(
|
|
251
|
-
"--output-dir",
|
|
252
|
-
type=click.Path(),
|
|
253
|
-
help="Directory for debug video outputs (batch mode only)",
|
|
254
|
-
)(func)
|
|
255
|
-
func = click.option(
|
|
256
|
-
"--json-output-dir",
|
|
257
|
-
type=click.Path(),
|
|
258
|
-
help="Directory for JSON metrics outputs (batch mode only)",
|
|
259
|
-
)(func)
|
|
260
|
-
func = click.option(
|
|
261
|
-
"--csv-summary",
|
|
262
|
-
type=click.Path(),
|
|
263
|
-
help="Path for CSV summary export (batch mode only)",
|
|
264
|
-
)(func)
|
|
265
|
-
return func
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
def common_expert_options(func: Callable) -> Callable: # type: ignore[type-arg]
|
|
269
|
-
"""Add expert parameter options to CLI command."""
|
|
270
|
-
func = click.option(
|
|
271
|
-
"--smoothing-window",
|
|
272
|
-
type=int,
|
|
273
|
-
default=None,
|
|
274
|
-
help="[EXPERT] Override auto-tuned smoothing window size",
|
|
275
|
-
)(func)
|
|
276
|
-
func = click.option(
|
|
277
|
-
"--velocity-threshold",
|
|
278
|
-
type=float,
|
|
279
|
-
default=None,
|
|
280
|
-
help="[EXPERT] Override auto-tuned velocity threshold",
|
|
281
|
-
)(func)
|
|
282
|
-
func = click.option(
|
|
283
|
-
"--min-contact-frames",
|
|
284
|
-
type=int,
|
|
285
|
-
default=None,
|
|
286
|
-
help="[EXPERT] Override auto-tuned minimum contact frames",
|
|
287
|
-
)(func)
|
|
288
|
-
func = click.option(
|
|
289
|
-
"--visibility-threshold",
|
|
290
|
-
type=float,
|
|
291
|
-
default=None,
|
|
292
|
-
help="[EXPERT] Override visibility threshold for landmarks",
|
|
293
|
-
)(func)
|
|
294
|
-
func = click.option(
|
|
295
|
-
"--detection-confidence",
|
|
296
|
-
type=float,
|
|
297
|
-
default=None,
|
|
298
|
-
help="[EXPERT] Override MediaPipe detection confidence (0.0-1.0)",
|
|
299
|
-
)(func)
|
|
300
|
-
func = click.option(
|
|
301
|
-
"--tracking-confidence",
|
|
302
|
-
type=float,
|
|
303
|
-
default=None,
|
|
304
|
-
help="[EXPERT] Override MediaPipe tracking confidence (0.0-1.0)",
|
|
305
|
-
)(func)
|
|
306
|
-
return func
|
|
@@ -48,29 +48,6 @@ def create_video_writer(
|
|
|
48
48
|
return writer, needs_resize
|
|
49
49
|
|
|
50
50
|
|
|
51
|
-
def prepare_frame_for_overlay(
|
|
52
|
-
frame: np.ndarray, needs_resize: bool, display_width: int, display_height: int
|
|
53
|
-
) -> np.ndarray:
|
|
54
|
-
"""
|
|
55
|
-
Prepare frame for overlay rendering by resizing if needed.
|
|
56
|
-
|
|
57
|
-
Args:
|
|
58
|
-
frame: Original video frame
|
|
59
|
-
needs_resize: Whether frame needs resizing
|
|
60
|
-
display_width: Target display width
|
|
61
|
-
display_height: Target display height
|
|
62
|
-
|
|
63
|
-
Returns:
|
|
64
|
-
Prepared frame ready for overlay
|
|
65
|
-
"""
|
|
66
|
-
# Apply SAR correction if needed
|
|
67
|
-
if needs_resize:
|
|
68
|
-
frame = cv2.resize(
|
|
69
|
-
frame, (display_width, display_height), interpolation=cv2.INTER_LINEAR
|
|
70
|
-
)
|
|
71
|
-
return frame
|
|
72
|
-
|
|
73
|
-
|
|
74
51
|
def write_overlay_frame(
|
|
75
52
|
writer: cv2.VideoWriter, frame: np.ndarray, width: int, height: int
|
|
76
53
|
) -> None:
|
|
@@ -162,5 +139,5 @@ class BaseDebugOverlayRenderer:
|
|
|
162
139
|
def __enter__(self) -> "BaseDebugOverlayRenderer":
|
|
163
140
|
return self
|
|
164
141
|
|
|
165
|
-
def __exit__(self,
|
|
142
|
+
def __exit__(self, _exc_type, _exc_val, _exc_tb) -> None: # type: ignore[no-untyped-def]
|
|
166
143
|
self.close()
|
|
@@ -151,10 +151,6 @@ class VideoProcessor:
|
|
|
151
151
|
|
|
152
152
|
return frame
|
|
153
153
|
|
|
154
|
-
def reset(self) -> None:
|
|
155
|
-
"""Reset video to beginning."""
|
|
156
|
-
self.cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
|
157
|
-
|
|
158
154
|
def close(self) -> None:
|
|
159
155
|
"""Release video capture."""
|
|
160
156
|
self.cap.release()
|
|
@@ -162,5 +158,5 @@ class VideoProcessor:
|
|
|
162
158
|
def __enter__(self) -> "VideoProcessor":
|
|
163
159
|
return self
|
|
164
160
|
|
|
165
|
-
def __exit__(self,
|
|
161
|
+
def __exit__(self, _exc_type, _exc_val, _exc_tb) -> None: # type: ignore[no-untyped-def]
|
|
166
162
|
self.close()
|