kinemotion 0.44.0__py3-none-any.whl → 0.45.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 +72 -634
- kinemotion/cmj/cli.py +12 -37
- kinemotion/core/cli_utils.py +38 -218
- kinemotion/core/pipeline_utils.py +429 -0
- kinemotion/dropjump/cli.py +11 -22
- {kinemotion-0.44.0.dist-info → kinemotion-0.45.0.dist-info}/METADATA +1 -1
- {kinemotion-0.44.0.dist-info → kinemotion-0.45.0.dist-info}/RECORD +10 -9
- {kinemotion-0.44.0.dist-info → kinemotion-0.45.0.dist-info}/WHEEL +0 -0
- {kinemotion-0.44.0.dist-info → kinemotion-0.45.0.dist-info}/entry_points.txt +0 -0
- {kinemotion-0.44.0.dist-info → kinemotion-0.45.0.dist-info}/licenses/LICENSE +0 -0
kinemotion/cmj/cli.py
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
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
6
|
from typing import Any
|
|
9
7
|
|
|
10
8
|
import click
|
|
11
9
|
|
|
12
10
|
from ..api import process_cmj_video
|
|
13
11
|
from ..core.auto_tuning import QualityPreset
|
|
14
|
-
from ..core.cli_utils import
|
|
12
|
+
from ..core.cli_utils import (
|
|
13
|
+
collect_video_files,
|
|
14
|
+
common_output_options,
|
|
15
|
+
generate_batch_output_paths,
|
|
16
|
+
)
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
@dataclass
|
|
@@ -27,33 +29,6 @@ class AnalysisParameters:
|
|
|
27
29
|
tracking_confidence: float | None = None
|
|
28
30
|
|
|
29
31
|
|
|
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
32
|
def _process_batch_videos(
|
|
58
33
|
video_files: list[str],
|
|
59
34
|
output_dir: str | None,
|
|
@@ -74,7 +49,7 @@ def _process_batch_videos(
|
|
|
74
49
|
for video in video_files:
|
|
75
50
|
try:
|
|
76
51
|
click.echo(f"\nProcessing: {video}", err=True)
|
|
77
|
-
out_path, json_path =
|
|
52
|
+
out_path, json_path = generate_batch_output_paths(
|
|
78
53
|
video, output_dir, json_output_dir
|
|
79
54
|
)
|
|
80
55
|
_process_single(
|
|
@@ -209,25 +184,25 @@ def cmj_analyze( # NOSONAR(S107) - Click CLI requires individual parameters
|
|
|
209
184
|
|
|
210
185
|
Examples:
|
|
211
186
|
|
|
212
|
-
|
|
187
|
+
\b
|
|
213
188
|
# Basic analysis
|
|
214
189
|
kinemotion cmj-analyze video.mp4
|
|
215
190
|
|
|
216
|
-
|
|
191
|
+
\b
|
|
217
192
|
# With debug video output
|
|
218
193
|
kinemotion cmj-analyze video.mp4 --output debug.mp4
|
|
219
194
|
|
|
220
|
-
|
|
195
|
+
\b
|
|
221
196
|
# Batch mode with glob pattern
|
|
222
197
|
kinemotion cmj-analyze videos/*.mp4 --batch --workers 4
|
|
223
198
|
|
|
224
|
-
|
|
199
|
+
\b
|
|
225
200
|
# Batch with output directories
|
|
226
|
-
kinemotion cmj-analyze videos/*.mp4 --batch
|
|
201
|
+
kinemotion cmj-analyze videos/*.mp4 --batch \
|
|
227
202
|
--json-output-dir results/ --csv-summary summary.csv
|
|
228
203
|
"""
|
|
229
204
|
# Expand glob patterns and collect all video files
|
|
230
|
-
video_files =
|
|
205
|
+
video_files = collect_video_files(video_path)
|
|
231
206
|
|
|
232
207
|
if not video_files:
|
|
233
208
|
click.echo("Error: No video files found", err=True)
|
kinemotion/core/cli_utils.py
CHANGED
|
@@ -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
|
|
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
|