kinemotion 0.66.2__py3-none-any.whl → 0.66.4__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/cmj/api.py +192 -113
- kinemotion/core/debug_overlay_utils.py +146 -131
- {kinemotion-0.66.2.dist-info → kinemotion-0.66.4.dist-info}/METADATA +1 -1
- {kinemotion-0.66.2.dist-info → kinemotion-0.66.4.dist-info}/RECORD +7 -7
- {kinemotion-0.66.2.dist-info → kinemotion-0.66.4.dist-info}/WHEEL +0 -0
- {kinemotion-0.66.2.dist-info → kinemotion-0.66.4.dist-info}/entry_points.txt +0 -0
- {kinemotion-0.66.2.dist-info → kinemotion-0.66.4.dist-info}/licenses/LICENSE +0 -0
kinemotion/cmj/api.py
CHANGED
|
@@ -214,6 +214,169 @@ def _create_result_metadata(
|
|
|
214
214
|
)
|
|
215
215
|
|
|
216
216
|
|
|
217
|
+
def _run_pose_tracking(
|
|
218
|
+
video: VideoProcessor,
|
|
219
|
+
quality_preset: QualityPreset,
|
|
220
|
+
detection_confidence: float | None,
|
|
221
|
+
tracking_confidence: float | None,
|
|
222
|
+
pose_tracker: PoseTracker | None,
|
|
223
|
+
verbose: bool,
|
|
224
|
+
timer: Timer,
|
|
225
|
+
) -> tuple[list[NDArray[np.uint8]], list, list[int]]:
|
|
226
|
+
"""Initialize tracker and process all frames."""
|
|
227
|
+
if verbose:
|
|
228
|
+
print(
|
|
229
|
+
f"Video: {video.width}x{video.height} @ {video.fps:.2f} fps, "
|
|
230
|
+
f"{video.frame_count} frames"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
det_conf, track_conf = determine_confidence_levels(
|
|
234
|
+
quality_preset, detection_confidence, tracking_confidence
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
if verbose:
|
|
238
|
+
print("Processing all frames with MediaPipe pose tracking...")
|
|
239
|
+
|
|
240
|
+
tracker = pose_tracker or PoseTracker(
|
|
241
|
+
min_detection_confidence=det_conf,
|
|
242
|
+
min_tracking_confidence=track_conf,
|
|
243
|
+
timer=timer,
|
|
244
|
+
)
|
|
245
|
+
should_close_tracker = pose_tracker is None
|
|
246
|
+
|
|
247
|
+
return process_all_frames(video, tracker, verbose, timer, close_tracker=should_close_tracker)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _get_tuned_parameters(
|
|
251
|
+
video: VideoProcessor,
|
|
252
|
+
landmarks_sequence: list,
|
|
253
|
+
quality_preset: QualityPreset,
|
|
254
|
+
overrides: AnalysisOverrides | None,
|
|
255
|
+
verbose: bool,
|
|
256
|
+
timer: Timer,
|
|
257
|
+
) -> AnalysisParameters:
|
|
258
|
+
"""Analyze sample and tune parameters with expert overrides."""
|
|
259
|
+
with timer.measure("parameter_auto_tuning"):
|
|
260
|
+
characteristics = analyze_video_sample(landmarks_sequence, video.fps, video.frame_count)
|
|
261
|
+
params = auto_tune_parameters(characteristics, quality_preset)
|
|
262
|
+
params = apply_expert_overrides(
|
|
263
|
+
params,
|
|
264
|
+
overrides.smoothing_window if overrides else None,
|
|
265
|
+
overrides.velocity_threshold if overrides else None,
|
|
266
|
+
overrides.min_contact_frames if overrides else None,
|
|
267
|
+
overrides.visibility_threshold if overrides else None,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
if verbose:
|
|
271
|
+
print_verbose_parameters(video, characteristics, quality_preset, params)
|
|
272
|
+
|
|
273
|
+
return params
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _run_kinematic_analysis(
|
|
277
|
+
video: VideoProcessor,
|
|
278
|
+
smoothed_landmarks: list,
|
|
279
|
+
params: AnalysisParameters,
|
|
280
|
+
verbose: bool,
|
|
281
|
+
timer: Timer,
|
|
282
|
+
) -> tuple[CMJMetrics, NDArray[np.float64], NDArray[np.float64]]:
|
|
283
|
+
"""Extract positions, detect phases, and calculate metrics."""
|
|
284
|
+
if verbose:
|
|
285
|
+
print("Extracting vertical positions (Hip and Foot)...")
|
|
286
|
+
with timer.measure("vertical_position_extraction"):
|
|
287
|
+
vertical_positions, visibilities = extract_vertical_positions(
|
|
288
|
+
smoothed_landmarks, target="hip"
|
|
289
|
+
)
|
|
290
|
+
foot_positions, _ = extract_vertical_positions(smoothed_landmarks, target="foot")
|
|
291
|
+
|
|
292
|
+
if verbose:
|
|
293
|
+
print("Detecting CMJ phases...")
|
|
294
|
+
with timer.measure("phase_detection"):
|
|
295
|
+
phases = detect_cmj_phases(
|
|
296
|
+
vertical_positions,
|
|
297
|
+
video.fps,
|
|
298
|
+
window_length=params.smoothing_window,
|
|
299
|
+
polyorder=params.polyorder,
|
|
300
|
+
landing_positions=foot_positions,
|
|
301
|
+
timer=timer,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
if phases is None:
|
|
305
|
+
raise ValueError("Could not detect CMJ phases in video")
|
|
306
|
+
|
|
307
|
+
standing_end, lowest_point, takeoff_frame, landing_frame = phases
|
|
308
|
+
|
|
309
|
+
if verbose:
|
|
310
|
+
print("Calculating metrics...")
|
|
311
|
+
with timer.measure("metrics_calculation"):
|
|
312
|
+
velocities = compute_signed_velocity(
|
|
313
|
+
vertical_positions,
|
|
314
|
+
window_length=params.smoothing_window,
|
|
315
|
+
polyorder=params.polyorder,
|
|
316
|
+
)
|
|
317
|
+
metrics = calculate_cmj_metrics(
|
|
318
|
+
vertical_positions,
|
|
319
|
+
velocities,
|
|
320
|
+
standing_end,
|
|
321
|
+
lowest_point,
|
|
322
|
+
takeoff_frame,
|
|
323
|
+
landing_frame,
|
|
324
|
+
video.fps,
|
|
325
|
+
tracking_method="hip_hybrid",
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
return metrics, vertical_positions, visibilities
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _finalize_analysis_results(
|
|
332
|
+
metrics: CMJMetrics,
|
|
333
|
+
video: VideoProcessor,
|
|
334
|
+
video_path: str,
|
|
335
|
+
vertical_positions: NDArray[np.float64],
|
|
336
|
+
visibilities: NDArray[np.float64],
|
|
337
|
+
params: AnalysisParameters,
|
|
338
|
+
quality_preset: QualityPreset,
|
|
339
|
+
start_time: float,
|
|
340
|
+
timer: Timer,
|
|
341
|
+
verbose: bool,
|
|
342
|
+
) -> None:
|
|
343
|
+
"""Assess quality, validate metrics, and attach metadata."""
|
|
344
|
+
if verbose:
|
|
345
|
+
print("Assessing tracking quality...")
|
|
346
|
+
with timer.measure("quality_assessment"):
|
|
347
|
+
_, outlier_mask = reject_outliers(
|
|
348
|
+
vertical_positions,
|
|
349
|
+
use_ransac=True,
|
|
350
|
+
use_median=True,
|
|
351
|
+
interpolate=False,
|
|
352
|
+
)
|
|
353
|
+
quality_result = assess_jump_quality(
|
|
354
|
+
visibilities=visibilities,
|
|
355
|
+
positions=vertical_positions,
|
|
356
|
+
outlier_mask=outlier_mask,
|
|
357
|
+
fps=video.fps,
|
|
358
|
+
phases_detected=True,
|
|
359
|
+
phase_count=4,
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
_print_quality_warnings(quality_result, verbose)
|
|
363
|
+
|
|
364
|
+
with timer.measure("metrics_validation"):
|
|
365
|
+
validator = CMJMetricsValidator()
|
|
366
|
+
validation_result = validator.validate(metrics.to_dict()) # type: ignore[arg-type]
|
|
367
|
+
metrics.validation_result = validation_result
|
|
368
|
+
|
|
369
|
+
algorithm_config = _create_algorithm_config(params)
|
|
370
|
+
video_info = _create_video_info(video_path, video)
|
|
371
|
+
processing_info = _create_processing_info(start_time, quality_preset, timer)
|
|
372
|
+
result_metadata = _create_result_metadata(
|
|
373
|
+
quality_result, video_info, processing_info, algorithm_config
|
|
374
|
+
)
|
|
375
|
+
metrics.result_metadata = result_metadata
|
|
376
|
+
|
|
377
|
+
_print_validation_results(validation_result, verbose)
|
|
378
|
+
|
|
379
|
+
|
|
217
380
|
@dataclass
|
|
218
381
|
class CMJVideoConfig:
|
|
219
382
|
"""Configuration for processing a single CMJ video."""
|
|
@@ -285,112 +448,29 @@ def process_cmj_video(
|
|
|
285
448
|
|
|
286
449
|
with timer.measure("video_initialization"):
|
|
287
450
|
with VideoProcessor(video_path, timer=timer) as video:
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
if verbose:
|
|
299
|
-
print("Processing all frames with MediaPipe pose tracking...")
|
|
300
|
-
|
|
301
|
-
tracker = pose_tracker or PoseTracker(
|
|
302
|
-
min_detection_confidence=det_conf,
|
|
303
|
-
min_tracking_confidence=track_conf,
|
|
304
|
-
timer=timer,
|
|
451
|
+
# 1. Pose Tracking
|
|
452
|
+
frames, landmarks_sequence, frame_indices = _run_pose_tracking(
|
|
453
|
+
video,
|
|
454
|
+
quality_preset,
|
|
455
|
+
detection_confidence,
|
|
456
|
+
tracking_confidence,
|
|
457
|
+
pose_tracker,
|
|
458
|
+
verbose,
|
|
459
|
+
timer,
|
|
305
460
|
)
|
|
306
|
-
should_close_tracker = pose_tracker is None
|
|
307
461
|
|
|
308
|
-
|
|
309
|
-
|
|
462
|
+
# 2. Parameters & Smoothing
|
|
463
|
+
params = _get_tuned_parameters(
|
|
464
|
+
video, landmarks_sequence, quality_preset, overrides, verbose, timer
|
|
310
465
|
)
|
|
311
|
-
|
|
312
|
-
with timer.measure("parameter_auto_tuning"):
|
|
313
|
-
characteristics = analyze_video_sample(
|
|
314
|
-
landmarks_sequence, video.fps, video.frame_count
|
|
315
|
-
)
|
|
316
|
-
params = auto_tune_parameters(characteristics, quality_preset)
|
|
317
|
-
params = apply_expert_overrides(
|
|
318
|
-
params,
|
|
319
|
-
overrides.smoothing_window if overrides else None,
|
|
320
|
-
overrides.velocity_threshold if overrides else None,
|
|
321
|
-
overrides.min_contact_frames if overrides else None,
|
|
322
|
-
overrides.visibility_threshold if overrides else None,
|
|
323
|
-
)
|
|
324
|
-
|
|
325
|
-
if verbose:
|
|
326
|
-
print_verbose_parameters(video, characteristics, quality_preset, params)
|
|
327
|
-
|
|
328
466
|
smoothed_landmarks = apply_smoothing(landmarks_sequence, params, verbose, timer)
|
|
329
467
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
smoothed_landmarks, target="hip"
|
|
335
|
-
)
|
|
336
|
-
foot_positions, _ = extract_vertical_positions(smoothed_landmarks, target="foot")
|
|
337
|
-
|
|
338
|
-
if verbose:
|
|
339
|
-
print("Detecting CMJ phases...")
|
|
340
|
-
with timer.measure("phase_detection"):
|
|
341
|
-
phases = detect_cmj_phases(
|
|
342
|
-
vertical_positions,
|
|
343
|
-
video.fps,
|
|
344
|
-
window_length=params.smoothing_window,
|
|
345
|
-
polyorder=params.polyorder,
|
|
346
|
-
landing_positions=foot_positions,
|
|
347
|
-
timer=timer,
|
|
348
|
-
)
|
|
349
|
-
|
|
350
|
-
if phases is None:
|
|
351
|
-
raise ValueError("Could not detect CMJ phases in video")
|
|
352
|
-
|
|
353
|
-
standing_end, lowest_point, takeoff_frame, landing_frame = phases
|
|
354
|
-
|
|
355
|
-
if verbose:
|
|
356
|
-
print("Calculating metrics...")
|
|
357
|
-
with timer.measure("metrics_calculation"):
|
|
358
|
-
velocities = compute_signed_velocity(
|
|
359
|
-
vertical_positions,
|
|
360
|
-
window_length=params.smoothing_window,
|
|
361
|
-
polyorder=params.polyorder,
|
|
362
|
-
)
|
|
363
|
-
metrics = calculate_cmj_metrics(
|
|
364
|
-
vertical_positions,
|
|
365
|
-
velocities,
|
|
366
|
-
standing_end,
|
|
367
|
-
lowest_point,
|
|
368
|
-
takeoff_frame,
|
|
369
|
-
landing_frame,
|
|
370
|
-
video.fps,
|
|
371
|
-
tracking_method="hip_hybrid",
|
|
372
|
-
)
|
|
373
|
-
|
|
374
|
-
if verbose:
|
|
375
|
-
print("Assessing tracking quality...")
|
|
376
|
-
with timer.measure("quality_assessment"):
|
|
377
|
-
_, outlier_mask = reject_outliers(
|
|
378
|
-
vertical_positions,
|
|
379
|
-
use_ransac=True,
|
|
380
|
-
use_median=True,
|
|
381
|
-
interpolate=False,
|
|
382
|
-
)
|
|
383
|
-
quality_result = assess_jump_quality(
|
|
384
|
-
visibilities=visibilities,
|
|
385
|
-
positions=vertical_positions,
|
|
386
|
-
outlier_mask=outlier_mask,
|
|
387
|
-
fps=video.fps,
|
|
388
|
-
phases_detected=True,
|
|
389
|
-
phase_count=4,
|
|
390
|
-
)
|
|
391
|
-
|
|
392
|
-
_print_quality_warnings(quality_result, verbose)
|
|
468
|
+
# 3. Kinematic Analysis
|
|
469
|
+
metrics, vertical_positions, visibilities = _run_kinematic_analysis(
|
|
470
|
+
video, smoothed_landmarks, params, verbose, timer
|
|
471
|
+
)
|
|
393
472
|
|
|
473
|
+
# 4. Debug Video Generation (Optional)
|
|
394
474
|
if output_video:
|
|
395
475
|
_generate_debug_video(
|
|
396
476
|
output_video,
|
|
@@ -403,24 +483,23 @@ def process_cmj_video(
|
|
|
403
483
|
verbose,
|
|
404
484
|
)
|
|
405
485
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
486
|
+
# 5. Finalization (Quality, Metadata, Validation)
|
|
487
|
+
_finalize_analysis_results(
|
|
488
|
+
metrics,
|
|
489
|
+
video,
|
|
490
|
+
video_path,
|
|
491
|
+
vertical_positions,
|
|
492
|
+
visibilities,
|
|
493
|
+
params,
|
|
494
|
+
quality_preset,
|
|
495
|
+
start_time,
|
|
496
|
+
timer,
|
|
497
|
+
verbose,
|
|
416
498
|
)
|
|
417
|
-
metrics.result_metadata = result_metadata
|
|
418
499
|
|
|
419
500
|
if json_output:
|
|
420
501
|
_save_metrics_to_json(metrics, json_output, timer, verbose)
|
|
421
502
|
|
|
422
|
-
_print_validation_results(validation_result, verbose)
|
|
423
|
-
|
|
424
503
|
if verbose:
|
|
425
504
|
_print_timing_summary(start_time, timer, metrics)
|
|
426
505
|
|
|
@@ -6,6 +6,7 @@ import shutil
|
|
|
6
6
|
import subprocess
|
|
7
7
|
import time
|
|
8
8
|
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
9
10
|
|
|
10
11
|
import cv2
|
|
11
12
|
import numpy as np
|
|
@@ -73,53 +74,67 @@ def create_video_writer(
|
|
|
73
74
|
# ⚠️ CRITICAL: VP9 (vp09) is EXCLUDED - not supported on iOS/iPhone/iPad browsers!
|
|
74
75
|
# Adding VP9 will break debug video playback on all iOS devices.
|
|
75
76
|
codecs_to_try = ["avc1", "mp4v"]
|
|
76
|
-
|
|
77
|
-
writer = None
|
|
78
|
-
used_codec = "mp4v" # Default fallback
|
|
79
|
-
codec_attempt_log = []
|
|
77
|
+
codec_attempt_log: list[dict[str, Any]] = []
|
|
80
78
|
|
|
81
79
|
for codec in codecs_to_try:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
writer = cv2.VideoWriter(output_path, fourcc, fps, (display_width, display_height))
|
|
85
|
-
if writer.isOpened():
|
|
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
|
-
)
|
|
96
|
-
if codec == "mp4v":
|
|
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)
|
|
102
|
-
break
|
|
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))
|
|
106
|
-
continue
|
|
107
|
-
|
|
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,
|
|
80
|
+
writer = _try_open_video_writer(
|
|
81
|
+
output_path, codec, fps, display_width, display_height, codec_attempt_log
|
|
116
82
|
)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
83
|
+
if writer:
|
|
84
|
+
return writer, needs_resize, codec
|
|
85
|
+
|
|
86
|
+
_log(
|
|
87
|
+
"error",
|
|
88
|
+
"debug_video_writer_creation_failed",
|
|
89
|
+
output_path=output_path,
|
|
90
|
+
dimensions=f"{display_width}x{display_height}",
|
|
91
|
+
fps=fps,
|
|
92
|
+
codec_attempts=codec_attempt_log,
|
|
93
|
+
)
|
|
94
|
+
raise ValueError(
|
|
95
|
+
f"Failed to create video writer for {output_path} with dimensions "
|
|
96
|
+
f"{display_width}x{display_height}"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _try_open_video_writer(
|
|
101
|
+
output_path: str,
|
|
102
|
+
codec: str,
|
|
103
|
+
fps: float,
|
|
104
|
+
width: int,
|
|
105
|
+
height: int,
|
|
106
|
+
attempt_log: list[dict[str, Any]],
|
|
107
|
+
) -> cv2.VideoWriter | None:
|
|
108
|
+
"""Attempt to open a video writer with a specific codec."""
|
|
109
|
+
try:
|
|
110
|
+
fourcc = cv2.VideoWriter_fourcc(*codec) # type: ignore[attr-defined]
|
|
111
|
+
writer = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
|
|
112
|
+
if writer.isOpened():
|
|
113
|
+
attempt_log.append({"codec": codec, "status": "success"})
|
|
114
|
+
_log(
|
|
115
|
+
"info",
|
|
116
|
+
"debug_video_codec_selected",
|
|
117
|
+
codec=codec,
|
|
118
|
+
width=width,
|
|
119
|
+
height=height,
|
|
120
|
+
fps=fps,
|
|
121
|
+
)
|
|
122
|
+
if codec == "mp4v":
|
|
123
|
+
msg = (
|
|
124
|
+
"Using fallback MPEG-4 codec; will re-encode with ffmpeg for "
|
|
125
|
+
"browser compatibility"
|
|
126
|
+
)
|
|
127
|
+
_log("warning", "debug_video_fallback_codec", codec="mp4v", warning=msg)
|
|
128
|
+
return writer
|
|
129
|
+
|
|
130
|
+
attempt_log.append(
|
|
131
|
+
{"codec": codec, "status": "failed", "error": "isOpened() returned False"}
|
|
120
132
|
)
|
|
133
|
+
except Exception as e:
|
|
134
|
+
attempt_log.append({"codec": codec, "status": "failed", "error": str(e)})
|
|
135
|
+
_log("info", "debug_video_codec_attempt_failed", codec=codec, error=str(e))
|
|
121
136
|
|
|
122
|
-
return
|
|
137
|
+
return None
|
|
123
138
|
|
|
124
139
|
|
|
125
140
|
def write_overlay_frame(
|
|
@@ -262,92 +277,17 @@ class BaseDebugOverlayRenderer:
|
|
|
262
277
|
codec=self.used_codec,
|
|
263
278
|
)
|
|
264
279
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
# Convert to H.264 with yuv420p pixel format for browser compatibility
|
|
274
|
-
# -y: Overwrite output file
|
|
275
|
-
# -vcodec libx264: Use H.264 codec
|
|
276
|
-
# -pix_fmt yuv420p: Required for wide browser support (Chrome,
|
|
277
|
-
# Safari, Firefox, iOS)
|
|
278
|
-
# -preset fast: Reasonable speed/compression tradeoff
|
|
279
|
-
# -crf 23: Standard quality
|
|
280
|
-
# -an: Remove audio (debug video has no audio)
|
|
281
|
-
cmd = [
|
|
282
|
-
"ffmpeg",
|
|
283
|
-
"-y",
|
|
284
|
-
"-i",
|
|
285
|
-
self.output_path,
|
|
286
|
-
"-vcodec",
|
|
287
|
-
"libx264",
|
|
288
|
-
"-pix_fmt",
|
|
289
|
-
"yuv420p",
|
|
290
|
-
"-preset",
|
|
291
|
-
"fast",
|
|
292
|
-
"-crf",
|
|
293
|
-
"23",
|
|
294
|
-
"-an",
|
|
295
|
-
temp_path,
|
|
296
|
-
]
|
|
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
|
-
|
|
308
|
-
# Suppress output unless error
|
|
309
|
-
reencode_start = time.time()
|
|
310
|
-
subprocess.run(
|
|
311
|
-
cmd,
|
|
312
|
-
check=True,
|
|
313
|
-
stdout=subprocess.DEVNULL,
|
|
314
|
-
stderr=subprocess.PIPE,
|
|
315
|
-
)
|
|
316
|
-
self.reencode_duration_s = time.time() - reencode_start
|
|
317
|
-
|
|
318
|
-
_log(
|
|
319
|
-
"info",
|
|
320
|
-
"debug_video_ffmpeg_reencoding_complete",
|
|
321
|
-
duration_ms=round(self.reencode_duration_s * 1000, 1),
|
|
322
|
-
)
|
|
323
|
-
|
|
324
|
-
# Overwrite original file
|
|
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
|
-
)
|
|
280
|
+
if self.used_codec != "mp4v":
|
|
281
|
+
_log(
|
|
282
|
+
"info",
|
|
283
|
+
"debug_video_ready_for_playback",
|
|
284
|
+
codec=self.used_codec,
|
|
285
|
+
path=self.output_path,
|
|
286
|
+
)
|
|
287
|
+
return
|
|
333
288
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
_log(
|
|
337
|
-
"warning",
|
|
338
|
-
"debug_video_ffmpeg_reencoding_failed",
|
|
339
|
-
error=str(e),
|
|
340
|
-
stderr=stderr_msg,
|
|
341
|
-
)
|
|
342
|
-
if temp_path and os.path.exists(temp_path):
|
|
343
|
-
os.remove(temp_path)
|
|
344
|
-
_log("info", "debug_video_temp_file_cleaned_up", temp_file=temp_path)
|
|
345
|
-
except Exception as e:
|
|
346
|
-
_log("warning", "debug_video_post_processing_error", error=str(e))
|
|
347
|
-
if temp_path and os.path.exists(temp_path):
|
|
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"):
|
|
289
|
+
ffmpeg_path = shutil.which("ffmpeg")
|
|
290
|
+
if not ffmpeg_path:
|
|
351
291
|
_log(
|
|
352
292
|
"warning",
|
|
353
293
|
"debug_video_ffmpeg_not_available",
|
|
@@ -355,13 +295,88 @@ class BaseDebugOverlayRenderer:
|
|
|
355
295
|
output_path=self.output_path,
|
|
356
296
|
warning="Video may not play in all browsers",
|
|
357
297
|
)
|
|
358
|
-
|
|
298
|
+
return
|
|
299
|
+
|
|
300
|
+
self._reencode_to_h264()
|
|
301
|
+
|
|
302
|
+
def _reencode_to_h264(self) -> None:
|
|
303
|
+
"""Re-encode video to H.264 for browser compatibility using ffmpeg."""
|
|
304
|
+
temp_path = str(
|
|
305
|
+
Path(self.output_path).with_suffix(".temp" + Path(self.output_path).suffix)
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
# Convert to H.264 with yuv420p pixel format for browser compatibility
|
|
309
|
+
# -y: Overwrite output file
|
|
310
|
+
# -vcodec libx264: Use H.264 codec
|
|
311
|
+
# -pix_fmt yuv420p: Required for wide browser support (Chrome, Safari, Firefox, iOS)
|
|
312
|
+
# -preset fast: Reasonable speed/compression tradeoff
|
|
313
|
+
# -crf 23: Standard quality
|
|
314
|
+
# -an: Remove audio (debug video has no audio)
|
|
315
|
+
cmd = [
|
|
316
|
+
"ffmpeg",
|
|
317
|
+
"-y",
|
|
318
|
+
"-i",
|
|
319
|
+
self.output_path,
|
|
320
|
+
"-vcodec",
|
|
321
|
+
"libx264",
|
|
322
|
+
"-pix_fmt",
|
|
323
|
+
"yuv420p",
|
|
324
|
+
"-preset",
|
|
325
|
+
"fast",
|
|
326
|
+
"-crf",
|
|
327
|
+
"23",
|
|
328
|
+
"-an",
|
|
329
|
+
temp_path,
|
|
330
|
+
]
|
|
331
|
+
|
|
332
|
+
_log(
|
|
333
|
+
"info",
|
|
334
|
+
"debug_video_ffmpeg_reencoding_start",
|
|
335
|
+
input_file=self.output_path,
|
|
336
|
+
output_file=temp_path,
|
|
337
|
+
output_codec="libx264",
|
|
338
|
+
pixel_format="yuv420p",
|
|
339
|
+
reason="iOS_compatibility",
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
try:
|
|
343
|
+
reencode_start = time.time()
|
|
344
|
+
subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
|
|
345
|
+
self.reencode_duration_s = time.time() - reencode_start
|
|
346
|
+
|
|
359
347
|
_log(
|
|
360
348
|
"info",
|
|
361
|
-
"
|
|
362
|
-
|
|
363
|
-
|
|
349
|
+
"debug_video_ffmpeg_reencoding_complete",
|
|
350
|
+
duration_ms=round(self.reencode_duration_s * 1000, 1),
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
os.replace(temp_path, self.output_path)
|
|
354
|
+
_log(
|
|
355
|
+
"info",
|
|
356
|
+
"debug_video_reencoded_file_replaced",
|
|
357
|
+
output_path=self.output_path,
|
|
358
|
+
final_codec="libx264",
|
|
359
|
+
pixel_format="yuv420p",
|
|
364
360
|
)
|
|
361
|
+
except (subprocess.CalledProcessError, Exception) as e:
|
|
362
|
+
self._handle_reencode_error(e, temp_path)
|
|
363
|
+
|
|
364
|
+
def _handle_reencode_error(self, e: Exception, temp_path: str) -> None:
|
|
365
|
+
"""Handle errors during ffmpeg re-encoding."""
|
|
366
|
+
if isinstance(e, subprocess.CalledProcessError):
|
|
367
|
+
stderr_msg = e.stderr.decode("utf-8", errors="ignore") if e.stderr else "N/A"
|
|
368
|
+
_log(
|
|
369
|
+
"warning",
|
|
370
|
+
"debug_video_ffmpeg_reencoding_failed",
|
|
371
|
+
error=str(e),
|
|
372
|
+
stderr=stderr_msg,
|
|
373
|
+
)
|
|
374
|
+
else:
|
|
375
|
+
_log("warning", "debug_video_post_processing_error", error=str(e))
|
|
376
|
+
|
|
377
|
+
if os.path.exists(temp_path):
|
|
378
|
+
os.remove(temp_path)
|
|
379
|
+
_log("info", "debug_video_temp_file_cleaned_up", temp_file=temp_path)
|
|
365
380
|
|
|
366
381
|
def __enter__(self) -> Self:
|
|
367
382
|
return self
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kinemotion
|
|
3
|
-
Version: 0.66.
|
|
3
|
+
Version: 0.66.4
|
|
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
|
|
@@ -3,7 +3,7 @@ kinemotion/api.py,sha256=uG1e4bTnj2c-6cbZJEZ_LjMwFdaG32ba2KcK_XjE_NI,1040
|
|
|
3
3
|
kinemotion/cli.py,sha256=cqYV_7URH0JUDy1VQ_EDLv63FmNO4Ns20m6s1XAjiP4,464
|
|
4
4
|
kinemotion/cmj/__init__.py,sha256=SkAw9ka8Yd1Qfv9hcvk22m3EfucROzYrSNGNF5kDzho,113
|
|
5
5
|
kinemotion/cmj/analysis.py,sha256=jM9ZX44h1__Cg2iIhAYRoo_5fPwIOeV5Q2FZ22rMvKY,22202
|
|
6
|
-
kinemotion/cmj/api.py,sha256=
|
|
6
|
+
kinemotion/cmj/api.py,sha256=DkrA6xgSp43g2xpT8ai32BvMGo3Bmj23upxhOYGlpXY,18284
|
|
7
7
|
kinemotion/cmj/cli.py,sha256=P2b77IIw6kqTSIkncxlShzhmjIwqMFBNd-pZxYP-TsI,9918
|
|
8
8
|
kinemotion/cmj/debug_overlay.py,sha256=bX9aPLhXiLCCMZW9v8Y4OiOAaZO0i-UGr-Pl8HCsmbI,15810
|
|
9
9
|
kinemotion/cmj/joint_angles.py,sha256=HmheIEiKcQz39cRezk4h-htorOhGNPsqKIR9RsAEKts,9960
|
|
@@ -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=85KI5TZef1e_6NU083nPHeVMx9rCE3TAdAjO6_5kBUQ,13066
|
|
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
|
|
@@ -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.66.
|
|
40
|
-
kinemotion-0.66.
|
|
41
|
-
kinemotion-0.66.
|
|
42
|
-
kinemotion-0.66.
|
|
43
|
-
kinemotion-0.66.
|
|
39
|
+
kinemotion-0.66.4.dist-info/METADATA,sha256=cwiaYXl2fLT-NpzE455l3Xe5oGTxl27clpbsQstpHW4,26061
|
|
40
|
+
kinemotion-0.66.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
41
|
+
kinemotion-0.66.4.dist-info/entry_points.txt,sha256=zaqnAnjLvcdrk1Qvj5nvXZCZ2gp0prS7it1zTJygcIY,50
|
|
42
|
+
kinemotion-0.66.4.dist-info/licenses/LICENSE,sha256=KZajvqsHw0NoOHOi2q0FZ4NBe9HdV6oey-IPYAtHXfg,1088
|
|
43
|
+
kinemotion-0.66.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|