kinemotion 0.44.0__py3-none-any.whl → 0.45.1__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/cmj/cli.py CHANGED
@@ -1,17 +1,18 @@
1
1
  """Command-line interface for counter movement jump (CMJ) analysis."""
2
2
 
3
- import glob
4
3
  import json
5
4
  import sys
6
5
  from dataclasses import dataclass
7
- from pathlib import Path
8
- from typing import Any
9
6
 
10
7
  import click
11
8
 
12
- from ..api import process_cmj_video
9
+ from ..api import CMJMetrics, process_cmj_video
13
10
  from ..core.auto_tuning import QualityPreset
14
- from ..core.cli_utils import common_output_options
11
+ from ..core.cli_utils import (
12
+ collect_video_files,
13
+ common_output_options,
14
+ generate_batch_output_paths,
15
+ )
15
16
 
16
17
 
17
18
  @dataclass
@@ -27,33 +28,6 @@ class AnalysisParameters:
27
28
  tracking_confidence: float | None = None
28
29
 
29
30
 
30
- def _collect_video_files(video_path: tuple[str, ...]) -> list[str]:
31
- """Expand glob patterns and collect all video files."""
32
- video_files: list[str] = []
33
- for pattern in video_path:
34
- expanded = glob.glob(pattern)
35
- if expanded:
36
- video_files.extend(expanded)
37
- elif Path(pattern).exists():
38
- video_files.append(pattern)
39
- else:
40
- click.echo(f"Warning: No files found for pattern: {pattern}", err=True)
41
- return video_files
42
-
43
-
44
- def _generate_output_paths(
45
- video: str, output_dir: str | None, json_output_dir: str | None
46
- ) -> tuple[str | None, str | None]:
47
- """Generate output paths for debug video and JSON."""
48
- out_path = None
49
- json_path = None
50
- if output_dir:
51
- out_path = str(Path(output_dir) / f"{Path(video).stem}_debug.mp4")
52
- if json_output_dir:
53
- json_path = str(Path(json_output_dir) / f"{Path(video).stem}.json")
54
- return out_path, json_path
55
-
56
-
57
31
  def _process_batch_videos(
58
32
  video_files: list[str],
59
33
  output_dir: str | None,
@@ -74,7 +48,7 @@ def _process_batch_videos(
74
48
  for video in video_files:
75
49
  try:
76
50
  click.echo(f"\nProcessing: {video}", err=True)
77
- out_path, json_path = _generate_output_paths(
51
+ out_path, json_path = generate_batch_output_paths(
78
52
  video, output_dir, json_output_dir
79
53
  )
80
54
  _process_single(
@@ -209,25 +183,25 @@ def cmj_analyze( # NOSONAR(S107) - Click CLI requires individual parameters
209
183
 
210
184
  Examples:
211
185
 
212
- \\b
186
+ \b
213
187
  # Basic analysis
214
188
  kinemotion cmj-analyze video.mp4
215
189
 
216
- \\b
190
+ \b
217
191
  # With debug video output
218
192
  kinemotion cmj-analyze video.mp4 --output debug.mp4
219
193
 
220
- \\b
194
+ \b
221
195
  # Batch mode with glob pattern
222
196
  kinemotion cmj-analyze videos/*.mp4 --batch --workers 4
223
197
 
224
- \\b
198
+ \b
225
199
  # Batch with output directories
226
- kinemotion cmj-analyze videos/*.mp4 --batch \\
200
+ kinemotion cmj-analyze videos/*.mp4 --batch \
227
201
  --json-output-dir results/ --csv-summary summary.csv
228
202
  """
229
203
  # Expand glob patterns and collect all video files
230
- video_files = _collect_video_files(video_path)
204
+ video_files = collect_video_files(video_path)
231
205
 
232
206
  if not video_files:
233
207
  click.echo("Error: No video files found", err=True)
@@ -312,7 +286,7 @@ def _process_single(
312
286
  sys.exit(1)
313
287
 
314
288
 
315
- def _output_results(metrics: Any, json_output: str | None) -> None:
289
+ def _output_results(metrics: CMJMetrics, json_output: str | None) -> None:
316
290
  """Output analysis results."""
317
291
  results = metrics.to_dict()
318
292
 
@@ -1,227 +1,11 @@
1
1
  """Shared CLI utilities for drop jump and CMJ analysis."""
2
2
 
3
+ import glob
3
4
  from collections.abc import Callable
4
- from typing import Any, Protocol
5
+ from pathlib import Path
5
6
 
6
7
  import click
7
8
 
8
- from .auto_tuning import AnalysisParameters, QualityPreset, VideoCharacteristics
9
- from .experimental import unused
10
- from .pose import PoseTracker
11
- from .smoothing import smooth_landmarks, smooth_landmarks_advanced
12
- from .video_io import VideoProcessor
13
-
14
-
15
- class ExpertParameters(Protocol):
16
- """Protocol for expert parameter overrides."""
17
-
18
- detection_confidence: float | None
19
- tracking_confidence: float | None
20
- smoothing_window: int | None
21
- velocity_threshold: float | None
22
- min_contact_frames: int | None
23
- visibility_threshold: float | None
24
-
25
-
26
- @unused(
27
- reason="Not called by analysis pipeline - remnant from CLI refactoring",
28
- remove_in="1.0.0",
29
- since="0.34.0",
30
- )
31
- def determine_initial_confidence(
32
- quality_preset: QualityPreset,
33
- expert_params: ExpertParameters,
34
- ) -> tuple[float, float]:
35
- """Determine initial detection and tracking confidence levels.
36
-
37
- Args:
38
- quality_preset: Quality preset enum
39
- expert_params: Expert parameter overrides
40
-
41
- Returns:
42
- Tuple of (detection_confidence, tracking_confidence)
43
- """
44
- initial_detection_conf = 0.5
45
- initial_tracking_conf = 0.5
46
-
47
- if quality_preset == QualityPreset.FAST:
48
- initial_detection_conf = 0.3
49
- initial_tracking_conf = 0.3
50
- elif quality_preset == QualityPreset.ACCURATE:
51
- initial_detection_conf = 0.6
52
- initial_tracking_conf = 0.6
53
-
54
- # Override with expert values if provided
55
- if expert_params.detection_confidence is not None:
56
- initial_detection_conf = expert_params.detection_confidence
57
- if expert_params.tracking_confidence is not None:
58
- initial_tracking_conf = expert_params.tracking_confidence
59
-
60
- return initial_detection_conf, initial_tracking_conf
61
-
62
-
63
- @unused(
64
- reason="Not called by analysis pipeline - remnant from CLI refactoring",
65
- remove_in="1.0.0",
66
- since="0.34.0",
67
- )
68
- def track_all_frames(video: VideoProcessor, tracker: PoseTracker) -> tuple[list, list]:
69
- """Track pose landmarks in all video frames.
70
-
71
- Args:
72
- video: Video processor
73
- tracker: Pose tracker
74
-
75
- Returns:
76
- Tuple of (frames, landmarks_sequence)
77
- """
78
- click.echo("Tracking pose landmarks...", err=True)
79
- landmarks_sequence = []
80
- frames = []
81
-
82
- bar: Any
83
- with click.progressbar(length=video.frame_count, label="Processing frames") as bar:
84
- while True:
85
- frame = video.read_frame()
86
- if frame is None:
87
- break
88
-
89
- frames.append(frame)
90
- landmarks = tracker.process_frame(frame)
91
- landmarks_sequence.append(landmarks)
92
- bar.update(1)
93
-
94
- tracker.close()
95
- return frames, landmarks_sequence
96
-
97
-
98
- @unused(
99
- reason="Not called by analysis pipeline - remnant from CLI refactoring",
100
- remove_in="1.0.0",
101
- since="0.34.0",
102
- )
103
- def apply_expert_param_overrides(
104
- params: AnalysisParameters, expert_params: ExpertParameters
105
- ) -> AnalysisParameters:
106
- """Apply expert parameter overrides to auto-tuned parameters.
107
-
108
- Args:
109
- params: Auto-tuned parameters
110
- expert_params: Expert overrides
111
-
112
- Returns:
113
- Modified params object (mutated in place)
114
- """
115
- if expert_params.smoothing_window is not None:
116
- params.smoothing_window = expert_params.smoothing_window
117
- if expert_params.velocity_threshold is not None:
118
- params.velocity_threshold = expert_params.velocity_threshold
119
- if expert_params.min_contact_frames is not None:
120
- params.min_contact_frames = expert_params.min_contact_frames
121
- if expert_params.visibility_threshold is not None:
122
- params.visibility_threshold = expert_params.visibility_threshold
123
- return params
124
-
125
-
126
- @unused(
127
- reason="Not called by analysis pipeline - remnant from CLI refactoring",
128
- remove_in="1.0.0",
129
- since="0.34.0",
130
- )
131
- def print_auto_tuned_params(
132
- video: VideoProcessor,
133
- quality_preset: QualityPreset,
134
- params: AnalysisParameters,
135
- characteristics: VideoCharacteristics | None = None,
136
- extra_params: dict[str, Any] | None = None,
137
- ) -> None:
138
- """Print auto-tuned parameters in verbose mode.
139
-
140
- Args:
141
- video: Video processor
142
- quality_preset: Quality preset
143
- params: Auto-tuned parameters
144
- characteristics: Optional video characteristics (for tracking quality
145
- display)
146
- extra_params: Optional extra parameters to display (e.g.,
147
- countermovement_threshold)
148
- """
149
- click.echo("\n" + "=" * 60, err=True)
150
- click.echo("AUTO-TUNED PARAMETERS", err=True)
151
- click.echo("=" * 60, err=True)
152
- click.echo(f"Video FPS: {video.fps:.2f}", err=True)
153
-
154
- if characteristics:
155
- click.echo(
156
- f"Tracking quality: {characteristics.tracking_quality} "
157
- f"(avg visibility: {characteristics.avg_visibility:.2f})",
158
- err=True,
159
- )
160
-
161
- click.echo(f"Quality preset: {quality_preset.value}", err=True)
162
- click.echo("\nSelected parameters:", err=True)
163
- click.echo(f" smoothing_window: {params.smoothing_window}", err=True)
164
- click.echo(f" polyorder: {params.polyorder}", err=True)
165
- click.echo(f" velocity_threshold: {params.velocity_threshold:.4f}", err=True)
166
-
167
- # Print extra parameters if provided
168
- if extra_params:
169
- for key, value in extra_params.items():
170
- if isinstance(value, float):
171
- click.echo(f" {key}: {value:.4f}", err=True)
172
- else:
173
- click.echo(f" {key}: {value}", err=True)
174
-
175
- click.echo(f" min_contact_frames: {params.min_contact_frames}", err=True)
176
- click.echo(f" visibility_threshold: {params.visibility_threshold}", err=True)
177
- click.echo(f" detection_confidence: {params.detection_confidence}", err=True)
178
- click.echo(f" tracking_confidence: {params.tracking_confidence}", err=True)
179
- click.echo(f" outlier_rejection: {params.outlier_rejection}", err=True)
180
- click.echo(f" bilateral_filter: {params.bilateral_filter}", err=True)
181
- click.echo(f" use_curvature: {params.use_curvature}", err=True)
182
- click.echo("=" * 60 + "\n", err=True)
183
-
184
-
185
- @unused(
186
- reason="Not called by analysis pipeline - remnant from CLI refactoring",
187
- remove_in="1.0.0",
188
- since="0.34.0",
189
- )
190
- def smooth_landmark_sequence(
191
- landmarks_sequence: list, params: AnalysisParameters
192
- ) -> list:
193
- """Apply smoothing to landmark sequence.
194
-
195
- Args:
196
- landmarks_sequence: Raw landmark sequence
197
- params: Auto-tuned parameters
198
-
199
- Returns:
200
- Smoothed landmark sequence
201
- """
202
- if params.outlier_rejection or params.bilateral_filter:
203
- if params.outlier_rejection:
204
- click.echo("Smoothing landmarks with outlier rejection...", err=True)
205
- if params.bilateral_filter:
206
- click.echo(
207
- "Using bilateral temporal filter for edge-preserving smoothing...",
208
- err=True,
209
- )
210
- return smooth_landmarks_advanced(
211
- landmarks_sequence,
212
- window_length=params.smoothing_window,
213
- polyorder=params.polyorder,
214
- use_outlier_rejection=params.outlier_rejection,
215
- use_bilateral=params.bilateral_filter,
216
- )
217
- else:
218
- click.echo("Smoothing landmarks...", err=True)
219
- return smooth_landmarks(
220
- landmarks_sequence,
221
- window_length=params.smoothing_window,
222
- polyorder=params.polyorder,
223
- )
224
-
225
9
 
226
10
  def common_output_options(func: Callable) -> Callable: # type: ignore[type-arg]
227
11
  """Add common output options to CLI command."""
@@ -238,3 +22,39 @@ def common_output_options(func: Callable) -> Callable: # type: ignore[type-arg]
238
22
  help="Path for JSON metrics output (default: stdout)",
239
23
  )(func)
240
24
  return func
25
+
26
+
27
+ def collect_video_files(video_path: tuple[str, ...]) -> list[str]:
28
+ """Expand glob patterns and collect all video files."""
29
+ video_files: list[str] = []
30
+ for pattern in video_path:
31
+ expanded = glob.glob(pattern)
32
+ if expanded:
33
+ video_files.extend(expanded)
34
+ elif Path(pattern).exists():
35
+ video_files.append(pattern)
36
+ else:
37
+ click.echo(f"Warning: No files found for pattern: {pattern}", err=True)
38
+ return video_files
39
+
40
+
41
+ def generate_batch_output_paths(
42
+ video_path: str, output_dir: str | None, json_output_dir: str | None
43
+ ) -> tuple[str | None, str | None]:
44
+ """Generate output paths for debug video and JSON in batch mode.
45
+
46
+ Args:
47
+ video_path: Path to source video
48
+ output_dir: Directory for debug video output (optional)
49
+ json_output_dir: Directory for JSON metrics output (optional)
50
+
51
+ Returns:
52
+ Tuple of (debug_video_path, json_output_path)
53
+ """
54
+ out_path = None
55
+ json_path = None
56
+ if output_dir:
57
+ out_path = str(Path(output_dir) / f"{Path(video_path).stem}_debug.mp4")
58
+ if json_output_dir:
59
+ json_path = str(Path(json_output_dir) / f"{Path(video_path).stem}.json")
60
+ return out_path, json_path
@@ -5,6 +5,7 @@ import shutil
5
5
  import subprocess
6
6
  import time
7
7
  from pathlib import Path
8
+ from typing import Self
8
9
 
9
10
  import cv2
10
11
  import numpy as np
@@ -251,7 +252,7 @@ class BaseDebugOverlayRenderer:
251
252
  if temp_path and os.path.exists(temp_path):
252
253
  os.remove(temp_path)
253
254
 
254
- def __enter__(self) -> "BaseDebugOverlayRenderer":
255
+ def __enter__(self) -> Self:
255
256
  return self
256
257
 
257
258
  def __exit__(self, _exc_type, _exc_val, _exc_tb) -> None: # type: ignore[no-untyped-def]