kinemotion 0.20.0__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.

Files changed (89) hide show
  1. {kinemotion-0.20.0 → kinemotion-0.20.2}/CHANGELOG.md +16 -0
  2. {kinemotion-0.20.0 → kinemotion-0.20.2}/PKG-INFO +1 -1
  3. {kinemotion-0.20.0 → kinemotion-0.20.2}/pyproject.toml +1 -1
  4. kinemotion-0.20.2/tests/test_aspect_ratio.py +341 -0
  5. {kinemotion-0.20.0 → kinemotion-0.20.2}/uv.lock +1 -1
  6. kinemotion-0.20.0/tests/test_aspect_ratio.py +0 -119
  7. {kinemotion-0.20.0 → kinemotion-0.20.2}/.dockerignore +0 -0
  8. {kinemotion-0.20.0 → kinemotion-0.20.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  9. {kinemotion-0.20.0 → kinemotion-0.20.2}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  10. {kinemotion-0.20.0 → kinemotion-0.20.2}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  11. {kinemotion-0.20.0 → kinemotion-0.20.2}/.github/pull_request_template.md +0 -0
  12. {kinemotion-0.20.0 → kinemotion-0.20.2}/.github/workflows/docs.yml +0 -0
  13. {kinemotion-0.20.0 → kinemotion-0.20.2}/.github/workflows/release.yml +0 -0
  14. {kinemotion-0.20.0 → kinemotion-0.20.2}/.github/workflows/test.yml +0 -0
  15. {kinemotion-0.20.0 → kinemotion-0.20.2}/.gitignore +0 -0
  16. {kinemotion-0.20.0 → kinemotion-0.20.2}/.pre-commit-config.yaml +0 -0
  17. {kinemotion-0.20.0 → kinemotion-0.20.2}/.readthedocs.yml +0 -0
  18. {kinemotion-0.20.0 → kinemotion-0.20.2}/.tool-versions +0 -0
  19. {kinemotion-0.20.0 → kinemotion-0.20.2}/CLAUDE.md +0 -0
  20. {kinemotion-0.20.0 → kinemotion-0.20.2}/CODE_OF_CONDUCT.md +0 -0
  21. {kinemotion-0.20.0 → kinemotion-0.20.2}/CONTRIBUTING.md +0 -0
  22. {kinemotion-0.20.0 → kinemotion-0.20.2}/Dockerfile +0 -0
  23. {kinemotion-0.20.0 → kinemotion-0.20.2}/GEMINI.md +0 -0
  24. {kinemotion-0.20.0 → kinemotion-0.20.2}/LICENSE +0 -0
  25. {kinemotion-0.20.0 → kinemotion-0.20.2}/README.md +0 -0
  26. {kinemotion-0.20.0 → kinemotion-0.20.2}/SECURITY.md +0 -0
  27. {kinemotion-0.20.0 → kinemotion-0.20.2}/docs/README.md +0 -0
  28. {kinemotion-0.20.0 → kinemotion-0.20.2}/docs/api/cmj.md +0 -0
  29. {kinemotion-0.20.0 → kinemotion-0.20.2}/docs/api/core.md +0 -0
  30. {kinemotion-0.20.0 → kinemotion-0.20.2}/docs/api/dropjump.md +0 -0
  31. {kinemotion-0.20.0 → kinemotion-0.20.2}/docs/api/overview.md +0 -0
  32. {kinemotion-0.20.0 → kinemotion-0.20.2}/docs/development/errors-findings.md +0 -0
  33. {kinemotion-0.20.0 → kinemotion-0.20.2}/docs/development/validation-plan.md +0 -0
  34. {kinemotion-0.20.0 → kinemotion-0.20.2}/docs/development/wallball-norep-detection.md +0 -0
  35. {kinemotion-0.20.0 → kinemotion-0.20.2}/docs/guides/bulk-processing.md +0 -0
  36. {kinemotion-0.20.0 → kinemotion-0.20.2}/docs/guides/camera-setup.md +0 -0
  37. {kinemotion-0.20.0 → kinemotion-0.20.2}/docs/guides/cmj-guide.md +0 -0
  38. {kinemotion-0.20.0 → kinemotion-0.20.2}/docs/index.md +0 -0
  39. {kinemotion-0.20.0 → kinemotion-0.20.2}/docs/reference/parameters.md +0 -0
  40. {kinemotion-0.20.0 → kinemotion-0.20.2}/docs/reference/pose-systems.md +0 -0
  41. {kinemotion-0.20.0 → kinemotion-0.20.2}/docs/research/sports-biomechanics-pose-estimation.md +0 -0
  42. {kinemotion-0.20.0 → kinemotion-0.20.2}/docs/technical/framerate.md +0 -0
  43. {kinemotion-0.20.0 → kinemotion-0.20.2}/docs/technical/imu-metadata.md +0 -0
  44. {kinemotion-0.20.0 → kinemotion-0.20.2}/docs/technical/real-time-analysis.md +0 -0
  45. {kinemotion-0.20.0 → kinemotion-0.20.2}/docs/technical/triple-extension.md +0 -0
  46. {kinemotion-0.20.0 → kinemotion-0.20.2}/docs/translations/es/camera-setup.md +0 -0
  47. {kinemotion-0.20.0 → kinemotion-0.20.2}/examples/bulk/README.md +0 -0
  48. {kinemotion-0.20.0 → kinemotion-0.20.2}/examples/bulk/bulk_processing.py +0 -0
  49. {kinemotion-0.20.0 → kinemotion-0.20.2}/examples/bulk/simple_example.py +0 -0
  50. {kinemotion-0.20.0 → kinemotion-0.20.2}/examples/programmatic_usage.py +0 -0
  51. {kinemotion-0.20.0 → kinemotion-0.20.2}/mkdocs.yml +0 -0
  52. {kinemotion-0.20.0 → kinemotion-0.20.2}/requirements-docs.txt +0 -0
  53. {kinemotion-0.20.0 → kinemotion-0.20.2}/samples/cmjs/README.md +0 -0
  54. {kinemotion-0.20.0 → kinemotion-0.20.2}/sonar-project.properties +0 -0
  55. {kinemotion-0.20.0 → kinemotion-0.20.2}/src/kinemotion/__init__.py +0 -0
  56. {kinemotion-0.20.0 → kinemotion-0.20.2}/src/kinemotion/api.py +0 -0
  57. {kinemotion-0.20.0 → kinemotion-0.20.2}/src/kinemotion/cli.py +0 -0
  58. {kinemotion-0.20.0 → kinemotion-0.20.2}/src/kinemotion/cmj/__init__.py +0 -0
  59. {kinemotion-0.20.0 → kinemotion-0.20.2}/src/kinemotion/cmj/analysis.py +0 -0
  60. {kinemotion-0.20.0 → kinemotion-0.20.2}/src/kinemotion/cmj/cli.py +0 -0
  61. {kinemotion-0.20.0 → kinemotion-0.20.2}/src/kinemotion/cmj/debug_overlay.py +0 -0
  62. {kinemotion-0.20.0 → kinemotion-0.20.2}/src/kinemotion/cmj/joint_angles.py +0 -0
  63. {kinemotion-0.20.0 → kinemotion-0.20.2}/src/kinemotion/cmj/kinematics.py +0 -0
  64. {kinemotion-0.20.0 → kinemotion-0.20.2}/src/kinemotion/core/__init__.py +0 -0
  65. {kinemotion-0.20.0 → kinemotion-0.20.2}/src/kinemotion/core/auto_tuning.py +0 -0
  66. {kinemotion-0.20.0 → kinemotion-0.20.2}/src/kinemotion/core/cli_utils.py +0 -0
  67. {kinemotion-0.20.0 → kinemotion-0.20.2}/src/kinemotion/core/debug_overlay_utils.py +0 -0
  68. {kinemotion-0.20.0 → kinemotion-0.20.2}/src/kinemotion/core/filtering.py +0 -0
  69. {kinemotion-0.20.0 → kinemotion-0.20.2}/src/kinemotion/core/pose.py +0 -0
  70. {kinemotion-0.20.0 → kinemotion-0.20.2}/src/kinemotion/core/smoothing.py +0 -0
  71. {kinemotion-0.20.0 → kinemotion-0.20.2}/src/kinemotion/core/video_io.py +0 -0
  72. {kinemotion-0.20.0 → kinemotion-0.20.2}/src/kinemotion/dropjump/__init__.py +0 -0
  73. {kinemotion-0.20.0 → kinemotion-0.20.2}/src/kinemotion/dropjump/analysis.py +0 -0
  74. {kinemotion-0.20.0 → kinemotion-0.20.2}/src/kinemotion/dropjump/cli.py +0 -0
  75. {kinemotion-0.20.0 → kinemotion-0.20.2}/src/kinemotion/dropjump/debug_overlay.py +0 -0
  76. {kinemotion-0.20.0 → kinemotion-0.20.2}/src/kinemotion/dropjump/kinematics.py +0 -0
  77. {kinemotion-0.20.0 → kinemotion-0.20.2}/src/kinemotion/py.typed +0 -0
  78. {kinemotion-0.20.0 → kinemotion-0.20.2}/tests/__init__.py +0 -0
  79. {kinemotion-0.20.0 → kinemotion-0.20.2}/tests/test_adaptive_threshold.py +0 -0
  80. {kinemotion-0.20.0 → kinemotion-0.20.2}/tests/test_api.py +0 -0
  81. {kinemotion-0.20.0 → kinemotion-0.20.2}/tests/test_cli_imports.py +0 -0
  82. {kinemotion-0.20.0 → kinemotion-0.20.2}/tests/test_cmj_analysis.py +0 -0
  83. {kinemotion-0.20.0 → kinemotion-0.20.2}/tests/test_cmj_kinematics.py +0 -0
  84. {kinemotion-0.20.0 → kinemotion-0.20.2}/tests/test_com_estimation.py +0 -0
  85. {kinemotion-0.20.0 → kinemotion-0.20.2}/tests/test_contact_detection.py +0 -0
  86. {kinemotion-0.20.0 → kinemotion-0.20.2}/tests/test_filtering.py +0 -0
  87. {kinemotion-0.20.0 → kinemotion-0.20.2}/tests/test_joint_angles.py +0 -0
  88. {kinemotion-0.20.0 → kinemotion-0.20.2}/tests/test_kinematics.py +0 -0
  89. {kinemotion-0.20.0 → kinemotion-0.20.2}/tests/test_polyorder.py +0 -0
@@ -7,6 +7,22 @@ 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
+
18
+ ## v0.20.1 (2025-11-10)
19
+
20
+ ### Bug Fixes
21
+
22
+ - Add test coverage for ffprobe warning path
23
+ ([`8ae3e55`](https://github.com/feniix/kinemotion/commit/8ae3e552a3bfb749d4e9bad10c634093db5eddee))
24
+
25
+
10
26
  ## v0.20.0 (2025-11-10)
11
27
 
12
28
  ### Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kinemotion
3
- Version: 0.20.0
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kinemotion"
3
- version = "0.20.0"
3
+ version = "0.20.2"
4
4
  description = "Video-based kinematic analysis for athletic performance"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10,<3.13"
@@ -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()
@@ -700,7 +700,7 @@ wheels = [
700
700
 
701
701
  [[package]]
702
702
  name = "kinemotion"
703
- version = "0.20.0"
703
+ version = "0.20.2"
704
704
  source = { editable = "." }
705
705
  dependencies = [
706
706
  { name = "click" },
@@ -1,119 +0,0 @@
1
- """Test that aspect ratio is preserved from source video."""
2
-
3
- import tempfile
4
- from pathlib import Path
5
-
6
- import cv2
7
- import numpy as np
8
-
9
- from kinemotion.core.video_io import VideoProcessor
10
- from kinemotion.dropjump.debug_overlay import DebugOverlayRenderer
11
-
12
-
13
- def create_test_video(
14
- width: int, height: int, fps: float = 30.0, num_frames: int = 10
15
- ) -> str:
16
- """Create a test video with specified dimensions."""
17
- temp_file = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False)
18
- temp_path = temp_file.name
19
- temp_file.close()
20
-
21
- fourcc = cv2.VideoWriter_fourcc(*"mp4v")
22
- writer = cv2.VideoWriter(temp_path, fourcc, fps, (width, height))
23
-
24
- rng = np.random.default_rng(42)
25
- for _ in range(num_frames):
26
- # Create a random frame
27
- frame = rng.integers(0, 255, (height, width, 3), dtype=np.uint8)
28
- writer.write(frame)
29
-
30
- writer.release()
31
- return temp_path
32
-
33
-
34
- def test_aspect_ratio_16_9():
35
- """Test 16:9 aspect ratio video."""
36
- # Create test video with 16:9 aspect ratio
37
- test_video = create_test_video(1920, 1080)
38
-
39
- try:
40
- # Read video
41
- video = VideoProcessor(test_video)
42
- assert video.width == 1920
43
- assert video.height == 1080
44
- video.close()
45
-
46
- # Create output video
47
- output_path = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False).name
48
- renderer = DebugOverlayRenderer(output_path, 1920, 1080, 1920, 1080, 30.0)
49
-
50
- # Write test frame
51
- test_frame = np.zeros((1080, 1920, 3), dtype=np.uint8)
52
- renderer.write_frame(test_frame)
53
- renderer.close()
54
-
55
- # Verify output dimensions
56
- cap = cv2.VideoCapture(output_path)
57
- ret, frame = cap.read()
58
- assert ret
59
- assert frame.shape[0] == 1080 # height
60
- assert frame.shape[1] == 1920 # width
61
- cap.release()
62
-
63
- Path(output_path).unlink()
64
-
65
- finally:
66
- Path(test_video).unlink()
67
-
68
-
69
- def test_aspect_ratio_4_3():
70
- """Test 4:3 aspect ratio video."""
71
- # Create test video with 4:3 aspect ratio
72
- test_video = create_test_video(640, 480)
73
-
74
- try:
75
- video = VideoProcessor(test_video)
76
- assert video.width == 640
77
- assert video.height == 480
78
- video.close()
79
-
80
- finally:
81
- Path(test_video).unlink()
82
-
83
-
84
- def test_aspect_ratio_9_16_portrait():
85
- """Test 9:16 portrait aspect ratio video."""
86
- # Create test video with portrait aspect ratio
87
- test_video = create_test_video(1080, 1920)
88
-
89
- try:
90
- video = VideoProcessor(test_video)
91
- assert video.width == 1080
92
- assert video.height == 1920
93
- video.close()
94
-
95
- finally:
96
- Path(test_video).unlink()
97
-
98
-
99
- def test_frame_dimension_validation():
100
- """Test that mismatched frame dimensions raise an error."""
101
- import pytest
102
-
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)
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