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.

@@ -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
- # This is critical for preserving aspect ratio, especially with mobile videos
54
- # that have rotation metadata. OpenCV properties (CAP_PROP_FRAME_WIDTH/HEIGHT)
55
- # may return incorrect dimensions, so we read the actual frame data.
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
- # Extract rotation metadata from video (iPhones store rotation in
67
- # side_data_list). OpenCV ignores rotation metadata, so we need to
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
- # Apply rotation to dimensions if needed
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
- metrics = process_cmj_video(
541
- video_path=config.video_path,
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
- @click.option(
63
- "--quality",
64
- type=click.Choice(["fast", "balanced", "accurate"], case_sensitive=False),
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",