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.
- kinemotion/__init__.py +3 -18
- kinemotion/api.py +7 -27
- kinemotion/cli.py +2 -4
- kinemotion/{countermovement_jump → cmj}/analysis.py +158 -16
- kinemotion/{countermovement_jump → cmj}/api.py +18 -46
- kinemotion/{countermovement_jump → cmj}/cli.py +46 -6
- kinemotion/cmj/debug_overlay.py +457 -0
- kinemotion/{countermovement_jump → cmj}/joint_angles.py +31 -96
- kinemotion/{countermovement_jump → cmj}/metrics_validator.py +293 -184
- kinemotion/{countermovement_jump → cmj}/validation_bounds.py +18 -1
- kinemotion/core/__init__.py +2 -11
- kinemotion/core/auto_tuning.py +107 -149
- kinemotion/core/cli_utils.py +0 -74
- kinemotion/core/debug_overlay_utils.py +15 -142
- kinemotion/core/experimental.py +51 -55
- kinemotion/core/filtering.py +56 -116
- kinemotion/core/pipeline_utils.py +2 -2
- kinemotion/core/pose.py +98 -47
- kinemotion/core/quality.py +6 -4
- kinemotion/core/smoothing.py +51 -65
- kinemotion/core/types.py +0 -15
- kinemotion/core/validation.py +7 -76
- kinemotion/core/video_io.py +27 -41
- kinemotion/{drop_jump → dropjump}/__init__.py +8 -2
- kinemotion/{drop_jump → dropjump}/analysis.py +120 -282
- kinemotion/{drop_jump → dropjump}/api.py +33 -59
- kinemotion/{drop_jump → dropjump}/cli.py +136 -70
- kinemotion/dropjump/debug_overlay.py +182 -0
- kinemotion/{drop_jump → dropjump}/kinematics.py +65 -175
- kinemotion/{drop_jump → dropjump}/metrics_validator.py +51 -25
- kinemotion/{drop_jump → dropjump}/validation_bounds.py +1 -1
- kinemotion/models/rtmpose-s_simcc-body7_pt-body7-halpe26_700e-256x192-7f134165_20230605.onnx +3 -0
- kinemotion/models/yolox_tiny_8xb8-300e_humanart-6f3252f9.onnx +3 -0
- {kinemotion-0.76.3.dist-info → kinemotion-1.0.0.dist-info}/METADATA +26 -75
- kinemotion-1.0.0.dist-info/RECORD +49 -0
- kinemotion/core/overlay_constants.py +0 -61
- kinemotion/core/video_analysis_base.py +0 -132
- kinemotion/countermovement_jump/debug_overlay.py +0 -325
- kinemotion/drop_jump/debug_overlay.py +0 -241
- kinemotion/squat_jump/__init__.py +0 -5
- kinemotion/squat_jump/analysis.py +0 -377
- kinemotion/squat_jump/api.py +0 -610
- kinemotion/squat_jump/cli.py +0 -309
- kinemotion/squat_jump/debug_overlay.py +0 -163
- kinemotion/squat_jump/kinematics.py +0 -342
- kinemotion/squat_jump/metrics_validator.py +0 -438
- kinemotion/squat_jump/validation_bounds.py +0 -221
- kinemotion-0.76.3.dist-info/RECORD +0 -57
- /kinemotion/{countermovement_jump → cmj}/__init__.py +0 -0
- /kinemotion/{countermovement_jump → cmj}/kinematics.py +0 -0
- {kinemotion-0.76.3.dist-info → kinemotion-1.0.0.dist-info}/WHEEL +0 -0
- {kinemotion-0.76.3.dist-info → kinemotion-1.0.0.dist-info}/entry_points.txt +0 -0
- {kinemotion-0.76.3.dist-info → kinemotion-1.0.0.dist-info}/licenses/LICENSE +0 -0
kinemotion/squat_jump/cli.py
DELETED
|
@@ -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
|