kinemotion 0.12.2__py3-none-any.whl → 0.12.3__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.

@@ -216,6 +216,59 @@ def auto_tune_parameters(
216
216
  )
217
217
 
218
218
 
219
+ def _collect_foot_visibility_and_positions(
220
+ frame_landmarks: dict[str, tuple[float, float, float]],
221
+ ) -> tuple[list[float], list[float]]:
222
+ """
223
+ Collect visibility scores and Y positions from foot landmarks.
224
+
225
+ Args:
226
+ frame_landmarks: Landmarks for a single frame
227
+
228
+ Returns:
229
+ Tuple of (visibility_scores, y_positions)
230
+ """
231
+ foot_keys = [
232
+ "left_ankle",
233
+ "right_ankle",
234
+ "left_heel",
235
+ "right_heel",
236
+ "left_foot_index",
237
+ "right_foot_index",
238
+ ]
239
+
240
+ frame_vis = []
241
+ frame_y_positions = []
242
+
243
+ for key in foot_keys:
244
+ if key in frame_landmarks:
245
+ _, y, vis = frame_landmarks[key] # x not needed for analysis
246
+ frame_vis.append(vis)
247
+ frame_y_positions.append(y)
248
+
249
+ return frame_vis, frame_y_positions
250
+
251
+
252
+ def _check_stable_period(positions: list[float]) -> bool:
253
+ """
254
+ Check if video has a stable period at the start.
255
+
256
+ A stable period (low variance in first 30 frames) indicates
257
+ the subject is standing on an elevated platform before jumping.
258
+
259
+ Args:
260
+ positions: List of average Y positions per frame
261
+
262
+ Returns:
263
+ True if stable period detected, False otherwise
264
+ """
265
+ if len(positions) < 30:
266
+ return False
267
+
268
+ first_30_std = float(np.std(positions[:30]))
269
+ return first_30_std < 0.01 # Very stable = on platform
270
+
271
+
219
272
  def analyze_video_sample(
220
273
  landmarks_sequence: list[dict[str, tuple[float, float, float]] | None],
221
274
  fps: float,
@@ -235,35 +288,22 @@ def analyze_video_sample(
235
288
  Returns:
236
289
  VideoCharacteristics with analyzed properties
237
290
  """
238
- # Calculate average landmark visibility
239
291
  visibilities = []
240
292
  positions = []
241
293
 
294
+ # Collect visibility and position data from all frames
242
295
  for frame_landmarks in landmarks_sequence:
243
- if frame_landmarks:
244
- # Collect visibility scores from foot landmarks
245
- foot_keys = [
246
- "left_ankle",
247
- "right_ankle",
248
- "left_heel",
249
- "right_heel",
250
- "left_foot_index",
251
- "right_foot_index",
252
- ]
253
-
254
- frame_vis = []
255
- frame_y_positions = []
256
-
257
- for key in foot_keys:
258
- if key in frame_landmarks:
259
- _, y, vis = frame_landmarks[key] # x not needed for analysis
260
- frame_vis.append(vis)
261
- frame_y_positions.append(y)
262
-
263
- if frame_vis:
264
- visibilities.append(float(np.mean(frame_vis)))
265
- if frame_y_positions:
266
- positions.append(float(np.mean(frame_y_positions)))
296
+ if not frame_landmarks:
297
+ continue
298
+
299
+ frame_vis, frame_y_positions = _collect_foot_visibility_and_positions(
300
+ frame_landmarks
301
+ )
302
+
303
+ if frame_vis:
304
+ visibilities.append(float(np.mean(frame_vis)))
305
+ if frame_y_positions:
306
+ positions.append(float(np.mean(frame_y_positions)))
267
307
 
268
308
  # Compute metrics
269
309
  avg_visibility = float(np.mean(visibilities)) if visibilities else 0.5
@@ -273,11 +313,7 @@ def analyze_video_sample(
273
313
  tracking_quality = analyze_tracking_quality(avg_visibility)
274
314
 
275
315
  # Check for stable period (indicates drop jump from elevated platform)
276
- # Simple check: do first 30 frames have low variance?
277
- has_stable_period = False
278
- if len(positions) >= 30:
279
- first_30_std = float(np.std(positions[:30]))
280
- has_stable_period = first_30_std < 0.01 # Very stable = on platform
316
+ has_stable_period = _check_stable_period(positions)
281
317
 
282
318
  return VideoCharacteristics(
283
319
  fps=fps,
@@ -65,6 +65,43 @@ class VideoProcessor:
65
65
  self.display_width,
66
66
  )
67
67
 
68
+ def _parse_sample_aspect_ratio(self, sar_str: str) -> None:
69
+ """
70
+ Parse SAR string and update display dimensions.
71
+
72
+ Args:
73
+ sar_str: SAR string in format "width:height" (e.g., "270:473")
74
+ """
75
+ if not sar_str or ":" not in sar_str:
76
+ return
77
+
78
+ sar_parts = sar_str.split(":")
79
+ sar_width = int(sar_parts[0])
80
+ sar_height = int(sar_parts[1])
81
+
82
+ # Calculate display dimensions if pixels are non-square
83
+ # DAR = (width * SAR_width) / (height * SAR_height)
84
+ if sar_width != sar_height:
85
+ self.display_width = int(self.width * sar_width / sar_height)
86
+ self.display_height = self.height
87
+
88
+ def _extract_rotation_from_stream(self, stream: dict) -> int: # type: ignore[type-arg]
89
+ """
90
+ Extract rotation metadata from video stream.
91
+
92
+ Args:
93
+ stream: ffprobe stream dictionary
94
+
95
+ Returns:
96
+ Rotation angle in degrees (0, 90, -90, 180)
97
+ """
98
+ side_data_list = stream.get("side_data_list", [])
99
+ for side_data in side_data_list:
100
+ if side_data.get("side_data_type") == "Display Matrix":
101
+ rotation = side_data.get("rotation", 0)
102
+ return int(rotation)
103
+ return 0
104
+
68
105
  def _extract_video_metadata(self) -> None:
69
106
  """
70
107
  Extract video metadata including SAR and rotation using ffprobe.
@@ -94,35 +131,22 @@ class VideoProcessor:
94
131
  timeout=5,
95
132
  )
96
133
 
97
- if result.returncode == 0:
98
- data = json.loads(result.stdout)
99
- if "streams" in data and len(data["streams"]) > 0:
100
- stream = data["streams"][0]
101
-
102
- # Extract SAR (Sample Aspect Ratio)
103
- sar_str = stream.get("sample_aspect_ratio", "1:1")
104
-
105
- # Parse SAR (e.g., "270:473")
106
- if sar_str and ":" in sar_str:
107
- sar_parts = sar_str.split(":")
108
- sar_width = int(sar_parts[0])
109
- sar_height = int(sar_parts[1])
110
-
111
- # Calculate display dimensions
112
- # DAR = (width * SAR_width) / (height * SAR_height)
113
- if sar_width != sar_height:
114
- self.display_width = int(
115
- self.width * sar_width / sar_height
116
- )
117
- self.display_height = self.height
118
-
119
- # Extract rotation from side_data_list (common for iPhone videos)
120
- side_data_list = stream.get("side_data_list", [])
121
- for side_data in side_data_list:
122
- if side_data.get("side_data_type") == "Display Matrix":
123
- rotation = side_data.get("rotation", 0)
124
- # Convert to int and normalize to 0, 90, -90, 180
125
- self.rotation = int(rotation)
134
+ if result.returncode != 0:
135
+ return
136
+
137
+ data = json.loads(result.stdout)
138
+ if "streams" not in data or len(data["streams"]) == 0:
139
+ return
140
+
141
+ stream = data["streams"][0]
142
+
143
+ # Extract and parse SAR (Sample Aspect Ratio)
144
+ sar_str = stream.get("sample_aspect_ratio", "1:1")
145
+ self._parse_sample_aspect_ratio(sar_str)
146
+
147
+ # Extract rotation from side_data_list (common for iPhone videos)
148
+ self.rotation = self._extract_rotation_from_stream(stream)
149
+
126
150
  except (subprocess.TimeoutExpired, FileNotFoundError, json.JSONDecodeError):
127
151
  # If ffprobe fails, keep original dimensions (square pixels)
128
152
  pass
@@ -752,3 +752,39 @@ def compute_average_foot_position(
752
752
  return (0.5, 0.5) # Default to center if no visible feet
753
753
 
754
754
  return (float(np.mean(x_positions)), float(np.mean(y_positions)))
755
+
756
+
757
+ def extract_foot_positions_and_visibilities(
758
+ smoothed_landmarks: list[dict[str, tuple[float, float, float]] | None],
759
+ ) -> tuple[np.ndarray, np.ndarray]:
760
+ """
761
+ Extract vertical positions and average visibilities from smoothed landmarks.
762
+
763
+ This utility function eliminates code duplication between CLI and programmatic usage.
764
+
765
+ Args:
766
+ smoothed_landmarks: Smoothed landmark sequence from tracking
767
+
768
+ Returns:
769
+ Tuple of (vertical_positions, visibilities) as numpy arrays
770
+ """
771
+ position_list: list[float] = []
772
+ visibilities_list: list[float] = []
773
+
774
+ for frame_landmarks in smoothed_landmarks:
775
+ if frame_landmarks:
776
+ _, foot_y = compute_average_foot_position(frame_landmarks)
777
+ position_list.append(foot_y)
778
+
779
+ # Average visibility of foot landmarks
780
+ foot_vis = []
781
+ for key in ["left_ankle", "right_ankle", "left_heel", "right_heel"]:
782
+ if key in frame_landmarks:
783
+ foot_vis.append(frame_landmarks[key][2])
784
+ visibilities_list.append(float(np.mean(foot_vis)) if foot_vis else 0.0)
785
+ else:
786
+ # Fill missing frames with last known position or default
787
+ position_list.append(position_list[-1] if position_list else 0.5)
788
+ visibilities_list.append(0.0)
789
+
790
+ return np.array(position_list), np.array(visibilities_list)
@@ -28,8 +28,8 @@ from ..core.pose import PoseTracker
28
28
  from ..core.video_io import VideoProcessor
29
29
  from .analysis import (
30
30
  ContactState,
31
- compute_average_foot_position,
32
31
  detect_ground_contact,
32
+ extract_foot_positions_and_visibilities,
33
33
  )
34
34
  from .debug_overlay import DebugOverlayRenderer
35
35
  from .kinematics import DropJumpMetrics, calculate_drop_jump_metrics
@@ -258,26 +258,7 @@ def _extract_positions_and_visibilities(
258
258
  Tuple of (vertical_positions, visibilities)
259
259
  """
260
260
  click.echo("Extracting foot positions...", err=True)
261
-
262
- position_list: list[float] = []
263
- visibilities_list: list[float] = []
264
-
265
- for frame_landmarks in smoothed_landmarks:
266
- if frame_landmarks:
267
- _, foot_y = compute_average_foot_position(frame_landmarks)
268
- position_list.append(foot_y)
269
-
270
- # Average visibility of foot landmarks
271
- foot_vis = []
272
- for key in ["left_ankle", "right_ankle", "left_heel", "right_heel"]:
273
- if key in frame_landmarks:
274
- foot_vis.append(frame_landmarks[key][2])
275
- visibilities_list.append(float(np.mean(foot_vis)) if foot_vis else 0.0)
276
- else:
277
- position_list.append(position_list[-1] if position_list else 0.5)
278
- visibilities_list.append(0.0)
279
-
280
- return np.array(position_list), np.array(visibilities_list)
261
+ return extract_foot_positions_and_visibilities(smoothed_landmarks)
281
262
 
282
263
 
283
264
  def _create_debug_video(
@@ -567,6 +548,63 @@ def _compute_batch_statistics(results: list[VideoResult]) -> None:
567
548
  )
568
549
 
569
550
 
551
+ def _format_time_metric(value: float | None, multiplier: float = 1000.0) -> str:
552
+ """Format time metric for CSV output.
553
+
554
+ Args:
555
+ value: Time value in seconds
556
+ multiplier: Multiplier to convert to milliseconds (default: 1000.0)
557
+
558
+ Returns:
559
+ Formatted string or "N/A" if value is None
560
+ """
561
+ return f"{value * multiplier:.1f}" if value is not None else "N/A"
562
+
563
+
564
+ def _format_distance_metric(value: float | None) -> str:
565
+ """Format distance metric for CSV output.
566
+
567
+ Args:
568
+ value: Distance value in meters
569
+
570
+ Returns:
571
+ Formatted string or "N/A" if value is None
572
+ """
573
+ return f"{value:.3f}" if value is not None else "N/A"
574
+
575
+
576
+ def _create_csv_row_from_result(result: VideoResult) -> list[str]:
577
+ """Create CSV row from video processing result.
578
+
579
+ Args:
580
+ result: Video processing result
581
+
582
+ Returns:
583
+ List of formatted values for CSV row
584
+ """
585
+ video_name = Path(result.video_path).name
586
+ processing_time = f"{result.processing_time:.2f}"
587
+
588
+ if result.success and result.metrics:
589
+ return [
590
+ video_name,
591
+ _format_time_metric(result.metrics.ground_contact_time),
592
+ _format_time_metric(result.metrics.flight_time),
593
+ _format_distance_metric(result.metrics.jump_height),
594
+ processing_time,
595
+ "Success",
596
+ ]
597
+ else:
598
+ return [
599
+ video_name,
600
+ "N/A",
601
+ "N/A",
602
+ "N/A",
603
+ processing_time,
604
+ f"Failed: {result.error}",
605
+ ]
606
+
607
+
570
608
  def _write_csv_summary(
571
609
  csv_summary: str | None, results: list[VideoResult], successful: list[VideoResult]
572
610
  ) -> None:
@@ -600,40 +638,7 @@ def _write_csv_summary(
600
638
 
601
639
  # Data rows
602
640
  for result in results:
603
- if result.success and result.metrics:
604
- writer.writerow(
605
- [
606
- Path(result.video_path).name,
607
- (
608
- f"{result.metrics.ground_contact_time * 1000:.1f}"
609
- if result.metrics.ground_contact_time
610
- else "N/A"
611
- ),
612
- (
613
- f"{result.metrics.flight_time * 1000:.1f}"
614
- if result.metrics.flight_time
615
- else "N/A"
616
- ),
617
- (
618
- f"{result.metrics.jump_height:.3f}"
619
- if result.metrics.jump_height
620
- else "N/A"
621
- ),
622
- f"{result.processing_time:.2f}",
623
- "Success",
624
- ]
625
- )
626
- else:
627
- writer.writerow(
628
- [
629
- Path(result.video_path).name,
630
- "N/A",
631
- "N/A",
632
- "N/A",
633
- f"{result.processing_time:.2f}",
634
- f"Failed: {result.error}",
635
- ]
636
- )
641
+ writer.writerow(_create_csv_row_from_result(result))
637
642
 
638
643
  click.echo("CSV summary written successfully", err=True)
639
644
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kinemotion
3
- Version: 0.12.2
3
+ Version: 0.12.3
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
@@ -8,21 +8,21 @@ kinemotion/cmj/debug_overlay.py,sha256=D-y2FQKI01KY0WXFKTKg6p9Qj3AkXCE7xjau3Ais0
8
8
  kinemotion/cmj/joint_angles.py,sha256=8ucpDGPvbt4iX3tx9eVxJEUv0laTm2Y58_--VzJCogE,9113
9
9
  kinemotion/cmj/kinematics.py,sha256=Xl_PlC2OqMoA-zOc3SRB_GqI0AgLlJol5FTPe5J_qLc,7573
10
10
  kinemotion/core/__init__.py,sha256=3yzDhb5PekDNjydqrs8aWGneUGJBt-lB0SoB_Y2FXqU,1010
11
- kinemotion/core/auto_tuning.py,sha256=cvmxUI-CbahpOJQtR2r5jOx4Q6yKPe3DO1o15hOQIdw,10508
11
+ kinemotion/core/auto_tuning.py,sha256=j6cul_qC6k0XyryCG93C1AWH2MKPj3UBMzuX02xaqfI,11235
12
12
  kinemotion/core/cli_utils.py,sha256=Pq1JF7yvK1YbH0tOUWKjplthCbWsJQt4Lv7esPYH4FM,7254
13
13
  kinemotion/core/debug_overlay_utils.py,sha256=TyUb5okv5qw8oeaX3jsUO_kpwf1NnaHEAOTm-8LwTno,4587
14
14
  kinemotion/core/filtering.py,sha256=f-m-aA59e4WqE6u-9MA51wssu7rI-Y_7n1cG8IWdeRQ,11241
15
15
  kinemotion/core/pose.py,sha256=ztemdZ_ysVVK3gbXabm8qS_dr1VfJX9KZjmcO-Z-iNE,8532
16
16
  kinemotion/core/smoothing.py,sha256=C9GK3PAN16RpqJw2UWeVslSTJZEvALeVADjtnJnSF88,14240
17
- kinemotion/core/video_io.py,sha256=UtmUndw22uFnZBK_BmeE912yRYH1YnU_P8LjuN33DPc,6461
17
+ kinemotion/core/video_io.py,sha256=kH5FYPx3y3lFZ3ybdgxaZfKPdHJ37eqxSeAaZjyQnJk,6817
18
18
  kinemotion/dropjump/__init__.py,sha256=yc1XiZ9vfo5h_n7PKVSiX2TTgaIfGL7Y7SkQtiDZj_E,838
19
- kinemotion/dropjump/analysis.py,sha256=PoBzlqciBFB_O7ejdjBhpnk19a_VoD31tDjXuN1-ovo,25764
20
- kinemotion/dropjump/cli.py,sha256=90GddzgMLwEKKwcG0VW94HeXFwEK5zSJm6w6UkPbaRk,21646
19
+ kinemotion/dropjump/analysis.py,sha256=xx5NWy6s0eb9BEyO_FByY1Ahunaoh3TyaTAxjlPrvxg,27153
20
+ kinemotion/dropjump/cli.py,sha256=Oni7gntysA6Zwb_ehsAnk6Ytd2ofUhN0yXVCsCsiris,21196
21
21
  kinemotion/dropjump/debug_overlay.py,sha256=LkPw6ucb7beoYWS4L-Lvjs1KLCm5wAWDAfiznUeV2IQ,5668
22
22
  kinemotion/dropjump/kinematics.py,sha256=txDxpDti3VJVctWGbe3aIrlIx83UY8-ynzlX01TOvTA,15577
23
23
  kinemotion/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
- kinemotion-0.12.2.dist-info/METADATA,sha256=FE1-EfYL73UDQE9xBj-qRCQ51PN6F1SylDmVJrt974s,18990
25
- kinemotion-0.12.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
26
- kinemotion-0.12.2.dist-info/entry_points.txt,sha256=zaqnAnjLvcdrk1Qvj5nvXZCZ2gp0prS7it1zTJygcIY,50
27
- kinemotion-0.12.2.dist-info/licenses/LICENSE,sha256=KZajvqsHw0NoOHOi2q0FZ4NBe9HdV6oey-IPYAtHXfg,1088
28
- kinemotion-0.12.2.dist-info/RECORD,,
24
+ kinemotion-0.12.3.dist-info/METADATA,sha256=rbD5mEdFYxRAlAbmaobBcrnMaFh0mFd2L3GyHipRlGY,18990
25
+ kinemotion-0.12.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
26
+ kinemotion-0.12.3.dist-info/entry_points.txt,sha256=zaqnAnjLvcdrk1Qvj5nvXZCZ2gp0prS7it1zTJygcIY,50
27
+ kinemotion-0.12.3.dist-info/licenses/LICENSE,sha256=KZajvqsHw0NoOHOi2q0FZ4NBe9HdV6oey-IPYAtHXfg,1088
28
+ kinemotion-0.12.3.dist-info/RECORD,,