sports2d 0.8.18__tar.gz → 0.8.20__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.
Files changed (35) hide show
  1. {sports2d-0.8.18 → sports2d-0.8.20}/.github/workflows/continuous-integration.yml +3 -5
  2. {sports2d-0.8.18 → sports2d-0.8.20}/PKG-INFO +12 -10
  3. {sports2d-0.8.18 → sports2d-0.8.20}/README.md +9 -7
  4. {sports2d-0.8.18 → sports2d-0.8.20}/Sports2D/Demo/Config_demo.toml +3 -2
  5. {sports2d-0.8.18 → sports2d-0.8.20}/Sports2D/Sports2D.py +5 -3
  6. {sports2d-0.8.18 → sports2d-0.8.20}/Sports2D/Utilities/tests.py +5 -5
  7. {sports2d-0.8.18 → sports2d-0.8.20}/Sports2D/process.py +258 -155
  8. {sports2d-0.8.18 → sports2d-0.8.20}/pyproject.toml +2 -2
  9. {sports2d-0.8.18 → sports2d-0.8.20}/sports2d.egg-info/PKG-INFO +12 -10
  10. {sports2d-0.8.18 → sports2d-0.8.20}/sports2d.egg-info/requires.txt +2 -2
  11. {sports2d-0.8.18 → sports2d-0.8.20}/.github/workflows/joss_pdf.yml +0 -0
  12. {sports2d-0.8.18 → sports2d-0.8.20}/.github/workflows/publish-on-release.yml +0 -0
  13. {sports2d-0.8.18 → sports2d-0.8.20}/.gitignore +0 -0
  14. {sports2d-0.8.18 → sports2d-0.8.20}/CITATION.cff +0 -0
  15. {sports2d-0.8.18 → sports2d-0.8.20}/Content/Demo_plots.png +0 -0
  16. {sports2d-0.8.18 → sports2d-0.8.20}/Content/Demo_results.png +0 -0
  17. {sports2d-0.8.18 → sports2d-0.8.20}/Content/Demo_terminal.png +0 -0
  18. {sports2d-0.8.18 → sports2d-0.8.20}/Content/Person_selection.png +0 -0
  19. {sports2d-0.8.18 → sports2d-0.8.20}/Content/Video_tuto_Sports2D_Colab.png +0 -0
  20. {sports2d-0.8.18 → sports2d-0.8.20}/Content/joint_convention.png +0 -0
  21. {sports2d-0.8.18 → sports2d-0.8.20}/Content/paper.bib +0 -0
  22. {sports2d-0.8.18 → sports2d-0.8.20}/Content/paper.md +0 -0
  23. {sports2d-0.8.18 → sports2d-0.8.20}/Content/sports2d_blender.gif +0 -0
  24. {sports2d-0.8.18 → sports2d-0.8.20}/Content/sports2d_opensim.gif +0 -0
  25. {sports2d-0.8.18 → sports2d-0.8.20}/LICENSE +0 -0
  26. {sports2d-0.8.18 → sports2d-0.8.20}/Sports2D/Demo/demo.mp4 +0 -0
  27. {sports2d-0.8.18 → sports2d-0.8.20}/Sports2D/Sports2D.ipynb +0 -0
  28. {sports2d-0.8.18 → sports2d-0.8.20}/Sports2D/Utilities/__init__.py +0 -0
  29. {sports2d-0.8.18 → sports2d-0.8.20}/Sports2D/Utilities/common.py +0 -0
  30. {sports2d-0.8.18 → sports2d-0.8.20}/Sports2D/__init__.py +0 -0
  31. {sports2d-0.8.18 → sports2d-0.8.20}/setup.cfg +0 -0
  32. {sports2d-0.8.18 → sports2d-0.8.20}/sports2d.egg-info/SOURCES.txt +0 -0
  33. {sports2d-0.8.18 → sports2d-0.8.20}/sports2d.egg-info/dependency_links.txt +0 -0
  34. {sports2d-0.8.18 → sports2d-0.8.20}/sports2d.egg-info/entry_points.txt +0 -0
  35. {sports2d-0.8.18 → sports2d-0.8.20}/sports2d.egg-info/top_level.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  # This workflow will install Python dependencies, run tests and lint with a variety of Python versions
2
2
  # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
3
3
 
4
- name: Build on Win-MacOS-Ubuntu with Python 3.10-3.11
4
+ name: Build on Win-MacOS-Ubuntu with Python 3.10-3.12
5
5
 
6
6
  on:
7
7
  push:
@@ -23,8 +23,8 @@ jobs:
23
23
  strategy:
24
24
  fail-fast: false
25
25
  matrix:
26
- os: [ubuntu-latest, windows-latest, macos-latest, macos-13]
27
- python-version: ["3.10", "3.11"]
26
+ os: [ubuntu-latest, windows-latest, macos-latest] #, macos-13] # opensim not supported on macos Intel AMD x64 beyond python 3.11
27
+ python-version: ["3.10", "3.11", "3.12"]
28
28
  include:
29
29
  - os: ubuntu-latest
30
30
  cache-path: ~/.cache/pip
@@ -32,8 +32,6 @@ jobs:
32
32
  cache-path: C:\Users\runneradmin\AppData\Local\pip\Cache
33
33
  - os: macos-latest
34
34
  cache-path: ~/Library/Caches/pip
35
- - os: macos-13
36
- cache-path: ~/Library/Caches/pip
37
35
 
38
36
  steps:
39
37
  - name: Checkout code
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sports2d
3
- Version: 0.8.18
3
+ Version: 0.8.20
4
4
  Summary: Compute 2D human pose and angles from a video or a webcam.
5
5
  Author-email: David Pagnon <contact@david-pagnon.com>
6
6
  Maintainer-email: David Pagnon <contact@david-pagnon.com>
@@ -35,10 +35,10 @@ Requires-Dist: ipython
35
35
  Requires-Dist: c3d
36
36
  Requires-Dist: rtmlib
37
37
  Requires-Dist: openvino
38
- Requires-Dist: opencv-python
38
+ Requires-Dist: opencv-python<4.12
39
39
  Requires-Dist: imageio_ffmpeg
40
40
  Requires-Dist: deep-sort-realtime
41
- Requires-Dist: Pose2Sim>=0.10.33
41
+ Requires-Dist: Pose2Sim>=0.10.36
42
42
  Dynamic: license-file
43
43
 
44
44
 
@@ -145,7 +145,7 @@ If you need 3D research-grade markerless joint kinematics, consider using severa
145
145
 
146
146
  > N.B.: Full install is required for OpenSim inverse kinematics.
147
147
 
