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 CHANGED
@@ -1,871 +1,38 @@
1
- "Public API for programmatic use of kinemotion analysis."
2
-
3
- import json
4
- import time
5
- from collections.abc import Callable
6
- from dataclasses import dataclass
7
- from pathlib import Path
8
-
9
- from .cmj.analysis import detect_cmj_phases
10
- from .cmj.debug_overlay import CMJDebugOverlayRenderer
11
- from .cmj.kinematics import CMJMetrics, calculate_cmj_metrics
12
- from .cmj.metrics_validator import CMJMetricsValidator
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
- DropDetectionConfig,
22
- ProcessingInfo,
23
- ResultMetadata,
24
- SmoothingConfig,
25
- VideoInfo,
26
- create_timestamp,
27
- get_kinemotion_version,
1
+ """Public API for programmatic use of kinemotion analysis.
2
+
3
+ This module provides a unified interface for both drop jump and CMJ video analysis.
4
+ The actual implementations have been moved to their respective submodules:
5
+ - Drop jump: kinemotion.dropjump.api
6
+ - CMJ: kinemotion.cmj.api
7
+ """
8
+
9
+ # CMJ API
10
+ from .cmj.api import (
11
+ CMJVideoConfig,
12
+ CMJVideoResult,
13
+ process_cmj_video,
14
+ process_cmj_videos_bulk,
28
15
  )
