kinemotion 0.64.0__py3-none-any.whl → 0.66.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of kinemotion might be problematic. Click here for more details.
- kinemotion/core/debug_overlay_utils.py +142 -10
- kinemotion/core/video_io.py +11 -0
- {kinemotion-0.64.0.dist-info → kinemotion-0.66.0.dist-info}/METADATA +1 -1
- {kinemotion-0.64.0.dist-info → kinemotion-0.66.0.dist-info}/RECORD +7 -7
- {kinemotion-0.64.0.dist-info → kinemotion-0.66.0.dist-info}/WHEEL +0 -0
- {kinemotion-0.64.0.dist-info → kinemotion-0.66.0.dist-info}/entry_points.txt +0 -0
- {kinemotion-0.64.0.dist-info → kinemotion-0.66.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Shared debug overlay utilities for video rendering."""
|
|
2
2
|
|
|
3
|
+
# pyright: reportCallIssue=false
|
|
3
4
|
import os
|
|
4
5
|
import shutil
|
|
5
6
|
import subprocess
|
|
@@ -12,6 +13,31 @@ from typing_extensions import Self
|
|
|
12
13
|
|
|
13
14
|
from .timing import NULL_TIMER, Timer
|
|
14
15
|
|
|
16
|
+
# Setup logging with structlog support for backend, fallback to standard logging for CLI
|
|
17
|
+
try:
|
|
18
|
+
import structlog
|
|
19
|
+
|
|
20
|
+
logger = structlog.get_logger(__name__)
|
|
21
|
+
_using_structlog = True
|
|
22
|
+
except ImportError:
|
|
23
|
+
import logging
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
_using_structlog = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _log(level: str, message: str, **kwargs: object) -> None:
|
|
30
|
+
"""Log message with kwargs support for both structlog and standard logging."""
|
|
31
|
+
if _using_structlog:
|
|
32
|
+
getattr(logger, level)(message, **kwargs)
|
|
33
|
+
else:
|
|
34
|
+
# For standard logging, format kwargs as part of the message
|
|
35
|
+
if kwargs:
|
|
36
|
+
kwargs_str = " ".join(f"{k}={v}" for k, v in kwargs.items())
|
|
37
|
+
getattr(logger, level)(f"{message} {kwargs_str}")
|
|
38
|
+
else:
|
|
39
|
+
getattr(logger, level)(message)
|
|
40
|
+
|
|
15
41
|
|
|
16
42
|
def create_video_writer(
|
|
17
43
|
output_path: str,
|
|
@@ -24,6 +50,10 @@ def create_video_writer(
|
|
|
24
50
|
"""
|
|
25
51
|
Create a video writer with fallback codec support.
|
|
26
52
|
|
|
53
|
+
⚠️ CRITICAL: DO NOT add "vp09" (VP9) to the codec list!
|
|
54
|
+
VP9 is not supported on iOS browsers (iPhone/iPad) and causes playback failures.
|
|
55
|
+
Regression test: tests/core/test_debug_overlay_utils.py::test_vp09_codec_never_in_codec_list
|
|
56
|
+
|
|
27
57
|
Args:
|
|
28
58
|
output_path: Path for output video
|
|
29
59
|
width: Encoded frame width (from source video)
|
|
@@ -38,13 +68,15 @@ def create_video_writer(
|
|
|
38
68
|
needs_resize = (display_width != width) or (display_height != height)
|
|
39
69
|
|
|
40
70
|
# Try browser-compatible codecs first
|
|
41
|
-
# avc1/h264: H.264 (Most compatible)
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
|
|
71
|
+
# avc1/h264: H.264 (Most compatible, including iOS)
|
|
72
|
+
# mp4v: MPEG-4 (Poor browser support, will trigger ffmpeg re-encoding for H.264)
|
|
73
|
+
# ⚠️ CRITICAL: VP9 (vp09) is EXCLUDED - not supported on iOS/iPhone/iPad browsers!
|
|
74
|
+
# Adding VP9 will break debug video playback on all iOS devices.
|
|
75
|
+
codecs_to_try = ["avc1", "h264", "mp4v"]
|
|
45
76
|
|
|
46
77
|
writer = None
|
|
47
78
|
used_codec = "mp4v" # Default fallback
|
|
79
|
+
codec_attempt_log = []
|
|
48
80
|
|
|
49
81
|
for codec in codecs_to_try:
|
|
50
82
|
try:
|
|
@@ -52,13 +84,36 @@ def create_video_writer(
|
|
|
52
84
|
writer = cv2.VideoWriter(output_path, fourcc, fps, (display_width, display_height))
|
|
53
85
|
if writer.isOpened():
|
|
54
86
|
used_codec = codec
|
|
87
|
+
codec_attempt_log.append({"codec": codec, "status": "success"})
|
|
88
|
+
_log(
|
|
89
|
+
"info",
|
|
90
|
+
"debug_video_codec_selected",
|
|
91
|
+
codec=codec,
|
|
92
|
+
width=display_width,
|
|
93
|
+
height=display_height,
|
|
94
|
+
fps=fps,
|
|
95
|
+
)
|
|
55
96
|
if codec == "mp4v":
|
|
56
|
-
|
|
97
|
+
msg = (
|
|
98
|
+
"Using fallback MPEG-4 codec; will re-encode with ffmpeg for "
|
|
99
|
+
"browser compatibility"
|
|
100
|
+
)
|
|
101
|
+
_log("warning", "debug_video_fallback_codec", codec="mp4v", message=msg)
|
|
57
102
|
break
|
|
58
|
-
except Exception:
|
|
103
|
+
except Exception as e:
|
|
104
|
+
codec_attempt_log.append({"codec": codec, "status": "failed", "error": str(e)})
|
|
105
|
+
_log("info", "debug_video_codec_attempt_failed", codec=codec, error=str(e))
|
|
59
106
|
continue
|
|
60
107
|
|
|
61
108
|
if writer is None or not writer.isOpened():
|
|
109
|
+
_log(
|
|
110
|
+
"error",
|
|
111
|
+
"debug_video_writer_creation_failed",
|
|
112
|
+
output_path=output_path,
|
|
113
|
+
dimensions=f"{display_width}x{display_height}",
|
|
114
|
+
fps=fps,
|
|
115
|
+
codec_attempts=codec_attempt_log,
|
|
116
|
+
)
|
|
62
117
|
raise ValueError(
|
|
63
118
|
f"Failed to create video writer for {output_path} with dimensions "
|
|
64
119
|
f"{display_width}x{display_height}"
|
|
@@ -130,9 +185,35 @@ class BaseDebugOverlayRenderer:
|
|
|
130
185
|
# Ensure dimensions are even for codec compatibility
|
|
131
186
|
self.display_width = int(display_width * scale) // 2 * 2
|
|
132
187
|
self.display_height = int(display_height * scale) // 2 * 2
|
|
188
|
+
_log(
|
|
189
|
+
"info",
|
|
190
|
+
"debug_video_resolution_optimized",
|
|
191
|
+
original_width=display_width,
|
|
192
|
+
original_height=display_height,
|
|
193
|
+
optimized_width=self.display_width,
|
|
194
|
+
optimized_height=self.display_height,
|
|
195
|
+
scale_factor=round(scale, 2),
|
|
196
|
+
)
|
|
133
197
|
else:
|
|
134
198
|
self.display_width = display_width
|
|
135
199
|
self.display_height = display_height
|
|
200
|
+
_log(
|
|
201
|
+
"info",
|
|
202
|
+
"debug_video_resolution_native",
|
|
203
|
+
width=self.display_width,
|
|
204
|
+
height=self.display_height,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
_log(
|
|
208
|
+
"info",
|
|
209
|
+
"debug_overlay_renderer_initialized",
|
|
210
|
+
output_path=output_path,
|
|
211
|
+
source_width=width,
|
|
212
|
+
source_height=height,
|
|
213
|
+
output_width=self.display_width,
|
|
214
|
+
output_height=self.display_height,
|
|
215
|
+
fps=fps,
|
|
216
|
+
)
|
|
136
217
|
|
|
137
218
|
# Duration of ffmpeg re-encoding (0.0 if not needed)
|
|
138
219
|
self.reencode_duration_s = 0.0
|
|
@@ -174,6 +255,12 @@ class BaseDebugOverlayRenderer:
|
|
|
174
255
|
def close(self) -> None:
|
|
175
256
|
"""Release video writer and re-encode if possible."""
|
|
176
257
|
self.writer.release()
|
|
258
|
+
_log(
|
|
259
|
+
"info",
|
|
260
|
+
"debug_video_writer_released",
|
|
261
|
+
output_path=self.output_path,
|
|
262
|
+
codec=self.used_codec,
|
|
263
|
+
)
|
|
177
264
|
|
|
178
265
|
# Post-process with ffmpeg ONLY if we fell back to the incompatible mp4v codec
|
|
179
266
|
if self.used_codec == "mp4v" and shutil.which("ffmpeg"):
|
|
@@ -187,7 +274,7 @@ class BaseDebugOverlayRenderer:
|
|
|
187
274
|
# -y: Overwrite output file
|
|
188
275
|
# -vcodec libx264: Use H.264 codec
|
|
189
276
|
# -pix_fmt yuv420p: Required for wide browser support (Chrome,
|
|
190
|
-
# Safari, Firefox)
|
|
277
|
+
# Safari, Firefox, iOS)
|
|
191
278
|
# -preset fast: Reasonable speed/compression tradeoff
|
|
192
279
|
# -crf 23: Standard quality
|
|
193
280
|
# -an: Remove audio (debug video has no audio)
|
|
@@ -208,6 +295,16 @@ class BaseDebugOverlayRenderer:
|
|
|
208
295
|
temp_path,
|
|
209
296
|
]
|
|
210
297
|
|
|
298
|
+
_log(
|
|
299
|
+
"info",
|
|
300
|
+
"debug_video_ffmpeg_reencoding_start",
|
|
301
|
+
input_file=self.output_path,
|
|
302
|
+
output_file=temp_path,
|
|
303
|
+
output_codec="libx264",
|
|
304
|
+
pixel_format="yuv420p",
|
|
305
|
+
reason="iOS_compatibility",
|
|
306
|
+
)
|
|
307
|
+
|
|
211
308
|
# Suppress output unless error
|
|
212
309
|
reencode_start = time.time()
|
|
213
310
|
subprocess.run(
|
|
@@ -217,19 +314,54 @@ class BaseDebugOverlayRenderer:
|
|
|
217
314
|
stderr=subprocess.PIPE,
|
|
218
315
|
)
|
|
219
316
|
self.reencode_duration_s = time.time() - reencode_start
|
|
220
|
-
|
|
317
|
+
|
|
318
|
+
_log(
|
|
319
|
+
"info",
|
|
320
|
+
"debug_video_ffmpeg_reencoding_complete",
|
|
321
|
+
duration_ms=round(self.reencode_duration_s * 1000, 1),
|
|
322
|
+
)
|
|
221
323
|
|
|
222
324
|
# Overwrite original file
|
|
223
325
|
os.replace(temp_path, self.output_path)
|
|
326
|
+
_log(
|
|
327
|
+
"info",
|
|
328
|
+
"debug_video_reencoded_file_replaced",
|
|
329
|
+
output_path=self.output_path,
|
|
330
|
+
final_codec="libx264",
|
|
331
|
+
pixel_format="yuv420p",
|
|
332
|
+
)
|
|
224
333
|
|
|
225
334
|
except subprocess.CalledProcessError as e:
|
|
226
|
-
|
|
335
|
+
stderr_msg = e.stderr.decode("utf-8", errors="ignore") if e.stderr else "N/A"
|
|
336
|
+
_log(
|
|
337
|
+
"warning",
|
|
338
|
+
"debug_video_ffmpeg_reencoding_failed",
|
|
339
|
+
error=str(e),
|
|
340
|
+
stderr=stderr_msg,
|
|
341
|
+
)
|
|
227
342
|
if temp_path and os.path.exists(temp_path):
|
|
228
343
|
os.remove(temp_path)
|
|
344
|
+
_log("info", "debug_video_temp_file_cleaned_up", temp_file=temp_path)
|
|
229
345
|
except Exception as e:
|
|
230
|
-
|
|
346
|
+
_log("warning", "debug_video_post_processing_error", error=str(e))
|
|
231
347
|
if temp_path and os.path.exists(temp_path):
|
|
232
348
|
os.remove(temp_path)
|
|
349
|
+
_log("info", "debug_video_temp_file_cleaned_up", temp_file=temp_path)
|
|
350
|
+
elif self.used_codec == "mp4v" and not shutil.which("ffmpeg"):
|
|
351
|
+
_log(
|
|
352
|
+
"warning",
|
|
353
|
+
"debug_video_ffmpeg_not_available",
|
|
354
|
+
codec=self.used_codec,
|
|
355
|
+
output_path=self.output_path,
|
|
356
|
+
warning="Video may not play in all browsers",
|
|
357
|
+
)
|
|
358
|
+
else:
|
|
359
|
+
_log(
|
|
360
|
+
"info",
|
|
361
|
+
"debug_video_ready_for_playback",
|
|
362
|
+
codec=self.used_codec,
|
|
363
|
+
path=self.output_path,
|
|
364
|
+
)
|
|
233
365
|
|
|
234
366
|
def __enter__(self) -> Self:
|
|
235
367
|
return self
|
kinemotion/core/video_io.py
CHANGED
|
@@ -203,6 +203,17 @@ class VideoProcessor:
|
|
|
203
203
|
"""Release video capture."""
|
|
204
204
|
self.cap.release()
|
|
205
205
|
|
|
206
|
+
def __iter__(self) -> "VideoProcessor":
|
|
207
|
+
"""Make the processor iterable."""
|
|
208
|
+
return self
|
|
209
|
+
|
|
210
|
+
def __next__(self) -> np.ndarray:
|
|
211
|
+
"""Get the next frame during iteration."""
|
|
212
|
+
frame = self.read_frame()
|
|
213
|
+
if frame is None:
|
|
214
|
+
raise StopIteration
|
|
215
|
+
return frame
|
|
216
|
+
|
|
206
217
|
def __enter__(self) -> "VideoProcessor":
|
|
207
218
|
return self
|
|
208
219
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kinemotion
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.66.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
|
|
@@ -13,7 +13,7 @@ kinemotion/cmj/validation_bounds.py,sha256=Ry915JdInPXbqjaVGNY_urnDO1PAkCSJqHwNK
|
|
|
13
13
|
kinemotion/core/__init__.py,sha256=U2fnLUGXQ0jbwpXhdksYKDXbeQndEHjn9gwTAEJ9Av0,1451
|
|
14
14
|
kinemotion/core/auto_tuning.py,sha256=lhAqPc-eLjMYx9BCvKdECE7TD2Dweb9KcifV6JHaXOE,11278
|
|
15
15
|
kinemotion/core/cli_utils.py,sha256=sQPbT6XWWau-sm9yuN5c3eS5xNzoQGGXwSz6hQXtRvM,1859
|
|
16
|
-
kinemotion/core/debug_overlay_utils.py,sha256=
|
|
16
|
+
kinemotion/core/debug_overlay_utils.py,sha256=Rbmuslc-imondbljiMrQ08JRxaw9TfyFi75nA17sZ9w,13390
|
|
17
17
|
kinemotion/core/determinism.py,sha256=Frw-KAOvAxTL_XtxoWpXCjMbQPUKEAusK6JctlkeuRo,2509
|
|
18
18
|
kinemotion/core/experimental.py,sha256=IK05AF4aZS15ke85hF3TWCqRIXU1AlD_XKzFz735Ua8,3640
|
|
19
19
|
kinemotion/core/filtering.py,sha256=Oc__pV6iHEGyyovbqa5SUi-6v8QyvaRVwA0LRayM884,11355
|
|
@@ -26,7 +26,7 @@ kinemotion/core/smoothing.py,sha256=ELMHL7pzSqYffjnLDBUMBJIgt1AwOssDInE8IiXBbig,
|
|
|
26
26
|
kinemotion/core/timing.py,sha256=d1rjZc07Nbi5Jrio9AC-zeS0dNAlbPyNIydLz7X75Pk,7804
|
|
27
27
|
kinemotion/core/types.py,sha256=A_HclzKpf3By5DiJ0wY9B-dQJrIVAAhUfGab7qTSIL8,1279
|
|
28
28
|
kinemotion/core/validation.py,sha256=0xVv-ftWveV60fJ97kmZMuy2Qqqb5aZLR50dDIrjnhg,6773
|
|
29
|
-
kinemotion/core/video_io.py,sha256=
|
|
29
|
+
kinemotion/core/video_io.py,sha256=ufbwfYkxuerat4ab6Hn1tkI5T9TAOvHwREIazJ0Q2tE,8174
|
|
30
30
|
kinemotion/dropjump/__init__.py,sha256=tC3H3BrCg8Oj-db-Vrtx4PH_llR1Ppkd5jwaOjhQcLg,862
|
|
31
31
|
kinemotion/dropjump/analysis.py,sha256=YomuoJF_peyrBSpeT89Q5_sBgY0kEDyq7TFrtEnRLjs,28049
|
|
32
32
|
kinemotion/dropjump/api.py,sha256=uidio49CXisyWKd287CnCrM51GusG9DWAIUKGH85fpM,20584
|
|
@@ -36,8 +36,8 @@ kinemotion/dropjump/kinematics.py,sha256=dx4PuXKfKMKcsc_HX6sXj8rHXf9ksiZIOAIkJ4v
|
|
|
36
36
|
kinemotion/dropjump/metrics_validator.py,sha256=lSfo4Lm5FHccl8ijUP6SA-kcSh50LS9hF8UIyWxcnW8,9243
|
|
37
37
|
kinemotion/dropjump/validation_bounds.py,sha256=x4yjcFxyvdMp5e7MkcoUosGLeGsxBh1Lft6h__AQ2G8,5124
|
|
38
38
|
kinemotion/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
39
|
-
kinemotion-0.
|
|
40
|
-
kinemotion-0.
|
|
41
|
-
kinemotion-0.
|
|
42
|
-
kinemotion-0.
|
|
43
|
-
kinemotion-0.
|
|
39
|
+
kinemotion-0.66.0.dist-info/METADATA,sha256=-fRqQu9C9z3U5xlHHgDeTwIeURdrbv9Kyt1e5hoMrUA,26061
|
|
40
|
+
kinemotion-0.66.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
41
|
+
kinemotion-0.66.0.dist-info/entry_points.txt,sha256=zaqnAnjLvcdrk1Qvj5nvXZCZ2gp0prS7it1zTJygcIY,50
|
|
42
|
+
kinemotion-0.66.0.dist-info/licenses/LICENSE,sha256=KZajvqsHw0NoOHOi2q0FZ4NBe9HdV6oey-IPYAtHXfg,1088
|
|
43
|
+
kinemotion-0.66.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|