kinemotion 0.9.0__tar.gz → 0.10.0__tar.gz

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.

Files changed (54) hide show
  1. {kinemotion-0.9.0 → kinemotion-0.10.0}/.gitignore +3 -0
  2. {kinemotion-0.9.0 → kinemotion-0.10.0}/CHANGELOG.md +8 -0
  3. {kinemotion-0.9.0 → kinemotion-0.10.0}/PKG-INFO +1 -1
  4. {kinemotion-0.9.0 → kinemotion-0.10.0}/pyproject.toml +1 -1
  5. {kinemotion-0.9.0 → kinemotion-0.10.0}/src/kinemotion/dropjump/cli.py +319 -4
  6. {kinemotion-0.9.0 → kinemotion-0.10.0}/uv.lock +1 -1
  7. {kinemotion-0.9.0 → kinemotion-0.10.0}/.dockerignore +0 -0
  8. {kinemotion-0.9.0 → kinemotion-0.10.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  9. {kinemotion-0.9.0 → kinemotion-0.10.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  10. {kinemotion-0.9.0 → kinemotion-0.10.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  11. {kinemotion-0.9.0 → kinemotion-0.10.0}/.github/pull_request_template.md +0 -0
  12. {kinemotion-0.9.0 → kinemotion-0.10.0}/.github/workflows/release.yml +0 -0
  13. {kinemotion-0.9.0 → kinemotion-0.10.0}/.pre-commit-config.yaml +0 -0
  14. {kinemotion-0.9.0 → kinemotion-0.10.0}/.tool-versions +0 -0
  15. {kinemotion-0.9.0 → kinemotion-0.10.0}/CLAUDE.md +0 -0
  16. {kinemotion-0.9.0 → kinemotion-0.10.0}/CODE_OF_CONDUCT.md +0 -0
  17. {kinemotion-0.9.0 → kinemotion-0.10.0}/CONTRIBUTING.md +0 -0
  18. {kinemotion-0.9.0 → kinemotion-0.10.0}/Dockerfile +0 -0
  19. {kinemotion-0.9.0 → kinemotion-0.10.0}/GEMINI.md +0 -0
  20. {kinemotion-0.9.0 → kinemotion-0.10.0}/LICENSE +0 -0
  21. {kinemotion-0.9.0 → kinemotion-0.10.0}/README.md +0 -0
  22. {kinemotion-0.9.0 → kinemotion-0.10.0}/SECURITY.md +0 -0
  23. {kinemotion-0.9.0 → kinemotion-0.10.0}/docs/ERRORS_FINDINGS.md +0 -0
  24. {kinemotion-0.9.0 → kinemotion-0.10.0}/docs/FRAMERATE.md +0 -0
  25. {kinemotion-0.9.0 → kinemotion-0.10.0}/docs/IMU_METADATA_PRESERVATION.md +0 -0
  26. {kinemotion-0.9.0 → kinemotion-0.10.0}/docs/PARAMETERS.md +0 -0
  27. {kinemotion-0.9.0 → kinemotion-0.10.0}/docs/VALIDATION_PLAN.md +0 -0
  28. {kinemotion-0.9.0 → kinemotion-0.10.0}/examples/bulk/README.md +0 -0
  29. {kinemotion-0.9.0 → kinemotion-0.10.0}/examples/bulk/bulk_processing.py +0 -0
  30. {kinemotion-0.9.0 → kinemotion-0.10.0}/examples/bulk/simple_example.py +0 -0
  31. {kinemotion-0.9.0 → kinemotion-0.10.0}/examples/programmatic_usage.py +0 -0
  32. {kinemotion-0.9.0 → kinemotion-0.10.0}/src/kinemotion/__init__.py +0 -0
  33. {kinemotion-0.9.0 → kinemotion-0.10.0}/src/kinemotion/api.py +0 -0
  34. {kinemotion-0.9.0 → kinemotion-0.10.0}/src/kinemotion/cli.py +0 -0
  35. {kinemotion-0.9.0 → kinemotion-0.10.0}/src/kinemotion/core/__init__.py +0 -0
  36. {kinemotion-0.9.0 → kinemotion-0.10.0}/src/kinemotion/core/auto_tuning.py +0 -0
  37. {kinemotion-0.9.0 → kinemotion-0.10.0}/src/kinemotion/core/filtering.py +0 -0
  38. {kinemotion-0.9.0 → kinemotion-0.10.0}/src/kinemotion/core/pose.py +0 -0
  39. {kinemotion-0.9.0 → kinemotion-0.10.0}/src/kinemotion/core/smoothing.py +0 -0
  40. {kinemotion-0.9.0 → kinemotion-0.10.0}/src/kinemotion/core/video_io.py +0 -0
  41. {kinemotion-0.9.0 → kinemotion-0.10.0}/src/kinemotion/dropjump/__init__.py +0 -0
  42. {kinemotion-0.9.0 → kinemotion-0.10.0}/src/kinemotion/dropjump/analysis.py +0 -0
  43. {kinemotion-0.9.0 → kinemotion-0.10.0}/src/kinemotion/dropjump/debug_overlay.py +0 -0
  44. {kinemotion-0.9.0 → kinemotion-0.10.0}/src/kinemotion/dropjump/kinematics.py +0 -0
  45. {kinemotion-0.9.0 → kinemotion-0.10.0}/src/kinemotion/py.typed +0 -0
  46. {kinemotion-0.9.0 → kinemotion-0.10.0}/tests/__init__.py +0 -0
  47. {kinemotion-0.9.0 → kinemotion-0.10.0}/tests/test_adaptive_threshold.py +0 -0
  48. {kinemotion-0.9.0 → kinemotion-0.10.0}/tests/test_api.py +0 -0
  49. {kinemotion-0.9.0 → kinemotion-0.10.0}/tests/test_aspect_ratio.py +0 -0
  50. {kinemotion-0.9.0 → kinemotion-0.10.0}/tests/test_com_estimation.py +0 -0
  51. {kinemotion-0.9.0 → kinemotion-0.10.0}/tests/test_contact_detection.py +0 -0
  52. {kinemotion-0.9.0 → kinemotion-0.10.0}/tests/test_filtering.py +0 -0
  53. {kinemotion-0.9.0 → kinemotion-0.10.0}/tests/test_kinematics.py +0 -0
  54. {kinemotion-0.9.0 → kinemotion-0.10.0}/tests/test_polyorder.py +0 -0
@@ -42,9 +42,12 @@ env/
42
42
  htmlcov/
43
43
 
44
44
  # Output files
45
+ *.mov
46
+ *.MOV
45
47
  *.mp4
46
48
  *.avi
47
49
  *.json
50
+ *.csv
48
51
  !pyproject.toml
49
52
 
50
53
  # Logs
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  <!-- version list -->
9
9
 
10
+ ## v0.10.0 (2025-11-02)
11
+
12
+ ### Features
13
+
14
+ - Add batch processing mode to CLI
15
+ ([`b0ab3c6`](https://github.com/feniix/kinemotion/commit/b0ab3c6b37a013402ff7a89305a68e49549eeae3))
16
+
17
+
10
18
  ## v0.9.0 (2025-11-02)
11
19
 
12
20
  ### Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kinemotion
3
- Version: 0.9.0
3
+ Version: 0.10.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,6 +1,6 @@
1
1
  [project]
2
2
  name = "kinemotion"
3
- version = "0.9.0"
3
+ version = "0.10.0"
4
4
  description = "Video-based kinematic analysis for athletic performance"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10,<3.13"
@@ -1,5 +1,7 @@
1
1
  """Command-line interface for drop jump analysis."""
2
2
 
3
+ import csv
4
+ import glob
3
5
  import json
4
6
  import sys
5
7
  from pathlib import Path
@@ -8,6 +10,7 @@ from typing import Any
8
10
  import click
9
11
  import numpy as np
10
12
 
13
+ from ..api import VideoConfig, VideoResult, process_videos_bulk
11
14
  from ..core.auto_tuning import (
12
15
  QualityPreset,
13
16
  analyze_video_sample,
@@ -25,7 +28,7 @@ from .kinematics import calculate_drop_jump_metrics
25
28
 
26
29
 
27
30
  @click.command(name="dropjump-analyze")
28
- @click.argument("video_path", type=click.Path(exists=True))
31
+ @click.argument("video_path", nargs=-1, type=click.Path(exists=False), required=True)
29
32
  @click.option(
30
33
  "--output",
31
34
  "-o",
@@ -65,6 +68,34 @@ from .kinematics import calculate_drop_jump_metrics
65
68
  is_flag=True,
66
69
  help="Show auto-selected parameters and analysis details",
67
70
  )
71
+ # Batch processing options
72
+ @click.option(
73
+ "--batch",
74
+ is_flag=True,
75
+ help="Enable batch processing mode for multiple videos",
76
+ )
77
+ @click.option(
78
+ "--workers",
79
+ type=int,
80
+ default=4,
81
+ help="Number of parallel workers for batch processing (default: 4)",
82
+ show_default=True,
83
+ )
84
+ @click.option(
85
+ "--output-dir",
86
+ type=click.Path(),
87
+ help="Directory for debug video outputs (batch mode only)",
88
+ )
89
+ @click.option(
90
+ "--json-output-dir",
91
+ type=click.Path(),
92
+ help="Directory for JSON metrics outputs (batch mode only)",
93
+ )
94
+ @click.option(
95
+ "--csv-summary",
96
+ type=click.Path(),
97
+ help="Path for CSV summary export (batch mode only)",
98
+ )
68
99
  # Expert parameters (hidden in help, but always available for advanced users)
69
100
  @click.option(
70
101
  "--drop-start-frame",
@@ -109,12 +140,17 @@ from .kinematics import calculate_drop_jump_metrics
109
140
  help="[EXPERT] Override pose tracking confidence",
110
141
  )
111
142
  def dropjump_analyze(
112
- video_path: str,
143
+ video_path: tuple[str, ...],
113
144
  output: str | None,
114
145
  json_output: str | None,
115
146
  drop_height: float,
116
147
  quality: str,
117
148
  verbose: bool,
149
+ batch: bool,
150
+ workers: int,
151
+ output_dir: str | None,
152
+ json_output_dir: str | None,
153
+ csv_summary: str | None,
118
154
  drop_start_frame: int | None,
119
155
  smoothing_window: int | None,
120
156
  velocity_threshold: float | None,
@@ -124,13 +160,101 @@ def dropjump_analyze(
124
160
  tracking_confidence: float | None,
125
161
  ) -> None:
126
162
  """
127
- Analyze drop-jump video to estimate ground contact time, flight time, and jump height.
163
+ Analyze drop-jump video(s) to estimate ground contact time, flight time, and jump height.
128
164
 
129
165
  Uses intelligent auto-tuning to select optimal parameters based on video characteristics.
130
166
  Parameters are automatically adjusted for frame rate, tracking quality, and analysis preset.
131
167
 
132
- VIDEO_PATH: Path to the input video file
168
+ VIDEO_PATH: Path(s) to video file(s). Supports glob patterns in batch mode
169
+ (e.g., "videos/*.mp4").
170
+
171
+ Examples:
172
+
173
+ \b
174
+ # Single video
175
+ kinemotion dropjump-analyze video.mp4 --drop-height 0.40
176
+
177
+ \b
178
+ # Batch mode with glob pattern
179
+ kinemotion dropjump-analyze videos/*.mp4 --batch --drop-height 0.40 --workers 4
180
+
181
+ \b
182
+ # Batch with output directories
183
+ kinemotion dropjump-analyze videos/*.mp4 --batch --drop-height 0.40 \\
184
+ --json-output-dir results/ --csv-summary summary.csv
133
185
  """
186
+ # Expand glob patterns and collect all video files
187
+ video_files: list[str] = []
188
+ for pattern in video_path:
189
+ expanded = glob.glob(pattern)
190
+ if expanded:
191
+ video_files.extend(expanded)
192
+ elif Path(pattern).exists():
193
+ # Direct path (not a glob pattern)
194
+ video_files.append(pattern)
195
+ else:
196
+ click.echo(f"Warning: No files found for pattern: {pattern}", err=True)
197
+
198
+ if not video_files:
199
+ click.echo("Error: No video files found", err=True)
200
+ sys.exit(1)
201
+
202
+ # Determine if batch mode should be used
203
+ use_batch = batch or len(video_files) > 1
204
+
205
+ if use_batch:
206
+ _process_batch(
207
+ video_files,
208
+ drop_height,
209
+ quality,
210
+ workers,
211
+ output_dir,
212
+ json_output_dir,
213
+ csv_summary,
214
+ drop_start_frame,
215
+ smoothing_window,
216
+ velocity_threshold,
217
+ min_contact_frames,
218
+ visibility_threshold,
219
+ detection_confidence,
220
+ tracking_confidence,
221
+ verbose,
222
+ )
223
+ else:
224
+ # Single video mode (original behavior)
225
+ _process_single(
226
+ video_files[0],
227
+ output,
228
+ json_output,
229
+ drop_height,
230
+ quality,
231
+ verbose,
232
+ drop_start_frame,
233
+ smoothing_window,
234
+ velocity_threshold,
235
+ min_contact_frames,
236
+ visibility_threshold,
237
+ detection_confidence,
238
+ tracking_confidence,
239
+ )
240
+
241
+
242
+ def _process_single(
243
+ video_path: str,
244
+ output: str | None,
245
+ json_output: str | None,
246
+ drop_height: float,
247
+ quality: str,
248
+ verbose: bool,
249
+ drop_start_frame: int | None,
250
+ smoothing_window: int | None,
251
+ velocity_threshold: float | None,
252
+ min_contact_frames: int | None,
253
+ visibility_threshold: float | None,
254
+ detection_confidence: float | None,
255
+ tracking_confidence: float | None,
256
+ ) -> None:
257
+ """Process a single video (original CLI behavior)."""
134
258
  click.echo(f"Analyzing video: {video_path}", err=True)
135
259
 
136
260
  # Convert quality string to enum
@@ -414,3 +538,194 @@ def dropjump_analyze(
414
538
  except Exception as e:
415
539
  click.echo(f"Error: {str(e)}", err=True)
416
540
  sys.exit(1)
541
+
542
+
543
+ def _process_batch(
544
+ video_files: list[str],
545
+ drop_height: float,
546
+ quality: str,
547
+ workers: int,
548
+ output_dir: str | None,
549
+ json_output_dir: str | None,
550
+ csv_summary: str | None,
551
+ drop_start_frame: int | None,
552
+ smoothing_window: int | None,
553
+ velocity_threshold: float | None,
554
+ min_contact_frames: int | None,
555
+ visibility_threshold: float | None,
556
+ detection_confidence: float | None,
557
+ tracking_confidence: float | None,
558
+ verbose: bool,
559
+ ) -> None:
560
+ """Process multiple videos in batch mode using parallel processing."""
561
+ click.echo(
562
+ f"\nBatch processing {len(video_files)} videos with {workers} workers", err=True
563
+ )
564
+ click.echo("=" * 70, err=True)
565
+
566
+ # Create output directories if specified
567
+ if output_dir:
568
+ Path(output_dir).mkdir(parents=True, exist_ok=True)
569
+ click.echo(f"Debug videos will be saved to: {output_dir}", err=True)
570
+
571
+ if json_output_dir:
572
+ Path(json_output_dir).mkdir(parents=True, exist_ok=True)
573
+ click.echo(f"JSON metrics will be saved to: {json_output_dir}", err=True)
574
+
575
+ # Build configurations for each video
576
+ configs: list[VideoConfig] = []
577
+ for video_file in video_files:
578
+ video_name = Path(video_file).stem
579
+
580
+ # Determine output paths
581
+ debug_video = None
582
+ if output_dir:
583
+ debug_video = str(Path(output_dir) / f"{video_name}_debug.mp4")
584
+
585
+ json_file = None
586
+ if json_output_dir:
587
+ json_file = str(Path(json_output_dir) / f"{video_name}.json")
588
+
589
+ config = VideoConfig(
590
+ video_path=video_file,
591
+ drop_height=drop_height,
592
+ quality=quality,
593
+ output_video=debug_video,
594
+ json_output=json_file,
595
+ drop_start_frame=drop_start_frame,
596
+ smoothing_window=smoothing_window,
597
+ velocity_threshold=velocity_threshold,
598
+ min_contact_frames=min_contact_frames,
599
+ visibility_threshold=visibility_threshold,
600
+ detection_confidence=detection_confidence,
601
+ tracking_confidence=tracking_confidence,
602
+ )
603
+ configs.append(config)
604
+
605
+ # Progress callback
606
+ completed = 0
607
+
608
+ def show_progress(result: VideoResult) -> None:
609
+ nonlocal completed
610
+ completed += 1
611
+ status = "✓" if result.success else "✗"
612
+ video_name = Path(result.video_path).name
613
+ click.echo(
614
+ f"[{completed}/{len(configs)}] {status} {video_name} "
615
+ f"({result.processing_time:.1f}s)",
616
+ err=True,
617
+ )
618
+ if not result.success:
619
+ click.echo(f" Error: {result.error}", err=True)
620
+
621
+ # Process all videos
622
+ click.echo("\nProcessing videos...", err=True)
623
+ results = process_videos_bulk(
624
+ configs, max_workers=workers, progress_callback=show_progress
625
+ )
626
+
627
+ # Generate summary
628
+ click.echo("\n" + "=" * 70, err=True)
629
+ click.echo("BATCH PROCESSING SUMMARY", err=True)
630
+ click.echo("=" * 70, err=True)
631
+
632
+ successful = [r for r in results if r.success]
633
+ failed = [r for r in results if not r.success]
634
+
635
+ click.echo(f"Total videos: {len(results)}", err=True)
636
+ click.echo(f"Successful: {len(successful)}", err=True)
637
+ click.echo(f"Failed: {len(failed)}", err=True)
638
+
639
+ if successful:
640
+ # Calculate average metrics
641
+ with_gct = [
642
+ r
643
+ for r in successful
644
+ if r.metrics and r.metrics.ground_contact_time is not None
645
+ ]
646
+ with_flight = [
647
+ r for r in successful if r.metrics and r.metrics.flight_time is not None
648
+ ]
649
+ with_jump = [
650
+ r for r in successful if r.metrics and r.metrics.jump_height is not None
651
+ ]
652
+
653
+ if with_gct:
654
+ avg_gct = sum(r.metrics.ground_contact_time * 1000 for r in with_gct) / len(
655
+ with_gct
656
+ )
657
+ click.echo(f"\nAverage ground contact time: {avg_gct:.1f} ms", err=True)
658
+
659
+ if with_flight:
660
+ avg_flight = sum(r.metrics.flight_time * 1000 for r in with_flight) / len(
661
+ with_flight
662
+ )
663
+ click.echo(f"Average flight time: {avg_flight:.1f} ms", err=True)
664
+
665
+ if with_jump:
666
+ avg_jump = sum(r.metrics.jump_height for r in with_jump) / len(with_jump)
667
+ click.echo(
668
+ f"Average jump height: {avg_jump:.3f} m ({avg_jump * 100:.1f} cm)",
669
+ err=True,
670
+ )
671
+
672
+ # Export CSV summary if requested
673
+ if csv_summary and successful:
674
+ click.echo(f"\nExporting CSV summary to: {csv_summary}", err=True)
675
+ Path(csv_summary).parent.mkdir(parents=True, exist_ok=True)
676
+
677
+ with open(csv_summary, "w", newline="") as f:
678
+ writer = csv.writer(f)
679
+
680
+ # Header
681
+ writer.writerow(
682
+ [
683
+ "Video",
684
+ "Ground Contact Time (ms)",
685
+ "Flight Time (ms)",
686
+ "Jump Height (m)",
687
+ "Processing Time (s)",
688
+ "Status",
689
+ ]
690
+ )
691
+
692
+ # Data rows
693
+ for result in results:
694
+ if result.success and result.metrics:
695
+ writer.writerow(
696
+ [
697
+ Path(result.video_path).name,
698
+ (
699
+ f"{result.metrics.ground_contact_time * 1000:.1f}"
700
+ if result.metrics.ground_contact_time
701
+ else "N/A"
702
+ ),
703
+ (
704
+ f"{result.metrics.flight_time * 1000:.1f}"
705
+ if result.metrics.flight_time
706
+ else "N/A"
707
+ ),
708
+ (
709
+ f"{result.metrics.jump_height:.3f}"
710
+ if result.metrics.jump_height
711
+ else "N/A"
712
+ ),
713
+ f"{result.processing_time:.2f}",
714
+ "Success",
715
+ ]
716
+ )
717
+ else:
718
+ writer.writerow(
719
+ [
720
+ Path(result.video_path).name,
721
+ "N/A",
722
+ "N/A",
723
+ "N/A",
724
+ f"{result.processing_time:.2f}",
725
+ f"Failed: {result.error}",
726
+ ]
727
+ )
728
+
729
+ click.echo("CSV summary written successfully", err=True)
730
+
731
+ click.echo("\nBatch processing complete!", err=True)
@@ -603,7 +603,7 @@ wheels = [
603
603
 
604
604
  [[package]]
605
605
  name = "kinemotion"
606
- version = "0.8.3"
606
+ version = "0.9.0"
607
607
  source = { editable = "." }
608
608
  dependencies = [
609
609
  { name = "click" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes