kinemotion 0.10.5__py3-none-any.whl → 0.10.6__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/dropjump/cli.py +447 -312
- {kinemotion-0.10.5.dist-info → kinemotion-0.10.6.dist-info}/METADATA +1 -1
- {kinemotion-0.10.5.dist-info → kinemotion-0.10.6.dist-info}/RECORD +6 -6
- {kinemotion-0.10.5.dist-info → kinemotion-0.10.6.dist-info}/WHEEL +0 -0
- {kinemotion-0.10.5.dist-info → kinemotion-0.10.6.dist-info}/entry_points.txt +0 -0
- {kinemotion-0.10.5.dist-info → kinemotion-0.10.6.dist-info}/licenses/LICENSE +0 -0
kinemotion/dropjump/cli.py
CHANGED
|
@@ -12,8 +12,12 @@ import click
|
|
|
12
12
|
import numpy as np
|
|
13
13
|
|
|
14
14
|
from ..api import VideoConfig, VideoResult, process_videos_bulk
|
|
15
|
+
from ..core.auto_tuning import (
|
|
16
|
+
AnalysisParameters as AutoTunedParams,
|
|
17
|
+
)
|
|
15
18
|
from ..core.auto_tuning import (
|
|
16
19
|
QualityPreset,
|
|
20
|
+
VideoCharacteristics,
|
|
17
21
|
analyze_video_sample,
|
|
18
22
|
auto_tune_parameters,
|
|
19
23
|
)
|
|
@@ -21,11 +25,12 @@ from ..core.pose import PoseTracker
|
|
|
21
25
|
from ..core.smoothing import smooth_landmarks, smooth_landmarks_advanced
|
|
22
26
|
from ..core.video_io import VideoProcessor
|
|
23
27
|
from .analysis import (
|
|
28
|
+
ContactState,
|
|
24
29
|
compute_average_foot_position,
|
|
25
30
|
detect_ground_contact,
|
|
26
31
|
)
|
|
27
32
|
from .debug_overlay import DebugOverlayRenderer
|
|
28
|
-
from .kinematics import calculate_drop_jump_metrics
|
|
33
|
+
from .kinematics import DropJumpMetrics, calculate_drop_jump_metrics
|
|
29
34
|
|
|
30
35
|
|
|
31
36
|
@dataclass
|
|
@@ -251,6 +256,260 @@ def dropjump_analyze(
|
|
|
251
256
|
)
|
|
252
257
|
|
|
253
258
|
|
|
259
|
+
def _determine_initial_confidence(
|
|
260
|
+
quality_preset: QualityPreset,
|
|
261
|
+
expert_params: AnalysisParameters,
|
|
262
|
+
) -> tuple[float, float]:
|
|
263
|
+
"""Determine initial detection and tracking confidence levels.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
quality_preset: Quality preset enum
|
|
267
|
+
expert_params: Expert parameter overrides
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
Tuple of (detection_confidence, tracking_confidence)
|
|
271
|
+
"""
|
|
272
|
+
initial_detection_conf = 0.5
|
|
273
|
+
initial_tracking_conf = 0.5
|
|
274
|
+
|
|
275
|
+
if quality_preset == QualityPreset.FAST:
|
|
276
|
+
initial_detection_conf = 0.3
|
|
277
|
+
initial_tracking_conf = 0.3
|
|
278
|
+
elif quality_preset == QualityPreset.ACCURATE:
|
|
279
|
+
initial_detection_conf = 0.6
|
|
280
|
+
initial_tracking_conf = 0.6
|
|
281
|
+
|
|
282
|
+
# Override with expert values if provided
|
|
283
|
+
if expert_params.detection_confidence is not None:
|
|
284
|
+
initial_detection_conf = expert_params.detection_confidence
|
|
285
|
+
if expert_params.tracking_confidence is not None:
|
|
286
|
+
initial_tracking_conf = expert_params.tracking_confidence
|
|
287
|
+
|
|
288
|
+
return initial_detection_conf, initial_tracking_conf
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _track_all_frames(video: VideoProcessor, tracker: PoseTracker) -> tuple[list, list]:
|
|
292
|
+
"""Track pose landmarks in all video frames.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
video: Video processor
|
|
296
|
+
tracker: Pose tracker
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
Tuple of (frames, landmarks_sequence)
|
|
300
|
+
"""
|
|
301
|
+
click.echo("Tracking pose landmarks...", err=True)
|
|
302
|
+
landmarks_sequence = []
|
|
303
|
+
frames = []
|
|
304
|
+
|
|
305
|
+
bar: Any
|
|
306
|
+
with click.progressbar(length=video.frame_count, label="Processing frames") as bar:
|
|
307
|
+
while True:
|
|
308
|
+
frame = video.read_frame()
|
|
309
|
+
if frame is None:
|
|
310
|
+
break
|
|
311
|
+
|
|
312
|
+
frames.append(frame)
|
|
313
|
+
landmarks = tracker.process_frame(frame)
|
|
314
|
+
landmarks_sequence.append(landmarks)
|
|
315
|
+
|
|
316
|
+
bar.update(1)
|
|
317
|
+
|
|
318
|
+
tracker.close()
|
|
319
|
+
return frames, landmarks_sequence
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _apply_expert_param_overrides(
|
|
323
|
+
params: AutoTunedParams, expert_params: AnalysisParameters
|
|
324
|
+
) -> AutoTunedParams:
|
|
325
|
+
"""Apply expert parameter overrides to auto-tuned parameters.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
params: Auto-tuned parameters
|
|
329
|
+
expert_params: Expert overrides
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
Modified params object (mutated in place)
|
|
333
|
+
"""
|
|
334
|
+
if expert_params.smoothing_window is not None:
|
|
335
|
+
params.smoothing_window = expert_params.smoothing_window
|
|
336
|
+
if expert_params.velocity_threshold is not None:
|
|
337
|
+
params.velocity_threshold = expert_params.velocity_threshold
|
|
338
|
+
if expert_params.min_contact_frames is not None:
|
|
339
|
+
params.min_contact_frames = expert_params.min_contact_frames
|
|
340
|
+
if expert_params.visibility_threshold is not None:
|
|
341
|
+
params.visibility_threshold = expert_params.visibility_threshold
|
|
342
|
+
return params
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _print_auto_tuned_params(
|
|
346
|
+
video: VideoProcessor,
|
|
347
|
+
characteristics: VideoCharacteristics,
|
|
348
|
+
quality_preset: QualityPreset,
|
|
349
|
+
params: AutoTunedParams,
|
|
350
|
+
) -> None:
|
|
351
|
+
"""Print auto-tuned parameters in verbose mode.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
video: Video processor
|
|
355
|
+
characteristics: Video characteristics
|
|
356
|
+
quality_preset: Quality preset
|
|
357
|
+
params: Auto-tuned parameters
|
|
358
|
+
"""
|
|
359
|
+
click.echo("\n" + "=" * 60, err=True)
|
|
360
|
+
click.echo("AUTO-TUNED PARAMETERS", err=True)
|
|
361
|
+
click.echo("=" * 60, err=True)
|
|
362
|
+
click.echo(f"Video FPS: {video.fps:.2f}", err=True)
|
|
363
|
+
click.echo(
|
|
364
|
+
f"Tracking quality: {characteristics.tracking_quality} "
|
|
365
|
+
f"(avg visibility: {characteristics.avg_visibility:.2f})",
|
|
366
|
+
err=True,
|
|
367
|
+
)
|
|
368
|
+
click.echo(f"Quality preset: {quality_preset.value}", err=True)
|
|
369
|
+
click.echo("\nSelected parameters:", err=True)
|
|
370
|
+
click.echo(f" smoothing_window: {params.smoothing_window}", err=True)
|
|
371
|
+
click.echo(f" polyorder: {params.polyorder}", err=True)
|
|
372
|
+
click.echo(f" velocity_threshold: {params.velocity_threshold:.4f}", err=True)
|
|
373
|
+
click.echo(f" min_contact_frames: {params.min_contact_frames}", err=True)
|
|
374
|
+
click.echo(f" visibility_threshold: {params.visibility_threshold}", err=True)
|
|
375
|
+
click.echo(f" detection_confidence: {params.detection_confidence}", err=True)
|
|
376
|
+
click.echo(f" tracking_confidence: {params.tracking_confidence}", err=True)
|
|
377
|
+
click.echo(f" outlier_rejection: {params.outlier_rejection}", err=True)
|
|
378
|
+
click.echo(f" bilateral_filter: {params.bilateral_filter}", err=True)
|
|
379
|
+
click.echo(f" use_curvature: {params.use_curvature}", err=True)
|
|
380
|
+
click.echo("=" * 60 + "\n", err=True)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _smooth_landmark_sequence(
|
|
384
|
+
landmarks_sequence: list, params: AutoTunedParams
|
|
385
|
+
) -> list:
|
|
386
|
+
"""Apply smoothing to landmark sequence.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
landmarks_sequence: Raw landmark sequence
|
|
390
|
+
params: Auto-tuned parameters
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
Smoothed landmarks
|
|
394
|
+
"""
|
|
395
|
+
if params.outlier_rejection or params.bilateral_filter:
|
|
396
|
+
if params.outlier_rejection:
|
|
397
|
+
click.echo("Smoothing landmarks with outlier rejection...", err=True)
|
|
398
|
+
if params.bilateral_filter:
|
|
399
|
+
click.echo(
|
|
400
|
+
"Using bilateral temporal filter for edge-preserving smoothing...",
|
|
401
|
+
err=True,
|
|
402
|
+
)
|
|
403
|
+
return smooth_landmarks_advanced(
|
|
404
|
+
landmarks_sequence,
|
|
405
|
+
window_length=params.smoothing_window,
|
|
406
|
+
polyorder=params.polyorder,
|
|
407
|
+
use_outlier_rejection=params.outlier_rejection,
|
|
408
|
+
use_bilateral=params.bilateral_filter,
|
|
409
|
+
)
|
|
410
|
+
else:
|
|
411
|
+
click.echo("Smoothing landmarks...", err=True)
|
|
412
|
+
return smooth_landmarks(
|
|
413
|
+
landmarks_sequence,
|
|
414
|
+
window_length=params.smoothing_window,
|
|
415
|
+
polyorder=params.polyorder,
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _extract_positions_and_visibilities(
|
|
420
|
+
smoothed_landmarks: list,
|
|
421
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
422
|
+
"""Extract vertical positions and visibilities from landmarks.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
smoothed_landmarks: Smoothed landmark sequence
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
Tuple of (vertical_positions, visibilities)
|
|
429
|
+
"""
|
|
430
|
+
click.echo("Extracting foot positions...", err=True)
|
|
431
|
+
|
|
432
|
+
position_list: list[float] = []
|
|
433
|
+
visibilities_list: list[float] = []
|
|
434
|
+
|
|
435
|
+
for frame_landmarks in smoothed_landmarks:
|
|
436
|
+
if frame_landmarks:
|
|
437
|
+
_, foot_y = compute_average_foot_position(frame_landmarks)
|
|
438
|
+
position_list.append(foot_y)
|
|
439
|
+
|
|
440
|
+
# Average visibility of foot landmarks
|
|
441
|
+
foot_vis = []
|
|
442
|
+
for key in ["left_ankle", "right_ankle", "left_heel", "right_heel"]:
|
|
443
|
+
if key in frame_landmarks:
|
|
444
|
+
foot_vis.append(frame_landmarks[key][2])
|
|
445
|
+
visibilities_list.append(float(np.mean(foot_vis)) if foot_vis else 0.0)
|
|
446
|
+
else:
|
|
447
|
+
position_list.append(position_list[-1] if position_list else 0.5)
|
|
448
|
+
visibilities_list.append(0.0)
|
|
449
|
+
|
|
450
|
+
return np.array(position_list), np.array(visibilities_list)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _create_debug_video(
|
|
454
|
+
output: str,
|
|
455
|
+
video: VideoProcessor,
|
|
456
|
+
frames: list,
|
|
457
|
+
smoothed_landmarks: list,
|
|
458
|
+
contact_states: list[ContactState],
|
|
459
|
+
metrics: DropJumpMetrics,
|
|
460
|
+
) -> None:
|
|
461
|
+
"""Generate debug video with overlays.
|
|
462
|
+
|
|
463
|
+
Args:
|
|
464
|
+
output: Output video path
|
|
465
|
+
video: Video processor
|
|
466
|
+
frames: Video frames
|
|
467
|
+
smoothed_landmarks: Smoothed landmarks
|
|
468
|
+
contact_states: Contact states
|
|
469
|
+
metrics: Calculated metrics
|
|
470
|
+
"""
|
|
471
|
+
click.echo(f"Generating debug video: {output}", err=True)
|
|
472
|
+
if video.display_width != video.width or video.display_height != video.height:
|
|
473
|
+
click.echo(f"Source video encoded: {video.width}x{video.height}", err=True)
|
|
474
|
+
click.echo(
|
|
475
|
+
f"Output dimensions: {video.display_width}x{video.display_height} "
|
|
476
|
+
f"(respecting display aspect ratio)",
|
|
477
|
+
err=True,
|
|
478
|
+
)
|
|
479
|
+
else:
|
|
480
|
+
click.echo(
|
|
481
|
+
f"Output dimensions: {video.width}x{video.height} "
|
|
482
|
+
f"(matching source video aspect ratio)",
|
|
483
|
+
err=True,
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
with DebugOverlayRenderer(
|
|
487
|
+
output,
|
|
488
|
+
video.width,
|
|
489
|
+
video.height,
|
|
490
|
+
video.display_width,
|
|
491
|
+
video.display_height,
|
|
492
|
+
video.fps,
|
|
493
|
+
) as renderer:
|
|
494
|
+
render_bar: Any
|
|
495
|
+
with click.progressbar(
|
|
496
|
+
length=len(frames), label="Rendering frames"
|
|
497
|
+
) as render_bar:
|
|
498
|
+
for i, frame in enumerate(frames):
|
|
499
|
+
annotated = renderer.render_frame(
|
|
500
|
+
frame,
|
|
501
|
+
smoothed_landmarks[i],
|
|
502
|
+
contact_states[i],
|
|
503
|
+
i,
|
|
504
|
+
metrics,
|
|
505
|
+
use_com=False,
|
|
506
|
+
)
|
|
507
|
+
renderer.write_frame(annotated)
|
|
508
|
+
render_bar.update(1)
|
|
509
|
+
|
|
510
|
+
click.echo(f"Debug video saved: {output}", err=True)
|
|
511
|
+
|
|
512
|
+
|
|
254
513
|
def _process_single(
|
|
255
514
|
video_path: str,
|
|
256
515
|
output: str | None,
|
|
@@ -263,11 +522,9 @@ def _process_single(
|
|
|
263
522
|
"""Process a single video (original CLI behavior)."""
|
|
264
523
|
click.echo(f"Analyzing video: {video_path}", err=True)
|
|
265
524
|
|
|
266
|
-
# Convert quality string to enum
|
|
267
525
|
quality_preset = QualityPreset(quality.lower())
|
|
268
526
|
|
|
269
527
|
try:
|
|
270
|
-
# Initialize video processor
|
|
271
528
|
with VideoProcessor(video_path) as video:
|
|
272
529
|
click.echo(
|
|
273
530
|
f"Video: {video.width}x{video.height} @ {video.fps:.2f} fps, "
|
|
@@ -275,180 +532,42 @@ def _process_single(
|
|
|
275
532
|
err=True,
|
|
276
533
|
)
|
|
277
534
|
|
|
278
|
-
#
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
# Analyze video characteristics from a sample to determine optimal parameters
|
|
283
|
-
# We'll use detection/tracking confidence from quality preset for initial tracking
|
|
284
|
-
initial_detection_conf = 0.5
|
|
285
|
-
initial_tracking_conf = 0.5
|
|
286
|
-
|
|
287
|
-
if quality_preset == QualityPreset.FAST:
|
|
288
|
-
initial_detection_conf = 0.3
|
|
289
|
-
initial_tracking_conf = 0.3
|
|
290
|
-
elif quality_preset == QualityPreset.ACCURATE:
|
|
291
|
-
initial_detection_conf = 0.6
|
|
292
|
-
initial_tracking_conf = 0.6
|
|
293
|
-
|
|
294
|
-
# Override with expert values if provided
|
|
295
|
-
if expert_params.detection_confidence is not None:
|
|
296
|
-
initial_detection_conf = expert_params.detection_confidence
|
|
297
|
-
if expert_params.tracking_confidence is not None:
|
|
298
|
-
initial_tracking_conf = expert_params.tracking_confidence
|
|
299
|
-
|
|
300
|
-
# Initialize pose tracker
|
|
301
|
-
tracker = PoseTracker(
|
|
302
|
-
min_detection_confidence=initial_detection_conf,
|
|
303
|
-
min_tracking_confidence=initial_tracking_conf,
|
|
535
|
+
# Determine confidence levels
|
|
536
|
+
detection_conf, tracking_conf = _determine_initial_confidence(
|
|
537
|
+
quality_preset, expert_params
|
|
304
538
|
)
|
|
305
539
|
|
|
306
|
-
#
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
with click.progressbar(
|
|
313
|
-
length=video.frame_count, label="Processing frames"
|
|
314
|
-
) as bar:
|
|
315
|
-
while True:
|
|
316
|
-
frame = video.read_frame()
|
|
317
|
-
if frame is None:
|
|
318
|
-
break
|
|
319
|
-
|
|
320
|
-
frames.append(frame)
|
|
321
|
-
landmarks = tracker.process_frame(frame)
|
|
322
|
-
landmarks_sequence.append(landmarks)
|
|
323
|
-
|
|
324
|
-
bar.update(1)
|
|
325
|
-
|
|
326
|
-
tracker.close()
|
|
540
|
+
# Track all frames
|
|
541
|
+
tracker = PoseTracker(
|
|
542
|
+
min_detection_confidence=detection_conf,
|
|
543
|
+
min_tracking_confidence=tracking_conf,
|
|
544
|
+
)
|
|
545
|
+
frames, landmarks_sequence = _track_all_frames(video, tracker)
|
|
327
546
|
|
|
328
547
|
if not landmarks_sequence:
|
|
329
548
|
click.echo("Error: No frames processed", err=True)
|
|
330
549
|
sys.exit(1)
|
|
331
550
|
|
|
332
|
-
#
|
|
333
|
-
# STEP 2: Analyze video characteristics and auto-tune parameters
|
|
334
|
-
# ================================================================
|
|
335
|
-
|
|
551
|
+
# Auto-tune parameters
|
|
336
552
|
characteristics = analyze_video_sample(
|
|
337
553
|
landmarks_sequence, video.fps, video.frame_count
|
|
338
554
|
)
|
|
339
|
-
|
|
340
|
-
# Auto-tune parameters based on video characteristics
|
|
341
555
|
params = auto_tune_parameters(characteristics, quality_preset)
|
|
556
|
+
params = _apply_expert_param_overrides(params, expert_params)
|
|
342
557
|
|
|
343
|
-
#
|
|
344
|
-
if expert_params.smoothing_window is not None:
|
|
345
|
-
params.smoothing_window = expert_params.smoothing_window
|
|
346
|
-
if expert_params.velocity_threshold is not None:
|
|
347
|
-
params.velocity_threshold = expert_params.velocity_threshold
|
|
348
|
-
if expert_params.min_contact_frames is not None:
|
|
349
|
-
params.min_contact_frames = expert_params.min_contact_frames
|
|
350
|
-
if expert_params.visibility_threshold is not None:
|
|
351
|
-
params.visibility_threshold = expert_params.visibility_threshold
|
|
352
|
-
|
|
353
|
-
# Show selected parameters if verbose
|
|
558
|
+
# Show parameters if verbose
|
|
354
559
|
if verbose:
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
click.echo(
|
|
360
|
-
f"Tracking quality: {characteristics.tracking_quality} "
|
|
361
|
-
f"(avg visibility: {characteristics.avg_visibility:.2f})",
|
|
362
|
-
err=True,
|
|
363
|
-
)
|
|
364
|
-
click.echo(f"Quality preset: {quality_preset.value}", err=True)
|
|
365
|
-
click.echo("\nSelected parameters:", err=True)
|
|
366
|
-
click.echo(f" smoothing_window: {params.smoothing_window}", err=True)
|
|
367
|
-
click.echo(f" polyorder: {params.polyorder}", err=True)
|
|
368
|
-
click.echo(
|
|
369
|
-
f" velocity_threshold: {params.velocity_threshold:.4f}", err=True
|
|
370
|
-
)
|
|
371
|
-
click.echo(
|
|
372
|
-
f" min_contact_frames: {params.min_contact_frames}", err=True
|
|
373
|
-
)
|
|
374
|
-
click.echo(
|
|
375
|
-
f" visibility_threshold: {params.visibility_threshold}", err=True
|
|
376
|
-
)
|
|
377
|
-
click.echo(
|
|
378
|
-
f" detection_confidence: {params.detection_confidence}", err=True
|
|
379
|
-
)
|
|
380
|
-
click.echo(
|
|
381
|
-
f" tracking_confidence: {params.tracking_confidence}", err=True
|
|
382
|
-
)
|
|
383
|
-
click.echo(f" outlier_rejection: {params.outlier_rejection}", err=True)
|
|
384
|
-
click.echo(f" bilateral_filter: {params.bilateral_filter}", err=True)
|
|
385
|
-
click.echo(f" use_curvature: {params.use_curvature}", err=True)
|
|
386
|
-
click.echo("=" * 60 + "\n", err=True)
|
|
387
|
-
|
|
388
|
-
# ================================================================
|
|
389
|
-
# STEP 3: Apply smoothing with auto-tuned parameters
|
|
390
|
-
# ================================================================
|
|
391
|
-
|
|
392
|
-
# Smooth landmarks using auto-tuned parameters
|
|
393
|
-
if params.outlier_rejection or params.bilateral_filter:
|
|
394
|
-
if params.outlier_rejection:
|
|
395
|
-
click.echo(
|
|
396
|
-
"Smoothing landmarks with outlier rejection...", err=True
|
|
397
|
-
)
|
|
398
|
-
if params.bilateral_filter:
|
|
399
|
-
click.echo(
|
|
400
|
-
"Using bilateral temporal filter for edge-preserving smoothing...",
|
|
401
|
-
err=True,
|
|
402
|
-
)
|
|
403
|
-
smoothed_landmarks = smooth_landmarks_advanced(
|
|
404
|
-
landmarks_sequence,
|
|
405
|
-
window_length=params.smoothing_window,
|
|
406
|
-
polyorder=params.polyorder,
|
|
407
|
-
use_outlier_rejection=params.outlier_rejection,
|
|
408
|
-
use_bilateral=params.bilateral_filter,
|
|
409
|
-
)
|
|
410
|
-
else:
|
|
411
|
-
click.echo("Smoothing landmarks...", err=True)
|
|
412
|
-
smoothed_landmarks = smooth_landmarks(
|
|
413
|
-
landmarks_sequence,
|
|
414
|
-
window_length=params.smoothing_window,
|
|
415
|
-
polyorder=params.polyorder,
|
|
416
|
-
)
|
|
560
|
+
_print_auto_tuned_params(video, characteristics, quality_preset, params)
|
|
561
|
+
|
|
562
|
+
# Apply smoothing
|
|
563
|
+
smoothed_landmarks = _smooth_landmark_sequence(landmarks_sequence, params)
|
|
417
564
|
|
|
418
|
-
# Extract
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
for frame_landmarks in smoothed_landmarks:
|
|
425
|
-
if frame_landmarks:
|
|
426
|
-
# Use average foot position
|
|
427
|
-
_, foot_y = compute_average_foot_position(frame_landmarks)
|
|
428
|
-
position_list.append(foot_y)
|
|
429
|
-
|
|
430
|
-
# Average visibility of foot landmarks
|
|
431
|
-
foot_vis = []
|
|
432
|
-
for key in [
|
|
433
|
-
"left_ankle",
|
|
434
|
-
"right_ankle",
|
|
435
|
-
"left_heel",
|
|
436
|
-
"right_heel",
|
|
437
|
-
]:
|
|
438
|
-
if key in frame_landmarks:
|
|
439
|
-
foot_vis.append(frame_landmarks[key][2])
|
|
440
|
-
visibilities_list.append(
|
|
441
|
-
float(np.mean(foot_vis)) if foot_vis else 0.0
|
|
442
|
-
)
|
|
443
|
-
else:
|
|
444
|
-
# Use previous position if available, otherwise default
|
|
445
|
-
position_list.append(position_list[-1] if position_list else 0.5)
|
|
446
|
-
visibilities_list.append(0.0)
|
|
447
|
-
|
|
448
|
-
vertical_positions: np.ndarray = np.array(position_list)
|
|
449
|
-
visibilities: np.ndarray = np.array(visibilities_list)
|
|
450
|
-
|
|
451
|
-
# Detect ground contact using auto-tuned parameters
|
|
565
|
+
# Extract positions
|
|
566
|
+
vertical_positions, visibilities = _extract_positions_and_visibilities(
|
|
567
|
+
smoothed_landmarks
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
# Detect ground contact
|
|
452
571
|
contact_states = detect_ground_contact(
|
|
453
572
|
vertical_positions,
|
|
454
573
|
velocity_threshold=params.velocity_threshold,
|
|
@@ -475,67 +594,22 @@ def _process_single(
|
|
|
475
594
|
smoothing_window=params.smoothing_window,
|
|
476
595
|
polyorder=params.polyorder,
|
|
477
596
|
use_curvature=params.use_curvature,
|
|
478
|
-
kinematic_correction_factor=1.0,
|
|
597
|
+
kinematic_correction_factor=1.0,
|
|
479
598
|
)
|
|
480
599
|
|
|
481
|
-
# Output metrics
|
|
482
|
-
|
|
483
|
-
metrics_json = json.dumps(metrics_dict, indent=2)
|
|
484
|
-
|
|
600
|
+
# Output metrics
|
|
601
|
+
metrics_json = json.dumps(metrics.to_dict(), indent=2)
|
|
485
602
|
if json_output:
|
|
486
|
-
|
|
487
|
-
output_path.write_text(metrics_json)
|
|
603
|
+
Path(json_output).write_text(metrics_json)
|
|
488
604
|
click.echo(f"Metrics written to: {json_output}", err=True)
|
|
489
605
|
else:
|
|
490
606
|
click.echo(metrics_json)
|
|
491
607
|
|
|
492
608
|
# Generate debug video if requested
|
|
493
609
|
if output:
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
or video.display_height != video.height
|
|
498
|
-
):
|
|
499
|
-
click.echo(
|
|
500
|
-
f"Source video encoded: {video.width}x{video.height}",
|
|
501
|
-
err=True,
|
|
502
|
-
)
|
|
503
|
-
click.echo(
|
|
504
|
-
f"Output dimensions: {video.display_width}x{video.display_height} "
|
|
505
|
-
f"(respecting display aspect ratio)",
|
|
506
|
-
err=True,
|
|
507
|
-
)
|
|
508
|
-
else:
|
|
509
|
-
click.echo(
|
|
510
|
-
f"Output dimensions: {video.width}x{video.height} "
|
|
511
|
-
f"(matching source video aspect ratio)",
|
|
512
|
-
err=True,
|
|
513
|
-
)
|
|
514
|
-
with DebugOverlayRenderer(
|
|
515
|
-
output,
|
|
516
|
-
video.width,
|
|
517
|
-
video.height,
|
|
518
|
-
video.display_width,
|
|
519
|
-
video.display_height,
|
|
520
|
-
video.fps,
|
|
521
|
-
) as renderer:
|
|
522
|
-
render_bar: Any
|
|
523
|
-
with click.progressbar(
|
|
524
|
-
length=len(frames), label="Rendering frames"
|
|
525
|
-
) as render_bar:
|
|
526
|
-
for i, frame in enumerate(frames):
|
|
527
|
-
annotated = renderer.render_frame(
|
|
528
|
-
frame,
|
|
529
|
-
smoothed_landmarks[i],
|
|
530
|
-
contact_states[i],
|
|
531
|
-
i,
|
|
532
|
-
metrics,
|
|
533
|
-
use_com=False,
|
|
534
|
-
)
|
|
535
|
-
renderer.write_frame(annotated)
|
|
536
|
-
render_bar.update(1)
|
|
537
|
-
|
|
538
|
-
click.echo(f"Debug video saved: {output}", err=True)
|
|
610
|
+
_create_debug_video(
|
|
611
|
+
output, video, frames, smoothed_landmarks, contact_states, metrics
|
|
612
|
+
)
|
|
539
613
|
|
|
540
614
|
click.echo("Analysis complete!", err=True)
|
|
541
615
|
|
|
@@ -544,23 +618,15 @@ def _process_single(
|
|
|
544
618
|
sys.exit(1)
|
|
545
619
|
|
|
546
620
|
|
|
547
|
-
def
|
|
548
|
-
|
|
549
|
-
drop_height: float,
|
|
550
|
-
quality: str,
|
|
551
|
-
workers: int,
|
|
552
|
-
output_dir: str | None,
|
|
553
|
-
json_output_dir: str | None,
|
|
554
|
-
csv_summary: str | None,
|
|
555
|
-
expert_params: AnalysisParameters,
|
|
621
|
+
def _setup_batch_output_dirs(
|
|
622
|
+
output_dir: str | None, json_output_dir: str | None
|
|
556
623
|
) -> None:
|
|
557
|
-
"""
|
|
558
|
-
click.echo(
|
|
559
|
-
f"\nBatch processing {len(video_files)} videos with {workers} workers", err=True
|
|
560
|
-
)
|
|
561
|
-
click.echo("=" * 70, err=True)
|
|
624
|
+
"""Create output directories for batch processing.
|
|
562
625
|
|
|
563
|
-
|
|
626
|
+
Args:
|
|
627
|
+
output_dir: Debug video output directory
|
|
628
|
+
json_output_dir: JSON metrics output directory
|
|
629
|
+
"""
|
|
564
630
|
if output_dir:
|
|
565
631
|
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
|
566
632
|
click.echo(f"Debug videos will be saved to: {output_dir}", err=True)
|
|
@@ -569,12 +635,32 @@ def _process_batch(
|
|
|
569
635
|
Path(json_output_dir).mkdir(parents=True, exist_ok=True)
|
|
570
636
|
click.echo(f"JSON metrics will be saved to: {json_output_dir}", err=True)
|
|
571
637
|
|
|
572
|
-
|
|
638
|
+
|
|
639
|
+
def _create_video_configs(
|
|
640
|
+
video_files: list[str],
|
|
641
|
+
drop_height: float,
|
|
642
|
+
quality: str,
|
|
643
|
+
output_dir: str | None,
|
|
644
|
+
json_output_dir: str | None,
|
|
645
|
+
expert_params: AnalysisParameters,
|
|
646
|
+
) -> list[VideoConfig]:
|
|
647
|
+
"""Build configuration objects for each video.
|
|
648
|
+
|
|
649
|
+
Args:
|
|
650
|
+
video_files: List of video file paths
|
|
651
|
+
drop_height: Drop height in meters
|
|
652
|
+
quality: Quality preset
|
|
653
|
+
output_dir: Debug video output directory
|
|
654
|
+
json_output_dir: JSON metrics output directory
|
|
655
|
+
expert_params: Expert parameter overrides
|
|
656
|
+
|
|
657
|
+
Returns:
|
|
658
|
+
List of VideoConfig objects
|
|
659
|
+
"""
|
|
573
660
|
configs: list[VideoConfig] = []
|
|
574
661
|
for video_file in video_files:
|
|
575
662
|
video_name = Path(video_file).stem
|
|
576
663
|
|
|
577
|
-
# Determine output paths
|
|
578
664
|
debug_video = None
|
|
579
665
|
if output_dir:
|
|
580
666
|
debug_video = str(Path(output_dir) / f"{video_name}_debug.mp4")
|
|
@@ -599,29 +685,15 @@ def _process_batch(
|
|
|
599
685
|
)
|
|
600
686
|
configs.append(config)
|
|
601
687
|
|
|
602
|
-
|
|
603
|
-
completed = 0
|
|
688
|
+
return configs
|
|
604
689
|
|
|
605
|
-
def show_progress(result: VideoResult) -> None:
|
|
606
|
-
nonlocal completed
|
|
607
|
-
completed += 1
|
|
608
|
-
status = "✓" if result.success else "✗"
|
|
609
|
-
video_name = Path(result.video_path).name
|
|
610
|
-
click.echo(
|
|
611
|
-
f"[{completed}/{len(configs)}] {status} {video_name} "
|
|
612
|
-
f"({result.processing_time:.1f}s)",
|
|
613
|
-
err=True,
|
|
614
|
-
)
|
|
615
|
-
if not result.success:
|
|
616
|
-
click.echo(f" Error: {result.error}", err=True)
|
|
617
690
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
results = process_videos_bulk(
|
|
621
|
-
configs, max_workers=workers, progress_callback=show_progress
|
|
622
|
-
)
|
|
691
|
+
def _compute_batch_statistics(results: list[VideoResult]) -> None:
|
|
692
|
+
"""Compute and display batch processing statistics.
|
|
623
693
|
|
|
624
|
-
|
|
694
|
+
Args:
|
|
695
|
+
results: List of video processing results
|
|
696
|
+
"""
|
|
625
697
|
click.echo("\n" + "=" * 70, err=True)
|
|
626
698
|
click.echo("BATCH PROCESSING SUMMARY", err=True)
|
|
627
699
|
click.echo("=" * 70, err=True)
|
|
@@ -648,7 +720,6 @@ def _process_batch(
|
|
|
648
720
|
]
|
|
649
721
|
|
|
650
722
|
if with_gct:
|
|
651
|
-
# Type assertion: filtering ensures metrics and ground_contact_time are not None
|
|
652
723
|
avg_gct = sum(
|
|
653
724
|
r.metrics.ground_contact_time * 1000
|
|
654
725
|
for r in with_gct
|
|
@@ -657,7 +728,6 @@ def _process_batch(
|
|
|
657
728
|
click.echo(f"\nAverage ground contact time: {avg_gct:.1f} ms", err=True)
|
|
658
729
|
|
|
659
730
|
if with_flight:
|
|
660
|
-
# Type assertion: filtering ensures metrics and flight_time are not None
|
|
661
731
|
avg_flight = sum(
|
|
662
732
|
r.metrics.flight_time * 1000
|
|
663
733
|
for r in with_flight
|
|
@@ -666,7 +736,6 @@ def _process_batch(
|
|
|
666
736
|
click.echo(f"Average flight time: {avg_flight:.1f} ms", err=True)
|
|
667
737
|
|
|
668
738
|
if with_jump:
|
|
669
|
-
# Type assertion: filtering ensures metrics and jump_height are not None
|
|
670
739
|
avg_jump = sum(
|
|
671
740
|
r.metrics.jump_height
|
|
672
741
|
for r in with_jump
|
|
@@ -677,63 +746,129 @@ def _process_batch(
|
|
|
677
746
|
err=True,
|
|
678
747
|
)
|
|
679
748
|
|
|
680
|
-
# Export CSV summary if requested
|
|
681
|
-
if csv_summary and successful:
|
|
682
|
-
click.echo(f"\nExporting CSV summary to: {csv_summary}", err=True)
|
|
683
|
-
Path(csv_summary).parent.mkdir(parents=True, exist_ok=True)
|
|
684
|
-
|
|
685
|
-
with open(csv_summary, "w", newline="") as f:
|
|
686
|
-
writer = csv.writer(f)
|
|
687
|
-
|
|
688
|
-
# Header
|
|
689
|
-
writer.writerow(
|
|
690
|
-
[
|
|
691
|
-
"Video",
|
|
692
|
-
"Ground Contact Time (ms)",
|
|
693
|
-
"Flight Time (ms)",
|
|
694
|
-
"Jump Height (m)",
|
|
695
|
-
"Processing Time (s)",
|
|
696
|
-
"Status",
|
|
697
|
-
]
|
|
698
|
-
)
|
|
699
749
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
750
|
+
def _write_csv_summary(
|
|
751
|
+
csv_summary: str | None, results: list[VideoResult], successful: list[VideoResult]
|
|
752
|
+
) -> None:
|
|
753
|
+
"""Write CSV summary of batch processing results.
|
|
754
|
+
|
|
755
|
+
Args:
|
|
756
|
+
csv_summary: Path to CSV output file
|
|
757
|
+
results: All processing results
|
|
758
|
+
successful: Successful processing results
|
|
759
|
+
"""
|
|
760
|
+
if not csv_summary or not successful:
|
|
761
|
+
return
|
|
762
|
+
|
|
763
|
+
click.echo(f"\nExporting CSV summary to: {csv_summary}", err=True)
|
|
764
|
+
Path(csv_summary).parent.mkdir(parents=True, exist_ok=True)
|
|
765
|
+
|
|
766
|
+
with open(csv_summary, "w", newline="") as f:
|
|
767
|
+
writer = csv.writer(f)
|
|
768
|
+
|
|
769
|
+
# Header
|
|
770
|
+
writer.writerow(
|
|
771
|
+
[
|
|
772
|
+
"Video",
|
|
773
|
+
"Ground Contact Time (ms)",
|
|
774
|
+
"Flight Time (ms)",
|
|
775
|
+
"Jump Height (m)",
|
|
776
|
+
"Processing Time (s)",
|
|
777
|
+
"Status",
|
|
778
|
+
]
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
# Data rows
|
|
782
|
+
for result in results:
|
|
783
|
+
if result.success and result.metrics:
|
|
784
|
+
writer.writerow(
|
|
785
|
+
[
|
|
786
|
+
Path(result.video_path).name,
|
|
787
|
+
(
|
|
788
|
+
f"{result.metrics.ground_contact_time * 1000:.1f}"
|
|
789
|
+
if result.metrics.ground_contact_time
|
|
790
|
+
else "N/A"
|
|
791
|
+
),
|
|
792
|
+
(
|
|
793
|
+
f"{result.metrics.flight_time * 1000:.1f}"
|
|
794
|
+
if result.metrics.flight_time
|
|
795
|
+
else "N/A"
|
|
796
|
+
),
|
|
797
|
+
(
|
|
798
|
+
f"{result.metrics.jump_height:.3f}"
|
|
799
|
+
if result.metrics.jump_height
|
|
800
|
+
else "N/A"
|
|
801
|
+
),
|
|
802
|
+
f"{result.processing_time:.2f}",
|
|
803
|
+
"Success",
|
|
804
|
+
]
|
|
805
|
+
)
|
|
806
|
+
else:
|
|
807
|
+
writer.writerow(
|
|
808
|
+
[
|
|
809
|
+
Path(result.video_path).name,
|
|
810
|
+
"N/A",
|
|
811
|
+
"N/A",
|
|
812
|
+
"N/A",
|
|
813
|
+
f"{result.processing_time:.2f}",
|
|
814
|
+
f"Failed: {result.error}",
|
|
815
|
+
]
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
click.echo("CSV summary written successfully", err=True)
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
def _process_batch(
|
|
822
|
+
video_files: list[str],
|
|
823
|
+
drop_height: float,
|
|
824
|
+
quality: str,
|
|
825
|
+
workers: int,
|
|
826
|
+
output_dir: str | None,
|
|
827
|
+
json_output_dir: str | None,
|
|
828
|
+
csv_summary: str | None,
|
|
829
|
+
expert_params: AnalysisParameters,
|
|
830
|
+
) -> None:
|
|
831
|
+
"""Process multiple videos in batch mode using parallel processing."""
|
|
832
|
+
click.echo(
|
|
833
|
+
f"\nBatch processing {len(video_files)} videos with {workers} workers", err=True
|
|
834
|
+
)
|
|
835
|
+
click.echo("=" * 70, err=True)
|
|
836
|
+
|
|
837
|
+
# Setup output directories
|
|
838
|
+
_setup_batch_output_dirs(output_dir, json_output_dir)
|
|
839
|
+
|
|
840
|
+
# Create video configurations
|
|
841
|
+
configs = _create_video_configs(
|
|
842
|
+
video_files, drop_height, quality, output_dir, json_output_dir, expert_params
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
# Progress callback
|
|
846
|
+
completed = 0
|
|
847
|
+
|
|
848
|
+
def show_progress(result: VideoResult) -> None:
|
|
849
|
+
nonlocal completed
|
|
850
|
+
completed += 1
|
|
851
|
+
status = "✓" if result.success else "✗"
|
|
852
|
+
video_name = Path(result.video_path).name
|
|
853
|
+
click.echo(
|
|
854
|
+
f"[{completed}/{len(configs)}] {status} {video_name} "
|
|
855
|
+
f"({result.processing_time:.1f}s)",
|
|
856
|
+
err=True,
|
|
857
|
+
)
|
|
858
|
+
if not result.success:
|
|
859
|
+
click.echo(f" Error: {result.error}", err=True)
|
|
860
|
+
|
|
861
|
+
# Process all videos
|
|
862
|
+
click.echo("\nProcessing videos...", err=True)
|
|
863
|
+
results = process_videos_bulk(
|
|
864
|
+
configs, max_workers=workers, progress_callback=show_progress
|
|
865
|
+
)
|
|
866
|
+
|
|
867
|
+
# Display statistics
|
|
868
|
+
_compute_batch_statistics(results)
|
|
869
|
+
|
|
870
|
+
# Export CSV summary if requested
|
|
871
|
+
successful = [r for r in results if r.success]
|
|
872
|
+
_write_csv_summary(csv_summary, results, successful)
|
|
738
873
|
|
|
739
874
|
click.echo("\nBatch processing complete!", err=True)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kinemotion
|
|
3
|
-
Version: 0.10.
|
|
3
|
+
Version: 0.10.6
|
|
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
|
|
@@ -9,12 +9,12 @@ kinemotion/core/smoothing.py,sha256=FON4qKtsSp1-03GnJrDkEUAePaACn4QPMJF0eTIYqR0,
|
|
|
9
9
|
kinemotion/core/video_io.py,sha256=z8Z0qbNaKbcdB40KnbNOBMzab3BbgnhBxp-mUBYeXgM,6577
|
|
10
10
|
kinemotion/dropjump/__init__.py,sha256=yc1XiZ9vfo5h_n7PKVSiX2TTgaIfGL7Y7SkQtiDZj_E,838
|
|
11
11
|
kinemotion/dropjump/analysis.py,sha256=HfJt2t9IsMBiBUz7apIzdxbRH9QqzlFnDVVWcKhU3ow,23291
|
|
12
|
-
kinemotion/dropjump/cli.py,sha256=
|
|
12
|
+
kinemotion/dropjump/cli.py,sha256=vQBY8C5zn2mqmsx4sG-d0aLwQIaQEayCZJGSx_9jtvw,27878
|
|
13
13
|
kinemotion/dropjump/debug_overlay.py,sha256=GMo-jCl5OPIv82uPxDbBVI7CsAMwATTvxZMeWfs8k8M,8701
|
|
14
14
|
kinemotion/dropjump/kinematics.py,sha256=RM_O8Kdc6aEiPIu_99N4cu-4EhYSQxtBGASJF_dmQaU,19081
|
|
15
15
|
kinemotion/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
-
kinemotion-0.10.
|
|
17
|
-
kinemotion-0.10.
|
|
18
|
-
kinemotion-0.10.
|
|
19
|
-
kinemotion-0.10.
|
|
20
|
-
kinemotion-0.10.
|
|
16
|
+
kinemotion-0.10.6.dist-info/METADATA,sha256=ZKGxPsgV5LF4D8XGannzkyYJbhpoOuKdCyxhEXzxr38,20333
|
|
17
|
+
kinemotion-0.10.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
18
|
+
kinemotion-0.10.6.dist-info/entry_points.txt,sha256=zaqnAnjLvcdrk1Qvj5nvXZCZ2gp0prS7it1zTJygcIY,50
|
|
19
|
+
kinemotion-0.10.6.dist-info/licenses/LICENSE,sha256=KZajvqsHw0NoOHOi2q0FZ4NBe9HdV6oey-IPYAtHXfg,1088
|
|
20
|
+
kinemotion-0.10.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|