kinemotion 0.8.2__py3-none-any.whl → 0.9.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/__init__.py CHANGED
@@ -1,3 +1,14 @@
1
1
  """Kinemotion: Video-based kinematic analysis for athletic performance."""
2
2
 
3
+ from .api import VideoConfig, VideoResult, process_video, process_videos_bulk
4
+ from .dropjump.kinematics import DropJumpMetrics
5
+
3
6
  __version__ = "0.1.0"
7
+
8
+ __all__ = [
9
+ "process_video",
10
+ "process_videos_bulk",
11
+ "VideoConfig",
12
+ "VideoResult",
13
+ "DropJumpMetrics",
14
+ ]
kinemotion/api.py ADDED
@@ -0,0 +1,428 @@
1
+ """Public API for programmatic use of kinemotion analysis."""
2
+
3
+ import time
4
+ from collections.abc import Callable
5
+ from concurrent.futures import ProcessPoolExecutor, as_completed
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+ import numpy as np
10
+
11
+ from .core.auto_tuning import (
12
+ QualityPreset,
13
+ analyze_video_sample,
14
+ auto_tune_parameters,
15
+ )
16
+ from .core.pose import PoseTracker
17
+ from .core.smoothing import smooth_landmarks, smooth_landmarks_advanced
18
+ from .core.video_io import VideoProcessor
19
+ from .dropjump.analysis import compute_average_foot_position, detect_ground_contact
20
+ from .dropjump.debug_overlay import DebugOverlayRenderer
21
+ from .dropjump.kinematics import DropJumpMetrics, calculate_drop_jump_metrics
22
+
23
+
24
+ @dataclass
25
+ class VideoResult:
26
+ """Result of processing a single video."""
27
+
28
+ video_path: str
29
+ success: bool
30
+ metrics: DropJumpMetrics | None = None
31
+ error: str | None = None
32
+ processing_time: float = 0.0
33
+
34
+
35
+ @dataclass
36
+ class VideoConfig:
37
+ """Configuration for processing a single video."""
38
+
39
+ video_path: str
40
+ drop_height: float
41
+ quality: str = "balanced"
42
+ output_video: str | None = None
43
+ json_output: str | None = None
44
+ drop_start_frame: int | None = None
45
+ smoothing_window: int | None = None
46
+ velocity_threshold: float | None = None
47
+ min_contact_frames: int | None = None
48
+ visibility_threshold: float | None = None
49
+ detection_confidence: float | None = None
50
+ tracking_confidence: float | None = None
51
+
52
+
53
+ def process_video(
54
+ video_path: str,
55
+ drop_height: float,
56
+ quality: str = "balanced",
57
+ output_video: str | None = None,
58
+ json_output: str | None = None,
59
+ drop_start_frame: int | None = None,
60
+ smoothing_window: int | None = None,
61
+ velocity_threshold: float | None = None,
62
+ min_contact_frames: int | None = None,
63
+ visibility_threshold: float | None = None,
64
+ detection_confidence: float | None = None,
65
+ tracking_confidence: float | None = None,
66
+ verbose: bool = False,
67
+ ) -> DropJumpMetrics:
68
+ """
69
+ Process a single drop jump video and return metrics.
70
+
71
+ Args:
72
+ video_path: Path to the input video file
73
+ drop_height: Height of drop box/platform in meters (e.g., 0.40 for 40cm)
74
+ quality: Analysis quality preset ("fast", "balanced", or "accurate")
75
+ output_video: Optional path for debug video output
76
+ json_output: Optional path for JSON metrics output
77
+ drop_start_frame: Optional manual drop start frame
78
+ smoothing_window: Optional override for smoothing window
79
+ velocity_threshold: Optional override for velocity threshold
80
+ min_contact_frames: Optional override for minimum contact frames
81
+ visibility_threshold: Optional override for visibility threshold
82
+ detection_confidence: Optional override for pose detection confidence
83
+ tracking_confidence: Optional override for pose tracking confidence
84
+ verbose: Print processing details
85
+
86
+ Returns:
87
+ DropJumpMetrics object containing analysis results
88
+
89
+ Raises:
90
+ ValueError: If video cannot be processed or parameters are invalid
91
+ FileNotFoundError: If video file does not exist
92
+ """
93
+ if not Path(video_path).exists():
94
+ raise FileNotFoundError(f"Video file not found: {video_path}")
95
+
96
+ # Convert quality string to enum
97
+ try:
98
+ quality_preset = QualityPreset(quality.lower())
99
+ except ValueError as e:
100
+ raise ValueError(
101
+ f"Invalid quality preset: {quality}. Must be 'fast', 'balanced', or 'accurate'"
102
+ ) from e
103
+
104
+ # Initialize video processor
105
+ with VideoProcessor(video_path) as video:
106
+ if verbose:
107
+ print(
108
+ f"Video: {video.width}x{video.height} @ {video.fps:.2f} fps, "
109
+ f"{video.frame_count} frames"
110
+ )
111
+
112
+ # Determine initial detection/tracking confidence from quality preset
113
+ initial_detection_conf = 0.5
114
+ initial_tracking_conf = 0.5
115
+
116
+ if quality_preset == QualityPreset.FAST:
117
+ initial_detection_conf = 0.3
118
+ initial_tracking_conf = 0.3
119
+ elif quality_preset == QualityPreset.ACCURATE:
120
+ initial_detection_conf = 0.6
121
+ initial_tracking_conf = 0.6
122
+
123
+ # Override with expert values if provided
124
+ if detection_confidence is not None:
125
+ initial_detection_conf = detection_confidence
126
+ if tracking_confidence is not None:
127
+ initial_tracking_conf = tracking_confidence
128
+
129
+ # Initialize pose tracker
130
+ tracker = PoseTracker(
131
+ min_detection_confidence=initial_detection_conf,
132
+ min_tracking_confidence=initial_tracking_conf,
133
+ )
134
+
135
+ # Process all frames
136
+ if verbose:
137
+ print("Tracking pose landmarks...")
138
+
139
+ landmarks_sequence = []
140
+ frames = []
141
+
142
+ while True:
143
+ frame = video.read_frame()
144
+ if frame is None:
145
+ break
146
+
147
+ frames.append(frame)
148
+ landmarks = tracker.process_frame(frame)
149
+ landmarks_sequence.append(landmarks)
150
+
151
+ tracker.close()
152
+
153
+ if not landmarks_sequence:
154
+ raise ValueError("No frames could be processed from video")
155
+
156
+ # Analyze video characteristics and auto-tune parameters
157
+ characteristics = analyze_video_sample(
158
+ landmarks_sequence, video.fps, video.frame_count
159
+ )
160
+
161
+ params = auto_tune_parameters(characteristics, quality_preset)
162
+
163
+ # Apply expert overrides if provided
164
+ if smoothing_window is not None:
165
+ params.smoothing_window = smoothing_window
166
+ if velocity_threshold is not None:
167
+ params.velocity_threshold = velocity_threshold
168
+ if min_contact_frames is not None:
169
+ params.min_contact_frames = min_contact_frames
170
+ if visibility_threshold is not None:
171
+ params.visibility_threshold = visibility_threshold
172
+
173
+ # Show selected parameters if verbose
174
+ if verbose:
175
+ print("\n" + "=" * 60)
176
+ print("AUTO-TUNED PARAMETERS")
177
+ print("=" * 60)
178
+ print(f"Video FPS: {video.fps:.2f}")
179
+ print(
180
+ f"Tracking quality: {characteristics.tracking_quality} "
181
+ f"(avg visibility: {characteristics.avg_visibility:.2f})"
182
+ )
183
+ print(f"Quality preset: {quality_preset.value}")
184
+ print("\nSelected parameters:")
185
+ print(f" smoothing_window: {params.smoothing_window}")
186
+ print(f" polyorder: {params.polyorder}")
187
+ print(f" velocity_threshold: {params.velocity_threshold:.4f}")
188
+ print(f" min_contact_frames: {params.min_contact_frames}")
189
+ print(f" visibility_threshold: {params.visibility_threshold}")
190
+ print(f" detection_confidence: {params.detection_confidence}")
191
+ print(f" tracking_confidence: {params.tracking_confidence}")
192
+ print(f" outlier_rejection: {params.outlier_rejection}")
193
+ print(f" bilateral_filter: {params.bilateral_filter}")
194
+ print(f" use_curvature: {params.use_curvature}")
195
+ print("=" * 60 + "\n")
196
+
197
+ # Apply smoothing with auto-tuned parameters
198
+ if params.outlier_rejection or params.bilateral_filter:
199
+ if verbose:
200
+ if params.outlier_rejection:
201
+ print("Smoothing landmarks with outlier rejection...")
202
+ if params.bilateral_filter:
203
+ print("Using bilateral temporal filter...")
204
+ smoothed_landmarks = smooth_landmarks_advanced(
205
+ landmarks_sequence,
206
+ window_length=params.smoothing_window,
207
+ polyorder=params.polyorder,
208
+ use_outlier_rejection=params.outlier_rejection,
209
+ use_bilateral=params.bilateral_filter,
210
+ )
211
+ else:
212
+ if verbose:
213
+ print("Smoothing landmarks...")
214
+ smoothed_landmarks = smooth_landmarks(
215
+ landmarks_sequence,
216
+ window_length=params.smoothing_window,
217
+ polyorder=params.polyorder,
218
+ )
219
+
220
+ # Extract vertical positions from feet
221
+ if verbose:
222
+ print("Extracting foot positions...")
223
+
224
+ position_list: list[float] = []
225
+ visibilities_list: list[float] = []
226
+
227
+ for frame_landmarks in smoothed_landmarks:
228
+ if frame_landmarks:
229
+ _, foot_y = compute_average_foot_position(frame_landmarks)
230
+ position_list.append(foot_y)
231
+
232
+ # Average visibility of foot landmarks
233
+ foot_vis = []
234
+ for key in ["left_ankle", "right_ankle", "left_heel", "right_heel"]:
235
+ if key in frame_landmarks:
236
+ foot_vis.append(frame_landmarks[key][2])
237
+ visibilities_list.append(float(np.mean(foot_vis)) if foot_vis else 0.0)
238
+ else:
239
+ position_list.append(position_list[-1] if position_list else 0.5)
240
+ visibilities_list.append(0.0)
241
+
242
+ vertical_positions: np.ndarray = np.array(position_list)
243
+ visibilities: np.ndarray = np.array(visibilities_list)
244
+
245
+ # Detect ground contact
246
+ contact_states = detect_ground_contact(
247
+ vertical_positions,
248
+ velocity_threshold=params.velocity_threshold,
249
+ min_contact_frames=params.min_contact_frames,
250
+ visibility_threshold=params.visibility_threshold,
251
+ visibilities=visibilities,
252
+ window_length=params.smoothing_window,
253
+ polyorder=params.polyorder,
254
+ )
255
+
256
+ # Calculate metrics
257
+ if verbose:
258
+ print("Calculating metrics...")
259
+ print(
260
+ f"Using drop height calibration: {drop_height}m ({drop_height*100:.0f}cm)"
261
+ )
262
+
263
+ metrics = calculate_drop_jump_metrics(
264
+ contact_states,
265
+ vertical_positions,
266
+ video.fps,
267
+ drop_height_m=drop_height,
268
+ drop_start_frame=drop_start_frame,
269
+ velocity_threshold=params.velocity_threshold,
270
+ smoothing_window=params.smoothing_window,
271
+ polyorder=params.polyorder,
272
+ use_curvature=params.use_curvature,
273
+ kinematic_correction_factor=1.0,
274
+ )
275
+
276
+ # Save JSON if requested
277
+ if json_output:
278
+ import json
279
+
280
+ output_path = Path(json_output)
281
+ output_path.write_text(json.dumps(metrics.to_dict(), indent=2))
282
+ if verbose:
283
+ print(f"Metrics written to: {json_output}")
284
+
285
+ # Generate debug video if requested
286
+ if output_video:
287
+ if verbose:
288
+ print(f"Generating debug video: {output_video}")
289
+
290
+ with DebugOverlayRenderer(
291
+ output_video,
292
+ video.width,
293
+ video.height,
294
+ video.display_width,
295
+ video.display_height,
296
+ video.fps,
297
+ ) as renderer:
298
+ for i, frame in enumerate(frames):
299
+ annotated = renderer.render_frame(
300
+ frame,
301
+ smoothed_landmarks[i],
302
+ contact_states[i],
303
+ i,
304
+ metrics,
305
+ use_com=False,
306
+ )
307
+ renderer.write_frame(annotated)
308
+
309
+ if verbose:
310
+ print(f"Debug video saved: {output_video}")
311
+
312
+ if verbose:
313
+ print("Analysis complete!")
314
+
315
+ return metrics
316
+
317
+
318
+ def process_videos_bulk(
319
+ configs: list[VideoConfig],
320
+ max_workers: int = 4,
321
+ progress_callback: Callable[[VideoResult], None] | None = None,
322
+ ) -> list[VideoResult]:
323
+ """
324
+ Process multiple videos in parallel using ProcessPoolExecutor.
325
+
326
+ Args:
327
+ configs: List of VideoConfig objects specifying video paths and parameters
328
+ max_workers: Maximum number of parallel workers (default: 4)
329
+ progress_callback: Optional callback function called after each video completes.
330
+ Receives VideoResult object.
331
+
332
+ Returns:
333
+ List of VideoResult objects, one per input video, in completion order
334
+
335
+ Example:
336
+ >>> configs = [
337
+ ... VideoConfig("video1.mp4", drop_height=0.40),
338
+ ... VideoConfig("video2.mp4", drop_height=0.30, quality="accurate"),
339
+ ... VideoConfig("video3.mp4", drop_height=0.50, output_video="debug3.mp4"),
340
+ ... ]
341
+ >>> results = process_videos_bulk(configs, max_workers=4)
342
+ >>> for result in results:
343
+ ... if result.success:
344
+ ... print(f"{result.video_path}: {result.metrics.jump_height_m:.3f}m")
345
+ ... else:
346
+ ... print(f"{result.video_path}: FAILED - {result.error}")
347
+ """
348
+ results: list[VideoResult] = []
349
+
350
+ # Use ProcessPoolExecutor for CPU-bound video processing
351
+ with ProcessPoolExecutor(max_workers=max_workers) as executor:
352
+ # Submit all jobs
353
+ future_to_config = {
354
+ executor.submit(_process_video_wrapper, config): config
355
+ for config in configs
356
+ }
357
+
358
+ # Process results as they complete
359
+ for future in as_completed(future_to_config):
360
+ config = future_to_config[future]
361
+ result: VideoResult
362
+
363
+ try:
364
+ result = future.result()
365
+ except Exception as exc:
366
+ # Handle unexpected errors
367
+ result = VideoResult(
368
+ video_path=config.video_path,
369
+ success=False,
370
+ error=f"Unexpected error: {str(exc)}",
371
+ )
372
+
373
+ results.append(result)
374
+
375
+ # Call progress callback if provided
376
+ if progress_callback:
377
+ progress_callback(result)
378
+
379
+ return results
380
+
381
+
382
+ def _process_video_wrapper(config: VideoConfig) -> VideoResult:
383
+ """
384
+ Wrapper function for parallel processing. Must be picklable (top-level function).
385
+
386
+ Args:
387
+ config: VideoConfig object with processing parameters
388
+
389
+ Returns:
390
+ VideoResult object with metrics or error information
391
+ """
392
+ start_time = time.time()
393
+
394
+ try:
395
+ metrics = process_video(
396
+ video_path=config.video_path,
397
+ drop_height=config.drop_height,
398
+ quality=config.quality,
399
+ output_video=config.output_video,
400
+ json_output=config.json_output,
401
+ drop_start_frame=config.drop_start_frame,
402
+ smoothing_window=config.smoothing_window,
403
+ velocity_threshold=config.velocity_threshold,
404
+ min_contact_frames=config.min_contact_frames,
405
+ visibility_threshold=config.visibility_threshold,
406
+ detection_confidence=config.detection_confidence,
407
+ tracking_confidence=config.tracking_confidence,
408
+ verbose=False, # Disable verbose in parallel mode
409
+ )
410
+
411
+ processing_time = time.time() - start_time
412
+
413
+ return VideoResult(
414
+ video_path=config.video_path,
415
+ success=True,
416
+ metrics=metrics,
417
+ processing_time=processing_time,
418
+ )
419
+
420
+ except Exception as e:
421
+ processing_time = time.time() - start_time
422
+
423
+ return VideoResult(
424
+ video_path=config.video_path,
425
+ success=False,
426
+ error=str(e),
427
+ processing_time=processing_time,
428
+ )
kinemotion/py.typed ADDED
File without changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kinemotion
3
- Version: 0.8.2
3
+ Version: 0.9.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,4 +1,5 @@
1
- kinemotion/__init__.py,sha256=JhS0ZTgcTdcMH5WcIyWxEqZJPOoBUSKX8tT8hsG-xWk,98
1
+ kinemotion/__init__.py,sha256=Z85xg29NA-r4IjrSbAkJpMFigyxACFGUb-37AiMp6YY,350
2
+ kinemotion/api.py,sha256=3Hswx5lfyWc-EanS6iV-4MPUa_uB5t8BpGe4EB4gIIQ,15453
2
3
  kinemotion/cli.py,sha256=2IFA2_TE9a5zBtmGVzv5SnX39w7yPuBlw42dL7ca25U,402
3
4
  kinemotion/core/__init__.py,sha256=3yzDhb5PekDNjydqrs8aWGneUGJBt-lB0SoB_Y2FXqU,1010
4
5
  kinemotion/core/auto_tuning.py,sha256=cvmxUI-CbahpOJQtR2r5jOx4Q6yKPe3DO1o15hOQIdw,10508
@@ -11,8 +12,9 @@ kinemotion/dropjump/analysis.py,sha256=HfJt2t9IsMBiBUz7apIzdxbRH9QqzlFnDVVWcKhU3
11
12
  kinemotion/dropjump/cli.py,sha256=nhcqYClTx9R0XeTduJCNspltNgeaK4W8ZUT1ACB8GFI,15601
12
13
  kinemotion/dropjump/debug_overlay.py,sha256=hmEtadqYP8K-kGr_Q03KDQyl1152-YSPeRJzEXMyuhs,8687
13
14
  kinemotion/dropjump/kinematics.py,sha256=RceIH2HndpHQpcOQd56MmEdXQNEst-CWXfBKPJk2g3Y,17659
14
- kinemotion-0.8.2.dist-info/METADATA,sha256=yV6syFKbsNo1gNzpZ1O54B6IHMn9WosJ96-US7mHMtQ,16318
15
- kinemotion-0.8.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
- kinemotion-0.8.2.dist-info/entry_points.txt,sha256=zaqnAnjLvcdrk1Qvj5nvXZCZ2gp0prS7it1zTJygcIY,50
17
- kinemotion-0.8.2.dist-info/licenses/LICENSE,sha256=KZajvqsHw0NoOHOi2q0FZ4NBe9HdV6oey-IPYAtHXfg,1088
18
- kinemotion-0.8.2.dist-info/RECORD,,
15
+ kinemotion/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ kinemotion-0.9.0.dist-info/METADATA,sha256=vGSBf1eZMZso9z-eg2C6-mwWQcyJW4aADAjuiCnBAKs,16318
17
+ kinemotion-0.9.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
18
+ kinemotion-0.9.0.dist-info/entry_points.txt,sha256=zaqnAnjLvcdrk1Qvj5nvXZCZ2gp0prS7it1zTJygcIY,50
19
+ kinemotion-0.9.0.dist-info/licenses/LICENSE,sha256=KZajvqsHw0NoOHOi2q0FZ4NBe9HdV6oey-IPYAtHXfg,1088
20
+ kinemotion-0.9.0.dist-info/RECORD,,