kinemotion 0.76.3__py3-none-any.whl → 1.0.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 +3 -18
- kinemotion/api.py +7 -27
- kinemotion/cli.py +2 -4
- kinemotion/{countermovement_jump → cmj}/analysis.py +158 -16
- kinemotion/{countermovement_jump → cmj}/api.py +18 -46
- kinemotion/{countermovement_jump → cmj}/cli.py +46 -6
- kinemotion/cmj/debug_overlay.py +457 -0
- kinemotion/{countermovement_jump → cmj}/joint_angles.py +31 -96
- kinemotion/{countermovement_jump → cmj}/metrics_validator.py +293 -184
- kinemotion/{countermovement_jump → cmj}/validation_bounds.py +18 -1
- kinemotion/core/__init__.py +2 -11
- kinemotion/core/auto_tuning.py +107 -149
- kinemotion/core/cli_utils.py +0 -74
- kinemotion/core/debug_overlay_utils.py +15 -142
- kinemotion/core/experimental.py +51 -55
- kinemotion/core/filtering.py +56 -116
- kinemotion/core/pipeline_utils.py +2 -2
- kinemotion/core/pose.py +98 -47
- kinemotion/core/quality.py +6 -4
- kinemotion/core/smoothing.py +51 -65
- kinemotion/core/types.py +0 -15
- kinemotion/core/validation.py +7 -76
- kinemotion/core/video_io.py +27 -41
- kinemotion/{drop_jump → dropjump}/__init__.py +8 -2
- kinemotion/{drop_jump → dropjump}/analysis.py +120 -282
- kinemotion/{drop_jump → dropjump}/api.py +33 -59
- kinemotion/{drop_jump → dropjump}/cli.py +136 -70
- kinemotion/dropjump/debug_overlay.py +182 -0
- kinemotion/{drop_jump → dropjump}/kinematics.py +65 -175
- kinemotion/{drop_jump → dropjump}/metrics_validator.py +51 -25
- kinemotion/{drop_jump → dropjump}/validation_bounds.py +1 -1
- kinemotion/models/rtmpose-s_simcc-body7_pt-body7-halpe26_700e-256x192-7f134165_20230605.onnx +3 -0
- kinemotion/models/yolox_tiny_8xb8-300e_humanart-6f3252f9.onnx +3 -0
- {kinemotion-0.76.3.dist-info → kinemotion-1.0.0.dist-info}/METADATA +26 -75
- kinemotion-1.0.0.dist-info/RECORD +49 -0
- kinemotion/core/overlay_constants.py +0 -61
- kinemotion/core/video_analysis_base.py +0 -132
- kinemotion/countermovement_jump/debug_overlay.py +0 -325
- kinemotion/drop_jump/debug_overlay.py +0 -241
- kinemotion/squat_jump/__init__.py +0 -5
- kinemotion/squat_jump/analysis.py +0 -377
- kinemotion/squat_jump/api.py +0 -610
- kinemotion/squat_jump/cli.py +0 -309
- kinemotion/squat_jump/debug_overlay.py +0 -163
- kinemotion/squat_jump/kinematics.py +0 -342
- kinemotion/squat_jump/metrics_validator.py +0 -438
- kinemotion/squat_jump/validation_bounds.py +0 -221
- kinemotion-0.76.3.dist-info/RECORD +0 -57
- /kinemotion/{countermovement_jump → cmj}/__init__.py +0 -0
- /kinemotion/{countermovement_jump → cmj}/kinematics.py +0 -0
- {kinemotion-0.76.3.dist-info → kinemotion-1.0.0.dist-info}/WHEEL +0 -0
- {kinemotion-0.76.3.dist-info → kinemotion-1.0.0.dist-info}/entry_points.txt +0 -0
- {kinemotion-0.76.3.dist-info → kinemotion-1.0.0.dist-info}/licenses/LICENSE +0 -0
kinemotion/squat_jump/api.py
DELETED
|
@@ -1,610 +0,0 @@
|
|
|
1
|
-
"""Public API for SJ (Squat Jump) video analysis."""
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import time
|
|
5
|
-
from collections.abc import Callable
|
|
6
|
-
from dataclasses import dataclass
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
|
|
9
|
-
import numpy as np
|
|
10
|
-
from numpy.typing import NDArray
|
|
11
|
-
|
|
12
|
-
from ..core.auto_tuning import (
|
|
13
|
-
AnalysisParameters,
|
|
14
|
-
QualityPreset,
|
|
15
|
-
analyze_video_sample,
|
|
16
|
-
auto_tune_parameters,
|
|
17
|
-
)
|
|
18
|
-
from ..core.experimental import experimental
|
|
19
|
-
from ..core.filtering import reject_outliers
|
|
20
|
-
from ..core.metadata import (
|
|
21
|
-
AlgorithmConfig,
|
|
22
|
-
DetectionConfig,
|
|
23
|
-
ProcessingInfo,
|
|
24
|
-
ResultMetadata,
|
|
25
|
-
SmoothingConfig,
|
|
26
|
-
VideoInfo,
|
|
27
|
-
create_timestamp,
|
|
28
|
-
get_kinemotion_version,
|
|
29
|
-
)
|
|
30
|
-
from ..core.pipeline_utils import (
|
|
31
|
-
apply_expert_overrides,
|
|
32
|
-
apply_smoothing,
|
|
33
|
-
convert_timer_to_stage_names,
|
|
34
|
-
determine_confidence_levels,
|
|
35
|
-
extract_vertical_positions,
|
|
36
|
-
parse_quality_preset,
|
|
37
|
-
print_verbose_parameters,
|
|
38
|
-
process_all_frames,
|
|
39
|
-
process_videos_bulk_generic,
|
|
40
|
-
)
|
|
41
|
-
from ..core.pose import MediaPipePoseTracker
|
|
42
|
-
from ..core.quality import QualityAssessment, assess_jump_quality
|
|
43
|
-
from ..core.timing import PerformanceTimer, Timer
|
|
44
|
-
from ..core.validation import ValidationResult
|
|
45
|
-
from ..core.video_io import VideoProcessor
|
|
46
|
-
from .analysis import detect_sj_phases
|
|
47
|
-
from .debug_overlay import SquatJumpDebugOverlayRenderer
|
|
48
|
-
from .kinematics import SJMetrics, calculate_sj_metrics
|
|
49
|
-
from .metrics_validator import SJMetricsValidator
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
@dataclass
|
|
53
|
-
class AnalysisOverrides:
|
|
54
|
-
"""Optional overrides for analysis parameters.
|
|
55
|
-
|
|
56
|
-
Allows fine-tuning of specific analysis parameters beyond quality presets.
|
|
57
|
-
If None, values will be determined by the quality preset.
|
|
58
|
-
"""
|
|
59
|
-
|
|
60
|
-
smoothing_window: int | None = None
|
|
61
|
-
velocity_threshold: float | None = None
|
|
62
|
-
min_contact_frames: int | None = None
|
|
63
|
-
visibility_threshold: float | None = None
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def _generate_debug_video(
|
|
67
|
-
output_video: str,
|
|
68
|
-
frames: list[NDArray[np.uint8]],
|
|
69
|
-
frame_indices: list[int],
|
|
70
|
-
smoothed_landmarks: list,
|
|
71
|
-
metrics: SJMetrics,
|
|
72
|
-
video_fps: float,
|
|
73
|
-
timer: Timer,
|
|
74
|
-
verbose: bool,
|
|
75
|
-
) -> None:
|
|
76
|
-
"""Generate debug video with SJ analysis overlay."""
|
|
77
|
-
if verbose:
|
|
78
|
-
print(f"Generating debug video: {output_video}")
|
|
79
|
-
|
|
80
|
-
debug_h, debug_w = frames[0].shape[:2]
|
|
81
|
-
step = max(1, int(video_fps / 30.0))
|
|
82
|
-
debug_fps = video_fps / step
|
|
83
|
-
|
|
84
|
-
with timer.measure("debug_video_generation"):
|
|
85
|
-
with SquatJumpDebugOverlayRenderer(
|
|
86
|
-
output_video,
|
|
87
|
-
debug_w,
|
|
88
|
-
debug_h,
|
|
89
|
-
debug_w,
|
|
90
|
-
debug_h,
|
|
91
|
-
debug_fps,
|
|
92
|
-
timer=timer,
|
|
93
|
-
) as renderer:
|
|
94
|
-
for frame, idx in zip(frames, frame_indices, strict=True):
|
|
95
|
-
annotated = renderer.render_frame(frame, smoothed_landmarks[idx], idx, metrics)
|
|
96
|
-
renderer.write_frame(annotated)
|
|
97
|
-
|
|
98
|
-
if verbose:
|
|
99
|
-
print(f"Debug video saved: {output_video}")
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def _save_metrics_to_json(
|
|
103
|
-
metrics: SJMetrics, json_output: str, timer: Timer, verbose: bool
|
|
104
|
-
) -> None:
|
|
105
|
-
"""Save metrics to JSON file."""
|
|
106
|
-
with timer.measure("json_serialization"):
|
|
107
|
-
output_path = Path(json_output)
|
|
108
|
-
metrics_dict = metrics.to_dict()
|
|
109
|
-
json_str = json.dumps(metrics_dict, indent=2)
|
|
110
|
-
output_path.write_text(json_str)
|
|
111
|
-
|
|
112
|
-
if verbose:
|
|
113
|
-
print(f"Metrics written to: {json_output}")
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def _print_timing_summary(start_time: float, timer: Timer, metrics: SJMetrics) -> None:
|
|
117
|
-
"""Print verbose timing summary and metrics."""
|
|
118
|
-
total_time = time.perf_counter() - start_time
|
|
119
|
-
stage_times = convert_timer_to_stage_names(timer.get_metrics())
|
|
120
|
-
|
|
121
|
-
print("\n=== Timing Summary ===")
|
|
122
|
-
for stage, duration in stage_times.items():
|
|
123
|
-
percentage = (duration / total_time) * 100
|
|
124
|
-
dur_ms = duration * 1000
|
|
125
|
-
print(f"{stage:.<40} {dur_ms:>6.0f}ms ({percentage:>5.1f}%)")
|
|
126
|
-
total_ms = total_time * 1000
|
|
127
|
-
print(f"{'Total':.<40} {total_ms:>6.0f}ms (100.0%)")
|
|
128
|
-
print()
|
|
129
|
-
|
|
130
|
-
print(f"\nJump height: {metrics.jump_height:.3f}m")
|
|
131
|
-
print(f"Flight time: {metrics.flight_time * 1000:.1f}ms")
|
|
132
|
-
print(f"Squat hold duration: {metrics.squat_hold_duration * 1000:.1f}ms")
|
|
133
|
-
print(f"Concentric duration: {metrics.concentric_duration * 1000:.1f}ms")
|
|
134
|
-
if metrics.peak_power is not None:
|
|
135
|
-
print(f"Peak power: {metrics.peak_power:.0f}W")
|
|
136
|
-
if metrics.peak_force is not None:
|
|
137
|
-
print(f"Peak force: {metrics.peak_force:.0f}N")
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
def _print_quality_warnings(quality_result: QualityAssessment, verbose: bool) -> None:
|
|
141
|
-
"""Print quality warnings if present."""
|
|
142
|
-
if verbose and quality_result.warnings:
|
|
143
|
-
print("\n⚠️ Quality Warnings:")
|
|
144
|
-
for warning in quality_result.warnings:
|
|
145
|
-
print(f" - {warning}")
|
|
146
|
-
print()
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
def _print_validation_results(validation_result: ValidationResult, verbose: bool) -> None:
|
|
150
|
-
"""Print validation issues if present."""
|
|
151
|
-
if verbose and validation_result.issues:
|
|
152
|
-
print("\n⚠️ Validation Results:")
|
|
153
|
-
for issue in validation_result.issues:
|
|
154
|
-
print(f" [{issue.severity.value}] {issue.metric}: {issue.message}")
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
def _create_algorithm_config(params: AnalysisParameters) -> AlgorithmConfig:
|
|
158
|
-
"""Create algorithm configuration from parameters."""
|
|
159
|
-
return AlgorithmConfig(
|
|
160
|
-
detection_method="static_squat",
|
|
161
|
-
tracking_method="mediapipe_pose",
|
|
162
|
-
model_complexity=1,
|
|
163
|
-
smoothing=SmoothingConfig(
|
|
164
|
-
window_size=params.smoothing_window,
|
|
165
|
-
polynomial_order=params.polyorder,
|
|
166
|
-
use_bilateral_filter=params.bilateral_filter,
|
|
167
|
-
use_outlier_rejection=params.outlier_rejection,
|
|
168
|
-
),
|
|
169
|
-
detection=DetectionConfig(
|
|
170
|
-
velocity_threshold=params.velocity_threshold,
|
|
171
|
-
min_contact_frames=params.min_contact_frames,
|
|
172
|
-
visibility_threshold=params.visibility_threshold,
|
|
173
|
-
use_curvature_refinement=params.use_curvature,
|
|
174
|
-
),
|
|
175
|
-
drop_detection=None,
|
|
176
|
-
)
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
def _create_video_info(video_path: str, video: VideoProcessor) -> VideoInfo:
|
|
180
|
-
"""Create video information metadata."""
|
|
181
|
-
return VideoInfo(
|
|
182
|
-
source_path=video_path,
|
|
183
|
-
fps=video.fps,
|
|
184
|
-
width=video.width,
|
|
185
|
-
height=video.height,
|
|
186
|
-
duration_s=video.frame_count / video.fps,
|
|
187
|
-
frame_count=video.frame_count,
|
|
188
|
-
codec=video.codec,
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
def _create_processing_info(
|
|
193
|
-
start_time: float, quality_preset: QualityPreset, timer: Timer
|
|
194
|
-
) -> ProcessingInfo:
|
|
195
|
-
"""Create processing information metadata."""
|
|
196
|
-
processing_time = time.perf_counter() - start_time
|
|
197
|
-
stage_times = convert_timer_to_stage_names(timer.get_metrics())
|
|
198
|
-
|
|
199
|
-
return ProcessingInfo(
|
|
200
|
-
version=get_kinemotion_version(),
|
|
201
|
-
timestamp=create_timestamp(),
|
|
202
|
-
quality_preset=quality_preset.value,
|
|
203
|
-
processing_time_s=processing_time,
|
|
204
|
-
timing_breakdown=stage_times,
|
|
205
|
-
)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
def _create_result_metadata(
|
|
209
|
-
quality_result: QualityAssessment,
|
|
210
|
-
video_info: VideoInfo,
|
|
211
|
-
processing_info: ProcessingInfo,
|
|
212
|
-
algorithm_config: AlgorithmConfig,
|
|
213
|
-
) -> ResultMetadata:
|
|
214
|
-
"""Create result metadata from components."""
|
|
215
|
-
return ResultMetadata(
|
|
216
|
-
quality=quality_result,
|
|
217
|
-
video=video_info,
|
|
218
|
-
processing=processing_info,
|
|
219
|
-
algorithm=algorithm_config,
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
def _run_pose_tracking(
|
|
224
|
-
video: VideoProcessor,
|
|
225
|
-
quality_preset: QualityPreset,
|
|
226
|
-
detection_confidence: float | None,
|
|
227
|
-
tracking_confidence: float | None,
|
|
228
|
-
pose_tracker: "MediaPipePoseTracker | None",
|
|
229
|
-
verbose: bool,
|
|
230
|
-
timer: Timer,
|
|
231
|
-
) -> tuple[list[NDArray[np.uint8]], list, list[int]]:
|
|
232
|
-
"""Initialize tracker and process all frames."""
|
|
233
|
-
if verbose:
|
|
234
|
-
print(
|
|
235
|
-
f"Video: {video.width}x{video.height} @ {video.fps:.2f} fps, "
|
|
236
|
-
f"{video.frame_count} frames"
|
|
237
|
-
)
|
|
238
|
-
|
|
239
|
-
det_conf, track_conf = determine_confidence_levels(
|
|
240
|
-
quality_preset, detection_confidence, tracking_confidence
|
|
241
|
-
)
|
|
242
|
-
|
|
243
|
-
if pose_tracker is None:
|
|
244
|
-
if verbose:
|
|
245
|
-
print("Processing all frames with MediaPipe pose tracking...")
|
|
246
|
-
tracker = MediaPipePoseTracker(
|
|
247
|
-
min_detection_confidence=det_conf,
|
|
248
|
-
min_tracking_confidence=track_conf,
|
|
249
|
-
timer=timer,
|
|
250
|
-
)
|
|
251
|
-
should_close_tracker = True
|
|
252
|
-
else:
|
|
253
|
-
tracker = pose_tracker
|
|
254
|
-
should_close_tracker = False
|
|
255
|
-
|
|
256
|
-
return process_all_frames(video, tracker, verbose, timer, close_tracker=should_close_tracker)
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
def _get_tuned_parameters(
|
|
260
|
-
video: VideoProcessor,
|
|
261
|
-
landmarks_sequence: list,
|
|
262
|
-
quality_preset: QualityPreset,
|
|
263
|
-
overrides: AnalysisOverrides | None,
|
|
264
|
-
verbose: bool,
|
|
265
|
-
timer: Timer,
|
|
266
|
-
) -> AnalysisParameters:
|
|
267
|
-
"""Analyze sample and tune parameters with expert overrides."""
|
|
268
|
-
with timer.measure("parameter_auto_tuning"):
|
|
269
|
-
characteristics = analyze_video_sample(landmarks_sequence, video.fps, video.frame_count)
|
|
270
|
-
params = auto_tune_parameters(characteristics, quality_preset)
|
|
271
|
-
|
|
272
|
-
if overrides:
|
|
273
|
-
params = apply_expert_overrides(
|
|
274
|
-
params,
|
|
275
|
-
overrides.smoothing_window,
|
|
276
|
-
overrides.velocity_threshold,
|
|
277
|
-
overrides.min_contact_frames,
|
|
278
|
-
overrides.visibility_threshold,
|
|
279
|
-
)
|
|
280
|
-
|
|
281
|
-
if verbose:
|
|
282
|
-
print_verbose_parameters(video, characteristics, quality_preset, params)
|
|
283
|
-
|
|
284
|
-
return params
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
def _run_kinematic_analysis(
|
|
288
|
-
video: VideoProcessor,
|
|
289
|
-
smoothed_landmarks: list,
|
|
290
|
-
params: AnalysisParameters,
|
|
291
|
-
mass_kg: float | None,
|
|
292
|
-
verbose: bool,
|
|
293
|
-
timer: Timer,
|
|
294
|
-
) -> tuple[SJMetrics, NDArray[np.float64], NDArray[np.float64]]:
|
|
295
|
-
"""Extract positions, detect phases, and calculate metrics."""
|
|
296
|
-
if verbose:
|
|
297
|
-
print("Extracting vertical positions (Hip and Foot)...")
|
|
298
|
-
with timer.measure("vertical_position_extraction"):
|
|
299
|
-
vertical_positions, visibilities = extract_vertical_positions(
|
|
300
|
-
smoothed_landmarks, target="hip"
|
|
301
|
-
)
|
|
302
|
-
foot_positions, _ = extract_vertical_positions(smoothed_landmarks, target="foot")
|
|
303
|
-
|
|
304
|
-
if verbose:
|
|
305
|
-
print("Detecting SJ phases...")
|
|
306
|
-
with timer.measure("phase_detection"):
|
|
307
|
-
phases = detect_sj_phases(
|
|
308
|
-
vertical_positions,
|
|
309
|
-
video.fps,
|
|
310
|
-
window_length=params.smoothing_window,
|
|
311
|
-
polyorder=params.polyorder,
|
|
312
|
-
)
|
|
313
|
-
|
|
314
|
-
if phases is None:
|
|
315
|
-
raise ValueError("Could not detect SJ phases in video")
|
|
316
|
-
|
|
317
|
-
squat_hold_start, concentric_start, takeoff_frame, landing_frame = phases
|
|
318
|
-
|
|
319
|
-
if verbose:
|
|
320
|
-
print("Calculating metrics...")
|
|
321
|
-
with timer.measure("metrics_calculation"):
|
|
322
|
-
metrics = calculate_sj_metrics(
|
|
323
|
-
vertical_positions,
|
|
324
|
-
foot_positions,
|
|
325
|
-
squat_hold_start,
|
|
326
|
-
concentric_start,
|
|
327
|
-
takeoff_frame,
|
|
328
|
-
landing_frame,
|
|
329
|
-
video.fps,
|
|
330
|
-
mass_kg=mass_kg,
|
|
331
|
-
tracking_method="hip_hybrid",
|
|
332
|
-
)
|
|
333
|
-
|
|
334
|
-
return metrics, vertical_positions, visibilities
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
def _finalize_analysis_results(
|
|
338
|
-
metrics: SJMetrics,
|
|
339
|
-
video: VideoProcessor,
|
|
340
|
-
video_path: str,
|
|
341
|
-
vertical_positions: NDArray[np.float64],
|
|
342
|
-
visibilities: NDArray[np.float64],
|
|
343
|
-
params: AnalysisParameters,
|
|
344
|
-
quality_preset: QualityPreset,
|
|
345
|
-
start_time: float,
|
|
346
|
-
timer: Timer,
|
|
347
|
-
verbose: bool,
|
|
348
|
-
) -> None:
|
|
349
|
-
"""Assess quality, validate metrics, and attach metadata."""
|
|
350
|
-
if verbose:
|
|
351
|
-
print("Assessing tracking quality...")
|
|
352
|
-
with timer.measure("quality_assessment"):
|
|
353
|
-
_, outlier_mask = reject_outliers(
|
|
354
|
-
vertical_positions,
|
|
355
|
-
use_ransac=True,
|
|
356
|
-
use_median=True,
|
|
357
|
-
interpolate=False,
|
|
358
|
-
)
|
|
359
|
-
quality_result = assess_jump_quality(
|
|
360
|
-
visibilities=visibilities,
|
|
361
|
-
positions=vertical_positions,
|
|
362
|
-
outlier_mask=outlier_mask,
|
|
363
|
-
fps=video.fps,
|
|
364
|
-
phases_detected=True,
|
|
365
|
-
phase_count=3, # SQUAT_HOLD, CONCENTRIC, FLIGHT
|
|
366
|
-
)
|
|
367
|
-
|
|
368
|
-
_print_quality_warnings(quality_result, verbose)
|
|
369
|
-
|
|
370
|
-
with timer.measure("metrics_validation"):
|
|
371
|
-
validator = SJMetricsValidator()
|
|
372
|
-
validation_result = validator.validate(metrics.to_dict()) # type: ignore[arg-type]
|
|
373
|
-
|
|
374
|
-
algorithm_config = _create_algorithm_config(params)
|
|
375
|
-
video_info = _create_video_info(video_path, video)
|
|
376
|
-
processing_info = _create_processing_info(start_time, quality_preset, timer)
|
|
377
|
-
result_metadata = _create_result_metadata(
|
|
378
|
-
quality_result, video_info, processing_info, algorithm_config
|
|
379
|
-
)
|
|
380
|
-
metrics.result_metadata = result_metadata
|
|
381
|
-
|
|
382
|
-
_print_validation_results(validation_result, verbose)
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
@dataclass
|
|
386
|
-
class SJVideoConfig:
|
|
387
|
-
"""Configuration for processing a single SJ video."""
|
|
388
|
-
|
|
389
|
-
video_path: str
|
|
390
|
-
quality: str = "balanced"
|
|
391
|
-
output_video: str | None = None
|
|
392
|
-
json_output: str | None = None
|
|
393
|
-
overrides: AnalysisOverrides | None = None
|
|
394
|
-
detection_confidence: float | None = None
|
|
395
|
-
tracking_confidence: float | None = None
|
|
396
|
-
mass_kg: float | None = None
|
|
397
|
-
verbose: bool = False
|
|
398
|
-
timer: Timer | None = None
|
|
399
|
-
pose_tracker: "MediaPipePoseTracker | None" = None
|
|
400
|
-
|
|
401
|
-
def to_kwargs(self) -> dict:
|
|
402
|
-
"""Convert config to kwargs dict for process_sj_video."""
|
|
403
|
-
return {
|
|
404
|
-
"video_path": self.video_path,
|
|
405
|
-
"quality": self.quality,
|
|
406
|
-
"output_video": self.output_video,
|
|
407
|
-
"json_output": self.json_output,
|
|
408
|
-
"overrides": self.overrides,
|
|
409
|
-
"detection_confidence": self.detection_confidence,
|
|
410
|
-
"tracking_confidence": self.tracking_confidence,
|
|
411
|
-
"mass_kg": self.mass_kg,
|
|
412
|
-
"verbose": self.verbose,
|
|
413
|
-
"timer": self.timer,
|
|
414
|
-
"pose_tracker": self.pose_tracker,
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
@dataclass
|
|
419
|
-
class SJVideoResult:
|
|
420
|
-
"""Result of processing a single SJ video."""
|
|
421
|
-
|
|
422
|
-
video_path: str
|
|
423
|
-
success: bool
|
|
424
|
-
metrics: SJMetrics | None = None
|
|
425
|
-
error: str | None = None
|
|
426
|
-
processing_time: float = 0.0
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
@experimental(
|
|
430
|
-
"Squat Jump analysis is new and awaiting validation studies. "
|
|
431
|
-
"Power/force calculations use validated Sayers regression but SJ-specific "
|
|
432
|
-
"phase detection may need refinement based on real-world data.",
|
|
433
|
-
since="0.74.0",
|
|
434
|
-
)
|
|
435
|
-
def process_sj_video(
|
|
436
|
-
video_path: str,
|
|
437
|
-
quality: str = "balanced",
|
|
438
|
-
output_video: str | None = None,
|
|
439
|
-
json_output: str | None = None,
|
|
440
|
-
overrides: AnalysisOverrides | None = None,
|
|
441
|
-
detection_confidence: float | None = None,
|
|
442
|
-
tracking_confidence: float | None = None,
|
|
443
|
-
mass_kg: float | None = None,
|
|
444
|
-
verbose: bool = False,
|
|
445
|
-
timer: Timer | None = None,
|
|
446
|
-
pose_tracker: MediaPipePoseTracker | None = None,
|
|
447
|
-
) -> SJMetrics:
|
|
448
|
-
"""
|
|
449
|
-
Process a single SJ video and return metrics.
|
|
450
|
-
|
|
451
|
-
SJ (Squat Jump) is performed from a static squat position without
|
|
452
|
-
countermovement. Athletes start in a squat hold, then explode
|
|
453
|
-
upward to measure pure concentric power output.
|
|
454
|
-
|
|
455
|
-
Args:
|
|
456
|
-
video_path: Path to the input video file
|
|
457
|
-
quality: Analysis quality preset ("fast", "balanced", or "accurate")
|
|
458
|
-
output_video: Optional path for debug video output
|
|
459
|
-
json_output: Optional path for JSON metrics output
|
|
460
|
-
overrides: Optional AnalysisOverrides with parameter fine-tuning
|
|
461
|
-
detection_confidence: Optional override for pose detection confidence
|
|
462
|
-
tracking_confidence: Optional override for pose tracking confidence
|
|
463
|
-
mass_kg: Athlete mass in kilograms (required for power calculations)
|
|
464
|
-
verbose: Print processing details
|
|
465
|
-
timer: Optional Timer for measuring operations
|
|
466
|
-
pose_tracker: Optional pre-initialized PoseTracker instance (reused if provided)
|
|
467
|
-
|
|
468
|
-
Returns:
|
|
469
|
-
SJMetrics object containing analysis results
|
|
470
|
-
|
|
471
|
-
Raises:
|
|
472
|
-
ValueError: If video cannot be processed or parameters are invalid
|
|
473
|
-
FileNotFoundError: If video file does not exist
|
|
474
|
-
"""
|
|
475
|
-
if not Path(video_path).exists():
|
|
476
|
-
raise FileNotFoundError(f"Video file not found: {video_path}")
|
|
477
|
-
|
|
478
|
-
if mass_kg is None or mass_kg <= 0:
|
|
479
|
-
raise ValueError("Athlete mass (mass_kg) must be provided and greater than 0")
|
|
480
|
-
|
|
481
|
-
start_time = time.perf_counter()
|
|
482
|
-
timer = timer or PerformanceTimer()
|
|
483
|
-
quality_preset = parse_quality_preset(quality)
|
|
484
|
-
|
|
485
|
-
with timer.measure("video_initialization"):
|
|
486
|
-
with VideoProcessor(video_path, timer=timer) as video:
|
|
487
|
-
# 1. Pose Tracking
|
|
488
|
-
frames, landmarks_sequence, frame_indices = _run_pose_tracking(
|
|
489
|
-
video,
|
|
490
|
-
quality_preset,
|
|
491
|
-
detection_confidence,
|
|
492
|
-
tracking_confidence,
|
|
493
|
-
pose_tracker,
|
|
494
|
-
verbose,
|
|
495
|
-
timer,
|
|
496
|
-
)
|
|
497
|
-
|
|
498
|
-
# 2. Parameters & Smoothing
|
|
499
|
-
params = _get_tuned_parameters(
|
|
500
|
-
video, landmarks_sequence, quality_preset, overrides, verbose, timer
|
|
501
|
-
)
|
|
502
|
-
smoothed_landmarks = apply_smoothing(landmarks_sequence, params, verbose, timer)
|
|
503
|
-
|
|
504
|
-
# 3. Kinematic Analysis
|
|
505
|
-
metrics, vertical_positions, visibilities = _run_kinematic_analysis(
|
|
506
|
-
video, smoothed_landmarks, params, mass_kg, verbose, timer
|
|
507
|
-
)
|
|
508
|
-
|
|
509
|
-
# 4. Debug Video Generation (Optional)
|
|
510
|
-
if output_video:
|
|
511
|
-
_generate_debug_video(
|
|
512
|
-
output_video,
|
|
513
|
-
frames,
|
|
514
|
-
frame_indices,
|
|
515
|
-
smoothed_landmarks,
|
|
516
|
-
metrics,
|
|
517
|
-
video.fps,
|
|
518
|
-
timer,
|
|
519
|
-
verbose,
|
|
520
|
-
)
|
|
521
|
-
|
|
522
|
-
# 5. Finalization (Quality, Metadata, Validation)
|
|
523
|
-
_finalize_analysis_results(
|
|
524
|
-
metrics,
|
|
525
|
-
video,
|
|
526
|
-
video_path,
|
|
527
|
-
vertical_positions,
|
|
528
|
-
visibilities,
|
|
529
|
-
params,
|
|
530
|
-
quality_preset,
|
|
531
|
-
start_time,
|
|
532
|
-
timer,
|
|
533
|
-
verbose,
|
|
534
|
-
)
|
|
535
|
-
|
|
536
|
-
if json_output:
|
|
537
|
-
_save_metrics_to_json(metrics, json_output, timer, verbose)
|
|
538
|
-
|
|
539
|
-
if verbose:
|
|
540
|
-
_print_timing_summary(start_time, timer, metrics)
|
|
541
|
-
|
|
542
|
-
return metrics
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
def process_sj_video_from_config(
|
|
546
|
-
config: SJVideoConfig,
|
|
547
|
-
) -> SJMetrics:
|
|
548
|
-
"""Process a SJ video using a configuration object.
|
|
549
|
-
|
|
550
|
-
This is a convenience wrapper around process_sj_video that
|
|
551
|
-
accepts a SJVideoConfig instead of individual parameters.
|
|
552
|
-
|
|
553
|
-
Args:
|
|
554
|
-
config: Configuration object containing all analysis parameters
|
|
555
|
-
|
|
556
|
-
Returns:
|
|
557
|
-
SJMetrics object containing analysis results
|
|
558
|
-
"""
|
|
559
|
-
return process_sj_video(**config.to_kwargs())
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
@experimental(
|
|
563
|
-
"Squat Jump analysis is new and awaiting validation studies. "
|
|
564
|
-
"Bulk processing uses parallel workers which may need tuning for large batches.",
|
|
565
|
-
since="0.74.0",
|
|
566
|
-
)
|
|
567
|
-
def process_sj_videos_bulk(
|
|
568
|
-
configs: list[SJVideoConfig],
|
|
569
|
-
max_workers: int = 4,
|
|
570
|
-
progress_callback: Callable[[SJVideoResult], None] | None = None,
|
|
571
|
-
) -> list[SJVideoResult]:
|
|
572
|
-
"""Process multiple SJ videos in parallel."""
|
|
573
|
-
|
|
574
|
-
def error_factory(video_path: str, error_msg: str) -> SJVideoResult:
|
|
575
|
-
return SJVideoResult(video_path=video_path, success=False, error=error_msg)
|
|
576
|
-
|
|
577
|
-
return process_videos_bulk_generic(
|
|
578
|
-
configs,
|
|
579
|
-
_process_sj_video_wrapper,
|
|
580
|
-
error_factory,
|
|
581
|
-
max_workers,
|
|
582
|
-
progress_callback,
|
|
583
|
-
)
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
def _process_sj_video_wrapper(config: SJVideoConfig) -> SJVideoResult:
|
|
587
|
-
"""Wrapper function for parallel SJ processing."""
|
|
588
|
-
start_time = time.perf_counter()
|
|
589
|
-
|
|
590
|
-
try:
|
|
591
|
-
# Use convenience wrapper to avoid parameter unpacking
|
|
592
|
-
metrics = process_sj_video_from_config(config)
|
|
593
|
-
processing_time = time.perf_counter() - start_time
|
|
594
|
-
|
|
595
|
-
return SJVideoResult(
|
|
596
|
-
video_path=config.video_path,
|
|
597
|
-
success=True,
|
|
598
|
-
metrics=metrics,
|
|
599
|
-
processing_time=processing_time,
|
|
600
|
-
)
|
|
601
|
-
|
|
602
|
-
except Exception as e:
|
|
603
|
-
processing_time = time.perf_counter() - start_time
|
|
604
|
-
|
|
605
|
-
return SJVideoResult(
|
|
606
|
-
video_path=config.video_path,
|
|
607
|
-
success=False,
|
|
608
|
-
error=str(e),
|
|
609
|
-
processing_time=processing_time,
|
|
610
|
-
)
|