kinemotion 0.76.3__py3-none-any.whl → 1.0.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.

Files changed (53) hide show
  1. kinemotion/__init__.py +3 -18
  2. kinemotion/api.py +7 -27
  3. kinemotion/cli.py +2 -4
  4. kinemotion/{countermovement_jump → cmj}/analysis.py +158 -16
  5. kinemotion/{countermovement_jump → cmj}/api.py +18 -46
  6. kinemotion/{countermovement_jump → cmj}/cli.py +46 -6
  7. kinemotion/cmj/debug_overlay.py +457 -0
  8. kinemotion/{countermovement_jump → cmj}/joint_angles.py +31 -96
  9. kinemotion/{countermovement_jump → cmj}/metrics_validator.py +293 -184
  10. kinemotion/{countermovement_jump → cmj}/validation_bounds.py +18 -1
  11. kinemotion/core/__init__.py +2 -11
  12. kinemotion/core/auto_tuning.py +107 -149
  13. kinemotion/core/cli_utils.py +0 -74
  14. kinemotion/core/debug_overlay_utils.py +15 -142
  15. kinemotion/core/experimental.py +51 -55
  16. kinemotion/core/filtering.py +56 -116
  17. kinemotion/core/pipeline_utils.py +2 -2
  18. kinemotion/core/pose.py +98 -47
  19. kinemotion/core/quality.py +6 -4
  20. kinemotion/core/smoothing.py +51 -65
  21. kinemotion/core/types.py +0 -15
  22. kinemotion/core/validation.py +7 -76
  23. kinemotion/core/video_io.py +27 -41
  24. kinemotion/{drop_jump → dropjump}/__init__.py +8 -2
  25. kinemotion/{drop_jump → dropjump}/analysis.py +120 -282
  26. kinemotion/{drop_jump → dropjump}/api.py +33 -59
  27. kinemotion/{drop_jump → dropjump}/cli.py +136 -70
  28. kinemotion/dropjump/debug_overlay.py +182 -0
  29. kinemotion/{drop_jump → dropjump}/kinematics.py +65 -175
  30. kinemotion/{drop_jump → dropjump}/metrics_validator.py +51 -25
  31. kinemotion/{drop_jump → dropjump}/validation_bounds.py +1 -1
  32. kinemotion/models/rtmpose-s_simcc-body7_pt-body7-halpe26_700e-256x192-7f134165_20230605.onnx +3 -0
  33. kinemotion/models/yolox_tiny_8xb8-300e_humanart-6f3252f9.onnx +3 -0
  34. {kinemotion-0.76.3.dist-info → kinemotion-1.0.0.dist-info}/METADATA +26 -75
  35. kinemotion-1.0.0.dist-info/RECORD +49 -0
  36. kinemotion/core/overlay_constants.py +0 -61
  37. kinemotion/core/video_analysis_base.py +0 -132
  38. kinemotion/countermovement_jump/debug_overlay.py +0 -325
  39. kinemotion/drop_jump/debug_overlay.py +0 -241
  40. kinemotion/squat_jump/__init__.py +0 -5
  41. kinemotion/squat_jump/analysis.py +0 -377
  42. kinemotion/squat_jump/api.py +0 -610
  43. kinemotion/squat_jump/cli.py +0 -309
  44. kinemotion/squat_jump/debug_overlay.py +0 -163
  45. kinemotion/squat_jump/kinematics.py +0 -342
  46. kinemotion/squat_jump/metrics_validator.py +0 -438
  47. kinemotion/squat_jump/validation_bounds.py +0 -221
  48. kinemotion-0.76.3.dist-info/RECORD +0 -57
  49. /kinemotion/{countermovement_jump → cmj}/__init__.py +0 -0
  50. /kinemotion/{countermovement_jump → cmj}/kinematics.py +0 -0
  51. {kinemotion-0.76.3.dist-info → kinemotion-1.0.0.dist-info}/WHEEL +0 -0
  52. {kinemotion-0.76.3.dist-info → kinemotion-1.0.0.dist-info}/entry_points.txt +0 -0
  53. {kinemotion-0.76.3.dist-info → kinemotion-1.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,309 +0,0 @@
1
- """Command-line interface for squat jump (SJ) analysis."""
2
-
3
- import json
4
- import sys
5
- from dataclasses import dataclass
6
-
7
- import click
8
-
9
- from ..core.auto_tuning import QualityPreset
10
- from ..core.cli_utils import (
11
- batch_processing_options,
12
- collect_video_files,
13
- common_output_options,
14
- generate_batch_output_paths,
15
- quality_option,
16
- verbose_option,
17
- )
18
- from .api import AnalysisOverrides, process_sj_video
19
- from .kinematics import SJMetrics
20
-
21
-
22
- @dataclass
23
- class AnalysisParameters:
24
- """Expert parameters for SJ analysis customization."""
25
-
26
- smoothing_window: int | None = None
27
- velocity_threshold: float | None = None
28
- squat_hold_threshold: float | None = None
29
- min_contact_frames: int | None = None
30
- visibility_threshold: float | None = None
31
- detection_confidence: float | None = None
32
- tracking_confidence: float | None = None
33
- mass_kg: float | None = None # For power calculations
34
-
35
-
36
- def _process_batch_videos(
37
- video_files: list[str],
38
- output_dir: str | None,
39
- json_output_dir: str | None,
40
- quality_preset: QualityPreset,
41
- verbose: bool,
42
- expert_params: AnalysisParameters,
43
- workers: int,
44
- ) -> None:
45
- """Process multiple videos in batch mode."""
46
- click.echo(
47
- f"Batch mode: Processing {len(video_files)} video(s) with {workers} workers",
48
- err=True,
49
- )
50
- click.echo("Note: Batch processing not yet fully implemented", err=True)
51
- click.echo("Processing videos sequentially...", err=True)
52
-
53
- for video in video_files:
54
- try:
55
- click.echo(f"\nProcessing: {video}", err=True)
56
- out_path, json_path = generate_batch_output_paths(video, output_dir, json_output_dir)
57
- _process_single(video, out_path, json_path, quality_preset, verbose, expert_params)
58
- except Exception as e:
59
- click.echo(f"Error processing {video}: {e}", err=True)
60
- continue
61
-
62
-
63
- @click.command(name="sj-analyze")
64
- @click.argument("video_path", nargs=-1, type=click.Path(exists=False), required=True)
65
- @common_output_options
66
- @quality_option
67
- @verbose_option
68
- @batch_processing_options
69
- @click.option(
70
- "--mass",
71
- type=float,
72
- required=True,
73
- help="Athlete mass in kilograms (required for power calculations)",
74
- )
75
- # Expert parameters (hidden in help, but always available for advanced users)
76
- @click.option(
77
- "--smoothing-window",
78
- type=int,
79
- default=None,
80
- help="[EXPERT] Override auto-tuned smoothing window size",
81
- )
82
- @click.option(
83
- "--velocity-threshold",
84
- type=float,
85
- default=None,
86
- help="[EXPERT] Override auto-tuned velocity threshold for flight detection",
87
- )
88
- @click.option(
89
- "--squat-hold-threshold",
90
- type=float,
91
- default=None,
92
- help="[EXPERT] Override auto-tuned squat hold threshold",
93
- )
94
- @click.option(
95
- "--min-contact-frames",
96
- type=int,
97
- default=None,
98
- help="[EXPERT] Override auto-tuned minimum contact frames",
99
- )
100
- @click.option(
101
- "--visibility-threshold",
102
- type=float,
103
- default=None,
104
- help="[EXPERT] Override visibility threshold",
105
- )
106
- @click.option(
107
- "--detection-confidence",
108
- type=float,
109
- default=None,
110
- help="[EXPERT] Override pose detection confidence",
111
- )
112
- @click.option(
113
- "--tracking-confidence",
114
- type=float,
115
- default=None,
116
- help="[EXPERT] Override pose tracking confidence",
117
- )
118
- def sj_analyze( # NOSONAR(S107) - Click CLI requires individual parameters
119
- # for each option
120
- video_path: tuple[str, ...],
121
- output: str | None,
122
- json_output: str | None,
123
- quality: str,
124
- verbose: bool,
125
- batch: bool,
126
- workers: int,
127
- output_dir: str | None,
128
- json_output_dir: str | None,
129
- csv_summary: str | None,
130
- mass: float,
131
- smoothing_window: int | None,
132
- velocity_threshold: float | None,
133
- squat_hold_threshold: float | None,
134
- min_contact_frames: int | None,
135
- visibility_threshold: float | None,
136
- detection_confidence: float | None,
137
- tracking_confidence: float | None,
138
- ) -> None:
139
- """
140
- Analyze squat jump (SJ) video(s) to estimate jump performance metrics.
141
-
142
- ⚠️ EXPERIMENTAL: Squat Jump analysis is new and awaiting validation studies.
143
- Power/force calculations use validated Sayers regression but SJ-specific
144
- phase detection may need refinement based on real-world data.
145
-
146
- Squat Jump starts from a static squat position and focuses on explosive
147
- upward movement without countermovement. Power calculations require
148
- athlete mass for force and power metrics.
149
-
150
- Uses intelligent auto-tuning to select optimal parameters based on video
151
- characteristics. Parameters are automatically adjusted for frame rate,
152
- tracking quality, and analysis preset.
153
-
154
- VIDEO_PATH: Path(s) to video file(s). Supports glob patterns in batch mode.
155
-
156
- Examples:
157
-
158
- \b
159
- # Basic analysis
160
- kinemotion sj-analyze video.mp4 --mass 75.0
161
-
162
- \b
163
- # With debug video output
164
- kinemotion sj-analyze video.mp4 --mass 75.0 --output debug.mp4
165
-
166
- \b
167
- # Batch mode with glob pattern
168
- kinemotion sj-analyze videos/*.mp4 --batch --workers 4 --mass 75.0
169
-
170
- \b
171
- # Batch with output directories
172
- kinemotion sj-analyze videos/*.mp4 --batch --mass 75.0 \\
173
- --json-output-dir results/ --csv-summary summary.csv
174
- """
175
- # Warn user that SJ is experimental
176
- click.echo(
177
- "⚠️ WARNING: Squat Jump analysis is experimental (since v0.74.0)",
178
- err=True,
179
- )
180
- click.echo(
181
- " Power/force calculations use validated Sayers regression, but",
182
- err=True,
183
- )
184
- click.echo(
185
- " SJ-specific phase detection may need refinement based on real-world data.",
186
- err=True,
187
- )
188
- click.echo(err=True)
189
- # Expand glob patterns and collect all video files
190
- video_files = collect_video_files(video_path)
191
-
192
- if not video_files:
193
- click.echo("Error: No video files found", err=True)
194
- sys.exit(1)
195
-
196
- # Determine if batch mode should be used
197
- use_batch = batch or len(video_files) > 1
198
-
199
- quality_preset = QualityPreset(quality.lower())
200
-
201
- # Group expert parameters
202
- expert_params = AnalysisParameters(
203
- smoothing_window=smoothing_window,
204
- velocity_threshold=velocity_threshold,
205
- squat_hold_threshold=squat_hold_threshold,
206
- min_contact_frames=min_contact_frames,
207
- visibility_threshold=visibility_threshold,
208
- detection_confidence=detection_confidence,
209
- tracking_confidence=tracking_confidence,
210
- mass_kg=mass,
211
- )
212
-
213
- if use_batch:
214
- _process_batch_videos(
215
- video_files,
216
- output_dir,
217
- json_output_dir,
218
- quality_preset,
219
- verbose,
220
- expert_params,
221
- workers,
222
- )
223
- else:
224
- # Single video mode
225
- try:
226
- _process_single(
227
- video_files[0],
228
- output,
229
- json_output,
230
- quality_preset,
231
- verbose,
232
- expert_params,
233
- )
234
- except Exception as e:
235
- click.echo(f"Error: {e}", err=True)
236
- sys.exit(1)
237
-
238
-
239
- def _process_single(
240
- video_path: str,
241
- output: str | None,
242
- json_output: str | None,
243
- quality_preset: QualityPreset,
244
- verbose: bool,
245
- expert_params: AnalysisParameters,
246
- ) -> None:
247
- """Process a single SJ video by calling the API."""
248
- try:
249
- # Create overrides from expert parameters
250
- overrides = AnalysisOverrides(
251
- smoothing_window=expert_params.smoothing_window,
252
- velocity_threshold=expert_params.velocity_threshold,
253
- min_contact_frames=expert_params.min_contact_frames,
254
- visibility_threshold=expert_params.visibility_threshold,
255
- )
256
-
257
- # Call the API function (handles all processing logic)
258
- metrics = process_sj_video(
259
- video_path=video_path,
260
- quality=quality_preset.value,
261
- output_video=output,
262
- json_output=json_output,
263
- overrides=overrides,
264
- detection_confidence=expert_params.detection_confidence,
265
- tracking_confidence=expert_params.tracking_confidence,
266
- mass_kg=expert_params.mass_kg,
267
- verbose=verbose,
268
- )
269
-
270
- # Print formatted summary to stdout
271
- _output_results(metrics, json_output=None) # Don't write JSON (API already did)
272
-
273
- except Exception as e:
274
- click.echo(f"Error processing video: {e}", err=True)
275
- if verbose:
276
- import traceback
277
-
278
- traceback.print_exc()
279
- sys.exit(1)
280
-
281
-
282
- def _output_results(metrics: SJMetrics, json_output: str | None) -> None:
283
- """Output analysis results."""
284
- results = metrics.to_dict()
285
-
286
- # Output JSON
287
- if json_output:
288
- with open(json_output, "w") as f:
289
- json.dump(results, f, indent=2)
290
- click.echo(f"Metrics saved to: {json_output}", err=True)
291
- else:
292
- # Output to stdout
293
- print(json.dumps(results, indent=2))
294
-
295
- # Print summary
296
- click.echo("\n" + "=" * 60, err=True)
297
- click.echo("SJ ANALYSIS RESULTS", err=True)
298
- click.echo("=" * 60, err=True)
299
- click.echo(f"Jump height: {metrics.jump_height:.3f} m", err=True)
300
- click.echo(f"Flight time: {metrics.flight_time * 1000:.1f} ms", err=True)
301
- click.echo(f"Squat hold duration: {metrics.squat_hold_duration * 1000:.1f} ms", err=True)
302
- click.echo(f"Concentric duration: {metrics.concentric_duration * 1000:.1f} ms", err=True)
303
- click.echo(f"Peak concentric velocity: {metrics.peak_concentric_velocity:.3f} m/s", err=True)
304
- if metrics.peak_power is not None:
305
- click.echo(f"Peak power: {metrics.peak_power:.0f} W", err=True)
306
- click.echo(f"Mean power: {metrics.mean_power:.0f} W", err=True)
307
- if metrics.peak_force is not None:
308
- click.echo(f"Peak force: {metrics.peak_force:.0f} N", err=True)
309
- click.echo("=" * 60, err=True)
@@ -1,163 +0,0 @@
1
- """Debug overlay visualization for Squat Jump analysis."""
2
-
3
- import cv2
4
- import numpy as np
5
-
6
- from ..core.debug_overlay_utils import BaseDebugOverlayRenderer
7
- from ..core.overlay_constants import (
8
- CYAN,
9
- GREEN,
10
- PHASE_LABEL_LINE_HEIGHT,
11
- PHASE_LABEL_START_Y,
12
- RED,
13
- WHITE,
14
- Color,
15
- LandmarkDict,
16
- )
17
- from .analysis import SJPhase
18
- from .kinematics import SJMetrics
19
-
20
-
21
- class SquatJumpDebugOverlayRenderer(BaseDebugOverlayRenderer):
22
- """Debug overlay renderer for Squat Jump analysis results."""
23
-
24
- def _get_phase_color(self, phase: SJPhase) -> Color:
25
- """Get color based on jump phase."""
26
- phase_colors = {
27
- SJPhase.SQUAT_HOLD: (255, 255, 0), # Yellow
28
- SJPhase.CONCENTRIC: (0, 165, 255), # Orange
29
- SJPhase.FLIGHT: RED,
30
- SJPhase.LANDING: GREEN,
31
- SJPhase.UNKNOWN: WHITE,
32
- }
33
- return phase_colors.get(phase, WHITE)
34
-
35
- def render_frame(
36
- self,
37
- frame: np.ndarray,
38
- landmarks: LandmarkDict | None,
39
- frame_index: int,
40
- metrics: SJMetrics | None = None,
41
- ) -> np.ndarray:
42
- """Render debug overlay on a single frame.
43
-
44
- Args:
45
- frame: Input frame (BGR format)
46
- landmarks: Pose landmarks for the frame
47
- frame_index: Frame index for timeline display
48
- metrics: Analysis metrics for data display
49
-
50
- Returns:
51
- Annotated frame with debug overlay
52
- """
53
- # Create a copy to avoid modifying the original
54
- annotated_frame = frame.copy()
55
-
56
- # Determine current phase
57
- current_phase = SJPhase.UNKNOWN
58
- if metrics:
59
- if (
60
- metrics.squat_hold_start_frame is not None
61
- and metrics.concentric_start_frame is not None
62
- and metrics.squat_hold_start_frame <= frame_index < metrics.concentric_start_frame
63
- ):
64
- current_phase = SJPhase.SQUAT_HOLD
65
- elif (
66
- metrics.concentric_start_frame is not None
67
- and metrics.takeoff_frame is not None
68
- and metrics.concentric_start_frame <= frame_index < metrics.takeoff_frame
69
- ):
70
- current_phase = SJPhase.CONCENTRIC
71
- elif (
72
- metrics.takeoff_frame is not None
73
- and metrics.landing_frame is not None
74
- and metrics.takeoff_frame <= frame_index < metrics.landing_frame
75
- ):
76
- current_phase = SJPhase.FLIGHT
77
- elif (
78
- metrics.landing_frame is not None
79
- and metrics.landing_frame <= frame_index < metrics.landing_frame + 15
80
- ):
81
- current_phase = SJPhase.LANDING
82
-
83
- # Draw skeleton and landmarks
84
- if landmarks:
85
- self._draw_skeleton(annotated_frame, landmarks)
86
-
87
- # Draw frame information
88
- self._draw_frame_info(annotated_frame, frame_index, current_phase)
89
-
90
- # Draw metrics if available
91
- if metrics:
92
- self._draw_metrics(annotated_frame, metrics, frame_index)
93
-
94
- return annotated_frame
95
-
96
- def _draw_frame_info(self, frame: np.ndarray, frame_index: int, phase: SJPhase) -> None:
97
- """Draw frame information overlay.
98
-
99
- Args:
100
- frame: Frame to draw on
101
- frame_index: Current frame index
102
- phase: Current jump phase
103
- """
104
- # Draw frame counter
105
- cv2.putText(
106
- frame,
107
- f"Frame: {frame_index}",
108
- (10, 30),
109
- cv2.FONT_HERSHEY_SIMPLEX,
110
- 0.7,
111
- WHITE,
112
- 2,
113
- cv2.LINE_AA,
114
- )
115
-
116
- # Draw phase label
117
- phase_color = self._get_phase_color(phase)
118
- cv2.putText(
119
- frame,
120
- f"Phase: {phase.value.replace('_', ' ').upper()}",
121
- (10, 70),
122
- cv2.FONT_HERSHEY_SIMPLEX,
123
- 0.8,
124
- phase_color,
125
- 2,
126
- cv2.LINE_AA,
127
- )
128
-
129
- def _draw_metrics(self, frame: np.ndarray, metrics: SJMetrics, frame_index: int) -> None:
130
- """Draw metrics information on frame.
131
-
132
- Args:
133
- frame: Frame to draw on
134
- metrics: Metrics object with analysis results
135
- frame_index: Current frame index
136
- """
137
- # Only show summary metrics after takeoff or at the end
138
- if metrics.takeoff_frame is None or frame_index < metrics.takeoff_frame:
139
- return
140
-
141
- y_offset = PHASE_LABEL_START_Y + 100
142
-
143
- # Display key metrics
144
- metric_items: list[tuple[str, Color]] = [
145
- (f"Jump Height: {metrics.jump_height:.3f} m", WHITE),
146
- (f"Flight Time: {metrics.flight_time * 1000:.1f} ms", RED),
147
- (f"Concentric: {metrics.concentric_duration * 1000:.1f} ms", CYAN),
148
- ]
149
- if metrics.peak_power is not None:
150
- metric_items.append((f"Peak Power: {metrics.peak_power:.0f} W", GREEN))
151
-
152
- for text, color in metric_items:
153
- cv2.putText(
154
- frame,
155
- text,
156
- (10, y_offset),
157
- cv2.FONT_HERSHEY_SIMPLEX,
158
- 0.6,
159
- color,
160
- 2,
161
- cv2.LINE_AA,
162
- )
163
- y_offset += PHASE_LABEL_LINE_HEIGHT