kinemotion 0.71.0__py3-none-any.whl → 0.71.1__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.
- kinemotion/cmj/analysis.py +2 -4
- kinemotion/cmj/api.py +9 -7
- kinemotion/cmj/debug_overlay.py +154 -286
- kinemotion/cmj/joint_angles.py +96 -31
- kinemotion/cmj/metrics_validator.py +22 -29
- kinemotion/cmj/validation_bounds.py +1 -18
- kinemotion/core/__init__.py +0 -2
- kinemotion/core/auto_tuning.py +91 -99
- kinemotion/core/debug_overlay_utils.py +142 -15
- kinemotion/core/experimental.py +55 -51
- kinemotion/core/filtering.py +15 -11
- kinemotion/core/overlay_constants.py +61 -0
- kinemotion/core/pose.py +47 -98
- kinemotion/core/smoothing.py +65 -51
- kinemotion/core/types.py +15 -0
- kinemotion/core/validation.py +6 -7
- kinemotion/core/video_io.py +14 -9
- kinemotion/dropjump/__init__.py +2 -2
- kinemotion/dropjump/analysis.py +67 -44
- kinemotion/dropjump/api.py +5 -17
- kinemotion/dropjump/cli.py +62 -78
- kinemotion/dropjump/debug_overlay.py +124 -65
- kinemotion/dropjump/validation_bounds.py +1 -1
- {kinemotion-0.71.0.dist-info → kinemotion-0.71.1.dist-info}/METADATA +1 -1
- kinemotion-0.71.1.dist-info/RECORD +50 -0
- kinemotion-0.71.0.dist-info/RECORD +0 -49
- {kinemotion-0.71.0.dist-info → kinemotion-0.71.1.dist-info}/WHEEL +0 -0
- {kinemotion-0.71.0.dist-info → kinemotion-0.71.1.dist-info}/entry_points.txt +0 -0
- {kinemotion-0.71.0.dist-info → kinemotion-0.71.1.dist-info}/licenses/LICENSE +0 -0
kinemotion/dropjump/cli.py
CHANGED
|
@@ -5,6 +5,7 @@ import json
|
|
|
5
5
|
import sys
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
8
9
|
|
|
9
10
|
import click
|
|
10
11
|
|
|
@@ -19,6 +20,9 @@ from .api import (
|
|
|
19
20
|
process_dropjump_videos_bulk,
|
|
20
21
|
)
|
|
21
22
|
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from .api import AnalysisOverrides
|
|
25
|
+
|
|
22
26
|
|
|
23
27
|
@dataclass
|
|
24
28
|
class AnalysisParameters:
|
|
@@ -225,6 +229,34 @@ def dropjump_analyze( # NOSONAR(S107) - Click CLI requires individual
|
|
|
225
229
|
)
|
|
226
230
|
|
|
227
231
|
|
|
232
|
+
def _create_overrides_if_needed(params: AnalysisParameters) -> "AnalysisOverrides | None":
|
|
233
|
+
"""Create AnalysisOverrides if any override parameters are set.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
params: Expert parameters from CLI
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
AnalysisOverrides if any relevant parameters are non-None, else None
|
|
240
|
+
"""
|
|
241
|
+
from .api import AnalysisOverrides
|
|
242
|
+
|
|
243
|
+
if any(
|
|
244
|
+
[
|
|
245
|
+
params.smoothing_window is not None,
|
|
246
|
+
params.velocity_threshold is not None,
|
|
247
|
+
params.min_contact_frames is not None,
|
|
248
|
+
params.visibility_threshold is not None,
|
|
249
|
+
]
|
|
250
|
+
):
|
|
251
|
+
return AnalysisOverrides(
|
|
252
|
+
smoothing_window=params.smoothing_window,
|
|
253
|
+
velocity_threshold=params.velocity_threshold,
|
|
254
|
+
min_contact_frames=params.min_contact_frames,
|
|
255
|
+
visibility_threshold=params.visibility_threshold,
|
|
256
|
+
)
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
|
|
228
260
|
def _process_single(
|
|
229
261
|
video_path: str,
|
|
230
262
|
output: str | None,
|
|
@@ -237,24 +269,7 @@ def _process_single(
|
|
|
237
269
|
click.echo(f"Analyzing video: {video_path}", err=True)
|
|
238
270
|
|
|
239
271
|
try:
|
|
240
|
-
|
|
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
|
-
)
|
|
272
|
+
overrides = _create_overrides_if_needed(expert_params)
|
|
258
273
|
|
|
259
274
|
# Call the API function (handles all processing logic)
|
|
260
275
|
metrics = process_dropjump_video(
|
|
@@ -328,24 +343,7 @@ def _create_video_configs(
|
|
|
328
343
|
video_file, output_dir, json_output_dir
|
|
329
344
|
)
|
|
330
345
|
|
|
331
|
-
|
|
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
|
-
)
|
|
346
|
+
overrides = _create_overrides_if_needed(expert_params)
|
|
349
347
|
|
|
350
348
|
config = DropJumpVideoConfig(
|
|
351
349
|
video_path=video_file,
|
|
@@ -380,35 +378,33 @@ def _compute_batch_statistics(results: list[DropJumpVideoResult]) -> None:
|
|
|
380
378
|
click.echo(f"Failed: {len(failed)}", err=True)
|
|
381
379
|
|
|
382
380
|
if successful:
|
|
383
|
-
# Calculate average metrics
|
|
384
|
-
|
|
385
|
-
r
|
|
381
|
+
# Calculate average metrics from results with non-None values
|
|
382
|
+
gct_values = [
|
|
383
|
+
r.metrics.ground_contact_time * 1000
|
|
384
|
+
for r in successful
|
|
385
|
+
if r.metrics and r.metrics.ground_contact_time is not None
|
|
386
|
+
]
|
|
387
|
+
flight_values = [
|
|
388
|
+
r.metrics.flight_time * 1000
|
|
389
|
+
for r in successful
|
|
390
|
+
if r.metrics and r.metrics.flight_time is not None
|
|
386
391
|
]
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
) / len(with_gct)
|
|
392
|
+
jump_values = [
|
|
393
|
+
r.metrics.jump_height
|
|
394
|
+
for r in successful
|
|
395
|
+
if r.metrics and r.metrics.jump_height is not None
|
|
396
|
+
]
|
|
397
|
+
|
|
398
|
+
if gct_values:
|
|
399
|
+
avg_gct = sum(gct_values) / len(gct_values)
|
|
396
400
|
click.echo(f"\nAverage ground contact time: {avg_gct:.1f} ms", err=True)
|
|
397
401
|
|
|
398
|
-
if
|
|
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)
|
|
402
|
+
if flight_values:
|
|
403
|
+
avg_flight = sum(flight_values) / len(flight_values)
|
|
404
404
|
click.echo(f"Average flight time: {avg_flight:.1f} ms", err=True)
|
|
405
405
|
|
|
406
|
-
if
|
|
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)
|
|
406
|
+
if jump_values:
|
|
407
|
+
avg_jump = sum(jump_values) / len(jump_values)
|
|
412
408
|
click.echo(
|
|
413
409
|
f"Average jump height: {avg_jump:.3f} m ({avg_jump * 100:.1f} cm)",
|
|
414
410
|
err=True,
|
|
@@ -453,38 +449,27 @@ def _create_csv_row_from_result(result: DropJumpVideoResult) -> list[str]:
|
|
|
453
449
|
processing_time = f"{result.processing_time:.2f}"
|
|
454
450
|
|
|
455
451
|
if result.success and result.metrics:
|
|
456
|
-
|
|
457
|
-
video_name,
|
|
452
|
+
metrics_data = [
|
|
458
453
|
_format_time_metric(result.metrics.ground_contact_time),
|
|
459
454
|
_format_time_metric(result.metrics.flight_time),
|
|
460
455
|
_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}",
|
|
472
456
|
]
|
|
457
|
+
return [video_name, *metrics_data, processing_time, "Success"]
|
|
458
|
+
|
|
459
|
+
return [video_name, "N/A", "N/A", "N/A", processing_time, f"Failed: {result.error}"]
|
|
473
460
|
|
|
474
461
|
|
|
475
462
|
def _write_csv_summary(
|
|
476
463
|
csv_summary: str | None,
|
|
477
464
|
results: list[DropJumpVideoResult],
|
|
478
|
-
successful: list[DropJumpVideoResult],
|
|
479
465
|
) -> None:
|
|
480
466
|
"""Write CSV summary of batch processing results.
|
|
481
467
|
|
|
482
468
|
Args:
|
|
483
469
|
csv_summary: Path to CSV output file
|
|
484
470
|
results: All processing results
|
|
485
|
-
successful: Successful processing results
|
|
486
471
|
"""
|
|
487
|
-
if not csv_summary
|
|
472
|
+
if not csv_summary:
|
|
488
473
|
return
|
|
489
474
|
|
|
490
475
|
click.echo(f"\nExporting CSV summary to: {csv_summary}", err=True)
|
|
@@ -558,7 +543,6 @@ def _process_batch(
|
|
|
558
543
|
_compute_batch_statistics(results)
|
|
559
544
|
|
|
560
545
|
# Export CSV summary if requested
|
|
561
|
-
|
|
562
|
-
_write_csv_summary(csv_summary, results, successful)
|
|
546
|
+
_write_csv_summary(csv_summary, results)
|
|
563
547
|
|
|
564
548
|
click.echo("\nBatch processing complete!", err=True)
|
|
@@ -4,61 +4,80 @@ import cv2
|
|
|
4
4
|
import numpy as np
|
|
5
5
|
|
|
6
6
|
from ..core.debug_overlay_utils import BaseDebugOverlayRenderer
|
|
7
|
+
from ..core.overlay_constants import (
|
|
8
|
+
BLACK,
|
|
9
|
+
COM_CIRCLE_RADIUS,
|
|
10
|
+
COM_OUTLINE_RADIUS,
|
|
11
|
+
CYAN,
|
|
12
|
+
FOOT_CIRCLE_RADIUS,
|
|
13
|
+
FOOT_LANDMARK_RADIUS,
|
|
14
|
+
FOOT_VISIBILITY_THRESHOLD,
|
|
15
|
+
GREEN,
|
|
16
|
+
HIP_MARKER_RADIUS,
|
|
17
|
+
METRICS_BOX_WIDTH,
|
|
18
|
+
ORANGE,
|
|
19
|
+
PHASE_LABEL_LINE_HEIGHT,
|
|
20
|
+
PHASE_LABEL_START_Y,
|
|
21
|
+
RED,
|
|
22
|
+
WHITE,
|
|
23
|
+
Color,
|
|
24
|
+
LandmarkDict,
|
|
25
|
+
)
|
|
7
26
|
from ..core.pose import compute_center_of_mass
|
|
8
27
|
from .analysis import ContactState, compute_average_foot_position
|
|
9
28
|
from .kinematics import DropJumpMetrics
|
|
10
29
|
|
|
11
30
|
|
|
12
|
-
class
|
|
31
|
+
class DropJumpDebugOverlayRenderer(BaseDebugOverlayRenderer):
|
|
13
32
|
"""Renders debug information on video frames."""
|
|
14
33
|
|
|
34
|
+
def _get_contact_state_color(self, contact_state: ContactState) -> Color:
|
|
35
|
+
"""Get color based on ground contact state."""
|
|
36
|
+
return GREEN if contact_state == ContactState.ON_GROUND else RED
|
|
37
|
+
|
|
15
38
|
def _draw_com_visualization(
|
|
16
39
|
self,
|
|
17
40
|
frame: np.ndarray,
|
|
18
|
-
landmarks:
|
|
41
|
+
landmarks: LandmarkDict,
|
|
19
42
|
contact_state: ContactState,
|
|
20
43
|
) -> None:
|
|
21
44
|
"""Draw center of mass visualization on frame."""
|
|
22
45
|
com_x, com_y, _ = compute_center_of_mass(landmarks)
|
|
23
|
-
px =
|
|
24
|
-
py = int(com_y * self.height)
|
|
46
|
+
px, py = self._normalize_to_pixels(com_x, com_y)
|
|
25
47
|
|
|
26
|
-
color = (
|
|
27
|
-
cv2.circle(frame, (px, py),
|
|
28
|
-
cv2.circle(frame, (px, py),
|
|
48
|
+
color = self._get_contact_state_color(contact_state)
|
|
49
|
+
cv2.circle(frame, (px, py), COM_CIRCLE_RADIUS, color, -1)
|
|
50
|
+
cv2.circle(frame, (px, py), COM_OUTLINE_RADIUS, WHITE, 2)
|
|
29
51
|
|
|
30
52
|
# Draw hip midpoint reference
|
|
31
53
|
if "left_hip" in landmarks and "right_hip" in landmarks:
|
|
32
54
|
lh_x, lh_y, _ = landmarks["left_hip"]
|
|
33
55
|
rh_x, rh_y, _ = landmarks["right_hip"]
|
|
34
|
-
hip_x =
|
|
35
|
-
|
|
36
|
-
cv2.
|
|
37
|
-
cv2.line(frame, (hip_x, hip_y), (px, py), (255, 165, 0), 2)
|
|
56
|
+
hip_x, hip_y = self._normalize_to_pixels((lh_x + rh_x) / 2, (lh_y + rh_y) / 2)
|
|
57
|
+
cv2.circle(frame, (hip_x, hip_y), HIP_MARKER_RADIUS, ORANGE, -1)
|
|
58
|
+
cv2.line(frame, (hip_x, hip_y), (px, py), ORANGE, 2)
|
|
38
59
|
|
|
39
60
|
def _draw_foot_visualization(
|
|
40
61
|
self,
|
|
41
62
|
frame: np.ndarray,
|
|
42
|
-
landmarks:
|
|
63
|
+
landmarks: LandmarkDict,
|
|
43
64
|
contact_state: ContactState,
|
|
44
65
|
) -> None:
|
|
45
66
|
"""Draw foot position visualization on frame."""
|
|
46
67
|
foot_x, foot_y = compute_average_foot_position(landmarks)
|
|
47
|
-
px =
|
|
48
|
-
py = int(foot_y * self.height)
|
|
68
|
+
px, py = self._normalize_to_pixels(foot_x, foot_y)
|
|
49
69
|
|
|
50
|
-
color = (
|
|
51
|
-
cv2.circle(frame, (px, py),
|
|
70
|
+
color = self._get_contact_state_color(contact_state)
|
|
71
|
+
cv2.circle(frame, (px, py), FOOT_CIRCLE_RADIUS, color, -1)
|
|
52
72
|
|
|
53
73
|
# Draw individual foot landmarks
|
|
54
74
|
foot_keys = ["left_ankle", "right_ankle", "left_heel", "right_heel"]
|
|
55
75
|
for key in foot_keys:
|
|
56
76
|
if key in landmarks:
|
|
57
77
|
x, y, vis = landmarks[key]
|
|
58
|
-
if vis >
|
|
59
|
-
lx =
|
|
60
|
-
ly
|
|
61
|
-
cv2.circle(frame, (lx, ly), 5, (255, 255, 0), -1)
|
|
78
|
+
if vis > FOOT_VISIBILITY_THRESHOLD:
|
|
79
|
+
lx, ly = self._normalize_to_pixels(x, y)
|
|
80
|
+
cv2.circle(frame, (lx, ly), FOOT_LANDMARK_RADIUS, CYAN, -1)
|
|
62
81
|
|
|
63
82
|
def _draw_phase_labels(
|
|
64
83
|
self,
|
|
@@ -67,58 +86,99 @@ class DebugOverlayRenderer(BaseDebugOverlayRenderer):
|
|
|
67
86
|
metrics: DropJumpMetrics,
|
|
68
87
|
) -> None:
|
|
69
88
|
"""Draw phase labels (ground contact, flight, peak) on frame."""
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
metrics.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
89
|
+
# Phase configurations: (start_frame, end_frame, label, color)
|
|
90
|
+
# For range-based phases (ground contact, flight)
|
|
91
|
+
range_phase_configs = [
|
|
92
|
+
(metrics.contact_start_frame, metrics.contact_end_frame, "GROUND CONTACT", GREEN),
|
|
93
|
+
(metrics.flight_start_frame, metrics.flight_end_frame, "FLIGHT PHASE", RED),
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
y_offset = PHASE_LABEL_START_Y
|
|
97
|
+
for start_frame, end_frame, label, color in range_phase_configs:
|
|
98
|
+
if start_frame and end_frame and start_frame <= frame_idx <= end_frame:
|
|
99
|
+
cv2.putText(
|
|
100
|
+
frame,
|
|
101
|
+
label,
|
|
102
|
+
(10, y_offset),
|
|
103
|
+
cv2.FONT_HERSHEY_SIMPLEX,
|
|
104
|
+
0.7,
|
|
105
|
+
color,
|
|
106
|
+
2,
|
|
107
|
+
)
|
|
108
|
+
y_offset += PHASE_LABEL_LINE_HEIGHT
|
|
109
|
+
|
|
110
|
+
# Single-frame indicator (peak height)
|
|
111
|
+
if metrics.peak_height_frame == frame_idx:
|
|
78
112
|
cv2.putText(
|
|
79
113
|
frame,
|
|
80
|
-
"
|
|
114
|
+
"PEAK HEIGHT",
|
|
81
115
|
(10, y_offset),
|
|
82
116
|
cv2.FONT_HERSHEY_SIMPLEX,
|
|
83
117
|
0.7,
|
|
84
|
-
(0, 255,
|
|
118
|
+
(255, 0, 255), # Magenta
|
|
85
119
|
2,
|
|
86
120
|
)
|
|
87
|
-
y_offset += 40
|
|
88
121
|
|
|
89
|
-
|
|
122
|
+
def _draw_info_box(
|
|
123
|
+
self,
|
|
124
|
+
frame: np.ndarray,
|
|
125
|
+
top_left: tuple[int, int],
|
|
126
|
+
bottom_right: tuple[int, int],
|
|
127
|
+
border_color: Color,
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Draw a filled box with border for displaying information."""
|
|
130
|
+
cv2.rectangle(frame, top_left, bottom_right, BLACK, -1)
|
|
131
|
+
cv2.rectangle(frame, top_left, bottom_right, border_color, 2)
|
|
132
|
+
|
|
133
|
+
def _draw_metrics_summary(
|
|
134
|
+
self, frame: np.ndarray, frame_idx: int, metrics: DropJumpMetrics
|
|
135
|
+
) -> None:
|
|
136
|
+
"""Draw metrics summary in bottom right after flight phase ends."""
|
|
137
|
+
if metrics.flight_end_frame is None or frame_idx < metrics.flight_end_frame:
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
# Build metrics text list
|
|
141
|
+
metrics_text: list[str] = []
|
|
142
|
+
|
|
143
|
+
if metrics.ground_contact_time is not None:
|
|
144
|
+
metrics_text.append(f"Contact Time: {metrics.ground_contact_time * 1000:.0f}ms")
|
|
145
|
+
|
|
146
|
+
if metrics.flight_time is not None:
|
|
147
|
+
metrics_text.append(f"Flight Time: {metrics.flight_time * 1000:.0f}ms")
|
|
148
|
+
|
|
149
|
+
if metrics.jump_height is not None:
|
|
150
|
+
metrics_text.append(f"Jump Height: {metrics.jump_height:.3f}m")
|
|
151
|
+
|
|
152
|
+
# Calculate RSI (Reactive Strength Index)
|
|
90
153
|
if (
|
|
91
|
-
metrics.
|
|
92
|
-
and metrics.
|
|
93
|
-
and metrics.
|
|
154
|
+
metrics.jump_height is not None
|
|
155
|
+
and metrics.ground_contact_time is not None
|
|
156
|
+
and metrics.ground_contact_time > 0
|
|
94
157
|
):
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
158
|
+
rsi = metrics.jump_height / metrics.ground_contact_time
|
|
159
|
+
metrics_text.append(f"RSI: {rsi:.2f}")
|
|
105
160
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
161
|
+
if not metrics_text:
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
# Calculate box dimensions
|
|
165
|
+
box_height = len(metrics_text) * 30 + 20
|
|
166
|
+
top_left = (self.width - METRICS_BOX_WIDTH, self.height - box_height - 10)
|
|
167
|
+
bottom_right = (self.width - 10, self.height - 10)
|
|
168
|
+
|
|
169
|
+
self._draw_info_box(frame, top_left, bottom_right, GREEN)
|
|
170
|
+
|
|
171
|
+
# Draw metrics text
|
|
172
|
+
text_x = self.width - METRICS_BOX_WIDTH + 10
|
|
173
|
+
text_y = self.height - box_height + 10
|
|
174
|
+
for text in metrics_text:
|
|
175
|
+
cv2.putText(frame, text, (text_x, text_y), cv2.FONT_HERSHEY_SIMPLEX, 0.6, WHITE, 1)
|
|
176
|
+
text_y += 30
|
|
117
177
|
|
|
118
178
|
def render_frame(
|
|
119
179
|
self,
|
|
120
180
|
frame: np.ndarray,
|
|
121
|
-
landmarks:
|
|
181
|
+
landmarks: LandmarkDict | None,
|
|
122
182
|
contact_state: ContactState,
|
|
123
183
|
frame_idx: int,
|
|
124
184
|
metrics: DropJumpMetrics | None = None,
|
|
@@ -141,16 +201,17 @@ class DebugOverlayRenderer(BaseDebugOverlayRenderer):
|
|
|
141
201
|
with self.timer.measure("debug_video_copy"):
|
|
142
202
|
annotated = frame.copy()
|
|
143
203
|
|
|
144
|
-
|
|
145
|
-
# Draw landmarks
|
|
204
|
+
with self.timer.measure("debug_video_draw"):
|
|
205
|
+
# Draw skeleton and landmarks
|
|
146
206
|
if landmarks:
|
|
207
|
+
self._draw_skeleton(annotated, landmarks)
|
|
147
208
|
if use_com:
|
|
148
209
|
self._draw_com_visualization(annotated, landmarks, contact_state)
|
|
149
210
|
else:
|
|
150
211
|
self._draw_foot_visualization(annotated, landmarks, contact_state)
|
|
151
212
|
|
|
152
213
|
# Draw contact state
|
|
153
|
-
state_color = (
|
|
214
|
+
state_color = self._get_contact_state_color(contact_state)
|
|
154
215
|
cv2.putText(
|
|
155
216
|
annotated,
|
|
156
217
|
f"State: {contact_state.value}",
|
|
@@ -168,15 +229,13 @@ class DebugOverlayRenderer(BaseDebugOverlayRenderer):
|
|
|
168
229
|
(10, 70),
|
|
169
230
|
cv2.FONT_HERSHEY_SIMPLEX,
|
|
170
231
|
0.7,
|
|
171
|
-
|
|
232
|
+
WHITE,
|
|
172
233
|
2,
|
|
173
234
|
)
|
|
174
235
|
|
|
175
|
-
# Draw phase labels
|
|
236
|
+
# Draw phase labels and metrics summary
|
|
176
237
|
if metrics:
|
|
177
238
|
self._draw_phase_labels(annotated, frame_idx, metrics)
|
|
178
|
-
|
|
179
|
-
with self.timer.measure("debug_video_draw"):
|
|
180
|
-
_draw_overlays()
|
|
239
|
+
self._draw_metrics_summary(annotated, frame_idx, metrics)
|
|
181
240
|
|
|
182
241
|
return annotated
|
|
@@ -124,7 +124,7 @@ def _classify_combined_score(combined_score: float) -> AthleteProfile:
|
|
|
124
124
|
return AthleteProfile.ELITE
|
|
125
125
|
|
|
126
126
|
|
|
127
|
-
def estimate_athlete_profile(metrics: MetricsDict,
|
|
127
|
+
def estimate_athlete_profile(metrics: MetricsDict, _gender: str | None = None) -> AthleteProfile:
|
|
128
128
|
"""Estimate athlete profile from drop jump metrics.
|
|
129
129
|
|
|
130
130
|
Uses jump_height and contact_time to classify athlete level.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kinemotion
|
|
3
|
-
Version: 0.71.
|
|
3
|
+
Version: 0.71.1
|
|
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
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
kinemotion/__init__.py,sha256=HkD8habCcfxGobxZcACOStla-L1nYHMIZp0th00Q3E8,1061
|
|
2
|
+
kinemotion/api.py,sha256=uG1e4bTnj2c-6cbZJEZ_LjMwFdaG32ba2KcK_XjE_NI,1040
|
|
3
|
+
kinemotion/cli.py,sha256=_Us9krSce4GUKtlLIPrFUhKmPWURzeJ1-ydR_YU2VGw,626
|
|
4
|
+
kinemotion/cmj/__init__.py,sha256=SkAw9ka8Yd1Qfv9hcvk22m3EfucROzYrSNGNF5kDzho,113
|
|
5
|
+
kinemotion/cmj/analysis.py,sha256=EQydClIbNkIj-FmCZGaPQe-COVW8fbO3139i9z1vomA,23643
|
|
6
|
+
kinemotion/cmj/api.py,sha256=P_lbqEqAKPO5n1Xn4IQZKNj9nLaO3ljkN2PgqvExGXU,18435
|
|
7
|
+
kinemotion/cmj/cli.py,sha256=P2b77IIw6kqTSIkncxlShzhmjIwqMFBNd-pZxYP-TsI,9918
|
|
8
|
+
kinemotion/cmj/debug_overlay.py,sha256=vF5Apiz8zDRpgrVzf52manLW99m1kHQAPSdUkar5rPs,11474
|
|
9
|
+
kinemotion/cmj/joint_angles.py,sha256=by5M4LDtUfd2_Z9DmcgUl0nsvarsBYjgsE8KWWYcn08,11255
|
|
10
|
+
kinemotion/cmj/kinematics.py,sha256=KwA8uSj3g1SeNf0NXMSHsp3gIw6Gfa-6QWIwdYdRXYw,13362
|
|
11
|
+
kinemotion/cmj/metrics_validator.py,sha256=IQofafpwLCXER3ucZXNfiJKFFKPOVxXnC4BNLHOMnNY,30013
|
|
12
|
+
kinemotion/cmj/validation_bounds.py,sha256=-0iXDhH-RntiGZi_Co22V6qtA5D-hLzkrPkVcfoNd2U,11343
|
|
13
|
+
kinemotion/core/__init__.py,sha256=8hMvfNK7v_eqswuk_J5s5FRGvPtp2-R4kasVMGchFkM,1766
|
|
14
|
+
kinemotion/core/auto_tuning.py,sha256=dF2opupuphbTd6sZIDyXX8hwedLaNlMiH-hT7PGqnfU,10251
|
|
15
|
+
kinemotion/core/cli_utils.py,sha256=sQPbT6XWWau-sm9yuN5c3eS5xNzoQGGXwSz6hQXtRvM,1859
|
|
16
|
+
kinemotion/core/debug_overlay_utils.py,sha256=QaVkHuFZpXUrdiMlm8ylQn6baJOj8jcZeiV4kDqODt0,17441
|
|
17
|
+
kinemotion/core/determinism.py,sha256=Frw-KAOvAxTL_XtxoWpXCjMbQPUKEAusK6JctlkeuRo,2509
|
|
18
|
+
kinemotion/core/experimental.py,sha256=G1EpkmWQ8d-rPaN1n0P7mF6XUzrbW0Br3nVkIzJ1D9M,3694
|
|
19
|
+
kinemotion/core/filtering.py,sha256=7KUeclXqZpNQA8WKNocDwhCxZpwwtizI3wvAEyq9SBo,11603
|
|
20
|
+
kinemotion/core/formatting.py,sha256=G_3eqgOtym9RFOZVEwCxye4A2cyrmgvtQ214vIshowU,2480
|
|
21
|
+
kinemotion/core/metadata.py,sha256=bJAVa4nym__zx1hNowSZduMGKBSGOPxTbBQkjm6N0D0,7207
|
|
22
|
+
kinemotion/core/model_downloader.py,sha256=mqhJBHGaNe0aN9qbcBqvcTk9FDd7xaHqEcwD-fyP89c,5205
|
|
23
|
+
kinemotion/core/overlay_constants.py,sha256=zZreHHWe00p2XuCJsbRFqN6g-AAUAnx53LwKqHm1Bl8,1438
|
|
24
|
+
kinemotion/core/pipeline_utils.py,sha256=B5jMXoiLaTh02uGA2MIe1uZLVSRGZ5nxbARuvdrjDrQ,15161
|
|
25
|
+
kinemotion/core/pose.py,sha256=Z795p0EnaTUeWHO8FuApFcMGTLwZ47JOjs5f5TzRvdk,14224
|
|
26
|
+
kinemotion/core/pose_landmarks.py,sha256=LcEbL5K5xKia6dCzWf6Ft18UIE1CLMMqCZ3KUjwUDzM,1558
|
|
27
|
+
kinemotion/core/quality.py,sha256=VUkRL2N6B7lfIZ2pE9han_U68JwarmZz1U0ygHkgkhE,13022
|
|
28
|
+
kinemotion/core/smoothing.py,sha256=F1DCsnvPBi62XJLygOJ5MkNlRa7BCLg_E9ORtCWcoKk,16562
|
|
29
|
+
kinemotion/core/timing.py,sha256=ITX77q4hbtajRuWfgwYhws8nCvOeKFlEdKjCu8lD9_w,7938
|
|
30
|
+
kinemotion/core/types.py,sha256=m141buSkEsqflt5VFaTHtRq_IcimjI3_T_EfaNpIVxY,1652
|
|
31
|
+
kinemotion/core/validation.py,sha256=rrhpI24Iq8WGtNaMg0beTWMbEGccdKF-f-pk-FCKJzI,6749
|
|
32
|
+
kinemotion/core/video_io.py,sha256=84IxC1n3HvYK28MSa5fqumdzlPDhP8k9IPB3OCvWku0,9198
|
|
33
|
+
kinemotion/dropjump/__init__.py,sha256=yBbEbPdY6sqozWtTvfbvuUZnrVWSSjBp61xK34M29F4,878
|
|
34
|
+
kinemotion/dropjump/analysis.py,sha256=Tc41jVctG4zJZOyYqM1SiM95mnF2xz4vcieGJ6vYi2M,29099
|
|
35
|
+
kinemotion/dropjump/api.py,sha256=5qBj05e6Zo-H4-UjBOIt_CYyDqLVcPhwyyLG04eJYMU,20639
|
|
36
|
+
kinemotion/dropjump/cli.py,sha256=FaBX637x7VcLcB8HupaZCkVS7sp8C0YuaKM0h-DBNIA,15906
|
|
37
|
+
kinemotion/dropjump/debug_overlay.py,sha256=X4mvCi5Qi1gnvSZZAsUs-0ZRUx9mVBbEUznOFO21HO8,8470
|
|
38
|
+
kinemotion/dropjump/kinematics.py,sha256=dx4PuXKfKMKcsc_HX6sXj8rHXf9ksiZIOAIkJ4vBlY4,19637
|
|
39
|
+
kinemotion/dropjump/metrics_validator.py,sha256=lSfo4Lm5FHccl8ijUP6SA-kcSh50LS9hF8UIyWxcnW8,9243
|
|
40
|
+
kinemotion/dropjump/validation_bounds.py,sha256=k31qy-kCXTiCTx0RPo2t8yZ-faLxqGO-AeF05QfBFb0,5125
|
|
41
|
+
kinemotion/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
42
|
+
kinemotion/models/pose_landmarker_lite.task,sha256=WZKeHR7pUodzXd2DOxnPSsRtKbx6_du_Z1PEWWkNV0o,5777746
|
|
43
|
+
kinemotion/models/rtmpose-s_simcc-body7_pt-body7-halpe26_700e-256x192-7f134165_20230605.onnx,sha256=dfZTq8kbhv8RxWiXS0HUIJNCUpxYTBN45dFIorPflEs,133
|
|
44
|
+
kinemotion/models/yolox_tiny_8xb8-300e_humanart-6f3252f9.onnx,sha256=UsutHVQ6GP3X5pCcp52EN8q7o2J3d-TnxZqlF48kY6I,133
|
|
45
|
+
kinemotion/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
46
|
+
kinemotion-0.71.1.dist-info/METADATA,sha256=D2ZS9pNSi3NAaSkYKoQ09PZaJ-B3qWDHPLChu3oxdbY,26125
|
|
47
|
+
kinemotion-0.71.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
48
|
+
kinemotion-0.71.1.dist-info/entry_points.txt,sha256=zaqnAnjLvcdrk1Qvj5nvXZCZ2gp0prS7it1zTJygcIY,50
|
|
49
|
+
kinemotion-0.71.1.dist-info/licenses/LICENSE,sha256=KZajvqsHw0NoOHOi2q0FZ4NBe9HdV6oey-IPYAtHXfg,1088
|
|
50
|
+
kinemotion-0.71.1.dist-info/RECORD,,
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
kinemotion/__init__.py,sha256=HkD8habCcfxGobxZcACOStla-L1nYHMIZp0th00Q3E8,1061
|
|
2
|
-
kinemotion/api.py,sha256=uG1e4bTnj2c-6cbZJEZ_LjMwFdaG32ba2KcK_XjE_NI,1040
|
|
3
|
-
kinemotion/cli.py,sha256=_Us9krSce4GUKtlLIPrFUhKmPWURzeJ1-ydR_YU2VGw,626
|
|
4
|
-
kinemotion/cmj/__init__.py,sha256=SkAw9ka8Yd1Qfv9hcvk22m3EfucROzYrSNGNF5kDzho,113
|
|
5
|
-
kinemotion/cmj/analysis.py,sha256=WcFNJVd9zpwvDrbe41VshXqP9MFfptlgYD4Nph5mHLA,23675
|
|
6
|
-
kinemotion/cmj/api.py,sha256=5PDV_vX3k63Ko4OEttyUkV4fWyklyy-CG_UyNKgNoyY,18476
|
|
7
|
-
kinemotion/cmj/cli.py,sha256=P2b77IIw6kqTSIkncxlShzhmjIwqMFBNd-pZxYP-TsI,9918
|
|
8
|
-
kinemotion/cmj/debug_overlay.py,sha256=bX9aPLhXiLCCMZW9v8Y4OiOAaZO0i-UGr-Pl8HCsmbI,15810
|
|
9
|
-
kinemotion/cmj/joint_angles.py,sha256=HmheIEiKcQz39cRezk4h-htorOhGNPsqKIR9RsAEKts,9960
|
|
10
|
-
kinemotion/cmj/kinematics.py,sha256=KwA8uSj3g1SeNf0NXMSHsp3gIw6Gfa-6QWIwdYdRXYw,13362
|
|
11
|
-
kinemotion/cmj/metrics_validator.py,sha256=JWuWFfDXyZMTHXFWVdI0MhaQjMR3cjd01tTrDy_if2U,30290
|
|
12
|
-
kinemotion/cmj/validation_bounds.py,sha256=Ry915JdInPXbqjaVGNY_urnDO1PAkCSJqHwNKRq-VkU,12048
|
|
13
|
-
kinemotion/core/__init__.py,sha256=8WB7tAJPKOxgNzbhIEOnGnkRr0CcdNeTnz91Jsiyafo,1812
|
|
14
|
-
kinemotion/core/auto_tuning.py,sha256=lhAqPc-eLjMYx9BCvKdECE7TD2Dweb9KcifV6JHaXOE,11278
|
|
15
|
-
kinemotion/core/cli_utils.py,sha256=sQPbT6XWWau-sm9yuN5c3eS5xNzoQGGXwSz6hQXtRvM,1859
|
|
16
|
-
kinemotion/core/debug_overlay_utils.py,sha256=D4aT8xstThPcV2i5D4KJZJEttW6E_4GE5QiERqe1MwI,13049
|
|
17
|
-
kinemotion/core/determinism.py,sha256=Frw-KAOvAxTL_XtxoWpXCjMbQPUKEAusK6JctlkeuRo,2509
|
|
18
|
-
kinemotion/core/experimental.py,sha256=IK05AF4aZS15ke85hF3TWCqRIXU1AlD_XKzFz735Ua8,3640
|
|
19
|
-
kinemotion/core/filtering.py,sha256=Oc__pV6iHEGyyovbqa5SUi-6v8QyvaRVwA0LRayM884,11355
|
|
20
|
-
kinemotion/core/formatting.py,sha256=G_3eqgOtym9RFOZVEwCxye4A2cyrmgvtQ214vIshowU,2480
|
|
21
|
-
kinemotion/core/metadata.py,sha256=bJAVa4nym__zx1hNowSZduMGKBSGOPxTbBQkjm6N0D0,7207
|
|
22
|
-
kinemotion/core/model_downloader.py,sha256=mqhJBHGaNe0aN9qbcBqvcTk9FDd7xaHqEcwD-fyP89c,5205
|
|
23
|
-
kinemotion/core/pipeline_utils.py,sha256=B5jMXoiLaTh02uGA2MIe1uZLVSRGZ5nxbARuvdrjDrQ,15161
|
|
24
|
-
kinemotion/core/pose.py,sha256=_qC4cbFD0Mp2JAGftZcY5AEDLgD2yRnTyRKD9bkqLI8,15306
|
|
25
|
-
kinemotion/core/pose_landmarks.py,sha256=LcEbL5K5xKia6dCzWf6Ft18UIE1CLMMqCZ3KUjwUDzM,1558
|
|
26
|
-
kinemotion/core/quality.py,sha256=VUkRL2N6B7lfIZ2pE9han_U68JwarmZz1U0ygHkgkhE,13022
|
|
27
|
-
kinemotion/core/smoothing.py,sha256=ELMHL7pzSqYffjnLDBUMBJIgt1AwOssDInE8IiXBbig,15942
|
|
28
|
-
kinemotion/core/timing.py,sha256=ITX77q4hbtajRuWfgwYhws8nCvOeKFlEdKjCu8lD9_w,7938
|
|
29
|
-
kinemotion/core/types.py,sha256=A_HclzKpf3By5DiJ0wY9B-dQJrIVAAhUfGab7qTSIL8,1279
|
|
30
|
-
kinemotion/core/validation.py,sha256=0xVv-ftWveV60fJ97kmZMuy2Qqqb5aZLR50dDIrjnhg,6773
|
|
31
|
-
kinemotion/core/video_io.py,sha256=TxdLUEpekGytesL3X3k78WWgZTOd5fuge30hU4Uy48Y,9198
|
|
32
|
-
kinemotion/dropjump/__init__.py,sha256=tC3H3BrCg8Oj-db-Vrtx4PH_llR1Ppkd5jwaOjhQcLg,862
|
|
33
|
-
kinemotion/dropjump/analysis.py,sha256=YomuoJF_peyrBSpeT89Q5_sBgY0kEDyq7TFrtEnRLjs,28049
|
|
34
|
-
kinemotion/dropjump/api.py,sha256=QlZxCrjOg78PXXip6Krb91RSYqH37x1AbBUy6U8uFt8,20833
|
|
35
|
-
kinemotion/dropjump/cli.py,sha256=gUef9nmyR5952h1WnfBGyCdFXQvzVTlCKYAjJGcO4sE,16819
|
|
36
|
-
kinemotion/dropjump/debug_overlay.py,sha256=9RQYXPRf0q2wdy6y2Ak2R4tpRceDwC8aJrXZzkmh3Wo,5942
|
|
37
|
-
kinemotion/dropjump/kinematics.py,sha256=dx4PuXKfKMKcsc_HX6sXj8rHXf9ksiZIOAIkJ4vBlY4,19637
|
|
38
|
-
kinemotion/dropjump/metrics_validator.py,sha256=lSfo4Lm5FHccl8ijUP6SA-kcSh50LS9hF8UIyWxcnW8,9243
|
|
39
|
-
kinemotion/dropjump/validation_bounds.py,sha256=x4yjcFxyvdMp5e7MkcoUosGLeGsxBh1Lft6h__AQ2G8,5124
|
|
40
|
-
kinemotion/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
41
|
-
kinemotion/models/pose_landmarker_lite.task,sha256=WZKeHR7pUodzXd2DOxnPSsRtKbx6_du_Z1PEWWkNV0o,5777746
|
|
42
|
-
kinemotion/models/rtmpose-s_simcc-body7_pt-body7-halpe26_700e-256x192-7f134165_20230605.onnx,sha256=dfZTq8kbhv8RxWiXS0HUIJNCUpxYTBN45dFIorPflEs,133
|
|
43
|
-
kinemotion/models/yolox_tiny_8xb8-300e_humanart-6f3252f9.onnx,sha256=UsutHVQ6GP3X5pCcp52EN8q7o2J3d-TnxZqlF48kY6I,133
|
|
44
|
-
kinemotion/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
45
|
-
kinemotion-0.71.0.dist-info/METADATA,sha256=sKhmkomkzrmp1YnX0SC9dzsRvDNlrVtVQ5npxSPWVeI,26125
|
|
46
|
-
kinemotion-0.71.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
47
|
-
kinemotion-0.71.0.dist-info/entry_points.txt,sha256=zaqnAnjLvcdrk1Qvj5nvXZCZ2gp0prS7it1zTJygcIY,50
|
|
48
|
-
kinemotion-0.71.0.dist-info/licenses/LICENSE,sha256=KZajvqsHw0NoOHOi2q0FZ4NBe9HdV6oey-IPYAtHXfg,1088
|
|
49
|
-
kinemotion-0.71.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|