kinemotion 0.11.5__py3-none-any.whl → 0.11.7__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/cmj/cli.py +2 -12
- kinemotion/cmj/debug_overlay.py +2 -85
- kinemotion/core/cli_utils.py +114 -0
- kinemotion/core/debug_overlay_utils.py +166 -0
- kinemotion/core/smoothing.py +167 -121
- kinemotion/dropjump/debug_overlay.py +2 -87
- {kinemotion-0.11.5.dist-info → kinemotion-0.11.7.dist-info}/METADATA +1 -1
- {kinemotion-0.11.5.dist-info → kinemotion-0.11.7.dist-info}/RECORD +11 -10
- {kinemotion-0.11.5.dist-info → kinemotion-0.11.7.dist-info}/WHEEL +0 -0
- {kinemotion-0.11.5.dist-info → kinemotion-0.11.7.dist-info}/entry_points.txt +0 -0
- {kinemotion-0.11.5.dist-info → kinemotion-0.11.7.dist-info}/licenses/LICENSE +0 -0
kinemotion/cmj/cli.py
CHANGED
|
@@ -17,6 +17,7 @@ from ..core.auto_tuning import (
|
|
|
17
17
|
)
|
|
18
18
|
from ..core.cli_utils import (
|
|
19
19
|
apply_expert_param_overrides,
|
|
20
|
+
common_output_options,
|
|
20
21
|
determine_initial_confidence,
|
|
21
22
|
print_auto_tuned_params,
|
|
22
23
|
smooth_landmark_sequence,
|
|
@@ -102,18 +103,7 @@ def _process_batch_videos(
|
|
|
102
103
|
|
|
103
104
|
@click.command(name="cmj-analyze")
|
|
104
105
|
@click.argument("video_path", nargs=-1, type=click.Path(exists=False), required=True)
|
|
105
|
-
@
|
|
106
|
-
"--output",
|
|
107
|
-
"-o",
|
|
108
|
-
type=click.Path(),
|
|
109
|
-
help="Path for debug video output (optional)",
|
|
110
|
-
)
|
|
111
|
-
@click.option(
|
|
112
|
-
"--json-output",
|
|
113
|
-
"-j",
|
|
114
|
-
type=click.Path(),
|
|
115
|
-
help="Path for JSON metrics output (default: stdout)",
|
|
116
|
-
)
|
|
106
|
+
@common_output_options
|
|
117
107
|
@click.option(
|
|
118
108
|
"--quality",
|
|
119
109
|
type=click.Choice(["fast", "balanced", "accurate"], case_sensitive=False),
|
kinemotion/cmj/debug_overlay.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import cv2
|
|
4
4
|
import numpy as np
|
|
5
5
|
|
|
6
|
+
from ..core.debug_overlay_utils import BaseDebugOverlayRenderer
|
|
6
7
|
from .joint_angles import calculate_triple_extension
|
|
7
8
|
from .kinematics import CMJMetrics
|
|
8
9
|
|
|
@@ -18,54 +19,9 @@ class CMJPhaseState:
|
|
|
18
19
|
LANDING = "landing"
|
|
19
20
|
|
|
20
21
|
|
|
21
|
-
class CMJDebugOverlayRenderer:
|
|
22
|
+
class CMJDebugOverlayRenderer(BaseDebugOverlayRenderer):
|
|
22
23
|
"""Renders debug information on CMJ video frames."""
|
|
23
24
|
|
|
24
|
-
def __init__(
|
|
25
|
-
self,
|
|
26
|
-
output_path: str,
|
|
27
|
-
width: int,
|
|
28
|
-
height: int,
|
|
29
|
-
display_width: int,
|
|
30
|
-
display_height: int,
|
|
31
|
-
fps: float,
|
|
32
|
-
):
|
|
33
|
-
"""
|
|
34
|
-
Initialize overlay renderer.
|
|
35
|
-
|
|
36
|
-
Args:
|
|
37
|
-
output_path: Path for output video
|
|
38
|
-
width: Encoded frame width (from source video)
|
|
39
|
-
height: Encoded frame height (from source video)
|
|
40
|
-
display_width: Display width (considering SAR)
|
|
41
|
-
display_height: Display height (considering SAR)
|
|
42
|
-
fps: Frames per second
|
|
43
|
-
"""
|
|
44
|
-
self.width = width
|
|
45
|
-
self.height = height
|
|
46
|
-
self.display_width = display_width
|
|
47
|
-
self.display_height = display_height
|
|
48
|
-
self.needs_resize = (display_width != width) or (display_height != height)
|
|
49
|
-
|
|
50
|
-
# Try H.264 codec first (better quality/compatibility), fallback to mp4v
|
|
51
|
-
fourcc = cv2.VideoWriter_fourcc(*"avc1")
|
|
52
|
-
self.writer = cv2.VideoWriter(
|
|
53
|
-
output_path, fourcc, fps, (display_width, display_height)
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
# Check if writer opened successfully, fallback to mp4v if not
|
|
57
|
-
if not self.writer.isOpened():
|
|
58
|
-
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
|
|
59
|
-
self.writer = cv2.VideoWriter(
|
|
60
|
-
output_path, fourcc, fps, (display_width, display_height)
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
if not self.writer.isOpened():
|
|
64
|
-
raise ValueError(
|
|
65
|
-
f"Failed to create video writer for {output_path} with dimensions "
|
|
66
|
-
f"{display_width}x{display_height}"
|
|
67
|
-
)
|
|
68
|
-
|
|
69
25
|
def _determine_phase(self, frame_idx: int, metrics: CMJMetrics) -> str:
|
|
70
26
|
"""Determine which phase the current frame is in."""
|
|
71
27
|
if metrics.standing_start_frame and frame_idx < metrics.standing_start_frame:
|
|
@@ -508,42 +464,3 @@ class CMJDebugOverlayRenderer:
|
|
|
508
464
|
self._draw_metrics_summary(annotated, frame_idx, metrics)
|
|
509
465
|
|
|
510
466
|
return annotated
|
|
511
|
-
|
|
512
|
-
def write_frame(self, frame: np.ndarray) -> None:
|
|
513
|
-
"""
|
|
514
|
-
Write frame to output video.
|
|
515
|
-
|
|
516
|
-
Args:
|
|
517
|
-
frame: Video frame with shape (height, width, 3)
|
|
518
|
-
|
|
519
|
-
Raises:
|
|
520
|
-
ValueError: If frame dimensions don't match expected encoded dimensions
|
|
521
|
-
"""
|
|
522
|
-
# Validate frame dimensions match expected encoded dimensions
|
|
523
|
-
frame_height, frame_width = frame.shape[:2]
|
|
524
|
-
if frame_height != self.height or frame_width != self.width:
|
|
525
|
-
raise ValueError(
|
|
526
|
-
f"Frame dimensions ({frame_width}x{frame_height}) don't match "
|
|
527
|
-
f"source dimensions ({self.width}x{self.height}). "
|
|
528
|
-
f"Aspect ratio must be preserved from source video."
|
|
529
|
-
)
|
|
530
|
-
|
|
531
|
-
# Resize to display dimensions if needed (to handle SAR)
|
|
532
|
-
if self.needs_resize:
|
|
533
|
-
frame = cv2.resize(
|
|
534
|
-
frame,
|
|
535
|
-
(self.display_width, self.display_height),
|
|
536
|
-
interpolation=cv2.INTER_LANCZOS4,
|
|
537
|
-
)
|
|
538
|
-
|
|
539
|
-
self.writer.write(frame)
|
|
540
|
-
|
|
541
|
-
def close(self) -> None:
|
|
542
|
-
"""Release video writer."""
|
|
543
|
-
self.writer.release()
|
|
544
|
-
|
|
545
|
-
def __enter__(self) -> "CMJDebugOverlayRenderer":
|
|
546
|
-
return self
|
|
547
|
-
|
|
548
|
-
def __exit__(self, exc_type: type, exc_val: Exception, exc_tb: object) -> None:
|
|
549
|
-
self.close()
|
kinemotion/core/cli_utils.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Shared CLI utilities for drop jump and CMJ analysis."""
|
|
2
2
|
|
|
3
|
+
from collections.abc import Callable
|
|
3
4
|
from typing import Any, Protocol
|
|
4
5
|
|
|
5
6
|
import click
|
|
@@ -190,3 +191,116 @@ def smooth_landmark_sequence(landmarks_sequence: list, params: AutoTunedParams)
|
|
|
190
191
|
window_length=params.smoothing_window,
|
|
191
192
|
polyorder=params.polyorder,
|
|
192
193
|
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def common_output_options(func: Callable) -> Callable: # type: ignore[type-arg]
|
|
197
|
+
"""Add common output options to CLI command."""
|
|
198
|
+
func = click.option(
|
|
199
|
+
"--output",
|
|
200
|
+
"-o",
|
|
201
|
+
type=click.Path(),
|
|
202
|
+
help="Path for debug video output (optional)",
|
|
203
|
+
)(func)
|
|
204
|
+
func = click.option(
|
|
205
|
+
"--json-output",
|
|
206
|
+
"-j",
|
|
207
|
+
type=click.Path(),
|
|
208
|
+
help="Path for JSON metrics output (default: stdout)",
|
|
209
|
+
)(func)
|
|
210
|
+
return func
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def common_quality_options(func: Callable) -> Callable: # type: ignore[type-arg]
|
|
214
|
+
"""Add quality and verbose options to CLI command."""
|
|
215
|
+
func = click.option(
|
|
216
|
+
"--quality",
|
|
217
|
+
type=click.Choice(["fast", "balanced", "accurate"], case_sensitive=False),
|
|
218
|
+
default="balanced",
|
|
219
|
+
help=(
|
|
220
|
+
"Analysis quality preset: "
|
|
221
|
+
"fast (quick, less precise), "
|
|
222
|
+
"balanced (default, good for most cases), "
|
|
223
|
+
"accurate (research-grade, slower)"
|
|
224
|
+
),
|
|
225
|
+
show_default=True,
|
|
226
|
+
)(func)
|
|
227
|
+
func = click.option(
|
|
228
|
+
"--verbose",
|
|
229
|
+
"-v",
|
|
230
|
+
is_flag=True,
|
|
231
|
+
help="Show auto-selected parameters and analysis details",
|
|
232
|
+
)(func)
|
|
233
|
+
return func
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def common_batch_options(func: Callable) -> Callable: # type: ignore[type-arg]
|
|
237
|
+
"""Add batch processing options to CLI command."""
|
|
238
|
+
func = click.option(
|
|
239
|
+
"--batch",
|
|
240
|
+
is_flag=True,
|
|
241
|
+
help="Enable batch processing mode for multiple videos",
|
|
242
|
+
)(func)
|
|
243
|
+
func = click.option(
|
|
244
|
+
"--workers",
|
|
245
|
+
type=int,
|
|
246
|
+
default=4,
|
|
247
|
+
help="Number of parallel workers for batch processing (default: 4)",
|
|
248
|
+
show_default=True,
|
|
249
|
+
)(func)
|
|
250
|
+
func = click.option(
|
|
251
|
+
"--output-dir",
|
|
252
|
+
type=click.Path(),
|
|
253
|
+
help="Directory for debug video outputs (batch mode only)",
|
|
254
|
+
)(func)
|
|
255
|
+
func = click.option(
|
|
256
|
+
"--json-output-dir",
|
|
257
|
+
type=click.Path(),
|
|
258
|
+
help="Directory for JSON metrics outputs (batch mode only)",
|
|
259
|
+
)(func)
|
|
260
|
+
func = click.option(
|
|
261
|
+
"--csv-summary",
|
|
262
|
+
type=click.Path(),
|
|
263
|
+
help="Path for CSV summary export (batch mode only)",
|
|
264
|
+
)(func)
|
|
265
|
+
return func
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def common_expert_options(func: Callable) -> Callable: # type: ignore[type-arg]
|
|
269
|
+
"""Add expert parameter options to CLI command."""
|
|
270
|
+
func = click.option(
|
|
271
|
+
"--smoothing-window",
|
|
272
|
+
type=int,
|
|
273
|
+
default=None,
|
|
274
|
+
help="[EXPERT] Override auto-tuned smoothing window size",
|
|
275
|
+
)(func)
|
|
276
|
+
func = click.option(
|
|
277
|
+
"--velocity-threshold",
|
|
278
|
+
type=float,
|
|
279
|
+
default=None,
|
|
280
|
+
help="[EXPERT] Override auto-tuned velocity threshold",
|
|
281
|
+
)(func)
|
|
282
|
+
func = click.option(
|
|
283
|
+
"--min-contact-frames",
|
|
284
|
+
type=int,
|
|
285
|
+
default=None,
|
|
286
|
+
help="[EXPERT] Override auto-tuned minimum contact frames",
|
|
287
|
+
)(func)
|
|
288
|
+
func = click.option(
|
|
289
|
+
"--visibility-threshold",
|
|
290
|
+
type=float,
|
|
291
|
+
default=None,
|
|
292
|
+
help="[EXPERT] Override visibility threshold for landmarks",
|
|
293
|
+
)(func)
|
|
294
|
+
func = click.option(
|
|
295
|
+
"--detection-confidence",
|
|
296
|
+
type=float,
|
|
297
|
+
default=None,
|
|
298
|
+
help="[EXPERT] Override MediaPipe detection confidence (0.0-1.0)",
|
|
299
|
+
)(func)
|
|
300
|
+
func = click.option(
|
|
301
|
+
"--tracking-confidence",
|
|
302
|
+
type=float,
|
|
303
|
+
default=None,
|
|
304
|
+
help="[EXPERT] Override MediaPipe tracking confidence (0.0-1.0)",
|
|
305
|
+
)(func)
|
|
306
|
+
return func
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Shared debug overlay utilities for video rendering."""
|
|
2
|
+
|
|
3
|
+
import cv2
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def create_video_writer(
|
|
8
|
+
output_path: str,
|
|
9
|
+
width: int,
|
|
10
|
+
height: int,
|
|
11
|
+
display_width: int,
|
|
12
|
+
display_height: int,
|
|
13
|
+
fps: float,
|
|
14
|
+
) -> tuple[cv2.VideoWriter, bool]:
|
|
15
|
+
"""
|
|
16
|
+
Create a video writer with fallback codec support.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
output_path: Path for output video
|
|
20
|
+
width: Encoded frame width (from source video)
|
|
21
|
+
height: Encoded frame height (from source video)
|
|
22
|
+
display_width: Display width (considering SAR)
|
|
23
|
+
display_height: Display height (considering SAR)
|
|
24
|
+
fps: Frames per second
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Tuple of (video_writer, needs_resize)
|
|
28
|
+
"""
|
|
29
|
+
needs_resize = (display_width != width) or (display_height != height)
|
|
30
|
+
|
|
31
|
+
# Try H.264 codec first (better quality/compatibility), fallback to mp4v
|
|
32
|
+
fourcc = cv2.VideoWriter_fourcc(*"avc1")
|
|
33
|
+
writer = cv2.VideoWriter(output_path, fourcc, fps, (display_width, display_height))
|
|
34
|
+
|
|
35
|
+
# Check if writer opened successfully, fallback to mp4v if not
|
|
36
|
+
if not writer.isOpened():
|
|
37
|
+
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
|
|
38
|
+
writer = cv2.VideoWriter(
|
|
39
|
+
output_path, fourcc, fps, (display_width, display_height)
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if not writer.isOpened():
|
|
43
|
+
raise ValueError(
|
|
44
|
+
f"Failed to create video writer for {output_path} with dimensions "
|
|
45
|
+
f"{display_width}x{display_height}"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
return writer, needs_resize
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def prepare_frame_for_overlay(
|
|
52
|
+
frame: np.ndarray, needs_resize: bool, display_width: int, display_height: int
|
|
53
|
+
) -> np.ndarray:
|
|
54
|
+
"""
|
|
55
|
+
Prepare frame for overlay rendering by resizing if needed.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
frame: Original video frame
|
|
59
|
+
needs_resize: Whether frame needs resizing
|
|
60
|
+
display_width: Target display width
|
|
61
|
+
display_height: Target display height
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Prepared frame ready for overlay
|
|
65
|
+
"""
|
|
66
|
+
# Apply SAR correction if needed
|
|
67
|
+
if needs_resize:
|
|
68
|
+
frame = cv2.resize(
|
|
69
|
+
frame, (display_width, display_height), interpolation=cv2.INTER_LINEAR
|
|
70
|
+
)
|
|
71
|
+
return frame
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def write_overlay_frame(
|
|
75
|
+
writer: cv2.VideoWriter, frame: np.ndarray, width: int, height: int
|
|
76
|
+
) -> None:
|
|
77
|
+
"""
|
|
78
|
+
Write a frame to the video writer with dimension validation.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
writer: Video writer instance
|
|
82
|
+
frame: Frame to write
|
|
83
|
+
width: Expected frame width
|
|
84
|
+
height: Expected frame height
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
ValueError: If frame dimensions don't match expected dimensions
|
|
88
|
+
"""
|
|
89
|
+
# Validate dimensions before writing
|
|
90
|
+
if frame.shape[0] != height or frame.shape[1] != width:
|
|
91
|
+
raise ValueError(
|
|
92
|
+
f"Frame dimensions {frame.shape[1]}x{frame.shape[0]} do not match "
|
|
93
|
+
f"expected dimensions {width}x{height}"
|
|
94
|
+
)
|
|
95
|
+
writer.write(frame)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class BaseDebugOverlayRenderer:
|
|
99
|
+
"""Base class for debug overlay renderers with common functionality."""
|
|
100
|
+
|
|
101
|
+
def __init__(
|
|
102
|
+
self,
|
|
103
|
+
output_path: str,
|
|
104
|
+
width: int,
|
|
105
|
+
height: int,
|
|
106
|
+
display_width: int,
|
|
107
|
+
display_height: int,
|
|
108
|
+
fps: float,
|
|
109
|
+
):
|
|
110
|
+
"""
|
|
111
|
+
Initialize overlay renderer.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
output_path: Path for output video
|
|
115
|
+
width: Encoded frame width (from source video)
|
|
116
|
+
height: Encoded frame height (from source video)
|
|
117
|
+
display_width: Display width (considering SAR)
|
|
118
|
+
display_height: Display height (considering SAR)
|
|
119
|
+
fps: Frames per second
|
|
120
|
+
"""
|
|
121
|
+
self.width = width
|
|
122
|
+
self.height = height
|
|
123
|
+
self.display_width = display_width
|
|
124
|
+
self.display_height = display_height
|
|
125
|
+
self.writer, self.needs_resize = create_video_writer(
|
|
126
|
+
output_path, width, height, display_width, display_height, fps
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def write_frame(self, frame: np.ndarray) -> None:
|
|
130
|
+
"""
|
|
131
|
+
Write frame to output video.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
frame: Video frame with shape (height, width, 3)
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
ValueError: If frame dimensions don't match expected encoded dimensions
|
|
138
|
+
"""
|
|
139
|
+
# Validate frame dimensions match expected encoded dimensions
|
|
140
|
+
frame_height, frame_width = frame.shape[:2]
|
|
141
|
+
if frame_height != self.height or frame_width != self.width:
|
|
142
|
+
raise ValueError(
|
|
143
|
+
f"Frame dimensions ({frame_width}x{frame_height}) don't match "
|
|
144
|
+
f"source dimensions ({self.width}x{self.height}). "
|
|
145
|
+
f"Aspect ratio must be preserved from source video."
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Resize to display dimensions if needed (to handle SAR)
|
|
149
|
+
if self.needs_resize:
|
|
150
|
+
frame = cv2.resize(
|
|
151
|
+
frame,
|
|
152
|
+
(self.display_width, self.display_height),
|
|
153
|
+
interpolation=cv2.INTER_LANCZOS4,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
write_overlay_frame(self.writer, frame, self.display_width, self.display_height)
|
|
157
|
+
|
|
158
|
+
def close(self) -> None:
|
|
159
|
+
"""Release video writer."""
|
|
160
|
+
self.writer.release()
|
|
161
|
+
|
|
162
|
+
def __enter__(self) -> "BaseDebugOverlayRenderer":
|
|
163
|
+
return self
|
|
164
|
+
|
|
165
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None: # type: ignore[no-untyped-def]
|
|
166
|
+
self.close()
|
kinemotion/core/smoothing.py
CHANGED
|
@@ -9,97 +9,199 @@ from .filtering import (
|
|
|
9
9
|
)
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
def
|
|
12
|
+
def _extract_landmark_coordinates(
|
|
13
13
|
landmark_sequence: list[dict[str, tuple[float, float, float]] | None],
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
) -> list[dict[str, tuple[float, float, float]] | None]:
|
|
14
|
+
landmark_name: str,
|
|
15
|
+
) -> tuple[list[float], list[float], list[int]]:
|
|
17
16
|
"""
|
|
18
|
-
|
|
17
|
+
Extract x, y coordinates and valid frame indices for a specific landmark.
|
|
19
18
|
|
|
20
19
|
Args:
|
|
21
20
|
landmark_sequence: List of landmark dictionaries from each frame
|
|
22
|
-
|
|
23
|
-
polyorder: Order of polynomial used to fit samples
|
|
21
|
+
landmark_name: Name of the landmark to extract
|
|
24
22
|
|
|
25
23
|
Returns:
|
|
26
|
-
|
|
24
|
+
Tuple of (x_coords, y_coords, valid_frames)
|
|
27
25
|
"""
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
x_coords = []
|
|
27
|
+
y_coords = []
|
|
28
|
+
valid_frames = []
|
|
29
|
+
|
|
30
|
+
for i, frame_landmarks in enumerate(landmark_sequence):
|
|
31
|
+
if frame_landmarks is not None and landmark_name in frame_landmarks:
|
|
32
|
+
x, y, _ = frame_landmarks[landmark_name] # vis not used
|
|
33
|
+
x_coords.append(x)
|
|
34
|
+
y_coords.append(y)
|
|
35
|
+
valid_frames.append(i)
|
|
36
|
+
|
|
37
|
+
return x_coords, y_coords, valid_frames
|
|
31
38
|
|
|
32
|
-
# Ensure window_length is odd
|
|
33
|
-
if window_length % 2 == 0:
|
|
34
|
-
window_length += 1
|
|
35
39
|
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
def _get_landmark_names(
|
|
41
|
+
landmark_sequence: list[dict[str, tuple[float, float, float]] | None],
|
|
42
|
+
) -> list[str] | None:
|
|
43
|
+
"""
|
|
44
|
+
Extract landmark names from first valid frame.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
landmark_sequence: List of landmark dictionaries from each frame
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
List of landmark names or None if no valid frame found
|
|
51
|
+
"""
|
|
38
52
|
for frame_landmarks in landmark_sequence:
|
|
39
53
|
if frame_landmarks is not None:
|
|
40
|
-
|
|
41
|
-
|
|
54
|
+
return list(frame_landmarks.keys())
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _fill_missing_frames(
|
|
59
|
+
smoothed_sequence: list[dict[str, tuple[float, float, float]] | None],
|
|
60
|
+
landmark_sequence: list[dict[str, tuple[float, float, float]] | None],
|
|
61
|
+
) -> None:
|
|
62
|
+
"""
|
|
63
|
+
Fill in any missing frames in smoothed sequence with original data.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
smoothed_sequence: Smoothed sequence (modified in place)
|
|
67
|
+
landmark_sequence: Original sequence
|
|
68
|
+
"""
|
|
69
|
+
for i in range(len(landmark_sequence)):
|
|
70
|
+
if i >= len(smoothed_sequence) or not smoothed_sequence[i]:
|
|
71
|
+
if i < len(smoothed_sequence):
|
|
72
|
+
smoothed_sequence[i] = landmark_sequence[i]
|
|
73
|
+
else:
|
|
74
|
+
smoothed_sequence.append(landmark_sequence[i])
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _store_smoothed_landmarks(
|
|
78
|
+
smoothed_sequence: list[dict[str, tuple[float, float, float]] | None],
|
|
79
|
+
landmark_sequence: list[dict[str, tuple[float, float, float]] | None],
|
|
80
|
+
landmark_name: str,
|
|
81
|
+
x_smooth: np.ndarray,
|
|
82
|
+
y_smooth: np.ndarray,
|
|
83
|
+
valid_frames: list[int],
|
|
84
|
+
) -> None:
|
|
85
|
+
"""
|
|
86
|
+
Store smoothed landmark values back into the sequence.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
smoothed_sequence: Sequence to store smoothed values into (modified in place)
|
|
90
|
+
landmark_sequence: Original sequence (for visibility values)
|
|
91
|
+
landmark_name: Name of the landmark being smoothed
|
|
92
|
+
x_smooth: Smoothed x coordinates
|
|
93
|
+
y_smooth: Smoothed y coordinates
|
|
94
|
+
valid_frames: Frame indices corresponding to smoothed values
|
|
95
|
+
"""
|
|
96
|
+
for idx, frame_idx in enumerate(valid_frames):
|
|
97
|
+
if frame_idx >= len(smoothed_sequence):
|
|
98
|
+
smoothed_sequence.extend([{}] * (frame_idx - len(smoothed_sequence) + 1))
|
|
99
|
+
|
|
100
|
+
# Ensure smoothed_sequence[frame_idx] is a dict, not None
|
|
101
|
+
if smoothed_sequence[frame_idx] is None:
|
|
102
|
+
smoothed_sequence[frame_idx] = {}
|
|
103
|
+
|
|
104
|
+
# Type narrowing: after the check above, we know it's a dict
|
|
105
|
+
frame_dict = smoothed_sequence[frame_idx]
|
|
106
|
+
assert frame_dict is not None # for type checker
|
|
107
|
+
|
|
108
|
+
if landmark_name not in frame_dict and landmark_sequence[frame_idx] is not None:
|
|
109
|
+
# Keep original visibility
|
|
110
|
+
orig_landmarks = landmark_sequence[frame_idx]
|
|
111
|
+
assert orig_landmarks is not None # for type checker
|
|
112
|
+
orig_vis = orig_landmarks[landmark_name][2]
|
|
113
|
+
frame_dict[landmark_name] = (
|
|
114
|
+
float(x_smooth[idx]),
|
|
115
|
+
float(y_smooth[idx]),
|
|
116
|
+
orig_vis,
|
|
117
|
+
)
|
|
42
118
|
|
|
119
|
+
|
|
120
|
+
def _smooth_landmarks_core(
|
|
121
|
+
landmark_sequence: list[dict[str, tuple[float, float, float]] | None],
|
|
122
|
+
window_length: int,
|
|
123
|
+
polyorder: int,
|
|
124
|
+
smoother_fn, # type: ignore[no-untyped-def]
|
|
125
|
+
) -> list[dict[str, tuple[float, float, float]] | None]:
|
|
126
|
+
"""
|
|
127
|
+
Core smoothing logic shared by both standard and advanced smoothing.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
landmark_sequence: List of landmark dictionaries from each frame
|
|
131
|
+
window_length: Length of filter window (must be odd)
|
|
132
|
+
polyorder: Order of polynomial used to fit samples
|
|
133
|
+
smoother_fn: Function that takes (x_coords, y_coords, valid_frames)
|
|
134
|
+
and returns (x_smooth, y_smooth)
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Smoothed landmark sequence
|
|
138
|
+
"""
|
|
139
|
+
landmark_names = _get_landmark_names(landmark_sequence)
|
|
43
140
|
if landmark_names is None:
|
|
44
141
|
return landmark_sequence
|
|
45
142
|
|
|
46
|
-
# Build arrays for each landmark coordinate
|
|
47
143
|
smoothed_sequence: list[dict[str, tuple[float, float, float]] | None] = []
|
|
48
144
|
|
|
49
145
|
for landmark_name in landmark_names:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
valid_frames = []
|
|
54
|
-
|
|
55
|
-
for i, frame_landmarks in enumerate(landmark_sequence):
|
|
56
|
-
if frame_landmarks is not None and landmark_name in frame_landmarks:
|
|
57
|
-
x, y, _ = frame_landmarks[landmark_name] # vis not used
|
|
58
|
-
x_coords.append(x)
|
|
59
|
-
y_coords.append(y)
|
|
60
|
-
valid_frames.append(i)
|
|
146
|
+
x_coords, y_coords, valid_frames = _extract_landmark_coordinates(
|
|
147
|
+
landmark_sequence, landmark_name
|
|
148
|
+
)
|
|
61
149
|
|
|
62
150
|
if len(x_coords) < window_length:
|
|
63
151
|
continue
|
|
64
152
|
|
|
65
|
-
# Apply
|
|
66
|
-
x_smooth =
|
|
67
|
-
y_smooth = savgol_filter(y_coords, window_length, polyorder)
|
|
153
|
+
# Apply smoothing function
|
|
154
|
+
x_smooth, y_smooth = smoother_fn(x_coords, y_coords, valid_frames)
|
|
68
155
|
|
|
69
156
|
# Store smoothed values back
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
smoothed_sequence[frame_idx] = {}
|
|
79
|
-
|
|
80
|
-
if (
|
|
81
|
-
landmark_name not in smoothed_sequence[frame_idx]
|
|
82
|
-
and landmark_sequence[frame_idx] is not None
|
|
83
|
-
):
|
|
84
|
-
# Keep original visibility
|
|
85
|
-
orig_vis = landmark_sequence[frame_idx][landmark_name][2]
|
|
86
|
-
smoothed_sequence[frame_idx][landmark_name] = (
|
|
87
|
-
float(x_smooth[idx]),
|
|
88
|
-
float(y_smooth[idx]),
|
|
89
|
-
orig_vis,
|
|
90
|
-
)
|
|
157
|
+
_store_smoothed_landmarks(
|
|
158
|
+
smoothed_sequence,
|
|
159
|
+
landmark_sequence,
|
|
160
|
+
landmark_name,
|
|
161
|
+
x_smooth,
|
|
162
|
+
y_smooth,
|
|
163
|
+
valid_frames,
|
|
164
|
+
)
|
|
91
165
|
|
|
92
166
|
# Fill in any missing frames with original data
|
|
93
|
-
|
|
94
|
-
if i >= len(smoothed_sequence) or not smoothed_sequence[i]:
|
|
95
|
-
if i < len(smoothed_sequence):
|
|
96
|
-
smoothed_sequence[i] = landmark_sequence[i]
|
|
97
|
-
else:
|
|
98
|
-
smoothed_sequence.append(landmark_sequence[i])
|
|
167
|
+
_fill_missing_frames(smoothed_sequence, landmark_sequence)
|
|
99
168
|
|
|
100
169
|
return smoothed_sequence
|
|
101
170
|
|
|
102
171
|
|
|
172
|
+
def smooth_landmarks(
|
|
173
|
+
landmark_sequence: list[dict[str, tuple[float, float, float]] | None],
|
|
174
|
+
window_length: int = 5,
|
|
175
|
+
polyorder: int = 2,
|
|
176
|
+
) -> list[dict[str, tuple[float, float, float]] | None]:
|
|
177
|
+
"""
|
|
178
|
+
Smooth landmark trajectories using Savitzky-Golay filter.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
landmark_sequence: List of landmark dictionaries from each frame
|
|
182
|
+
window_length: Length of filter window (must be odd, >= polyorder + 2)
|
|
183
|
+
polyorder: Order of polynomial used to fit samples
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Smoothed landmark sequence with same structure as input
|
|
187
|
+
"""
|
|
188
|
+
if len(landmark_sequence) < window_length:
|
|
189
|
+
return landmark_sequence
|
|
190
|
+
|
|
191
|
+
# Ensure window_length is odd
|
|
192
|
+
if window_length % 2 == 0:
|
|
193
|
+
window_length += 1
|
|
194
|
+
|
|
195
|
+
def savgol_smoother(x_coords, y_coords, _valid_frames): # type: ignore[no-untyped-def]
|
|
196
|
+
x_smooth = savgol_filter(x_coords, window_length, polyorder)
|
|
197
|
+
y_smooth = savgol_filter(y_coords, window_length, polyorder)
|
|
198
|
+
return x_smooth, y_smooth
|
|
199
|
+
|
|
200
|
+
return _smooth_landmarks_core(
|
|
201
|
+
landmark_sequence, window_length, polyorder, savgol_smoother
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
103
205
|
def compute_velocity(
|
|
104
206
|
positions: np.ndarray, fps: float, smooth_window: int = 3
|
|
105
207
|
) -> np.ndarray:
|
|
@@ -259,42 +361,13 @@ def smooth_landmarks_advanced(
|
|
|
259
361
|
Smoothed landmark sequence with same structure as input
|
|
260
362
|
"""
|
|
261
363
|
if len(landmark_sequence) < window_length:
|
|
262
|
-
# Not enough frames to smooth effectively
|
|
263
364
|
return landmark_sequence
|
|
264
365
|
|
|
265
366
|
# Ensure window_length is odd
|
|
266
367
|
if window_length % 2 == 0:
|
|
267
368
|
window_length += 1
|
|
268
369
|
|
|
269
|
-
|
|
270
|
-
landmark_names = None
|
|
271
|
-
for frame_landmarks in landmark_sequence:
|
|
272
|
-
if frame_landmarks is not None:
|
|
273
|
-
landmark_names = list(frame_landmarks.keys())
|
|
274
|
-
break
|
|
275
|
-
|
|
276
|
-
if landmark_names is None:
|
|
277
|
-
return landmark_sequence
|
|
278
|
-
|
|
279
|
-
# Build arrays for each landmark coordinate
|
|
280
|
-
smoothed_sequence: list[dict[str, tuple[float, float, float]] | None] = []
|
|
281
|
-
|
|
282
|
-
for landmark_name in landmark_names:
|
|
283
|
-
# Extract x, y coordinates for this landmark across all frames
|
|
284
|
-
x_coords = []
|
|
285
|
-
y_coords = []
|
|
286
|
-
valid_frames = []
|
|
287
|
-
|
|
288
|
-
for i, frame_landmarks in enumerate(landmark_sequence):
|
|
289
|
-
if frame_landmarks is not None and landmark_name in frame_landmarks:
|
|
290
|
-
x, y, _ = frame_landmarks[landmark_name] # vis not used
|
|
291
|
-
x_coords.append(x)
|
|
292
|
-
y_coords.append(y)
|
|
293
|
-
valid_frames.append(i)
|
|
294
|
-
|
|
295
|
-
if len(x_coords) < window_length:
|
|
296
|
-
continue
|
|
297
|
-
|
|
370
|
+
def advanced_smoother(x_coords, y_coords, _valid_frames): # type: ignore[no-untyped-def]
|
|
298
371
|
x_array = np.array(x_coords)
|
|
299
372
|
y_array = np.array(y_coords)
|
|
300
373
|
|
|
@@ -332,35 +405,8 @@ def smooth_landmarks_advanced(
|
|
|
332
405
|
x_smooth = savgol_filter(x_array, window_length, polyorder)
|
|
333
406
|
y_smooth = savgol_filter(y_array, window_length, polyorder)
|
|
334
407
|
|
|
335
|
-
|
|
336
|
-
for idx, frame_idx in enumerate(valid_frames):
|
|
337
|
-
if frame_idx >= len(smoothed_sequence):
|
|
338
|
-
smoothed_sequence.extend(
|
|
339
|
-
[{}] * (frame_idx - len(smoothed_sequence) + 1)
|
|
340
|
-
)
|
|
341
|
-
|
|
342
|
-
# Ensure smoothed_sequence[frame_idx] is a dict, not None
|
|
343
|
-
if smoothed_sequence[frame_idx] is None:
|
|
344
|
-
smoothed_sequence[frame_idx] = {}
|
|
345
|
-
|
|
346
|
-
if (
|
|
347
|
-
landmark_name not in smoothed_sequence[frame_idx]
|
|
348
|
-
and landmark_sequence[frame_idx] is not None
|
|
349
|
-
):
|
|
350
|
-
# Keep original visibility
|
|
351
|
-
orig_vis = landmark_sequence[frame_idx][landmark_name][2]
|
|
352
|
-
smoothed_sequence[frame_idx][landmark_name] = (
|
|
353
|
-
float(x_smooth[idx]),
|
|
354
|
-
float(y_smooth[idx]),
|
|
355
|
-
orig_vis,
|
|
356
|
-
)
|
|
357
|
-
|
|
358
|
-
# Fill in any missing frames with original data
|
|
359
|
-
for i in range(len(landmark_sequence)):
|
|
360
|
-
if i >= len(smoothed_sequence) or not smoothed_sequence[i]:
|
|
361
|
-
if i < len(smoothed_sequence):
|
|
362
|
-
smoothed_sequence[i] = landmark_sequence[i]
|
|
363
|
-
else:
|
|
364
|
-
smoothed_sequence.append(landmark_sequence[i])
|
|
408
|
+
return x_smooth, y_smooth
|
|
365
409
|
|
|
366
|
-
return
|
|
410
|
+
return _smooth_landmarks_core(
|
|
411
|
+
landmark_sequence, window_length, polyorder, advanced_smoother
|
|
412
|
+
)
|
|
@@ -3,61 +3,15 @@
|
|
|
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
|
-
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)
|
|
53
|
-
)
|
|
54
|
-
|
|
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}"
|
|
59
|
-
)
|
|
60
|
-
|
|
61
15
|
def render_frame(
|
|
62
16
|
self,
|
|
63
17
|
frame: np.ndarray,
|
|
@@ -211,42 +165,3 @@ class DebugOverlayRenderer:
|
|
|
211
165
|
)
|
|
212
166
|
|
|
213
167
|
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."
|
|
232
|
-
)
|
|
233
|
-
|
|
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,
|
|
240
|
-
)
|
|
241
|
-
|
|
242
|
-
self.writer.write(frame)
|
|
243
|
-
|
|
244
|
-
def close(self) -> None:
|
|
245
|
-
"""Release video writer."""
|
|
246
|
-
self.writer.release()
|
|
247
|
-
|
|
248
|
-
def __enter__(self) -> "DebugOverlayRenderer":
|
|
249
|
-
return self
|
|
250
|
-
|
|
251
|
-
def __exit__(self, exc_type, exc_val, exc_tb) -> None: # type: ignore[no-untyped-def]
|
|
252
|
-
self.close()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kinemotion
|
|
3
|
-
Version: 0.11.
|
|
3
|
+
Version: 0.11.7
|
|
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
|
|
@@ -3,25 +3,26 @@ kinemotion/api.py,sha256=M6KqzdspOvtJ9Wl555HGe3ITzvRFhPK4xeqcX7IA98s,31463
|
|
|
3
3
|
kinemotion/cli.py,sha256=cqYV_7URH0JUDy1VQ_EDLv63FmNO4Ns20m6s1XAjiP4,464
|
|
4
4
|
kinemotion/cmj/__init__.py,sha256=Ynv0-Oco4I3Y1Ubj25m3h9h2XFqeNwpAewXmAYOmwfU,127
|
|
5
5
|
kinemotion/cmj/analysis.py,sha256=4HYGn4VDIB6oExAees-VcPfpNgWOltpgwjyNTU7YAb4,18263
|
|
6
|
-
kinemotion/cmj/cli.py,sha256=
|
|
7
|
-
kinemotion/cmj/debug_overlay.py,sha256=
|
|
6
|
+
kinemotion/cmj/cli.py,sha256=bmDvNvL7cu65-R8YkRIZYKD0nuTA0IJnWLcLlH_kFm0,16843
|
|
7
|
+
kinemotion/cmj/debug_overlay.py,sha256=5Uwtyx9FP-tKhQyUvFW2t_ULVBV7oMCvzcLzf4hFcUg,15910
|
|
8
8
|
kinemotion/cmj/joint_angles.py,sha256=8ucpDGPvbt4iX3tx9eVxJEUv0laTm2Y58_--VzJCogE,9113
|
|
9
9
|
kinemotion/cmj/kinematics.py,sha256=Xl_PlC2OqMoA-zOc3SRB_GqI0AgLlJol5FTPe5J_qLc,7573
|
|
10
10
|
kinemotion/core/__init__.py,sha256=3yzDhb5PekDNjydqrs8aWGneUGJBt-lB0SoB_Y2FXqU,1010
|
|
11
11
|
kinemotion/core/auto_tuning.py,sha256=cvmxUI-CbahpOJQtR2r5jOx4Q6yKPe3DO1o15hOQIdw,10508
|
|
12
|
-
kinemotion/core/cli_utils.py,sha256=
|
|
12
|
+
kinemotion/core/cli_utils.py,sha256=Pmg8z0nGhkYJm0o-y3jyvzeRy9yvol14ddaHrp6f7cw,10161
|
|
13
|
+
kinemotion/core/debug_overlay_utils.py,sha256=E3krJ0SeIhSl6AM5WYiGuaONKsLNKL93P3aHqMgStY8,5235
|
|
13
14
|
kinemotion/core/filtering.py,sha256=f-m-aA59e4WqE6u-9MA51wssu7rI-Y_7n1cG8IWdeRQ,11241
|
|
14
15
|
kinemotion/core/pose.py,sha256=Wfd1RR-2ZznYpWeQUbySwcV3mvReqn8n3XO6S7pGq4M,8390
|
|
15
|
-
kinemotion/core/smoothing.py,sha256=
|
|
16
|
+
kinemotion/core/smoothing.py,sha256=Zdhqw4NyCrZaEb-Jo3sASzP-QlEL5sVTgHoXU8zT_xU,14136
|
|
16
17
|
kinemotion/core/video_io.py,sha256=z8Z0qbNaKbcdB40KnbNOBMzab3BbgnhBxp-mUBYeXgM,6577
|
|
17
18
|
kinemotion/dropjump/__init__.py,sha256=yc1XiZ9vfo5h_n7PKVSiX2TTgaIfGL7Y7SkQtiDZj_E,838
|
|
18
19
|
kinemotion/dropjump/analysis.py,sha256=HfJt2t9IsMBiBUz7apIzdxbRH9QqzlFnDVVWcKhU3ow,23291
|
|
19
20
|
kinemotion/dropjump/cli.py,sha256=-iBgHNwW_dijHe6_JIEGSBUzvFb6tZV0aopbPd-9jC8,22402
|
|
20
|
-
kinemotion/dropjump/debug_overlay.py,sha256=
|
|
21
|
+
kinemotion/dropjump/debug_overlay.py,sha256=2L4VAZwWFnaOQ7LAF3ALXCjEaVNzkfpLT5-h0qKL_6g,5707
|
|
21
22
|
kinemotion/dropjump/kinematics.py,sha256=RM_O8Kdc6aEiPIu_99N4cu-4EhYSQxtBGASJF_dmQaU,19081
|
|
22
23
|
kinemotion/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
|
-
kinemotion-0.11.
|
|
24
|
-
kinemotion-0.11.
|
|
25
|
-
kinemotion-0.11.
|
|
26
|
-
kinemotion-0.11.
|
|
27
|
-
kinemotion-0.11.
|
|
24
|
+
kinemotion-0.11.7.dist-info/METADATA,sha256=7z0ZpUWjjZXDq-fe3006Pgy7CCZTucZ_cCRh9lbLjkU,18990
|
|
25
|
+
kinemotion-0.11.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
26
|
+
kinemotion-0.11.7.dist-info/entry_points.txt,sha256=zaqnAnjLvcdrk1Qvj5nvXZCZ2gp0prS7it1zTJygcIY,50
|
|
27
|
+
kinemotion-0.11.7.dist-info/licenses/LICENSE,sha256=KZajvqsHw0NoOHOi2q0FZ4NBe9HdV6oey-IPYAtHXfg,1088
|
|
28
|
+
kinemotion-0.11.7.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|