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 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
- if TYPE_CHECKING:
11
- pass
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
- smoothing_window: int | None = None
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
- smoothing_window: int | None = None,
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: "PoseTracker | None" = None,
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
- 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
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
- if timer is None:
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
- 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
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=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=phases_detected,
251
- phase_count=phase_count,
398
+ phases_detected=True,
399
+ phase_count=4,
252
400
  )
253
401
 
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()
402
+ _print_quality_warnings(quality_result, verbose)
288
403
 
289
404
  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}")
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
- 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,
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
- 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}")
430
+ _save_metrics_to_json(metrics, json_output, timer, verbose)
350
431
 
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}")
432
+ _print_validation_results(validation_result, verbose)
355
433
 
356
434
  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")
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
- 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,
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
- smoothing_window=expert_params.smoothing_window,
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,
@@ -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
- smoothing_window: int | None = None
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
- smoothing_window: int | None = None,
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
- smoothing_window: Optional override for smoothing window
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
- if timer is None:
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
- detection_conf, tracking_conf = determine_confidence_levels(
342
- quality_preset, detection_confidence, tracking_confidence
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
- if verbose:
346
- print("Processing all frames with MediaPipe pose tracking...")
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
- with timer.measure("parameter_auto_tuning"):
364
- characteristics = analyze_video_sample(
365
- landmarks_sequence, video.fps, video.frame_count
366
- )
367
- params = auto_tune_parameters(characteristics, quality_preset)
368
-
369
- params = apply_expert_overrides(
370
- params,
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
- print("Extracting foot positions...")
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
- if verbose:
394
- print("Detecting ground contact...")
395
- with timer.measure("ground_contact_detection"):
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
- if verbose:
423
- print("Assessing tracking quality...")
424
- with timer.measure("quality_assessment"):
425
- quality_result, _, _, _ = _assess_dropjump_quality(
426
- vertical_positions, visibilities, contact_states, video.fps
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
- if verbose and quality_result.warnings:
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
- with timer.measure("metrics_validation"):
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
- smoothing_window=config.smoothing_window,
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,
@@ -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
- smoothing_window=expert_params.smoothing_window,
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
- smoothing_window=expert_params.smoothing_window,
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.47.3
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=uj3py8jXuG3mYnmsZQnzuCQtWrO4O6gvZzGAMfZne4o,891
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=jFHBYgk05pZUG3FKAeMu-40DGROfRzXOeLQMrG4KUJk,15527
7
- kinemotion/cmj/cli.py,sha256=S4-3YmaCjtGutDwjG475h8nIiw5utiLg5L6hCGfLOHY,9926
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=9tetwjoFdY7Z8PqXpNfaS96L9YVqEkJl7jejGnewhbE,17517
32
- kinemotion/dropjump/cli.py,sha256=pPQkjpuPUUefGcsRuMvRTtjsxpPSqSgQ9K49rsN_X_o,15823
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.47.3.dist-info/METADATA,sha256=LNbw4aGD3V6P3wkgH_Puury1B9D4kmwOOqKmkiCTo0Y,26020
39
- kinemotion-0.47.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
40
- kinemotion-0.47.3.dist-info/entry_points.txt,sha256=zaqnAnjLvcdrk1Qvj5nvXZCZ2gp0prS7it1zTJygcIY,50
41
- kinemotion-0.47.3.dist-info/licenses/LICENSE,sha256=KZajvqsHw0NoOHOi2q0FZ4NBe9HdV6oey-IPYAtHXfg,1088
42
- kinemotion-0.47.3.dist-info/RECORD,,
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,,