kinemotion 0.70.1__py3-none-any.whl → 0.71.1__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.
Files changed (35) hide show
  1. kinemotion/__init__.py +4 -1
  2. kinemotion/cmj/analysis.py +79 -30
  3. kinemotion/cmj/api.py +16 -39
  4. kinemotion/cmj/cli.py +0 -21
  5. kinemotion/cmj/debug_overlay.py +154 -286
  6. kinemotion/cmj/joint_angles.py +96 -31
  7. kinemotion/cmj/metrics_validator.py +30 -51
  8. kinemotion/cmj/validation_bounds.py +1 -18
  9. kinemotion/core/__init__.py +0 -2
  10. kinemotion/core/auto_tuning.py +91 -99
  11. kinemotion/core/debug_overlay_utils.py +142 -15
  12. kinemotion/core/experimental.py +55 -51
  13. kinemotion/core/filtering.py +15 -11
  14. kinemotion/core/overlay_constants.py +61 -0
  15. kinemotion/core/pose.py +67 -499
  16. kinemotion/core/smoothing.py +65 -51
  17. kinemotion/core/types.py +15 -0
  18. kinemotion/core/validation.py +6 -7
  19. kinemotion/core/video_io.py +14 -9
  20. kinemotion/dropjump/__init__.py +2 -2
  21. kinemotion/dropjump/analysis.py +67 -44
  22. kinemotion/dropjump/api.py +12 -44
  23. kinemotion/dropjump/cli.py +63 -105
  24. kinemotion/dropjump/debug_overlay.py +124 -65
  25. kinemotion/dropjump/validation_bounds.py +1 -1
  26. kinemotion/models/rtmpose-s_simcc-body7_pt-body7-halpe26_700e-256x192-7f134165_20230605.onnx +0 -0
  27. kinemotion/models/yolox_tiny_8xb8-300e_humanart-6f3252f9.onnx +0 -0
  28. {kinemotion-0.70.1.dist-info → kinemotion-0.71.1.dist-info}/METADATA +1 -5
  29. kinemotion-0.71.1.dist-info/RECORD +50 -0
  30. kinemotion/core/rtmpose_cpu.py +0 -626
  31. kinemotion/core/rtmpose_wrapper.py +0 -190
  32. kinemotion-0.70.1.dist-info/RECORD +0 -51
  33. {kinemotion-0.70.1.dist-info → kinemotion-0.71.1.dist-info}/WHEEL +0 -0
  34. {kinemotion-0.70.1.dist-info → kinemotion-0.71.1.dist-info}/entry_points.txt +0 -0
  35. {kinemotion-0.70.1.dist-info → kinemotion-0.71.1.dist-info}/licenses/LICENSE +0 -0
kinemotion/core/pose.py CHANGED
@@ -18,6 +18,8 @@ Configuration strategies for matching Solution API behavior:
18
18
 
19
19
  from __future__ import annotations
20
20
 
21
+ from typing import Any
22
+
21
23
  import cv2
22
24
  import mediapipe as mp
23
25
  import numpy as np
@@ -159,474 +161,42 @@ class MediaPipePoseTracker:
159
161
 
160
162
 
161
163
  class PoseTrackerFactory:
162
- """Factory for creating pose trackers with automatic backend selection.
164
+ """Factory for creating pose trackers.
163
165
 
164
- Supports multiple backends with auto-detection:
165
- - RTMPose CUDA: NVIDIA GPU acceleration (fastest, 133 FPS)
166
- - RTMPose CoreML: Apple Silicon acceleration (42 FPS)
167
- - RTMPose CPU: Optimized CPU implementation (40-68 FPS)
168
- - MediaPipe: Fallback baseline (48 FPS)
166
+ Currently supports MediaPipe as the only backend.
169
167
 
170
168
  Usage:
171
- # Auto-detect best backend
172
169
  tracker = PoseTrackerFactory.create()
