kinemotion 0.47.1__py3-none-any.whl → 0.47.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 +35 -868
- kinemotion/cmj/__init__.py +1 -1
- kinemotion/cmj/analysis.py +2 -2
- kinemotion/cmj/api.py +433 -0
- kinemotion/cmj/cli.py +2 -1
- kinemotion/cmj/kinematics.py +165 -79
- kinemotion/cmj/metrics_validator.py +2 -2
- kinemotion/core/__init__.py +0 -4
- kinemotion/core/timing.py +3 -138
- kinemotion/dropjump/api.py +541 -0
- kinemotion/dropjump/cli.py +5 -5
- kinemotion/dropjump/validation_bounds.py +58 -39
- {kinemotion-0.47.1.dist-info → kinemotion-0.47.3.dist-info}/METADATA +1 -1
- {kinemotion-0.47.1.dist-info → kinemotion-0.47.3.dist-info}/RECORD +17 -15
- {kinemotion-0.47.1.dist-info → kinemotion-0.47.3.dist-info}/WHEEL +0 -0
- {kinemotion-0.47.1.dist-info → kinemotion-0.47.3.dist-info}/entry_points.txt +0 -0
- {kinemotion-0.47.1.dist-info → kinemotion-0.47.3.dist-info}/licenses/LICENSE +0 -0
kinemotion/cmj/__init__.py
CHANGED
kinemotion/cmj/analysis.py
CHANGED
|
@@ -480,7 +480,7 @@ def compute_average_hip_position(
|
|
|
480
480
|
def find_standing_end(
|
|
481
481
|
velocities: np.ndarray,
|
|
482
482
|
lowest_point: float,
|
|
483
|
-
|
|
483
|
+
_positions: np.ndarray | None = None,
|
|
484
484
|
accelerations: np.ndarray | None = None,
|
|
485
485
|
) -> float | None:
|
|
486
486
|
"""
|
|
@@ -493,7 +493,7 @@ def find_standing_end(
|
|
|
493
493
|
Args:
|
|
494
494
|
velocities: Signed velocity array (for backward compatibility)
|
|
495
495
|
lowest_point: Frame index of lowest point
|
|
496
|
-
|
|
496
|
+
_positions: Intentionally unused - kept for backward compatibility
|
|
497
497
|
accelerations: Acceleration array (if provided, uses
|
|
498
498
|
acceleration-based detection)
|
|
499
499
|
|
kinemotion/cmj/api.py
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
"""Public API for CMJ (Counter Movement 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
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
from ..core.auto_tuning import (
|
|
14
|
+
analyze_video_sample,
|
|
15
|
+
auto_tune_parameters,
|
|
16
|
+
)
|
|
17
|
+
from ..core.filtering import reject_outliers
|
|
18
|
+
from ..core.metadata import (
|
|
19
|
+
AlgorithmConfig,
|
|
20
|
+
DetectionConfig,
|
|
21
|
+
ProcessingInfo,
|
|
22
|
+
ResultMetadata,
|
|
23
|
+
SmoothingConfig,
|
|
24
|
+
VideoInfo,
|
|
25
|
+
create_timestamp,
|
|
26
|
+
get_kinemotion_version,
|
|
27
|
+
)
|
|
28
|
+
from ..core.pipeline_utils import (
|
|
29
|
+
apply_expert_overrides,
|
|
30
|
+
apply_smoothing,
|
|
31
|
+
convert_timer_to_stage_names,
|
|
32
|
+
determine_confidence_levels,
|
|
33
|
+
extract_vertical_positions,
|
|
34
|
+
parse_quality_preset,
|
|
35
|
+
print_verbose_parameters,
|
|
36
|
+
process_all_frames,
|
|
37
|
+
process_videos_bulk_generic,
|
|
38
|
+
)
|
|
39
|
+
from ..core.pose import PoseTracker
|
|
40
|
+
from ..core.quality import assess_jump_quality
|
|
41
|
+
from ..core.timing import PerformanceTimer, Timer
|
|
42
|
+
from ..core.video_io import VideoProcessor
|
|
43
|
+
from .analysis import compute_signed_velocity, detect_cmj_phases
|
|
44
|
+
from .debug_overlay import CMJDebugOverlayRenderer
|
|
45
|
+
from .kinematics import CMJMetrics, calculate_cmj_metrics
|
|
46
|
+
from .metrics_validator import CMJMetricsValidator
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class CMJVideoConfig:
|
|
51
|
+
"""Configuration for processing a single CMJ video."""
|
|
52
|
+
|
|
53
|
+
video_path: str
|
|
54
|
+
quality: str = "balanced"
|
|
55
|
+
output_video: str | None = None
|
|
56
|
+
json_output: str | None = None
|
|
57
|
+
smoothing_window: int | None = None
|
|
58
|
+
velocity_threshold: float | None = None
|
|
59
|
+
min_contact_frames: int | None = None
|
|
60
|
+
visibility_threshold: float | None = None
|
|
61
|
+
detection_confidence: float | None = None
|
|
62
|
+
tracking_confidence: float | None = None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class CMJVideoResult:
|
|
67
|
+
"""Result of processing a single CMJ video."""
|
|
68
|
+
|
|
69
|
+
video_path: str
|
|
70
|
+
success: bool
|
|
71
|
+
metrics: CMJMetrics | None = None
|
|
72
|
+
error: str | None = None
|
|
73
|
+
processing_time: float = 0.0
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def process_cmj_video(
|
|
77
|
+
video_path: str,
|
|
78
|
+
quality: str = "balanced",
|
|
79
|
+
output_video: str | None = None,
|
|
80
|
+
json_output: str | None = None,
|
|
81
|
+
smoothing_window: int | None = None,
|
|
82
|
+
velocity_threshold: float | None = None,
|
|
83
|
+
min_contact_frames: int | None = None,
|
|
84
|
+
visibility_threshold: float | None = None,
|
|
85
|
+
detection_confidence: float | None = None,
|
|
86
|
+
tracking_confidence: float | None = None,
|
|
87
|
+
verbose: bool = False,
|
|
88
|
+
timer: Timer | None = None,
|
|
89
|
+
pose_tracker: "PoseTracker | None" = None,
|
|
90
|
+
) -> CMJMetrics:
|
|
91
|
+
"""
|
|
92
|
+
Process a single CMJ video and return metrics.
|
|
93
|
+
|
|
94
|
+
CMJ (Counter Movement Jump) is performed at floor level without a drop box.
|
|
95
|
+
Athletes start standing, perform a countermovement (eccentric phase), then
|
|
96
|
+
jump upward (concentric phase).
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
video_path: Path to the input video file
|
|
100
|
+
quality: Analysis quality preset ("fast", "balanced", or "accurate")
|
|
101
|
+
output_video: Optional path for debug video output
|
|
102
|
+
json_output: Optional path for JSON metrics output
|
|
103
|
+
smoothing_window: Optional override for smoothing window
|
|
104
|
+
velocity_threshold: Optional override for velocity threshold
|
|
105
|
+
min_contact_frames: Optional override for minimum contact frames
|
|
106
|
+
visibility_threshold: Optional override for visibility threshold
|
|
107
|
+
detection_confidence: Optional override for pose detection confidence
|
|
108
|
+
tracking_confidence: Optional override for pose tracking confidence
|
|
109
|
+
verbose: Print processing details
|
|
110
|
+
timer: Optional Timer for measuring operations
|
|
111
|
+
pose_tracker: Optional pre-initialized PoseTracker instance (reused if provided)
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
CMJMetrics object containing analysis results
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
ValueError: If video cannot be processed or parameters are invalid
|
|
118
|
+
FileNotFoundError: If video file does not exist
|
|
119
|
+
"""
|
|
120
|
+
if not Path(video_path).exists():
|
|
121
|
+
raise FileNotFoundError(f"Video file not found: {video_path}")
|
|
122
|
+
|
|
123
|
+
start_time = time.time()
|
|
124
|
+
if timer is None:
|
|
125
|
+
timer = PerformanceTimer()
|
|
126
|
+
|
|
127
|
+
quality_preset = parse_quality_preset(quality)
|
|
128
|
+
|
|
129
|
+
with timer.measure("video_initialization"):
|
|
130
|
+
with VideoProcessor(video_path, timer=timer) as video:
|
|
131
|
+
if verbose:
|
|
132
|
+
print(
|
|
133
|
+
f"Video: {video.width}x{video.height} @ {video.fps:.2f} fps, "
|
|
134
|
+
f"{video.frame_count} frames"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
det_conf, track_conf = determine_confidence_levels(
|
|
138
|
+
quality_preset, detection_confidence, tracking_confidence
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if verbose:
|
|
142
|
+
print("Processing all frames with MediaPipe pose tracking...")
|
|
143
|
+
|
|
144
|
+
tracker = pose_tracker
|
|
145
|
+
should_close_tracker = False
|
|
146
|
+
|
|
147
|
+
if tracker is None:
|
|
148
|
+
tracker = PoseTracker(
|
|
149
|
+
min_detection_confidence=det_conf,
|
|
150
|
+
min_tracking_confidence=track_conf,
|
|
151
|
+
timer=timer,
|
|
152
|
+
)
|
|
153
|
+
should_close_tracker = True
|
|
154
|
+
|
|
155
|
+
frames, landmarks_sequence, frame_indices = process_all_frames(
|
|
156
|
+
video, tracker, verbose, timer, close_tracker=should_close_tracker
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
with timer.measure("parameter_auto_tuning"):
|
|
160
|
+
characteristics = analyze_video_sample(
|
|
161
|
+
landmarks_sequence, video.fps, video.frame_count
|
|
162
|
+
)
|
|
163
|
+
params = auto_tune_parameters(characteristics, quality_preset)
|
|
164
|
+
|
|
165
|
+
params = apply_expert_overrides(
|
|
166
|
+
params,
|
|
167
|
+
smoothing_window,
|
|
168
|
+
velocity_threshold,
|
|
169
|
+
min_contact_frames,
|
|
170
|
+
visibility_threshold,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
if verbose:
|
|
174
|
+
print_verbose_parameters(
|
|
175
|
+
video, characteristics, quality_preset, params
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
smoothed_landmarks = apply_smoothing(
|
|
179
|
+
landmarks_sequence, params, verbose, timer
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
if verbose:
|
|
183
|
+
print("Extracting vertical positions (Hip and Foot)...")
|
|
184
|
+
with timer.measure("vertical_position_extraction"):
|
|
185
|
+
vertical_positions, visibilities = extract_vertical_positions(
|
|
186
|
+
smoothed_landmarks, target="hip"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
foot_positions, _ = extract_vertical_positions(
|
|
190
|
+
smoothed_landmarks, target="foot"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
tracking_method = "hip_hybrid"
|
|
194
|
+
|
|
195
|
+
if verbose:
|
|
196
|
+
print("Detecting CMJ phases...")
|
|
197
|
+
with timer.measure("phase_detection"):
|
|
198
|
+
phases = detect_cmj_phases(
|
|
199
|
+
vertical_positions,
|
|
200
|
+
video.fps,
|
|
201
|
+
window_length=params.smoothing_window,
|
|
202
|
+
polyorder=params.polyorder,
|
|
203
|
+
landing_positions=foot_positions,
|
|
204
|
+
timer=timer,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if phases is None:
|
|
208
|
+
raise ValueError("Could not detect CMJ phases in video")
|
|
209
|
+
|
|
210
|
+
standing_end, lowest_point, takeoff_frame, landing_frame = phases
|
|
211
|
+
|
|
212
|
+
if verbose:
|
|
213
|
+
print("Calculating metrics...")
|
|
214
|
+
with timer.measure("metrics_calculation"):
|
|
215
|
+
velocities = compute_signed_velocity(
|
|
216
|
+
vertical_positions,
|
|
217
|
+
window_length=params.smoothing_window,
|
|
218
|
+
polyorder=params.polyorder,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
metrics = calculate_cmj_metrics(
|
|
222
|
+
vertical_positions,
|
|
223
|
+
velocities,
|
|
224
|
+
standing_end,
|
|
225
|
+
lowest_point,
|
|
226
|
+
takeoff_frame,
|
|
227
|
+
landing_frame,
|
|
228
|
+
video.fps,
|
|
229
|
+
tracking_method=tracking_method,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
if verbose:
|
|
233
|
+
print("Assessing tracking quality...")
|
|
234
|
+
with timer.measure("quality_assessment"):
|
|
235
|
+
_, outlier_mask = reject_outliers(
|
|
236
|
+
vertical_positions,
|
|
237
|
+
use_ransac=True,
|
|
238
|
+
use_median=True,
|
|
239
|
+
interpolate=False,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
phases_detected = True
|
|
243
|
+
phase_count = 4
|
|
244
|
+
|
|
245
|
+
quality_result = assess_jump_quality(
|
|
246
|
+
visibilities=visibilities,
|
|
247
|
+
positions=vertical_positions,
|
|
248
|
+
outlier_mask=outlier_mask,
|
|
249
|
+
fps=video.fps,
|
|
250
|
+
phases_detected=phases_detected,
|
|
251
|
+
phase_count=phase_count,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
algorithm_config = AlgorithmConfig(
|
|
255
|
+
detection_method="backward_search",
|
|
256
|
+
tracking_method="mediapipe_pose",
|
|
257
|
+
model_complexity=1,
|
|
258
|
+
smoothing=SmoothingConfig(
|
|
259
|
+
window_size=params.smoothing_window,
|
|
260
|
+
polynomial_order=params.polyorder,
|
|
261
|
+
use_bilateral_filter=params.bilateral_filter,
|
|
262
|
+
use_outlier_rejection=params.outlier_rejection,
|
|
263
|
+
),
|
|
264
|
+
detection=DetectionConfig(
|
|
265
|
+
velocity_threshold=params.velocity_threshold,
|
|
266
|
+
min_contact_frames=params.min_contact_frames,
|
|
267
|
+
visibility_threshold=params.visibility_threshold,
|
|
268
|
+
use_curvature_refinement=params.use_curvature,
|
|
269
|
+
),
|
|
270
|
+
drop_detection=None,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
video_info = VideoInfo(
|
|
274
|
+
source_path=video_path,
|
|
275
|
+
fps=video.fps,
|
|
276
|
+
width=video.width,
|
|
277
|
+
height=video.height,
|
|
278
|
+
duration_s=video.frame_count / video.fps,
|
|
279
|
+
frame_count=video.frame_count,
|
|
280
|
+
codec=video.codec,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
if verbose and quality_result.warnings:
|
|
284
|
+
print("\n⚠️ Quality Warnings:")
|
|
285
|
+
for warning in quality_result.warnings:
|
|
286
|
+
print(f" - {warning}")
|
|
287
|
+
print()
|
|
288
|
+
|
|
289
|
+
if output_video:
|
|
290
|
+
if verbose:
|
|
291
|
+
print(f"Generating debug video: {output_video}")
|
|
292
|
+
|
|
293
|
+
debug_h, debug_w = frames[0].shape[:2]
|
|
294
|
+
step = max(1, int(video.fps / 30.0))
|
|
295
|
+
debug_fps = video.fps / step
|
|
296
|
+
|
|
297
|
+
with timer.measure("debug_video_generation"):
|
|
298
|
+
with CMJDebugOverlayRenderer(
|
|
299
|
+
output_video,
|
|
300
|
+
debug_w,
|
|
301
|
+
debug_h,
|
|
302
|
+
debug_w,
|
|
303
|
+
debug_h,
|
|
304
|
+
debug_fps,
|
|
305
|
+
timer=timer,
|
|
306
|
+
) as renderer:
|
|
307
|
+
for frame, idx in zip(frames, frame_indices, strict=True):
|
|
308
|
+
annotated = renderer.render_frame(
|
|
309
|
+
frame, smoothed_landmarks[idx], idx, metrics
|
|
310
|
+
)
|
|
311
|
+
renderer.write_frame(annotated)
|
|
312
|
+
|
|
313
|
+
if verbose:
|
|
314
|
+
print(f"Debug video saved: {output_video}")
|
|
315
|
+
|
|
316
|
+
with timer.measure("metrics_validation"):
|
|
317
|
+
validator = CMJMetricsValidator()
|
|
318
|
+
validation_result = validator.validate(metrics.to_dict()) # type: ignore[arg-type]
|
|
319
|
+
metrics.validation_result = validation_result
|
|
320
|
+
|
|
321
|
+
processing_time = time.time() - start_time
|
|
322
|
+
stage_times = convert_timer_to_stage_names(timer.get_metrics())
|
|
323
|
+
|
|
324
|
+
processing_info = ProcessingInfo(
|
|
325
|
+
version=get_kinemotion_version(),
|
|
326
|
+
timestamp=create_timestamp(),
|
|
327
|
+
quality_preset=quality_preset.value,
|
|
328
|
+
processing_time_s=processing_time,
|
|
329
|
+
timing_breakdown=stage_times,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
result_metadata = ResultMetadata(
|
|
333
|
+
quality=quality_result,
|
|
334
|
+
video=video_info,
|
|
335
|
+
processing=processing_info,
|
|
336
|
+
algorithm=algorithm_config,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
metrics.result_metadata = result_metadata
|
|
340
|
+
|
|
341
|
+
if json_output:
|
|
342
|
+
with timer.measure("json_serialization"):
|
|
343
|
+
output_path = Path(json_output)
|
|
344
|
+
metrics_dict = metrics.to_dict()
|
|
345
|
+
json_str = json.dumps(metrics_dict, indent=2)
|
|
346
|
+
output_path.write_text(json_str)
|
|
347
|
+
|
|
348
|
+
if verbose:
|
|
349
|
+
print(f"Metrics written to: {json_output}")
|
|
350
|
+
|
|
351
|
+
if verbose and validation_result.issues:
|
|
352
|
+
print("\n⚠️ Validation Results:")
|
|
353
|
+
for issue in validation_result.issues:
|
|
354
|
+
print(f" [{issue.severity.value}] {issue.metric}: {issue.message}")
|
|
355
|
+
|
|
356
|
+
if verbose:
|
|
357
|
+
total_time = time.time() - start_time
|
|
358
|
+
stage_times = convert_timer_to_stage_names(timer.get_metrics())
|
|
359
|
+
|
|
360
|
+
print("\n=== Timing Summary ===")
|
|
361
|
+
for stage, duration in stage_times.items():
|
|
362
|
+
percentage = (duration / total_time) * 100
|
|
363
|
+
dur_ms = duration * 1000
|
|
364
|
+
print(f"{stage:. <40} {dur_ms:>6.0f}ms ({percentage:>5.1f}%)")
|
|
365
|
+
total_ms = total_time * 1000
|
|
366
|
+
print(f"{('Total'):.>40} {total_ms:>6.0f}ms (100.0%)")
|
|
367
|
+
print()
|
|
368
|
+
|
|
369
|
+
print(f"\nJump height: {metrics.jump_height:.3f}m")
|
|
370
|
+
print(f"Flight time: {metrics.flight_time * 1000:.1f}ms")
|
|
371
|
+
print(f"Countermovement depth: {metrics.countermovement_depth:.3f}m")
|
|
372
|
+
|
|
373
|
+
return metrics
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def process_cmj_videos_bulk(
|
|
377
|
+
configs: list[CMJVideoConfig],
|
|
378
|
+
max_workers: int = 4,
|
|
379
|
+
progress_callback: Callable[[CMJVideoResult], None] | None = None,
|
|
380
|
+
) -> list[CMJVideoResult]:
|
|
381
|
+
"""
|
|
382
|
+
Process multiple CMJ videos in parallel.
|
|
383
|
+
"""
|
|
384
|
+
|
|
385
|
+
def error_factory(video_path: str, error_msg: str) -> CMJVideoResult:
|
|
386
|
+
return CMJVideoResult(video_path=video_path, success=False, error=error_msg)
|
|
387
|
+
|
|
388
|
+
return process_videos_bulk_generic(
|
|
389
|
+
configs,
|
|
390
|
+
_process_cmj_video_wrapper,
|
|
391
|
+
error_factory,
|
|
392
|
+
max_workers,
|
|
393
|
+
progress_callback,
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _process_cmj_video_wrapper(config: CMJVideoConfig) -> CMJVideoResult:
|
|
398
|
+
"""Wrapper function for parallel CMJ processing."""
|
|
399
|
+
start_time = time.time()
|
|
400
|
+
|
|
401
|
+
try:
|
|
402
|
+
metrics = process_cmj_video(
|
|
403
|
+
video_path=config.video_path,
|
|
404
|
+
quality=config.quality,
|
|
405
|
+
output_video=config.output_video,
|
|
406
|
+
json_output=config.json_output,
|
|
407
|
+
smoothing_window=config.smoothing_window,
|
|
408
|
+
velocity_threshold=config.velocity_threshold,
|
|
409
|
+
min_contact_frames=config.min_contact_frames,
|
|
410
|
+
visibility_threshold=config.visibility_threshold,
|
|
411
|
+
detection_confidence=config.detection_confidence,
|
|
412
|
+
tracking_confidence=config.tracking_confidence,
|
|
413
|
+
verbose=False,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
processing_time = time.time() - start_time
|
|
417
|
+
|
|
418
|
+
return CMJVideoResult(
|
|
419
|
+
video_path=config.video_path,
|
|
420
|
+
success=True,
|
|
421
|
+
metrics=metrics,
|
|
422
|
+
processing_time=processing_time,
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
except Exception as e:
|
|
426
|
+
processing_time = time.time() - start_time
|
|
427
|
+
|
|
428
|
+
return CMJVideoResult(
|
|
429
|
+
video_path=config.video_path,
|
|
430
|
+
success=False,
|
|
431
|
+
error=str(e),
|
|
432
|
+
processing_time=processing_time,
|
|
433
|
+
)
|
kinemotion/cmj/cli.py
CHANGED
|
@@ -6,13 +6,14 @@ from dataclasses import dataclass
|
|
|
6
6
|
|
|
7
7
|
import click
|
|
8
8
|
|
|
9
|
-
from ..api import CMJMetrics, process_cmj_video
|
|
10
9
|
from ..core.auto_tuning import QualityPreset
|
|
11
10
|
from ..core.cli_utils import (
|
|
12
11
|
collect_video_files,
|
|
13
12
|
common_output_options,
|
|
14
13
|
generate_batch_output_paths,
|
|
15
14
|
)
|
|
15
|
+
from .api import process_cmj_video
|
|
16
|
+
from .kinematics import CMJMetrics
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
@dataclass
|