kinemotion 0.12.1__py3-none-any.whl → 0.12.3__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 +0 -2
- kinemotion/core/auto_tuning.py +66 -30
- kinemotion/core/pose.py +134 -95
- kinemotion/core/smoothing.py +2 -2
- kinemotion/core/video_io.py +53 -29
- kinemotion/dropjump/analysis.py +205 -123
- kinemotion/dropjump/cli.py +60 -57
- kinemotion/dropjump/debug_overlay.py +109 -97
- kinemotion/dropjump/kinematics.py +3 -6
- {kinemotion-0.12.1.dist-info → kinemotion-0.12.3.dist-info}/METADATA +1 -1
- {kinemotion-0.12.1.dist-info → kinemotion-0.12.3.dist-info}/RECORD +14 -14
- {kinemotion-0.12.1.dist-info → kinemotion-0.12.3.dist-info}/WHEEL +0 -0
- {kinemotion-0.12.1.dist-info → kinemotion-0.12.3.dist-info}/entry_points.txt +0 -0
- {kinemotion-0.12.1.dist-info → kinemotion-0.12.3.dist-info}/licenses/LICENSE +0 -0
kinemotion/dropjump/analysis.py
CHANGED
|
@@ -89,6 +89,87 @@ def calculate_adaptive_threshold(
|
|
|
89
89
|
return adaptive_threshold
|
|
90
90
|
|
|
91
91
|
|
|
92
|
+
def _find_stable_baseline(
|
|
93
|
+
positions: np.ndarray,
|
|
94
|
+
min_stable_frames: int,
|
|
95
|
+
stability_threshold: float = 0.01,
|
|
96
|
+
debug: bool = False,
|
|
97
|
+
) -> tuple[int, float]:
|
|
98
|
+
"""Find first stable period and return baseline position.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Tuple of (baseline_start_frame, baseline_position). Returns (-1, 0.0) if not found.
|
|
102
|
+
"""
|
|
103
|
+
stable_window = min_stable_frames
|
|
104
|
+
|
|
105
|
+
for start_idx in range(0, len(positions) - stable_window, 5):
|
|
106
|
+
window = positions[start_idx : start_idx + stable_window]
|
|
107
|
+
window_std = float(np.std(window))
|
|
108
|
+
|
|
109
|
+
if window_std < stability_threshold:
|
|
110
|
+
baseline_start = start_idx
|
|
111
|
+
baseline_position = float(np.median(window))
|
|
112
|
+
|
|
113
|
+
if debug:
|
|
114
|
+
end_frame = baseline_start + stable_window - 1
|
|
115
|
+
print("[detect_drop_start] Found stable period:")
|
|
116
|
+
print(f" frames {baseline_start}-{end_frame}")
|
|
117
|
+
print(f" baseline_position: {baseline_position:.4f}")
|
|
118
|
+
print(f" baseline_std: {window_std:.4f} < {stability_threshold:.4f}")
|
|
119
|
+
|
|
120
|
+
return baseline_start, baseline_position
|
|
121
|
+
|
|
122
|
+
if debug:
|
|
123
|
+
print(
|
|
124
|
+
f"[detect_drop_start] No stable period found "
|
|
125
|
+
f"(variance always > {stability_threshold:.4f})"
|
|
126
|
+
)
|
|
127
|
+
return -1, 0.0
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _find_drop_from_baseline(
|
|
131
|
+
positions: np.ndarray,
|
|
132
|
+
baseline_start: int,
|
|
133
|
+
baseline_position: float,
|
|
134
|
+
stable_window: int,
|
|
135
|
+
position_change_threshold: float,
|
|
136
|
+
smoothing_window: int,
|
|
137
|
+
debug: bool = False,
|
|
138
|
+
) -> int:
|
|
139
|
+
"""Find drop start after stable baseline period.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Drop frame index, or 0 if not found.
|
|
143
|
+
"""
|
|
144
|
+
search_start = baseline_start + stable_window
|
|
145
|
+
window_size = max(3, smoothing_window)
|
|
146
|
+
|
|
147
|
+
for i in range(search_start, len(positions) - window_size):
|
|
148
|
+
window_positions = positions[i : i + window_size]
|
|
149
|
+
avg_position = float(np.mean(window_positions))
|
|
150
|
+
position_change = avg_position - baseline_position
|
|
151
|
+
|
|
152
|
+
if position_change > position_change_threshold:
|
|
153
|
+
drop_frame = max(baseline_start, i - window_size)
|
|
154
|
+
|
|
155
|
+
if debug:
|
|
156
|
+
print(f"[detect_drop_start] Drop detected at frame {drop_frame}")
|
|
157
|
+
print(
|
|
158
|
+
f" position_change: {position_change:.4f} > "
|
|
159
|
+
f"{position_change_threshold:.4f}"
|
|
160
|
+
)
|
|
161
|
+
print(
|
|
162
|
+
f" avg_position: {avg_position:.4f} vs "
|
|
163
|
+
f"baseline: {baseline_position:.4f}"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
return drop_frame
|
|
167
|
+
|
|
168
|
+
if debug:
|
|
169
|
+
print("[detect_drop_start] No drop detected after stable period")
|
|
170
|
+
return 0
|
|
171
|
+
|
|
172
|
+
|
|
92
173
|
def detect_drop_start(
|
|
93
174
|
positions: np.ndarray,
|
|
94
175
|
fps: float,
|
|
@@ -126,84 +207,32 @@ def detect_drop_start(
|
|
|
126
207
|
- Returns: 119
|
|
127
208
|
"""
|
|
128
209
|
min_stable_frames = int(fps * min_stationary_duration)
|
|
129
|
-
if len(positions) < min_stable_frames + 30:
|
|
210
|
+
if len(positions) < min_stable_frames + 30:
|
|
130
211
|
if debug:
|
|
131
|
-
min_frames_needed = min_stable_frames + 30
|
|
132
212
|
print(
|
|
133
|
-
f"[detect_drop_start] Video too short: {len(positions)} <
|
|
213
|
+
f"[detect_drop_start] Video too short: {len(positions)} < "
|
|
214
|
+
f"{min_stable_frames + 30}"
|
|
134
215
|
)
|
|
135
216
|
return 0
|
|
136
217
|
|
|
137
|
-
#
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
baseline_start = -1
|
|
143
|
-
baseline_position = 0.0
|
|
144
|
-
|
|
145
|
-
# Scan from start, looking for stable window
|
|
146
|
-
for start_idx in range(0, len(positions) - stable_window, 5): # Step by 5 frames
|
|
147
|
-
window = positions[start_idx : start_idx + stable_window]
|
|
148
|
-
window_std = float(np.std(window))
|
|
149
|
-
|
|
150
|
-
if window_std < stability_threshold:
|
|
151
|
-
# Found stable period!
|
|
152
|
-
baseline_start = start_idx
|
|
153
|
-
baseline_position = float(np.median(window))
|
|
154
|
-
|
|
155
|
-
if debug:
|
|
156
|
-
end_frame = baseline_start + stable_window - 1
|
|
157
|
-
print("[detect_drop_start] Found stable period:")
|
|
158
|
-
print(f" frames {baseline_start}-{end_frame}")
|
|
159
|
-
print(f" baseline_position: {baseline_position:.4f}")
|
|
160
|
-
print(f" baseline_std: {window_std:.4f} < {stability_threshold:.4f}")
|
|
161
|
-
break
|
|
218
|
+
# Find stable baseline period
|
|
219
|
+
baseline_start, baseline_position = _find_stable_baseline(
|
|
220
|
+
positions, min_stable_frames, debug=debug
|
|
221
|
+
)
|
|
162
222
|
|
|
163
223
|
if baseline_start < 0:
|
|
164
|
-
if debug:
|
|
165
|
-
msg = (
|
|
166
|
-
f"No stable period found (variance always > {stability_threshold:.4f})"
|
|
167
|
-
)
|
|
168
|
-
print(f"[detect_drop_start] {msg}")
|
|
169
224
|
return 0
|
|
170
225
|
|
|
171
|
-
#
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
# Check if position has increased (dropped) significantly
|
|
182
|
-
position_change = avg_position - baseline_position
|
|
183
|
-
|
|
184
|
-
if position_change > position_change_threshold:
|
|
185
|
-
# Found start of drop - back up slightly to catch beginning
|
|
186
|
-
drop_frame_candidate = i - window_size
|
|
187
|
-
if drop_frame_candidate < baseline_start:
|
|
188
|
-
drop_frame = baseline_start
|
|
189
|
-
else:
|
|
190
|
-
drop_frame = drop_frame_candidate
|
|
191
|
-
|
|
192
|
-
if debug:
|
|
193
|
-
print(f"[detect_drop_start] Drop detected at frame {drop_frame}")
|
|
194
|
-
print(
|
|
195
|
-
f" position_change: {position_change:.4f} > {position_change_threshold:.4f}"
|
|
196
|
-
)
|
|
197
|
-
print(
|
|
198
|
-
f" avg_position: {avg_position:.4f} vs baseline: {baseline_position:.4f}"
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
return drop_frame
|
|
202
|
-
|
|
203
|
-
# No significant position change detected
|
|
204
|
-
if debug:
|
|
205
|
-
print("[detect_drop_start] No drop detected after stable period")
|
|
206
|
-
return 0
|
|
226
|
+
# Find drop from baseline
|
|
227
|
+
return _find_drop_from_baseline(
|
|
228
|
+
positions,
|
|
229
|
+
baseline_start,
|
|
230
|
+
baseline_position,
|
|
231
|
+
min_stable_frames,
|
|
232
|
+
position_change_threshold,
|
|
233
|
+
smoothing_window,
|
|
234
|
+
debug,
|
|
235
|
+
)
|
|
207
236
|
|
|
208
237
|
|
|
209
238
|
def detect_ground_contact(
|
|
@@ -349,6 +378,71 @@ def interpolate_threshold_crossing(
|
|
|
349
378
|
return float(max(0.0, min(1.0, t)))
|
|
350
379
|
|
|
351
380
|
|
|
381
|
+
def _interpolate_phase_start(
|
|
382
|
+
start_idx: int,
|
|
383
|
+
state: ContactState,
|
|
384
|
+
velocities: np.ndarray,
|
|
385
|
+
velocity_threshold: float,
|
|
386
|
+
) -> float:
|
|
387
|
+
"""Interpolate start boundary of a phase with sub-frame precision.
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
Fractional start frame, or float(start_idx) if no interpolation.
|
|
391
|
+
"""
|
|
392
|
+
if start_idx <= 0 or start_idx >= len(velocities):
|
|
393
|
+
return float(start_idx)
|
|
394
|
+
|
|
395
|
+
vel_before = velocities[start_idx - 1]
|
|
396
|
+
vel_at = velocities[start_idx]
|
|
397
|
+
|
|
398
|
+
# Check threshold crossing based on state
|
|
399
|
+
is_landing = (
|
|
400
|
+
state == ContactState.ON_GROUND and vel_before > velocity_threshold > vel_at
|
|
401
|
+
)
|
|
402
|
+
is_takeoff = (
|
|
403
|
+
state == ContactState.IN_AIR and vel_before < velocity_threshold < vel_at
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
if is_landing or is_takeoff:
|
|
407
|
+
offset = interpolate_threshold_crossing(vel_before, vel_at, velocity_threshold)
|
|
408
|
+
return (start_idx - 1) + offset
|
|
409
|
+
|
|
410
|
+
return float(start_idx)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _interpolate_phase_end(
|
|
414
|
+
end_idx: int,
|
|
415
|
+
state: ContactState,
|
|
416
|
+
velocities: np.ndarray,
|
|
417
|
+
velocity_threshold: float,
|
|
418
|
+
max_idx: int,
|
|
419
|
+
) -> float:
|
|
420
|
+
"""Interpolate end boundary of a phase with sub-frame precision.
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
Fractional end frame, or float(end_idx) if no interpolation.
|
|
424
|
+
"""
|
|
425
|
+
if end_idx >= max_idx - 1 or end_idx + 1 >= len(velocities):
|
|
426
|
+
return float(end_idx)
|
|
427
|
+
|
|
428
|
+
vel_at = velocities[end_idx]
|
|
429
|
+
vel_after = velocities[end_idx + 1]
|
|
430
|
+
|
|
431
|
+
# Check threshold crossing based on state
|
|
432
|
+
is_takeoff = (
|
|
433
|
+
state == ContactState.ON_GROUND and vel_at < velocity_threshold < vel_after
|
|
434
|
+
)
|
|
435
|
+
is_landing = (
|
|
436
|
+
state == ContactState.IN_AIR and vel_at > velocity_threshold > vel_after
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
if is_takeoff or is_landing:
|
|
440
|
+
offset = interpolate_threshold_crossing(vel_at, vel_after, velocity_threshold)
|
|
441
|
+
return end_idx + offset
|
|
442
|
+
|
|
443
|
+
return float(end_idx)
|
|
444
|
+
|
|
445
|
+
|
|
352
446
|
def find_interpolated_phase_transitions(
|
|
353
447
|
foot_positions: np.ndarray,
|
|
354
448
|
contact_states: list[ContactState],
|
|
@@ -371,13 +465,10 @@ def find_interpolated_phase_transitions(
|
|
|
371
465
|
Returns:
|
|
372
466
|
List of (start_frame, end_frame, state) tuples with fractional frame indices
|
|
373
467
|
"""
|
|
374
|
-
# First get integer frame phases
|
|
375
468
|
phases = find_contact_phases(contact_states)
|
|
376
469
|
if not phases or len(foot_positions) < 2:
|
|
377
470
|
return []
|
|
378
471
|
|
|
379
|
-
# Compute velocities from derivative of smoothed trajectory
|
|
380
|
-
# This gives much smoother velocity estimates than simple frame differences
|
|
381
472
|
velocities = compute_velocity_from_derivative(
|
|
382
473
|
foot_positions, window_length=smoothing_window, polyorder=2
|
|
383
474
|
)
|
|
@@ -385,57 +476,12 @@ def find_interpolated_phase_transitions(
|
|
|
385
476
|
interpolated_phases: list[tuple[float, float, ContactState]] = []
|
|
386
477
|
|
|
387
478
|
for start_idx, end_idx, state in phases:
|
|
388
|
-
start_frac =
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
velocities[start_idx - 1] if start_idx > 0 else velocities[start_idx]
|
|
395
|
-
)
|
|
396
|
-
vel_at = velocities[start_idx]
|
|
397
|
-
|
|
398
|
-
# Check if we're crossing the threshold at this boundary
|
|
399
|
-
if state == ContactState.ON_GROUND:
|
|
400
|
-
# Transition air→ground: velocity dropping below threshold
|
|
401
|
-
if vel_before > velocity_threshold > vel_at:
|
|
402
|
-
# Interpolate between start_idx-1 and start_idx
|
|
403
|
-
offset = interpolate_threshold_crossing(
|
|
404
|
-
vel_before, vel_at, velocity_threshold
|
|
405
|
-
)
|
|
406
|
-
start_frac = (start_idx - 1) + offset
|
|
407
|
-
elif state == ContactState.IN_AIR:
|
|
408
|
-
# Transition ground→air: velocity rising above threshold
|
|
409
|
-
if vel_before < velocity_threshold < vel_at:
|
|
410
|
-
# Interpolate between start_idx-1 and start_idx
|
|
411
|
-
offset = interpolate_threshold_crossing(
|
|
412
|
-
vel_before, vel_at, velocity_threshold
|
|
413
|
-
)
|
|
414
|
-
start_frac = (start_idx - 1) + offset
|
|
415
|
-
|
|
416
|
-
# Interpolate end boundary (transition OUT OF this phase)
|
|
417
|
-
if end_idx < len(foot_positions) - 1 and end_idx + 1 < len(velocities):
|
|
418
|
-
vel_at = velocities[end_idx]
|
|
419
|
-
vel_after = velocities[end_idx + 1]
|
|
420
|
-
|
|
421
|
-
# Check if we're crossing the threshold at this boundary
|
|
422
|
-
if state == ContactState.ON_GROUND:
|
|
423
|
-
# Transition ground→air: velocity rising above threshold
|
|
424
|
-
if vel_at < velocity_threshold < vel_after:
|
|
425
|
-
# Interpolate between end_idx and end_idx+1
|
|
426
|
-
offset = interpolate_threshold_crossing(
|
|
427
|
-
vel_at, vel_after, velocity_threshold
|
|
428
|
-
)
|
|
429
|
-
end_frac = end_idx + offset
|
|
430
|
-
elif state == ContactState.IN_AIR:
|
|
431
|
-
# Transition air→ground: velocity dropping below threshold
|
|
432
|
-
if vel_at > velocity_threshold > vel_after:
|
|
433
|
-
# Interpolate between end_idx and end_idx+1
|
|
434
|
-
offset = interpolate_threshold_crossing(
|
|
435
|
-
vel_at, vel_after, velocity_threshold
|
|
436
|
-
)
|
|
437
|
-
end_frac = end_idx + offset
|
|
438
|
-
|
|
479
|
+
start_frac = _interpolate_phase_start(
|
|
480
|
+
start_idx, state, velocities, velocity_threshold
|
|
481
|
+
)
|
|
482
|
+
end_frac = _interpolate_phase_end(
|
|
483
|
+
end_idx, state, velocities, velocity_threshold, len(foot_positions)
|
|
484
|
+
)
|
|
439
485
|
interpolated_phases.append((start_frac, end_frac, state))
|
|
440
486
|
|
|
441
487
|
return interpolated_phases
|
|
@@ -706,3 +752,39 @@ def compute_average_foot_position(
|
|
|
706
752
|
return (0.5, 0.5) # Default to center if no visible feet
|
|
707
753
|
|
|
708
754
|
return (float(np.mean(x_positions)), float(np.mean(y_positions)))
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def extract_foot_positions_and_visibilities(
|
|
758
|
+
smoothed_landmarks: list[dict[str, tuple[float, float, float]] | None],
|
|
759
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
760
|
+
"""
|
|
761
|
+
Extract vertical positions and average visibilities from smoothed landmarks.
|
|
762
|
+
|
|
763
|
+
This utility function eliminates code duplication between CLI and programmatic usage.
|
|
764
|
+
|
|
765
|
+
Args:
|
|
766
|
+
smoothed_landmarks: Smoothed landmark sequence from tracking
|
|
767
|
+
|
|
768
|
+
Returns:
|
|
769
|
+
Tuple of (vertical_positions, visibilities) as numpy arrays
|
|
770
|
+
"""
|
|
771
|
+
position_list: list[float] = []
|
|
772
|
+
visibilities_list: list[float] = []
|
|
773
|
+
|
|
774
|
+
for frame_landmarks in smoothed_landmarks:
|
|
775
|
+
if frame_landmarks:
|
|
776
|
+
_, foot_y = compute_average_foot_position(frame_landmarks)
|
|
777
|
+
position_list.append(foot_y)
|
|
778
|
+
|
|
779
|
+
# Average visibility of foot landmarks
|
|
780
|
+
foot_vis = []
|
|
781
|
+
for key in ["left_ankle", "right_ankle", "left_heel", "right_heel"]:
|
|
782
|
+
if key in frame_landmarks:
|
|
783
|
+
foot_vis.append(frame_landmarks[key][2])
|
|
784
|
+
visibilities_list.append(float(np.mean(foot_vis)) if foot_vis else 0.0)
|
|
785
|
+
else:
|
|
786
|
+
# Fill missing frames with last known position or default
|
|
787
|
+
position_list.append(position_list[-1] if position_list else 0.5)
|
|
788
|
+
visibilities_list.append(0.0)
|
|
789
|
+
|
|
790
|
+
return np.array(position_list), np.array(visibilities_list)
|
kinemotion/dropjump/cli.py
CHANGED
|
@@ -28,8 +28,8 @@ from ..core.pose import PoseTracker
|
|
|
28
28
|
from ..core.video_io import VideoProcessor
|
|
29
29
|
from .analysis import (
|
|
30
30
|
ContactState,
|
|
31
|
-
compute_average_foot_position,
|
|
32
31
|
detect_ground_contact,
|
|
32
|
+
extract_foot_positions_and_visibilities,
|
|
33
33
|
)
|
|
34
34
|
from .debug_overlay import DebugOverlayRenderer
|
|
35
35
|
from .kinematics import DropJumpMetrics, calculate_drop_jump_metrics
|
|
@@ -258,26 +258,7 @@ def _extract_positions_and_visibilities(
|
|
|
258
258
|
Tuple of (vertical_positions, visibilities)
|
|
259
259
|
"""
|
|
260
260
|
click.echo("Extracting foot positions...", err=True)
|
|
261
|
-
|
|
262
|
-
position_list: list[float] = []
|
|
263
|
-
visibilities_list: list[float] = []
|
|
264
|
-
|
|
265
|
-
for frame_landmarks in smoothed_landmarks:
|
|
266
|
-
if frame_landmarks:
|
|
267
|
-
_, foot_y = compute_average_foot_position(frame_landmarks)
|
|
268
|
-
position_list.append(foot_y)
|
|
269
|
-
|
|
270
|
-
# Average visibility of foot landmarks
|
|
271
|
-
foot_vis = []
|
|
272
|
-
for key in ["left_ankle", "right_ankle", "left_heel", "right_heel"]:
|
|
273
|
-
if key in frame_landmarks:
|
|
274
|
-
foot_vis.append(frame_landmarks[key][2])
|
|
275
|
-
visibilities_list.append(float(np.mean(foot_vis)) if foot_vis else 0.0)
|
|
276
|
-
else:
|
|
277
|
-
position_list.append(position_list[-1] if position_list else 0.5)
|
|
278
|
-
visibilities_list.append(0.0)
|
|
279
|
-
|
|
280
|
-
return np.array(position_list), np.array(visibilities_list)
|
|
261
|
+
return extract_foot_positions_and_visibilities(smoothed_landmarks)
|
|
281
262
|
|
|
282
263
|
|
|
283
264
|
def _create_debug_video(
|
|
@@ -413,13 +394,11 @@ def _process_single(
|
|
|
413
394
|
contact_states,
|
|
414
395
|
vertical_positions,
|
|
415
396
|
video.fps,
|
|
416
|
-
drop_height_m=None,
|
|
417
397
|
drop_start_frame=expert_params.drop_start_frame,
|
|
418
398
|
velocity_threshold=params.velocity_threshold,
|
|
419
399
|
smoothing_window=params.smoothing_window,
|
|
420
400
|
polyorder=params.polyorder,
|
|
421
401
|
use_curvature=params.use_curvature,
|
|
422
|
-
kinematic_correction_factor=1.0,
|
|
423
402
|
)
|
|
424
403
|
|
|
425
404
|
# Output metrics
|
|
@@ -569,6 +548,63 @@ def _compute_batch_statistics(results: list[VideoResult]) -> None:
|
|
|
569
548
|
)
|
|
570
549
|
|
|
571
550
|
|
|
551
|
+
def _format_time_metric(value: float | None, multiplier: float = 1000.0) -> str:
|
|
552
|
+
"""Format time metric for CSV output.
|
|
553
|
+
|
|
554
|
+
Args:
|
|
555
|
+
value: Time value in seconds
|
|
556
|
+
multiplier: Multiplier to convert to milliseconds (default: 1000.0)
|
|
557
|
+
|
|
558
|
+
Returns:
|
|
559
|
+
Formatted string or "N/A" if value is None
|
|
560
|
+
"""
|
|
561
|
+
return f"{value * multiplier:.1f}" if value is not None else "N/A"
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def _format_distance_metric(value: float | None) -> str:
|
|
565
|
+
"""Format distance metric for CSV output.
|
|
566
|
+
|
|
567
|
+
Args:
|
|
568
|
+
value: Distance value in meters
|
|
569
|
+
|
|
570
|
+
Returns:
|
|
571
|
+
Formatted string or "N/A" if value is None
|
|
572
|
+
"""
|
|
573
|
+
return f"{value:.3f}" if value is not None else "N/A"
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _create_csv_row_from_result(result: VideoResult) -> list[str]:
|
|
577
|
+
"""Create CSV row from video processing result.
|
|
578
|
+
|
|
579
|
+
Args:
|
|
580
|
+
result: Video processing result
|
|
581
|
+
|
|
582
|
+
Returns:
|
|
583
|
+
List of formatted values for CSV row
|
|
584
|
+
"""
|
|
585
|
+
video_name = Path(result.video_path).name
|
|
586
|
+
processing_time = f"{result.processing_time:.2f}"
|
|
587
|
+
|
|
588
|
+
if result.success and result.metrics:
|
|
589
|
+
return [
|
|
590
|
+
video_name,
|
|
591
|
+
_format_time_metric(result.metrics.ground_contact_time),
|
|
592
|
+
_format_time_metric(result.metrics.flight_time),
|
|
593
|
+
_format_distance_metric(result.metrics.jump_height),
|
|
594
|
+
processing_time,
|
|
595
|
+
"Success",
|
|
596
|
+
]
|
|
597
|
+
else:
|
|
598
|
+
return [
|
|
599
|
+
video_name,
|
|
600
|
+
"N/A",
|
|
601
|
+
"N/A",
|
|
602
|
+
"N/A",
|
|
603
|
+
processing_time,
|
|
604
|
+
f"Failed: {result.error}",
|
|
605
|
+
]
|
|
606
|
+
|
|
607
|
+
|
|
572
608
|
def _write_csv_summary(
|
|
573
609
|
csv_summary: str | None, results: list[VideoResult], successful: list[VideoResult]
|
|
574
610
|
) -> None:
|
|
@@ -602,40 +638,7 @@ def _write_csv_summary(
|
|
|
602
638
|
|
|
603
639
|
# Data rows
|
|
604
640
|
for result in results:
|
|
605
|
-
|
|
606
|
-
writer.writerow(
|
|
607
|
-
[
|
|
608
|
-
Path(result.video_path).name,
|
|
609
|
-
(
|
|
610
|
-
f"{result.metrics.ground_contact_time * 1000:.1f}"
|
|
611
|
-
if result.metrics.ground_contact_time
|
|
612
|
-
else "N/A"
|
|
613
|
-
),
|
|
614
|
-
(
|
|
615
|
-
f"{result.metrics.flight_time * 1000:.1f}"
|
|
616
|
-
if result.metrics.flight_time
|
|
617
|
-
else "N/A"
|
|
618
|
-
),
|
|
619
|
-
(
|
|
620
|
-
f"{result.metrics.jump_height:.3f}"
|
|
621
|
-
if result.metrics.jump_height
|
|
622
|
-
else "N/A"
|
|
623
|
-
),
|
|
624
|
-
f"{result.processing_time:.2f}",
|
|
625
|
-
"Success",
|
|
626
|
-
]
|
|
627
|
-
)
|
|
628
|
-
else:
|
|
629
|
-
writer.writerow(
|
|
630
|
-
[
|
|
631
|
-
Path(result.video_path).name,
|
|
632
|
-
"N/A",
|
|
633
|
-
"N/A",
|
|
634
|
-
"N/A",
|
|
635
|
-
f"{result.processing_time:.2f}",
|
|
636
|
-
f"Failed: {result.error}",
|
|
637
|
-
]
|
|
638
|
-
)
|
|
641
|
+
writer.writerow(_create_csv_row_from_result(result))
|
|
639
642
|
|
|
640
643
|
click.echo("CSV summary written successfully", err=True)
|
|
641
644
|
|