148
- Open a terminal. Type `python -V` to make sure python >=3.10 <=3.11 is installed. If not, install it [from there](https://www.python.org/downloads/).
148
+ Open a terminal. Type `python -V` to make sure python >=3.10 <=3.12 is installed. If not, install it [from there](https://www.python.org/downloads/).
149
149
 
150
150
  Run:
151
151
  ``` cmd
@@ -169,7 +169,7 @@ pip install .
169
169
  - Install Anaconda or [Miniconda](https://docs.conda.io/en/latest/miniconda.html):\
170
170
  Open an Anaconda prompt and create a virtual environment:
171
171
  ``` cmd
172
- conda create -n Sports2D python=3.10 -y
172
+ conda create -n Sports2D python=3.12 -y
173
173
  conda activate Sports2D
174
174
  ```
175
175
  - **Install OpenSim**:\
@@ -568,7 +568,7 @@ Note that any detection and pose models can be used (first [deploy them with MMP
568
568
  'pose_model':'https://download.openmmlab.com/mmpose/v1/projects/rtmposev1/onnx_sdk/rtmpose-t_simcc-body7_pt-body7_420e-256x192-026a1439_20230504.zip',
569
569
  'pose_input_size':[192,256]}"""
570
570
  ```
571
- - Use `--det_frequency 50`: Will detect poses only every 50 frames, and track keypoints in between, which is faster.
571
+ - Use `--det_frequency 50`: Rtmlib is (by default) a top-down method: detects bounding boxes for every person in the frame, and then detects keypoints inside of each box. The person detection stage is much slower. You can choose to detect persons only every 50 frames (for example), and track bounding boxes inbetween, which is much faster.
572
572
  - Use `--load_trc_px <path_to_file_px.trc>`: Will use pose estimation results from a file. Useful if you want to use different parameters for pixel to meter conversion or angle calculation without running detection and pose estimation all over.
573
573
  - Make sure you use `--tracking_mode sports2d`: Will use the default Sports2D tracker. Unlike DeepSort, it is faster, does not require any parametrization, and is as good in non-crowded scenes.
574
574
 
@@ -637,13 +637,13 @@ Sports2D:
637
637
 
638
638
  1. **Reads stream from a webcam, from one video, or from a list of videos**. Selects the specified time range to process.
639
639
 
640
- 2. **Sets up pose estimation with RTMLib.** It can be run in lightweight, balanced, or performance mode, and for faster inference, keypoints can be tracked instead of detected for a certain number of frames. Any RTMPose model can be used.
640
+ 2. **Sets up pose estimation with RTMLib.** It can be run in lightweight, balanced, or performance mode, and for faster inference, the person bounding boxes can be tracked instead of detected every frame. Any RTMPose model can be used.
641
641
 
642
642
  3. **Tracks people** so that their IDs are consistent across frames. A person is associated to another in the next frame when they are at a small distance. IDs remain consistent even if the person disappears from a few frames. We crafted a 'sports2D' tracker which gives good results and runs in real time, but it is also possible to use `deepsort` in particularly challenging situations.
643
643
 
644
- 4. **Chooses the right persons to keep.** In single-person mode, only keeps the person with the highest average scores over the sequence. In multi-person mode, only retrieves the keypoints with high enough confidence, and only keeps the persons with high enough average confidence over each frame.
644
+ 4. **Chooses which persons to analyze.** In single-person mode, only keeps the person with the highest average scores over the sequence. In multi-person mode, you can choose the number of persons to analyze (`nb_persons_to_detect`), and how to order them (`person_ordering_method`). The ordering method can be 'on_click', 'highest_likelihood', 'largest_size', 'smallest_size', 'greatest_displacement', 'least_displacement', 'first_detected', or 'last_detected'. `on_click` is default and lets the user click on the persons they are interested in, in the desired order.
645
645
 
646
- 4. **Converts the pixel coordinates to meters.** The user can provide a calibration file, or simply the size of a specified person. The floor angle and the coordinate origin can either be detected automatically from the gait sequence, or be manually specified. The depth coordinates are set to normative values, depending on whether the person is going left, right, facing the camera, or looking away.
646
+ 4. **Converts the pixel coordinates to meters.** The user can provide the size of a specified person to scale results accordingly. The floor angle and the coordinate origin can either be detected automatically from the gait sequence, or be manually specified. The depth coordinates are set to normative values, depending on whether the person is going left, right, facing the camera, or looking away.
647
647
 
648
648
  5. **Computes the selected joint and segment angles**, and flips them on the left/right side if the respective foot is pointing to the left/right.
649
649
 
@@ -652,12 +652,14 @@ Sports2D:
652
652
  Draws the skeleton and the keypoints, with a green to red color scale to account for their confidence\
653
653
  Draws joint and segment angles on the body, and writes the values either near the joint/segment, or on the upper-left of the image with a progress bar
654
654
 
655
- 6. **Interpolates and filters results:** Missing pose and angle sequences are interpolated unless gaps are too large. Outliers are rejected with a Hampel filter. Results are filtered with a 6 Hz Butterworth filter. Many other filters are available, and all of the above can be configured or deactivated (see [Config_Demo.toml](https://github.com/davidpagnon/Sports2D/blob/main/Sports2D/Demo/Config_demo.toml))
655
+ 6. **Interpolates and filters results:** (1) Swaps between right and left limbs are corrected, (2) Missing pose and angle sequences are interpolated unless gaps are too large, (3) Outliers are rejected with a Hampel filter, and finally (4) Results are filtered, by default with a 6 Hz Butterworth filter. All of the above can be configured or deactivated, and other filters such as Kalman, GCV, Gaussian, LOESS, Median, and Butterworth on speeds are also available (see [Config_Demo.toml](https://github.com/davidpagnon/Sports2D/blob/main/Sports2D/Demo/Config_demo.toml))
656
656
 
657
657
  7. **Optionally show** processed images, saves them, or saves them as a video\
658
658
  **Optionally plots** pose and angle data before and after processing for comparison\
659
659
  **Optionally saves** poses for each person as a TRC file in pixels and meters, angles as a MOT file, and calibration data as a [Pose2Sim](https://github.com/perfanalytics/pose2sim) TOML file
660
660
 
661
+ 8. **Optionally runs scaling and inverse kinematics** with OpenSim via [Pose2Sim](https://github.com/perfanalytics/pose2sim).
662
+
661
663
  <br>
662
664
 
663
665
  **Joint angle conventions:**
@@ -102,7 +102,7 @@ If you need 3D research-grade markerless joint kinematics, consider using severa
102
102
 
103
103
  > N.B.: Full install is required for OpenSim inverse kinematics.
104
104
 
105
- Open a terminal. Type `python -V` to make sure python >=3.10 <=3.11 is installed. If not, install it [from there](https://www.python.org/downloads/).
105
+ Open a terminal. Type `python -V` to make sure python >=3.10 <=3.12 is installed. If not, install it [from there](https://www.python.org/downloads/).
106
106
 
107
107
  Run:
108
108
  ``` cmd
@@ -126,7 +126,7 @@ pip install .
126
126
  - Install Anaconda or [Miniconda](https://docs.conda.io/en/latest/miniconda.html):\
127
127
  Open an Anaconda prompt and create a virtual environment:
128
128
  ``` cmd
129
- conda create -n Sports2D python=3.10 -y
129
+ conda create -n Sports2D python=3.12 -y
130
130
  conda activate Sports2D
131
131
  ```
132
132
  - **Install OpenSim**:\
@@ -525,7 +525,7 @@ Note that any detection and pose models can be used (first [deploy them with MMP
525
525
  'pose_model':'https://download.openmmlab.com/mmpose/v1/projects/rtmposev1/onnx_sdk/rtmpose-t_simcc-body7_pt-body7_420e-256x192-026a1439_20230504.zip',
526
526
  'pose_input_size':[192,256]}"""
527
527
  ```
528
- - Use `--det_frequency 50`: Will detect poses only every 50 frames, and track keypoints in between, which is faster.
528
+ - Use `--det_frequency 50`: Rtmlib is (by default) a top-down method: detects bounding boxes for every person in the frame, and then detects keypoints inside of each box. The person detection stage is much slower. You can choose to detect persons only every 50 frames (for example), and track bounding boxes inbetween, which is much faster.
529
529
  - Use `--load_trc_px <path_to_file_px.trc>`: Will use pose estimation results from a file. Useful if you want to use different parameters for pixel to meter conversion or angle calculation without running detection and pose estimation all over.
530
530
  - Make sure you use `--tracking_mode sports2d`: Will use the default Sports2D tracker. Unlike DeepSort, it is faster, does not require any parametrization, and is as good in non-crowded scenes.
531
531
 
@@ -594,13 +594,13 @@ Sports2D:
594
594
 
595
595
  1. **Reads stream from a webcam, from one video, or from a list of videos**. Selects the specified time range to process.
596
596
 
597
- 2. **Sets up pose estimation with RTMLib.** It can be run in lightweight, balanced, or performance mode, and for faster inference, keypoints can be tracked instead of detected for a certain number of frames. Any RTMPose model can be used.
597
+ 2. **Sets up pose estimation with RTMLib.** It can be run in lightweight, balanced, or performance mode, and for faster inference, the person bounding boxes can be tracked instead of detected every frame. Any RTMPose model can be used.
598
598
 
599
599
  3. **Tracks people** so that their IDs are consistent across frames. A person is associated to another in the next frame when they are at a small distance. IDs remain consistent even if the person disappears from a few frames. We crafted a 'sports2D' tracker which gives good results and runs in real time, but it is also possible to use `deepsort` in particularly challenging situations.
600
600
 
601
- 4. **Chooses the right persons to keep.** In single-person mode, only keeps the person with the highest average scores over the sequence. In multi-person mode, only retrieves the keypoints with high enough confidence, and only keeps the persons with high enough average confidence over each frame.
601
+ 4. **Chooses which persons to analyze.** In single-person mode, only keeps the person with the highest average scores over the sequence. In multi-person mode, you can choose the number of persons to analyze (`nb_persons_to_detect`), and how to order them (`person_ordering_method`). The ordering method can be 'on_click', 'highest_likelihood', 'largest_size', 'smallest_size', 'greatest_displacement', 'least_displacement', 'first_detected', or 'last_detected'. `on_click` is default and lets the user click on the persons they are interested in, in the desired order.
602
602
 
603
- 4. **Converts the pixel coordinates to meters.** The user can provide a calibration file, or simply the size of a specified person. The floor angle and the coordinate origin can either be detected automatically from the gait sequence, or be manually specified. The depth coordinates are set to normative values, depending on whether the person is going left, right, facing the camera, or looking away.
603
+ 4. **Converts the pixel coordinates to meters.** The user can provide the size of a specified person to scale results accordingly. The floor angle and the coordinate origin can either be detected automatically from the gait sequence, or be manually specified. The depth coordinates are set to normative values, depending on whether the person is going left, right, facing the camera, or looking away.
604
604
 
605
605
  5. **Computes the selected joint and segment angles**, and flips them on the left/right side if the respective foot is pointing to the left/right.
606
606
 
@@ -609,12 +609,14 @@ Sports2D:
609
609
  Draws the skeleton and the keypoints, with a green to red color scale to account for their confidence\
610
610
  Draws joint and segment angles on the body, and writes the values either near the joint/segment, or on the upper-left of the image with a progress bar
611
611
 
612
- 6. **Interpolates and filters results:** Missing pose and angle sequences are interpolated unless gaps are too large. Outliers are rejected with a Hampel filter. Results are filtered with a 6 Hz Butterworth filter. Many other filters are available, and all of the above can be configured or deactivated (see [Config_Demo.toml](https://github.com/davidpagnon/Sports2D/blob/main/Sports2D/Demo/Config_demo.toml))
612
+ 6. **Interpolates and filters results:** (1) Swaps between right and left limbs are corrected, (2) Missing pose and angle sequences are interpolated unless gaps are too large, (3) Outliers are rejected with a Hampel filter, and finally (4) Results are filtered, by default with a 6 Hz Butterworth filter. All of the above can be configured or deactivated, and other filters such as Kalman, GCV, Gaussian, LOESS, Median, and Butterworth on speeds are also available (see [Config_Demo.toml](https://github.com/davidpagnon/Sports2D/blob/main/Sports2D/Demo/Config_demo.toml))
613
613
 
614
614
  7. **Optionally show** processed images, saves them, or saves them as a video\
615
615
  **Optionally plots** pose and angle data before and after processing for comparison\
616
616
  **Optionally saves** poses for each person as a TRC file in pixels and meters, angles as a MOT file, and calibration data as a [Pose2Sim](https://github.com/perfanalytics/pose2sim) TOML file
617
617
 
618
+ 8. **Optionally runs scaling and inverse kinematics** with OpenSim via [Pose2Sim](https://github.com/perfanalytics/pose2sim).
619
+
618
620
  <br>
619
621
 
620
622
  **Joint angle conventions:**
@@ -60,8 +60,8 @@ pose_model = 'Body_with_feet' #With RTMLib:
60
60
  # - Hand (HAND_21, only lightweight mode. Potentially better results with Whole_body),
61
61
  # - Face (FACE_106),
62
62
  # - Animal (ANIMAL2D_17)
63
- # /!\ Only RTMPose is natively embeded in Pose2Sim. For all other pose estimation methods, you will have to run them yourself, and then refer to the documentation to convert the output files if needed
64
- # /!\ For Face and Animal, use mode="""{dictionary}""", and find the corresponding .onnx model there https://github.com/open-mmlab/mmpose/tree/main/projects/rtmpose
63
+ # Only RTMPose is natively embeded in Pose2Sim. For all other pose estimation methods, you will have to run them yourself, and then refer to the documentation to convert the output files if needed
64
+ # For Face and Animal, use mode="""{dictionary}""", and find the corresponding .onnx model there https://github.com/open-mmlab/mmpose/tree/main/projects/rtmpose
65
65
  mode = 'balanced' # 'lightweight', 'balanced', 'performance', or """{dictionary}""" (see below)
66
66
 
67
67
  # A dictionary (WITHIN THREE DOUBLE QUOTES) allows you to manually select the person detection (if top_down approach) and/or pose estimation models (see https://github.com/Tau-J/rtmlib).
@@ -139,6 +139,7 @@ reject_outliers = true # Hampel filter for outlier rejection before other f
139
139
 
140
140
  filter = true
141
141
  show_graphs = true # Show plots of raw and processed results
142
+ save_graphs = false # Save position and angle plots of raw and processed results
142
143
  filter_type = 'butterworth' # butterworth, kalman, gcv_spline, gaussian, loess, median, butterworth_on_speed
143
144
 
144
145
  # Most intuitive and standard filter in biomechanics
@@ -28,7 +28,7 @@
28
28
  - Run on webcam with default parameters:
29
29
  sports2d --video_input webcam
30
30
  - Run with custom parameters (all non specified are set to default):
31
- sports2d --show_plots False --time_range 0 2.1 --result_dir path_to_result_dir
31
+ sports2d --show_graphs False --time_range 0 2.1 --result_dir path_to_result_dir
32
32
  sports2d --person_detection_method highest_likelihood --mode lightweight --det_frequency 50
33
33
  - Run with a toml configuration file:
34
34
  sports2d --config path_to_config.toml
@@ -44,7 +44,7 @@
44
44
  pip install .
45
45
 
46
46
  -----
47
- /!\ Warning /!\
47
+ Warning
48
48
  -----
49
49
  - The angle estimation is only as good as the pose estimation algorithm, i.e., it is not perfect.
50
50
  - It will only lead to acceptable results if the persons move in the 2D plane (sagittal plane).
@@ -236,6 +236,7 @@ DEFAULT_CONFIG = {'base': {'video_input': ['demo.mp4'],
236
236
  'reject_outliers': True,
237
237
  'filter': True,
238
238
  'show_graphs': True,
239
+ 'save_graphs': False,
239
240
  'filter_type': 'butterworth',
240
241
  'butterworth': {'order': 4, 'cut_off_frequency': 6.0},
241
242
  'kalman': {'trust_ratio': 500.0, 'smooth':True},
@@ -279,6 +280,7 @@ CONFIG_HELP = {'config': ["C", "path to a toml configuration file"],
279
280
  'show_realtime_results': ["R", "show results in real-time. true if not specified"],
280
281
  'display_angle_values_on': ["a", '"body", "list", "body" "list", or "none". body list if not specified'],
281
282
  'show_graphs': ["G", "show plots of raw and processed results. true if not specified"],
283
+ 'save_graphs': ["", "save position and angle plots of raw and processed results. false if not specified"],
282
284
  'joint_angles': ["j", '"Right ankle" "Left ankle" "Right knee" "Left knee" "Right hip" "Left hip" "Right shoulder" "Left shoulder" "Right elbow" "Left elbow" if not specified'],
283
285
  'segment_angles': ["s", '"Right foot" "Left foot" "Right shank" "Left shank" "Right thigh" "Left thigh" "Pelvis" "Trunk" "Shoulders" "Head" "Right arm" "Left arm" "Right forearm" "Left forearm" if not specified'],
284
286
  'save_vid': ["V", "save processed video. true if not specified"],
@@ -546,7 +548,7 @@ def main():
546
548
  - Run on webcam with default parameters:
547
549
  sports2d --video_input webcam
548
550
  - Run with custom parameters (all non specified are set to default):
549
- sports2d --show_plots False --time_range 0 2.1 --result_dir path_to_result_dir
551
+ sports2d --show_graphs False --time_range 0 2.1 --result_dir path_to_result_dir
550
552
  sports2d --mode lightweight --det_frequency 50
551
553
  - Run with a toml configuration file:
552
554
  sports2d --config path_to_config.toml
@@ -63,14 +63,14 @@ def test_workflow():
63
63
 
64
64
  # Default
65
65
  demo_cmd = ["sports2d", "--person_ordering_method", "highest_likelihood", "--show_realtime_results", "False", "--show_graphs", "False"]
66
- subprocess.run(demo_cmd, check=True, capture_output=True, text=True, encoding='utf-8')
66
+ subprocess.run(demo_cmd, check=True, capture_output=True, text=True, encoding='utf-8', errors='replace')
67
67
 
68
68
  # With loading a trc file, visible_side 'front', first_person_height '1.76", floor_angle 0, xy_origin [0, 928]
69
69
  demo_cmd2 = ["sports2d", "--show_realtime_results", "False", "--show_graphs", "False",
70
70
  "--load_trc_px", os.path.join(root_dir, "demo_Sports2D", "demo_Sports2D_px_person01.trc"),
71
71
  "--visible_side", "front", "--first_person_height", "1.76", "--time_range", "1.2", "2.7",
72
72
  "--floor_angle", "0", "--xy_origin", "0", "928"]
73
- subprocess.run(demo_cmd2, check=True, capture_output=True, text=True, encoding='utf-8')
73
+ subprocess.run(demo_cmd2, check=True, capture_output=True, text=True, encoding='utf-8', errors='replace')
74
74
 
75
75
  # With no pixels to meters conversion, one person to select, lightweight mode, detection frequency, slowmo factor, gaussian filter, RTMO body pose model
76
76
  demo_cmd3 = ["sports2d", "--show_realtime_results", "False", "--show_graphs", "False",
@@ -80,7 +80,7 @@ def test_workflow():
80
80
  "--slowmo_factor", "4",
81
81
  "--filter_type", "gaussian",
82
82
  "--pose_model", "body", "--mode", """{'pose_class':'RTMO', 'pose_model':'https://download.openmmlab.com/mmpose/v1/projects/rtmo/onnx_sdk/rtmo-m_16xb16-600e_body7-640x640-39e78cc4_20231211.zip', 'pose_input_size':[640, 640]}"""]
83
- subprocess.run(demo_cmd3, check=True, capture_output=True, text=True, encoding='utf-8')
83
+ subprocess.run(demo_cmd3, check=True, capture_output=True, text=True, encoding='utf-8', errors='replace')
84
84
 
85
85
  # With a time range, inverse kinematics, marker augmentation
86
86
  demo_cmd4 = ["sports2d", "--person_ordering_method", "greatest_displacement", "--show_realtime_results", "False", "--show_graphs", "False",
@@ -88,7 +88,7 @@ def test_workflow():
88
88
  "--do_ik", "True", "--use_augmentation", "True",
89
89
  "--nb_persons_to_detect", "all", "--first_person_height", "1.65",
90
90
  "--visible_side", "auto", "front", "--participant_mass", "55.0", "67.0"]
91
- subprocess.run(demo_cmd4, check=True, capture_output=True, text=True, encoding='utf-8')
91
+ subprocess.run(demo_cmd4, check=True, capture_output=True, text=True, encoding='utf-8', errors='replace')
92
92
 
93
93
  # From config file
94
94
  config_path = Path(__file__).resolve().parent.parent / 'Demo' / 'Config_demo.toml'
@@ -98,7 +98,7 @@ def test_workflow():
98
98
  config_dict.get("base").update({"person_ordering_method": "highest_likelihood"})
99
99
  with open(config_path, 'w') as f: toml.dump(config_dict, f)
100
100
  demo_cmd5 = ["sports2d", "--config", str(config_path), "--show_realtime_results", "False", "--show_graphs", "False"]
101
- subprocess.run(demo_cmd5, check=True, capture_output=True, text=True, encoding='utf-8')
101
+ subprocess.run(demo_cmd5, check=True, capture_output=True, text=True, encoding='utf-8', errors='replace')
102
102
 
103
103
 
104
104
  if __name__ == "__main__":
@@ -29,7 +29,7 @@
29
29
  - optionally plots pose and angle data before and after processing for comparison
30
30
  - optionally saves poses for each person as a trc file, and angles as a mot file
31
31
 
32
- /!\ Warning /!\
32
+ Warning
33
33
  - The pose detection is only as good as the pose estimation algorithm, i.e., it is not perfect.
34
34
  - It will lead to reliable results only if the persons move in the 2D plane (sagittal or frontal plane).
35
35
  - The persons need to be filmed as perpendicularly as possible from their side.
@@ -77,12 +77,14 @@ from matplotlib.widgets import Slider, Button
77
77
  from matplotlib import patheffects
78
78
 
79
79
  from rtmlib import PoseTracker, BodyWithFeet, Wholebody, Body, Hand, Custom
80
+ from rtmlib.tools.object_detection.post_processings import nms
80
81
  from deep_sort_realtime.deepsort_tracker import DeepSort
81
82
 
82
83
  from Sports2D.Utilities.common import *
83
84
  from Pose2Sim.common import *
84
85
  from Pose2Sim.skeletons import *
85
86
  from Pose2Sim.triangulation import indices_of_first_last_non_nan_chunks
87
+ from Pose2Sim.personAssociation import *
86
88
  from Pose2Sim.filtering import *
87
89
 
88
90
  # Not safe, but to be used until OpenMMLab/RTMlib's SSL certificates are updated
@@ -106,7 +108,7 @@ __status__ = "Development"
106
108
 
107
109
 
108
110
  # FUNCTIONS
109
- def setup_webcam(webcam_id, save_vid, vid_output_path, input_size):
111
+ def setup_webcam(webcam_id, vid_output_path, input_size):
110
112
  '''
111
113
  Set up webcam capture with OpenCV.
112
114
 
@@ -132,29 +134,28 @@ def setup_webcam(webcam_id, save_vid, vid_output_path, input_size):
132
134
  cap.set(cv2.CAP_PROP_FRAME_HEIGHT, input_size[1])
133
135
  cam_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
134
136
  cam_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
137
+ cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
135
138
  fps = round(cap.get(cv2.CAP_PROP_FPS))
136
139
  if fps == 0: fps = 30
137
140
 
138
141
  if cam_width != input_size[0] or cam_height != input_size[1]:
139
142
  logging.warning(f"Warning: Your webcam does not support {input_size[0]}x{input_size[1]} resolution. Resolution set to the closest supported one: {cam_width}x{cam_height}.")
140
143
 
141
- out_vid = None
142
- if save_vid:
143
- # fourcc MJPG produces very large files but is faster. If it is too slow, consider using it and then converting the video to h264
144
- # try:
145
- # fourcc = cv2.VideoWriter_fourcc(*'avc1') # =h264. better compression and quality but may fail on some systems
146
- # out_vid = cv2.VideoWriter(vid_output_path, fourcc, fps, (cam_width, cam_height))
147
- # if not out_vid.isOpened():
148
- # raise ValueError("Failed to open video writer with 'avc1' (h264)")
149
- # except Exception:
150
- fourcc = cv2.VideoWriter_fourcc(*'mp4v')
151
- out_vid = cv2.VideoWriter(vid_output_path, fourcc, fps, (cam_width, cam_height))
152
- # logging.info("Failed to open video writer with 'avc1' (h264). Using 'mp4v' instead.")
144
+ # fourcc MJPG produces very large files but is faster. If it is too slow, consider using it and then converting the video to h264
145
+ # try:
146
+ # fourcc = cv2.VideoWriter_fourcc(*'avc1') # =h264. better compression and quality but may fail on some systems
147
+ # out_vid = cv2.VideoWriter(vid_output_path, fourcc, fps, (cam_width, cam_height))
148
+ # if not out_vid.isOpened():
149
+ # raise ValueError("Failed to open video writer with 'avc1' (h264)")
150
+ # except Exception:
151
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
152
+ out_vid = cv2.VideoWriter(vid_output_path, fourcc, fps, (cam_width, cam_height))
153
+ # logging.info("Failed to open video writer with 'avc1' (h264). Using 'mp4v' instead.")
153
154
 
154
155
  return cap, out_vid, cam_width, cam_height, fps
155
156
 
156
157
 
157
- def setup_video(video_file_path, save_vid, vid_output_path):
158
+ def setup_video(video_file_path, vid_output_path, save_vid):
158
159
  '''
159
160
  Set up video capture with OpenCV.
160
161
 
@@ -789,10 +790,10 @@ def make_mot_with_angles(angles, time, mot_path):
789
790
  return angles
790
791
 
791
792
 
792
- def pose_plots(trc_data_unfiltered, trc_data, person_id):
793
+ def pose_plots(trc_data_unfiltered, trc_data, person_id, show=True):
793
794
  '''
794
795
  Displays trc filtered and unfiltered data for comparison
795
- /!\ Often crashes on the third window...
796
+ Often crashes on the third window...
796
797
 
797
798
  INPUTS:
798
799
  - trc_data_unfiltered: pd.DataFrame. The unfiltered trc data
@@ -835,13 +836,16 @@ def pose_plots(trc_data_unfiltered, trc_data, person_id):
835
836
 
836
837
  pw.addPlot(keypoint, f)
837
838
 
838
- pw.show()
839
+ if show:
840
+ pw.show()
841
+
842
+ return pw
839
843
 
840
844
 
841
- def angle_plots(angle_data_unfiltered, angle_data, person_id):
845
+ def angle_plots(angle_data_unfiltered, angle_data, person_id, show=True):
842
846
  '''
843
847
  Displays angle filtered and unfiltered data for comparison
844
- /!\ Often crashes on the third window...
848
+ Often crashes on the third window...
845
849
 
846
850
  INPUTS:
847
851
  - angle_data_unfiltered: pd.DataFrame. The unfiltered angle data
@@ -878,7 +882,10 @@ def angle_plots(angle_data_unfiltered, angle_data, person_id):
878
882
 
879
883
  pw.addPlot(angle, f)
880
884
 
881
- pw.show()
885
+ if show:
886
+ pw.show()
887
+
888
+ return pw
882
889
 
883
890
 
884
891
  def get_personIDs_with_highest_scores(all_frames_scores, nb_persons_to_detect):
@@ -985,12 +992,13 @@ def get_personIDs_with_greatest_displacement(all_frames_X_homog, all_frames_Y_ho
985
992
  return selected_persons
986
993
 
987
994
 
988
- def get_personIDs_on_click(frames, all_frames_X_homog, all_frames_Y_homog):
995
+ def get_personIDs_on_click(video_file_path, frame_range, all_frames_X_homog, all_frames_Y_homog):
989
996
  '''
990
997
  Get the person IDs on click in the image
991
998
 
992
999
  INPUTS:
993
- - frames: list of images read by cv2.imread. shape (Nframes, H, W, 3)
1000
+ - video_file_path: path to video file
1001
+ - frame_range: tuple (start_frame, end_frame)
994
1002
  - all_frames_X_homog: shape (Nframes, Npersons, Nkpts)
995
1003
  - all_frames_Y_homog: shape (Nframes, Npersons, Nkpts)
996
1004
 
@@ -1001,23 +1009,19 @@ def get_personIDs_on_click(frames, all_frames_X_homog, all_frames_Y_homog):
1001
1009
  # Reorganize the coordinates to shape (Nframes, Npersons, Nkpts, Ndims)
1002
1010
  all_pose_coords = np.stack((all_frames_X_homog, all_frames_Y_homog), axis=-1)
1003
1011
 
1004
- # Trim all_pose_coords and frames to the same size
1005
- min_frames = min(all_pose_coords.shape[0], len(frames))
1006
- all_pose_coords = all_pose_coords[:min_frames]
1007
- frames = frames[:min_frames]
1008
-
1009
1012
  # Select person IDs on click on video/image
1010
- selected_persons = select_persons_on_vid(frames, all_pose_coords)
1013
+ selected_persons = select_persons_on_vid(video_file_path, frame_range, all_pose_coords)
1011
1014
 
1012
1015
  return selected_persons
1013
1016
 
1014
1017
 
1015
- def select_persons_on_vid(frames, all_pose_coords):
1018
+ def select_persons_on_vid(video_file_path, frame_range, all_pose_coords):
1016
1019
  '''
1017
1020
  Interactive UI to select persons from a video by clicking on their bounding boxes.
1018
1021
 
1019
1022
  INPUTS:
1020
- - frames: list of images read by cv2.imread. shape (Nframes, H, W, 3)
1023
+ - video_file_path: path to video file
1024
+ - frame_range: tuple (start_frame, end_frame)
1021
1025
  - all_pose_coords: keypoints coordinates. shape (Nframes, Npersons, Nkpts, Ndims)
1022
1026
 
1023
1027
  OUTPUT:
@@ -1031,93 +1035,42 @@ def select_persons_on_vid(frames, all_pose_coords):
1031
1035
  LINE_UNSELECTED_COLOR = 'white'
1032
1036
  LINE_SELECTED_COLOR = 'darkorange'
1033
1037
 
1034
- selected_persons = []
1035
-
1036
- # Calculate bounding boxes for each person in each frame
1037
- n_frames, n_persons = all_pose_coords.shape[0], all_pose_coords.shape[1]
1038
- all_bboxes = []
1039
- for frame_idx in range(n_frames):
1040
- frame_bboxes = []
1041
- for person_idx in range(n_persons):
1042
- # Get keypoints for current person
1043
- keypoints = all_pose_coords[frame_idx, person_idx]
1044
- valid_keypoints = keypoints[~np.isnan(keypoints).all(axis=1)]
1045
- if len(valid_keypoints) > 0:
1046
- # Calculate bounding box
1047
- x_min, y_min = np.min(valid_keypoints, axis=0)
1048
- x_max, y_max = np.max(valid_keypoints, axis=0)
1049
- frame_bboxes.append((x_min, y_min, x_max, y_max))
1050
- else:
1051
- frame_bboxes.append((np.nan, np.nan, np.nan, np.nan)) # No valid bounding box for this person
1052
- all_bboxes.append(frame_bboxes)
1053
- all_bboxes = np.array(all_bboxes) # Shape: (Nframes, Npersons, 4)
1054
-
1055
- # Create figure, axes, and slider
1056
- frame_height, frame_width = frames[0].shape[:2]
1057
- is_vertical = frame_height > frame_width
1058
- if is_vertical:
1059
- fig_height = frame_height / 250 # For vertical videos
1060
- else:
1061
- fig_height = max(frame_height / 300, 6) # For horizontal videos
1062
- fig = plt.figure(figsize=(8, fig_height), num=f'Select the persons to analyze in the desired order')
1063
- fig.patch.set_facecolor(BACKGROUND_COLOR)
1064
-
1065
- video_axes_height = 0.7 if is_vertical else 0.6
1066
- ax_video = plt.axes([0.1, 0.2, 0.8, video_axes_height])
1067
- ax_video.axis('off')
1068
- ax_video.set_facecolor(BACKGROUND_COLOR)
1069
-
1070
- # First image
1071
- frame_rgb = cv2.cvtColor(frames[0], cv2.COLOR_BGR2RGB)
1072
- rects, annotations = [], []
1073
- for person_idx, bbox in enumerate(all_bboxes[0]):
1074
- if ~np.isnan(bbox).any():
1075
- x_min, y_min, x_max, y_max = bbox.astype(int)
1076
- rect = plt.Rectangle(
1077
- (x_min, y_min), x_max - x_min, y_max - y_min,
1078
- linewidth=1, edgecolor=LINE_UNSELECTED_COLOR, facecolor=UNSELECTED_COLOR,
1079
- linestyle='-', path_effects=[patheffects.withSimplePatchShadow()], zorder=2
1080
- )
1081
- ax_video.add_patch(rect)
1082
- annotation = ax_video.text(
1083
- x_min, y_min - 10, f'{person_idx}', color=LINE_UNSELECTED_COLOR, fontsize=7, fontweight='normal',
1084
- bbox=dict(facecolor=UNSELECTED_COLOR, edgecolor=LINE_UNSELECTED_COLOR, boxstyle='square,pad=0.3', path_effects=[patheffects.withSimplePatchShadow()]), zorder=3
1085
- )
1086
- rects.append(rect)
1087
- annotations.append(annotation)
1088
- img_plot = ax_video.imshow(frame_rgb)
1089
-
1090
- # Slider
1091
- ax_slider = plt.axes([ax_video.get_position().x0, ax_video.get_position().y0-0.05, ax_video.get_position().width, 0.04])
1092
- ax_slider.set_facecolor(BACKGROUND_COLOR)
1093
- frame_slider = Slider(
1094
- ax=ax_slider,
1095
- label='',
1096
- valmin=0,
1097
- valmax=len(all_pose_coords)-1,
1098
- valinit=0,
1099
- valstep=1,
1100
- valfmt=None
1101
- )
1102
- frame_slider.poly.set_edgecolor(SLIDER_EDGE_COLOR)
1103
- frame_slider.poly.set_facecolor(SLIDER_COLOR)
1104
- frame_slider.poly.set_linewidth(1)
1105
- frame_slider.valtext.set_visible(False)
1106
-
1107
-
1108
- # Status text and OK button
1109
- ax_status = plt.axes([ax_video.get_position().x0, ax_video.get_position().y0-0.1, 2*ax_video.get_position().width/3, 0.04])
1110
- ax_status.axis('off')
1111
- status_text = ax_status.text(0.0, 0.5, f"Selected: None", color='black', fontsize=10)
1112
1038
 
1113
- ax_button = plt.axes([ax_video.get_position().x0 + 3*ax_video.get_position().width/4, ax_video.get_position().y0-0.1, ax_video.get_position().width/4, 0.04])
1114
- ok_button = Button(ax_button, 'OK', color=BACKGROUND_COLOR)
1039
+ def get_frame(frame_idx):
1040
+ """Get frame with caching"""
1041
+ actual_frame_idx = start_frame + frame_idx
1042
+
1043
+ # Check cache first
1044
+ if actual_frame_idx in frame_cache:
1045
+ # Move to end of cache order (recently used)
1046
+ cache_order.remove(actual_frame_idx)
1047
+ cache_order.append(actual_frame_idx)
1048
+ return frame_cache[actual_frame_idx]
1049
+
1050
+ # Load from video
1051
+ cap.set(cv2.CAP_PROP_POS_FRAMES, actual_frame_idx)
1052
+ success, frame = cap.read()
1053
+ if not success:
1054
+ raise ValueError(f"Could not read frame {actual_frame_idx}")
1055
+
1056
+ # Add to cache
1057
+ frame_cache[actual_frame_idx] = frame.copy()
1058
+ cache_order.append(actual_frame_idx)
1059
+
1060
+ # Remove old frames if cache too large
1061
+ while len(frame_cache) > cache_size:
1062
+ oldest_frame = cache_order.pop(0)
1063
+ if oldest_frame in frame_cache:
1064
+ del frame_cache[oldest_frame]
1065
+
1066
+ return frame
1115
1067
 
1116
1068
 
1117
1069
  def update_frame(val):
1118
1070
  # Update image
1119
1071
  frame_idx = int(frame_slider.val)
1120
- frame_rgb = cv2.cvtColor(frames[frame_idx], cv2.COLOR_BGR2RGB)
1072
+ frame = get_frame(frame_idx)
1073
+ frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
1121
1074
 
1122
1075
  # Update bboxes and annotations
1123
1076
  for items in [rects, annotations]:
@@ -1210,6 +1163,101 @@ def select_persons_on_vid(frames, all_pose_coords):
1210
1163
  plt.close(fig)
1211
1164
 
1212
1165
 
1166
+ # Open video
1167
+ cap = cv2.VideoCapture(video_file_path)
1168
+ if not cap.isOpened():
1169
+ raise ValueError(f"Could not open video: {video_file_path}")
1170
+ start_frame, end_frame = frame_range
1171
+
1172
+
1173
+ # Frame cache for efficiency - only keep recently accessed frames
1174
+ frame_cache = {}
1175
+ cache_size = 20 # Keep last 20 frames in memory
1176
+ cache_order = []
1177
+
1178
+ # Calculate bounding boxes for each person in each frame
1179
+ selected_persons = []
1180
+ n_frames, n_persons = all_pose_coords.shape[0], all_pose_coords.shape[1]
1181
+ all_bboxes = []
1182
+ for frame_idx in range(n_frames):
1183
+ frame_bboxes = []
1184
+ for person_idx in range(n_persons):
1185
+ # Get keypoints for current person
1186
+ keypoints = all_pose_coords[frame_idx, person_idx]
1187
+ valid_keypoints = keypoints[~np.isnan(keypoints).all(axis=1)]
1188
+ if len(valid_keypoints) > 0:
1189
+ # Calculate bounding box
1190
+ x_min, y_min = np.min(valid_keypoints, axis=0)
1191
+ x_max, y_max = np.max(valid_keypoints, axis=0)
1192
+ frame_bboxes.append((x_min, y_min, x_max, y_max))
1193
+ else:
1194
+ frame_bboxes.append((np.nan, np.nan, np.nan, np.nan)) # No valid bounding box for this person
1195
+ all_bboxes.append(frame_bboxes)
1196
+ all_bboxes = np.array(all_bboxes) # Shape: (Nframes, Npersons, 4)
1197
+
1198
+ # Create figure, axes, and slider
1199
+ first_frame = get_frame(0)
1200
+ frame_height, frame_width = first_frame.shape[:2]
1201
+ is_vertical = frame_height > frame_width
1202
+ if is_vertical:
1203
+ fig_height = frame_height / 250 # For vertical videos
1204
+ else:
1205
+ fig_height = max(frame_height / 300, 6) # For horizontal videos
1206
+ fig = plt.figure(figsize=(8, fig_height), num=f'Select the persons to analyze in the desired order')
1207
+ fig.patch.set_facecolor(BACKGROUND_COLOR)
1208
+
1209
+ video_axes_height = 0.7 if is_vertical else 0.6
1210
+ ax_video = plt.axes([0.1, 0.2, 0.8, video_axes_height])
1211
+ ax_video.axis('off')
1212
+ ax_video.set_facecolor(BACKGROUND_COLOR)
1213
+
1214
+ # First image
1215
+ frame_rgb = cv2.cvtColor(first_frame, cv2.COLOR_BGR2RGB)
1216
+ rects, annotations = [], []
1217
+ for person_idx, bbox in enumerate(all_bboxes[0]):
1218
+ if ~np.isnan(bbox).any():
1219
+ x_min, y_min, x_max, y_max = bbox.astype(int)
1220
+ rect = plt.Rectangle(
1221
+ (x_min, y_min), x_max - x_min, y_max - y_min,
1222
+ linewidth=1, edgecolor=LINE_UNSELECTED_COLOR, facecolor=UNSELECTED_COLOR,
1223
+ linestyle='-', path_effects=[patheffects.withSimplePatchShadow()], zorder=2
1224
+ )
1225
+ ax_video.add_patch(rect)
1226
+ annotation = ax_video.text(
1227
+ x_min, y_min - 10, f'{person_idx}', color=LINE_UNSELECTED_COLOR, fontsize=7, fontweight='normal',
1228
+ bbox=dict(facecolor=UNSELECTED_COLOR, edgecolor=LINE_UNSELECTED_COLOR, boxstyle='square,pad=0.3', path_effects=[patheffects.withSimplePatchShadow()]), zorder=3
1229
+ )
1230
+ rects.append(rect)
1231
+ annotations.append(annotation)
1232
+ img_plot = ax_video.imshow(frame_rgb)
1233
+
1234
+ # Slider
1235
+ ax_slider = plt.axes([ax_video.get_position().x0, ax_video.get_position().y0-0.05, ax_video.get_position().width, 0.04])
1236
+ ax_slider.set_facecolor(BACKGROUND_COLOR)
1237
+ frame_slider = Slider(
1238
+ ax=ax_slider,
1239
+ label='',
1240
+ valmin=0,
1241
+ valmax=len(all_pose_coords)-1,
1242
+ valinit=0,
1243
+ valstep=1,
1244
+ valfmt=None
1245
+ )
1246
+ frame_slider.poly.set_edgecolor(SLIDER_EDGE_COLOR)
1247
+ frame_slider.poly.set_facecolor(SLIDER_COLOR)
1248
+ frame_slider.poly.set_linewidth(1)
1249
+ frame_slider.valtext.set_visible(False)
1250
+
1251
+
1252
+ # Status text and OK button
1253
+ ax_status = plt.axes([ax_video.get_position().x0, ax_video.get_position().y0-0.1, 2*ax_video.get_position().width/3, 0.04])
1254
+ ax_status.axis('off')
1255
+ status_text = ax_status.text(0.0, 0.5, f"Selected: None", color='black', fontsize=10)
1256
+
1257
+ ax_button = plt.axes([ax_video.get_position().x0 + 3*ax_video.get_position().width/4, ax_video.get_position().y0-0.1, ax_video.get_position().width/4, 0.04])
1258
+ ok_button = Button(ax_button, 'OK', color=BACKGROUND_COLOR)
1259
+
1260
+
1213
1261
  # Connect events
1214
1262
  frame_slider.on_changed(update_frame)
1215
1263
  fig.canvas.mpl_connect('button_press_event', on_click)
@@ -1333,7 +1381,7 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1333
1381
  - optionally plots pose and angle data before and after processing for comparison
1334
1382
  - optionally saves poses for each person as a trc file, and angles as a mot file
1335
1383
 
1336
- /!\ Warning /!\d
1384
+ Warning
1337
1385
  - The pose detection is only as good as the pose estimation algorithm, i.e., it is not perfect.
1338
1386
  - It will lead to reliable results only if the persons move in the 2D plane (sagittal or frontal plane).
1339
1387
  - The persons need to be filmed as perpendicularly as possible from their side.
@@ -1449,6 +1497,7 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1449
1497
  handle_LR_swap = config_dict.get('post-processing').get('handle_LR_swap', False)
1450
1498
  reject_outliers = config_dict.get('post-processing').get('reject_outliers', False)
1451
1499
  show_plots = config_dict.get('post-processing').get('show_graphs')
1500
+ save_plots = config_dict.get('post-processing').get('save_graphs')
1452
1501
  filter_type = config_dict.get('post-processing').get('filter_type')
1453
1502
  butterworth_filter_order = config_dict.get('post-processing').get('butterworth', {}).get('order')
1454
1503
  butterworth_filter_cutoff = config_dict.get('post-processing').get('butterworth', {}).get('cut_off_frequency')
@@ -1465,12 +1514,14 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1465
1514
  # Create output directories
1466
1515
  if video_file == "webcam":
1467
1516
  current_date = datetime.now().strftime("%Y%m%d_%H%M%S")
1468
- output_dir_name = f'webcam_{current_date}'
1517
+ output_dir_name = f'webcam_{current_date}_Sports2D'
1518
+ video_file_path = result_dir / output_dir_name / f'webcam_{current_date}_raw.mp4'
1469
1519
  else:
1470
- video_file_path = video_dir / video_file
1471
1520
  video_file_stem = video_file.stem
1472
1521
  output_dir_name = f'{video_file_stem}_Sports2D'
1522
+ video_file_path = video_dir / video_file
1473
1523
  output_dir = result_dir / output_dir_name
1524
+ plots_output_dir = output_dir / f'{output_dir_name}_graphs'
1474
1525
  img_output_dir = output_dir / f'{output_dir_name}_img'
1475
1526
  vid_output_path = output_dir / f'{output_dir_name}.mp4'
1476
1527
  pose_output_path = output_dir / f'{output_dir_name}_px.trc'
@@ -1479,6 +1530,8 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1479
1530
  output_dir.mkdir(parents=True, exist_ok=True)
1480
1531
  if save_img:
1481
1532
  img_output_dir.mkdir(parents=True, exist_ok=True)
1533
+ if save_plots:
1534
+ plots_output_dir.mkdir(parents=True, exist_ok=True)
1482
1535
 
1483
1536
  # Inverse kinematics settings
1484
1537
  do_ik = config_dict.get('kinematics').get('do_ik')
@@ -1491,7 +1544,10 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1491
1544
  trimmed_extrema_percent = config_dict.get('kinematics').get('trimmed_extrema_percent')
1492
1545
  close_to_zero_speed_px = config_dict.get('kinematics').get('close_to_zero_speed_px')
1493
1546
  close_to_zero_speed_m = config_dict.get('kinematics').get('close_to_zero_speed_m')
1494
- if do_ik or use_augmentation or do_filter:
1547
+ # Create a Pose2Sim dictionary and fill in missing keys
1548
+ recursivedict = lambda: defaultdict(recursivedict)
1549
+ Pose2Sim_config_dict = recursivedict()
1550
+ if do_ik or use_augmentation:
1495
1551
  try:
1496
1552
  if use_augmentation:
1497
1553
  from Pose2Sim.markerAugmentation import augment_markers_all
@@ -1501,9 +1557,6 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1501
1557
  logging.error("OpenSim package is not installed. Please install it to use inverse kinematics or marker augmentation features (see 'Full install' section of the documentation).")
1502
1558
  raise ImportError("OpenSim package is not installed. Please install it to use inverse kinematics or marker augmentation features (see 'Full install' section of the documentation).")
1503
1559
 
1504
- # Create a Pose2Sim dictionary and fill in missing keys
1505
- recursivedict = lambda: defaultdict(recursivedict)
1506
- Pose2Sim_config_dict = recursivedict()
1507
1560
  # Fill Pose2Sim dictionary (height and mass will be filled later)
1508
1561
  Pose2Sim_config_dict['project']['project_dir'] = str(output_dir)
1509
1562
  Pose2Sim_config_dict['markerAugmentation']['make_c3d'] = make_c3d
@@ -1534,12 +1587,13 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1534
1587
 
1535
1588
  # Set up video capture
1536
1589
  if video_file == "webcam":
1537
- cap, out_vid, cam_width, cam_height, fps = setup_webcam(webcam_id, save_vid, vid_output_path, input_size)
1590
+ cap, out_vid, cam_width, cam_height, fps = setup_webcam(webcam_id, vid_output_path, input_size)
1591
+ frame_rate = fps
1538
1592
  frame_range = [0,sys.maxsize]
1539
1593
  frame_iterator = range(*frame_range)
1540
1594
  logging.warning('Webcam input: the framerate may vary. If results are filtered, Sports2D will use the average framerate as input.')
1541
1595
  else:
1542
- cap, out_vid, cam_width, cam_height, fps = setup_video(video_file_path, save_vid, vid_output_path)
1596
+ cap, out_vid, cam_width, cam_height, fps = setup_video(video_file_path, vid_output_path, save_vid)
1543
1597
  fps *= slowmo_factor
1544
1598
  start_time = get_start_time_ffmpeg(video_file_path)
1545
1599
  frame_range = [int((time_range[0]-start_time) * frame_rate), int((time_range[1]-start_time) * frame_rate)] if time_range else [0, int(cap.get(cv2.CAP_PROP_FRAME_COUNT))]
@@ -1636,10 +1690,11 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1636
1690
  all_frames_X, all_frames_X_flipped, all_frames_Y, all_frames_scores, all_frames_angles = [], [], [], [], []
1637
1691
  frame_processing_times = []
1638
1692
  frame_count = 0
1639
- frames = []
1693
+ first_frame = max(int(t0 * fps), frame_range[0])
1694
+ # frames = []
1640
1695
  while cap.isOpened():
1641
1696
  # Skip to the starting frame
1642
- if frame_count <= int(t0 * fps) or frame_count < frame_range[0]:
1697
+ if frame_count < first_frame:
1643
1698
  cap.read()
1644
1699
  frame_count += 1
1645
1700
  continue
@@ -1659,9 +1714,9 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1659
1714
  if save_angles:
1660
1715
  all_frames_angles.append([])
1661
1716
  continue
1662
- else: # does not store all frames in memory if they are not saved or used for ordering
1663
- if save_img or save_vid or person_ordering_method == 'on_click':
1664
- frames.append(frame.copy())
1717
+ # else: # does not store all frames in memory if they are not saved or used for ordering
1718
+ # if save_img or save_vid or person_ordering_method == 'on_click':
1719
+ # frames.append(frame.copy())
1665
1720
 
1666
1721
  # Retrieve pose or Estimate pose and track people
1667
1722
  if load_trc_px:
@@ -1670,9 +1725,20 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1670
1725
  keypoints = keypoints_all[frame_nb]
1671
1726
  scores = scores_all[frame_nb]
1672
1727
  else:
1728
+ # Save video on the fly if the input is a webcam
1729
+ if video_file == "webcam":
1730
+ out_vid.write(frame)
1731
+
1673
1732
  # Detect poses
1674
1733
  keypoints, scores = pose_tracker(frame)
1675
1734
 
1735
+ # Non maximum suppression (at pose level, not detection)
1736
+ frame_shape = frame.shape
1737
+ bboxes = bbox_xyxy_compute(frame_shape, keypoints, padding=0)
1738
+ score_bboxes = np.array([np.mean(s) for s in scores])
1739
+ keep = nms(bboxes, score_bboxes, nms_thr=0.45)
1740
+ keypoints, scores = keypoints[keep], scores[keep]
1741
+
1676
1742
  # Track poses across frames
1677
1743
  if tracking_mode == 'deepsort':
1678
1744
  keypoints, scores = sort_people_deepsort(keypoints, scores, deepsort_tracker, frame, frame_count)
@@ -1775,8 +1841,11 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1775
1841
  # End of the video is reached
1776
1842
  cap.release()
1777
1843
  logging.info(f"Video processing completed.")
1778
- if save_vid:
1844
+ if save_vid or video_file == "webcam":
1779
1845
  out_vid.release()
1846
+ if video_file == "webcam":
1847
+ vid_output_path.absolute().rename(video_file_path)
1848
+
1780
1849
  if show_realtime_results:
1781
1850
  cv2.destroyAllWindows()
1782
1851
 
@@ -1813,7 +1882,7 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1813
1882
  nb_persons_to_detect = nb_detected_persons
1814
1883
 
1815
1884
  if person_ordering_method == 'on_click':
1816
- selected_persons = get_personIDs_on_click(frames, all_frames_X_homog, all_frames_Y_homog)
1885
+ selected_persons = get_personIDs_on_click(video_file_path, frame_range, all_frames_X_homog, all_frames_Y_homog)
1817
1886
  if len(selected_persons) == 0:
1818
1887
  logging.warning('No persons selected. Analyzing all detected persons.')
1819
1888
  selected_persons = list(range(nb_detected_persons))
@@ -1890,8 +1959,13 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1890
1959
  all_frames_Y_person_interp.replace(np.nan, 0, inplace=True)
1891
1960
 
1892
1961
  # Filter
1962
+ # if handle_LR_swap:
1963
+ # logging.info(f'Handling left-right swaps.')
1964
+ # all_frames_X_person_interp = all_frames_X_person_interp.apply(LR_unswap, axis=0)
1965
+ # all_frames_Y_person_interp = all_frames_Y_person_interp.apply(LR_unswap, axis=0)
1966
+
1893
1967
  if reject_outliers:
1894
- logging.info('Rejecting outliers with Hampel filter.')
1968
+ logging.info('Rejecting outliers with a Hampel filter.')
1895
1969
  all_frames_X_person_interp = all_frames_X_person_interp.apply(hampel_filter, axis=0, args = [round(7*frame_rate/30), 2])
1896
1970
  all_frames_Y_person_interp = all_frames_Y_person_interp.apply(hampel_filter, axis=0, args = [round(7*frame_rate/30), 2])
1897
1971
 
@@ -1943,9 +2017,17 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1943
2017
  columns_to_concat.extend([all_frames_X_person.iloc[:,kpt], all_frames_Y_person.iloc[:,kpt], all_frames_Z_homog.iloc[:,kpt]])
1944
2018
  trc_data_unfiltered_i = pd.concat([all_frames_time] + columns_to_concat, axis=1)
1945
2019
  trc_data_unfiltered.append(trc_data_unfiltered_i)
1946
- if show_plots and not to_meters:
1947
- pose_plots(trc_data_unfiltered_i, trc_data_i, i)
1948
-
2020
+ if not to_meters and (show_plots or save_plots):
2021
+ pw = pose_plots(trc_data_unfiltered_i, trc_data_i, i, show=show_plots)
2022
+ if save_plots:
2023
+ for n, f in enumerate(pw.figure_handles):
2024
+ dpi = pw.canvases[i].figure.dpi
2025
+ f.set_size_inches(1280/dpi, 720/dpi)
2026
+ title = pw.tabs.tabText(n)
2027
+ plot_path = plots_output_dir / (pose_output_path.stem + f'_person{i:02d}_px_{title.replace(" ","_").replace("/","_")}.png')
2028
+ f.savefig(plot_path, dpi=dpi, bbox_inches='tight')
2029
+ logging.info(f'Pose plots (px) saved in {plots_output_dir}.')
2030
+
1949
2031
  all_frames_X_processed[:,idx_person,:], all_frames_Y_processed[:,idx_person,:] = all_frames_X_person_filt, all_frames_Y_person_filt
1950
2032
  if calculate_angles or save_angles:
1951
2033
  all_frames_X_flipped_processed[:,idx_person,:] = all_frames_X_flipped_person
@@ -2031,9 +2113,17 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
2031
2113
  px_to_m_unfiltered_i = [convert_px_to_meters(trc_data_unfiltered[i][kpt_name], first_person_height, height_px, cx, cy, -floor_angle_estim) for kpt_name in new_keypoints_names]
2032
2114
  trc_data_unfiltered_m_i = pd.concat([all_frames_time.rename('time')]+px_to_m_unfiltered_i, axis=1)
2033
2115
 
2034
- if to_meters and show_plots:
2035
- pose_plots(trc_data_unfiltered_m_i, trc_data_m_i, i)
2036
-
2116
+ if to_meters and (show_plots or save_plots):
2117
+ pw = pose_plots(trc_data_unfiltered_m_i, trc_data_m_i, i, show=show_plots)
2118
+ if save_plots:
2119
+ for n, f in enumerate(pw.figure_handles):
2120
+ dpi = pw.canvases[i].figure.dpi
2121
+ f.set_size_inches(1280/dpi, 720/dpi)
2122
+ title = pw.tabs.tabText(n)
2123
+ plot_path = plots_output_dir / (pose_output_path_m.stem + f'_person{i:02d}_m_{title.replace(" ","_").replace("/","_")}.png')
2124
+ f.savefig(plot_path, dpi=dpi, bbox_inches='tight')
2125
+ logging.info(f'Pose plots (m) saved in {plots_output_dir}.')
2126
+
2037
2127
  # Write to trc file
2038
2128
  trc_data_m.append(trc_data_m_i)
2039
2129
  pose_path_person_m_i = (pose_output_path.parent / (pose_output_path_m.stem + f'_person{i:02d}.trc'))
@@ -2140,7 +2230,7 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
2140
2230
 
2141
2231
  # Filter
2142
2232
  if reject_outliers:
2143
- logging.info(f'Rejecting outliers with Hampel filter.')
2233
+ logging.info(f'Rejecting outliers with a Hampel filter.')
2144
2234
  all_frames_angles_person_interp = all_frames_angles_person_interp.apply(hampel_filter, axis=0)
2145
2235
 
2146
2236
  if not do_filter:
@@ -2172,7 +2262,7 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
2172
2262
  logging.error(f"Invalid filter_type: {filter_type}. Must be 'butterworth', 'gcv_spline', 'kalman', 'gaussian', 'loess', or 'median'.")
2173
2263
  raise ValueError(f"Invalid filter_type: {filter_type}. Must be 'butterworth', 'gcv_spline', 'kalman', 'gaussian', 'loess', or 'median'.")
2174
2264
 
2175
- logging.info(f'Filtering with {args}.')
2265
+ logging.info(f'Filtering with {args}')
2176
2266
  all_frames_angles_person_filt = all_frames_angles_person_interp.apply(filter1d, axis=0, args = [Pose2Sim_config_dict, filter_type, frame_rate])
2177
2267
 
2178
2268
  # Add floor_angle_estim to segment angles
@@ -2192,9 +2282,16 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
2192
2282
  logging.info(f'Angles saved to {angles_path_person.resolve()}.')
2193
2283
 
2194
2284
  # Plotting angles before and after interpolation and filtering
2195
- if show_plots:
2196
- all_frames_angles_person.insert(0, 'time', all_frames_time)
2197
- angle_plots(all_frames_angles_person, angle_data, i) # i = current person
2285
+ all_frames_angles_person.insert(0, 'time', all_frames_time)
2286
+ if save_plots and (show_plots or save_plots):
2287
+ pw = angle_plots(all_frames_angles_person, angle_data, i, show=show_plots) # i = current person
2288
+ for n, f in enumerate(pw.figure_handles):
2289
+ dpi = pw.canvases[i].figure.dpi
2290
+ f.set_size_inches(1280/dpi, 720/dpi)
2291
+ title = pw.tabs.tabText(n)
2292
+ plot_path = plots_output_dir / (pose_output_path_m.stem + f'_person{i:02d}_ang_{title.replace(" ","_").replace("/","_")}.png')
2293
+ f.savefig(plot_path, dpi=dpi, bbox_inches='tight')
2294
+ logging.info(f'Pose plots (m) saved in {plots_output_dir}.')
2198
2295
 
2199
2296
 
2200
2297
  #%% ==================================================
@@ -2228,22 +2325,28 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
2228
2325
  new_keypoints_ids = list(range(len(new_keypoints_ids)))
2229
2326
 
2230
2327
  # Draw pose and angles
2328
+ first_frame, last_frame = frame_range
2231
2329
  if 'first_trim' not in locals():
2232
- first_trim, last_trim = 0, frame_count-1
2233
- for frame_count, (frame, valid_X, valid_X_flipped, valid_Y, valid_scores, valid_angles) in enumerate(zip(frames, all_frames_X_processed, all_frames_X_flipped_processed, all_frames_Y_processed, all_frames_scores_processed, all_frames_angles_processed)):
2234
- if frame_count >= first_trim and frame_count <= last_trim:
2235
- img = frame.copy()
2236
- img = draw_bounding_box(img, valid_X, valid_Y, colors=colors, fontSize=fontSize, thickness=thickness)
2237
- img = draw_keypts(img, valid_X, valid_Y, valid_scores, cmap_str='RdYlGn')
2238
- img = draw_skel(img, valid_X, valid_Y, pose_model_with_new_ids)
2239
- if calculate_angles:
2240
- img = draw_angles(img, valid_X, valid_Y, valid_angles, valid_X_flipped, new_keypoints_ids, new_keypoints_names, angle_names, display_angle_values_on=display_angle_values_on, colors=colors, fontSize=fontSize, thickness=thickness)
2241
-
2242
- # Save video or images
2243
- if save_vid:
2244
- out_vid.write(img)
2245
- if save_img:
2246
- cv2.imwrite(str((img_output_dir / f'{output_dir_name}_{(frame_count+frame_range[0]):06d}.png')), img)
2330
+ first_trim, last_trim = first_frame, last_frame
2331
+ cap = cv2.VideoCapture(video_file_path)
2332
+ cap.set(cv2.CAP_PROP_POS_FRAMES, first_frame+first_trim)
2333
+ for i in range(first_trim, last_trim):
2334
+ success, frame = cap.read()
2335
+ if not success:
2336
+ raise ValueError(f"Could not read frame {i}")
2337
+ img = frame.copy()
2338
+ img = draw_bounding_box(img, all_frames_X_processed[i], all_frames_Y_processed[i], colors=colors, fontSize=fontSize, thickness=thickness)
2339
+ img = draw_keypts(img, all_frames_X_processed[i], all_frames_Y_processed[i], all_frames_scores_processed[i], cmap_str='RdYlGn')
2340
+ img = draw_skel(img, all_frames_X_processed[i], all_frames_Y_processed[i], pose_model_with_new_ids)
2341
+ if calculate_angles:
2342
+ img = draw_angles(img, all_frames_X_processed[i], all_frames_Y_processed[i], all_frames_angles_processed[i], all_frames_X_flipped_processed[i], new_keypoints_ids, new_keypoints_names, angle_names, display_angle_values_on=display_angle_values_on, colors=colors, fontSize=fontSize, thickness=thickness)
2343
+
2344
+ # Save video or images
2345
+ if save_vid:
2346
+ out_vid.write(img)
2347
+ if save_img:
2348
+ cv2.imwrite(str((img_output_dir / f'{output_dir_name}_{(i+frame_range[0]):06d}.png')), img)
2349
+ cap.release()
2247
2350
 
2248
2351
  if save_vid:
2249
2352
  out_vid.release()
@@ -46,10 +46,10 @@ dependencies = [
46
46
  "c3d",
47
47
  "rtmlib",
48
48
  "openvino",
49
- "opencv-python",
49
+ "opencv-python<4.12", # otherwise forces numpy>=2.0, which is incompatible with some opensim/python combinations
50
50
  "imageio_ffmpeg",
51
51
  "deep-sort-realtime",
52
- "Pose2Sim>=0.10.33"
52
+ "Pose2Sim>=0.10.36"
53
53
  ]
54
54
 
55
55
  [tool.setuptools_scm]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sports2d
3
- Version: 0.8.18
3
+ Version: 0.8.20
4
4
  Summary: Compute 2D human pose and angles from a video or a webcam.
5
5
  Author-email: David Pagnon <contact@david-pagnon.com>
6
6
  Maintainer-email: David Pagnon <contact@david-pagnon.com>
@@ -35,10 +35,10 @@ Requires-Dist: ipython
35
35
  Requires-Dist: c3d
36
36
  Requires-Dist: rtmlib
37
37
  Requires-Dist: openvino
38
- Requires-Dist: opencv-python
38
+ Requires-Dist: opencv-python<4.12
39
39
  Requires-Dist: imageio_ffmpeg
40
40
  Requires-Dist: deep-sort-realtime
41
- Requires-Dist: Pose2Sim>=0.10.33
41
+ Requires-Dist: Pose2Sim>=0.10.36
42
42
  Dynamic: license-file
43
43
 
44
44
 
@@ -145,7 +145,7 @@ If you need 3D research-grade markerless joint kinematics, consider using severa
145
145
 
146
146
  > N.B.: Full install is required for OpenSim inverse kinematics.
147
147
 
148
- Open a terminal. Type `python -V` to make sure python >=3.10 <=3.11 is installed. If not, install it [from there](https://www.python.org/downloads/).
148
+ Open a terminal. Type `python -V` to make sure python >=3.10 <=3.12 is installed. If not, install it [from there](https://www.python.org/downloads/).
149
149
 
150
150
  Run:
151
151
  ``` cmd
@@ -169,7 +169,7 @@ pip install .
169
169
  - Install Anaconda or [Miniconda](https://docs.conda.io/en/latest/miniconda.html):\
170
170
  Open an Anaconda prompt and create a virtual environment:
171
171
  ``` cmd
172
- conda create -n Sports2D python=3.10 -y
172
+ conda create -n Sports2D python=3.12 -y
173
173
  conda activate Sports2D
174
174
  ```
175
175
  - **Install OpenSim**:\
@@ -568,7 +568,7 @@ Note that any detection and pose models can be used (first [deploy them with MMP
568
568
  'pose_model':'https://download.openmmlab.com/mmpose/v1/projects/rtmposev1/onnx_sdk/rtmpose-t_simcc-body7_pt-body7_420e-256x192-026a1439_20230504.zip',
569
569
  'pose_input_size':[192,256]}"""
570
570
  ```
571
- - Use `--det_frequency 50`: Will detect poses only every 50 frames, and track keypoints in between, which is faster.
571
+ - Use `--det_frequency 50`: Rtmlib is (by default) a top-down method: detects bounding boxes for every person in the frame, and then detects keypoints inside of each box. The person detection stage is much slower. You can choose to detect persons only every 50 frames (for example), and track bounding boxes inbetween, which is much faster.
572
572
  - Use `--load_trc_px <path_to_file_px.trc>`: Will use pose estimation results from a file. Useful if you want to use different parameters for pixel to meter conversion or angle calculation without running detection and pose estimation all over.
573
573
  - Make sure you use `--tracking_mode sports2d`: Will use the default Sports2D tracker. Unlike DeepSort, it is faster, does not require any parametrization, and is as good in non-crowded scenes.
574
574
 
@@ -637,13 +637,13 @@ Sports2D:
637
637
 
638
638
  1. **Reads stream from a webcam, from one video, or from a list of videos**. Selects the specified time range to process.
639
639
 
640
- 2. **Sets up pose estimation with RTMLib.** It can be run in lightweight, balanced, or performance mode, and for faster inference, keypoints can be tracked instead of detected for a certain number of frames. Any RTMPose model can be used.
640
+ 2. **Sets up pose estimation with RTMLib.** It can be run in lightweight, balanced, or performance mode, and for faster inference, the person bounding boxes can be tracked instead of detected every frame. Any RTMPose model can be used.
641
641
 
642
642
  3. **Tracks people** so that their IDs are consistent across frames. A person is associated to another in the next frame when they are at a small distance. IDs remain consistent even if the person disappears from a few frames. We crafted a 'sports2D' tracker which gives good results and runs in real time, but it is also possible to use `deepsort` in particularly challenging situations.
643
643
 
644
- 4. **Chooses the right persons to keep.** In single-person mode, only keeps the person with the highest average scores over the sequence. In multi-person mode, only retrieves the keypoints with high enough confidence, and only keeps the persons with high enough average confidence over each frame.
644
+ 4. **Chooses which persons to analyze.** In single-person mode, only keeps the person with the highest average scores over the sequence. In multi-person mode, you can choose the number of persons to analyze (`nb_persons_to_detect`), and how to order them (`person_ordering_method`). The ordering method can be 'on_click', 'highest_likelihood', 'largest_size', 'smallest_size', 'greatest_displacement', 'least_displacement', 'first_detected', or 'last_detected'. `on_click` is default and lets the user click on the persons they are interested in, in the desired order.
645
645
 
646
- 4. **Converts the pixel coordinates to meters.** The user can provide a calibration file, or simply the size of a specified person. The floor angle and the coordinate origin can either be detected automatically from the gait sequence, or be manually specified. The depth coordinates are set to normative values, depending on whether the person is going left, right, facing the camera, or looking away.
646
+ 4. **Converts the pixel coordinates to meters.** The user can provide the size of a specified person to scale results accordingly. The floor angle and the coordinate origin can either be detected automatically from the gait sequence, or be manually specified. The depth coordinates are set to normative values, depending on whether the person is going left, right, facing the camera, or looking away.
647
647
 
648
648
  5. **Computes the selected joint and segment angles**, and flips them on the left/right side if the respective foot is pointing to the left/right.
649
649
 
@@ -652,12 +652,14 @@ Sports2D:
652
652
  Draws the skeleton and the keypoints, with a green to red color scale to account for their confidence\
653
653
  Draws joint and segment angles on the body, and writes the values either near the joint/segment, or on the upper-left of the image with a progress bar
654
654
 
655
- 6. **Interpolates and filters results:** Missing pose and angle sequences are interpolated unless gaps are too large. Outliers are rejected with a Hampel filter. Results are filtered with a 6 Hz Butterworth filter. Many other filters are available, and all of the above can be configured or deactivated (see [Config_Demo.toml](https://github.com/davidpagnon/Sports2D/blob/main/Sports2D/Demo/Config_demo.toml))
655
+ 6. **Interpolates and filters results:** (1) Swaps between right and left limbs are corrected, (2) Missing pose and angle sequences are interpolated unless gaps are too large, (3) Outliers are rejected with a Hampel filter, and finally (4) Results are filtered, by default with a 6 Hz Butterworth filter. All of the above can be configured or deactivated, and other filters such as Kalman, GCV, Gaussian, LOESS, Median, and Butterworth on speeds are also available (see [Config_Demo.toml](https://github.com/davidpagnon/Sports2D/blob/main/Sports2D/Demo/Config_demo.toml))
656
656
 
657
657
  7. **Optionally show** processed images, saves them, or saves them as a video\
658
658
  **Optionally plots** pose and angle data before and after processing for comparison\
659
659
  **Optionally saves** poses for each person as a TRC file in pixels and meters, angles as a MOT file, and calibration data as a [Pose2Sim](https://github.com/perfanalytics/pose2sim) TOML file
660
660
 
661
+ 8. **Optionally runs scaling and inverse kinematics** with OpenSim via [Pose2Sim](https://github.com/perfanalytics/pose2sim).
662
+
661
663
  <br>
662
664
 
663
665
  **Joint angle conventions:**
@@ -11,7 +11,7 @@ ipython
11
11
  c3d
12
12
  rtmlib
13
13
  openvino
14
- opencv-python
14
+ opencv-python<4.12
15
15
  imageio_ffmpeg
16
16
  deep-sort-realtime
17
- Pose2Sim>=0.10.33
17
+ Pose2Sim>=0.10.36
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes