kinemotion 0.45.1__py3-none-any.whl → 0.47.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 +14 -42
- kinemotion/cmj/analysis.py +46 -32
- kinemotion/core/__init__.py +13 -1
- kinemotion/core/debug_overlay_utils.py +6 -18
- kinemotion/core/pipeline_utils.py +30 -16
- kinemotion/core/pose.py +31 -32
- kinemotion/core/smoothing.py +33 -26
- kinemotion/core/timing.py +355 -22
- kinemotion/core/video_io.py +6 -20
- kinemotion/dropjump/analysis.py +10 -4
- kinemotion/dropjump/debug_overlay.py +2 -8
- kinemotion/dropjump/kinematics.py +33 -25
- {kinemotion-0.45.1.dist-info → kinemotion-0.47.0.dist-info}/METADATA +1 -1
- {kinemotion-0.45.1.dist-info → kinemotion-0.47.0.dist-info}/RECORD +17 -17
- {kinemotion-0.45.1.dist-info → kinemotion-0.47.0.dist-info}/WHEEL +0 -0
- {kinemotion-0.45.1.dist-info → kinemotion-0.47.0.dist-info}/entry_points.txt +0 -0
- {kinemotion-0.45.1.dist-info → kinemotion-0.47.0.dist-info}/licenses/LICENSE +0 -0
kinemotion/core/smoothing.py
CHANGED
|
@@ -9,6 +9,7 @@ from .filtering import (
|
|
|
9
9
|
bilateral_temporal_filter,
|
|
10
10
|
reject_outliers,
|
|
11
11
|
)
|
|
12
|
+
from .timing import NULL_TIMER, Timer
|
|
12
13
|
|
|
13
14
|
# Type aliases for landmark data structures
|
|
14
15
|
LandmarkCoord: TypeAlias = tuple[float, float, float] # (x, y, visibility)
|
|
@@ -347,6 +348,7 @@ def smooth_landmarks_advanced(
|
|
|
347
348
|
ransac_threshold: float = 0.02,
|
|
348
349
|
bilateral_sigma_spatial: float = 3.0,
|
|
349
350
|
bilateral_sigma_intensity: float = 0.02,
|
|
351
|
+
timer: Timer | None = None,
|
|
350
352
|
) -> LandmarkSequence:
|
|
351
353
|
"""
|
|
352
354
|
Advanced landmark smoothing with outlier rejection and bilateral filtering.
|
|
@@ -365,10 +367,12 @@ def smooth_landmarks_advanced(
|
|
|
365
367
|
ransac_threshold: Threshold for RANSAC outlier detection
|
|
366
368
|
bilateral_sigma_spatial: Spatial sigma for bilateral filter
|
|
367
369
|
bilateral_sigma_intensity: Intensity sigma for bilateral filter
|
|
370
|
+
timer: Optional Timer for measuring operations
|
|
368
371
|
|
|
369
372
|
Returns:
|
|
370
373
|
Smoothed landmark sequence with same structure as input
|
|
371
374
|
"""
|
|
375
|
+
timer = timer or NULL_TIMER
|
|
372
376
|
if len(landmark_sequence) < window_length:
|
|
373
377
|
return landmark_sequence
|
|
374
378
|
|
|
@@ -382,37 +386,40 @@ def smooth_landmarks_advanced(
|
|
|
382
386
|
|
|
383
387
|
# Step 1: Outlier rejection
|
|
384
388
|
if use_outlier_rejection:
|
|
385
|
-
|
|
386
|
-
x_array,
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
y_array,
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
389
|
+
with timer.measure("smoothing_outlier_rejection"):
|
|
390
|
+
x_array, _ = reject_outliers(
|
|
391
|
+
x_array,
|
|
392
|
+
use_ransac=True,
|
|
393
|
+
use_median=True,
|
|
394
|
+
ransac_threshold=ransac_threshold,
|
|
395
|
+
)
|
|
396
|
+
y_array, _ = reject_outliers(
|
|
397
|
+
y_array,
|
|
398
|
+
use_ransac=True,
|
|
399
|
+
use_median=True,
|
|
400
|
+
ransac_threshold=ransac_threshold,
|
|
401
|
+
)
|
|
397
402
|
|
|
398
403
|
# Step 2: Smoothing (bilateral or Savitzky-Golay)
|
|
399
404
|
if use_bilateral:
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
405
|
+
with timer.measure("smoothing_bilateral"):
|
|
406
|
+
x_smooth = bilateral_temporal_filter(
|
|
407
|
+
x_array,
|
|
408
|
+
window_size=window_length,
|
|
409
|
+
sigma_spatial=bilateral_sigma_spatial,
|
|
410
|
+
sigma_intensity=bilateral_sigma_intensity,
|
|
411
|
+
)
|
|
412
|
+
y_smooth = bilateral_temporal_filter(
|
|
413
|
+
y_array,
|
|
414
|
+
window_size=window_length,
|
|
415
|
+
sigma_spatial=bilateral_sigma_spatial,
|
|
416
|
+
sigma_intensity=bilateral_sigma_intensity,
|
|
417
|
+
)
|
|
412
418
|
else:
|
|
413
419
|
# Standard Savitzky-Golay
|
|
414
|
-
|
|
415
|
-
|
|
420
|
+
with timer.measure("smoothing_savgol"):
|
|
421
|
+
x_smooth = savgol_filter(x_array, window_length, polyorder)
|
|
422
|
+
y_smooth = savgol_filter(y_array, window_length, polyorder)
|
|
416
423
|
|
|
417
424
|
return x_smooth, y_smooth
|
|
418
425
|
|
kinemotion/core/timing.py
CHANGED
|
@@ -1,44 +1,275 @@
|
|
|
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, ExitStack, contextmanager
|
|
30
|
+
from typing import Protocol, runtime_checkable
|
|
31
|
+
|
|
32
|
+
# OpenTelemetry related imports, guarded by try-except for optional dependency
|
|
33
|
+
_trace_module = None # This will hold the actual 'trace' module if imported
|
|
34
|
+
_otel_tracer_class = None # This will hold the actual 'Tracer' class if imported
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
import opentelemetry.trace as _trace_module_import # Import the module directly
|
|
38
|
+
|
|
39
|
+
_otel_tracer_class = (
|
|
40
|
+
_trace_module_import.Tracer
|
|
41
|
+
) # Get the Tracer class from the module
|
|
42
|
+
_trace_module = (
|
|
43
|
+
_trace_module_import # Expose the trace module globally after successful import
|
|
44
|
+
)
|
|
45
|
+
except ImportError:
|
|
46
|
+
pass # No OTel, so these remain None
|
|
47
|
+
|
|
48
|
+
# Now define the global/module-level variables used elsewhere
|
|
49
|
+
# Conditionally expose 'trace' and 'Tracer' aliases
|
|
50
|
+
trace = _trace_module # This will be the actual module or None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Tracer: # Dummy for type hints if actual Tracer is not available
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if _otel_tracer_class:
|
|
58
|
+
Tracer = _otel_tracer_class # Override dummy if actual Tracer is available
|
|
59
|
+
|
|
60
|
+
# This _OPENTELEMETRY_AVAILABLE variable is assigned only once,
|
|
61
|
+
# after the try-except block
|
|
62
|
+
_OPENTELEMETRY_AVAILABLE = bool(
|
|
63
|
+
_otel_tracer_class
|
|
64
|
+
) # True if Tracer class was successfully loaded
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@runtime_checkable
|
|
68
|
+
class Timer(Protocol):
|
|
69
|
+
"""Protocol for timer implementations.
|
|
70
|
+
|
|
71
|
+
Enables type-safe substitution of PerformanceTimer with NullTimer.
|
|
72
|
+
Uses structural subtyping - any class implementing these methods
|
|
73
|
+
conforms to the protocol.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def measure(self, name: str) -> AbstractContextManager[None]:
|
|
77
|
+
"""Context manager to measure execution time of a block.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
name: Name of the step being measured (e.g., "pose_tracking")
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Context manager that measures execution time
|
|
84
|
+
"""
|
|
85
|
+
...
|
|
86
|
+
|
|
87
|
+
def get_metrics(self) -> dict[str, float]:
|
|
88
|
+
"""Retrieve all collected timing metrics.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Dictionary mapping operation names to durations in seconds
|
|
92
|
+
"""
|
|
93
|
+
...
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class _NullContext(AbstractContextManager[None]):
|
|
97
|
+
"""Singleton null context manager with zero overhead.
|
|
98
|
+
|
|
99
|
+
Implements the context manager protocol but performs no operations.
|
|
100
|
+
Optimized away by the Python interpreter for minimal overhead.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
__slots__ = ()
|
|
104
|
+
|
|
105
|
+
def __enter__(self) -> None:
|
|
106
|
+
"""No-op entry - returns immediately."""
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> bool:
|
|
110
|
+
"""No-op exit - returns immediately.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
exc_type: Exception type (ignored)
|
|
114
|
+
exc_val: Exception value (ignored)
|
|
115
|
+
exc_tb: Exception traceback (ignored)
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
False (does not suppress exceptions)
|
|
119
|
+
"""
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class NullTimer:
|
|
124
|
+
"""No-op timer implementing the Null Object Pattern.
|
|
125
|
+
|
|
126
|
+
Provides zero-overhead instrumentation when profiling is disabled.
|
|
127
|
+
All methods are no-ops that optimize away at runtime.
|
|
128
|
+
|
|
129
|
+
Performance: ~20-30 nanoseconds overhead per measure() call.
|
|
130
|
+
This is negligible compared to any actual work being measured.
|
|
131
|
+
|
|
132
|
+
Use Cases:
|
|
133
|
+
- Production deployments (profiling disabled)
|
|
134
|
+
- Performance-critical paths
|
|
135
|
+
- Testing without timing dependencies
|
|
136
|
+
|
|
137
|
+
Example:
|
|
138
|
+
# Use global singleton for zero allocation overhead
|
|
139
|
+
tracker = PoseTracker(timer=NULL_TIMER)
|
|
140
|
+
|
|
141
|
+
# No overhead - measure() call optimizes to nothing
|
|
142
|
+
with tracker.timer.measure("operation"):
|
|
143
|
+
do_work()
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
__slots__ = ()
|
|
147
|
+
|
|
148
|
+
def measure(self, name: str) -> AbstractContextManager[None]:
|
|
149
|
+
"""Return a no-op context manager.
|
|
150
|
+
|
|
151
|
+
This method does nothing and is optimized away by the Python interpreter.
|
|
152
|
+
The context manager protocol (__enter__/__exit__) has minimal overhead.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
name: Ignored - kept for protocol compatibility
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Singleton null context manager
|
|
159
|
+
"""
|
|
160
|
+
return _NULL_CONTEXT
|
|
161
|
+
|
|
162
|
+
def get_metrics(self) -> dict[str, float]:
|
|
163
|
+
"""Return empty metrics dictionary.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Empty dictionary (no metrics collected)
|
|
167
|
+
"""
|
|
168
|
+
return {}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# Singleton instances for global reuse
|
|
172
|
+
# Use these instead of creating new instances to avoid allocation overhead
|
|
173
|
+
_NULL_CONTEXT = _NullContext()
|
|
174
|
+
NULL_TIMER: Timer = NullTimer()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class _MeasureContext(AbstractContextManager[None]):
|
|
178
|
+
"""Optimized context manager for active timing.
|
|
179
|
+
|
|
180
|
+
Uses __slots__ for memory efficiency and perf_counter for precision.
|
|
181
|
+
Accumulates durations for repeated measurements of the same operation.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
__slots__ = ("_metrics", "_name", "_start")
|
|
185
|
+
|
|
186
|
+
def __init__(self, metrics: dict[str, float], name: str) -> None:
|
|
187
|
+
"""Initialize measurement context.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
metrics: Dictionary to store timing results
|
|
191
|
+
name: Name of the operation being measured
|
|
192
|
+
"""
|
|
193
|
+
self._metrics = metrics
|
|
194
|
+
self._name = name
|
|
195
|
+
self._start = 0.0
|
|
196
|
+
|
|
197
|
+
def __enter__(self) -> None:
|
|
198
|
+
"""Start timing measurement using high-precision counter."""
|
|
199
|
+
self._start = time.perf_counter()
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> bool:
|
|
203
|
+
"""Complete timing measurement and accumulate duration.
|
|
204
|
+
|
|
205
|
+
Accumulates duration if the same operation is measured multiple times.
|
|
206
|
+
This is useful for measuring operations in loops.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
exc_type: Exception type (if any)
|
|
210
|
+
exc_val: Exception value (if any)
|
|
211
|
+
exc_tb: Exception traceback (if any)
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
False (does not suppress exceptions)
|
|
215
|
+
"""
|
|
216
|
+
duration = time.perf_counter() - self._start
|
|
217
|
+
# Accumulate for repeated measurements (e.g., in loops)
|
|
218
|
+
self._metrics[self._name] = self._metrics.get(self._name, 0.0) + duration
|
|
219
|
+
return False
|
|
6
220
|
|
|
7
221
|
|
|
8
222
|
class PerformanceTimer:
|
|
9
|
-
"""
|
|
223
|
+
"""High-precision timer for tracking execution duration of named steps.
|
|
224
|
+
|
|
225
|
+
Uses time.perf_counter() for high-resolution monotonic timing.
|
|
226
|
+
Suitable for development, profiling, and performance analysis.
|
|
227
|
+
|
|
228
|
+
Accumulates timing data for repeated measurements of the same operation,
|
|
229
|
+
making it suitable for measuring operations in loops.
|
|
230
|
+
|
|
231
|
+
Precision: ~1 microsecond on most platforms
|
|
232
|
+
Overhead: ~200 nanoseconds per measurement
|
|
233
|
+
|
|
234
|
+
Example:
|
|
235
|
+
timer = PerformanceTimer()
|
|
10
236
|
|
|
11
|
-
|
|
12
|
-
|
|
237
|
+
# Measure single operation
|
|
238
|
+
with timer.measure("video_initialization"):
|
|
239
|
+
initialize_video(path)
|
|
240
|
+
|
|
241
|
+
# Measure in loop (accumulates)
|
|
242
|
+
for frame in frames:
|
|
243
|
+
with timer.measure("pose_tracking"):
|
|
244
|
+
track_pose(frame)
|
|
245
|
+
|
|
246
|
+
metrics = timer.get_metrics()
|
|
247
|
+
print(f"Total pose tracking: {metrics['pose_tracking']:.3f}s")
|
|
13
248
|
"""
|
|
14
249
|
|
|
250
|
+
__slots__ = ("metrics",)
|
|
251
|
+
|
|
15
252
|
def __init__(self) -> None:
|
|
16
253
|
"""Initialize timer with empty metrics dictionary."""
|
|
17
254
|
self.metrics: dict[str, float] = {}
|
|
18
255
|
|
|
19
|
-
|
|
20
|
-
def measure(self, name: str) -> Generator[None, None, None]:
|
|
256
|
+
def measure(self, name: str) -> AbstractContextManager[None]:
|
|
21
257
|
"""Context manager to measure execution time of a block.
|
|
22
258
|
|
|
259
|
+
Uses perf_counter() for high-resolution monotonic timing.
|
|
260
|
+
More precise and reliable than time.time() for performance measurement.
|
|
261
|
+
|
|
23
262
|
Args:
|
|
24
263
|
name: Name of the step being measured (e.g., "pose_tracking")
|
|
25
264
|
|
|
26
|
-
|
|
27
|
-
|
|
265
|
+
Returns:
|
|
266
|
+
Context manager that measures execution time
|
|
28
267
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
# code to measure
|
|
33
|
-
pass
|
|
34
|
-
metrics = timer.get_metrics() # {"video_initialization": 0.123}
|
|
268
|
+
Note:
|
|
269
|
+
perf_counter() is monotonic - not affected by system clock adjustments.
|
|
270
|
+
Repeated measurements of the same operation name will accumulate.
|
|
35
271
|
"""
|
|
36
|
-
|
|
37
|
-
try:
|
|
38
|
-
yield
|
|
39
|
-
finally:
|
|
40
|
-
duration = time.time() - start_time
|
|
41
|
-
self.metrics[name] = duration
|
|
272
|
+
return _MeasureContext(self.metrics, name)
|
|
42
273
|
|
|
43
274
|
def get_metrics(self) -> dict[str, float]:
|
|
44
275
|
"""Get collected timing metrics in seconds.
|
|
@@ -47,3 +278,105 @@ class PerformanceTimer:
|
|
|
47
278
|
A copy of the metrics dictionary to prevent external modification.
|
|
48
279
|
"""
|
|
49
280
|
return self.metrics.copy()
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
@contextmanager
|
|
284
|
+
def _composite_context_manager(contexts: list[AbstractContextManager[None]]):
|
|
285
|
+
"""Helper to combine multiple context managers into one.
|
|
286
|
+
|
|
287
|
+
Uses ExitStack to manage entering and exiting multiple contexts transparently.
|
|
288
|
+
"""
|
|
289
|
+
with ExitStack() as stack:
|
|
290
|
+
for ctx in contexts:
|
|
291
|
+
stack.enter_context(ctx)
|
|
292
|
+
yield
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class CompositeTimer:
|
|
296
|
+
"""Timer that delegates measurements to multiple underlying timers.
|
|
297
|
+
|
|
298
|
+
Useful for enabling both local performance timing (for JSON output)
|
|
299
|
+
and distributed tracing (OpenTelemetry) simultaneously.
|
|
300
|
+
"""
|
|
301
|
+
|
|
302
|
+
__slots__ = ("timers",)
|
|
303
|
+
|
|
304
|
+
def __init__(self, timers: list[Timer]) -> None:
|
|
305
|
+
"""Initialize composite timer.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
timers: List of timer instances to delegate to
|
|
309
|
+
"""
|
|
310
|
+
self.timers = timers
|
|
311
|
+
|
|
312
|
+
def measure(self, name: str) -> AbstractContextManager[None]:
|
|
313
|
+
"""Measure using all underlying timers.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
name: Name of the operation
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
Context manager that manages all underlying timers
|
|
320
|
+
"""
|
|
321
|
+
contexts = [timer.measure(name) for timer in self.timers]
|
|
322
|
+
return _composite_context_manager(contexts)
|
|
323
|
+
|
|
324
|
+
def get_metrics(self) -> dict[str, float]:
|
|
325
|
+
"""Get combined metrics from all timers.
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
Merged dictionary of metrics
|
|
329
|
+
"""
|
|
330
|
+
metrics = {}
|
|
331
|
+
for timer in self.timers:
|
|
332
|
+
metrics.update(timer.get_metrics())
|
|
333
|
+
return metrics
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class OpenTelemetryTimer:
|
|
337
|
+
"""Timer implementation that creates OpenTelemetry spans.
|
|
338
|
+
|
|
339
|
+
Maps 'measure' calls to OTel spans. Requires opentelemetry-api installed.
|
|
340
|
+
"""
|
|
341
|
+
|
|
342
|
+
__slots__ = ("tracer",)
|
|
343
|
+
|
|
344
|
+
def __init__(self, tracer: Tracer | None = None) -> None:
|
|
345
|
+
"""Initialize OTel timer.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
tracer: Optional OTel tracer. If None, gets tracer for module name.
|
|
349
|
+
"""
|
|
350
|
+
if not _OPENTELEMETRY_AVAILABLE:
|
|
351
|
+
self.tracer = None # Always initialize self.tracer for __slots__
|
|
352
|
+
return
|
|
353
|
+
|
|
354
|
+
if trace is not None:
|
|
355
|
+
self.tracer = tracer or trace.get_tracer(__name__)
|
|
356
|
+
else:
|
|
357
|
+
# This branch should ideally not be reached if _OPENTELEMETRY_AVAILABLE
|
|
358
|
+
# is True but trace is None (meaning import succeeded but trace was not what
|
|
359
|
+
# expected). Defensive programming: ensure self.tracer is set.
|
|
360
|
+
self.tracer = None
|
|
361
|
+
|
|
362
|
+
def measure(self, name: str) -> AbstractContextManager[None]:
|
|
363
|
+
"""Start an OpenTelemetry span.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
name: Name of the span
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
Span context manager (compatible with AbstractContextManager)
|
|
370
|
+
"""
|
|
371
|
+
if not _OPENTELEMETRY_AVAILABLE or self.tracer is None:
|
|
372
|
+
return _NULL_CONTEXT # Return the no-op context
|
|
373
|
+
|
|
374
|
+
return self.tracer.start_as_current_span(name)
|
|
375
|
+
|
|
376
|
+
def get_metrics(self) -> dict[str, float]:
|
|
377
|
+
"""Return empty metrics (OTel handles export asynchronously).
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
Empty dictionary
|
|
381
|
+
"""
|
|
382
|
+
return {}
|
kinemotion/core/video_io.py
CHANGED
|
@@ -7,7 +7,7 @@ import warnings
|
|
|
7
7
|
import cv2
|
|
8
8
|
import numpy as np
|
|
9
9
|
|
|
10
|
-
from .timing import
|
|
10
|
+
from .timing import NULL_TIMER, Timer
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class VideoProcessor:
|
|
@@ -18,16 +18,16 @@ class VideoProcessor:
|
|
|
18
18
|
No dimensions are hardcoded - all dimensions are extracted from actual frame data.
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
|
-
def __init__(self, video_path: str, timer:
|
|
21
|
+
def __init__(self, video_path: str, timer: Timer | None = None) -> None:
|
|
22
22
|
"""
|
|
23
23
|
Initialize video processor.
|
|
24
24
|
|
|
25
25
|
Args:
|
|
26
26
|
video_path: Path to input video file
|
|
27
|
-
timer: Optional
|
|
27
|
+
timer: Optional Timer for measuring operations
|
|
28
28
|
"""
|
|
29
29
|
self.video_path = video_path
|
|
30
|
-
self.timer = timer
|
|
30
|
+
self.timer = timer or NULL_TIMER
|
|
31
31
|
self.cap = cv2.VideoCapture(video_path)
|
|
32
32
|
|
|
33
33
|
if not self.cap.isOpened():
|
|
@@ -179,28 +179,14 @@ class VideoProcessor:
|
|
|
179
179
|
OpenCV ignores rotation metadata, so we manually apply rotation
|
|
180
180
|
based on the display matrix metadata extracted from the video.
|
|
181
181
|
"""
|
|
182
|
-
|
|
183
|
-
with self.timer.measure("frame_read"):
|
|
184
|
-
ret, frame = self.cap.read()
|
|
185
|
-
else:
|
|
182
|
+
with self.timer.measure("frame_read"):
|
|
186
183
|
ret, frame = self.cap.read()
|
|
187
184
|
|
|
188
185
|
if not ret:
|
|
189
186
|
return None
|
|
190
187
|
|
|
191
188
|
# Apply rotation if video has rotation metadata
|
|
192
|
-
|
|
193
|
-
with self.timer.measure("frame_rotation"):
|
|
194
|
-
if self.rotation == -90 or self.rotation == 270:
|
|
195
|
-
# -90 degrees = rotate 90 degrees clockwise
|
|
196
|
-
frame = cv2.rotate(frame, cv2.ROTATE_90_CLOCKWISE)
|
|
197
|
-
elif self.rotation == 90 or self.rotation == -270:
|
|
198
|
-
# 90 degrees = rotate 90 degrees counter-clockwise
|
|
199
|
-
frame = cv2.rotate(frame, cv2.ROTATE_90_COUNTERCLOCKWISE)
|
|
200
|
-
elif self.rotation == 180 or self.rotation == -180:
|
|
201
|
-
# 180 degrees rotation
|
|
202
|
-
frame = cv2.rotate(frame, cv2.ROTATE_180)
|
|
203
|
-
else:
|
|
189
|
+
with self.timer.measure("frame_rotation"):
|
|
204
190
|
if self.rotation == -90 or self.rotation == 270:
|
|
205
191
|
# -90 degrees = rotate 90 degrees clockwise
|
|
206
192
|
frame = cv2.rotate(frame, cv2.ROTATE_90_CLOCKWISE)
|
kinemotion/dropjump/analysis.py
CHANGED
|
@@ -10,6 +10,7 @@ from ..core.smoothing import (
|
|
|
10
10
|
compute_velocity_from_derivative,
|
|
11
11
|
interpolate_threshold_crossing,
|
|
12
12
|
)
|
|
13
|
+
from ..core.timing import NULL_TIMER, Timer
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
class ContactState(Enum):
|
|
@@ -345,6 +346,7 @@ def detect_ground_contact(
|
|
|
345
346
|
visibilities: np.ndarray | None = None,
|
|
346
347
|
window_length: int = 5,
|
|
347
348
|
polyorder: int = 2,
|
|
349
|
+
timer: Timer | None = None,
|
|
348
350
|
) -> list[ContactState]:
|
|
349
351
|
"""
|
|
350
352
|
Detect when feet are in contact with ground based on vertical motion.
|
|
@@ -361,19 +363,22 @@ def detect_ground_contact(
|
|
|
361
363
|
visibilities: Array of visibility scores for each frame
|
|
362
364
|
window_length: Window size for velocity derivative calculation (must be odd)
|
|
363
365
|
polyorder: Polynomial order for Savitzky-Golay filter (default: 2)
|
|
366
|
+
timer: Optional Timer for measuring operations
|
|
364
367
|
|
|
365
368
|
Returns:
|
|
366
369
|
List of ContactState for each frame
|
|
367
370
|
"""
|
|
371
|
+
timer = timer or NULL_TIMER
|
|
368
372
|
n_frames = len(foot_positions)
|
|
369
373
|
|
|
370
374
|
if n_frames < 2:
|
|
371
375
|
return [ContactState.UNKNOWN] * n_frames
|
|
372
376
|
|
|
373
377
|
# Compute vertical velocity using derivative-based method
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
378
|
+
with timer.measure("dj_compute_velocity"):
|
|
379
|
+
velocities = compute_velocity_from_derivative(
|
|
380
|
+
foot_positions, window_length=window_length, polyorder=polyorder
|
|
381
|
+
)
|
|
377
382
|
|
|
378
383
|
# Detect stationary frames based on velocity threshold
|
|
379
384
|
is_stationary = np.abs(velocities) < velocity_threshold
|
|
@@ -384,7 +389,8 @@ def detect_ground_contact(
|
|
|
384
389
|
)
|
|
385
390
|
|
|
386
391
|
# Find frames with sustained contact
|
|
387
|
-
|
|
392
|
+
with timer.measure("dj_find_contact_frames"):
|
|
393
|
+
contact_frames = _find_contact_frames(is_stationary, min_contact_frames)
|
|
388
394
|
|
|
389
395
|
# Assign states
|
|
390
396
|
return _assign_contact_states(
|
|
@@ -138,10 +138,7 @@ class DebugOverlayRenderer(BaseDebugOverlayRenderer):
|
|
|
138
138
|
Returns:
|
|
139
139
|
Frame with debug overlay
|
|
140
140
|
"""
|
|
141
|
-
|
|
142
|
-
with self.timer.measure("debug_video_copy"):
|
|
143
|
-
annotated = frame.copy()
|
|
144
|
-
else:
|
|
141
|
+
with self.timer.measure("debug_video_copy"):
|
|
145
142
|
annotated = frame.copy()
|
|
146
143
|
|
|
147
144
|
def _draw_overlays() -> None:
|
|
@@ -181,10 +178,7 @@ class DebugOverlayRenderer(BaseDebugOverlayRenderer):
|
|
|
181
178
|
if metrics:
|
|
182
179
|
self._draw_phase_labels(annotated, frame_idx, metrics)
|
|
183
180
|
|
|
184
|
-
|
|
185
|
-
with self.timer.measure("debug_video_draw"):
|
|
186
|
-
_draw_overlays()
|
|
187
|
-
else:
|
|
181
|
+
with self.timer.measure("debug_video_draw"):
|
|
188
182
|
_draw_overlays()
|
|
189
183
|
|
|
190
184
|
return annotated
|