kinemotion 0.10.6__py3-none-any.whl → 0.67.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of kinemotion might be problematic. Click here for more details.
- kinemotion/__init__.py +31 -6
- kinemotion/api.py +39 -598
- kinemotion/cli.py +2 -0
- kinemotion/cmj/__init__.py +5 -0
- kinemotion/cmj/analysis.py +621 -0
- kinemotion/cmj/api.py +563 -0
- kinemotion/cmj/cli.py +324 -0
- kinemotion/cmj/debug_overlay.py +457 -0
- kinemotion/cmj/joint_angles.py +307 -0
- kinemotion/cmj/kinematics.py +360 -0
- kinemotion/cmj/metrics_validator.py +767 -0
- kinemotion/cmj/validation_bounds.py +341 -0
- kinemotion/core/__init__.py +28 -0
- kinemotion/core/auto_tuning.py +71 -37
- kinemotion/core/cli_utils.py +60 -0
- kinemotion/core/debug_overlay_utils.py +385 -0
- kinemotion/core/determinism.py +83 -0
- kinemotion/core/experimental.py +103 -0
- kinemotion/core/filtering.py +9 -6
- kinemotion/core/formatting.py +75 -0
- kinemotion/core/metadata.py +231 -0
- kinemotion/core/model_downloader.py +172 -0
- kinemotion/core/pipeline_utils.py +433 -0
- kinemotion/core/pose.py +298 -141
- kinemotion/core/pose_landmarks.py +67 -0
- kinemotion/core/quality.py +393 -0
- kinemotion/core/smoothing.py +250 -154
- kinemotion/core/timing.py +247 -0
- kinemotion/core/types.py +42 -0
- kinemotion/core/validation.py +201 -0
- kinemotion/core/video_io.py +135 -50
- kinemotion/dropjump/__init__.py +1 -1
- kinemotion/dropjump/analysis.py +367 -182
- kinemotion/dropjump/api.py +665 -0
- kinemotion/dropjump/cli.py +156 -466
- kinemotion/dropjump/debug_overlay.py +136 -206
- kinemotion/dropjump/kinematics.py +232 -255
- kinemotion/dropjump/metrics_validator.py +240 -0
- kinemotion/dropjump/validation_bounds.py +157 -0
- kinemotion/models/__init__.py +0 -0
- kinemotion/models/pose_landmarker_lite.task +0 -0
- kinemotion-0.67.0.dist-info/METADATA +726 -0
- kinemotion-0.67.0.dist-info/RECORD +47 -0
- {kinemotion-0.10.6.dist-info → kinemotion-0.67.0.dist-info}/WHEEL +1 -1
- kinemotion-0.10.6.dist-info/METADATA +0 -561
- kinemotion-0.10.6.dist-info/RECORD +0 -20
- {kinemotion-0.10.6.dist-info → kinemotion-0.67.0.dist-info}/entry_points.txt +0 -0
- {kinemotion-0.10.6.dist-info → kinemotion-0.67.0.dist-info}/licenses/LICENSE +0 -0
kinemotion/core/smoothing.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Landmark smoothing utilities to reduce jitter in pose tracking."""
|
|
2
2
|
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
|
|
3
5
|
import numpy as np
|
|
4
6
|
from scipy.signal import savgol_filter
|
|
5
7
|
|
|
@@ -7,102 +9,212 @@ from .filtering import (
|
|
|
7
9
|
bilateral_temporal_filter,
|
|
8
10
|
reject_outliers,
|
|
9
11
|
)
|
|
12
|
+
from .timing import NULL_TIMER, Timer
|
|
13
|
+
from .types import FloatArray, LandmarkCoord, LandmarkSequence
|
|
10
14
|
|
|
15
|
+
# Type alias for smoothing function callback
|
|
16
|
+
SmootherFn = Callable[[list[float], list[float], list[int]], tuple[FloatArray, FloatArray]]
|
|
11
17
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
) -> list[
|
|
18
|
+
|
|
19
|
+
def _extract_landmark_coordinates(
|
|
20
|
+
landmark_sequence: LandmarkSequence,
|
|
21
|
+
landmark_name: str,
|
|
22
|
+
) -> tuple[list[float], list[float], list[int]]:
|
|
17
23
|
"""
|
|
18
|
-
|
|
24
|
+
Extract x, y coordinates and valid frame indices for a specific landmark.
|
|
19
25
|
|
|
20
26
|
Args:
|
|
21
27
|
landmark_sequence: List of landmark dictionaries from each frame
|
|
22
|
-
|
|
23
|
-
polyorder: Order of polynomial used to fit samples
|
|
28
|
+
landmark_name: Name of the landmark to extract
|
|
24
29
|
|
|
25
30
|
Returns:
|
|
26
|
-
|
|
31
|
+
Tuple of (x_coords, y_coords, valid_frames)
|
|
27
32
|
"""
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
x_coords: list[float] = []
|
|
34
|
+
y_coords: list[float] = []
|
|
35
|
+
valid_frames: list[int] = []
|
|
36
|
+
|
|
37
|
+
for i, frame_landmarks in enumerate(landmark_sequence):
|
|
38
|
+
if frame_landmarks is not None and landmark_name in frame_landmarks:
|
|
39
|
+
x, y, _ = frame_landmarks[landmark_name] # vis not used
|
|
40
|
+
x_coords.append(x)
|
|
41
|
+
y_coords.append(y)
|
|
42
|
+
valid_frames.append(i)
|
|
43
|
+
|
|
44
|
+
return x_coords, y_coords, valid_frames
|
|
31
45
|
|
|
32
|
-
# Ensure window_length is odd
|
|
33
|
-
if window_length % 2 == 0:
|
|
34
|
-
window_length += 1
|
|
35
46
|
|
|
36
|
-
|
|
37
|
-
|
|
47
|
+
def _get_landmark_names(
|
|
48
|
+
landmark_sequence: LandmarkSequence,
|
|
49
|
+
) -> list[str] | None:
|
|
50
|
+
"""
|
|
51
|
+
Extract landmark names from first valid frame.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
landmark_sequence: List of landmark dictionaries from each frame
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
List of landmark names or None if no valid frame found
|
|
58
|
+
"""
|
|
38
59
|
for frame_landmarks in landmark_sequence:
|
|
39
60
|
if frame_landmarks is not None:
|
|
40
|
-
|
|
41
|
-
|
|
61
|
+
return list(frame_landmarks.keys())
|
|
62
|
+
return None
|
|
63
|
+
|
|
42
64
|
|
|
65
|
+
def _fill_missing_frames(
|
|
66
|
+
smoothed_sequence: LandmarkSequence,
|
|
67
|
+
landmark_sequence: LandmarkSequence,
|
|
68
|
+
) -> None:
|
|
69
|
+
"""
|
|
70
|
+
Fill in any missing frames in smoothed sequence with original data.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
smoothed_sequence: Smoothed sequence (modified in place)
|
|
74
|
+
landmark_sequence: Original sequence
|
|
75
|
+
"""
|
|
76
|
+
for i in range(len(landmark_sequence)):
|
|
77
|
+
if i >= len(smoothed_sequence) or not smoothed_sequence[i]:
|
|
78
|
+
if i < len(smoothed_sequence):
|
|
79
|
+
smoothed_sequence[i] = landmark_sequence[i]
|
|
80
|
+
else:
|
|
81
|
+
smoothed_sequence.append(landmark_sequence[i])
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _store_smoothed_landmarks(
|
|
85
|
+
smoothed_sequence: LandmarkSequence,
|
|
86
|
+
landmark_sequence: LandmarkSequence,
|
|
87
|
+
landmark_name: str,
|
|
88
|
+
x_smooth: FloatArray,
|
|
89
|
+
y_smooth: FloatArray,
|
|
90
|
+
valid_frames: list[int],
|
|
91
|
+
) -> None:
|
|
92
|
+
"""
|
|
93
|
+
Store smoothed landmark values back into the sequence.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
smoothed_sequence: Sequence to store smoothed values into (modified in place)
|
|
97
|
+
landmark_sequence: Original sequence (for visibility values)
|
|
98
|
+
landmark_name: Name of the landmark being smoothed
|
|
99
|
+
x_smooth: Smoothed x coordinates
|
|
100
|
+
y_smooth: Smoothed y coordinates
|
|
101
|
+
valid_frames: Frame indices corresponding to smoothed values
|
|
102
|
+
"""
|
|
103
|
+
for idx, frame_idx in enumerate(valid_frames):
|
|
104
|
+
if frame_idx >= len(smoothed_sequence):
|
|
105
|
+
empty_frames: list[dict[str, LandmarkCoord]] = [{}] * (
|
|
106
|
+
frame_idx - len(smoothed_sequence) + 1
|
|
107
|
+
)
|
|
108
|
+
smoothed_sequence.extend(empty_frames)
|
|
109
|
+
|
|
110
|
+
# Ensure smoothed_sequence[frame_idx] is a dict, not None
|
|
111
|
+
if smoothed_sequence[frame_idx] is None:
|
|
112
|
+
smoothed_sequence[frame_idx] = {}
|
|
113
|
+
|
|
114
|
+
# Type narrowing: after the check above, we know it's a dict
|
|
115
|
+
frame_dict = smoothed_sequence[frame_idx]
|
|
116
|
+
assert frame_dict is not None # for type checker
|
|
117
|
+
|
|
118
|
+
if landmark_name not in frame_dict and landmark_sequence[frame_idx] is not None:
|
|
119
|
+
# Keep original visibility
|
|
120
|
+
orig_landmarks = landmark_sequence[frame_idx]
|
|
121
|
+
assert orig_landmarks is not None # for type checker
|
|
122
|
+
orig_vis = orig_landmarks[landmark_name][2]
|
|
123
|
+
frame_dict[landmark_name] = (
|
|
124
|
+
float(x_smooth[idx]),
|
|
125
|
+
float(y_smooth[idx]),
|
|
126
|
+
orig_vis,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _smooth_landmarks_core( # NOSONAR(S1172) - polyorder used via closure
|
|
131
|
+
# capture in smoother_fn
|
|
132
|
+
landmark_sequence: LandmarkSequence,
|
|
133
|
+
window_length: int,
|
|
134
|
+
polyorder: int,
|
|
135
|
+
smoother_fn: SmootherFn,
|
|
136
|
+
) -> LandmarkSequence:
|
|
137
|
+
"""
|
|
138
|
+
Core smoothing logic shared by both standard and advanced smoothing.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
landmark_sequence: List of landmark dictionaries from each frame
|
|
142
|
+
window_length: Length of filter window (must be odd)
|
|
143
|
+
polyorder: Order of polynomial used to fit samples (captured by
|
|
144
|
+
smoother_fn closure)
|
|
145
|
+
smoother_fn: Function that takes (x_coords, y_coords, valid_frames)
|
|
146
|
+
and returns (x_smooth, y_smooth)
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Smoothed landmark sequence
|
|
150
|
+
"""
|
|
151
|
+
landmark_names = _get_landmark_names(landmark_sequence)
|
|
43
152
|
if landmark_names is None:
|
|
44
153
|
return landmark_sequence
|
|
45
154
|
|
|
46
|
-
|
|
47
|
-
smoothed_sequence: list[dict[str, tuple[float, float, float]] | None] = []
|
|
155
|
+
smoothed_sequence: LandmarkSequence = []
|
|
48
156
|
|
|
49
157
|
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)
|
|
158
|
+
x_coords, y_coords, valid_frames = _extract_landmark_coordinates(
|
|
159
|
+
landmark_sequence, landmark_name
|
|
160
|
+
)
|
|
61
161
|
|
|
62
162
|
if len(x_coords) < window_length:
|
|
63
163
|
continue
|
|
64
164
|
|
|
65
|
-
# Apply
|
|
66
|
-
x_smooth =
|
|
67
|
-
y_smooth = savgol_filter(y_coords, window_length, polyorder)
|
|
165
|
+
# Apply smoothing function
|
|
166
|
+
x_smooth, y_smooth = smoother_fn(x_coords, y_coords, valid_frames)
|
|
68
167
|
|
|
69
168
|
# 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
|
-
)
|
|
169
|
+
_store_smoothed_landmarks(
|
|
170
|
+
smoothed_sequence,
|
|
171
|
+
landmark_sequence,
|
|
172
|
+
landmark_name,
|
|
173
|
+
x_smooth,
|
|
174
|
+
y_smooth,
|
|
175
|
+
valid_frames,
|
|
176
|
+
)
|
|
91
177
|
|
|
92
178
|
# 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])
|
|
179
|
+
_fill_missing_frames(smoothed_sequence, landmark_sequence)
|
|
99
180
|
|
|
100
181
|
return smoothed_sequence
|
|
101
182
|
|
|
102
183
|
|
|
103
|
-
def
|
|
104
|
-
|
|
105
|
-
|
|
184
|
+
def smooth_landmarks(
|
|
185
|
+
landmark_sequence: LandmarkSequence,
|
|
186
|
+
window_length: int = 5,
|
|
187
|
+
polyorder: int = 2,
|
|
188
|
+
) -> LandmarkSequence:
|
|
189
|
+
"""
|
|
190
|
+
Smooth landmark trajectories using Savitzky-Golay filter.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
landmark_sequence: List of landmark dictionaries from each frame
|
|
194
|
+
window_length: Length of filter window (must be odd, >= polyorder + 2)
|
|
195
|
+
polyorder: Order of polynomial used to fit samples
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Smoothed landmark sequence with same structure as input
|
|
199
|
+
"""
|
|
200
|
+
if len(landmark_sequence) < window_length:
|
|
201
|
+
return landmark_sequence
|
|
202
|
+
|
|
203
|
+
# Ensure window_length is odd
|
|
204
|
+
if window_length % 2 == 0:
|
|
205
|
+
window_length += 1
|
|
206
|
+
|
|
207
|
+
def savgol_smoother(
|
|
208
|
+
x_coords: list[float], y_coords: list[float], _valid_frames: list[int]
|
|
209
|
+
) -> tuple[FloatArray, FloatArray]:
|
|
210
|
+
x_smooth: FloatArray = savgol_filter(x_coords, window_length, polyorder)
|
|
211
|
+
y_smooth: FloatArray = savgol_filter(y_coords, window_length, polyorder)
|
|
212
|
+
return x_smooth, y_smooth
|
|
213
|
+
|
|
214
|
+
return _smooth_landmarks_core(landmark_sequence, window_length, polyorder, savgol_smoother)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def compute_velocity(positions: np.ndarray, fps: float, smooth_window: int = 3) -> np.ndarray:
|
|
106
218
|
"""
|
|
107
219
|
Compute velocity from position data.
|
|
108
220
|
|
|
@@ -228,7 +340,7 @@ def compute_acceleration_from_derivative(
|
|
|
228
340
|
|
|
229
341
|
|
|
230
342
|
def smooth_landmarks_advanced(
|
|
231
|
-
landmark_sequence:
|
|
343
|
+
landmark_sequence: LandmarkSequence,
|
|
232
344
|
window_length: int = 5,
|
|
233
345
|
polyorder: int = 2,
|
|
234
346
|
use_outlier_rejection: bool = True,
|
|
@@ -236,7 +348,8 @@ def smooth_landmarks_advanced(
|
|
|
236
348
|
ransac_threshold: float = 0.02,
|
|
237
349
|
bilateral_sigma_spatial: float = 3.0,
|
|
238
350
|
bilateral_sigma_intensity: float = 0.02,
|
|
239
|
-
|
|
351
|
+
timer: Timer | None = None,
|
|
352
|
+
) -> LandmarkSequence:
|
|
240
353
|
"""
|
|
241
354
|
Advanced landmark smoothing with outlier rejection and bilateral filtering.
|
|
242
355
|
|
|
@@ -254,113 +367,96 @@ def smooth_landmarks_advanced(
|
|
|
254
367
|
ransac_threshold: Threshold for RANSAC outlier detection
|
|
255
368
|
bilateral_sigma_spatial: Spatial sigma for bilateral filter
|
|
256
369
|
bilateral_sigma_intensity: Intensity sigma for bilateral filter
|
|
370
|
+
timer: Optional Timer for measuring operations
|
|
257
371
|
|
|
258
372
|
Returns:
|
|
259
373
|
Smoothed landmark sequence with same structure as input
|
|
260
374
|
"""
|
|
375
|
+
timer = timer or NULL_TIMER
|
|
261
376
|
if len(landmark_sequence) < window_length:
|
|
262
|
-
# Not enough frames to smooth effectively
|
|
263
377
|
return landmark_sequence
|
|
264
378
|
|
|
265
379
|
# Ensure window_length is odd
|
|
266
380
|
if window_length % 2 == 0:
|
|
267
381
|
window_length += 1
|
|
268
382
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
298
|
-
x_array = np.array(x_coords)
|
|
299
|
-
y_array = np.array(y_coords)
|
|
383
|
+
def advanced_smoother(
|
|
384
|
+
x_coords: list[float], y_coords: list[float], _valid_frames: list[int]
|
|
385
|
+
) -> tuple[FloatArray, FloatArray]:
|
|
386
|
+
x_array: FloatArray = np.array(x_coords)
|
|
387
|
+
y_array: FloatArray = np.array(y_coords)
|
|
300
388
|
|
|
301
389
|
# Step 1: Outlier rejection
|
|
302
390
|
if use_outlier_rejection:
|
|
303
|
-
|
|
304
|
-
x_array,
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
y_array,
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
391
|
+
with timer.measure("smoothing_outlier_rejection"):
|
|
392
|
+
x_array, _ = reject_outliers(
|
|
393
|
+
x_array,
|
|
394
|
+
use_ransac=True,
|
|
395
|
+
use_median=True,
|
|
396
|
+
ransac_threshold=ransac_threshold,
|
|
397
|
+
)
|
|
398
|
+
y_array, _ = reject_outliers(
|
|
399
|
+
y_array,
|
|
400
|
+
use_ransac=True,
|
|
401
|
+
use_median=True,
|
|
402
|
+
ransac_threshold=ransac_threshold,
|
|
403
|
+
)
|
|
315
404
|
|
|
316
405
|
# Step 2: Smoothing (bilateral or Savitzky-Golay)
|
|
317
406
|
if use_bilateral:
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
407
|
+
with timer.measure("smoothing_bilateral"):
|
|
408
|
+
x_smooth = bilateral_temporal_filter(
|
|
409
|
+
x_array,
|
|
410
|
+
window_size=window_length,
|
|
411
|
+
sigma_spatial=bilateral_sigma_spatial,
|
|
412
|
+
sigma_intensity=bilateral_sigma_intensity,
|
|
413
|
+
)
|
|
414
|
+
y_smooth = bilateral_temporal_filter(
|
|
415
|
+
y_array,
|
|
416
|
+
window_size=window_length,
|
|
417
|
+
sigma_spatial=bilateral_sigma_spatial,
|
|
418
|
+
sigma_intensity=bilateral_sigma_intensity,
|
|
419
|
+
)
|
|
330
420
|
else:
|
|
331
421
|
# Standard Savitzky-Golay
|
|
332
|
-
|
|
333
|
-
|
|
422
|
+
with timer.measure("smoothing_savgol"):
|
|
423
|
+
x_smooth: FloatArray = savgol_filter(x_array, window_length, polyorder) # type: ignore[reportUnknownVariableType]
|
|
424
|
+
y_smooth: FloatArray = savgol_filter(y_array, window_length, polyorder) # type: ignore[reportUnknownVariableType]
|
|
334
425
|
|
|
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
|
-
)
|
|
426
|
+
return x_smooth, y_smooth
|
|
341
427
|
|
|
342
|
-
|
|
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
|
-
)
|
|
428
|
+
return _smooth_landmarks_core(landmark_sequence, window_length, polyorder, advanced_smoother)
|
|
357
429
|
|
|
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])
|
|
365
430
|
|
|
366
|
-
|
|
431
|
+
def interpolate_threshold_crossing(
|
|
432
|
+
vel_before: float,
|
|
433
|
+
vel_after: float,
|
|
434
|
+
velocity_threshold: float,
|
|
435
|
+
) -> float:
|
|
436
|
+
"""
|
|
437
|
+
Find fractional offset where velocity crosses threshold between two frames.
|
|
438
|
+
|
|
439
|
+
Uses linear interpolation assuming velocity changes linearly between frames.
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
vel_before: Velocity at frame boundary N (absolute value)
|
|
443
|
+
vel_after: Velocity at frame boundary N+1 (absolute value)
|
|
444
|
+
velocity_threshold: Threshold value
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
Fractional offset from frame N (0.0 to 1.0)
|
|
448
|
+
"""
|
|
449
|
+
# Handle edge cases
|
|
450
|
+
if abs(vel_after - vel_before) < 1e-9: # Velocity not changing
|
|
451
|
+
return 0.5
|
|
452
|
+
|
|
453
|
+
# Linear interpolation: at what fraction t does velocity equal threshold?
|
|
454
|
+
# vel(t) = vel_before + t * (vel_after - vel_before)
|
|
455
|
+
# Solve for t when vel(t) = threshold:
|
|
456
|
+
# threshold = vel_before + t * (vel_after - vel_before)
|
|
457
|
+
# t = (threshold - vel_before) / (vel_after - vel_before)
|
|
458
|
+
|
|
459
|
+
t = (velocity_threshold - vel_before) / (vel_after - vel_before)
|
|
460
|
+
|
|
461
|
+
# Clamp to [0, 1] range
|
|
462
|
+
return float(max(0.0, min(1.0, t)))
|