kinemotion 0.1.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.

dropjump/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Drop-jump video analysis tool."""
2
+
3
+ __version__ = "0.1.0"
dropjump/cli.py ADDED
@@ -0,0 +1,294 @@
1
+ """Command-line interface for kinemetry analysis."""
2
+
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import click
8
+ import numpy as np
9
+
10
+ from .contact_detection import (
11
+ compute_average_foot_position,
12
+ detect_ground_contact,
13
+ )
14
+ from .kinematics import calculate_drop_jump_metrics
15
+ from .pose_tracker import PoseTracker
16
+ from .smoothing import smooth_landmarks
17
+ from .video_io import DebugOverlayRenderer, VideoProcessor
18
+
19
+
20
+ @click.group()
21
+ @click.version_option(package_name="dropjump-analyze")
22
+ def cli() -> None:
23
+ """Kinemetry: Video-based kinematic analysis for athletic performance."""
24
+ pass
25
+
26
+
27
+ @cli.command(name="dropjump-analyze")
28
+ @click.argument("video_path", type=click.Path(exists=True))
29
+ @click.option(
30
+ "--output",
31
+ "-o",
32
+ type=click.Path(),
33
+ help="Path for debug video output (optional)",
34
+ )
35
+ @click.option(
36
+ "--json-output",
37
+ "-j",
38
+ type=click.Path(),
39
+ help="Path for JSON metrics output (default: stdout)",
40
+ )
41
+ @click.option(
42
+ "--smoothing-window",
43
+ type=int,
44
+ default=5,
45
+ help="Smoothing window size (must be odd, >= 3)",
46
+ show_default=True,
47
+ )
48
+ @click.option(
49
+ "--velocity-threshold",
50
+ type=float,
51
+ default=0.02,
52
+ help="Velocity threshold for contact detection (normalized units)",
53
+ show_default=True,
54
+ )
55
+ @click.option(
56
+ "--min-contact-frames",
57
+ type=int,
58
+ default=3,
59
+ help="Minimum frames for valid ground contact",
60
+ show_default=True,
61
+ )
62
+ @click.option(
63
+ "--visibility-threshold",
64
+ type=float,
65
+ default=0.5,
66
+ help="Minimum landmark visibility score (0-1)",
67
+ show_default=True,
68
+ )
69
+ @click.option(
70
+ "--detection-confidence",
71
+ type=float,
72
+ default=0.5,
73
+ help="Pose detection confidence threshold (0-1)",
74
+ show_default=True,
75
+ )
76
+ @click.option(
77
+ "--tracking-confidence",
78
+ type=float,
79
+ default=0.5,
80
+ help="Pose tracking confidence threshold (0-1)",
81
+ show_default=True,
82
+ )
83
+ @click.option(
84
+ "--drop-height",
85
+ type=float,
86
+ default=None,
87
+ help="Height of drop box/platform in meters (e.g., 0.40 for 40cm) - used for calibration",
88
+ )
89
+ @click.option(
90
+ "--use-curvature/--no-curvature",
91
+ default=True,
92
+ help="Use trajectory curvature analysis for refining transitions (default: enabled)",
93
+ )
94
+ def dropjump_analyze(
95
+ video_path: str,
96
+ output: str | None,
97
+ json_output: str | None,
98
+ smoothing_window: int,
99
+ velocity_threshold: float,
100
+ min_contact_frames: int,
101
+ visibility_threshold: float,
102
+ detection_confidence: float,
103
+ tracking_confidence: float,
104
+ drop_height: float | None,
105
+ use_curvature: bool,
106
+ ) -> None:
107
+ """
108
+ Analyze drop-jump video to estimate ground contact time, flight time, and jump height.
109
+
110
+ VIDEO_PATH: Path to the input video file
111
+ """
112
+ click.echo(f"Analyzing video: {video_path}", err=True)
113
+
114
+ # Validate parameters
115
+ if smoothing_window < 3:
116
+ click.echo("Error: smoothing-window must be >= 3", err=True)
117
+ sys.exit(1)
118
+
119
+ if smoothing_window % 2 == 0:
120
+ smoothing_window += 1
121
+ click.echo(
122
+ f"Adjusting smoothing-window to {smoothing_window} (must be odd)", err=True
123
+ )
124
+
125
+ try:
126
+ # Initialize video processor
127
+ with VideoProcessor(video_path) as video:
128
+ click.echo(
129
+ f"Video: {video.width}x{video.height} @ {video.fps:.2f} fps, "
130
+ f"{video.frame_count} frames",
131
+ err=True,
132
+ )
133
+
134
+ # Initialize pose tracker
135
+ tracker = PoseTracker(
136
+ min_detection_confidence=detection_confidence,
137
+ min_tracking_confidence=tracking_confidence,
138
+ )
139
+
140
+ # Process all frames
141
+ click.echo("Tracking pose landmarks...", err=True)
142
+ landmarks_sequence = []
143
+ frames = []
144
+
145
+ frame_idx = 0
146
+ with click.progressbar(
147
+ length=video.frame_count, label="Processing frames"
148
+ ) as bar:
149
+ while True:
150
+ frame = video.read_frame()
151
+ if frame is None:
152
+ break
153
+
154
+ frames.append(frame)
155
+ landmarks = tracker.process_frame(frame)
156
+ landmarks_sequence.append(landmarks)
157
+
158
+ frame_idx += 1
159
+ bar.update(1)
160
+
161
+ tracker.close()
162
+
163
+ if not landmarks_sequence:
164
+ click.echo("Error: No frames processed", err=True)
165
+ sys.exit(1)
166
+
167
+ # Smooth landmarks
168
+ click.echo("Smoothing landmarks...", err=True)
169
+ smoothed_landmarks = smooth_landmarks(
170
+ landmarks_sequence, window_length=smoothing_window
171
+ )
172
+
173
+ # Extract foot positions
174
+ click.echo("Detecting ground contact...", err=True)
175
+ foot_positions_list: list[float] = []
176
+ visibilities_list: list[float] = []
177
+
178
+ for frame_landmarks in smoothed_landmarks:
179
+ if frame_landmarks:
180
+ foot_x, foot_y = compute_average_foot_position(frame_landmarks)
181
+ foot_positions_list.append(foot_y)
182
+
183
+ # Average visibility of foot landmarks
184
+ foot_vis = []
185
+ for key in [
186
+ "left_ankle",
187
+ "right_ankle",
188
+ "left_heel",
189
+ "right_heel",
190
+ ]:
191
+ if key in frame_landmarks:
192
+ foot_vis.append(frame_landmarks[key][2])
193
+ visibilities_list.append(
194
+ float(np.mean(foot_vis)) if foot_vis else 0.0
195
+ )
196
+ else:
197
+ # Use previous position if available, otherwise default
198
+ foot_positions_list.append(
199
+ foot_positions_list[-1] if foot_positions_list else 0.5
200
+ )
201
+ visibilities_list.append(0.0)
202
+
203
+ foot_positions: np.ndarray = np.array(foot_positions_list)
204
+ visibilities: np.ndarray = np.array(visibilities_list)
205
+
206
+ # Detect ground contact
207
+ contact_states = detect_ground_contact(
208
+ foot_positions,
209
+ velocity_threshold=velocity_threshold,
210
+ min_contact_frames=min_contact_frames,
211
+ visibility_threshold=visibility_threshold,
212
+ visibilities=visibilities,
213
+ )
214
+
215
+ # Calculate metrics
216
+ click.echo("Calculating metrics...", err=True)
217
+ if drop_height:
218
+ click.echo(
219
+ f"Using drop height calibration: {drop_height}m ({drop_height*100:.0f}cm)",
220
+ err=True,
221
+ )
222
+ metrics = calculate_drop_jump_metrics(
223
+ contact_states,
224
+ foot_positions,
225
+ video.fps,
226
+ drop_height_m=drop_height,
227
+ velocity_threshold=velocity_threshold,
228
+ smoothing_window=smoothing_window,
229
+ use_curvature=use_curvature,
230
+ )
231
+
232
+ # Output metrics as JSON
233
+ metrics_dict = metrics.to_dict()
234
+ metrics_json = json.dumps(metrics_dict, indent=2)
235
+
236
+ if json_output:
237
+ output_path = Path(json_output)
238
+ output_path.write_text(metrics_json)
239
+ click.echo(f"Metrics written to: {json_output}", err=True)
240
+ else:
241
+ click.echo(metrics_json)
242
+
243
+ # Generate debug video if requested
244
+ if output:
245
+ click.echo(f"Generating debug video: {output}", err=True)
246
+ if video.display_width != video.width or video.display_height != video.height:
247
+ click.echo(
248
+ f"Source video encoded: {video.width}x{video.height}",
249
+ err=True,
250
+ )
251
+ click.echo(
252
+ f"Output dimensions: {video.display_width}x{video.display_height} "
253
+ f"(respecting display aspect ratio)",
254
+ err=True,
255
+ )
256
+ else:
257
+ click.echo(
258
+ f"Output dimensions: {video.width}x{video.height} "
259
+ f"(matching source video aspect ratio)",
260
+ err=True,
261
+ )
262
+ with DebugOverlayRenderer(
263
+ output,
264
+ video.width,
265
+ video.height,
266
+ video.display_width,
267
+ video.display_height,
268
+ video.fps,
269
+ ) as renderer:
270
+ with click.progressbar(
271
+ length=len(frames), label="Rendering frames"
272
+ ) as bar:
273
+ for i, frame in enumerate(frames):
274
+ annotated = renderer.render_frame(
275
+ frame,
276
+ smoothed_landmarks[i],
277
+ contact_states[i],
278
+ i,
279
+ metrics,
280
+ )
281
+ renderer.write_frame(annotated)
282
+ bar.update(1)
283
+
284
+ click.echo(f"Debug video saved: {output}", err=True)
285
+
286
+ click.echo("Analysis complete!", err=True)
287
+
288
+ except Exception as e:
289
+ click.echo(f"Error: {str(e)}", err=True)
290
+ sys.exit(1)
291
+
292
+
293
+ if __name__ == "__main__":
294
+ cli()