kinemotion 0.73.0__py3-none-any.whl → 0.75.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/core/__init__.py +11 -0
- kinemotion/core/auto_tuning.py +74 -27
- kinemotion/core/cli_utils.py +74 -0
- kinemotion/core/filtering.py +101 -45
- kinemotion/core/quality.py +4 -6
- kinemotion/core/validation.py +70 -0
- kinemotion/core/video_analysis_base.py +132 -0
- kinemotion/core/video_io.py +27 -18
- kinemotion/countermovement_jump/analysis.py +0 -97
- kinemotion/countermovement_jump/api.py +37 -11
- kinemotion/countermovement_jump/cli.py +6 -46
- kinemotion/countermovement_jump/metrics_validator.py +143 -229
- kinemotion/drop_jump/analysis.py +82 -37
- kinemotion/drop_jump/api.py +46 -16
- kinemotion/drop_jump/cli.py +8 -58
- kinemotion/drop_jump/kinematics.py +98 -50
- kinemotion/drop_jump/metrics_validator.py +24 -50
- {kinemotion-0.73.0.dist-info → kinemotion-0.75.0.dist-info}/METADATA +1 -1
- {kinemotion-0.73.0.dist-info → kinemotion-0.75.0.dist-info}/RECORD +22 -21
- {kinemotion-0.73.0.dist-info → kinemotion-0.75.0.dist-info}/WHEEL +0 -0
- {kinemotion-0.73.0.dist-info → kinemotion-0.75.0.dist-info}/entry_points.txt +0 -0
- {kinemotion-0.73.0.dist-info → kinemotion-0.75.0.dist-info}/licenses/LICENSE +0 -0
kinemotion/core/video_io.py
CHANGED
|
@@ -50,9 +50,27 @@ class VideoProcessor:
|
|
|
50
50
|
self._current_timestamp_ms: int = 0 # Timestamp for the current frame
|
|
51
51
|
|
|
52
52
|
# Read first frame to get actual dimensions
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
#
|
|
53
|
+
self._extract_dimensions_from_frame()
|
|
54
|
+
|
|
55
|
+
# Initialize metadata placeholders
|
|
56
|
+
self.rotation = 0 # Will be set by _extract_video_metadata()
|
|
57
|
+
self.codec: str | None = None # Will be set by _extract_video_metadata()
|
|
58
|
+
|
|
59
|
+
# Initialize display dimensions (may be adjusted by SAR metadata)
|
|
60
|
+
self.display_width = self.width
|
|
61
|
+
self.display_height = self.height
|
|
62
|
+
self._extract_video_metadata()
|
|
63
|
+
|
|
64
|
+
# Apply rotation to dimensions if needed
|
|
65
|
+
self._apply_rotation_to_dimensions()
|
|
66
|
+
|
|
67
|
+
def _extract_dimensions_from_frame(self) -> None:
|
|
68
|
+
"""Extract video dimensions by reading the first frame.
|
|
69
|
+
|
|
70
|
+
This is critical for preserving aspect ratio, especially with mobile videos
|
|
71
|
+
that have rotation metadata. OpenCV properties (CAP_PROP_FRAME_WIDTH/HEIGHT)
|
|
72
|
+
may return incorrect dimensions, so we read the actual frame data.
|
|
73
|
+
"""
|
|
56
74
|
ret, first_frame = self.cap.read()
|
|
57
75
|
if ret:
|
|
58
76
|
# frame.shape is (height, width, channels) - extract actual dimensions
|
|
@@ -63,22 +81,13 @@ class VideoProcessor:
|
|
|
63
81
|
self.width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
64
82
|
self.height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
65
83
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
# extract and apply it manually
|
|
69
|
-
self.rotation = 0 # Will be set by _extract_video_metadata()
|
|
70
|
-
|
|
71
|
-
# Extract codec information from video metadata
|
|
72
|
-
self.codec: str | None = None # Will be set by _extract_video_metadata()
|
|
73
|
-
|
|
74
|
-
# Calculate display dimensions considering SAR (Sample Aspect Ratio)
|
|
75
|
-
# Mobile videos often have non-square pixels encoded in SAR metadata
|
|
76
|
-
# OpenCV doesn't directly expose SAR, but we need to handle display correctly
|
|
77
|
-
self.display_width = self.width
|
|
78
|
-
self.display_height = self.height
|
|
79
|
-
self._extract_video_metadata()
|
|
84
|
+
def _apply_rotation_to_dimensions(self) -> None:
|
|
85
|
+
"""Swap width/height for 90/-90 degree rotations.
|
|
80
86
|
|
|
81
|
-
|
|
87
|
+
Extract rotation metadata from video (iPhones store rotation in
|
|
88
|
+
side_data_list). OpenCV ignores rotation metadata, so we need to
|
|
89
|
+
extract and apply it manually.
|
|
90
|
+
"""
|
|
82
91
|
if self.rotation in [90, -90, 270]:
|
|
83
92
|
# Swap dimensions for 90/-90 degree rotations
|
|
84
93
|
self.width, self.height = self.height, self.width
|
|
@@ -55,103 +55,6 @@ class CMJPhase(Enum):
|
|
|
55
55
|
UNKNOWN = "unknown"
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
@unused(
|
|
59
|
-
reason="Alternative implementation not called by pipeline",
|
|
60
|
-
since="0.34.0",
|
|
61
|
-
)
|
|
62
|
-
def find_standing_phase(
|
|
63
|
-
positions: FloatArray,
|
|
64
|
-
velocities: FloatArray,
|
|
65
|
-
fps: float,
|
|
66
|
-
min_standing_duration: float = 0.5,
|
|
67
|
-
velocity_threshold: float = 0.01,
|
|
68
|
-
) -> int | None:
|
|
69
|
-
"""
|
|
70
|
-
Find the end of standing phase (start of countermovement).
|
|
71
|
-
|
|
72
|
-
Looks for a period of low velocity (standing) followed by consistent
|
|
73
|
-
downward motion.
|
|
74
|
-
|
|
75
|
-
Args:
|
|
76
|
-
positions: Array of vertical positions (normalized 0-1)
|
|
77
|
-
velocities: Array of vertical velocities
|
|
78
|
-
fps: Video frame rate
|
|
79
|
-
min_standing_duration: Minimum standing duration in seconds (default: 0.5s)
|
|
80
|
-
velocity_threshold: Velocity threshold for standing detection
|
|
81
|
-
|
|
82
|
-
Returns:
|
|
83
|
-
Frame index where countermovement begins, or None if not detected.
|
|
84
|
-
"""
|
|
85
|
-
min_standing_frames = int(fps * min_standing_duration)
|
|
86
|
-
|
|
87
|
-
if len(positions) < min_standing_frames:
|
|
88
|
-
return None
|
|
89
|
-
|
|
90
|
-
# Find periods of low velocity (standing)
|
|
91
|
-
is_standing = np.abs(velocities) < velocity_threshold
|
|
92
|
-
|
|
93
|
-
# Look for first sustained standing period
|
|
94
|
-
standing_count = 0
|
|
95
|
-
standing_end = None
|
|
96
|
-
|
|
97
|
-
for i in range(len(is_standing)):
|
|
98
|
-
if is_standing[i]:
|
|
99
|
-
standing_count += 1
|
|
100
|
-
if standing_count >= min_standing_frames:
|
|
101
|
-
standing_end = i
|
|
102
|
-
else:
|
|
103
|
-
if standing_end is not None:
|
|
104
|
-
# Found end of standing phase
|
|
105
|
-
return standing_end
|
|
106
|
-
standing_count = 0
|
|
107
|
-
|
|
108
|
-
return None
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
@unused(
|
|
112
|
-
reason="Alternative implementation not called by pipeline",
|
|
113
|
-
since="0.34.0",
|
|
114
|
-
)
|
|
115
|
-
def find_countermovement_start(
|
|
116
|
-
velocities: FloatArray,
|
|
117
|
-
countermovement_threshold: float = 0.015,
|
|
118
|
-
min_eccentric_frames: int = 3,
|
|
119
|
-
standing_start: int | None = None,
|
|
120
|
-
) -> int | None:
|
|
121
|
-
"""
|
|
122
|
-
Find the start of countermovement (eccentric phase).
|
|
123
|
-
|
|
124
|
-
Detects when velocity becomes consistently positive (downward motion in
|
|
125
|
-
normalized coords).
|
|
126
|
-
|
|
127
|
-
Args:
|
|
128
|
-
velocities: Array of SIGNED vertical velocities
|
|
129
|
-
countermovement_threshold: Velocity threshold for detecting downward
|
|
130
|
-
motion (POSITIVE)
|
|
131
|
-
min_eccentric_frames: Minimum consecutive frames of downward motion
|
|
132
|
-
standing_start: Optional frame where standing phase ended
|
|
133
|
-
|
|
134
|
-
Returns:
|
|
135
|
-
Frame index where countermovement begins, or None if not detected.
|
|
136
|
-
"""
|
|
137
|
-
start_frame = standing_start if standing_start is not None else 0
|
|
138
|
-
|
|
139
|
-
# Look for sustained downward velocity (POSITIVE in normalized coords)
|
|
140
|
-
is_downward = velocities[start_frame:] > countermovement_threshold
|
|
141
|
-
consecutive_count = 0
|
|
142
|
-
|
|
143
|
-
for i in range(len(is_downward)):
|
|
144
|
-
if is_downward[i]:
|
|
145
|
-
consecutive_count += 1
|
|
146
|
-
if consecutive_count >= min_eccentric_frames:
|
|
147
|
-
# Found start of eccentric phase
|
|
148
|
-
return start_frame + i - consecutive_count + 1
|
|
149
|
-
else:
|
|
150
|
-
consecutive_count = 0
|
|
151
|
-
|
|
152
|
-
return None
|
|
153
|
-
|
|
154
|
-
|
|
155
58
|
def find_lowest_point(
|
|
156
59
|
positions: FloatArray,
|
|
157
60
|
velocities: FloatArray,
|
|
@@ -393,6 +393,24 @@ class CMJVideoConfig:
|
|
|
393
393
|
overrides: AnalysisOverrides | None = None
|
|
394
394
|
detection_confidence: float | None = None
|
|
395
395
|
tracking_confidence: float | None = None
|
|
396
|
+
verbose: bool = False
|
|
397
|
+
timer: Timer | None = None
|
|
398
|
+
pose_tracker: "MediaPipePoseTracker | None" = None
|
|
399
|
+
|
|
400
|
+
def to_kwargs(self) -> dict:
|
|
401
|
+
"""Convert config to kwargs dict for process_cmj_video."""
|
|
402
|
+
return {
|
|
403
|
+
"video_path": self.video_path,
|
|
404
|
+
"quality": self.quality,
|
|
405
|
+
"output_video": self.output_video,
|
|
406
|
+
"json_output": self.json_output,
|
|
407
|
+
"overrides": self.overrides,
|
|
408
|
+
"detection_confidence": self.detection_confidence,
|
|
409
|
+
"tracking_confidence": self.tracking_confidence,
|
|
410
|
+
"verbose": self.verbose,
|
|
411
|
+
"timer": self.timer,
|
|
412
|
+
"pose_tracker": self.pose_tracker,
|
|
413
|
+
}
|
|
396
414
|
|
|
397
415
|
|
|
398
416
|
@dataclass
|
|
@@ -511,6 +529,23 @@ def process_cmj_video(
|
|
|
511
529
|
return metrics
|
|
512
530
|
|
|
513
531
|
|
|
532
|
+
def process_cmj_video_from_config(
|
|
533
|
+
config: CMJVideoConfig,
|
|
534
|
+
) -> CMJMetrics:
|
|
535
|
+
"""Process a CMJ video using a configuration object.
|
|
536
|
+
|
|
537
|
+
This is a convenience wrapper around process_cmj_video that
|
|
538
|
+
accepts a CMJVideoConfig instead of individual parameters.
|
|
539
|
+
|
|
540
|
+
Args:
|
|
541
|
+
config: Configuration object containing all analysis parameters
|
|
542
|
+
|
|
543
|
+
Returns:
|
|
544
|
+
CMJMetrics object containing analysis results
|
|
545
|
+
"""
|
|
546
|
+
return process_cmj_video(**config.to_kwargs())
|
|
547
|
+
|
|
548
|
+
|
|
514
549
|
def process_cmj_videos_bulk(
|
|
515
550
|
configs: list[CMJVideoConfig],
|
|
516
551
|
max_workers: int = 4,
|
|
@@ -537,17 +572,8 @@ def _process_cmj_video_wrapper(config: CMJVideoConfig) -> CMJVideoResult:
|
|
|
537
572
|
start_time = time.perf_counter()
|
|
538
573
|
|
|
539
574
|
try:
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
quality=config.quality,
|
|
543
|
-
output_video=config.output_video,
|
|
544
|
-
json_output=config.json_output,
|
|
545
|
-
overrides=config.overrides,
|
|
546
|
-
detection_confidence=config.detection_confidence,
|
|
547
|
-
tracking_confidence=config.tracking_confidence,
|
|
548
|
-
verbose=False,
|
|
549
|
-
)
|
|
550
|
-
|
|
575
|
+
# Use convenience wrapper to avoid parameter unpacking
|
|
576
|
+
metrics = process_cmj_video_from_config(config)
|
|
551
577
|
processing_time = time.perf_counter() - start_time
|
|
552
578
|
|
|
553
579
|
return CMJVideoResult(
|
|
@@ -8,9 +8,12 @@ import click
|
|
|
8
8
|
|
|
9
9
|
from ..core.auto_tuning import QualityPreset
|
|
10
10
|
from ..core.cli_utils import (
|
|
11
|
+
batch_processing_options,
|
|
11
12
|
collect_video_files,
|
|
12
13
|
common_output_options,
|
|
13
14
|
generate_batch_output_paths,
|
|
15
|
+
quality_option,
|
|
16
|
+
verbose_option,
|
|
14
17
|
)
|
|
15
18
|
from .api import AnalysisOverrides, process_cmj_video
|
|
16
19
|
from .kinematics import CMJMetrics
|
|
@@ -59,52 +62,9 @@ def _process_batch_videos(
|
|
|
59
62
|
@click.command(name="cmj-analyze")
|
|
60
63
|
@click.argument("video_path", nargs=-1, type=click.Path(exists=False), required=True)
|
|
61
64
|
@common_output_options
|
|
62
|
-
@
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
default="balanced",
|
|
66
|
-
help=(
|
|
67
|
-
"Analysis quality preset: "
|
|
68
|
-
"fast (quick, less precise), "
|
|
69
|
-
"balanced (default, good for most cases), "
|
|
70
|
-
"accurate (research-grade, slower)"
|
|
71
|
-
),
|
|
72
|
-
show_default=True,
|
|
73
|
-
)
|
|
74
|
-
@click.option(
|
|
75
|
-
"--verbose",
|
|
76
|
-
"-v",
|
|
77
|
-
is_flag=True,
|
|
78
|
-
help="Show auto-selected parameters and analysis details",
|
|
79
|
-
)
|
|
80
|
-
# Batch processing options
|
|
81
|
-
@click.option(
|
|
82
|
-
"--batch",
|
|
83
|
-
is_flag=True,
|
|
84
|
-
help="Enable batch processing mode for multiple videos",
|
|
85
|
-
)
|
|
86
|
-
@click.option(
|
|
87
|
-
"--workers",
|
|
88
|
-
type=int,
|
|
89
|
-
default=4,
|
|
90
|
-
help="Number of parallel workers for batch processing (default: 4)",
|
|
91
|
-
show_default=True,
|
|
92
|
-
)
|
|
93
|
-
@click.option(
|
|
94
|
-
"--output-dir",
|
|
95
|
-
type=click.Path(),
|
|
96
|
-
help="Directory for debug video outputs (batch mode only)",
|
|
97
|
-
)
|
|
98
|
-
@click.option(
|
|
99
|
-
"--json-output-dir",
|
|
100
|
-
type=click.Path(),
|
|
101
|
-
help="Directory for JSON metrics outputs (batch mode only)",
|
|
102
|
-
)
|
|
103
|
-
@click.option(
|
|
104
|
-
"--csv-summary",
|
|
105
|
-
type=click.Path(),
|
|
106
|
-
help="Path for CSV summary export (batch mode only)",
|
|
107
|
-
)
|
|
65
|
+
@quality_option
|
|
66
|
+
@verbose_option
|
|
67
|
+
@batch_processing_options
|
|
108
68
|
# Expert parameters (hidden in help, but always available for advanced users)
|
|
109
69
|
@click.option(
|
|
110
70
|
"--smoothing-window",
|