173
-
174
- # Force specific backend
175
- tracker = PoseTrackerFactory.create(backend='rtmpose-cuda')
176
-
177
- # Check available backends
178
- available = PoseTrackerFactory.get_available_backends()
179
170
  """
180
171
 
181
- # Backend class mappings
182
- _BACKENDS: dict[str, type] = {}
183
-
184
172
  @classmethod
185
173
  def create(
186
174
  cls,
187
- backend: str = "auto",
188
- mode: str = "lightweight",
189
- **kwargs: object,
190
- ) -> object:
191
- """Create a pose tracker with the specified backend.
192
-
193
- Args:
194
- backend: Backend selection:
195
- - 'auto': Auto-detect best available backend
196
- - 'mediapipe': MediaPipe Tasks API (baseline)
197
- - 'rtmpose-cpu': RTMPose optimized CPU
198
- - 'rtmpose-cuda': RTMPose with CUDA (NVIDIA GPU)
199
- - 'rtmpose-coreml': RTMPose with CoreML (Apple Silicon)
200
- mode: RTMPose performance mode ('lightweight', 'balanced', 'performance')
201
- Only used for RTMPose backends
202
- **kwargs: Additional arguments passed to tracker constructor
203
-
204
- Returns:
205
- Configured pose tracker instance
206
-
207
- Raises:
208
- ValueError: If backend is not available or recognized
209
- """
210
- # Auto-detect backend
211
- if backend == "auto":
212
- backend = cls._detect_best_backend()
213
- backend = cls._check_backend_available(backend)
214
-
215
- # Check environment variable override
216
- import os
217
-
218
- env_backend = os.environ.get("POSE_TRACKER_BACKEND")
219
- if env_backend:
220
- backend = cls._normalize_backend_name(env_backend)
221
-
222
- # Verify backend is available
223
- backend = cls._check_backend_available(backend)
224
-
225
- # Get tracker class
226
- tracker_class = cls._get_tracker_class(backend)
227
-
228
- # Create tracker with appropriate arguments
229
- return cls._create_tracker(tracker_class, backend, mode, kwargs)
230
-
231
- @classmethod
232
- def _detect_best_backend(cls) -> str:
233
- """Detect the best available backend.
234
-
235
- Priority order:
236
- 1. CUDA (NVIDIA GPU) - fastest
237
- 2. CoreML (Apple Silicon) - good performance
238
- 3. RTMPose CPU - optimized CPU
239
- 4. MediaPipe - baseline fallback
240
-
241
- Returns:
242
- Backend name string
243
- """
244
- # Check for CUDA (NVIDIA GPU)
245
- try:
246
- import torch
247
-
248
- if torch.cuda.is_available():
249
- return "rtmpose-cuda"
250
- except ImportError:
251
- pass
252
-
253
- # Check for CoreML (Apple Silicon)
254
- import sys
255
-
256
- if sys.platform == "darwin":
257
- return "rtmpose-coreml"
258
-
259
- # Check for RTMPose CPU
260
- try:
261
- from kinemotion.core.rtmpose_cpu import (
262
- OptimizedCPUTracker as _RTMPoseCPU, # type: ignore
263
- )
264
-
265
- _ = _RTMPoseCPU # Mark as intentionally used for availability check
266
-
267
- return "rtmpose-cpu"
268
- except ImportError:
269
- pass
270
-
271
- # Fallback to MediaPipe
272
- return "mediapipe"
273
-
274
- @classmethod
275
- def _check_backend_available(cls, backend: str) -> str:
276
- """Check if a backend is available and return a fallback if not.
175
+ backend: str = "mediapipe",
176
+ **kwargs: Any,
177
+ ) -> MediaPipePoseTracker:
178
+ """Create a MediaPipe pose tracker.
277
179
 
278
180
  Args:
279
- backend: Requested backend name
181
+ backend: Backend selection (only 'mediapipe' supported)
182
+ **kwargs: Arguments passed to MediaPipePoseTracker
280
183
 
281
184
  Returns:
282
- Available backend name (may be different from requested)
185
+ Configured MediaPipePoseTracker instance
283
186
 
284
187
  Raises:
285
- ValueError: If no backend is available
286
- """
287
- normalized = cls._normalize_backend_name(backend)
288
-
289
- # Check if specific backend can be imported
290
- if normalized == "rtmpose-cuda":
291
- try:
292
- import torch # noqa: F401
293
-
294
- if not torch.cuda.is_available():
295
- # CUDA not available, fall back to CPU
296
- return cls._check_backend_available("rtmpose-cpu")
297
- # CUDA is available, use rtmpose-cuda
298
- return normalized
299
- except ImportError:
300
- return cls._check_backend_available("rtmpose-cpu")
301
-
302
- if normalized == "rtmpose-coreml":
303
- import sys
304
-
305
- if sys.platform != "darwin":
306
- # Not macOS, fall back to CPU
307
- return cls._check_backend_available("rtmpose-cpu")
308
-
309
- # On macOS, verify CoreML wrapper is available
310
- try:
311
- from kinemotion.core.rtmpose_wrapper import RTMPoseWrapper
312
-
313
- _ = RTMPoseWrapper # Mark as intentionally used
314
- return normalized
315
- except ImportError:
316
- # CoreML wrapper not available, fall back to CPU
317
- return cls._check_backend_available("rtmpose-cpu")
318
-
319
- if normalized == "rtmpose-cpu":
320
- try:
321
- from kinemotion.core.rtmpose_cpu import (
322
- OptimizedCPUTracker as _RTMPoseCPU,
323
- ) # type: ignore
324
-
325
- _ = _RTMPoseCPU # Mark as intentionally used for availability check
326
-
327
- return normalized
328
- except ImportError:
329
- # RTMPose not available, fall back to MediaPipe
330
- return "mediapipe"
331
-
332
- if normalized == "mediapipe":
333
- try:
334
- import mediapipe as _mp # noqa: F401
335
-
336
- _ = _mp # Mark as intentionally used for availability check
337
- return normalized
338
- except ImportError as err:
339
- raise ValueError(
340
- "No pose tracking backend available. Please install mediapipe or rtmlib."
341
- ) from err
342
-
343
- raise ValueError(f"Unknown backend: {backend}")
344
-
345
- @classmethod
346
- def _normalize_backend_name(cls, backend: str) -> str:
347
- """Normalize backend name to canonical form.
348
-
349
- Args:
350
- backend: User-provided backend name
351
-
352
- Returns:
353
- Canonical backend name
354
- """
355
- # Normalize various aliases to canonical names
356
- aliases = {
357
- "mp": "mediapipe",
358
- "mediapipe": "mediapipe",
359
- "rtmpose": "rtmpose-cpu",
360
- "rtmpose-cpu": "rtmpose-cpu",
361
- "rtmpose_cpu": "rtmpose-cpu",
362
- "cpu": "rtmpose-cpu",
363
- "cuda": "rtmpose-cuda",
364
- "rtmpose-cuda": "rtmpose-cuda",
365
- "rtmpose_cuda": "rtmpose-cuda",
366
- "gpu": "rtmpose-cuda",
367
- "mps": "rtmpose-coreml",
368
- "coreml": "rtmpose-coreml",
369
- "rtmpose-coreml": "rtmpose-coreml",
370
- "rtmpose_coreml": "rtmpose-coreml",
371
- }
372
- return aliases.get(backend.lower(), backend)
373
-
374
- @classmethod
375
- def _get_tracker_class(cls, backend: str):
376
- """Get the tracker class for a backend.
377
-
378
- Args:
379
- backend: Canonical backend name
380
-
381
- Returns:
382
- Tracker class
383
-
384
- Raises:
385
- ValueError: If backend is not recognized
386
- """
387
- if backend == "mediapipe":
388
- return MediaPipePoseTracker
389
-
390
- if backend == "rtmpose-cpu":
391
- try:
392
- from kinemotion.core.rtmpose_cpu import OptimizedCPUTracker
393
-
394
- return OptimizedCPUTracker
395
- except ImportError as e:
396
- raise ValueError(f"RTMPose CPU backend requested but not available: {e}") from e
397
-
398
- if backend in ("rtmpose-cuda", "rtmpose-coreml"):
399
- try:
400
- from kinemotion.core.rtmpose_wrapper import RTMPoseWrapper
401
-
402
- return RTMPoseWrapper
403
- except ImportError as e:
404
- raise ValueError(
405
- f"RTMPose wrapper backend requested but not available: {e}"
406
- ) from e
407
-
408
- raise ValueError(f"Unknown backend: {backend}")
409
-
410
- @classmethod
411
- def _create_tracker(
412
- cls,
413
- tracker_class: type,
414
- backend: str,
415
- mode: str,
416
- kwargs: dict[str, object],
417
- ) -> object:
418
- """Create a tracker instance with appropriate arguments.
419
-
420
- Args:
421
- tracker_class: Tracker class to instantiate
422
- backend: Backend name (for parameter mapping)
423
- mode: RTMPose mode (only used for RTMPose backends)
424
- kwargs: Additional arguments from user
425
-
426
- Returns:
427
- Tracker instance
428
- """
429
- # MediaPipe-specific arguments
430
- if backend == "mediapipe":
431
- # Remove RTMPose-specific arguments
432
- rttmpose_keys = {"mode", "backend", "device", "pose_input_size"}
433
- filtered_kwargs = {k: v for k, v in kwargs.items() if k not in rttmpose_keys}
434
- return tracker_class(**filtered_kwargs)
435
-
436
- # OptimizedCPUTracker (CPU-only, doesn't accept device parameter)
437
- if backend == "rtmpose-cpu":
438
- # Remove RTMPoseWrapper-specific and MediaPipe-specific arguments
439
- unsupported_keys = {
440
- "backend",
441
- "device",
442
- "min_detection_confidence",
443
- "min_tracking_confidence",
444
- }
445
- filtered_kwargs = {k: v for k, v in kwargs.items() if k not in unsupported_keys}
446
- filtered_kwargs.setdefault("mode", mode)
447
- return tracker_class(**filtered_kwargs)
448
-
449
- # RTMPoseWrapper (CUDA/CoreML, requires device parameter)
450
- # Remove MediaPipe-specific arguments
451
- mediapipe_keys = {"min_detection_confidence", "min_tracking_confidence"}
452
- filtered_kwargs = {k: v for k, v in kwargs.items() if k not in mediapipe_keys}
453
-
454
- device = backend.split("-")[-1] # Extract 'cuda', 'cpu', 'coreml'
455
- if device == "coreml":
456
- device = "mps" # RTMLib uses 'mps' for Apple Silicon
457
-
458
- filtered_kwargs.setdefault("device", device)
459
- filtered_kwargs.setdefault("mode", mode)
460
-
461
- return tracker_class(**filtered_kwargs)
462
-
463
- @classmethod
464
- def get_available_backends(cls) -> list[str]:
465
- """Get list of available backends on current system.
466
-
467
- Returns:
468
- List of available backend names
188
+ ValueError: If backend is not 'mediapipe'
469
189
  """
470
- available = []
471
-
472
- # Always have MediaPipe as fallback
473
- try:
474
- import mediapipe as _mp # noqa: F401
475
-
476
- _ = _mp # Mark as intentionally used for availability check
477
- available.append("mediapipe")
478
- except ImportError:
479
- pass
480
-
481
- # Check RTMPose CPU
482
- try:
483
- from kinemotion.core.rtmpose_cpu import (
484
- OptimizedCPUTracker as _RTMPoseCPU,
485
- ) # type: ignore
486
-
487
- _ = _RTMPoseCPU # Mark as intentionally used for availability check
190
+ # Normalize and validate backend
191
+ normalized = backend.lower()
192
+ if normalized not in ("mediapipe", "mp", "auto"):
193
+ raise ValueError(f"Unknown backend: {backend}. Only 'mediapipe' is supported.")
488
194
 
489
- available.append("rtmpose-cpu")
490
- except ImportError:
491
- pass
195
+ # Filter out any legacy kwargs that don't apply to MediaPipe
196
+ legacy_keys = {"mode", "backend", "device", "pose_input_size"}
197
+ filtered_kwargs = {k: v for k, v in kwargs.items() if k not in legacy_keys}
492
198
 
493
- # Check CUDA
494
- try:
495
- import torch
496
-
497
- if torch.cuda.is_available():
498
- from kinemotion.core.rtmpose_wrapper import (
499
- RTMPoseWrapper as _RTMPoseWrapper,
500
- ) # type: ignore
501
-
502
- _ = _RTMPoseWrapper # Mark as intentionally used for availability check
503
-
504
- available.append("rtmpose-cuda")
505
- except ImportError:
506
- pass
507
-
508
- # Check CoreML (Apple Silicon)
509
- import sys
510
-
511
- if sys.platform == "darwin":
512
- try:
513
- from kinemotion.core.rtmpose_wrapper import (
514
- RTMPoseWrapper as _RTMPoseWrapperMPS,
515
- ) # type: ignore
516
-
517
- _ = _RTMPoseWrapperMPS # Mark as intentionally used for availability check
518
-
519
- available.append("rtmpose-coreml")
520
- except ImportError:
521
- pass
522
-
523
- return available
524
-
525
- @classmethod
526
- def get_backend_info(cls, backend: str) -> dict[str, str]:
527
- """Get information about a backend.
528
-
529
- Args:
530
- backend: Backend name
531
-
532
- Returns:
533
- Dictionary with backend information
534
- """
535
- info = {
536
- "mediapipe": {
537
- "name": "MediaPipe",
538
- "description": "Baseline pose tracking using MediaPipe Tasks API",
539
- "performance": "~48 FPS",
540
- "accuracy": "Baseline (reference)",
541
- "requirements": "mediapipe package",
542
- },
543
- "rtmpose-cpu": {
544
- "name": "RTMPose CPU",
545
- "description": "Optimized CPU implementation with ONNX Runtime",
546
- "performance": "~40-68 FPS (134% of MediaPipe)",
547
- "accuracy": "9-12px mean difference (1-5% metric accuracy)",
548
- "requirements": "rtmlib package",
549
- },
550
- "rtmpose-cuda": {
551
- "name": "RTMPose CUDA",
552
- "description": "NVIDIA GPU acceleration with CUDA",
553
- "performance": "~133 FPS (271% of MediaPipe)",
554
- "accuracy": "9-12px mean difference (1-5% metric accuracy)",
555
- "requirements": "rtmlib + CUDA-capable GPU",
556
- },
557
- "rtmpose-coreml": {
558
- "name": "RTMPose CoreML",
559
- "description": "Apple Silicon acceleration with CoreML",
560
- "performance": "~42 FPS (94% of MediaPipe)",
561
- "accuracy": "9-12px mean difference (1-5% metric accuracy)",
562
- "requirements": "rtmlib + Apple Silicon",
563
- },
564
- }
565
-
566
- normalized = cls._normalize_backend_name(backend)
567
- return info.get(normalized, {})
568
-
569
-
570
- def get_tracker_info(tracker: object) -> str:
571
- """Get detailed information about a pose tracker instance.
572
-
573
- Args:
574
- tracker: Pose tracker instance
575
-
576
- Returns:
577
- Formatted string with tracker details
578
- """
579
- tracker_class = type(tracker).__name__
580
- module = type(tracker).__module__
581
-
582
- info = f"{tracker_class} (from {module})"
583
-
584
- # Add backend-specific details
585
- if tracker_class == "MediaPipePoseTracker":
586
- info += " [MediaPipe Tasks API]"
587
- elif tracker_class == "OptimizedCPUTracker":
588
- # Check if ONNX Runtime has CUDA
589
- try:
590
- import onnxruntime as ort
591
-
592
- providers = ort.get_available_providers()
593
- if "CUDAExecutionProvider" in providers:
594
- # Check what providers the session is actually using
595
- det_session = getattr(tracker, "det_session", None)
596
- if det_session is not None:
597
- active_providers = det_session.get_providers()
598
- if "CUDAExecutionProvider" in active_providers:
599
- info += " [ONNX Runtime: CUDA]"
600
- else:
601
- info += " [ONNX Runtime: CPU]"
602
- else:
603
- info += " [ONNX Runtime]"
604
- else:
605
- info += " [ONNX Runtime: CPU]"
606
- except ImportError:
607
- info += " [ONNX Runtime]"
608
- elif tracker_class == "RTMPoseWrapper":
609
- device = getattr(tracker, "device", None)
610
- if device:
611
- if device == "cuda":
612
- try:
613
- import torch
614
-
615
- if torch.cuda.is_available():
616
- device_name = torch.cuda.get_device_name(0)
617
- info += f" [PyTorch CUDA: {device_name}]"
618
- else:
619
- info += " [PyTorch: CPU fallback]"
620
- except ImportError:
621
- info += " [PyTorch CUDA]"
622
- elif device == "mps":
623
- info += " [PyTorch: Apple Silicon GPU]"
624
- else:
625
- info += f" [PyTorch: {device}]"
626
- else:
627
- info += " [PyTorch]"
628
-
629
- return info
199
+ return MediaPipePoseTracker(**filtered_kwargs)
630
200
 
