kinemotion 0.11.1__tar.gz → 0.11.3__tar.gz

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.

Files changed (70) hide show
  1. {kinemotion-0.11.1 → kinemotion-0.11.3}/CHANGELOG.md +28 -0
  2. {kinemotion-0.11.1 → kinemotion-0.11.3}/PKG-INFO +1 -1
  3. {kinemotion-0.11.1 → kinemotion-0.11.3}/pyproject.toml +1 -1
  4. {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/api.py +57 -36
  5. {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/cmj/analysis.py +1 -5
  6. {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/cmj/cli.py +92 -120
  7. {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/cmj/debug_overlay.py +200 -165
  8. kinemotion-0.11.3/src/kinemotion/core/cli_utils.py +113 -0
  9. {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/dropjump/cli.py +8 -103
  10. {kinemotion-0.11.1 → kinemotion-0.11.3}/uv.lock +1 -1
  11. {kinemotion-0.11.1 → kinemotion-0.11.3}/.dockerignore +0 -0
  12. {kinemotion-0.11.1 → kinemotion-0.11.3}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  13. {kinemotion-0.11.1 → kinemotion-0.11.3}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  14. {kinemotion-0.11.1 → kinemotion-0.11.3}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  15. {kinemotion-0.11.1 → kinemotion-0.11.3}/.github/pull_request_template.md +0 -0
  16. {kinemotion-0.11.1 → kinemotion-0.11.3}/.github/workflows/release.yml +0 -0
  17. {kinemotion-0.11.1 → kinemotion-0.11.3}/.gitignore +0 -0
  18. {kinemotion-0.11.1 → kinemotion-0.11.3}/.pre-commit-config.yaml +0 -0
  19. {kinemotion-0.11.1 → kinemotion-0.11.3}/.tool-versions +0 -0
  20. {kinemotion-0.11.1 → kinemotion-0.11.3}/CLAUDE.md +0 -0
  21. {kinemotion-0.11.1 → kinemotion-0.11.3}/CODE_OF_CONDUCT.md +0 -0
  22. {kinemotion-0.11.1 → kinemotion-0.11.3}/CONTRIBUTING.md +0 -0
  23. {kinemotion-0.11.1 → kinemotion-0.11.3}/Dockerfile +0 -0
  24. {kinemotion-0.11.1 → kinemotion-0.11.3}/GEMINI.md +0 -0
  25. {kinemotion-0.11.1 → kinemotion-0.11.3}/LICENSE +0 -0
  26. {kinemotion-0.11.1 → kinemotion-0.11.3}/README.md +0 -0
  27. {kinemotion-0.11.1 → kinemotion-0.11.3}/SECURITY.md +0 -0
  28. {kinemotion-0.11.1 → kinemotion-0.11.3}/docs/BULK_PROCESSING.md +0 -0
  29. {kinemotion-0.11.1 → kinemotion-0.11.3}/docs/CAMERA_SETUP.md +0 -0
  30. {kinemotion-0.11.1 → kinemotion-0.11.3}/docs/CAMERA_SETUP_ES.md +0 -0
  31. {kinemotion-0.11.1 → kinemotion-0.11.3}/docs/CMJ_GUIDE.md +0 -0
  32. {kinemotion-0.11.1 → kinemotion-0.11.3}/docs/ERRORS_FINDINGS.md +0 -0
  33. {kinemotion-0.11.1 → kinemotion-0.11.3}/docs/FRAMERATE.md +0 -0
  34. {kinemotion-0.11.1 → kinemotion-0.11.3}/docs/IMU_METADATA_PRESERVATION.md +0 -0
  35. {kinemotion-0.11.1 → kinemotion-0.11.3}/docs/PARAMETERS.md +0 -0
  36. {kinemotion-0.11.1 → kinemotion-0.11.3}/docs/REAL_TIME_ANALYSIS.md +0 -0
  37. {kinemotion-0.11.1 → kinemotion-0.11.3}/docs/TRIPLE_EXTENSION.md +0 -0
  38. {kinemotion-0.11.1 → kinemotion-0.11.3}/docs/VALIDATION_PLAN.md +0 -0
  39. {kinemotion-0.11.1 → kinemotion-0.11.3}/examples/bulk/README.md +0 -0
  40. {kinemotion-0.11.1 → kinemotion-0.11.3}/examples/bulk/bulk_processing.py +0 -0
  41. {kinemotion-0.11.1 → kinemotion-0.11.3}/examples/bulk/simple_example.py +0 -0
  42. {kinemotion-0.11.1 → kinemotion-0.11.3}/examples/programmatic_usage.py +0 -0
  43. {kinemotion-0.11.1 → kinemotion-0.11.3}/samples/cmjs/README.md +0 -0
  44. {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/__init__.py +0 -0
  45. {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/cli.py +0 -0
  46. {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/cmj/__init__.py +0 -0
  47. {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/cmj/joint_angles.py +0 -0
  48. {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/cmj/kinematics.py +0 -0
  49. {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/core/__init__.py +0 -0
  50. {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/core/auto_tuning.py +0 -0
  51. {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/core/filtering.py +0 -0
  52. {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/core/pose.py +0 -0
  53. {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/core/smoothing.py +0 -0
  54. {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/core/video_io.py +0 -0
  55. {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/dropjump/__init__.py +0 -0
  56. {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/dropjump/analysis.py +0 -0
  57. {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/dropjump/debug_overlay.py +0 -0
  58. {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/dropjump/kinematics.py +0 -0
  59. {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/py.typed +0 -0
  60. {kinemotion-0.11.1 → kinemotion-0.11.3}/tests/__init__.py +0 -0
  61. {kinemotion-0.11.1 → kinemotion-0.11.3}/tests/test_adaptive_threshold.py +0 -0
  62. {kinemotion-0.11.1 → kinemotion-0.11.3}/tests/test_api.py +0 -0
  63. {kinemotion-0.11.1 → kinemotion-0.11.3}/tests/test_aspect_ratio.py +0 -0
  64. {kinemotion-0.11.1 → kinemotion-0.11.3}/tests/test_cmj_analysis.py +0 -0
  65. {kinemotion-0.11.1 → kinemotion-0.11.3}/tests/test_cmj_kinematics.py +0 -0
  66. {kinemotion-0.11.1 → kinemotion-0.11.3}/tests/test_com_estimation.py +0 -0
  67. {kinemotion-0.11.1 → kinemotion-0.11.3}/tests/test_contact_detection.py +0 -0
  68. {kinemotion-0.11.1 → kinemotion-0.11.3}/tests/test_filtering.py +0 -0
  69. {kinemotion-0.11.1 → kinemotion-0.11.3}/tests/test_kinematics.py +0 -0
  70. {kinemotion-0.11.1 → kinemotion-0.11.3}/tests/test_polyorder.py +0 -0
@@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  <!-- version list -->
9
9
 
10
+ ## v0.11.3 (2025-11-06)
11
+
12
+ ### Bug Fixes
13
+
14
+ - Deduplicate CLI utilities across CMJ and drop jump modules
15
+ ([`c314083`](https://github.com/feniix/kinemotion/commit/c314083dd6601071f75ded38864f7ba9a9daab3d))
16
+
17
+ - **cmj**: Remove unused countermovement_threshold parameter from process_cmj_video
18
+ ([`a8d9425`](https://github.com/feniix/kinemotion/commit/a8d9425a509b44ccf5c9e983e2d8552e9b5f8839))
19
+
20
+
21
+ ## v0.11.2 (2025-11-06)
22
+
23
+ ### Bug Fixes
24
+
25
+ - **cmj**: Reduce cognitive complexity in _extract_positions_from_landmarks
26
+ ([`9772df6`](https://github.com/feniix/kinemotion/commit/9772df69ca8fb2a46726614dd0adda3795cf0ad1))
27
+
28
+ - **cmj**: Reduce cognitive complexity in cmj_analyze CLI function
29
+ ([`e9c7200`](https://github.com/feniix/kinemotion/commit/e9c720081df171d2b18150a5b370c4471fdf9b19))
30
+
31
+ - **cmj**: Reduce cognitive complexity in debug overlay rendering
32
+ ([`11f35c4`](https://github.com/feniix/kinemotion/commit/11f35c4cf675301bccfef376e12c0ed06470e259))
33
+
34
+ - **cmj**: Remove unused variable and parameters in api and analysis
35
+ ([`e8ef607`](https://github.com/feniix/kinemotion/commit/e8ef60735711f4c715d53049477362284efca433))
36
+
37
+
10
38
  ## v0.11.1 (2025-11-06)
11
39
 
12
40
  ### Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kinemotion
3
- Version: 0.11.1
3
+ Version: 0.11.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kinemotion"
3
- version = "0.11.1"
3
+ version = "0.11.3"
4
4
  description = "Video-based kinematic analysis for athletic performance"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10,<3.13"
@@ -638,6 +638,50 @@ class CMJVideoResult:
638
638
  processing_time: float = 0.0
639
639
 
640
640
 
641
+ def _generate_cmj_outputs(
642
+ output_video: str | None,
643
+ json_output: str | None,
644
+ metrics: CMJMetrics,
645
+ frames: list,
646
+ smoothed_landmarks: list,
647
+ video_width: int,
648
+ video_height: int,
649
+ video_display_width: int,
650
+ video_display_height: int,
651
+ video_fps: float,
652
+ verbose: bool,
653
+ ) -> None:
654
+ """Generate JSON and debug video outputs for CMJ analysis."""
655
+ if json_output:
656
+ import json
657
+
658
+ output_path = Path(json_output)
659
+ output_path.write_text(json.dumps(metrics.to_dict(), indent=2))
660
+ if verbose:
661
+ print(f"Metrics written to: {json_output}")
662
+
663
+ if output_video:
664
+ if verbose:
665
+ print(f"Generating debug video: {output_video}")
666
+
667
+ with CMJDebugOverlayRenderer(
668
+ output_video,
669
+ video_width,
670
+ video_height,
671
+ video_display_width,
672
+ video_display_height,
673
+ video_fps,
674
+ ) as renderer:
675
+ for i, frame in enumerate(frames):
676
+ annotated = renderer.render_frame(
677
+ frame, smoothed_landmarks[i], i, metrics
678
+ )
679
+ renderer.write_frame(annotated)
680
+
681
+ if verbose:
682
+ print(f"Debug video saved: {output_video}")
683
+
684
+
641
685
  def process_cmj_video(
642
686
  video_path: str,
643
687
  quality: str = "balanced",
@@ -645,7 +689,6 @@ def process_cmj_video(
645
689
  json_output: str | None = None,
646
690
  smoothing_window: int | None = None,
647
691
  velocity_threshold: float | None = None,
648
- countermovement_threshold: float | None = None,
649
692
  min_contact_frames: int | None = None,
650
693
  visibility_threshold: float | None = None,
651
694
  detection_confidence: float | None = None,
@@ -666,7 +709,6 @@ def process_cmj_video(
666
709
  json_output: Optional path for JSON metrics output
667
710
  smoothing_window: Optional override for smoothing window
668
711
  velocity_threshold: Optional override for velocity threshold
669
- countermovement_threshold: Optional override for countermovement threshold
670
712
  min_contact_frames: Optional override for minimum contact frames
671
713
  visibility_threshold: Optional override for visibility threshold
672
714
  detection_confidence: Optional override for pose detection confidence
@@ -741,12 +783,6 @@ def process_cmj_video(
741
783
  vertical_positions, _ = _extract_vertical_positions(smoothed_landmarks)
742
784
  tracking_method = "foot"
743
785
 
744
- # Calculate countermovement threshold (FPS-adjusted)
745
- # POSITIVE threshold for downward motion (squatting) in normalized coordinates
746
- cm_threshold = countermovement_threshold
747
- if cm_threshold is None:
748
- cm_threshold = 0.015 * (30.0 / video.fps)
749
-
750
786
  # Detect CMJ phases
751
787
  if verbose:
752
788
  print("Detecting CMJ phases...")
@@ -788,34 +824,19 @@ def process_cmj_video(
788
824
  )
789
825
 
790
826
  # Generate outputs if requested
791
- if json_output:
792
- import json
793
-
794
- output_path = Path(json_output)
795
- output_path.write_text(json.dumps(metrics.to_dict(), indent=2))
796
- if verbose:
797
- print(f"Metrics written to: {json_output}")
798
-
799
- if output_video:
800
- if verbose:
801
- print(f"Generating debug video: {output_video}")
802
-
803
- with CMJDebugOverlayRenderer(
804
- output_video,
805
- video.width,
806
- video.height,
807
- video.display_width,
808
- video.display_height,
809
- video.fps,
810
- ) as renderer:
811
- for i, frame in enumerate(frames):
812
- annotated = renderer.render_frame(
813
- frame, smoothed_landmarks[i], i, metrics
814
- )
815
- renderer.write_frame(annotated)
816
-
817
- if verbose:
818
- print(f"Debug video saved: {output_video}")
827
+ _generate_cmj_outputs(
828
+ output_video,
829
+ json_output,
830
+ metrics,
831
+ frames,
832
+ smoothed_landmarks,
833
+ video.width,
834
+ video.height,
835
+ video.display_width,
836
+ video.display_height,
837
+ video.fps,
838
+ verbose,
839
+ )
819
840
 
820
841
  if verbose:
821
842
  print(f"\nJump height: {metrics.jump_height:.3f}m")
@@ -279,8 +279,6 @@ def find_cmj_takeoff_from_velocity_peak(
279
279
  velocities: np.ndarray,
280
280
  lowest_point_frame: int,
281
281
  fps: float,
282
- window_length: int = 5,
283
- polyorder: int = 2,
284
282
  ) -> float:
285
283
  """
286
284
  Find CMJ takeoff frame as peak upward velocity during concentric phase.
@@ -293,8 +291,6 @@ def find_cmj_takeoff_from_velocity_peak(
293
291
  velocities: Array of SIGNED vertical velocities (negative = upward)
294
292
  lowest_point_frame: Frame at lowest point
295
293
  fps: Video frame rate
296
- window_length: Window size for derivative calculations
297
- polyorder: Polynomial order for Savitzky-Golay filter
298
294
 
299
295
  Returns:
300
296
  Takeoff frame with fractional precision.
@@ -407,7 +403,7 @@ def find_interpolated_takeoff_landing(
407
403
 
408
404
  # Find takeoff using peak velocity method (CMJ-specific)
409
405
  takeoff_frame = find_cmj_takeoff_from_velocity_peak(
410
- positions, velocities, lowest_point_frame, fps, window_length, polyorder
406
+ positions, velocities, lowest_point_frame, fps
411
407
  )
412
408
 
413
409
  # Find landing using position peak and impact detection
@@ -18,8 +18,12 @@ from ..core.auto_tuning import (
18
18
  analyze_video_sample,
19
19
  auto_tune_parameters,
20
20
  )
21
+ from ..core.cli_utils import (
22
+ determine_initial_confidence,
23
+ smooth_landmark_sequence,
24
+ track_all_frames,
25
+ )
21
26
  from ..core.pose import PoseTracker
22
- from ..core.smoothing import smooth_landmarks, smooth_landmarks_advanced
23
27
  from ..core.video_io import VideoProcessor
24
28
  from .analysis import detect_cmj_phases
25
29
  from .debug_overlay import CMJDebugOverlayRenderer
@@ -39,6 +43,64 @@ class AnalysisParameters:
39
43
  tracking_confidence: float | None = None
40
44
 
41
45
 
46
+ def _collect_video_files(video_path: tuple[str, ...]) -> list[str]:
47
+ """Expand glob patterns and collect all video files."""
48
+ video_files: list[str] = []
49
+ for pattern in video_path:
50
+ expanded = glob.glob(pattern)
51
+ if expanded:
52
+ video_files.extend(expanded)
53
+ elif Path(pattern).exists():
54
+ video_files.append(pattern)
55
+ else:
56
+ click.echo(f"Warning: No files found for pattern: {pattern}", err=True)
57
+ return video_files
58
+
59
+
60
+ def _generate_output_paths(
61
+ video: str, output_dir: str | None, json_output_dir: str | None
62
+ ) -> tuple[str | None, str | None]:
63
+ """Generate output paths for debug video and JSON."""
64
+ out_path = None
65
+ json_path = None
66
+ if output_dir:
67
+ out_path = str(Path(output_dir) / f"{Path(video).stem}_debug.mp4")
68
+ if json_output_dir:
69
+ json_path = str(Path(json_output_dir) / f"{Path(video).stem}.json")
70
+ return out_path, json_path
71
+
72
+
73
+ def _process_batch_videos(
74
+ video_files: list[str],
75
+ output_dir: str | None,
76
+ json_output_dir: str | None,
77
+ quality_preset: QualityPreset,
78
+ verbose: bool,
79
+ expert_params: AnalysisParameters,
80
+ workers: int,
81
+ ) -> None:
82
+ """Process multiple videos in batch mode."""
83
+ click.echo(
84
+ f"Batch mode: Processing {len(video_files)} video(s) with {workers} workers",
85
+ err=True,
86
+ )
87
+ click.echo("Note: Batch processing not yet fully implemented", err=True)
88
+ click.echo("Processing videos sequentially...", err=True)
89
+
90
+ for video in video_files:
91
+ try:
92
+ click.echo(f"\nProcessing: {video}", err=True)
93
+ out_path, json_path = _generate_output_paths(
94
+ video, output_dir, json_output_dir
95
+ )
96
+ _process_single(
97
+ video, out_path, json_path, quality_preset, verbose, expert_params
98
+ )
99
+ except Exception as e:
100
+ click.echo(f"Error processing {video}: {e}", err=True)
101
+ continue
102
+
103
+
42
104
  @click.command(name="cmj-analyze")
43
105
  @click.argument("video_path", nargs=-1, type=click.Path(exists=False), required=True)
44
106
  @click.option(
@@ -189,15 +251,7 @@ def cmj_analyze( # NOSONAR(S107) - Click CLI requires individual parameters for
189
251
  --json-output-dir results/ --csv-summary summary.csv
190
252
  """
191
253
  # Expand glob patterns and collect all video files
192
- video_files: list[str] = []
193
- for pattern in video_path:
194
- expanded = glob.glob(pattern)
195
- if expanded:
196
- video_files.extend(expanded)
197
- elif Path(pattern).exists():
198
- video_files.append(pattern)
199
- else:
200
- click.echo(f"Warning: No files found for pattern: {pattern}", err=True)
254
+ video_files = _collect_video_files(video_path)
201
255
 
202
256
  if not video_files:
203
257
  click.echo("Error: No video files found", err=True)
@@ -220,27 +274,15 @@ def cmj_analyze( # NOSONAR(S107) - Click CLI requires individual parameters for
220
274
  )
221
275
 
222
276
  if use_batch:
223
- click.echo(
224
- f"Batch mode: Processing {len(video_files)} video(s) with {workers} workers",
225
- err=True,
277
+ _process_batch_videos(
278
+ video_files,
279
+ output_dir,
280
+ json_output_dir,
281
+ quality_preset,
282
+ verbose,
283
+ expert_params,
284
+ workers,
226
285
  )
227
- click.echo("Note: Batch processing not yet fully implemented", err=True)
228
- click.echo("Processing videos sequentially...", err=True)
229
- for video in video_files:
230
- try:
231
- click.echo(f"\nProcessing: {video}", err=True)
232
- out_path = None
233
- json_path = None
234
- if output_dir:
235
- out_path = str(Path(output_dir) / f"{Path(video).stem}_debug.mp4")
236
- if json_output_dir:
237
- json_path = str(Path(json_output_dir) / f"{Path(video).stem}.json")
238
- _process_single(
239
- video, out_path, json_path, quality_preset, verbose, expert_params
240
- )
241
- except Exception as e:
242
- click.echo(f"Error processing {video}: {e}", err=True)
243
- continue
244
286
  else:
245
287
  # Single video mode
246
288
  try:
@@ -257,53 +299,6 @@ def cmj_analyze( # NOSONAR(S107) - Click CLI requires individual parameters for
257
299
  sys.exit(1)
258
300
 
259
301
 
260
- def _determine_initial_confidence(
261
- quality_preset: QualityPreset,
262
- expert_params: AnalysisParameters,
263
- ) -> tuple[float, float]:
264
- """Determine initial detection and tracking confidence levels."""
265
- initial_detection_conf = 0.5
266
- initial_tracking_conf = 0.5
267
-
268
- if quality_preset == QualityPreset.FAST:
269
- initial_detection_conf = 0.3
270
- initial_tracking_conf = 0.3
271
- elif quality_preset == QualityPreset.ACCURATE:
272
- initial_detection_conf = 0.6
273
- initial_tracking_conf = 0.6
274
-
275
- # Override with expert values if provided
276
- if expert_params.detection_confidence is not None:
277
- initial_detection_conf = expert_params.detection_confidence
278
- if expert_params.tracking_confidence is not None:
279
- initial_tracking_conf = expert_params.tracking_confidence
280
-
281
- return initial_detection_conf, initial_tracking_conf
282
-
283
-
284
- def _track_all_frames(video: VideoProcessor, tracker: PoseTracker) -> tuple[list, list]:
285
- """Track pose landmarks in all video frames."""
286
- click.echo("Tracking pose landmarks...", err=True)
287
- landmarks_sequence = []
288
- frames = []
289
-
290
- bar: Any
291
- with click.progressbar(length=video.frame_count, label="Processing frames") as bar:
292
- while True:
293
- frame = video.read_frame()
294
- if frame is None:
295
- break
296
-
297
- frames.append(frame)
298
- landmarks = tracker.process_frame(frame)
299
- landmarks_sequence.append(landmarks)
300
-
301
- bar.update(1)
302
-
303
- tracker.close()
304
- return frames, landmarks_sequence
305
-
306
-
307
302
  def _apply_expert_param_overrides(
308
303
  params: AutoTunedParams, expert_params: AnalysisParameters
309
304
  ) -> AutoTunedParams:
@@ -348,32 +343,20 @@ def _print_auto_tuned_params(
348
343
  click.echo("=" * 60 + "\n", err=True)
349
344
 
350
345
 
351
- def _smooth_landmark_sequence(
352
- landmarks_sequence: list, params: AutoTunedParams
353
- ) -> list:
354
- """Apply smoothing to landmark sequence."""
355
- if params.outlier_rejection or params.bilateral_filter:
356
- if params.outlier_rejection:
357
- click.echo("Smoothing landmarks with outlier rejection...", err=True)
358
- if params.bilateral_filter:
359
- click.echo(
360
- "Using bilateral temporal filter for edge-preserving smoothing...",
361
- err=True,
362
- )
363
- return smooth_landmarks_advanced(
364
- landmarks_sequence,
365
- window_length=params.smoothing_window,
366
- polyorder=params.polyorder,
367
- use_outlier_rejection=params.outlier_rejection,
368
- use_bilateral=params.bilateral_filter,
369
- )
370
- else:
371
- click.echo("Smoothing landmarks...", err=True)
372
- return smooth_landmarks(
373
- landmarks_sequence,
374
- window_length=params.smoothing_window,
375
- polyorder=params.polyorder,
376
- )
346
+ def _get_foot_position(frame_landmarks: dict | None, last_position: float) -> float:
347
+ """Extract average foot position from frame landmarks."""
348
+ if not frame_landmarks:
349
+ return last_position
350
+
351
+ # Average foot position (ankles and heels)
352
+ foot_y_values = []
353
+ for key in ["left_ankle", "right_ankle", "left_heel", "right_heel"]:
354
+ if key in frame_landmarks:
355
+ foot_y_values.append(frame_landmarks[key][1])
356
+
357
+ if foot_y_values:
358
+ return float(np.mean(foot_y_values))
359
+ return last_position
377
360
 
378
361
 
379
362
  def _extract_positions_from_landmarks(
@@ -391,20 +374,9 @@ def _extract_positions_from_landmarks(
391
374
  position_list: list[float] = []
392
375
 
393
376
  for frame_landmarks in smoothed_landmarks:
394
- if frame_landmarks:
395
- # Average foot position (ankles and heels)
396
- foot_y_values = []
397
- for key in ["left_ankle", "right_ankle", "left_heel", "right_heel"]:
398
- if key in frame_landmarks:
399
- foot_y_values.append(frame_landmarks[key][1])
400
-
401
- if foot_y_values:
402
- avg_y = float(np.mean(foot_y_values))
403
- position_list.append(avg_y)
404
- else:
405
- position_list.append(position_list[-1] if position_list else 0.5)
406
- else:
407
- position_list.append(position_list[-1] if position_list else 0.5)
377
+ last_pos = position_list[-1] if position_list else 0.5
378
+ position = _get_foot_position(frame_landmarks, last_pos)
379
+ position_list.append(position)
408
380
 
409
381
  return np.array(position_list), "foot"
410
382
 
@@ -427,7 +399,7 @@ def _process_single(
427
399
  )
428
400
 
429
401
  # Determine confidence levels
430
- detection_conf, tracking_conf = _determine_initial_confidence(
402
+ detection_conf, tracking_conf = determine_initial_confidence(
431
403
  quality_preset, expert_params
432
404
  )
433
405
 
@@ -436,7 +408,7 @@ def _process_single(
436
408
  min_detection_confidence=detection_conf,
437
409
  min_tracking_confidence=tracking_conf,
438
410
  )
439
- frames, landmarks_sequence = _track_all_frames(video, tracker)
411
+ frames, landmarks_sequence = track_all_frames(video, tracker)
440
412
 
441
413
  if not landmarks_sequence:
442
414
  click.echo("Error: No frames processed", err=True)
@@ -462,7 +434,7 @@ def _process_single(
462
434
  )
463
435
 
464
436
  # Apply smoothing
465
- smoothed_landmarks = _smooth_landmark_sequence(landmarks_sequence, params)
437
+ smoothed_landmarks = smooth_landmark_sequence(landmarks_sequence, params)
466
438
 
467
439
  # Extract foot positions
468
440
  vertical_positions, tracking_method = _extract_positions_from_landmarks(