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.
- {kinemotion-0.11.1 → kinemotion-0.11.3}/CHANGELOG.md +28 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/PKG-INFO +1 -1
- {kinemotion-0.11.1 → kinemotion-0.11.3}/pyproject.toml +1 -1
- {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/api.py +57 -36
- {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/cmj/analysis.py +1 -5
- {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/cmj/cli.py +92 -120
- {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/cmj/debug_overlay.py +200 -165
- kinemotion-0.11.3/src/kinemotion/core/cli_utils.py +113 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/dropjump/cli.py +8 -103
- {kinemotion-0.11.1 → kinemotion-0.11.3}/uv.lock +1 -1
- {kinemotion-0.11.1 → kinemotion-0.11.3}/.dockerignore +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/.github/pull_request_template.md +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/.github/workflows/release.yml +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/.gitignore +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/.pre-commit-config.yaml +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/.tool-versions +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/CLAUDE.md +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/CODE_OF_CONDUCT.md +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/CONTRIBUTING.md +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/Dockerfile +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/GEMINI.md +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/LICENSE +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/README.md +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/SECURITY.md +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/docs/BULK_PROCESSING.md +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/docs/CAMERA_SETUP.md +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/docs/CAMERA_SETUP_ES.md +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/docs/CMJ_GUIDE.md +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/docs/ERRORS_FINDINGS.md +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/docs/FRAMERATE.md +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/docs/IMU_METADATA_PRESERVATION.md +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/docs/PARAMETERS.md +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/docs/REAL_TIME_ANALYSIS.md +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/docs/TRIPLE_EXTENSION.md +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/docs/VALIDATION_PLAN.md +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/examples/bulk/README.md +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/examples/bulk/bulk_processing.py +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/examples/bulk/simple_example.py +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/examples/programmatic_usage.py +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/samples/cmjs/README.md +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/__init__.py +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/cli.py +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/cmj/__init__.py +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/cmj/joint_angles.py +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/cmj/kinematics.py +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/core/__init__.py +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/core/auto_tuning.py +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/core/filtering.py +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/core/pose.py +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/core/smoothing.py +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/core/video_io.py +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/dropjump/__init__.py +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/dropjump/analysis.py +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/dropjump/debug_overlay.py +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/dropjump/kinematics.py +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/src/kinemotion/py.typed +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/tests/__init__.py +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/tests/test_adaptive_threshold.py +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/tests/test_api.py +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/tests/test_aspect_ratio.py +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/tests/test_cmj_analysis.py +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/tests/test_cmj_kinematics.py +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/tests/test_com_estimation.py +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/tests/test_contact_detection.py +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/tests/test_filtering.py +0 -0
- {kinemotion-0.11.1 → kinemotion-0.11.3}/tests/test_kinematics.py +0 -0
- {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.
|
|
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
|
|
@@ -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
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
return
|
|
364
|
-
|
|
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
|
|
395
|
-
|
|
396
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|