kinemotion 0.10.6__py3-none-any.whl → 0.67.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 +31 -6
- kinemotion/api.py +39 -598
- kinemotion/cli.py +2 -0
- kinemotion/cmj/__init__.py +5 -0
- kinemotion/cmj/analysis.py +621 -0
- kinemotion/cmj/api.py +563 -0
- kinemotion/cmj/cli.py +324 -0
- kinemotion/cmj/debug_overlay.py +457 -0
- kinemotion/cmj/joint_angles.py +307 -0
- kinemotion/cmj/kinematics.py +360 -0
- kinemotion/cmj/metrics_validator.py +767 -0
- kinemotion/cmj/validation_bounds.py +341 -0
- kinemotion/core/__init__.py +28 -0
- kinemotion/core/auto_tuning.py +71 -37
- kinemotion/core/cli_utils.py +60 -0
- kinemotion/core/debug_overlay_utils.py +385 -0
- kinemotion/core/determinism.py +83 -0
- kinemotion/core/experimental.py +103 -0
- kinemotion/core/filtering.py +9 -6
- kinemotion/core/formatting.py +75 -0
- kinemotion/core/metadata.py +231 -0
- kinemotion/core/model_downloader.py +172 -0
- kinemotion/core/pipeline_utils.py +433 -0
- kinemotion/core/pose.py +298 -141
- kinemotion/core/pose_landmarks.py +67 -0
- kinemotion/core/quality.py +393 -0
- kinemotion/core/smoothing.py +250 -154
- kinemotion/core/timing.py +247 -0
- kinemotion/core/types.py +42 -0
- kinemotion/core/validation.py +201 -0
- kinemotion/core/video_io.py +135 -50
- kinemotion/dropjump/__init__.py +1 -1
- kinemotion/dropjump/analysis.py +367 -182
- kinemotion/dropjump/api.py +665 -0
- kinemotion/dropjump/cli.py +156 -466
- kinemotion/dropjump/debug_overlay.py +136 -206
- kinemotion/dropjump/kinematics.py +232 -255
- kinemotion/dropjump/metrics_validator.py +240 -0
- kinemotion/dropjump/validation_bounds.py +157 -0
- kinemotion/models/__init__.py +0 -0
- kinemotion/models/pose_landmarker_lite.task +0 -0
- kinemotion-0.67.0.dist-info/METADATA +726 -0
- kinemotion-0.67.0.dist-info/RECORD +47 -0
- {kinemotion-0.10.6.dist-info → kinemotion-0.67.0.dist-info}/WHEEL +1 -1
- kinemotion-0.10.6.dist-info/METADATA +0 -561
- kinemotion-0.10.6.dist-info/RECORD +0 -20
- {kinemotion-0.10.6.dist-info → kinemotion-0.67.0.dist-info}/entry_points.txt +0 -0
- {kinemotion-0.10.6.dist-info → kinemotion-0.67.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -3,59 +3,116 @@
|
|
|
3
3
|
import cv2
|
|
4
4
|
import numpy as np
|
|
5
5
|
|
|
6
|
+
from ..core.debug_overlay_utils import BaseDebugOverlayRenderer
|
|
6
7
|
from ..core.pose import compute_center_of_mass
|
|
7
8
|
from .analysis import ContactState, compute_average_foot_position
|
|
8
9
|
from .kinematics import DropJumpMetrics
|
|
9
10
|
|
|
10
11
|
|
|
11
|
-
class DebugOverlayRenderer:
|
|
12
|
+
class DebugOverlayRenderer(BaseDebugOverlayRenderer):
|
|
12
13
|
"""Renders debug information on video frames."""
|
|
13
14
|
|
|
14
|
-
def
|
|
15
|
+
def _draw_com_visualization(
|
|
15
16
|
self,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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,
|
|
53
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
|
|
54
105
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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,
|
|
59
116
|
)
|
|
60
117
|
|
|
61
118
|
def render_frame(
|
|
@@ -81,172 +138,45 @@ class DebugOverlayRenderer:
|
|
|
81
138
|
Returns:
|
|
82
139
|
Frame with debug overlay
|
|
83
140
|
"""
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
cv2.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if "left_hip" in landmarks and "right_hip" in landmarks:
|
|
106
|
-
lh_x, lh_y, _ = landmarks["left_hip"]
|
|
107
|
-
rh_x, rh_y, _ = landmarks["right_hip"]
|
|
108
|
-
hip_x = int((lh_x + rh_x) / 2 * self.width)
|
|
109
|
-
hip_y = int((lh_y + rh_y) / 2 * self.height)
|
|
110
|
-
cv2.circle(
|
|
111
|
-
annotated, (hip_x, hip_y), 8, (255, 165, 0), -1
|
|
112
|
-
) # Orange
|
|
113
|
-
# Draw line from hip to CoM
|
|
114
|
-
cv2.line(annotated, (hip_x, hip_y), (px, py), (255, 165, 0), 2)
|
|
115
|
-
else:
|
|
116
|
-
# Draw foot position (original method)
|
|
117
|
-
foot_x, foot_y = compute_average_foot_position(landmarks)
|
|
118
|
-
px = int(foot_x * self.width)
|
|
119
|
-
py = int(foot_y * self.height)
|
|
120
|
-
|
|
121
|
-
# Draw foot position circle
|
|
122
|
-
color = (
|
|
123
|
-
(0, 255, 0)
|
|
124
|
-
if contact_state == ContactState.ON_GROUND
|
|
125
|
-
else (0, 0, 255)
|
|
126
|
-
)
|
|
127
|
-
cv2.circle(annotated, (px, py), 10, color, -1)
|
|
128
|
-
|
|
129
|
-
# Draw individual foot landmarks
|
|
130
|
-
foot_keys = ["left_ankle", "right_ankle", "left_heel", "right_heel"]
|
|
131
|
-
for key in foot_keys:
|
|
132
|
-
if key in landmarks:
|
|
133
|
-
x, y, vis = landmarks[key]
|
|
134
|
-
if vis > 0.5:
|
|
135
|
-
lx = int(x * self.width)
|
|
136
|
-
ly = int(y * self.height)
|
|
137
|
-
cv2.circle(annotated, (lx, ly), 5, (255, 255, 0), -1)
|
|
138
|
-
|
|
139
|
-
# Draw contact state
|
|
140
|
-
state_text = f"State: {contact_state.value}"
|
|
141
|
-
state_color = (
|
|
142
|
-
(0, 255, 0) if contact_state == ContactState.ON_GROUND else (0, 0, 255)
|
|
143
|
-
)
|
|
144
|
-
cv2.putText(
|
|
145
|
-
annotated,
|
|
146
|
-
state_text,
|
|
147
|
-
(10, 30),
|
|
148
|
-
cv2.FONT_HERSHEY_SIMPLEX,
|
|
149
|
-
1,
|
|
150
|
-
state_color,
|
|
151
|
-
2,
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
# Draw frame number
|
|
155
|
-
cv2.putText(
|
|
156
|
-
annotated,
|
|
157
|
-
f"Frame: {frame_idx}",
|
|
158
|
-
(10, 70),
|
|
159
|
-
cv2.FONT_HERSHEY_SIMPLEX,
|
|
160
|
-
0.7,
|
|
161
|
-
(255, 255, 255),
|
|
162
|
-
2,
|
|
163
|
-
)
|
|
164
|
-
|
|
165
|
-
# Draw metrics if in relevant phase
|
|
166
|
-
if metrics:
|
|
167
|
-
y_offset = 110
|
|
168
|
-
if (
|
|
169
|
-
metrics.contact_start_frame
|
|
170
|
-
and metrics.contact_end_frame
|
|
171
|
-
and metrics.contact_start_frame
|
|
172
|
-
<= frame_idx
|
|
173
|
-
<= metrics.contact_end_frame
|
|
174
|
-
):
|
|
175
|
-
cv2.putText(
|
|
176
|
-
annotated,
|
|
177
|
-
"GROUND CONTACT",
|
|
178
|
-
(10, y_offset),
|
|
179
|
-
cv2.FONT_HERSHEY_SIMPLEX,
|
|
180
|
-
0.7,
|
|
181
|
-
(0, 255, 0),
|
|
182
|
-
2,
|
|
183
|
-
)
|
|
184
|
-
y_offset += 40
|
|
185
|
-
|
|
186
|
-
if (
|
|
187
|
-
metrics.flight_start_frame
|
|
188
|
-
and metrics.flight_end_frame
|
|
189
|
-
and metrics.flight_start_frame <= frame_idx <= metrics.flight_end_frame
|
|
190
|
-
):
|
|
191
|
-
cv2.putText(
|
|
192
|
-
annotated,
|
|
193
|
-
"FLIGHT PHASE",
|
|
194
|
-
(10, y_offset),
|
|
195
|
-
cv2.FONT_HERSHEY_SIMPLEX,
|
|
196
|
-
0.7,
|
|
197
|
-
(0, 0, 255),
|
|
198
|
-
2,
|
|
199
|
-
)
|
|
200
|
-
y_offset += 40
|
|
201
|
-
|
|
202
|
-
if metrics.peak_height_frame == frame_idx:
|
|
203
|
-
cv2.putText(
|
|
204
|
-
annotated,
|
|
205
|
-
"PEAK HEIGHT",
|
|
206
|
-
(10, y_offset),
|
|
207
|
-
cv2.FONT_HERSHEY_SIMPLEX,
|
|
208
|
-
0.7,
|
|
209
|
-
(255, 0, 255),
|
|
210
|
-
2,
|
|
211
|
-
)
|
|
212
|
-
|
|
213
|
-
return annotated
|
|
214
|
-
|
|
215
|
-
def write_frame(self, frame: np.ndarray) -> None:
|
|
216
|
-
"""
|
|
217
|
-
Write frame to output video.
|
|
218
|
-
|
|
219
|
-
Args:
|
|
220
|
-
frame: Video frame with shape (height, width, 3)
|
|
221
|
-
|
|
222
|
-
Raises:
|
|
223
|
-
ValueError: If frame dimensions don't match expected encoded dimensions
|
|
224
|
-
"""
|
|
225
|
-
# Validate frame dimensions match expected encoded dimensions
|
|
226
|
-
frame_height, frame_width = frame.shape[:2]
|
|
227
|
-
if frame_height != self.height or frame_width != self.width:
|
|
228
|
-
raise ValueError(
|
|
229
|
-
f"Frame dimensions ({frame_width}x{frame_height}) don't match "
|
|
230
|
-
f"source dimensions ({self.width}x{self.height}). "
|
|
231
|
-
f"Aspect ratio must be preserved from source video."
|
|
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,
|
|
232
162
|
)
|
|
233
163
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
(
|
|
239
|
-
|
|
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,
|
|
240
173
|
)
|
|
241
174
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
"""Release video writer."""
|
|
246
|
-
self.writer.release()
|
|
175
|
+
# Draw phase labels
|
|
176
|
+
if metrics:
|
|
177
|
+
self._draw_phase_labels(annotated, frame_idx, metrics)
|
|
247
178
|
|
|
248
|
-
|
|
249
|
-
|
|
179
|
+
with self.timer.measure("debug_video_draw"):
|
|
180
|
+
_draw_overlays()
|
|
250
181
|
|
|
251
|
-
|
|
252
|
-
self.close()
|
|
182
|
+
return annotated
|