kinemotion 0.47.3__py3-none-any.whl → 0.48.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 +6 -0
- kinemotion/cmj/api.py +214 -153
- kinemotion/cmj/cli.py +10 -5
- kinemotion/dropjump/api.py +242 -110
- kinemotion/dropjump/cli.py +40 -8
- {kinemotion-0.47.3.dist-info → kinemotion-0.48.0.dist-info}/METADATA +1 -1
- {kinemotion-0.47.3.dist-info → kinemotion-0.48.0.dist-info}/RECORD +10 -10
- {kinemotion-0.47.3.dist-info → kinemotion-0.48.0.dist-info}/WHEEL +0 -0
- {kinemotion-0.47.3.dist-info → kinemotion-0.48.0.dist-info}/entry_points.txt +0 -0
- {kinemotion-0.47.3.dist-info → kinemotion-0.48.0.dist-info}/licenses/LICENSE +0 -0
kinemotion/api.py
CHANGED
|
@@ -7,6 +7,9 @@ The actual implementations have been moved to their respective submodules:
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
# CMJ API
|
|
10
|
+
from .cmj.api import (
|
|
11
|
+
AnalysisOverrides as CMJAnalysisOverrides,
|
|
12
|
+
)
|
|
10
13
|
from .cmj.api import (
|
|
11
14
|
CMJVideoConfig,
|
|
12
15
|
CMJVideoResult,
|
|
@@ -17,6 +20,7 @@ from .cmj.kinematics import CMJMetrics
|
|
|
17
20
|
|
|
18
21
|
# Drop jump API
|
|
19
22
|
from .dropjump.api import (
|
|
23
|
+
AnalysisOverrides,
|
|
20
24
|
DropJumpVideoConfig,
|
|
21
25
|
DropJumpVideoResult,
|
|
22
26
|
process_dropjump_video,
|
|
@@ -25,11 +29,13 @@ from .dropjump.api import (
|
|
|
25
29
|
|
|
26
30
|
__all__ = [
|
|
27
31
|
# Drop jump
|
|
32
|
+
"AnalysisOverrides",
|
|
28
33
|
"DropJumpVideoConfig",
|
|
29
34
|
"DropJumpVideoResult",
|
|
30
35
|
"process_dropjump_video",
|
|
31
36
|
"process_dropjump_videos_bulk",
|
|
32
37
|
# CMJ
|
|
38
|
+
"CMJAnalysisOverrides",
|
|
33
39
|
"CMJMetrics",
|
|
34
40
|
"CMJVideoConfig",
|
|
35
41
|
"CMJVideoResult",
|
kinemotion/cmj/api.py
CHANGED
|
@@ -5,12 +5,13 @@ import time
|
|
|
5
5
|
from collections.abc import Callable
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import TYPE_CHECKING
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
import numpy as np
|
|
10
|
+
from numpy.typing import NDArray
|
|
12
11
|
|
|
13
12
|
from ..core.auto_tuning import (
|
|
13
|
+
AnalysisParameters,
|
|
14
|
+
QualityPreset,
|
|
14
15
|
analyze_video_sample,
|
|
15
16
|
auto_tune_parameters,
|
|
16
17
|
)
|
|
@@ -37,8 +38,9 @@ from ..core.pipeline_utils import (
|
|
|
37
38
|
process_videos_bulk_generic,
|
|
38
39
|
)
|
|
39
40
|
from ..core.pose import PoseTracker
|
|
40
|
-
from ..core.quality import assess_jump_quality
|
|
41
|
+
from ..core.quality import QualityAssessment, assess_jump_quality
|
|
41
42
|
from ..core.timing import PerformanceTimer, Timer
|
|
43
|
+
from ..core.validation import ValidationResult
|
|
42
44
|
from ..core.video_io import VideoProcessor
|
|
43
45
|
from .analysis import compute_signed_velocity, detect_cmj_phases
|
|
44
46
|
from .debug_overlay import CMJDebugOverlayRenderer
|
|
@@ -46,6 +48,176 @@ from .kinematics import CMJMetrics, calculate_cmj_metrics
|
|
|
46
48
|
from .metrics_validator import CMJMetricsValidator
|
|
47
49
|
|
|
48
50
|
|
|
51
|
+
@dataclass
|
|
52
|
+
class AnalysisOverrides:
|
|
53
|
+
"""Optional overrides for analysis parameters.
|
|
54
|
+
|
|
55
|
+
Allows fine-tuning of specific analysis parameters beyond quality presets.
|
|
56
|
+
If None, values will be determined by the quality preset.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
smoothing_window: int | None = None
|
|
60
|
+
velocity_threshold: float | None = None
|
|
61
|
+
min_contact_frames: int | None = None
|
|
62
|
+
visibility_threshold: float | None = None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _generate_debug_video(
|
|
66
|
+
output_video: str,
|
|
67
|
+
frames: list[NDArray[np.uint8]],
|
|
68
|
+
frame_indices: list[int],
|
|
69
|
+
smoothed_landmarks: list,
|
|
70
|
+
metrics: CMJMetrics,
|
|
71
|
+
video_fps: float,
|
|
72
|
+
timer: Timer,
|
|
73
|
+
verbose: bool,
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Generate debug video with CMJ analysis overlay."""
|
|
76
|
+
if verbose:
|
|
77
|
+
print(f"Generating debug video: {output_video}")
|
|
78
|
+
|
|
79
|
+
debug_h, debug_w = frames[0].shape[:2]
|
|
80
|
+
step = max(1, int(video_fps / 30.0))
|
|
81
|
+
debug_fps = video_fps / step
|
|
82
|
+
|
|
83
|
+
with timer.measure("debug_video_generation"):
|
|
84
|
+
with CMJDebugOverlayRenderer(
|
|
85
|
+
output_video,
|
|
86
|
+
debug_w,
|
|
87
|
+
debug_h,
|
|
88
|
+
debug_w,
|
|
89
|
+
debug_h,
|
|
90
|
+
debug_fps,
|
|
91
|
+
timer=timer,
|
|
92
|
+
) as renderer:
|
|
93
|
+
for frame, idx in zip(frames, frame_indices, strict=True):
|
|
94
|
+
annotated = renderer.render_frame(
|
|
95
|
+
frame, smoothed_landmarks[idx], idx, metrics
|
|
96
|
+
)
|
|
97
|
+
renderer.write_frame(annotated)
|
|
98
|
+
|
|
99
|
+
if verbose:
|
|
100
|
+
print(f"Debug video saved: {output_video}")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _save_metrics_to_json(
|
|
104
|
+
metrics: CMJMetrics, json_output: str, timer: Timer, verbose: bool
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Save metrics to JSON file."""
|
|
107
|
+
with timer.measure("json_serialization"):
|
|
108
|
+
output_path = Path(json_output)
|
|
109
|
+
metrics_dict = metrics.to_dict()
|
|
110
|
+
json_str = json.dumps(metrics_dict, indent=2)
|
|
111
|
+
output_path.write_text(json_str)
|
|
112
|
+
|
|
113
|
+
if verbose:
|
|
114
|
+
print(f"Metrics written to: {json_output}")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _print_timing_summary(start_time: float, timer: Timer, metrics: CMJMetrics) -> None:
|
|
118
|
+
"""Print verbose timing summary and metrics."""
|
|
119
|
+
total_time = time.time() - start_time
|
|
120
|
+
stage_times = convert_timer_to_stage_names(timer.get_metrics())
|
|
121
|
+
|
|
122
|
+
print("\n=== Timing Summary ===")
|
|
123
|
+
for stage, duration in stage_times.items():
|
|
124
|
+
percentage = (duration / total_time) * 100
|
|
125
|
+
dur_ms = duration * 1000
|
|
126
|
+
print(f"{stage:.<40} {dur_ms:>6.0f}ms ({percentage:>5.1f}%)")
|
|
127
|
+
total_ms = total_time * 1000
|
|
128
|
+
print(f"{'Total':.<40} {total_ms:>6.0f}ms (100.0%)")
|
|
129
|
+
print()
|
|
130
|
+
|
|
131
|
+
print(f"\nJump height: {metrics.jump_height:.3f}m")
|
|
132
|
+
print(f"Flight time: {metrics.flight_time * 1000:.1f}ms")
|
|
133
|
+
print(f"Countermovement depth: {metrics.countermovement_depth:.3f}m")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _print_quality_warnings(quality_result: QualityAssessment, verbose: bool) -> None:
|
|
137
|
+
"""Print quality warnings if present."""
|
|
138
|
+
if verbose and quality_result.warnings:
|
|
139
|
+
print("\n⚠️ Quality Warnings:")
|
|
140
|
+
for warning in quality_result.warnings:
|
|
141
|
+
print(f" - {warning}")
|
|
142
|
+
print()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _print_validation_results(
|
|
146
|
+
validation_result: ValidationResult, verbose: bool
|
|
147
|
+
) -> None:
|
|
148
|
+
"""Print validation issues if present."""
|
|
149
|
+
if verbose and validation_result.issues:
|
|
150
|
+
print("\n⚠️ Validation Results:")
|
|
151
|
+
for issue in validation_result.issues:
|
|
152
|
+
print(f" [{issue.severity.value}] {issue.metric}: {issue.message}")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _create_algorithm_config(params: AnalysisParameters) -> AlgorithmConfig:
|
|
156
|
+
"""Create algorithm configuration from parameters."""
|
|
157
|
+
return AlgorithmConfig(
|
|
158
|
+
detection_method="backward_search",
|
|
159
|
+
tracking_method="mediapipe_pose",
|
|
160
|
+
model_complexity=1,
|
|
161
|
+
smoothing=SmoothingConfig(
|
|
162
|
+
window_size=params.smoothing_window,
|
|
163
|
+
polynomial_order=params.polyorder,
|
|
164
|
+
use_bilateral_filter=params.bilateral_filter,
|
|
165
|
+
use_outlier_rejection=params.outlier_rejection,
|
|
166
|
+
),
|
|
167
|
+
detection=DetectionConfig(
|
|
168
|
+
velocity_threshold=params.velocity_threshold,
|
|
169
|
+
min_contact_frames=params.min_contact_frames,
|
|
170
|
+
visibility_threshold=params.visibility_threshold,
|
|
171
|
+
use_curvature_refinement=params.use_curvature,
|
|
172
|
+
),
|
|
173
|
+
drop_detection=None,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _create_video_info(video_path: str, video: VideoProcessor) -> VideoInfo:
|
|
178
|
+
"""Create video information metadata."""
|
|
179
|
+
return VideoInfo(
|
|
180
|
+
source_path=video_path,
|
|
181
|
+
fps=video.fps,
|
|
182
|
+
width=video.width,
|
|
183
|
+
height=video.height,
|
|
184
|
+
duration_s=video.frame_count / video.fps,
|
|
185
|
+
frame_count=video.frame_count,
|
|
186
|
+
codec=video.codec,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _create_processing_info(
|
|
191
|
+
start_time: float, quality_preset: QualityPreset, timer: Timer
|
|
192
|
+
) -> ProcessingInfo:
|
|
193
|
+
"""Create processing information metadata."""
|
|
194
|
+
processing_time = time.time() - start_time
|
|
195
|
+
stage_times = convert_timer_to_stage_names(timer.get_metrics())
|
|
196
|
+
|
|
197
|
+
return ProcessingInfo(
|
|
198
|
+
version=get_kinemotion_version(),
|
|
199
|
+
timestamp=create_timestamp(),
|
|
200
|
+
quality_preset=quality_preset.value,
|
|
201
|
+
processing_time_s=processing_time,
|
|
202
|
+
timing_breakdown=stage_times,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _create_result_metadata(
|
|
207
|
+
quality_result: QualityAssessment,
|
|
208
|
+
video_info: VideoInfo,
|
|
209
|
+
processing_info: ProcessingInfo,
|
|
210
|
+
algorithm_config: AlgorithmConfig,
|
|
211
|
+
) -> ResultMetadata:
|
|
212
|
+
"""Create result metadata from components."""
|
|
213
|
+
return ResultMetadata(
|
|
214
|
+
quality=quality_result,
|
|
215
|
+
video=video_info,
|
|
216
|
+
processing=processing_info,
|
|
217
|
+
algorithm=algorithm_config,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
49
221
|
@dataclass
|
|
50
222
|
class CMJVideoConfig:
|
|
51
223
|
"""Configuration for processing a single CMJ video."""
|
|
@@ -54,10 +226,7 @@ class CMJVideoConfig:
|
|
|
54
226
|
quality: str = "balanced"
|
|
55
227
|
output_video: str | None = None
|
|
56
228
|
json_output: str | None = None
|
|
57
|
-
|
|
58
|
-
velocity_threshold: float | None = None
|
|
59
|
-
min_contact_frames: int | None = None
|
|
60
|
-
visibility_threshold: float | None = None
|
|
229
|
+
overrides: AnalysisOverrides | None = None
|
|
61
230
|
detection_confidence: float | None = None
|
|
62
231
|
tracking_confidence: float | None = None
|
|
63
232
|
|
|
@@ -78,15 +247,12 @@ def process_cmj_video(
|
|
|
78
247
|
quality: str = "balanced",
|
|
79
248
|
output_video: str | None = None,
|
|
80
249
|
json_output: str | None = None,
|
|
81
|
-
|
|
82
|
-
velocity_threshold: float | None = None,
|
|
83
|
-
min_contact_frames: int | None = None,
|
|
84
|
-
visibility_threshold: float | None = None,
|
|
250
|
+
overrides: AnalysisOverrides | None = None,
|
|
85
251
|
detection_confidence: float | None = None,
|
|
86
252
|
tracking_confidence: float | None = None,
|
|
87
253
|
verbose: bool = False,
|
|
88
254
|
timer: Timer | None = None,
|
|
89
|
-
pose_tracker:
|
|
255
|
+
pose_tracker: PoseTracker | None = None,
|
|
90
256
|
) -> CMJMetrics:
|
|
91
257
|
"""
|
|
92
258
|
Process a single CMJ video and return metrics.
|
|
@@ -100,10 +266,7 @@ def process_cmj_video(
|
|
|
100
266
|
quality: Analysis quality preset ("fast", "balanced", or "accurate")
|
|
101
267
|
output_video: Optional path for debug video output
|
|
102
268
|
json_output: Optional path for JSON metrics output
|
|
103
|
-
|
|
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
|
|
269
|
+
overrides: Optional AnalysisOverrides with parameter fine-tuning
|
|
107
270
|
detection_confidence: Optional override for pose detection confidence
|
|
108
271
|
tracking_confidence: Optional override for pose tracking confidence
|
|
109
272
|
verbose: Print processing details
|
|
@@ -121,9 +284,7 @@ def process_cmj_video(
|
|
|
121
284
|
raise FileNotFoundError(f"Video file not found: {video_path}")
|
|
122
285
|
|
|
123
286
|
start_time = time.time()
|
|
124
|
-
|
|
125
|
-
timer = PerformanceTimer()
|
|
126
|
-
|
|
287
|
+
timer = timer or PerformanceTimer()
|
|
127
288
|
quality_preset = parse_quality_preset(quality)
|
|
128
289
|
|
|
129
290
|
with timer.measure("video_initialization"):
|
|
@@ -141,16 +302,12 @@ def process_cmj_video(
|
|
|
141
302
|
if verbose:
|
|
142
303
|
print("Processing all frames with MediaPipe pose tracking...")
|
|
143
304
|
|
|
144
|
-
tracker = pose_tracker
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
min_tracking_confidence=track_conf,
|
|
151
|
-
timer=timer,
|
|
152
|
-
)
|
|
153
|
-
should_close_tracker = True
|
|
305
|
+
tracker = pose_tracker or PoseTracker(
|
|
306
|
+
min_detection_confidence=det_conf,
|
|
307
|
+
min_tracking_confidence=track_conf,
|
|
308
|
+
timer=timer,
|
|
309
|
+
)
|
|
310
|
+
should_close_tracker = pose_tracker is None
|
|
154
311
|
|
|
155
312
|
frames, landmarks_sequence, frame_indices = process_all_frames(
|
|
156
313
|
video, tracker, verbose, timer, close_tracker=should_close_tracker
|
|
@@ -161,13 +318,12 @@ def process_cmj_video(
|
|
|
161
318
|
landmarks_sequence, video.fps, video.frame_count
|
|
162
319
|
)
|
|
163
320
|
params = auto_tune_parameters(characteristics, quality_preset)
|
|
164
|
-
|
|
165
321
|
params = apply_expert_overrides(
|
|
166
322
|
params,
|
|
167
|
-
smoothing_window,
|
|
168
|
-
velocity_threshold,
|
|
169
|
-
min_contact_frames,
|
|
170
|
-
visibility_threshold,
|
|
323
|
+
overrides.smoothing_window if overrides else None,
|
|
324
|
+
overrides.velocity_threshold if overrides else None,
|
|
325
|
+
overrides.min_contact_frames if overrides else None,
|
|
326
|
+
overrides.visibility_threshold if overrides else None,
|
|
171
327
|
)
|
|
172
328
|
|
|
173
329
|
if verbose:
|
|
@@ -185,13 +341,10 @@ def process_cmj_video(
|
|
|
185
341
|
vertical_positions, visibilities = extract_vertical_positions(
|
|
186
342
|
smoothed_landmarks, target="hip"
|
|
187
343
|
)
|
|
188
|
-
|
|
189
344
|
foot_positions, _ = extract_vertical_positions(
|
|
190
345
|
smoothed_landmarks, target="foot"
|
|
191
346
|
)
|
|
192
347
|
|
|
193
|
-
tracking_method = "hip_hybrid"
|
|
194
|
-
|
|
195
348
|
if verbose:
|
|
196
349
|
print("Detecting CMJ phases...")
|
|
197
350
|
with timer.measure("phase_detection"):
|
|
@@ -217,7 +370,6 @@ def process_cmj_video(
|
|
|
217
370
|
window_length=params.smoothing_window,
|
|
218
371
|
polyorder=params.polyorder,
|
|
219
372
|
)
|
|
220
|
-
|
|
221
373
|
metrics = calculate_cmj_metrics(
|
|
222
374
|
vertical_positions,
|
|
223
375
|
velocities,
|
|
@@ -226,7 +378,7 @@ def process_cmj_video(
|
|
|
226
378
|
takeoff_frame,
|
|
227
379
|
landing_frame,
|
|
228
380
|
video.fps,
|
|
229
|
-
tracking_method=
|
|
381
|
+
tracking_method="hip_hybrid",
|
|
230
382
|
)
|
|
231
383
|
|
|
232
384
|
if verbose:
|
|
@@ -238,137 +390,49 @@ def process_cmj_video(
|
|
|
238
390
|
use_median=True,
|
|
239
391
|
interpolate=False,
|
|
240
392
|
)
|
|
241
|
-
|
|
242
|
-
phases_detected = True
|
|
243
|
-
phase_count = 4
|
|
244
|
-
|
|
245
393
|
quality_result = assess_jump_quality(
|
|
246
394
|
visibilities=visibilities,
|
|
247
395
|
positions=vertical_positions,
|
|
248
396
|
outlier_mask=outlier_mask,
|
|
249
397
|
fps=video.fps,
|
|
250
|
-
phases_detected=
|
|
251
|
-
phase_count=
|
|
398
|
+
phases_detected=True,
|
|
399
|
+
phase_count=4,
|
|
252
400
|
)
|
|
253
401
|
|
|
254
|
-
|
|
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()
|
|
402
|
+
_print_quality_warnings(quality_result, verbose)
|
|
288
403
|
|
|
289
404
|
if output_video:
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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}")
|
|
405
|
+
_generate_debug_video(
|
|
406
|
+
output_video,
|
|
407
|
+
frames,
|
|
408
|
+
frame_indices,
|
|
409
|
+
smoothed_landmarks,
|
|
410
|
+
metrics,
|
|
411
|
+
video.fps,
|
|
412
|
+
timer,
|
|
413
|
+
verbose,
|
|
414
|
+
)
|
|
315
415
|
|
|
316
416
|
with timer.measure("metrics_validation"):
|
|
317
417
|
validator = CMJMetricsValidator()
|
|
318
418
|
validation_result = validator.validate(metrics.to_dict()) # type: ignore[arg-type]
|
|
319
419
|
metrics.validation_result = validation_result
|
|
320
420
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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,
|
|
421
|
+
algorithm_config = _create_algorithm_config(params)
|
|
422
|
+
video_info = _create_video_info(video_path, video)
|
|
423
|
+
processing_info = _create_processing_info(start_time, quality_preset, timer)
|
|
424
|
+
result_metadata = _create_result_metadata(
|
|
425
|
+
quality_result, video_info, processing_info, algorithm_config
|
|
337
426
|
)
|
|
338
|
-
|
|
339
427
|
metrics.result_metadata = result_metadata
|
|
340
428
|
|
|
341
429
|
if json_output:
|
|
342
|
-
|
|
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}")
|
|
430
|
+
_save_metrics_to_json(metrics, json_output, timer, verbose)
|
|
350
431
|
|
|
351
|
-
|
|
352
|
-
print("\n⚠️ Validation Results:")
|
|
353
|
-
for issue in validation_result.issues:
|
|
354
|
-
print(f" [{issue.severity.value}] {issue.metric}: {issue.message}")
|
|
432
|
+
_print_validation_results(validation_result, verbose)
|
|
355
433
|
|
|
356
434
|
if verbose:
|
|
357
|
-
|
|
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")
|
|
435
|
+
_print_timing_summary(start_time, timer, metrics)
|
|
372
436
|
|
|
373
437
|
return metrics
|
|
374
438
|
|
|
@@ -404,10 +468,7 @@ def _process_cmj_video_wrapper(config: CMJVideoConfig) -> CMJVideoResult:
|
|
|
404
468
|
quality=config.quality,
|
|
405
469
|
output_video=config.output_video,
|
|
406
470
|
json_output=config.json_output,
|
|
407
|
-
|
|
408
|
-
velocity_threshold=config.velocity_threshold,
|
|
409
|
-
min_contact_frames=config.min_contact_frames,
|
|
410
|
-
visibility_threshold=config.visibility_threshold,
|
|
471
|
+
overrides=config.overrides,
|
|
411
472
|
detection_confidence=config.detection_confidence,
|
|
412
473
|
tracking_confidence=config.tracking_confidence,
|
|
413
474
|
verbose=False,
|
kinemotion/cmj/cli.py
CHANGED
|
@@ -12,7 +12,7 @@ from ..core.cli_utils import (
|
|
|
12
12
|
common_output_options,
|
|
13
13
|
generate_batch_output_paths,
|
|
14
14
|
)
|
|
15
|
-
from .api import process_cmj_video
|
|
15
|
+
from .api import AnalysisOverrides, process_cmj_video
|
|
16
16
|
from .kinematics import CMJMetrics
|
|
17
17
|
|
|
18
18
|
|
|
@@ -260,16 +260,21 @@ def _process_single(
|
|
|
260
260
|
) -> None:
|
|
261
261
|
"""Process a single CMJ video by calling the API."""
|
|
262
262
|
try:
|
|
263
|
+
# Create overrides from expert parameters
|
|
264
|
+
overrides = AnalysisOverrides(
|
|
265
|
+
smoothing_window=expert_params.smoothing_window,
|
|
266
|
+
velocity_threshold=expert_params.velocity_threshold,
|
|
267
|
+
min_contact_frames=expert_params.min_contact_frames,
|
|
268
|
+
visibility_threshold=expert_params.visibility_threshold,
|
|
269
|
+
)
|
|
270
|
+
|
|
263
271
|
# Call the API function (handles all processing logic)
|
|
264
272
|
metrics = process_cmj_video(
|
|
265
273
|
video_path=video_path,
|
|
266
274
|
quality=quality_preset.value,
|
|
267
275
|
output_video=output,
|
|
268
276
|
json_output=json_output,
|
|
269
|
-
|
|
270
|
-
velocity_threshold=expert_params.velocity_threshold,
|
|
271
|
-
min_contact_frames=expert_params.min_contact_frames,
|
|
272
|
-
visibility_threshold=expert_params.visibility_threshold,
|
|
277
|
+
overrides=overrides,
|
|
273
278
|
detection_confidence=expert_params.detection_confidence,
|
|
274
279
|
tracking_confidence=expert_params.tracking_confidence,
|
|
275
280
|
verbose=verbose,
|
kinemotion/dropjump/api.py
CHANGED
|
@@ -13,6 +13,7 @@ if TYPE_CHECKING:
|
|
|
13
13
|
from ..core.auto_tuning import (
|
|
14
14
|
AnalysisParameters,
|
|
15
15
|
QualityPreset,
|
|
16
|
+
VideoCharacteristics,
|
|
16
17
|
analyze_video_sample,
|
|
17
18
|
auto_tune_parameters,
|
|
18
19
|
)
|
|
@@ -52,6 +53,20 @@ from .kinematics import DropJumpMetrics, calculate_drop_jump_metrics
|
|
|
52
53
|
from .metrics_validator import DropJumpMetricsValidator
|
|
53
54
|
|
|
54
55
|
|
|
56
|
+
@dataclass
|
|
57
|
+
class AnalysisOverrides:
|
|
58
|
+
"""Optional overrides for analysis parameters.
|
|
59
|
+
|
|
60
|
+
Allows fine-tuning of specific analysis parameters beyond quality presets.
|
|
61
|
+
If None, values will be determined by the quality preset.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
smoothing_window: int | None = None
|
|
65
|
+
velocity_threshold: float | None = None
|
|
66
|
+
min_contact_frames: int | None = None
|
|
67
|
+
visibility_threshold: float | None = None
|
|
68
|
+
|
|
69
|
+
|
|
55
70
|
@dataclass
|
|
56
71
|
class DropJumpVideoResult:
|
|
57
72
|
"""Result of processing a single drop jump video."""
|
|
@@ -72,10 +87,7 @@ class DropJumpVideoConfig:
|
|
|
72
87
|
output_video: str | None = None
|
|
73
88
|
json_output: str | None = None
|
|
74
89
|
drop_start_frame: int | None = None
|
|
75
|
-
|
|
76
|
-
velocity_threshold: float | None = None
|
|
77
|
-
min_contact_frames: int | None = None
|
|
78
|
-
visibility_threshold: float | None = None
|
|
90
|
+
overrides: AnalysisOverrides | None = None
|
|
79
91
|
detection_confidence: float | None = None
|
|
80
92
|
tracking_confidence: float | None = None
|
|
81
93
|
|
|
@@ -219,6 +231,195 @@ def _print_dropjump_summary(
|
|
|
219
231
|
print("Analysis complete!")
|
|
220
232
|
|
|
221
233
|
|
|
234
|
+
def _setup_pose_tracker(
|
|
235
|
+
quality_preset: QualityPreset,
|
|
236
|
+
detection_confidence: float | None,
|
|
237
|
+
tracking_confidence: float | None,
|
|
238
|
+
pose_tracker: "PoseTracker | None",
|
|
239
|
+
timer: Timer,
|
|
240
|
+
) -> tuple["PoseTracker", bool]:
|
|
241
|
+
"""Set up pose tracker and determine if it should be closed."""
|
|
242
|
+
detection_conf, tracking_conf = determine_confidence_levels(
|
|
243
|
+
quality_preset, detection_confidence, tracking_confidence
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
tracker = pose_tracker
|
|
247
|
+
should_close_tracker = False
|
|
248
|
+
|
|
249
|
+
if tracker is None:
|
|
250
|
+
tracker = PoseTracker(
|
|
251
|
+
min_detection_confidence=detection_conf,
|
|
252
|
+
min_tracking_confidence=tracking_conf,
|
|
253
|
+
timer=timer,
|
|
254
|
+
)
|
|
255
|
+
should_close_tracker = True
|
|
256
|
+
|
|
257
|
+
return tracker, should_close_tracker
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _process_frames_and_landmarks(
|
|
261
|
+
video: "VideoProcessor",
|
|
262
|
+
tracker: "PoseTracker",
|
|
263
|
+
should_close_tracker: bool,
|
|
264
|
+
verbose: bool,
|
|
265
|
+
timer: Timer,
|
|
266
|
+
) -> tuple[list, list, list[int]]:
|
|
267
|
+
"""Process all video frames and extract landmarks."""
|
|
268
|
+
if verbose:
|
|
269
|
+
print("Processing all frames with MediaPipe pose tracking...")
|
|
270
|
+
|
|
271
|
+
frames, landmarks_sequence, frame_indices = process_all_frames(
|
|
272
|
+
video, tracker, verbose, timer, close_tracker=should_close_tracker
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
return frames, landmarks_sequence, frame_indices
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _tune_and_smooth(
|
|
279
|
+
landmarks_sequence: list,
|
|
280
|
+
video_fps: float,
|
|
281
|
+
frame_count: int,
|
|
282
|
+
quality_preset: QualityPreset,
|
|
283
|
+
overrides: AnalysisOverrides | None,
|
|
284
|
+
timer: Timer,
|
|
285
|
+
verbose: bool,
|
|
286
|
+
) -> tuple[list, AnalysisParameters, VideoCharacteristics]:
|
|
287
|
+
"""Tune parameters and apply smoothing to landmarks.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
landmarks_sequence: Sequence of pose landmarks
|
|
291
|
+
video_fps: Video frame rate
|
|
292
|
+
frame_count: Total number of frames
|
|
293
|
+
quality_preset: Quality preset for analysis
|
|
294
|
+
overrides: Optional parameter overrides
|
|
295
|
+
timer: Performance timer
|
|
296
|
+
verbose: Verbose output flag
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
Tuple of (smoothed_landmarks, params, characteristics)
|
|
300
|
+
"""
|
|
301
|
+
with timer.measure("parameter_auto_tuning"):
|
|
302
|
+
characteristics = analyze_video_sample(
|
|
303
|
+
landmarks_sequence, video_fps, frame_count
|
|
304
|
+
)
|
|
305
|
+
params = auto_tune_parameters(characteristics, quality_preset)
|
|
306
|
+
|
|
307
|
+
# Apply overrides if provided
|
|
308
|
+
if overrides:
|
|
309
|
+
params = apply_expert_overrides(
|
|
310
|
+
params,
|
|
311
|
+
overrides.smoothing_window,
|
|
312
|
+
overrides.velocity_threshold,
|
|
313
|
+
overrides.min_contact_frames,
|
|
314
|
+
overrides.visibility_threshold,
|
|
315
|
+
)
|
|
316
|
+
else:
|
|
317
|
+
params = apply_expert_overrides(
|
|
318
|
+
params,
|
|
319
|
+
None,
|
|
320
|
+
None,
|
|
321
|
+
None,
|
|
322
|
+
None,
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
smoothed_landmarks = apply_smoothing(landmarks_sequence, params, verbose, timer)
|
|
326
|
+
|
|
327
|
+
return smoothed_landmarks, params, characteristics
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _extract_positions_and_detect_contact(
|
|
331
|
+
smoothed_landmarks: list,
|
|
332
|
+
params: AnalysisParameters,
|
|
333
|
+
timer: Timer,
|
|
334
|
+
verbose: bool,
|
|
335
|
+
) -> tuple["NDArray", "NDArray", list]:
|
|
336
|
+
"""Extract vertical positions and detect ground contact."""
|
|
337
|
+
if verbose:
|
|
338
|
+
print("Extracting foot positions...")
|
|
339
|
+
with timer.measure("vertical_position_extraction"):
|
|
340
|
+
vertical_positions, visibilities = extract_vertical_positions(
|
|
341
|
+
smoothed_landmarks
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
if verbose:
|
|
345
|
+
print("Detecting ground contact...")
|
|
346
|
+
with timer.measure("ground_contact_detection"):
|
|
347
|
+
contact_states = detect_ground_contact(
|
|
348
|
+
vertical_positions,
|
|
349
|
+
velocity_threshold=params.velocity_threshold,
|
|
350
|
+
min_contact_frames=params.min_contact_frames,
|
|
351
|
+
visibility_threshold=params.visibility_threshold,
|
|
352
|
+
visibilities=visibilities,
|
|
353
|
+
window_length=params.smoothing_window,
|
|
354
|
+
polyorder=params.polyorder,
|
|
355
|
+
timer=timer,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
return vertical_positions, visibilities, contact_states
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _calculate_metrics_and_assess_quality(
|
|
362
|
+
contact_states: list,
|
|
363
|
+
vertical_positions: "NDArray",
|
|
364
|
+
visibilities: "NDArray",
|
|
365
|
+
video_fps: float,
|
|
366
|
+
drop_start_frame: int | None,
|
|
367
|
+
params: AnalysisParameters,
|
|
368
|
+
timer: Timer,
|
|
369
|
+
verbose: bool,
|
|
370
|
+
) -> tuple[DropJumpMetrics, QualityAssessment]:
|
|
371
|
+
"""Calculate metrics and assess quality."""
|
|
372
|
+
if verbose:
|
|
373
|
+
print("Calculating metrics...")
|
|
374
|
+
with timer.measure("metrics_calculation"):
|
|
375
|
+
metrics = calculate_drop_jump_metrics(
|
|
376
|
+
contact_states,
|
|
377
|
+
vertical_positions,
|
|
378
|
+
video_fps,
|
|
379
|
+
drop_start_frame=drop_start_frame,
|
|
380
|
+
velocity_threshold=params.velocity_threshold,
|
|
381
|
+
smoothing_window=params.smoothing_window,
|
|
382
|
+
polyorder=params.polyorder,
|
|
383
|
+
use_curvature=params.use_curvature,
|
|
384
|
+
timer=timer,
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
if verbose:
|
|
388
|
+
print("Assessing tracking quality...")
|
|
389
|
+
with timer.measure("quality_assessment"):
|
|
390
|
+
quality_result, _, _, _ = _assess_dropjump_quality(
|
|
391
|
+
vertical_positions, visibilities, contact_states, video_fps
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
return metrics, quality_result
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _print_quality_warnings(quality_result: QualityAssessment, verbose: bool) -> None:
|
|
398
|
+
"""Print quality warnings if present."""
|
|
399
|
+
if verbose and quality_result.warnings:
|
|
400
|
+
print("\n⚠️ Quality Warnings:")
|
|
401
|
+
for warning in quality_result.warnings:
|
|
402
|
+
print(f" - {warning}")
|
|
403
|
+
print()
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _validate_metrics_and_print_results(
|
|
407
|
+
metrics: DropJumpMetrics,
|
|
408
|
+
timer: Timer,
|
|
409
|
+
verbose: bool,
|
|
410
|
+
) -> None:
|
|
411
|
+
"""Validate metrics and print validation results if verbose."""
|
|
412
|
+
with timer.measure("metrics_validation"):
|
|
413
|
+
validator = DropJumpMetricsValidator()
|
|
414
|
+
validation_result = validator.validate(metrics.to_dict()) # type: ignore[arg-type]
|
|
415
|
+
metrics.validation_result = validation_result
|
|
416
|
+
|
|
417
|
+
if verbose and validation_result.issues:
|
|
418
|
+
print("\n⚠️ Validation Results:")
|
|
419
|
+
for issue in validation_result.issues:
|
|
420
|
+
print(f" [{issue.severity.value}] {issue.metric}: {issue.message}")
|
|
421
|
+
|
|
422
|
+
|
|
222
423
|
def _generate_debug_video(
|
|
223
424
|
output_video: str,
|
|
224
425
|
frames: list,
|
|
@@ -285,10 +486,7 @@ def process_dropjump_video(
|
|
|
285
486
|
output_video: str | None = None,
|
|
286
487
|
json_output: str | None = None,
|
|
287
488
|
drop_start_frame: int | None = None,
|
|
288
|
-
|
|
289
|
-
velocity_threshold: float | None = None,
|
|
290
|
-
min_contact_frames: int | None = None,
|
|
291
|
-
visibility_threshold: float | None = None,
|
|
489
|
+
overrides: AnalysisOverrides | None = None,
|
|
292
490
|
detection_confidence: float | None = None,
|
|
293
491
|
tracking_confidence: float | None = None,
|
|
294
492
|
verbose: bool = False,
|
|
@@ -306,10 +504,7 @@ def process_dropjump_video(
|
|
|
306
504
|
output_video: Optional path for debug video output
|
|
307
505
|
json_output: Optional path for JSON metrics output
|
|
308
506
|
drop_start_frame: Optional manual drop start frame
|
|
309
|
-
|
|
310
|
-
velocity_threshold: Optional override for velocity threshold
|
|
311
|
-
min_contact_frames: Optional override for minimum contact frames
|
|
312
|
-
visibility_threshold: Optional override for visibility threshold
|
|
507
|
+
overrides: Optional AnalysisOverrides for fine-tuning parameters
|
|
313
508
|
detection_confidence: Optional override for pose detection confidence
|
|
314
509
|
tracking_confidence: Optional override for pose tracking confidence
|
|
315
510
|
verbose: Print processing details
|
|
@@ -331,106 +526,54 @@ def process_dropjump_video(
|
|
|
331
526
|
set_deterministic_mode(seed=42)
|
|
332
527
|
|
|
333
528
|
start_time = time.time()
|
|
334
|
-
|
|
335
|
-
timer = PerformanceTimer()
|
|
336
|
-
|
|
529
|
+
timer = timer or PerformanceTimer()
|
|
337
530
|
quality_preset = parse_quality_preset(quality)
|
|
338
531
|
|
|
339
532
|
with timer.measure("video_initialization"):
|
|
340
533
|
with VideoProcessor(video_path, timer=timer) as video:
|
|
341
|
-
|
|
342
|
-
quality_preset,
|
|
534
|
+
tracker, should_close_tracker = _setup_pose_tracker(
|
|
535
|
+
quality_preset,
|
|
536
|
+
detection_confidence,
|
|
537
|
+
tracking_confidence,
|
|
538
|
+
pose_tracker,
|
|
539
|
+
timer,
|
|
343
540
|
)
|
|
344
541
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
tracker = pose_tracker
|
|
349
|
-
should_close_tracker = False
|
|
350
|
-
|
|
351
|
-
if tracker is None:
|
|
352
|
-
tracker = PoseTracker(
|
|
353
|
-
min_detection_confidence=detection_conf,
|
|
354
|
-
min_tracking_confidence=tracking_conf,
|
|
355
|
-
timer=timer,
|
|
356
|
-
)
|
|
357
|
-
should_close_tracker = True
|
|
358
|
-
|
|
359
|
-
frames, landmarks_sequence, frame_indices = process_all_frames(
|
|
360
|
-
video, tracker, verbose, timer, close_tracker=should_close_tracker
|
|
542
|
+
frames, landmarks_sequence, frame_indices = _process_frames_and_landmarks(
|
|
543
|
+
video, tracker, should_close_tracker, verbose, timer
|
|
361
544
|
)
|
|
362
545
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
smoothing_window,
|
|
372
|
-
velocity_threshold,
|
|
373
|
-
min_contact_frames,
|
|
374
|
-
visibility_threshold,
|
|
375
|
-
)
|
|
376
|
-
|
|
377
|
-
if verbose:
|
|
378
|
-
print_verbose_parameters(
|
|
379
|
-
video, characteristics, quality_preset, params
|
|
380
|
-
)
|
|
381
|
-
|
|
382
|
-
smoothed_landmarks = apply_smoothing(
|
|
383
|
-
landmarks_sequence, params, verbose, timer
|
|
546
|
+
smoothed_landmarks, params, characteristics = _tune_and_smooth(
|
|
547
|
+
landmarks_sequence,
|
|
548
|
+
video.fps,
|
|
549
|
+
video.frame_count,
|
|
550
|
+
quality_preset,
|
|
551
|
+
overrides,
|
|
552
|
+
timer,
|
|
553
|
+
verbose,
|
|
384
554
|
)
|
|
385
555
|
|
|
386
556
|
if verbose:
|
|
387
|
-
|
|
388
|
-
with timer.measure("vertical_position_extraction"):
|
|
389
|
-
vertical_positions, visibilities = extract_vertical_positions(
|
|
390
|
-
smoothed_landmarks
|
|
391
|
-
)
|
|
557
|
+
print_verbose_parameters(video, characteristics, quality_preset, params)
|
|
392
558
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
contact_states = detect_ground_contact(
|
|
397
|
-
vertical_positions,
|
|
398
|
-
velocity_threshold=params.velocity_threshold,
|
|
399
|
-
min_contact_frames=params.min_contact_frames,
|
|
400
|
-
visibility_threshold=params.visibility_threshold,
|
|
401
|
-
visibilities=visibilities,
|
|
402
|
-
window_length=params.smoothing_window,
|
|
403
|
-
polyorder=params.polyorder,
|
|
404
|
-
timer=timer,
|
|
405
|
-
)
|
|
406
|
-
|
|
407
|
-
if verbose:
|
|
408
|
-
print("Calculating metrics...")
|
|
409
|
-
with timer.measure("metrics_calculation"):
|
|
410
|
-
metrics = calculate_drop_jump_metrics(
|
|
411
|
-
contact_states,
|
|
412
|
-
vertical_positions,
|
|
413
|
-
video.fps,
|
|
414
|
-
drop_start_frame=drop_start_frame,
|
|
415
|
-
velocity_threshold=params.velocity_threshold,
|
|
416
|
-
smoothing_window=params.smoothing_window,
|
|
417
|
-
polyorder=params.polyorder,
|
|
418
|
-
use_curvature=params.use_curvature,
|
|
419
|
-
timer=timer,
|
|
559
|
+
vertical_positions, visibilities, contact_states = (
|
|
560
|
+
_extract_positions_and_detect_contact(
|
|
561
|
+
smoothed_landmarks, params, timer, verbose
|
|
420
562
|
)
|
|
563
|
+
)
|
|
421
564
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
565
|
+
metrics, quality_result = _calculate_metrics_and_assess_quality(
|
|
566
|
+
contact_states,
|
|
567
|
+
vertical_positions,
|
|
568
|
+
visibilities,
|
|
569
|
+
video.fps,
|
|
570
|
+
drop_start_frame,
|
|
571
|
+
params,
|
|
572
|
+
timer,
|
|
573
|
+
verbose,
|
|
574
|
+
)
|
|
428
575
|
|
|
429
|
-
|
|
430
|
-
print("\n⚠️ Quality Warnings:")
|
|
431
|
-
for warning in quality_result.warnings:
|
|
432
|
-
print(f" - {warning}")
|
|
433
|
-
print()
|
|
576
|
+
_print_quality_warnings(quality_result, verbose)
|
|
434
577
|
|
|
435
578
|
if output_video:
|
|
436
579
|
_generate_debug_video(
|
|
@@ -445,15 +588,7 @@ def process_dropjump_video(
|
|
|
445
588
|
verbose,
|
|
446
589
|
)
|
|
447
590
|
|
|
448
|
-
|
|
449
|
-
validator = DropJumpMetricsValidator()
|
|
450
|
-
validation_result = validator.validate(metrics.to_dict()) # type: ignore[arg-type]
|
|
451
|
-
metrics.validation_result = validation_result
|
|
452
|
-
|
|
453
|
-
if verbose and validation_result.issues:
|
|
454
|
-
print("\n⚠️ Validation Results:")
|
|
455
|
-
for issue in validation_result.issues:
|
|
456
|
-
print(f" [{issue.severity.value}] {issue.metric}: {issue.message}")
|
|
591
|
+
_validate_metrics_and_print_results(metrics, timer, verbose)
|
|
457
592
|
|
|
458
593
|
processing_time = time.time() - start_time
|
|
459
594
|
result_metadata = _build_dropjump_metadata(
|
|
@@ -512,10 +647,7 @@ def _process_dropjump_video_wrapper(config: DropJumpVideoConfig) -> DropJumpVide
|
|
|
512
647
|
output_video=config.output_video,
|
|
513
648
|
json_output=config.json_output,
|
|
514
649
|
drop_start_frame=config.drop_start_frame,
|
|
515
|
-
|
|
516
|
-
velocity_threshold=config.velocity_threshold,
|
|
517
|
-
min_contact_frames=config.min_contact_frames,
|
|
518
|
-
visibility_threshold=config.visibility_threshold,
|
|
650
|
+
overrides=config.overrides,
|
|
519
651
|
detection_confidence=config.detection_confidence,
|
|
520
652
|
tracking_confidence=config.tracking_confidence,
|
|
521
653
|
verbose=False,
|
kinemotion/dropjump/cli.py
CHANGED
|
@@ -237,6 +237,25 @@ def _process_single(
|
|
|
237
237
|
click.echo(f"Analyzing video: {video_path}", err=True)
|
|
238
238
|
|
|
239
239
|
try:
|
|
240
|
+
# Create AnalysisOverrides if any expert parameters are set
|
|
241
|
+
from .api import AnalysisOverrides
|
|
242
|
+
|
|
243
|
+
overrides = None
|
|
244
|
+
if any(
|
|
245
|
+
[
|
|
246
|
+
expert_params.smoothing_window is not None,
|
|
247
|
+
expert_params.velocity_threshold is not None,
|
|
248
|
+
expert_params.min_contact_frames is not None,
|
|
249
|
+
expert_params.visibility_threshold is not None,
|
|
250
|
+
]
|
|
251
|
+
):
|
|
252
|
+
overrides = AnalysisOverrides(
|
|
253
|
+
smoothing_window=expert_params.smoothing_window,
|
|
254
|
+
velocity_threshold=expert_params.velocity_threshold,
|
|
255
|
+
min_contact_frames=expert_params.min_contact_frames,
|
|
256
|
+
visibility_threshold=expert_params.visibility_threshold,
|
|
257
|
+
)
|
|
258
|
+
|
|
240
259
|
# Call the API function (handles all processing logic)
|
|
241
260
|
metrics = process_dropjump_video(
|
|
242
261
|
video_path=video_path,
|
|
@@ -244,10 +263,7 @@ def _process_single(
|
|
|
244
263
|
output_video=output,
|
|
245
264
|
json_output=json_output,
|
|
246
265
|
drop_start_frame=expert_params.drop_start_frame,
|
|
247
|
-
|
|
248
|
-
velocity_threshold=expert_params.velocity_threshold,
|
|
249
|
-
min_contact_frames=expert_params.min_contact_frames,
|
|
250
|
-
visibility_threshold=expert_params.visibility_threshold,
|
|
266
|
+
overrides=overrides,
|
|
251
267
|
detection_confidence=expert_params.detection_confidence,
|
|
252
268
|
tracking_confidence=expert_params.tracking_confidence,
|
|
253
269
|
verbose=verbose,
|
|
@@ -312,16 +328,32 @@ def _create_video_configs(
|
|
|
312
328
|
video_file, output_dir, json_output_dir
|
|
313
329
|
)
|
|
314
330
|
|
|
331
|
+
# Create AnalysisOverrides if any expert parameters are set
|
|
332
|
+
from .api import AnalysisOverrides
|
|
333
|
+
|
|
334
|
+
overrides = None
|
|
335
|
+
if any(
|
|
336
|
+
[
|
|
337
|
+
expert_params.smoothing_window is not None,
|
|
338
|
+
expert_params.velocity_threshold is not None,
|
|
339
|
+
expert_params.min_contact_frames is not None,
|
|
340
|
+
expert_params.visibility_threshold is not None,
|
|
341
|
+
]
|
|
342
|
+
):
|
|
343
|
+
overrides = AnalysisOverrides(
|
|
344
|
+
smoothing_window=expert_params.smoothing_window,
|
|
345
|
+
velocity_threshold=expert_params.velocity_threshold,
|
|
346
|
+
min_contact_frames=expert_params.min_contact_frames,
|
|
347
|
+
visibility_threshold=expert_params.visibility_threshold,
|
|
348
|
+
)
|
|
349
|
+
|
|
315
350
|
config = DropJumpVideoConfig(
|
|
316
351
|
video_path=video_file,
|
|
317
352
|
quality=quality,
|
|
318
353
|
output_video=debug_video,
|
|
319
354
|
json_output=json_file,
|
|
320
355
|
drop_start_frame=expert_params.drop_start_frame,
|
|
321
|
-
|
|
322
|
-
velocity_threshold=expert_params.velocity_threshold,
|
|
323
|
-
min_contact_frames=expert_params.min_contact_frames,
|
|
324
|
-
visibility_threshold=expert_params.visibility_threshold,
|
|
356
|
+
overrides=overrides,
|
|
325
357
|
detection_confidence=expert_params.detection_confidence,
|
|
326
358
|
tracking_confidence=expert_params.tracking_confidence,
|
|
327
359
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kinemotion
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.48.0
|
|
4
4
|
Summary: Video-based kinematic analysis for athletic performance
|
|
5
5
|
Project-URL: Homepage, https://github.com/feniix/kinemotion
|
|
6
6
|
Project-URL: Repository, https://github.com/feniix/kinemotion
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
kinemotion/__init__.py,sha256=wPItmyGJUOFM6GPRVhAEvRz0-ErI7e2qiUREYJ9EfPQ,943
|
|
2
|
-
kinemotion/api.py,sha256=
|
|
2
|
+
kinemotion/api.py,sha256=q33-C6xlZKzXthjii1FKnschFU_WT60EHabRgczy3ic,1039
|
|
3
3
|
kinemotion/cli.py,sha256=cqYV_7URH0JUDy1VQ_EDLv63FmNO4Ns20m6s1XAjiP4,464
|
|
4
4
|
kinemotion/cmj/__init__.py,sha256=SkAw9ka8Yd1Qfv9hcvk22m3EfucROzYrSNGNF5kDzho,113
|
|
5
5
|
kinemotion/cmj/analysis.py,sha256=3l0vYQB9tN4HtEO2MPFHVtrdzSmXgwpCm03qzYLCF0c,22196
|
|
6
|
-
kinemotion/cmj/api.py,sha256=
|
|
7
|
-
kinemotion/cmj/cli.py,sha256=
|
|
6
|
+
kinemotion/cmj/api.py,sha256=TYWja-Ellfyq_R2ixfvQyCWnPON7CG7IZk8odlLVM8E,16784
|
|
7
|
+
kinemotion/cmj/cli.py,sha256=r3k5LDRXob12PV_6f6XnXOzKXoGn5WfeCMXkxiJ_CYE,10078
|
|
8
8
|
kinemotion/cmj/debug_overlay.py,sha256=fXmWoHhqMLGo4vTtB6Ezs3yLUDOLw63zLIgU2gFlJQU,15892
|
|
9
9
|
kinemotion/cmj/joint_angles.py,sha256=HmheIEiKcQz39cRezk4h-htorOhGNPsqKIR9RsAEKts,9960
|
|
10
10
|
kinemotion/cmj/kinematics.py,sha256=Q-L8M7wG-MJ6EJTq6GO17c8sD5cb0Jg6Hc5vUZr14bA,13673
|
|
@@ -28,15 +28,15 @@ kinemotion/core/validation.py,sha256=LmKfSl4Ayw3DgwKD9IrhsPdzp5ia4drLsHA2UuU1SCM
|
|
|
28
28
|
kinemotion/core/video_io.py,sha256=vCwpWnlW2y29l48dFXokdehQn42w_IQvayxbVTjpXqQ,7863
|
|
29
29
|
kinemotion/dropjump/__init__.py,sha256=tC3H3BrCg8Oj-db-Vrtx4PH_llR1Ppkd5jwaOjhQcLg,862
|
|
30
30
|
kinemotion/dropjump/analysis.py,sha256=p7nnCe7V6vnhQKZVYk--_nhsTvVa_WY-A3zXmyplsew,28211
|
|
31
|
-
kinemotion/dropjump/api.py,sha256=
|
|
32
|
-
kinemotion/dropjump/cli.py,sha256=
|
|
31
|
+
kinemotion/dropjump/api.py,sha256=O8DSTLankRibFH8pf1A9idK0x9-khKpG1h2X5nlg5Ms,20688
|
|
32
|
+
kinemotion/dropjump/cli.py,sha256=Ho80fSOgH8zo2e8dGQA90VXL-mZPVvnpc1ZKtl51vB0,16917
|
|
33
33
|
kinemotion/dropjump/debug_overlay.py,sha256=8XVuDyZ3nuNoCYkxcUWC7wyEoHyBxx77Sb--B1KiYWw,5974
|
|
34
34
|
kinemotion/dropjump/kinematics.py,sha256=PATlGaClutGKJslL-LRIXHmTsvb-xEB8PUIMScU_K4c,19849
|
|
35
35
|
kinemotion/dropjump/metrics_validator.py,sha256=CrTlGup8q2kyPXtA6HNwm7_yq0AsBaDllG7RVZdXmYA,9342
|
|
36
36
|
kinemotion/dropjump/validation_bounds.py,sha256=fyl04ZV7nfvHkL5eob6oEpV9Hxce6aiOWQ9pclLp7AQ,5077
|
|
37
37
|
kinemotion/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
38
|
-
kinemotion-0.
|
|
39
|
-
kinemotion-0.
|
|
40
|
-
kinemotion-0.
|
|
41
|
-
kinemotion-0.
|
|
42
|
-
kinemotion-0.
|
|
38
|
+
kinemotion-0.48.0.dist-info/METADATA,sha256=xLTdYPgI6XtwGbaxYOnwpA0IZKfZkMF0ld48pP-rp7c,26020
|
|
39
|
+
kinemotion-0.48.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
40
|
+
kinemotion-0.48.0.dist-info/entry_points.txt,sha256=zaqnAnjLvcdrk1Qvj5nvXZCZ2gp0prS7it1zTJygcIY,50
|
|
41
|
+
kinemotion-0.48.0.dist-info/licenses/LICENSE,sha256=KZajvqsHw0NoOHOi2q0FZ4NBe9HdV6oey-IPYAtHXfg,1088
|
|
42
|
+
kinemotion-0.48.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|