kinemotion 0.20.1__tar.gz → 0.20.2__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.1 → kinemotion-0.20.2}/CHANGELOG.md +8 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/PKG-INFO +1 -1
- {kinemotion-0.20.1 → kinemotion-0.20.2}/pyproject.toml +1 -1
- kinemotion-0.20.2/tests/test_aspect_ratio.py +341 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/uv.lock +1 -1
- kinemotion-0.20.1/tests/test_aspect_ratio.py +0 -138
- {kinemotion-0.20.1 → kinemotion-0.20.2}/.dockerignore +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/.github/pull_request_template.md +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/.github/workflows/docs.yml +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/.github/workflows/release.yml +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/.github/workflows/test.yml +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/.gitignore +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/.pre-commit-config.yaml +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/.readthedocs.yml +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/.tool-versions +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/CLAUDE.md +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/CODE_OF_CONDUCT.md +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/CONTRIBUTING.md +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/Dockerfile +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/GEMINI.md +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/LICENSE +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/README.md +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/SECURITY.md +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/README.md +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/api/cmj.md +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/api/core.md +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/api/dropjump.md +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/api/overview.md +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/development/errors-findings.md +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/development/validation-plan.md +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/development/wallball-norep-detection.md +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/guides/bulk-processing.md +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/guides/camera-setup.md +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/guides/cmj-guide.md +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/index.md +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/reference/parameters.md +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/reference/pose-systems.md +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/research/sports-biomechanics-pose-estimation.md +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/technical/framerate.md +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/technical/imu-metadata.md +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/technical/real-time-analysis.md +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/technical/triple-extension.md +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/docs/translations/es/camera-setup.md +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/examples/bulk/README.md +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/examples/bulk/bulk_processing.py +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/examples/bulk/simple_example.py +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/examples/programmatic_usage.py +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/mkdocs.yml +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/requirements-docs.txt +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/samples/cmjs/README.md +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/sonar-project.properties +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/__init__.py +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/api.py +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/cli.py +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/cmj/__init__.py +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/cmj/analysis.py +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/cmj/cli.py +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/cmj/debug_overlay.py +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/cmj/joint_angles.py +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/cmj/kinematics.py +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/core/__init__.py +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/core/auto_tuning.py +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/core/cli_utils.py +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/core/debug_overlay_utils.py +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/core/filtering.py +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/core/pose.py +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/core/smoothing.py +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/core/video_io.py +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/dropjump/__init__.py +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/dropjump/analysis.py +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/dropjump/cli.py +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/dropjump/debug_overlay.py +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/dropjump/kinematics.py +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/src/kinemotion/py.typed +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/tests/__init__.py +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/tests/test_adaptive_threshold.py +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/tests/test_api.py +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/tests/test_cli_imports.py +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/tests/test_cmj_analysis.py +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/tests/test_cmj_kinematics.py +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/tests/test_com_estimation.py +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/tests/test_contact_detection.py +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/tests/test_filtering.py +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/tests/test_joint_angles.py +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/tests/test_kinematics.py +0 -0
- {kinemotion-0.20.1 → kinemotion-0.20.2}/tests/test_polyorder.py +0 -0
|
@@ -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.20.2 (2025-11-10)
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
- Achieve 80%+ coverage on video_io for SonarCloud quality gate
|
|
15
|
+
([`ed77fdb`](https://github.com/feniix/kinemotion/commit/ed77fdb080f143c492c724c9f4a138b2a364ad7e))
|
|
16
|
+
|
|
17
|
+
|
|
10
18
|
## v0.20.1 (2025-11-10)
|
|
11
19
|
|
|
12
20
|
### Bug Fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kinemotion
|
|
3
|
-
Version: 0.20.
|
|
3
|
+
Version: 0.20.2
|
|
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
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""Test that aspect ratio is preserved from source video."""
|
|
2
|
+
|
|
3
|
+
import tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
|
|
7
|
+
import cv2
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from kinemotion.core.video_io import VideoProcessor
|
|
12
|
+
from kinemotion.dropjump.debug_overlay import DebugOverlayRenderer
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def create_test_video(
|
|
16
|
+
width: int, height: int, fps: float = 30.0, num_frames: int = 10
|
|
17
|
+
) -> str:
|
|
18
|
+
"""Create a test video with specified dimensions."""
|
|
19
|
+
temp_file = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False)
|
|
20
|
+
temp_path = temp_file.name
|
|
21
|
+
temp_file.close()
|
|
22
|
+
|
|
23
|
+
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
|
|
24
|
+
writer = cv2.VideoWriter(temp_path, fourcc, fps, (width, height))
|
|
25
|
+
|
|
26
|
+
rng = np.random.default_rng(42)
|
|
27
|
+
for _ in range(num_frames):
|
|
28
|
+
# Create a random frame
|
|
29
|
+
frame = rng.integers(0, 255, (height, width, 3), dtype=np.uint8)
|
|
30
|
+
writer.write(frame)
|
|
31
|
+
|
|
32
|
+
writer.release()
|
|
33
|
+
return temp_path
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_aspect_ratio_16_9():
|
|
37
|
+
"""Test 16:9 aspect ratio video."""
|
|
38
|
+
# Create test video with 16:9 aspect ratio
|
|
39
|
+
test_video = create_test_video(1920, 1080)
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
# Read video
|
|
43
|
+
video = VideoProcessor(test_video)
|
|
44
|
+
assert video.width == 1920
|
|
45
|
+
assert video.height == 1080
|
|
46
|
+
video.close()
|
|
47
|
+
|
|
48
|
+
# Create output video
|
|
49
|
+
output_path = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False).name
|
|
50
|
+
renderer = DebugOverlayRenderer(output_path, 1920, 1080, 1920, 1080, 30.0)
|
|
51
|
+
|
|
52
|
+
# Write test frame
|
|
53
|
+
test_frame = np.zeros((1080, 1920, 3), dtype=np.uint8)
|
|
54
|
+
renderer.write_frame(test_frame)
|
|
55
|
+
renderer.close()
|
|
56
|
+
|
|
57
|
+
# Verify output dimensions
|
|
58
|
+
cap = cv2.VideoCapture(output_path)
|
|
59
|
+
ret, frame = cap.read()
|
|
60
|
+
assert ret
|
|
61
|
+
assert frame.shape[0] == 1080 # height
|
|
62
|
+
assert frame.shape[1] == 1920 # width
|
|
63
|
+
cap.release()
|
|
64
|
+
|
|
65
|
+
Path(output_path).unlink()
|
|
66
|
+
|
|
67
|
+
finally:
|
|
68
|
+
Path(test_video).unlink()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_aspect_ratio_4_3():
|
|
72
|
+
"""Test 4:3 aspect ratio video."""
|
|
73
|
+
# Create test video with 4:3 aspect ratio
|
|
74
|
+
test_video = create_test_video(640, 480)
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
video = VideoProcessor(test_video)
|
|
78
|
+
assert video.width == 640
|
|
79
|
+
assert video.height == 480
|
|
80
|
+
video.close()
|
|
81
|
+
|
|
82
|
+
finally:
|
|
83
|
+
Path(test_video).unlink()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_aspect_ratio_9_16_portrait():
|
|
87
|
+
"""Test 9:16 portrait aspect ratio video."""
|
|
88
|
+
# Create test video with portrait aspect ratio
|
|
89
|
+
test_video = create_test_video(1080, 1920)
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
video = VideoProcessor(test_video)
|
|
93
|
+
assert video.width == 1080
|
|
94
|
+
assert video.height == 1920
|
|
95
|
+
video.close()
|
|
96
|
+
|
|
97
|
+
finally:
|
|
98
|
+
Path(test_video).unlink()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_frame_dimension_validation():
|
|
102
|
+
"""Test that mismatched frame dimensions raise an error."""
|
|
103
|
+
output_path = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False).name
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
renderer = DebugOverlayRenderer(output_path, 1920, 1080, 1920, 1080, 30.0)
|
|
107
|
+
|
|
108
|
+
# Try to write frame with wrong dimensions
|
|
109
|
+
wrong_frame = np.zeros(
|
|
110
|
+
(1080, 1080, 3), dtype=np.uint8
|
|
111
|
+
) # Square instead of 16:9
|
|
112
|
+
|
|
113
|
+
with pytest.raises(ValueError, match="don't match"):
|
|
114
|
+
renderer.write_frame(wrong_frame)
|
|
115
|
+
|
|
116
|
+
renderer.close()
|
|
117
|
+
|
|
118
|
+
finally:
|
|
119
|
+
Path(output_path).unlink(missing_ok=True)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_ffprobe_not_found_warning():
|
|
123
|
+
"""Test that warning is shown when ffprobe is not available."""
|
|
124
|
+
test_video = create_test_video(640, 480)
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
# Mock subprocess.run to raise FileNotFoundError (ffprobe not found)
|
|
128
|
+
with patch(
|
|
129
|
+
"subprocess.run", side_effect=FileNotFoundError("ffprobe not found")
|
|
130
|
+
):
|
|
131
|
+
with pytest.warns(
|
|
132
|
+
UserWarning, match="ffprobe not found.*rotation and aspect ratio"
|
|
133
|
+
):
|
|
134
|
+
video = VideoProcessor(test_video)
|
|
135
|
+
video.close()
|
|
136
|
+
|
|
137
|
+
finally:
|
|
138
|
+
Path(test_video).unlink()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_ffprobe_timeout_silent():
|
|
142
|
+
"""Test that ffprobe timeout is handled silently."""
|
|
143
|
+
import subprocess
|
|
144
|
+
|
|
145
|
+
test_video = create_test_video(640, 480)
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
# Mock subprocess.run to raise TimeoutExpired
|
|
149
|
+
with patch(
|
|
150
|
+
"subprocess.run",
|
|
151
|
+
side_effect=subprocess.TimeoutExpired("ffprobe", timeout=5),
|
|
152
|
+
):
|
|
153
|
+
# Should not raise exception or warning, just continue
|
|
154
|
+
video = VideoProcessor(test_video)
|
|
155
|
+
assert video.rotation == 0 # Default rotation
|
|
156
|
+
video.close()
|
|
157
|
+
|
|
158
|
+
finally:
|
|
159
|
+
Path(test_video).unlink()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_ffprobe_json_decode_error_silent():
|
|
163
|
+
"""Test that ffprobe JSON decode error is handled silently."""
|
|
164
|
+
import json
|
|
165
|
+
|
|
166
|
+
test_video = create_test_video(640, 480)
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
# Mock subprocess.run to raise JSONDecodeError
|
|
170
|
+
with patch(
|
|
171
|
+
"subprocess.run",
|
|
172
|
+
side_effect=json.JSONDecodeError("Invalid JSON", "", 0),
|
|
173
|
+
):
|
|
174
|
+
# Should not raise exception or warning, just continue
|
|
175
|
+
video = VideoProcessor(test_video)
|
|
176
|
+
assert video.rotation == 0 # Default rotation
|
|
177
|
+
video.close()
|
|
178
|
+
|
|
179
|
+
finally:
|
|
180
|
+
Path(test_video).unlink()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def test_video_not_found():
|
|
184
|
+
"""Test that VideoProcessor raises ValueError for non-existent video."""
|
|
185
|
+
with pytest.raises(ValueError, match="Could not open video"):
|
|
186
|
+
VideoProcessor("/nonexistent/path/to/video.mp4")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def test_ffprobe_returncode_error():
|
|
190
|
+
"""Test that ffprobe non-zero returncode is handled silently."""
|
|
191
|
+
from unittest.mock import MagicMock
|
|
192
|
+
|
|
193
|
+
test_video = create_test_video(640, 480)
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
# Mock subprocess.run to return non-zero returncode
|
|
197
|
+
mock_result = MagicMock()
|
|
198
|
+
mock_result.returncode = 1
|
|
199
|
+
mock_result.stdout = ""
|
|
200
|
+
|
|
201
|
+
with patch("subprocess.run", return_value=mock_result):
|
|
202
|
+
# Should continue silently with defaults
|
|
203
|
+
video = VideoProcessor(test_video)
|
|
204
|
+
assert video.rotation == 0
|
|
205
|
+
video.close()
|
|
206
|
+
|
|
207
|
+
finally:
|
|
208
|
+
Path(test_video).unlink()
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def test_ffprobe_empty_streams():
|
|
212
|
+
"""Test that ffprobe with no streams is handled silently."""
|
|
213
|
+
from unittest.mock import MagicMock
|
|
214
|
+
|
|
215
|
+
test_video = create_test_video(640, 480)
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
# Mock subprocess.run to return empty streams
|
|
219
|
+
mock_result = MagicMock()
|
|
220
|
+
mock_result.returncode = 0
|
|
221
|
+
mock_result.stdout = '{"streams": []}'
|
|
222
|
+
|
|
223
|
+
with patch("subprocess.run", return_value=mock_result):
|
|
224
|
+
# Should continue silently with defaults
|
|
225
|
+
video = VideoProcessor(test_video)
|
|
226
|
+
assert video.rotation == 0
|
|
227
|
+
video.close()
|
|
228
|
+
|
|
229
|
+
finally:
|
|
230
|
+
Path(test_video).unlink()
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def test_video_rotation_90_degrees():
|
|
234
|
+
"""Test video rotation handling for 90 degree rotation."""
|
|
235
|
+
from unittest.mock import MagicMock
|
|
236
|
+
|
|
237
|
+
test_video = create_test_video(640, 480)
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
# Mock ffprobe to return 90 degree rotation
|
|
241
|
+
mock_result = MagicMock()
|
|
242
|
+
mock_result.returncode = 0
|
|
243
|
+
mock_result.stdout = """{
|
|
244
|
+
"streams": [{
|
|
245
|
+
"sample_aspect_ratio": "1:1",
|
|
246
|
+
"side_data_list": [{
|
|
247
|
+
"side_data_type": "Display Matrix",
|
|
248
|
+
"rotation": 90
|
|
249
|
+
}]
|
|
250
|
+
}]
|
|
251
|
+
}"""
|
|
252
|
+
|
|
253
|
+
with patch("subprocess.run", return_value=mock_result):
|
|
254
|
+
video = VideoProcessor(test_video)
|
|
255
|
+
assert video.rotation == 90
|
|
256
|
+
|
|
257
|
+
# Read frame and verify rotation is applied
|
|
258
|
+
frame = video.read_frame()
|
|
259
|
+
assert frame is not None
|
|
260
|
+
# After 90° rotation: width and height should be swapped
|
|
261
|
+
assert frame.shape[1] == 480 # Original height becomes width
|
|
262
|
+
assert frame.shape[0] == 640 # Original width becomes height
|
|
263
|
+
|
|
264
|
+
video.close()
|
|
265
|
+
|
|
266
|
+
finally:
|
|
267
|
+
Path(test_video).unlink()
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def test_video_rotation_negative_90_degrees():
|
|
271
|
+
"""Test video rotation handling for -90 degree rotation."""
|
|
272
|
+
from unittest.mock import MagicMock
|
|
273
|
+
|
|
274
|
+
test_video = create_test_video(640, 480)
|
|
275
|
+
|
|
276
|
+
try:
|
|
277
|
+
# Mock ffprobe to return -90 degree rotation
|
|
278
|
+
mock_result = MagicMock()
|
|
279
|
+
mock_result.returncode = 0
|
|
280
|
+
mock_result.stdout = """{
|
|
281
|
+
"streams": [{
|
|
282
|
+
"sample_aspect_ratio": "1:1",
|
|
283
|
+
"side_data_list": [{
|
|
284
|
+
"side_data_type": "Display Matrix",
|
|
285
|
+
"rotation": -90
|
|
286
|
+
}]
|
|
287
|
+
}]
|
|
288
|
+
}"""
|
|
289
|
+
|
|
290
|
+
with patch("subprocess.run", return_value=mock_result):
|
|
291
|
+
video = VideoProcessor(test_video)
|
|
292
|
+
assert video.rotation == -90
|
|
293
|
+
|
|
294
|
+
# Read frame and verify rotation is applied
|
|
295
|
+
frame = video.read_frame()
|
|
296
|
+
assert frame is not None
|
|
297
|
+
# After -90° rotation: dimensions swapped
|
|
298
|
+
assert frame.shape[1] == 480
|
|
299
|
+
assert frame.shape[0] == 640
|
|
300
|
+
|
|
301
|
+
video.close()
|
|
302
|
+
|
|
303
|
+
finally:
|
|
304
|
+
Path(test_video).unlink()
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def test_video_rotation_180_degrees():
|
|
308
|
+
"""Test video rotation handling for 180 degree rotation."""
|
|
309
|
+
from unittest.mock import MagicMock
|
|
310
|
+
|
|
311
|
+
test_video = create_test_video(640, 480)
|
|
312
|
+
|
|
313
|
+
try:
|
|
314
|
+
# Mock ffprobe to return 180 degree rotation
|
|
315
|
+
mock_result = MagicMock()
|
|
316
|
+
mock_result.returncode = 0
|
|
317
|
+
mock_result.stdout = """{
|
|
318
|
+
"streams": [{
|
|
319
|
+
"sample_aspect_ratio": "1:1",
|
|
320
|
+
"side_data_list": [{
|
|
321
|
+
"side_data_type": "Display Matrix",
|
|
322
|
+
"rotation": 180
|
|
323
|
+
}]
|
|
324
|
+
}]
|
|
325
|
+
}"""
|
|
326
|
+
|
|
327
|
+
with patch("subprocess.run", return_value=mock_result):
|
|
328
|
+
video = VideoProcessor(test_video)
|
|
329
|
+
assert video.rotation == 180
|
|
330
|
+
|
|
331
|
+
# Read frame and verify rotation is applied
|
|
332
|
+
frame = video.read_frame()
|
|
333
|
+
assert frame is not None
|
|
334
|
+
# After 180° rotation: dimensions stay the same
|
|
335
|
+
assert frame.shape[1] == 640
|
|
336
|
+
assert frame.shape[0] == 480
|
|
337
|
+
|
|
338
|
+
video.close()
|
|
339
|
+
|
|
340
|
+
finally:
|
|
341
|
+
Path(test_video).unlink()
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
"""Test that aspect ratio is preserved from source video."""
|
|
2
|
-
|
|
3
|
-
import tempfile
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from unittest.mock import patch
|
|
6
|
-
|
|
7
|
-
import cv2
|
|
8
|
-
import numpy as np
|
|
9
|
-
import pytest
|
|
10
|
-
|
|
11
|
-
from kinemotion.core.video_io import VideoProcessor
|
|
12
|
-
from kinemotion.dropjump.debug_overlay import DebugOverlayRenderer
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def create_test_video(
|
|
16
|
-
width: int, height: int, fps: float = 30.0, num_frames: int = 10
|
|
17
|
-
) -> str:
|
|
18
|
-
"""Create a test video with specified dimensions."""
|
|
19
|
-
temp_file = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False)
|
|
20
|
-
temp_path = temp_file.name
|
|
21
|
-
temp_file.close()
|
|
22
|
-
|
|
23
|
-
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
|
|
24
|
-
writer = cv2.VideoWriter(temp_path, fourcc, fps, (width, height))
|
|
25
|
-
|
|
26
|
-
rng = np.random.default_rng(42)
|
|
27
|
-
for _ in range(num_frames):
|
|
28
|
-
# Create a random frame
|
|
29
|
-
frame = rng.integers(0, 255, (height, width, 3), dtype=np.uint8)
|
|
30
|
-
writer.write(frame)
|
|
31
|
-
|
|
32
|
-
writer.release()
|
|
33
|
-
return temp_path
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def test_aspect_ratio_16_9():
|
|
37
|
-
"""Test 16:9 aspect ratio video."""
|
|
38
|
-
# Create test video with 16:9 aspect ratio
|
|
39
|
-
test_video = create_test_video(1920, 1080)
|
|
40
|
-
|
|
41
|
-
try:
|
|
42
|
-
# Read video
|
|
43
|
-
video = VideoProcessor(test_video)
|
|
44
|
-
assert video.width == 1920
|
|
45
|
-
assert video.height == 1080
|
|
46
|
-
video.close()
|
|
47
|
-
|
|
48
|
-
# Create output video
|
|
49
|
-
output_path = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False).name
|
|
50
|
-
renderer = DebugOverlayRenderer(output_path, 1920, 1080, 1920, 1080, 30.0)
|
|
51
|
-
|
|
52
|
-
# Write test frame
|
|
53
|
-
test_frame = np.zeros((1080, 1920, 3), dtype=np.uint8)
|
|
54
|
-
renderer.write_frame(test_frame)
|
|
55
|
-
renderer.close()
|
|
56
|
-
|
|
57
|
-
# Verify output dimensions
|
|
58
|
-
cap = cv2.VideoCapture(output_path)
|
|
59
|
-
ret, frame = cap.read()
|
|
60
|
-
assert ret
|
|
61
|
-
assert frame.shape[0] == 1080 # height
|
|
62
|
-
assert frame.shape[1] == 1920 # width
|
|
63
|
-
cap.release()
|
|
64
|
-
|
|
65
|
-
Path(output_path).unlink()
|
|
66
|
-
|
|
67
|
-
finally:
|
|
68
|
-
Path(test_video).unlink()
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def test_aspect_ratio_4_3():
|
|
72
|
-
"""Test 4:3 aspect ratio video."""
|
|
73
|
-
# Create test video with 4:3 aspect ratio
|
|
74
|
-
test_video = create_test_video(640, 480)
|
|
75
|
-
|
|
76
|
-
try:
|
|
77
|
-
video = VideoProcessor(test_video)
|
|
78
|
-
assert video.width == 640
|
|
79
|
-
assert video.height == 480
|
|
80
|
-
video.close()
|
|
81
|
-
|
|
82
|
-
finally:
|
|
83
|
-
Path(test_video).unlink()
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def test_aspect_ratio_9_16_portrait():
|
|
87
|
-
"""Test 9:16 portrait aspect ratio video."""
|
|
88
|
-
# Create test video with portrait aspect ratio
|
|
89
|
-
test_video = create_test_video(1080, 1920)
|
|
90
|
-
|
|
91
|
-
try:
|
|
92
|
-
video = VideoProcessor(test_video)
|
|
93
|
-
assert video.width == 1080
|
|
94
|
-
assert video.height == 1920
|
|
95
|
-
video.close()
|
|
96
|
-
|
|
97
|
-
finally:
|
|
98
|
-
Path(test_video).unlink()
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def test_frame_dimension_validation():
|
|
102
|
-
"""Test that mismatched frame dimensions raise an error."""
|
|
103
|
-
output_path = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False).name
|
|
104
|
-
|
|
105
|
-
try:
|
|
106
|
-
renderer = DebugOverlayRenderer(output_path, 1920, 1080, 1920, 1080, 30.0)
|
|
107
|
-
|
|
108
|
-
# Try to write frame with wrong dimensions
|
|
109
|
-
wrong_frame = np.zeros(
|
|
110
|
-
(1080, 1080, 3), dtype=np.uint8
|
|
111
|
-
) # Square instead of 16:9
|
|
112
|
-
|
|
113
|
-
with pytest.raises(ValueError, match="don't match"):
|
|
114
|
-
renderer.write_frame(wrong_frame)
|
|
115
|
-
|
|
116
|
-
renderer.close()
|
|
117
|
-
|
|
118
|
-
finally:
|
|
119
|
-
Path(output_path).unlink(missing_ok=True)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
def test_ffprobe_not_found_warning():
|
|
123
|
-
"""Test that warning is shown when ffprobe is not available."""
|
|
124
|
-
test_video = create_test_video(640, 480)
|
|
125
|
-
|
|
126
|
-
try:
|
|
127
|
-
# Mock subprocess.run to raise FileNotFoundError (ffprobe not found)
|
|
128
|
-
with patch(
|
|
129
|
-
"subprocess.run", side_effect=FileNotFoundError("ffprobe not found")
|
|
130
|
-
):
|
|
131
|
-
with pytest.warns(
|
|
132
|
-
UserWarning, match="ffprobe not found.*rotation and aspect ratio"
|
|
133
|
-
):
|
|
134
|
-
video = VideoProcessor(test_video)
|
|
135
|
-
video.close()
|
|
136
|
-
|
|
137
|
-
finally:
|
|
138
|
-
Path(test_video).unlink()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kinemotion-0.20.1 → kinemotion-0.20.2}/docs/research/sports-biomechanics-pose-estimation.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|