29
- from .core.pipeline_utils import (
30
- apply_expert_overrides,
31
- apply_smoothing,
32
- convert_timer_to_stage_names,
33
- determine_confidence_levels,
34
- extract_vertical_positions,
35
- parse_quality_preset,
36
- print_verbose_parameters,
37
- process_all_frames,
38
- process_videos_bulk_generic,
16
+ from .cmj.kinematics import CMJMetrics
17
+
18
+ # Drop jump API
19
+ from .dropjump.api import (
20
+ DropJumpVideoConfig,
21
+ DropJumpVideoResult,
22
+ process_dropjump_video,
23
+ process_dropjump_videos_bulk,
39
24
  )
40
- from .core.pose import PoseTracker
41
- from .core.quality import assess_jump_quality
42
- from .core.timing import NULL_TIMER, PerformanceTimer, Timer
43
- from .core.video_io import VideoProcessor
44
- from .dropjump.analysis import (
45
- detect_ground_contact,
46
- find_contact_phases,
47
- )
48
- from .dropjump.debug_overlay import DebugOverlayRenderer
49
- from .dropjump.kinematics import DropJumpMetrics, calculate_drop_jump_metrics
50
- from .dropjump.metrics_validator import DropJumpMetricsValidator
51
-
52
-
53
- @dataclass
54
- class DropJumpVideoResult:
55
- """Result of processing a single drop jump video."""
56
-
57
- video_path: str
58
- success: bool
59
- metrics: DropJumpMetrics | None = None
60
- error: str | None = None
61
- processing_time: float = 0.0
62
-
63
-
64
- @dataclass
65
- class DropJumpVideoConfig:
66
- """Configuration for processing a single drop jump video."""
67
-
68
- video_path: str
69
- quality: str = "balanced"
70
- output_video: str | None = None
71
- json_output: str | None = None
72
- drop_start_frame: int | None = None
73
- smoothing_window: int | None = None
74
- velocity_threshold: float | None = None
75
- min_contact_frames: int | None = None
76
- visibility_threshold: float | None = None
77
- detection_confidence: float | None = None
78
- tracking_confidence: float | None = None
79
-
80
-
81
- def _generate_debug_video(
82
- output_video: str,
83
- frames: list,
84
- frame_indices: list[int],
85
- video_fps: float,
86
- smoothed_landmarks: list,
87
- contact_states: list,
88
- metrics: DropJumpMetrics,
89
- timer: Timer | None,
90
- verbose: bool,
91
- ) -> None:
92
- """Generate debug video with overlay."""
93
- if verbose:
94
- print(f"Generating debug video: {output_video}")
95
-
96
- if not frames:
97
- return
98
-
99
- timer = timer or NULL_TIMER
100
- debug_h, debug_w = frames[0].shape[:2]
101
-
102
- if video_fps > 30:
103
- debug_fps = video_fps / (video_fps / 30.0)
104
- else:
105
- debug_fps = video_fps
106
-
107
- if len(frames) < len(smoothed_landmarks):
108
- step = max(1, int(video_fps / 30.0))
109
- debug_fps = video_fps / step
110
-
111
- def _render_frames(renderer: DebugOverlayRenderer) -> None:
112
- for frame, idx in zip(frames, frame_indices, strict=True):
113
- annotated = renderer.render_frame(
114
- frame,
115
- smoothed_landmarks[idx],
116
- contact_states[idx],
117
- idx,
118
- metrics,
119
- use_com=False,
120
- )
121
- renderer.write_frame(annotated)
122
-
123
- renderer_context = DebugOverlayRenderer(
124
- output_video,
125
- debug_w,
126
- debug_h,
127
- debug_w,
128
- debug_h,
129
- debug_fps,
130
- timer=timer,
131
- )
132
-
133
- with timer.measure("debug_video_generation"):
134
- with renderer_context as renderer:
135
- _render_frames(renderer)
136
-
137
- if verbose:
138
- print(f"Debug video saved: {output_video}")
139
-
140
-
141
- def process_dropjump_video(
142
- video_path: str,
143
- quality: str = "balanced",
144
- output_video: str | None = None,
145
- json_output: str | None = None,
146
- drop_start_frame: int | None = None,
147
- smoothing_window: int | None = None,
148
- velocity_threshold: float | None = None,
149
- min_contact_frames: int | None = None,
150
- visibility_threshold: float | None = None,
151
- detection_confidence: float | None = None,
152
- tracking_confidence: float | None = None,
153
- verbose: bool = False,
154
- timer: Timer | None = None,
155
- pose_tracker: "PoseTracker | None" = None,
156
- ) -> DropJumpMetrics:
157
- """
158
- Process a single drop jump video and return metrics.
159
-
160
- Jump height is calculated from flight time using kinematic formula (h = g*t²/8).
161
-
162
- Args:
163
- video_path: Path to the input video file
164
- quality: Analysis quality preset ("fast", "balanced", or "accurate")
165
- output_video: Optional path for debug video output
166
- json_output: Optional path for JSON metrics output
167
- drop_start_frame: Optional manual drop start frame
168
- smoothing_window: Optional override for smoothing window
169
- velocity_threshold: Optional override for velocity threshold
170
- min_contact_frames: Optional override for minimum contact frames
171
- visibility_threshold: Optional override for visibility threshold
172
- detection_confidence: Optional override for pose detection confidence
173
- tracking_confidence: Optional override for pose tracking confidence
174
- verbose: Print processing details
175
- timer: Optional Timer for measuring operations
176
- pose_tracker: Optional pre-initialized PoseTracker instance (reused if provided)
177
-
178
- Returns:
179
- DropJumpMetrics object containing analysis results
180
-
181
- Raises:
182
- ValueError: If video cannot be processed or parameters are invalid
183
- FileNotFoundError: If video file does not exist
184
- """
185
- if not Path(video_path).exists():
186
- raise FileNotFoundError(f"Video file not found: {video_path}")
187
-
188
- from .core.determinism import set_deterministic_mode
189
-
190
- set_deterministic_mode(seed=42)
191
-
192
- start_time = time.time()
193
- if timer is None:
194
- timer = PerformanceTimer()
195
-
196
- quality_preset = parse_quality_preset(quality)
197
-
198
- with timer.measure("video_initialization"):
199
- with VideoProcessor(video_path, timer=timer) as video:
200
- detection_conf, tracking_conf = determine_confidence_levels(
201
- quality_preset, detection_confidence, tracking_confidence
202
- )
203
-
204
- if verbose:
205
- print("Processing all frames with MediaPipe pose tracking...")
206
-
207
- tracker = pose_tracker
208
- should_close_tracker = False
209
-
210
- if tracker is None:
211
- tracker = PoseTracker(
212
- min_detection_confidence=detection_conf,
213
- min_tracking_confidence=tracking_conf,
214
- timer=timer,
215
- )
216
- should_close_tracker = True
217
-
218
- frames, landmarks_sequence, frame_indices = process_all_frames(
219
- video, tracker, verbose, timer, close_tracker=should_close_tracker
220
- )
221
-
222
- with timer.measure("parameter_auto_tuning"):
223
- characteristics = analyze_video_sample(
224
- landmarks_sequence, video.fps, video.frame_count
225
- )
226
- params = auto_tune_parameters(characteristics, quality_preset)
227
-
228
- params = apply_expert_overrides(
229
- params,
230
- smoothing_window,
231
- velocity_threshold,
232
- min_contact_frames,
233
- visibility_threshold,
234
- )
235
-
236
- if verbose:
237
- print_verbose_parameters(
238
- video, characteristics, quality_preset, params
239
- )
240
-
241
- smoothed_landmarks = apply_smoothing(
242
- landmarks_sequence, params, verbose, timer
243
- )
244
-
245
- if verbose:
246
- print("Extracting foot positions...")
247
- with timer.measure("vertical_position_extraction"):
248
- vertical_positions, visibilities = extract_vertical_positions(
249
- smoothed_landmarks
250
- )
251
-
252
- if verbose:
253
- print("Detecting ground contact...")
254
- with timer.measure("ground_contact_detection"):
255
- contact_states = detect_ground_contact(
256
- vertical_positions,
257
- velocity_threshold=params.velocity_threshold,
258
- min_contact_frames=params.min_contact_frames,
259
- visibility_threshold=params.visibility_threshold,
260
- visibilities=visibilities,
261
- window_length=params.smoothing_window,
262
- polyorder=params.polyorder,
263
- timer=timer,
264
- )
265
-
266
- if verbose:
267
- print("Calculating metrics...")
268
- with timer.measure("metrics_calculation"):
269
- metrics = calculate_drop_jump_metrics(
270
- contact_states,
271
- vertical_positions,
272
- video.fps,
273
- drop_start_frame=drop_start_frame,
274
- velocity_threshold=params.velocity_threshold,
275
- smoothing_window=params.smoothing_window,
276
- polyorder=params.polyorder,
277
- use_curvature=params.use_curvature,
278
- timer=timer,
279
- )
280
-
281
- if verbose:
282
- print("Assessing tracking quality...")
283
- with timer.measure("quality_assessment"):
284
- _, outlier_mask = reject_outliers(
285
- vertical_positions,
286
- use_ransac=True,
287
- use_median=True,
288
- interpolate=False,
289
- )
290
-
291
- phases = find_contact_phases(contact_states)
292
- phases_detected = len(phases) > 0
293
- phase_count = len(phases)
294
-
295
- quality_result = assess_jump_quality(
296
- visibilities=visibilities,
297
- positions=vertical_positions,
298
- outlier_mask=outlier_mask,
299
- fps=video.fps,
300
- phases_detected=phases_detected,
301
- phase_count=phase_count,
302
- )
303
-
304
- drop_frame = None
305
- if drop_start_frame is None and metrics.drop_start_frame is not None:
306
- drop_frame = metrics.drop_start_frame
307
- elif drop_start_frame is not None:
308
- drop_frame = drop_start_frame
309
-
310
- algorithm_config = AlgorithmConfig(
311
- detection_method="forward_search",
312
- tracking_method="mediapipe_pose",
313
- model_complexity=1,
314
- smoothing=SmoothingConfig(
315
- window_size=params.smoothing_window,
316
- polynomial_order=params.polyorder,
317
- use_bilateral_filter=params.bilateral_filter,
318
- use_outlier_rejection=params.outlier_rejection,
319
- ),
320
- detection=DetectionConfig(
321
- velocity_threshold=params.velocity_threshold,
322
- min_contact_frames=params.min_contact_frames,
323
- visibility_threshold=params.visibility_threshold,
324
- use_curvature_refinement=params.use_curvature,
325
- ),
326
- drop_detection=DropDetectionConfig(
327
- auto_detect_drop_start=(drop_start_frame is None),
328
- detected_drop_frame=drop_frame,
329
- min_stationary_duration_s=0.5,
330
- ),
331
- )
332
-
333
- video_info = VideoInfo(
334
- source_path=video_path,
335
- fps=video.fps,
336
- width=video.width,
337
- height=video.height,
338
- duration_s=video.frame_count / video.fps,
339
- frame_count=video.frame_count,
340
- codec=video.codec,
341
- )
342
-
343
- if verbose and quality_result.warnings:
344
- print("\n⚠️ Quality Warnings:")
345
- for warning in quality_result.warnings:
346
- print(f" - {warning}")
347
- print()
348
-
349
- if output_video:
350
- _generate_debug_video(
351
- output_video,
352
- frames,
353
- frame_indices,
354
- video.fps,
355
- smoothed_landmarks,
356
- contact_states,
357
- metrics,
358
- timer,
359
- verbose,
360
- )
361
-
362
- with timer.measure("metrics_validation"):
363
- validator = DropJumpMetricsValidator()
364
- validation_result = validator.validate(metrics.to_dict()) # type: ignore[arg-type]
365
- metrics.validation_result = validation_result
366
-
367
- if verbose and validation_result.issues:
368
- print("\n⚠️ Validation Results:")
369
- for issue in validation_result.issues:
370
- print(f" [{issue.severity.value}] {issue.metric}: {issue.message}")
371
-
372
- processing_time = time.time() - start_time
373
- stage_times = convert_timer_to_stage_names(timer.get_metrics())
374
-
375
- processing_info = ProcessingInfo(
376
- version=get_kinemotion_version(),
377
- timestamp=create_timestamp(),
378
- quality_preset=quality_preset.value,
379
- processing_time_s=processing_time,
380
- timing_breakdown=stage_times,
381
- )
382
-
383
- result_metadata = ResultMetadata(
384
- quality=quality_result,
385
- video=video_info,
386
- processing=processing_info,
387
- algorithm=algorithm_config,
388
- )
389
-
390
- metrics.result_metadata = result_metadata
391
-
392
- if json_output:
393
- with timer.measure("json_serialization"):
394
- output_path = Path(json_output)
395
- metrics_dict = metrics.to_dict()
396
- json_str = json.dumps(metrics_dict, indent=2)
397
- output_path.write_text(json_str)
398
-
399
- if verbose:
400
- print(f"Metrics written to: {json_output}")
401
-
402
- if verbose:
403
- total_time = time.time() - start_time
404
- stage_times_verbose = convert_timer_to_stage_names(timer.get_metrics())
405
-
406
- print("\n=== Timing Summary ===")
407
- for stage, duration in stage_times_verbose.items():
408
- percentage = (duration / total_time) * 100
409
- dur_ms = duration * 1000
410
- print(f"{stage:.<40} {dur_ms:>6.0f}ms ({percentage:>5.1f}%)")
411
- total_ms = total_time * 1000
412
- print(f"{('Total'):.>40} {total_ms:>6.0f}ms (100.0%)")
413
- print()
414
- print("Analysis complete!")
415
-
416
- return metrics
417
-
418
-
419
- def process_dropjump_videos_bulk(
420
- configs: list[DropJumpVideoConfig],
421
- max_workers: int = 4,
422
- progress_callback: Callable[[DropJumpVideoResult], None] | None = None,
423
- ) -> list[DropJumpVideoResult]:
424
- """
425
- Process multiple drop jump videos in parallel.
426
- """
427
-
428
- def error_factory(video_path: str, error_msg: str) -> DropJumpVideoResult:
429
- return DropJumpVideoResult(
430
- video_path=video_path, success=False, error=error_msg
431
- )
432
-
433
- return process_videos_bulk_generic(
434
- configs,
435
- _process_dropjump_video_wrapper,
436
- error_factory,
437
- max_workers,
438
- progress_callback,
439
- )
440
-
441
-
442
- def _process_dropjump_video_wrapper(config: DropJumpVideoConfig) -> DropJumpVideoResult:
443
- """Wrapper function for parallel processing."""
444
- start_time = time.time()
445
-
446
- try:
447
- metrics = process_dropjump_video(
448
- video_path=config.video_path,
449
- quality=config.quality,
450
- output_video=config.output_video,
451
- json_output=config.json_output,
452
- drop_start_frame=config.drop_start_frame,
453
- smoothing_window=config.smoothing_window,
454
- velocity_threshold=config.velocity_threshold,
455
- min_contact_frames=config.min_contact_frames,
456
- visibility_threshold=config.visibility_threshold,
457
- detection_confidence=config.detection_confidence,
458
- tracking_confidence=config.tracking_confidence,
459
- verbose=False,
460
- )
461
-
462
- processing_time = time.time() - start_time
463
-
464
- return DropJumpVideoResult(
465
- video_path=config.video_path,
466
- success=True,
467
- metrics=metrics,
468
- processing_time=processing_time,
469
- )
470
-
471
- except Exception as e:
472
- processing_time = time.time() - start_time
473
-
474
- return DropJumpVideoResult(
475
- video_path=config.video_path,
476
- success=False,
477
- error=str(e),
478
- processing_time=processing_time,
479
- )
480
-
481
-
482
- # ========== CMJ Analysis API ==========
483
-
484
-
485
- @dataclass
486
- class CMJVideoConfig:
487
- """Configuration for processing a single CMJ video."""
488
-
489
- video_path: str
490
- quality: str = "balanced"
491
- output_video: str | None = None
492
- json_output: str | None = None
493
- smoothing_window: int | None = None
494
- velocity_threshold: float | None = None
495
- min_contact_frames: int | None = None
496
- visibility_threshold: float | None = None
497
- detection_confidence: float | None = None
498
- tracking_confidence: float | None = None
499
-
500
-
501
- @dataclass
502
- class CMJVideoResult:
503
- """Result of processing a single CMJ video."""
504
-
505
- video_path: str
506
- success: bool
507
- metrics: CMJMetrics | None = None
508
- error: str | None = None
509
- processing_time: float = 0.0
510
-
511
-
512
- def process_cmj_video(
513
- video_path: str,
514
- quality: str = "balanced",
515
- output_video: str | None = None,
516
- json_output: str | None = None,
517
- smoothing_window: int | None = None,
518
- velocity_threshold: float | None = None,
519
- min_contact_frames: int | None = None,
520
- visibility_threshold: float | None = None,
521
- detection_confidence: float | None = None,
522
- tracking_confidence: float | None = None,
523
- verbose: bool = False,
524
- timer: Timer | None = None,
525
- pose_tracker: "PoseTracker | None" = None,
526
- ) -> CMJMetrics:
527
- """
528
- Process a single CMJ video and return metrics.
529
-
530
- CMJ (Counter Movement Jump) is performed at floor level without a drop box.
531
- Athletes start standing, perform a countermovement (eccentric phase), then
532
- jump upward (concentric phase).
533
-
534
- Args:
535
- video_path: Path to the input video file
536
- quality: Analysis quality preset ("fast", "balanced", or "accurate")
537
- output_video: Optional path for debug video output
538
- json_output: Optional path for JSON metrics output
539
- smoothing_window: Optional override for smoothing window
540
- velocity_threshold: Optional override for velocity threshold
541
- min_contact_frames: Optional override for minimum contact frames
542
- visibility_threshold: Optional override for visibility threshold
543
- detection_confidence: Optional override for pose detection confidence
544
- tracking_confidence: Optional override for pose tracking confidence
545
- verbose: Print processing details
546
- timer: Optional Timer for measuring operations
547
- pose_tracker: Optional pre-initialized PoseTracker instance (reused if provided)
548
-
549
- Returns:
550
- CMJMetrics object containing analysis results
551
-
552
- Raises:
553
- ValueError: If video cannot be processed or parameters are invalid
554
- FileNotFoundError: If video file does not exist
555
- """
556
- if not Path(video_path).exists():
557
- raise FileNotFoundError(f"Video file not found: {video_path}")
558
-
559
- start_time = time.time()
560
- if timer is None:
561
- timer = PerformanceTimer()
562
-
563
- quality_preset = parse_quality_preset(quality)
564
-
565
- with timer.measure("video_initialization"):
566
- with VideoProcessor(video_path, timer=timer) as video:
567
- if verbose:
568
- print(
569
- f"Video: {video.width}x{video.height} @ {video.fps:.2f} fps, "
570
- f"{video.frame_count} frames"
571
- )
572
-
573
- det_conf, track_conf = determine_confidence_levels(
574
- quality_preset, detection_confidence, tracking_confidence
575
- )
576
-
577
- if verbose:
578
- print("Processing all frames with MediaPipe pose tracking...")
579
-
580
- tracker = pose_tracker
581
- should_close_tracker = False
582
-
583
- if tracker is None:
584
- tracker = PoseTracker(
585
- min_detection_confidence=det_conf,
586
- min_tracking_confidence=track_conf,
587
- timer=timer,
588
- )
589
- should_close_tracker = True
590
-
591
- frames, landmarks_sequence, frame_indices = process_all_frames(
592
- video, tracker, verbose, timer, close_tracker=should_close_tracker
593
- )
594
-
595
- with timer.measure("parameter_auto_tuning"):
596
- characteristics = analyze_video_sample(
597
- landmarks_sequence, video.fps, video.frame_count
598
- )
599
- params = auto_tune_parameters(characteristics, quality_preset)
600
-
601
- params = apply_expert_overrides(
602
- params,
603
- smoothing_window,
604
- velocity_threshold,
605
- min_contact_frames,
606
- visibility_threshold,
607
- )
608
-
609
- if verbose:
610
- print_verbose_parameters(
611
- video, characteristics, quality_preset, params
612
- )
613
-
614
- smoothed_landmarks = apply_smoothing(
615
- landmarks_sequence, params, verbose, timer
616
- )
617
-
618
- if verbose:
619
- print("Extracting vertical positions (Hip and Foot)...")
620
- with timer.measure("vertical_position_extraction"):
621
- vertical_positions, visibilities = extract_vertical_positions(
622
- smoothed_landmarks, target="hip"
623
- )
624
-
625
- foot_positions, _ = extract_vertical_positions(
626
- smoothed_landmarks, target="foot"
627
- )
628
-
629
- tracking_method = "hip_hybrid"
630
-
631
- if verbose:
632
- print("Detecting CMJ phases...")
633
- with timer.measure("phase_detection"):
634
- phases = detect_cmj_phases(
635
- vertical_positions,
636
- video.fps,
637
- window_length=params.smoothing_window,
638
- polyorder=params.polyorder,
639
- landing_positions=foot_positions,
640
- timer=timer,
641
- )
642
-
643
- if phases is None:
644
- raise ValueError("Could not detect CMJ phases in video")
645
-
646
- standing_end, lowest_point, takeoff_frame, landing_frame = phases
647
-
648
- if verbose:
649
- print("Calculating metrics...")
650
- with timer.measure("metrics_calculation"):
651
- from .cmj.analysis import compute_signed_velocity
652
-
653
- velocities = compute_signed_velocity(
654
- vertical_positions,
655
- window_length=params.smoothing_window,
656
- polyorder=params.polyorder,
657
- )
658
-
659
- metrics = calculate_cmj_metrics(
660
- vertical_positions,
661
- velocities,
662
- standing_end,
663
- lowest_point,
664
- takeoff_frame,
665
- landing_frame,
666
- video.fps,
667
- tracking_method=tracking_method,
668
- )
669
-
670
- if verbose:
671
- print("Assessing tracking quality...")
672
- with timer.measure("quality_assessment"):
673
- _, outlier_mask = reject_outliers(
674
- vertical_positions,
675
- use_ransac=True,
676
- use_median=True,
677
- interpolate=False,
678
- )
679
-
680
- phases_detected = True
681
- phase_count = 4
682
-
683
- quality_result = assess_jump_quality(
684
- visibilities=visibilities,
685
- positions=vertical_positions,
686
- outlier_mask=outlier_mask,
687
- fps=video.fps,
688
- phases_detected=phases_detected,
689
- phase_count=phase_count,
690
- )
691
-
692
- algorithm_config = AlgorithmConfig(
693
- detection_method="backward_search",
694
- tracking_method="mediapipe_pose",
695
- model_complexity=1,
696
- smoothing=SmoothingConfig(
697
- window_size=params.smoothing_window,
698
- polynomial_order=params.polyorder,
699
- use_bilateral_filter=params.bilateral_filter,
700
- use_outlier_rejection=params.outlier_rejection,
701
- ),
702
- detection=DetectionConfig(
703
- velocity_threshold=params.velocity_threshold,
704
- min_contact_frames=params.min_contact_frames,
705
- visibility_threshold=params.visibility_threshold,
706
- use_curvature_refinement=params.use_curvature,
707
- ),
708
- drop_detection=None,
709
- )
710
-
711
- video_info = VideoInfo(
712
- source_path=video_path,
713
- fps=video.fps,
714
- width=video.width,
715
- height=video.height,
716
- duration_s=video.frame_count / video.fps,
717
- frame_count=video.frame_count,
718
- codec=video.codec,
719
- )
720
-
721
- if verbose and quality_result.warnings:
722
- print("\n⚠️ Quality Warnings:")
723
- for warning in quality_result.warnings:
724
- print(f" - {warning}")
725
- print()
726
-
727
- if output_video:
728
- if verbose:
729
- print(f"Generating debug video: {output_video}")
730
-
731
- debug_h, debug_w = frames[0].shape[:2]
732
- step = max(1, int(video.fps / 30.0))
733
- debug_fps = video.fps / step
734
-
735
- with timer.measure("debug_video_generation"):
736
- with CMJDebugOverlayRenderer(
737
- output_video,
738
- debug_w,
739
- debug_h,
740
- debug_w,
741
- debug_h,
742
- debug_fps,
743
- timer=timer,
744
- ) as renderer:
745
- for frame, idx in zip(frames, frame_indices, strict=True):
746
- annotated = renderer.render_frame(
747
- frame, smoothed_landmarks[idx], idx, metrics
748
- )
749
- renderer.write_frame(annotated)
750
-
751
- if verbose:
752
- print(f"Debug video saved: {output_video}")
753
-
754
- with timer.measure("metrics_validation"):
755
- validator = CMJMetricsValidator()
756
- validation_result = validator.validate(metrics.to_dict()) # type: ignore[arg-type]
757
- metrics.validation_result = validation_result
758
-
759
- processing_time = time.time() - start_time
760
- stage_times = convert_timer_to_stage_names(timer.get_metrics())
761
-
762
- processing_info = ProcessingInfo(
763
- version=get_kinemotion_version(),
764
- timestamp=create_timestamp(),
765
- quality_preset=quality_preset.value,
766
- processing_time_s=processing_time,
767
- timing_breakdown=stage_times,
768
- )
769
-
770
- result_metadata = ResultMetadata(
771
- quality=quality_result,
772
- video=video_info,
773
- processing=processing_info,
774
- algorithm=algorithm_config,
775
- )
776
-
777
- metrics.result_metadata = result_metadata
778
-
779
- if json_output:
780
- with timer.measure("json_serialization"):
781
- output_path = Path(json_output)
782
- metrics_dict = metrics.to_dict()
783
- json_str = json.dumps(metrics_dict, indent=2)
784
- output_path.write_text(json_str)
785
-
786
- if verbose:
787
- print(f"Metrics written to: {json_output}")
788
-
789
- if verbose and validation_result.issues:
790
- print("\n⚠️ Validation Results:")
791
- for issue in validation_result.issues:
792
- print(f" [{issue.severity.value}] {issue.metric}: {issue.message}")
793
-
794
- if verbose:
795
- total_time = time.time() - start_time
796
- stage_times = convert_timer_to_stage_names(timer.get_metrics())
797
-
798
- print("\n=== Timing Summary ===")
799
- for stage, duration in stage_times.items():
800
- percentage = (duration / total_time) * 100
801
- dur_ms = duration * 1000
802
- print(f"{stage:. <40} {dur_ms:>6.0f}ms ({percentage:>5.1f}%)")
803
- total_ms = total_time * 1000
804
- print(f"{('Total'):.>40} {total_ms:>6.0f}ms (100.0%)")
805
- print()
806
-
807
- print(f"\nJump height: {metrics.jump_height:.3f}m")
808
- print(f"Flight time: {metrics.flight_time * 1000:.1f}ms")
809
- print(f"Countermovement depth: {metrics.countermovement_depth:.3f}m")
810
-
811
- return metrics
812
-
813
-
814
- def process_cmj_videos_bulk(
815
- configs: list[CMJVideoConfig],
816
- max_workers: int = 4,
817
- progress_callback: Callable[[CMJVideoResult], None] | None = None,
818
- ) -> list[CMJVideoResult]:
819
- """
820
- Process multiple CMJ videos in parallel.
821
- """
822
-
823
- def error_factory(video_path: str, error_msg: str) -> CMJVideoResult:
824
- return CMJVideoResult(video_path=video_path, success=False, error=error_msg)
825
-
826
- return process_videos_bulk_generic(
827
- configs,
828
- _process_cmj_video_wrapper,
829
- error_factory,
830
- max_workers,
831
- progress_callback,
832
- )
833
-
834
-
835
- def _process_cmj_video_wrapper(config: CMJVideoConfig) -> CMJVideoResult:
836
- """Wrapper function for parallel CMJ processing."""
837
- start_time = time.time()
838
-
839
- try:
840
- metrics = process_cmj_video(
841
- video_path=config.video_path,
842
- quality=config.quality,
843
- output_video=config.output_video,
844
- json_output=config.json_output,
845
- smoothing_window=config.smoothing_window,
846
- velocity_threshold=config.velocity_threshold,
847
- min_contact_frames=config.min_contact_frames,
848
- visibility_threshold=config.visibility_threshold,
849
- detection_confidence=config.detection_confidence,
850
- tracking_confidence=config.tracking_confidence,
851
- verbose=False,
852
- )
853
-
854
- processing_time = time.time() - start_time
855
-
856
- return CMJVideoResult(
857
- video_path=config.video_path,
858
- success=True,
859
- metrics=metrics,
860
- processing_time=processing_time,
861
- )
862
-
863
- except Exception as e:
864
- processing_time = time.time() - start_time
865
25
 
866
- return CMJVideoResult(
867
- video_path=config.video_path,
868
- success=False,
869
- error=str(e),
870
- processing_time=processing_time,
871
- )
26
+ __all__ = [
27
+ # Drop jump
28
+ "DropJumpVideoConfig",
29
+ "DropJumpVideoResult",
30
+ "process_dropjump_video",
31
+ "process_dropjump_videos_bulk",
32
+ # CMJ
33
+ "CMJMetrics",
34
+ "CMJVideoConfig",
35
+ "CMJVideoResult",
36
+ "process_cmj_video",
37
+ "process_cmj_videos_bulk",
38
+ ]