kinemotion 0.45.0__py3-none-any.whl → 0.46.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/api.py +74 -60
- kinemotion/cmj/cli.py +2 -3
- kinemotion/core/__init__.py +4 -1
- kinemotion/core/debug_overlay_utils.py +2 -1
- kinemotion/core/pipeline_utils.py +58 -68
- kinemotion/core/timing.py +219 -22
- {kinemotion-0.45.0.dist-info → kinemotion-0.46.0.dist-info}/METADATA +1 -1
- {kinemotion-0.45.0.dist-info → kinemotion-0.46.0.dist-info}/RECORD +11 -11
- {kinemotion-0.45.0.dist-info → kinemotion-0.46.0.dist-info}/WHEEL +0 -0
- {kinemotion-0.45.0.dist-info → kinemotion-0.46.0.dist-info}/entry_points.txt +0 -0
- {kinemotion-0.45.0.dist-info → kinemotion-0.46.0.dist-info}/licenses/LICENSE +0 -0
kinemotion/api.py
CHANGED
|
@@ -78,6 +78,69 @@ class DropJumpVideoConfig:
|
|
|
78
78
|
tracking_confidence: float | None = None
|
|
79
79
|
|
|
80
80
|
|
|
81
|
+
def _generate_debug_video(
|
|
82
|
+
output_video: str,
|
|
83
|
+
frames: list,
|
|
84
|
+
frame_indices: list[int],
|
|
85
|
+
video_fps: float,
|
|
86
|
+
smoothed_landmarks: list,
|
|
87
|
+
contact_states: list,
|
|
88
|
+
metrics: DropJumpMetrics,
|
|
89
|
+
timer: PerformanceTimer | None,
|
|
90
|
+
verbose: bool,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Generate debug video with overlay."""
|
|
93
|
+
if verbose:
|
|
94
|
+
print(f"Generating debug video: {output_video}")
|
|
95
|
+
|
|
96
|
+
if not frames:
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
debug_h, debug_w = frames[0].shape[:2]
|
|
100
|
+
|
|
101
|
+
if video_fps > 30:
|
|
102
|
+
debug_fps = video_fps / (video_fps / 30.0)
|
|
103
|
+
else:
|
|
104
|
+
debug_fps = video_fps
|
|
105
|
+
|
|
106
|
+
if len(frames) < len(smoothed_landmarks):
|
|
107
|
+
step = max(1, int(video_fps / 30.0))
|
|
108
|
+
debug_fps = video_fps / step
|
|
109
|
+
|
|
110
|
+
def _render_frames(renderer: DebugOverlayRenderer) -> None:
|
|
111
|
+
for frame, idx in zip(frames, frame_indices, strict=True):
|
|
112
|
+
annotated = renderer.render_frame(
|
|
113
|
+
frame,
|
|
114
|
+
smoothed_landmarks[idx],
|
|
115
|
+
contact_states[idx],
|
|
116
|
+
idx,
|
|
117
|
+
metrics,
|
|
118
|
+
use_com=False,
|
|
119
|
+
)
|
|
120
|
+
renderer.write_frame(annotated)
|
|
121
|
+
|
|
122
|
+
renderer_context = DebugOverlayRenderer(
|
|
123
|
+
output_video,
|
|
124
|
+
debug_w,
|
|
125
|
+
debug_h,
|
|
126
|
+
debug_w,
|
|
127
|
+
debug_h,
|
|
128
|
+
debug_fps,
|
|
129
|
+
timer=timer,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if timer:
|
|
133
|
+
with timer.measure("debug_video_generation"):
|
|
134
|
+
with renderer_context as renderer:
|
|
135
|
+
_render_frames(renderer)
|
|
136
|
+
else:
|
|
137
|
+
with renderer_context as renderer:
|
|
138
|
+
_render_frames(renderer)
|
|
139
|
+
|
|
140
|
+
if verbose:
|
|
141
|
+
print(f"Debug video saved: {output_video}")
|
|
142
|
+
|
|
143
|
+
|
|
81
144
|
def process_dropjump_video(
|
|
82
145
|
video_path: str,
|
|
83
146
|
quality: str = "balanced",
|
|
@@ -285,64 +348,17 @@ def process_dropjump_video(
|
|
|
285
348
|
print()
|
|
286
349
|
|
|
287
350
|
if output_video:
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
if timer:
|
|
301
|
-
with timer.measure("debug_video_generation"):
|
|
302
|
-
with DebugOverlayRenderer(
|
|
303
|
-
output_video,
|
|
304
|
-
debug_w,
|
|
305
|
-
debug_h,
|
|
306
|
-
debug_w,
|
|
307
|
-
debug_h,
|
|
308
|
-
debug_fps,
|
|
309
|
-
timer=timer,
|
|
310
|
-
) as renderer:
|
|
311
|
-
for frame, idx in zip(frames, frame_indices, strict=True):
|
|
312
|
-
annotated = renderer.render_frame(
|
|
313
|
-
frame,
|
|
314
|
-
smoothed_landmarks[idx],
|
|
315
|
-
contact_states[idx],
|
|
316
|
-
idx,
|
|
317
|
-
metrics,
|
|
318
|
-
use_com=False,
|
|
319
|
-
)
|
|
320
|
-
renderer.write_frame(annotated)
|
|
321
|
-
with timer.measure("debug_video_reencode"):
|
|
322
|
-
pass
|
|
323
|
-
else:
|
|
324
|
-
with DebugOverlayRenderer(
|
|
325
|
-
output_video,
|
|
326
|
-
debug_w,
|
|
327
|
-
debug_h,
|
|
328
|
-
debug_w,
|
|
329
|
-
debug_h,
|
|
330
|
-
debug_fps,
|
|
331
|
-
timer=timer,
|
|
332
|
-
) as renderer:
|
|
333
|
-
for frame, idx in zip(frames, frame_indices, strict=True):
|
|
334
|
-
annotated = renderer.render_frame(
|
|
335
|
-
frame,
|
|
336
|
-
smoothed_landmarks[idx],
|
|
337
|
-
contact_states[idx],
|
|
338
|
-
idx,
|
|
339
|
-
metrics,
|
|
340
|
-
use_com=False,
|
|
341
|
-
)
|
|
342
|
-
renderer.write_frame(annotated)
|
|
343
|
-
|
|
344
|
-
if verbose:
|
|
345
|
-
print(f"Debug video saved: {output_video}")
|
|
351
|
+
_generate_debug_video(
|
|
352
|
+
output_video,
|
|
353
|
+
frames,
|
|
354
|
+
frame_indices,
|
|
355
|
+
video.fps,
|
|
356
|
+
smoothed_landmarks,
|
|
357
|
+
contact_states,
|
|
358
|
+
metrics,
|
|
359
|
+
timer,
|
|
360
|
+
verbose,
|
|
361
|
+
)
|
|
346
362
|
|
|
347
363
|
with timer.measure("metrics_validation"):
|
|
348
364
|
validator = DropJumpMetricsValidator()
|
|
@@ -738,8 +754,6 @@ def process_cmj_video(
|
|
|
738
754
|
frame, smoothed_landmarks[idx], idx, metrics
|
|
739
755
|
)
|
|
740
756
|
renderer.write_frame(annotated)
|
|
741
|
-
with timer.measure("debug_video_reencode"):
|
|
742
|
-
pass
|
|
743
757
|
else:
|
|
744
758
|
with CMJDebugOverlayRenderer(
|
|
745
759
|
output_video,
|
kinemotion/cmj/cli.py
CHANGED
|
@@ -3,11 +3,10 @@
|
|
|
3
3
|
import json
|
|
4
4
|
import sys
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
-
from typing import Any
|
|
7
6
|
|
|
8
7
|
import click
|
|
9
8
|
|
|
10
|
-
from ..api import process_cmj_video
|
|
9
|
+
from ..api import CMJMetrics, process_cmj_video
|
|
11
10
|
from ..core.auto_tuning import QualityPreset
|
|
12
11
|
from ..core.cli_utils import (
|
|
13
12
|
collect_video_files,
|
|
@@ -287,7 +286,7 @@ def _process_single(
|
|
|
287
286
|
sys.exit(1)
|
|
288
287
|
|
|
289
288
|
|
|
290
|
-
def _output_results(metrics:
|
|
289
|
+
def _output_results(metrics: CMJMetrics, json_output: str | None) -> None:
|
|
291
290
|
"""Output analysis results."""
|
|
292
291
|
results = metrics.to_dict()
|
|
293
292
|
|
kinemotion/core/__init__.py
CHANGED
|
@@ -22,7 +22,7 @@ from .smoothing import (
|
|
|
22
22
|
smooth_landmarks,
|
|
23
23
|
smooth_landmarks_advanced,
|
|
24
24
|
)
|
|
25
|
-
from .timing import PerformanceTimer
|
|
25
|
+
from .timing import NULL_TIMER, NullTimer, PerformanceTimer, Timer
|
|
26
26
|
from .video_io import VideoProcessor
|
|
27
27
|
|
|
28
28
|
__all__ = [
|
|
@@ -49,6 +49,9 @@ __all__ = [
|
|
|
49
49
|
"calculate_position_stability",
|
|
50
50
|
# Timing
|
|
51
51
|
"PerformanceTimer",
|
|
52
|
+
"Timer",
|
|
53
|
+
"NullTimer",
|
|
54
|
+
"NULL_TIMER",
|
|
52
55
|
# Video I/O
|
|
53
56
|
"VideoProcessor",
|
|
54
57
|
]
|
|
@@ -5,6 +5,7 @@ import shutil
|
|
|
5
5
|
import subprocess
|
|
6
6
|
import time
|
|
7
7
|
from pathlib import Path
|
|
8
|
+
from typing import Self
|
|
8
9
|
|
|
9
10
|
import cv2
|
|
10
11
|
import numpy as np
|
|
@@ -251,7 +252,7 @@ class BaseDebugOverlayRenderer:
|
|
|
251
252
|
if temp_path and os.path.exists(temp_path):
|
|
252
253
|
os.remove(temp_path)
|
|
253
254
|
|
|
254
|
-
def __enter__(self) ->
|
|
255
|
+
def __enter__(self) -> Self:
|
|
255
256
|
return self
|
|
256
257
|
|
|
257
258
|
def __exit__(self, _exc_type, _exc_val, _exc_tb) -> None: # type: ignore[no-untyped-def]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"
|
|
1
|
+
"Shared pipeline utilities for kinematic analysis."
|
|
2
2
|
|
|
3
3
|
from collections.abc import Callable
|
|
4
4
|
from concurrent.futures import ProcessPoolExecutor, as_completed
|
|
@@ -140,6 +140,44 @@ def print_verbose_parameters(
|
|
|
140
140
|
print("=" * 60 + "\n")
|
|
141
141
|
|
|
142
142
|
|
|
143
|
+
def _process_frames_loop(
|
|
144
|
+
video: VideoProcessor,
|
|
145
|
+
tracker: PoseTracker,
|
|
146
|
+
step: int,
|
|
147
|
+
should_resize: bool,
|
|
148
|
+
debug_w: int,
|
|
149
|
+
debug_h: int,
|
|
150
|
+
) -> tuple[list, list, list]:
|
|
151
|
+
"""Internal loop for processing frames to reduce complexity."""
|
|
152
|
+
landmarks_sequence = []
|
|
153
|
+
debug_frames = []
|
|
154
|
+
frame_indices = []
|
|
155
|
+
frame_idx = 0
|
|
156
|
+
|
|
157
|
+
while True:
|
|
158
|
+
frame = video.read_frame()
|
|
159
|
+
if frame is None:
|
|
160
|
+
break
|
|
161
|
+
|
|
162
|
+
landmarks = tracker.process_frame(frame)
|
|
163
|
+
landmarks_sequence.append(landmarks)
|
|
164
|
+
|
|
165
|
+
if frame_idx % step == 0:
|
|
166
|
+
if should_resize:
|
|
167
|
+
processed_frame = cv2.resize(
|
|
168
|
+
frame, (debug_w, debug_h), interpolation=cv2.INTER_LINEAR
|
|
169
|
+
)
|
|
170
|
+
else:
|
|
171
|
+
processed_frame = frame
|
|
172
|
+
|
|
173
|
+
debug_frames.append(processed_frame)
|
|
174
|
+
frame_indices.append(frame_idx)
|
|
175
|
+
|
|
176
|
+
frame_idx += 1
|
|
177
|
+
|
|
178
|
+
return debug_frames, landmarks_sequence, frame_indices
|
|
179
|
+
|
|
180
|
+
|
|
143
181
|
def process_all_frames(
|
|
144
182
|
video: VideoProcessor,
|
|
145
183
|
tracker: PoseTracker,
|
|
@@ -169,10 +207,6 @@ def process_all_frames(
|
|
|
169
207
|
if verbose:
|
|
170
208
|
print("Tracking pose landmarks...")
|
|
171
209
|
|
|
172
|
-
landmarks_sequence = []
|
|
173
|
-
debug_frames = []
|
|
174
|
-
frame_indices = []
|
|
175
|
-
|
|
176
210
|
step = max(1, int(video.fps / target_debug_fps))
|
|
177
211
|
|
|
178
212
|
w, h = video.display_width, video.display_height
|
|
@@ -184,51 +218,15 @@ def process_all_frames(
|
|
|
184
218
|
debug_h = int(h * scale) // 2 * 2
|
|
185
219
|
should_resize = (debug_w != video.width) or (debug_h != video.height)
|
|
186
220
|
|
|
187
|
-
frame_idx = 0
|
|
188
|
-
|
|
189
221
|
if timer:
|
|
190
222
|
with timer.measure("pose_tracking"):
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
break
|
|
195
|
-
|
|
196
|
-
landmarks = tracker.process_frame(frame)
|
|
197
|
-
landmarks_sequence.append(landmarks)
|
|
198
|
-
|
|
199
|
-
if frame_idx % step == 0:
|
|
200
|
-
if should_resize:
|
|
201
|
-
processed_frame = cv2.resize(
|
|
202
|
-
frame, (debug_w, debug_h), interpolation=cv2.INTER_LINEAR
|
|
203
|
-
)
|
|
204
|
-
else:
|
|
205
|
-
processed_frame = frame
|
|
206
|
-
|
|
207
|
-
debug_frames.append(processed_frame)
|
|
208
|
-
frame_indices.append(frame_idx)
|
|
209
|
-
|
|
210
|
-
frame_idx += 1
|
|
223
|
+
debug_frames, landmarks_sequence, frame_indices = _process_frames_loop(
|
|
224
|
+
video, tracker, step, should_resize, debug_w, debug_h
|
|
225
|
+
)
|
|
211
226
|
else:
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
break
|
|
216
|
-
|
|
217
|
-
landmarks = tracker.process_frame(frame)
|
|
218
|
-
landmarks_sequence.append(landmarks)
|
|
219
|
-
|
|
220
|
-
if frame_idx % step == 0:
|
|
221
|
-
if should_resize:
|
|
222
|
-
processed_frame = cv2.resize(
|
|
223
|
-
frame, (debug_w, debug_h), interpolation=cv2.INTER_LINEAR
|
|
224
|
-
)
|
|
225
|
-
else:
|
|
226
|
-
processed_frame = frame
|
|
227
|
-
|
|
228
|
-
debug_frames.append(processed_frame)
|
|
229
|
-
frame_indices.append(frame_idx)
|
|
230
|
-
|
|
231
|
-
frame_idx += 1
|
|
227
|
+
debug_frames, landmarks_sequence, frame_indices = _process_frames_loop(
|
|
228
|
+
video, tracker, step, should_resize, debug_w, debug_h
|
|
229
|
+
)
|
|
232
230
|
|
|
233
231
|
if close_tracker:
|
|
234
232
|
tracker.close()
|
|
@@ -256,22 +254,19 @@ def apply_smoothing(
|
|
|
256
254
|
Returns:
|
|
257
255
|
Smoothed landmarks sequence
|
|
258
256
|
"""
|
|
259
|
-
|
|
260
|
-
|
|
257
|
+
use_advanced = params.outlier_rejection or params.bilateral_filter
|
|
258
|
+
|
|
259
|
+
if verbose:
|
|
260
|
+
if use_advanced:
|
|
261
261
|
if params.outlier_rejection:
|
|
262
262
|
print("Smoothing landmarks with outlier rejection...")
|
|
263
263
|
if params.bilateral_filter:
|
|
264
264
|
print("Using bilateral temporal filter...")
|
|
265
|
-
if timer:
|
|
266
|
-
with timer.measure("smoothing"):
|
|
267
|
-
return smooth_landmarks_advanced(
|
|
268
|
-
landmarks_sequence,
|
|
269
|
-
window_length=params.smoothing_window,
|
|
270
|
-
polyorder=params.polyorder,
|
|
271
|
-
use_outlier_rejection=params.outlier_rejection,
|
|
272
|
-
use_bilateral=params.bilateral_filter,
|
|
273
|
-
)
|
|
274
265
|
else:
|
|
266
|
+
print("Smoothing landmarks...")
|
|
267
|
+
|
|
268
|
+
def _run_smoothing() -> list:
|
|
269
|
+
if use_advanced:
|
|
275
270
|
return smooth_landmarks_advanced(
|
|
276
271
|
landmarks_sequence,
|
|
277
272
|
window_length=params.smoothing_window,
|
|
@@ -279,16 +274,6 @@ def apply_smoothing(
|
|
|
279
274
|
use_outlier_rejection=params.outlier_rejection,
|
|
280
275
|
use_bilateral=params.bilateral_filter,
|
|
281
276
|
)
|
|
282
|
-
else:
|
|
283
|
-
if verbose:
|
|
284
|
-
print("Smoothing landmarks...")
|
|
285
|
-
if timer:
|
|
286
|
-
with timer.measure("smoothing"):
|
|
287
|
-
return smooth_landmarks(
|
|
288
|
-
landmarks_sequence,
|
|
289
|
-
window_length=params.smoothing_window,
|
|
290
|
-
polyorder=params.polyorder,
|
|
291
|
-
)
|
|
292
277
|
else:
|
|
293
278
|
return smooth_landmarks(
|
|
294
279
|
landmarks_sequence,
|
|
@@ -296,6 +281,11 @@ def apply_smoothing(
|
|
|
296
281
|
polyorder=params.polyorder,
|
|
297
282
|
)
|
|
298
283
|
|
|
284
|
+
if timer:
|
|
285
|
+
with timer.measure("smoothing"):
|
|
286
|
+
return _run_smoothing()
|
|
287
|
+
return _run_smoothing()
|
|
288
|
+
|
|
299
289
|
|
|
300
290
|
def calculate_foot_visibility(frame_landmarks: dict) -> float:
|
|
301
291
|
"""Calculate average visibility of foot landmarks.
|
kinemotion/core/timing.py
CHANGED
|
@@ -1,44 +1,160 @@
|
|
|
1
|
-
"""Timing utilities for performance profiling.
|
|
1
|
+
"""Timing utilities for performance profiling.
|
|
2
|
+
|
|
3
|
+
This module implements a hybrid instrumentation pattern combining:
|
|
4
|
+
1. Protocol-based type safety (structural subtyping)
|
|
5
|
+
2. Null Object Pattern (zero overhead when disabled)
|
|
6
|
+
3. High-precision timing (time.perf_counter)
|
|
7
|
+
4. Memory optimization (__slots__)
|
|
8
|
+
5. Accumulation support (for loops and repeated measurements)
|
|
9
|
+
|
|
10
|
+
Performance Characteristics:
|
|
11
|
+
- PerformanceTimer overhead: ~200ns per measurement
|
|
12
|
+
- NullTimer overhead: ~20ns per measurement
|
|
13
|
+
- Memory: 32 bytes per timer instance
|
|
14
|
+
- Precision: ~1 microsecond (perf_counter)
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
# Active timing
|
|
18
|
+
timer = PerformanceTimer()
|
|
19
|
+
with timer.measure("video_processing"):
|
|
20
|
+
process_video(frames)
|
|
21
|
+
metrics = timer.get_metrics()
|
|
22
|
+
|
|
23
|
+
# Zero-overhead timing (disabled)
|
|
24
|
+
tracker = PoseTracker(timer=NULL_TIMER)
|
|
25
|
+
# No timing overhead, but maintains API compatibility
|
|
26
|
+
"""
|
|
2
27
|
|
|
3
28
|
import time
|
|
4
|
-
from
|
|
5
|
-
from
|
|
29
|
+
from contextlib import AbstractContextManager
|
|
30
|
+
from typing import Protocol, runtime_checkable
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@runtime_checkable
|
|
34
|
+
class Timer(Protocol):
|
|
35
|
+
"""Protocol for timer implementations.
|
|
36
|
+
|
|
37
|
+
Enables type-safe substitution of PerformanceTimer with NullTimer.
|
|
38
|
+
Uses structural subtyping - any class implementing these methods
|
|
39
|
+
conforms to the protocol.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def measure(self, name: str) -> AbstractContextManager[None]:
|
|
43
|
+
"""Context manager to measure execution time of a block.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
name: Name of the step being measured (e.g., "pose_tracking")
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Context manager that measures execution time
|
|
50
|
+
"""
|
|
51
|
+
...
|
|
52
|
+
|
|
53
|
+
def get_metrics(self) -> dict[str, float]:
|
|
54
|
+
"""Retrieve all collected timing metrics.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Dictionary mapping operation names to durations in seconds
|
|
58
|
+
"""
|
|
59
|
+
...
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class _MeasureContext(AbstractContextManager[None]):
|
|
63
|
+
"""Optimized context manager for active timing.
|
|
64
|
+
|
|
65
|
+
Uses __slots__ for memory efficiency and perf_counter for precision.
|
|
66
|
+
Accumulates durations for repeated measurements of the same operation.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
__slots__ = ("_metrics", "_name", "_start")
|
|
70
|
+
|
|
71
|
+
def __init__(self, metrics: dict[str, float], name: str) -> None:
|
|
72
|
+
"""Initialize measurement context.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
metrics: Dictionary to store timing results
|
|
76
|
+
name: Name of the operation being measured
|
|
77
|
+
"""
|
|
78
|
+
self._metrics = metrics
|
|
79
|
+
self._name = name
|
|
80
|
+
self._start = 0.0
|
|
81
|
+
|
|
82
|
+
def __enter__(self) -> None:
|
|
83
|
+
"""Start timing measurement using high-precision counter."""
|
|
84
|
+
self._start = time.perf_counter()
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> bool:
|
|
88
|
+
"""Complete timing measurement and accumulate duration.
|
|
89
|
+
|
|
90
|
+
Accumulates duration if the same operation is measured multiple times.
|
|
91
|
+
This is useful for measuring operations in loops.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
exc_type: Exception type (if any)
|
|
95
|
+
exc_val: Exception value (if any)
|
|
96
|
+
exc_tb: Exception traceback (if any)
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
False (does not suppress exceptions)
|
|
100
|
+
"""
|
|
101
|
+
duration = time.perf_counter() - self._start
|
|
102
|
+
# Accumulate for repeated measurements (e.g., in loops)
|
|
103
|
+
self._metrics[self._name] = self._metrics.get(self._name, 0.0) + duration
|
|
104
|
+
return False
|
|
6
105
|
|
|
7
106
|
|
|
8
107
|
class PerformanceTimer:
|
|
9
|
-
"""
|
|
108
|
+
"""High-precision timer for tracking execution duration of named steps.
|
|
109
|
+
|
|
110
|
+
Uses time.perf_counter() for high-resolution monotonic timing.
|
|
111
|
+
Suitable for development, profiling, and performance analysis.
|
|
112
|
+
|
|
113
|
+
Accumulates timing data for repeated measurements of the same operation,
|
|
114
|
+
making it suitable for measuring operations in loops.
|
|
115
|
+
|
|
116
|
+
Precision: ~1 microsecond on most platforms
|
|
117
|
+
Overhead: ~200 nanoseconds per measurement
|
|
10
118
|
|
|
11
|
-
|
|
12
|
-
|
|
119
|
+
Example:
|
|
120
|
+
timer = PerformanceTimer()
|
|
121
|
+
|
|
122
|
+
# Measure single operation
|
|
123
|
+
with timer.measure("video_initialization"):
|
|
124
|
+
initialize_video(path)
|
|
125
|
+
|
|
126
|
+
# Measure in loop (accumulates)
|
|
127
|
+
for frame in frames:
|
|
128
|
+
with timer.measure("pose_tracking"):
|
|
129
|
+
track_pose(frame)
|
|
130
|
+
|
|
131
|
+
metrics = timer.get_metrics()
|
|
132
|
+
print(f"Total pose tracking: {metrics['pose_tracking']:.3f}s")
|
|
13
133
|
"""
|
|
14
134
|
|
|
135
|
+
__slots__ = ("metrics",)
|
|
136
|
+
|
|
15
137
|
def __init__(self) -> None:
|
|
16
138
|
"""Initialize timer with empty metrics dictionary."""
|
|
17
139
|
self.metrics: dict[str, float] = {}
|
|
18
140
|
|
|
19
|
-
|
|
20
|
-
def measure(self, name: str) -> Generator[None, None, None]:
|
|
141
|
+
def measure(self, name: str) -> AbstractContextManager[None]:
|
|
21
142
|
"""Context manager to measure execution time of a block.
|
|
22
143
|
|
|
144
|
+
Uses perf_counter() for high-resolution monotonic timing.
|
|
145
|
+
More precise and reliable than time.time() for performance measurement.
|
|
146
|
+
|
|
23
147
|
Args:
|
|
24
148
|
name: Name of the step being measured (e.g., "pose_tracking")
|
|
25
149
|
|
|
26
|
-
|
|
27
|
-
|
|
150
|
+
Returns:
|
|
151
|
+
Context manager that measures execution time
|
|
28
152
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
# code to measure
|
|
33
|
-
pass
|
|
34
|
-
metrics = timer.get_metrics() # {"video_initialization": 0.123}
|
|
153
|
+
Note:
|
|
154
|
+
perf_counter() is monotonic - not affected by system clock adjustments.
|
|
155
|
+
Repeated measurements of the same operation name will accumulate.
|
|
35
156
|
"""
|
|
36
|
-
|
|
37
|
-
try:
|
|
38
|
-
yield
|
|
39
|
-
finally:
|
|
40
|
-
duration = time.time() - start_time
|
|
41
|
-
self.metrics[name] = duration
|
|
157
|
+
return _MeasureContext(self.metrics, name)
|
|
42
158
|
|
|
43
159
|
def get_metrics(self) -> dict[str, float]:
|
|
44
160
|
"""Get collected timing metrics in seconds.
|
|
@@ -47,3 +163,84 @@ class PerformanceTimer:
|
|
|
47
163
|
A copy of the metrics dictionary to prevent external modification.
|
|
48
164
|
"""
|
|
49
165
|
return self.metrics.copy()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class _NullContext(AbstractContextManager[None]):
|
|
169
|
+
"""Singleton null context manager with zero overhead.
|
|
170
|
+
|
|
171
|
+
Implements the context manager protocol but performs no operations.
|
|
172
|
+
Optimized away by the Python interpreter for minimal overhead.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
__slots__ = ()
|
|
176
|
+
|
|
177
|
+
def __enter__(self) -> None:
|
|
178
|
+
"""No-op entry - returns immediately."""
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> bool:
|
|
182
|
+
"""No-op exit - returns immediately.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
exc_type: Exception type (ignored)
|
|
186
|
+
exc_val: Exception value (ignored)
|
|
187
|
+
exc_tb: Exception traceback (ignored)
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
False (does not suppress exceptions)
|
|
191
|
+
"""
|
|
192
|
+
return False
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class NullTimer:
|
|
196
|
+
"""No-op timer implementing the Null Object Pattern.
|
|
197
|
+
|
|
198
|
+
Provides zero-overhead instrumentation when profiling is disabled.
|
|
199
|
+
All methods are no-ops that optimize away at runtime.
|
|
200
|
+
|
|
201
|
+
Performance: ~20-30 nanoseconds overhead per measure() call.
|
|
202
|
+
This is negligible compared to any actual work being measured.
|
|
203
|
+
|
|
204
|
+
Use Cases:
|
|
205
|
+
- Production deployments (profiling disabled)
|
|
206
|
+
- Performance-critical paths
|
|
207
|
+
- Testing without timing dependencies
|
|
208
|
+
|
|
209
|
+
Example:
|
|
210
|
+
# Use global singleton for zero allocation overhead
|
|
211
|
+
tracker = PoseTracker(timer=NULL_TIMER)
|
|
212
|
+
|
|
213
|
+
# No overhead - measure() call optimizes to nothing
|
|
214
|
+
with tracker.timer.measure("operation"):
|
|
215
|
+
do_work()
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
__slots__ = ()
|
|
219
|
+
|
|
220
|
+
def measure(self, name: str) -> AbstractContextManager[None]:
|
|
221
|
+
"""Return a no-op context manager.
|
|
222
|
+
|
|
223
|
+
This method does nothing and is optimized away by the Python interpreter.
|
|
224
|
+
The context manager protocol (__enter__/__exit__) has minimal overhead.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
name: Ignored - kept for protocol compatibility
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
Singleton null context manager
|
|
231
|
+
"""
|
|
232
|
+
return _NULL_CONTEXT
|
|
233
|
+
|
|
234
|
+
def get_metrics(self) -> dict[str, float]:
|
|
235
|
+
"""Return empty metrics dictionary.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Empty dictionary (no metrics collected)
|
|
239
|
+
"""
|
|
240
|
+
return {}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# Singleton instances for global reuse
|
|
244
|
+
# Use these instead of creating new instances to avoid allocation overhead
|
|
245
|
+
_NULL_CONTEXT = _NullContext()
|
|
246
|
+
NULL_TIMER: Timer = NullTimer()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kinemotion
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.46.0
|
|
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
|
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
kinemotion/__init__.py,sha256=wPItmyGJUOFM6GPRVhAEvRz0-ErI7e2qiUREYJ9EfPQ,943
|
|
2
|
-
kinemotion/api.py,sha256=
|
|
2
|
+
kinemotion/api.py,sha256=AWURqiz0SI1BGh6mTlywTOWKFGrXyoZJbmo_t6sRkjQ,32538
|
|
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=qtULzp9uYzm5M0_Qu5YGJpuwjg9fz1VKAg6xg4NJxvM,21639
|
|
6
|
-
kinemotion/cmj/cli.py,sha256=
|
|
6
|
+
kinemotion/cmj/cli.py,sha256=HpZgLWoLjcgsfOZu6EQ_26tg6QwTgFjR-Ly8WCBg24c,9904
|
|
7
7
|
kinemotion/cmj/debug_overlay.py,sha256=fXmWoHhqMLGo4vTtB6Ezs3yLUDOLw63zLIgU2gFlJQU,15892
|
|
8
8
|
kinemotion/cmj/joint_angles.py,sha256=HmheIEiKcQz39cRezk4h-htorOhGNPsqKIR9RsAEKts,9960
|
|
9
9
|
kinemotion/cmj/kinematics.py,sha256=Lq9m9MNQxnXv31VhKmXVrlM7rRkhi8PxW50N_CC8_8Y,11860
|
|
10
10
|
kinemotion/cmj/metrics_validator.py,sha256=V_fmlczYH06SBtwqESv-IfGi3wDsIy3RQbd7VwOyNo0,31359
|
|
11
11
|
kinemotion/cmj/validation_bounds.py,sha256=9ZTo68fl3ooyWjXXyTMRLpK9tFANa_rQf3oHhq7iQGE,11995
|
|
12
|
-
kinemotion/core/__init__.py,sha256=
|
|
12
|
+
kinemotion/core/__init__.py,sha256=mIsuXS9L7jk-3TCSlEdQ5nlgEAMXl7v5xfRFycwDn80,1430
|
|
13
13
|
kinemotion/core/auto_tuning.py,sha256=wtCUMOhBChVJNXfEeku3GCMW4qED6MF-O_mv2sPTiVQ,11324
|
|
14
14
|
kinemotion/core/cli_utils.py,sha256=sQPbT6XWWau-sm9yuN5c3eS5xNzoQGGXwSz6hQXtRvM,1859
|
|
15
|
-
kinemotion/core/debug_overlay_utils.py,sha256=
|
|
15
|
+
kinemotion/core/debug_overlay_utils.py,sha256=Eu4GXm8VeaDhU7voDjPJ4JvR-7ypT1mYmCz0d-M39N4,9027
|
|
16
16
|
kinemotion/core/determinism.py,sha256=NwVrHqJiVxxFHTBPVy8aDBJH2SLIcYIpdGFp7glblB8,2515
|
|
17
17
|
kinemotion/core/experimental.py,sha256=IK05AF4aZS15ke85hF3TWCqRIXU1AlD_XKzFz735Ua8,3640
|
|
18
18
|
kinemotion/core/filtering.py,sha256=GsC9BB71V07LJJHgS2lsaxUAtJsupcUiwtZFDgODh8c,11417
|
|
19
19
|
kinemotion/core/formatting.py,sha256=G_3eqgOtym9RFOZVEwCxye4A2cyrmgvtQ214vIshowU,2480
|
|
20
20
|
kinemotion/core/metadata.py,sha256=bJAVa4nym__zx1hNowSZduMGKBSGOPxTbBQkjm6N0D0,7207
|
|
21
|
-
kinemotion/core/pipeline_utils.py,sha256=
|
|
21
|
+
kinemotion/core/pipeline_utils.py,sha256=n6ee90xOYfBGkDCM1_F2rpYVsC3wWyKSTtWpAFz0Fh0,14161
|
|
22
22
|
kinemotion/core/pose.py,sha256=Tq4VS0YmMzrprVUsELm6FQczyLhP8UKurM9ccYn1LLU,8959
|
|
23
23
|
kinemotion/core/quality.py,sha256=dPGQp08y8DdEUbUdjTThnUOUsALgF0D2sdz50cm6wLI,13098
|
|
24
24
|
kinemotion/core/smoothing.py,sha256=GAfC-jxu1eqNyDjsUXqUBicKx9um5hrk49wz1FxfRNM,15219
|
|
25
|
-
kinemotion/core/timing.py,sha256=
|
|
25
|
+
kinemotion/core/timing.py,sha256=Zjhue9LBM1kOcYhqYx3K-OIulnMN8yJer_m3V9i_vqo,7730
|
|
26
26
|
kinemotion/core/validation.py,sha256=LmKfSl4Ayw3DgwKD9IrhsPdzp5ia4drLsHA2UuU1SCM,6310
|
|
27
27
|
kinemotion/core/video_io.py,sha256=HyLwn22fKe37j18853YYYrQi0JQWAwxpepPLNkuZKnQ,8586
|
|
28
28
|
kinemotion/dropjump/__init__.py,sha256=tC3H3BrCg8Oj-db-Vrtx4PH_llR1Ppkd5jwaOjhQcLg,862
|
|
@@ -33,8 +33,8 @@ kinemotion/dropjump/kinematics.py,sha256=kH-XM66wlOCYMpjvyb6_Qh5ZebyOfFZ47rmhgE1
|
|
|
33
33
|
kinemotion/dropjump/metrics_validator.py,sha256=CrTlGup8q2kyPXtA6HNwm7_yq0AsBaDllG7RVZdXmYA,9342
|
|
34
34
|
kinemotion/dropjump/validation_bounds.py,sha256=5b4I3CKPybuvrbn-nP5yCcGF_sH4Vtyw3a5AWWvWnBk,4645
|
|
35
35
|
kinemotion/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
36
|
-
kinemotion-0.
|
|
37
|
-
kinemotion-0.
|
|
38
|
-
kinemotion-0.
|
|
39
|
-
kinemotion-0.
|
|
40
|
-
kinemotion-0.
|
|
36
|
+
kinemotion-0.46.0.dist-info/METADATA,sha256=IRNoNMIpHqtIEc1LZzTvL6k4_8SzAaPLlY6SqI1RzsM,26020
|
|
37
|
+
kinemotion-0.46.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
38
|
+
kinemotion-0.46.0.dist-info/entry_points.txt,sha256=zaqnAnjLvcdrk1Qvj5nvXZCZ2gp0prS7it1zTJygcIY,50
|
|
39
|
+
kinemotion-0.46.0.dist-info/licenses/LICENSE,sha256=KZajvqsHw0NoOHOi2q0FZ4NBe9HdV6oey-IPYAtHXfg,1088
|
|
40
|
+
kinemotion-0.46.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|