kinemotion 0.76.3__py3-none-any.whl → 1.0.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/__init__.py +3 -18
- kinemotion/api.py +7 -27
- kinemotion/cli.py +2 -4
- kinemotion/{countermovement_jump → cmj}/analysis.py +158 -16
- kinemotion/{countermovement_jump → cmj}/api.py +18 -46
- kinemotion/{countermovement_jump → cmj}/cli.py +46 -6
- kinemotion/cmj/debug_overlay.py +457 -0
- kinemotion/{countermovement_jump → cmj}/joint_angles.py +31 -96
- kinemotion/{countermovement_jump → cmj}/metrics_validator.py +293 -184
- kinemotion/{countermovement_jump → cmj}/validation_bounds.py +18 -1
- kinemotion/core/__init__.py +2 -11
- kinemotion/core/auto_tuning.py +107 -149
- kinemotion/core/cli_utils.py +0 -74
- kinemotion/core/debug_overlay_utils.py +15 -142
- kinemotion/core/experimental.py +51 -55
- kinemotion/core/filtering.py +56 -116
- kinemotion/core/pipeline_utils.py +2 -2
- kinemotion/core/pose.py +98 -47
- kinemotion/core/quality.py +6 -4
- kinemotion/core/smoothing.py +51 -65
- kinemotion/core/types.py +0 -15
- kinemotion/core/validation.py +7 -76
- kinemotion/core/video_io.py +27 -41
- kinemotion/{drop_jump → dropjump}/__init__.py +8 -2
- kinemotion/{drop_jump → dropjump}/analysis.py +120 -282
- kinemotion/{drop_jump → dropjump}/api.py +33 -59
- kinemotion/{drop_jump → dropjump}/cli.py +136 -70
- kinemotion/dropjump/debug_overlay.py +182 -0
- kinemotion/{drop_jump → dropjump}/kinematics.py +65 -175
- kinemotion/{drop_jump → dropjump}/metrics_validator.py +51 -25
- kinemotion/{drop_jump → dropjump}/validation_bounds.py +1 -1
- kinemotion/models/rtmpose-s_simcc-body7_pt-body7-halpe26_700e-256x192-7f134165_20230605.onnx +3 -0
- kinemotion/models/yolox_tiny_8xb8-300e_humanart-6f3252f9.onnx +3 -0
- {kinemotion-0.76.3.dist-info → kinemotion-1.0.0.dist-info}/METADATA +26 -75
- kinemotion-1.0.0.dist-info/RECORD +49 -0
- kinemotion/core/overlay_constants.py +0 -61
- kinemotion/core/video_analysis_base.py +0 -132
- kinemotion/countermovement_jump/debug_overlay.py +0 -325
- kinemotion/drop_jump/debug_overlay.py +0 -241
- kinemotion/squat_jump/__init__.py +0 -5
- kinemotion/squat_jump/analysis.py +0 -377
- kinemotion/squat_jump/api.py +0 -610
- kinemotion/squat_jump/cli.py +0 -309
- kinemotion/squat_jump/debug_overlay.py +0 -163
- kinemotion/squat_jump/kinematics.py +0 -342
- kinemotion/squat_jump/metrics_validator.py +0 -438
- kinemotion/squat_jump/validation_bounds.py +0 -221
- kinemotion-0.76.3.dist-info/RECORD +0 -57
- /kinemotion/{countermovement_jump → cmj}/__init__.py +0 -0
- /kinemotion/{countermovement_jump → cmj}/kinematics.py +0 -0
- {kinemotion-0.76.3.dist-info → kinemotion-1.0.0.dist-info}/WHEEL +0 -0
- {kinemotion-0.76.3.dist-info → kinemotion-1.0.0.dist-info}/entry_points.txt +0 -0
- {kinemotion-0.76.3.dist-info → kinemotion-1.0.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -8,11 +8,8 @@ from pathlib import Path
|
|
|
8
8
|
from typing import TYPE_CHECKING
|
|
9
9
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
11
|
-
import numpy as np
|
|
12
11
|
from numpy.typing import NDArray
|
|
13
12
|
|
|
14
|
-
from .analysis import ContactState
|
|
15
|
-
|
|
16
13
|
from ..core.auto_tuning import (
|
|
17
14
|
AnalysisParameters,
|
|
18
15
|
QualityPreset,
|
|
@@ -51,19 +48,10 @@ from .analysis import (
|
|
|
51
48
|
detect_ground_contact,
|
|
52
49
|
find_contact_phases,
|
|
53
50
|
)
|
|
54
|
-
from .debug_overlay import
|
|
51
|
+
from .debug_overlay import DebugOverlayRenderer
|
|
55
52
|
from .kinematics import DropJumpMetrics, calculate_drop_jump_metrics
|
|
56
53
|
from .metrics_validator import DropJumpMetricsValidator
|
|
57
54
|
|
|
58
|
-
__all__ = [
|
|
59
|
-
"AnalysisOverrides",
|
|
60
|
-
"DropJumpVideoConfig",
|
|
61
|
-
"DropJumpVideoResult",
|
|
62
|
-
"process_dropjump_video",
|
|
63
|
-
"process_dropjump_video_from_config",
|
|
64
|
-
"process_dropjump_videos_bulk",
|
|
65
|
-
]
|
|
66
|
-
|
|
67
55
|
|
|
68
56
|
@dataclass
|
|
69
57
|
class AnalysisOverrides:
|
|
@@ -102,33 +90,14 @@ class DropJumpVideoConfig:
|
|
|
102
90
|
overrides: AnalysisOverrides | None = None
|
|
103
91
|
detection_confidence: float | None = None
|
|
104
92
|
tracking_confidence: float | None = None
|
|
105
|
-
verbose: bool = False
|
|
106
|
-
timer: Timer | None = None
|
|
107
|
-
pose_tracker: "MediaPipePoseTracker | None" = None
|
|
108
|
-
|
|
109
|
-
def to_kwargs(self) -> dict:
|
|
110
|
-
"""Convert config to kwargs dict for process_dropjump_video."""
|
|
111
|
-
return {
|
|
112
|
-
"video_path": self.video_path,
|
|
113
|
-
"quality": self.quality,
|
|
114
|
-
"output_video": self.output_video,
|
|
115
|
-
"json_output": self.json_output,
|
|
116
|
-
"drop_start_frame": self.drop_start_frame,
|
|
117
|
-
"overrides": self.overrides,
|
|
118
|
-
"detection_confidence": self.detection_confidence,
|
|
119
|
-
"tracking_confidence": self.tracking_confidence,
|
|
120
|
-
"verbose": self.verbose,
|
|
121
|
-
"timer": self.timer,
|
|
122
|
-
"pose_tracker": self.pose_tracker,
|
|
123
|
-
}
|
|
124
93
|
|
|
125
94
|
|
|
126
95
|
def _assess_dropjump_quality(
|
|
127
|
-
vertical_positions: "NDArray
|
|
128
|
-
visibilities: "NDArray
|
|
129
|
-
contact_states: list
|
|
96
|
+
vertical_positions: "NDArray",
|
|
97
|
+
visibilities: "NDArray",
|
|
98
|
+
contact_states: list,
|
|
130
99
|
fps: float,
|
|
131
|
-
) -> tuple
|
|
100
|
+
) -> tuple:
|
|
132
101
|
"""Assess tracking quality and detect phases.
|
|
133
102
|
|
|
134
103
|
Returns:
|
|
@@ -336,6 +305,7 @@ def _tune_and_smooth(
|
|
|
336
305
|
characteristics = analyze_video_sample(landmarks_sequence, video_fps, frame_count)
|
|
337
306
|
params = auto_tune_parameters(characteristics, quality_preset)
|
|
338
307
|
|
|
308
|
+
# Apply overrides if provided
|
|
339
309
|
if overrides:
|
|
340
310
|
params = apply_expert_overrides(
|
|
341
311
|
params,
|
|
@@ -344,6 +314,14 @@ def _tune_and_smooth(
|
|
|
344
314
|
overrides.min_contact_frames,
|
|
345
315
|
overrides.visibility_threshold,
|
|
346
316
|
)
|
|
317
|
+
else:
|
|
318
|
+
params = apply_expert_overrides(
|
|
319
|
+
params,
|
|
320
|
+
None,
|
|
321
|
+
None,
|
|
322
|
+
None,
|
|
323
|
+
None,
|
|
324
|
+
)
|
|
347
325
|
|
|
348
326
|
smoothed_landmarks = apply_smoothing(landmarks_sequence, params, verbose, timer)
|
|
349
327
|
|
|
@@ -462,13 +440,16 @@ def _generate_debug_video(
|
|
|
462
440
|
timer = timer or NULL_TIMER
|
|
463
441
|
debug_h, debug_w = frames[0].shape[:2]
|
|
464
442
|
|
|
465
|
-
|
|
466
|
-
|
|
443
|
+
if video_fps > 30:
|
|
444
|
+
debug_fps = video_fps / (video_fps / 30.0)
|
|
445
|
+
else:
|
|
446
|
+
debug_fps = video_fps
|
|
447
|
+
|
|
467
448
|
if len(frames) < len(smoothed_landmarks):
|
|
468
449
|
step = max(1, int(video_fps / 30.0))
|
|
469
450
|
debug_fps = video_fps / step
|
|
470
451
|
|
|
471
|
-
def _render_frames(renderer:
|
|
452
|
+
def _render_frames(renderer: DebugOverlayRenderer) -> None:
|
|
472
453
|
for frame, idx in zip(frames, frame_indices, strict=True):
|
|
473
454
|
annotated = renderer.render_frame(
|
|
474
455
|
frame,
|
|
@@ -480,7 +461,7 @@ def _generate_debug_video(
|
|
|
480
461
|
)
|
|
481
462
|
renderer.write_frame(annotated)
|
|
482
463
|
|
|
483
|
-
renderer_context =
|
|
464
|
+
renderer_context = DebugOverlayRenderer(
|
|
484
465
|
output_video,
|
|
485
466
|
debug_w,
|
|
486
467
|
debug_h,
|
|
@@ -630,23 +611,6 @@ def process_dropjump_video(
|
|
|
630
611
|
return metrics
|
|
631
612
|
|
|
632
613
|
|
|
633
|
-
def process_dropjump_video_from_config(
|
|
634
|
-
config: DropJumpVideoConfig,
|
|
635
|
-
) -> DropJumpMetrics:
|
|
636
|
-
"""Process a drop jump video using a configuration object.
|
|
637
|
-
|
|
638
|
-
This is a convenience wrapper around process_dropjump_video that
|
|
639
|
-
accepts a DropJumpVideoConfig instead of individual parameters.
|
|
640
|
-
|
|
641
|
-
Args:
|
|
642
|
-
config: Configuration object containing all analysis parameters
|
|
643
|
-
|
|
644
|
-
Returns:
|
|
645
|
-
DropJumpMetrics object containing analysis results
|
|
646
|
-
"""
|
|
647
|
-
return process_dropjump_video(**config.to_kwargs())
|
|
648
|
-
|
|
649
|
-
|
|
650
614
|
def process_dropjump_videos_bulk(
|
|
651
615
|
configs: list[DropJumpVideoConfig],
|
|
652
616
|
max_workers: int = 4,
|
|
@@ -673,8 +637,18 @@ def _process_dropjump_video_wrapper(config: DropJumpVideoConfig) -> DropJumpVide
|
|
|
673
637
|
start_time = time.perf_counter()
|
|
674
638
|
|
|
675
639
|
try:
|
|
676
|
-
|
|
677
|
-
|
|
640
|
+
metrics = process_dropjump_video(
|
|
641
|
+
video_path=config.video_path,
|
|
642
|
+
quality=config.quality,
|
|
643
|
+
output_video=config.output_video,
|
|
644
|
+
json_output=config.json_output,
|
|
645
|
+
drop_start_frame=config.drop_start_frame,
|
|
646
|
+
overrides=config.overrides,
|
|
647
|
+
detection_confidence=config.detection_confidence,
|
|
648
|
+
tracking_confidence=config.tracking_confidence,
|
|
649
|
+
verbose=False,
|
|
650
|
+
)
|
|
651
|
+
|
|
678
652
|
processing_time = time.perf_counter() - start_time
|
|
679
653
|
|
|
680
654
|
return DropJumpVideoResult(
|
|
@@ -5,17 +5,12 @@ import json
|
|
|
5
5
|
import sys
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import TYPE_CHECKING
|
|
9
8
|
|
|
10
9
|
import click
|
|
11
10
|
|
|
12
11
|
from ..core.cli_utils import (
|
|
13
|
-
batch_processing_options,
|
|
14
12
|
collect_video_files,
|
|
15
|
-
common_output_options,
|
|
16
13
|
generate_batch_output_paths,
|
|
17
|
-
quality_option,
|
|
18
|
-
verbose_option,
|
|
19
14
|
)
|
|
20
15
|
from .api import (
|
|
21
16
|
DropJumpVideoConfig,
|
|
@@ -24,9 +19,6 @@ from .api import (
|
|
|
24
19
|
process_dropjump_videos_bulk,
|
|
25
20
|
)
|
|
26
21
|
|
|
27
|
-
if TYPE_CHECKING:
|
|
28
|
-
from .api import AnalysisOverrides
|
|
29
|
-
|
|
30
22
|
|
|
31
23
|
@dataclass
|
|
32
24
|
class AnalysisParameters:
|
|
@@ -43,10 +35,64 @@ class AnalysisParameters:
|
|
|
43
35
|
|
|
44
36
|
@click.command(name="dropjump-analyze")
|
|
45
37
|
@click.argument("video_path", nargs=-1, type=click.Path(exists=False), required=True)
|
|
46
|
-
@
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
38
|
+
@click.option(
|
|
39
|
+
"--output",
|
|
40
|
+
"-o",
|
|
41
|
+
type=click.Path(),
|
|
42
|
+
help="Path for debug video output (optional)",
|
|
43
|
+
)
|
|
44
|
+
@click.option(
|
|
45
|
+
"--json-output",
|
|
46
|
+
"-j",
|
|
47
|
+
type=click.Path(),
|
|
48
|
+
help="Path for JSON metrics output (default: stdout)",
|
|
49
|
+
)
|
|
50
|
+
@click.option(
|
|
51
|
+
"--quality",
|
|
52
|
+
type=click.Choice(["fast", "balanced", "accurate"], case_sensitive=False),
|
|
53
|
+
default="balanced",
|
|
54
|
+
help=(
|
|
55
|
+
"Analysis quality preset: "
|
|
56
|
+
"fast (quick, less precise), "
|
|
57
|
+
"balanced (default, good for most cases), "
|
|
58
|
+
"accurate (research-grade, slower)"
|
|
59
|
+
),
|
|
60
|
+
show_default=True,
|
|
61
|
+
)
|
|
62
|
+
@click.option(
|
|
63
|
+
"--verbose",
|
|
64
|
+
"-v",
|
|
65
|
+
is_flag=True,
|
|
66
|
+
help="Show auto-selected parameters and analysis details",
|
|
67
|
+
)
|
|
68
|
+
# Batch processing options
|
|
69
|
+
@click.option(
|
|
70
|
+
"--batch",
|
|
71
|
+
is_flag=True,
|
|
72
|
+
help="Enable batch processing mode for multiple videos",
|
|
73
|
+
)
|
|
74
|
+
@click.option(
|
|
75
|
+
"--workers",
|
|
76
|
+
type=int,
|
|
77
|
+
default=4,
|
|
78
|
+
help="Number of parallel workers for batch processing (default: 4)",
|
|
79
|
+
show_default=True,
|
|
80
|
+
)
|
|
81
|
+
@click.option(
|
|
82
|
+
"--output-dir",
|
|
83
|
+
type=click.Path(),
|
|
84
|
+
help="Directory for debug video outputs (batch mode only)",
|
|
85
|
+
)
|
|
86
|
+
@click.option(
|
|
87
|
+
"--json-output-dir",
|
|
88
|
+
type=click.Path(),
|
|
89
|
+
help="Directory for JSON metrics outputs (batch mode only)",
|
|
90
|
+
)
|
|
91
|
+
@click.option(
|
|
92
|
+
"--csv-summary",
|
|
93
|
+
type=click.Path(),
|
|
94
|
+
help="Path for CSV summary export (batch mode only)",
|
|
95
|
+
)
|
|
50
96
|
# Expert parameters (hidden in help, but always available for advanced users)
|
|
51
97
|
@click.option(
|
|
52
98
|
"--drop-start-frame",
|
|
@@ -179,34 +225,6 @@ def dropjump_analyze( # NOSONAR(S107) - Click CLI requires individual
|
|
|
179
225
|
)
|
|
180
226
|
|
|
181
227
|
|
|
182
|
-
def _create_overrides_if_needed(params: AnalysisParameters) -> "AnalysisOverrides | None":
|
|
183
|
-
"""Create AnalysisOverrides if any override parameters are set.
|
|
184
|
-
|
|
185
|
-
Args:
|
|
186
|
-
params: Expert parameters from CLI
|
|
187
|
-
|
|
188
|
-
Returns:
|
|
189
|
-
AnalysisOverrides if any relevant parameters are non-None, else None
|
|
190
|
-
"""
|
|
191
|
-
from .api import AnalysisOverrides
|
|
192
|
-
|
|
193
|
-
if any(
|
|
194
|
-
[
|
|
195
|
-
params.smoothing_window is not None,
|
|
196
|
-
params.velocity_threshold is not None,
|
|
197
|
-
params.min_contact_frames is not None,
|
|
198
|
-
params.visibility_threshold is not None,
|
|
199
|
-
]
|
|
200
|
-
):
|
|
201
|
-
return AnalysisOverrides(
|
|
202
|
-
smoothing_window=params.smoothing_window,
|
|
203
|
-
velocity_threshold=params.velocity_threshold,
|
|
204
|
-
min_contact_frames=params.min_contact_frames,
|
|
205
|
-
visibility_threshold=params.visibility_threshold,
|
|
206
|
-
)
|
|
207
|
-
return None
|
|
208
|
-
|
|
209
|
-
|
|
210
228
|
def _process_single(
|
|
211
229
|
video_path: str,
|
|
212
230
|
output: str | None,
|
|
@@ -219,7 +237,24 @@ def _process_single(
|
|
|
219
237
|
click.echo(f"Analyzing video: {video_path}", err=True)
|
|
220
238
|
|
|
221
239
|
try:
|
|
222
|
-
|
|
240
|
+
# Create AnalysisOverrides if any expert parameters are set
|
|
241
|
+
from .api import AnalysisOverrides
|
|
242
|
+
|
|
243
|
+
overrides = None
|
|
244
|
+
if any(
|
|
245
|
+
[
|
|
246
|
+
expert_params.smoothing_window is not None,
|
|
247
|
+
expert_params.velocity_threshold is not None,
|
|
248
|
+
expert_params.min_contact_frames is not None,
|
|
249
|
+
expert_params.visibility_threshold is not None,
|
|
250
|
+
]
|
|
251
|
+
):
|
|
252
|
+
overrides = AnalysisOverrides(
|
|
253
|
+
smoothing_window=expert_params.smoothing_window,
|
|
254
|
+
velocity_threshold=expert_params.velocity_threshold,
|
|
255
|
+
min_contact_frames=expert_params.min_contact_frames,
|
|
256
|
+
visibility_threshold=expert_params.visibility_threshold,
|
|
257
|
+
)
|
|
223
258
|
|
|
224
259
|
# Call the API function (handles all processing logic)
|
|
225
260
|
metrics = process_dropjump_video(
|
|
@@ -293,7 +328,24 @@ def _create_video_configs(
|
|
|
293
328
|
video_file, output_dir, json_output_dir
|
|
294
329
|
)
|
|
295
330
|
|
|
296
|
-
|
|
331
|
+
# Create AnalysisOverrides if any expert parameters are set
|
|
332
|
+
from .api import AnalysisOverrides
|
|
333
|
+
|
|
334
|
+
overrides = None
|
|
335
|
+
if any(
|
|
336
|
+
[
|
|
337
|
+
expert_params.smoothing_window is not None,
|
|
338
|
+
expert_params.velocity_threshold is not None,
|
|
339
|
+
expert_params.min_contact_frames is not None,
|
|
340
|
+
expert_params.visibility_threshold is not None,
|
|
341
|
+
]
|
|
342
|
+
):
|
|
343
|
+
overrides = AnalysisOverrides(
|
|
344
|
+
smoothing_window=expert_params.smoothing_window,
|
|
345
|
+
velocity_threshold=expert_params.velocity_threshold,
|
|
346
|
+
min_contact_frames=expert_params.min_contact_frames,
|
|
347
|
+
visibility_threshold=expert_params.visibility_threshold,
|
|
348
|
+
)
|
|
297
349
|
|
|
298
350
|
config = DropJumpVideoConfig(
|
|
299
351
|
video_path=video_file,
|
|
@@ -328,33 +380,35 @@ def _compute_batch_statistics(results: list[DropJumpVideoResult]) -> None:
|
|
|
328
380
|
click.echo(f"Failed: {len(failed)}", err=True)
|
|
329
381
|
|
|
330
382
|
if successful:
|
|
331
|
-
# Calculate average metrics
|
|
332
|
-
|
|
333
|
-
r.metrics.ground_contact_time
|
|
334
|
-
for r in successful
|
|
335
|
-
if r.metrics and r.metrics.ground_contact_time is not None
|
|
336
|
-
]
|
|
337
|
-
flight_values = [
|
|
338
|
-
r.metrics.flight_time * 1000
|
|
339
|
-
for r in successful
|
|
340
|
-
if r.metrics and r.metrics.flight_time is not None
|
|
341
|
-
]
|
|
342
|
-
jump_values = [
|
|
343
|
-
r.metrics.jump_height
|
|
344
|
-
for r in successful
|
|
345
|
-
if r.metrics and r.metrics.jump_height is not None
|
|
383
|
+
# Calculate average metrics
|
|
384
|
+
with_gct = [
|
|
385
|
+
r for r in successful if r.metrics and r.metrics.ground_contact_time is not None
|
|
346
386
|
]
|
|
347
|
-
|
|
348
|
-
if
|
|
349
|
-
|
|
387
|
+
with_flight = [r for r in successful if r.metrics and r.metrics.flight_time is not None]
|
|
388
|
+
with_jump = [r for r in successful if r.metrics and r.metrics.jump_height is not None]
|
|
389
|
+
|
|
390
|
+
if with_gct:
|
|
391
|
+
avg_gct = sum(
|
|
392
|
+
r.metrics.ground_contact_time * 1000
|
|
393
|
+
for r in with_gct
|
|
394
|
+
if r.metrics and r.metrics.ground_contact_time is not None
|
|
395
|
+
) / len(with_gct)
|
|
350
396
|
click.echo(f"\nAverage ground contact time: {avg_gct:.1f} ms", err=True)
|
|
351
397
|
|
|
352
|
-
if
|
|
353
|
-
avg_flight = sum(
|
|
398
|
+
if with_flight:
|
|
399
|
+
avg_flight = sum(
|
|
400
|
+
r.metrics.flight_time * 1000
|
|
401
|
+
for r in with_flight
|
|
402
|
+
if r.metrics and r.metrics.flight_time is not None
|
|
403
|
+
) / len(with_flight)
|
|
354
404
|
click.echo(f"Average flight time: {avg_flight:.1f} ms", err=True)
|
|
355
405
|
|
|
356
|
-
if
|
|
357
|
-
avg_jump = sum(
|
|
406
|
+
if with_jump:
|
|
407
|
+
avg_jump = sum(
|
|
408
|
+
r.metrics.jump_height
|
|
409
|
+
for r in with_jump
|
|
410
|
+
if r.metrics and r.metrics.jump_height is not None
|
|
411
|
+
) / len(with_jump)
|
|
358
412
|
click.echo(
|
|
359
413
|
f"Average jump height: {avg_jump:.3f} m ({avg_jump * 100:.1f} cm)",
|
|
360
414
|
err=True,
|
|
@@ -399,27 +453,38 @@ def _create_csv_row_from_result(result: DropJumpVideoResult) -> list[str]:
|
|
|
399
453
|
processing_time = f"{result.processing_time:.2f}"
|
|
400
454
|
|
|
401
455
|
if result.success and result.metrics:
|
|
402
|
-
|
|
456
|
+
return [
|
|
457
|
+
video_name,
|
|
403
458
|
_format_time_metric(result.metrics.ground_contact_time),
|
|
404
459
|
_format_time_metric(result.metrics.flight_time),
|
|
405
460
|
_format_distance_metric(result.metrics.jump_height),
|
|
461
|
+
processing_time,
|
|
462
|
+
"Success",
|
|
463
|
+
]
|
|
464
|
+
else:
|
|
465
|
+
return [
|
|
466
|
+
video_name,
|
|
467
|
+
"N/A",
|
|
468
|
+
"N/A",
|
|
469
|
+
"N/A",
|
|
470
|
+
processing_time,
|
|
471
|
+
f"Failed: {result.error}",
|
|
406
472
|
]
|
|
407
|
-
return [video_name, *metrics_data, processing_time, "Success"]
|
|
408
|
-
|
|
409
|
-
return [video_name, "N/A", "N/A", "N/A", processing_time, f"Failed: {result.error}"]
|
|
410
473
|
|
|
411
474
|
|
|
412
475
|
def _write_csv_summary(
|
|
413
476
|
csv_summary: str | None,
|
|
414
477
|
results: list[DropJumpVideoResult],
|
|
478
|
+
successful: list[DropJumpVideoResult],
|
|
415
479
|
) -> None:
|
|
416
480
|
"""Write CSV summary of batch processing results.
|
|
417
481
|
|
|
418
482
|
Args:
|
|
419
483
|
csv_summary: Path to CSV output file
|
|
420
484
|
results: All processing results
|
|
485
|
+
successful: Successful processing results
|
|
421
486
|
"""
|
|
422
|
-
if not csv_summary:
|
|
487
|
+
if not csv_summary or not successful:
|
|
423
488
|
return
|
|
424
489
|
|
|
425
490
|
click.echo(f"\nExporting CSV summary to: {csv_summary}", err=True)
|
|
@@ -493,6 +558,7 @@ def _process_batch(
|
|
|
493
558
|
_compute_batch_statistics(results)
|
|
494
559
|
|
|
495
560
|
# Export CSV summary if requested
|
|
496
|
-
|
|
561
|
+
successful = [r for r in results if r.success]
|
|
562
|
+
_write_csv_summary(csv_summary, results, successful)
|
|
497
563
|
|
|
498
564
|
click.echo("\nBatch processing complete!", err=True)
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Debug overlay rendering for drop jump analysis."""
|
|
2
|
+
|
|
3
|
+
import cv2
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from ..core.debug_overlay_utils import BaseDebugOverlayRenderer
|
|
7
|
+
from ..core.pose import compute_center_of_mass
|
|
8
|
+
from .analysis import ContactState, compute_average_foot_position
|
|
9
|
+
from .kinematics import DropJumpMetrics
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DebugOverlayRenderer(BaseDebugOverlayRenderer):
|
|
13
|
+
"""Renders debug information on video frames."""
|
|
14
|
+
|
|
15
|
+
def _draw_com_visualization(
|
|
16
|
+
self,
|
|
17
|
+
frame: np.ndarray,
|
|
18
|
+
landmarks: dict[str, tuple[float, float, float]],
|
|
19
|
+
contact_state: ContactState,
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Draw center of mass visualization on frame."""
|
|
22
|
+
com_x, com_y, _ = compute_center_of_mass(landmarks)
|
|
23
|
+
px = int(com_x * self.width)
|
|
24
|
+
py = int(com_y * self.height)
|
|
25
|
+
|
|
26
|
+
color = (0, 255, 0) if contact_state == ContactState.ON_GROUND else (0, 0, 255)
|
|
27
|
+
cv2.circle(frame, (px, py), 15, color, -1)
|
|
28
|
+
cv2.circle(frame, (px, py), 17, (255, 255, 255), 2)
|
|
29
|
+
|
|
30
|
+
# Draw hip midpoint reference
|
|
31
|
+
if "left_hip" in landmarks and "right_hip" in landmarks:
|
|
32
|
+
lh_x, lh_y, _ = landmarks["left_hip"]
|
|
33
|
+
rh_x, rh_y, _ = landmarks["right_hip"]
|
|
34
|
+
hip_x = int((lh_x + rh_x) / 2 * self.width)
|
|
35
|
+
hip_y = int((lh_y + rh_y) / 2 * self.height)
|
|
36
|
+
cv2.circle(frame, (hip_x, hip_y), 8, (255, 165, 0), -1)
|
|
37
|
+
cv2.line(frame, (hip_x, hip_y), (px, py), (255, 165, 0), 2)
|
|
38
|
+
|
|
39
|
+
def _draw_foot_visualization(
|
|
40
|
+
self,
|
|
41
|
+
frame: np.ndarray,
|
|
42
|
+
landmarks: dict[str, tuple[float, float, float]],
|
|
43
|
+
contact_state: ContactState,
|
|
44
|
+
) -> None:
|
|
45
|
+
"""Draw foot position visualization on frame."""
|
|
46
|
+
foot_x, foot_y = compute_average_foot_position(landmarks)
|
|
47
|
+
px = int(foot_x * self.width)
|
|
48
|
+
py = int(foot_y * self.height)
|
|
49
|
+
|
|
50
|
+
color = (0, 255, 0) if contact_state == ContactState.ON_GROUND else (0, 0, 255)
|
|
51
|
+
cv2.circle(frame, (px, py), 10, color, -1)
|
|
52
|
+
|
|
53
|
+
# Draw individual foot landmarks
|
|
54
|
+
foot_keys = ["left_ankle", "right_ankle", "left_heel", "right_heel"]
|
|
55
|
+
for key in foot_keys:
|
|
56
|
+
if key in landmarks:
|
|
57
|
+
x, y, vis = landmarks[key]
|
|
58
|
+
if vis > 0.5:
|
|
59
|
+
lx = int(x * self.width)
|
|
60
|
+
ly = int(y * self.height)
|
|
61
|
+
cv2.circle(frame, (lx, ly), 5, (255, 255, 0), -1)
|
|
62
|
+
|
|
63
|
+
def _draw_phase_labels(
|
|
64
|
+
self,
|
|
65
|
+
frame: np.ndarray,
|
|
66
|
+
frame_idx: int,
|
|
67
|
+
metrics: DropJumpMetrics,
|
|
68
|
+
) -> None:
|
|
69
|
+
"""Draw phase labels (ground contact, flight, peak) on frame."""
|
|
70
|
+
y_offset = 110
|
|
71
|
+
|
|
72
|
+
# Ground contact phase
|
|
73
|
+
if (
|
|
74
|
+
metrics.contact_start_frame
|
|
75
|
+
and metrics.contact_end_frame
|
|
76
|
+
and metrics.contact_start_frame <= frame_idx <= metrics.contact_end_frame
|
|
77
|
+
):
|
|
78
|
+
cv2.putText(
|
|
79
|
+
frame,
|
|
80
|
+
"GROUND CONTACT",
|
|
81
|
+
(10, y_offset),
|
|
82
|
+
cv2.FONT_HERSHEY_SIMPLEX,
|
|
83
|
+
0.7,
|
|
84
|
+
(0, 255, 0),
|
|
85
|
+
2,
|
|
86
|
+
)
|
|
87
|
+
y_offset += 40
|
|
88
|
+
|
|
89
|
+
# Flight phase
|
|
90
|
+
if (
|
|
91
|
+
metrics.flight_start_frame
|
|
92
|
+
and metrics.flight_end_frame
|
|
93
|
+
and metrics.flight_start_frame <= frame_idx <= metrics.flight_end_frame
|
|
94
|
+
):
|
|
95
|
+
cv2.putText(
|
|
96
|
+
frame,
|
|
97
|
+
"FLIGHT PHASE",
|
|
98
|
+
(10, y_offset),
|
|
99
|
+
cv2.FONT_HERSHEY_SIMPLEX,
|
|
100
|
+
0.7,
|
|
101
|
+
(0, 0, 255),
|
|
102
|
+
2,
|
|
103
|
+
)
|
|
104
|
+
y_offset += 40
|
|
105
|
+
|
|
106
|
+
# Peak height
|
|
107
|
+
if metrics.peak_height_frame == frame_idx:
|
|
108
|
+
cv2.putText(
|
|
109
|
+
frame,
|
|
110
|
+
"PEAK HEIGHT",
|
|
111
|
+
(10, y_offset),
|
|
112
|
+
cv2.FONT_HERSHEY_SIMPLEX,
|
|
113
|
+
0.7,
|
|
114
|
+
(255, 0, 255),
|
|
115
|
+
2,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def render_frame(
|
|
119
|
+
self,
|
|
120
|
+
frame: np.ndarray,
|
|
121
|
+
landmarks: dict[str, tuple[float, float, float]] | None,
|
|
122
|
+
contact_state: ContactState,
|
|
123
|
+
frame_idx: int,
|
|
124
|
+
metrics: DropJumpMetrics | None = None,
|
|
125
|
+
use_com: bool = False,
|
|
126
|
+
) -> np.ndarray:
|
|
127
|
+
"""
|
|
128
|
+
Render debug overlay on frame.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
frame: Original video frame
|
|
132
|
+
landmarks: Pose landmarks for this frame
|
|
133
|
+
contact_state: Ground contact state
|
|
134
|
+
frame_idx: Current frame index
|
|
135
|
+
metrics: Drop-jump metrics (optional)
|
|
136
|
+
use_com: Whether to visualize CoM instead of feet (optional)
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Frame with debug overlay
|
|
140
|
+
"""
|
|
141
|
+
with self.timer.measure("debug_video_copy"):
|
|
142
|
+
annotated = frame.copy()
|
|
143
|
+
|
|
144
|
+
def _draw_overlays() -> None:
|
|
145
|
+
# Draw landmarks
|
|
146
|
+
if landmarks:
|
|
147
|
+
if use_com:
|
|
148
|
+
self._draw_com_visualization(annotated, landmarks, contact_state)
|
|
149
|
+
else:
|
|
150
|
+
self._draw_foot_visualization(annotated, landmarks, contact_state)
|
|
151
|
+
|
|
152
|
+
# Draw contact state
|
|
153
|
+
state_color = (0, 255, 0) if contact_state == ContactState.ON_GROUND else (0, 0, 255)
|
|
154
|
+
cv2.putText(
|
|
155
|
+
annotated,
|
|
156
|
+
f"State: {contact_state.value}",
|
|
157
|
+
(10, 30),
|
|
158
|
+
cv2.FONT_HERSHEY_SIMPLEX,
|
|
159
|
+
1,
|
|
160
|
+
state_color,
|
|
161
|
+
2,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Draw frame number
|
|
165
|
+
cv2.putText(
|
|
166
|
+
annotated,
|
|
167
|
+
f"Frame: {frame_idx}",
|
|
168
|
+
(10, 70),
|
|
169
|
+
cv2.FONT_HERSHEY_SIMPLEX,
|
|
170
|
+
0.7,
|
|
171
|
+
(255, 255, 255),
|
|
172
|
+
2,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Draw phase labels
|
|
176
|
+
if metrics:
|
|
177
|
+
self._draw_phase_labels(annotated, frame_idx, metrics)
|
|
178
|
+
|
|
179
|
+
with self.timer.measure("debug_video_draw"):
|
|
180
|
+
_draw_overlays()
|
|
181
|
+
|
|
182
|
+
return annotated
|