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.

Files changed (48) hide show
  1. kinemotion/__init__.py +31 -6
  2. kinemotion/api.py +39 -598
  3. kinemotion/cli.py +2 -0
  4. kinemotion/cmj/__init__.py +5 -0
  5. kinemotion/cmj/analysis.py +621 -0
  6. kinemotion/cmj/api.py +563 -0
  7. kinemotion/cmj/cli.py +324 -0
  8. kinemotion/cmj/debug_overlay.py +457 -0
  9. kinemotion/cmj/joint_angles.py +307 -0
  10. kinemotion/cmj/kinematics.py +360 -0
  11. kinemotion/cmj/metrics_validator.py +767 -0
  12. kinemotion/cmj/validation_bounds.py +341 -0
  13. kinemotion/core/__init__.py +28 -0
  14. kinemotion/core/auto_tuning.py +71 -37
  15. kinemotion/core/cli_utils.py +60 -0
  16. kinemotion/core/debug_overlay_utils.py +385 -0
  17. kinemotion/core/determinism.py +83 -0
  18. kinemotion/core/experimental.py +103 -0
  19. kinemotion/core/filtering.py +9 -6
  20. kinemotion/core/formatting.py +75 -0
  21. kinemotion/core/metadata.py +231 -0
  22. kinemotion/core/model_downloader.py +172 -0
  23. kinemotion/core/pipeline_utils.py +433 -0
  24. kinemotion/core/pose.py +298 -141
  25. kinemotion/core/pose_landmarks.py +67 -0
  26. kinemotion/core/quality.py +393 -0
  27. kinemotion/core/smoothing.py +250 -154
  28. kinemotion/core/timing.py +247 -0
  29. kinemotion/core/types.py +42 -0
  30. kinemotion/core/validation.py +201 -0
  31. kinemotion/core/video_io.py +135 -50
  32. kinemotion/dropjump/__init__.py +1 -1
  33. kinemotion/dropjump/analysis.py +367 -182
  34. kinemotion/dropjump/api.py +665 -0
  35. kinemotion/dropjump/cli.py +156 -466
  36. kinemotion/dropjump/debug_overlay.py +136 -206
  37. kinemotion/dropjump/kinematics.py +232 -255
  38. kinemotion/dropjump/metrics_validator.py +240 -0
  39. kinemotion/dropjump/validation_bounds.py +157 -0
  40. kinemotion/models/__init__.py +0 -0
  41. kinemotion/models/pose_landmarker_lite.task +0 -0
  42. kinemotion-0.67.0.dist-info/METADATA +726 -0
  43. kinemotion-0.67.0.dist-info/RECORD +47 -0
  44. {kinemotion-0.10.6.dist-info → kinemotion-0.67.0.dist-info}/WHEEL +1 -1
  45. kinemotion-0.10.6.dist-info/METADATA +0 -561
  46. kinemotion-0.10.6.dist-info/RECORD +0 -20
  47. {kinemotion-0.10.6.dist-info → kinemotion-0.67.0.dist-info}/entry_points.txt +0 -0
  48. {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 __init__(
15
+ def _draw_com_visualization(
15
16
  self,
16
- output_path: str,
17
- width: int,
18
- height: int,
19
- display_width: int,
20
- display_height: int,
21
- fps: float,
22
- ):
23
- """
24
- Initialize overlay renderer.
25
-
26
- Args:
27
- output_path: Path for output video
28
- width: Encoded frame width (from source video)
29
- height: Encoded frame height (from source video)
30
- display_width: Display width (considering SAR)
31
- display_height: Display height (considering SAR)
32
- fps: Frames per second
33
- """
34
- self.width = width
35
- self.height = height
36
- self.display_width = display_width
37
- self.display_height = display_height
38
- self.needs_resize = (display_width != width) or (display_height != height)
39
-
40
- # Try H.264 codec first (better quality/compatibility), fallback to mp4v
41
- fourcc = cv2.VideoWriter_fourcc(*"avc1")
42
- # IMPORTANT: cv2.VideoWriter expects (width, height) tuple - NOT (height, width)
43
- # Write at display dimensions so video displays correctly without SAR metadata
44
- self.writer = cv2.VideoWriter(
45
- output_path, fourcc, fps, (display_width, display_height)
46
- )
47
-
48
- # Check if writer opened successfully, fallback to mp4v if not
49
- if not self.writer.isOpened():
50
- fourcc = cv2.VideoWriter_fourcc(*"mp4v")
51
- self.writer = cv2.VideoWriter(
52
- output_path, fourcc, fps, (display_width, display_height)
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
- if not self.writer.isOpened():
56
- raise ValueError(
57
- f"Failed to create video writer for {output_path} with dimensions "
58
- f"{display_width}x{display_height}"
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
- annotated = frame.copy()
85
-
86
- # Draw landmarks if available
87
- if landmarks:
88
- if use_com:
89
- # Draw center of mass position
90
- com_x, com_y, _ = compute_center_of_mass(landmarks) # com_vis not used
91
- px = int(com_x * self.width)
92
- py = int(com_y * self.height)
93
-
94
- # Draw CoM with larger circle
95
- color = (
96
- (0, 255, 0)
97
- if contact_state == ContactState.ON_GROUND
98
- else (0, 0, 255)
99
- )
100
- cv2.circle(annotated, (px, py), 15, color, -1)
101
- cv2.circle(annotated, (px, py), 17, (255, 255, 255), 2) # White border
102
-
103
- # Draw body segments for reference
104
- # Draw hip midpoint
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
- # Resize to display dimensions if needed (to handle SAR)
235
- if self.needs_resize:
236
- frame = cv2.resize(
237
- frame,
238
- (self.display_width, self.display_height),
239
- interpolation=cv2.INTER_LANCZOS4,
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
- self.writer.write(frame)
243
-
244
- def close(self) -> None:
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
- def __enter__(self) -> "DebugOverlayRenderer":
249
- return self
179
+ with self.timer.measure("debug_video_draw"):
180
+ _draw_overlays()
250
181
 
251
- def __exit__(self, exc_type, exc_val, exc_tb) -> None: # type: ignore[no-untyped-def]
252
- self.close()
182
+ return annotated