631
201
 
632
202
  def _extract_landmarks_from_results(
@@ -654,28 +224,6 @@ def _extract_landmarks_from_results(
654
224
  return landmarks
655
225
 
656
226
 
657
- # Legacy compatibility aliases for Solution API enum values
658
- class _LegacyPoseLandmark:
659
- """Compatibility shim for Solution API enum values."""
660
-
661
- LEFT_ANKLE = 27
662
- RIGHT_ANKLE = 28
663
- LEFT_HEEL = 29
664
- RIGHT_HEEL = 30
665
- LEFT_FOOT_INDEX = 31
666
- RIGHT_FOOT_INDEX = 32
667
- LEFT_HIP = 23
668
- RIGHT_HIP = 24
669
- LEFT_SHOULDER = 11
670
- RIGHT_SHOULDER = 12
671
- NOSE = 0
672
- LEFT_KNEE = 25
673
- RIGHT_KNEE = 26
674
-
675
-
676
- PoseLandmark = _LegacyPoseLandmark
677
-
678
-
679
227
  def compute_center_of_mass(
680
228
  landmarks: dict[str, tuple[float, float, float]],
681
229
  visibility_threshold: float = 0.5,
@@ -754,6 +302,37 @@ def compute_center_of_mass(
754
302
  return (com_x, com_y, com_visibility)
755
303
 
756
304
 
305
+ def _compute_mean_landmark_position(
306
+ landmark_keys: list[str],
307
+ landmarks: dict[str, tuple[float, float, float]],
308
+ vis_threshold: float,
309
+ ) -> tuple[float, float, float] | None:
310
+ """Compute mean position and visibility from multiple landmarks.
311
+
312
+ Args:
313
+ landmark_keys: List of landmark key names to average
314
+ landmarks: Dictionary of landmark positions
315
+ vis_threshold: Minimum visibility threshold
316
+
317
+ Returns:
318
+ (x, y, visibility) tuple if any landmarks are visible, else None
319
+ """
320
+ positions = [
321
+ (x, y, vis)
322
+ for key in landmark_keys
323
+ if key in landmarks
324
+ for x, y, vis in [landmarks[key]]
325
+ if vis > vis_threshold
326
+ ]
327
+ if not positions:
328
+ return None
329
+
330
+ x = float(np.mean([p[0] for p in positions]))
331
+ y = float(np.mean([p[1] for p in positions]))
332
+ vis = float(np.mean([p[2] for p in positions]))
333
+ return (x, y, vis)
334
+
335
+
757
336
  def _add_head_segment(
758
337
  segments: list,
759
338
  weights: list,
@@ -779,20 +358,17 @@ def _add_trunk_segment(
779
358
  ) -> None:
780
359
  """Add trunk segment (50% body mass) if visible."""
781
360
  trunk_keys = ["left_shoulder", "right_shoulder", "left_hip", "right_hip"]
782
- trunk_pos = [
783
- (x, y, vis)
784
- for key in trunk_keys
785
- if key in landmarks
786
- for x, y, vis in [landmarks[key]]
787
- if vis > vis_threshold
788
- ]
789
- if len(trunk_pos) >= 2:
790
- trunk_x = float(np.mean([p[0] for p in trunk_pos]))
791
- trunk_y = float(np.mean([p[1] for p in trunk_pos]))
792
- trunk_vis = float(np.mean([p[2] for p in trunk_pos]))
793
- segments.append((trunk_x, trunk_y))
794
- weights.append(0.50)
795
- visibilities.append(trunk_vis)
361
+ trunk_pos = _compute_mean_landmark_position(trunk_keys, landmarks, vis_threshold)
362
+
363
+ if trunk_pos is not None:
364
+ # Require at least 2 visible landmarks for valid trunk
365
+ visible_count = sum(
366
+ 1 for key in trunk_keys if key in landmarks and landmarks[key][2] > vis_threshold
367
+ )
368
+ if visible_count >= 2:
369
+ segments.append((trunk_pos[0], trunk_pos[1]))
370
+ weights.append(0.50)
371
+ visibilities.append(trunk_pos[2])
796
372
 
797
373
 
798
374
  def _add_limb_segment(
@@ -832,17 +408,9 @@ def _add_foot_segment(
832
408
  ) -> None:
833
409
  """Add foot segment (1.5% body mass per foot) if visible."""
834
410
  foot_keys = [f"{side}_ankle", f"{side}_heel", f"{side}_foot_index"]
835
- foot_pos = [
836
- (x, y, vis)
837
- for key in foot_keys
838
- if key in landmarks
839
- for x, y, vis in [landmarks[key]]
840
- if vis > vis_threshold
841
- ]
842
- if foot_pos:
843
- foot_x = float(np.mean([p[0] for p in foot_pos]))
844
- foot_y = float(np.mean([p[1] for p in foot_pos]))
845
- foot_vis = float(np.mean([p[2] for p in foot_pos]))
846
- segments.append((foot_x, foot_y))
411
+ foot_pos = _compute_mean_landmark_position(foot_keys, landmarks, vis_threshold)
412
+
413
+ if foot_pos is not None:
414
+ segments.append((foot_pos[0], foot_pos[1]))
847
415
  weights.append(0.015)
848
- visibilities.append(foot_vis)
416
+ visibilities.append(foot_pos[2])