sports2d 0.6.1__tar.gz → 0.6.2__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.
- {sports2d-0.6.1 → sports2d-0.6.2}/PKG-INFO +13 -5
- {sports2d-0.6.1 → sports2d-0.6.2}/README.md +10 -3
- {sports2d-0.6.1 → sports2d-0.6.2}/Sports2D/Demo/Config_demo.toml +7 -4
- {sports2d-0.6.1 → sports2d-0.6.2}/Sports2D/Sports2D.py +5 -1
- {sports2d-0.6.1 → sports2d-0.6.2}/Sports2D/Utilities/common.py +371 -20
- {sports2d-0.6.1 → sports2d-0.6.2}/Sports2D/process.py +36 -273
- {sports2d-0.6.1 → sports2d-0.6.2}/setup.cfg +3 -2
- {sports2d-0.6.1 → sports2d-0.6.2}/sports2d.egg-info/PKG-INFO +13 -5
- {sports2d-0.6.1 → sports2d-0.6.2}/sports2d.egg-info/requires.txt +2 -1
- {sports2d-0.6.1 → sports2d-0.6.2}/LICENSE +0 -0
- {sports2d-0.6.1 → sports2d-0.6.2}/Sports2D/Demo/demo.mp4 +0 -0
- {sports2d-0.6.1 → sports2d-0.6.2}/Sports2D/Utilities/__init__.py +0 -0
- {sports2d-0.6.1 → sports2d-0.6.2}/Sports2D/Utilities/filter.py +0 -0
- {sports2d-0.6.1 → sports2d-0.6.2}/Sports2D/Utilities/skeletons.py +0 -0
- {sports2d-0.6.1 → sports2d-0.6.2}/Sports2D/Utilities/tests.py +0 -0
- {sports2d-0.6.1 → sports2d-0.6.2}/Sports2D/__init__.py +0 -0
- {sports2d-0.6.1 → sports2d-0.6.2}/pyproject.toml +0 -0
- {sports2d-0.6.1 → sports2d-0.6.2}/setup.py +0 -0
- {sports2d-0.6.1 → sports2d-0.6.2}/sports2d.egg-info/SOURCES.txt +0 -0
- {sports2d-0.6.1 → sports2d-0.6.2}/sports2d.egg-info/dependency_links.txt +0 -0
- {sports2d-0.6.1 → sports2d-0.6.2}/sports2d.egg-info/entry_points.txt +0 -0
- {sports2d-0.6.1 → sports2d-0.6.2}/sports2d.egg-info/not-zip-safe +0 -0
- {sports2d-0.6.1 → sports2d-0.6.2}/sports2d.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: sports2d
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.2
|
|
4
4
|
Summary: Detect pose and compute 2D joint angles from a video.
|
|
5
5
|
Home-page: https://github.com/davidpagnon/Sports2D
|
|
6
6
|
Author: David Pagnon
|
|
@@ -33,10 +33,11 @@ Requires-Dist: opencv-python
|
|
|
33
33
|
Requires-Dist: matplotlib
|
|
34
34
|
Requires-Dist: PyQt5
|
|
35
35
|
Requires-Dist: statsmodels
|
|
36
|
-
Requires-Dist:
|
|
36
|
+
Requires-Dist: rtmlib
|
|
37
37
|
Requires-Dist: openvino
|
|
38
38
|
Requires-Dist: tqdm
|
|
39
39
|
Requires-Dist: imageio_ffmpeg
|
|
40
|
+
Requires-Dist: deep-sort-realtime
|
|
40
41
|
|
|
41
42
|
|
|
42
43
|
[](https://github.com/davidpagnon/sports2d/actions/workflows/continuous-integration.yml)
|
|
@@ -212,6 +213,9 @@ Note that it does not take distortions into account, and that it will be less ac
|
|
|
212
213
|
``` cmd
|
|
213
214
|
sports2d --multiperson false --pose_model Body --mode lightweight --det_frequency 50
|
|
214
215
|
```
|
|
216
|
+
``` cmd
|
|
217
|
+
sports2d --tracking_mode deepsort --deepsort_params """{'max_age':30, 'n_init':3, 'nms_max_overlap':0.8, 'max_cosine_distance':0.3, 'nn_budget':200, 'max_iou_distance':0.8, 'embedder_gpu': True}"""
|
|
218
|
+
```
|
|
215
219
|
<br>
|
|
216
220
|
|
|
217
221
|
#### Run with a toml configuration file:
|
|
@@ -249,6 +253,7 @@ Note that any detection and pose models can be used (first [deploy them with MMP
|
|
|
249
253
|
- Use `--det_frequency 50`: Will detect poses only every 50 frames, and track keypoints in between, which is faster.
|
|
250
254
|
- Use `--multiperson false`: Can be used if one single person is present in the video. Otherwise, persons' IDs may be mixed up.
|
|
251
255
|
- Use `--load_trc <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.
|
|
256
|
+
- 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.
|
|
252
257
|
|
|
253
258
|
<br>
|
|
254
259
|
|
|
@@ -369,7 +374,7 @@ sports2d --time_range 1.2 2.7 --ik true --person_orientation front none left
|
|
|
369
374
|
|
|
370
375
|
### All the parameters
|
|
371
376
|
|
|
372
|
-
|
|
377
|
+
For a full list of the available parameters, have a look at the [Config_Demo.toml](https://github.com/davidpagnon/Sports2D/blob/main/Sports2D/Demo/Config_demo.toml) file or type:
|
|
373
378
|
|
|
374
379
|
``` cmd
|
|
375
380
|
sports2d --help
|
|
@@ -414,7 +419,10 @@ sports2d --help
|
|
|
414
419
|
'osim_setup_path': ["", "path to OpenSim setup. '../OpenSim_setup' if not specified"],
|
|
415
420
|
'person_orientation': ["", "front, back, left, right, auto, or none. 'front none left' if not specified. If 'auto', will be either left or right depending on the direction of the motion."],
|
|
416
421
|
'close_to_zero_speed_m': ["","Sum for all keypoints: about 50 px/frame or 0.2 m/frame"],
|
|
417
|
-
'multiperson': ["", "multiperson involves tracking: will be faster if set to false. true if not specified"],
|
|
422
|
+
'multiperson': ["", "multiperson involves tracking: will be faster if set to false. true if not specified"],
|
|
423
|
+
'tracking_mode': ["", "sports2d or rtmlib. sports2d is generally much more accurate and comparable in speed. sports2d if not specified"],
|
|
424
|
+
'deepsort_params': ["", 'Deepsort tracking parameters: """{dictionary between 3 double quotes}""". \n\
|
|
425
|
+
More information there: https://github.com/levan92/deep_sort_realtime/blob/master/deep_sort_realtime/deepsort_tracker.py#L51'],
|
|
418
426
|
'input_size': ["", "width, height. 1280, 720 if not specified. Lower resolution will be faster but less precise"],
|
|
419
427
|
'keypoint_likelihood_threshold': ["", "detected keypoints are not retained if likelihood is below this threshold. 0.3 if not specified"],
|
|
420
428
|
'average_likelihood_threshold': ["", "detected persons are not retained if average keypoint likelihood is below this threshold. 0.5 if not specified"],
|
|
@@ -459,7 +467,7 @@ Sports2D:
|
|
|
459
467
|
|
|
460
468
|
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.
|
|
461
469
|
|
|
462
|
-
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.
|
|
470
|
+
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.
|
|
463
471
|
|
|
464
472
|
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.
|
|
465
473
|
|
|
@@ -172,6 +172,9 @@ Note that it does not take distortions into account, and that it will be less ac
|
|
|
172
172
|
``` cmd
|
|
173
173
|
sports2d --multiperson false --pose_model Body --mode lightweight --det_frequency 50
|
|
174
174
|
```
|
|
175
|
+
``` cmd
|
|
176
|
+
sports2d --tracking_mode deepsort --deepsort_params """{'max_age':30, 'n_init':3, 'nms_max_overlap':0.8, 'max_cosine_distance':0.3, 'nn_budget':200, 'max_iou_distance':0.8, 'embedder_gpu': True}"""
|
|
177
|
+
```
|
|
175
178
|
<br>
|
|
176
179
|
|
|
177
180
|
#### Run with a toml configuration file:
|
|
@@ -209,6 +212,7 @@ Note that any detection and pose models can be used (first [deploy them with MMP
|
|
|
209
212
|
- Use `--det_frequency 50`: Will detect poses only every 50 frames, and track keypoints in between, which is faster.
|
|
210
213
|
- Use `--multiperson false`: Can be used if one single person is present in the video. Otherwise, persons' IDs may be mixed up.
|
|
211
214
|
- Use `--load_trc <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.
|
|
215
|
+
- 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.
|
|
212
216
|
|
|
213
217
|
<br>
|
|
214
218
|
|
|
@@ -329,7 +333,7 @@ sports2d --time_range 1.2 2.7 --ik true --person_orientation front none left
|
|
|
329
333
|
|
|
330
334
|
### All the parameters
|
|
331
335
|
|
|
332
|
-
|
|
336
|
+
For a full list of the available parameters, have a look at the [Config_Demo.toml](https://github.com/davidpagnon/Sports2D/blob/main/Sports2D/Demo/Config_demo.toml) file or type:
|
|
333
337
|
|
|
334
338
|
``` cmd
|
|
335
339
|
sports2d --help
|
|
@@ -374,7 +378,10 @@ sports2d --help
|
|
|
374
378
|
'osim_setup_path': ["", "path to OpenSim setup. '../OpenSim_setup' if not specified"],
|
|
375
379
|
'person_orientation': ["", "front, back, left, right, auto, or none. 'front none left' if not specified. If 'auto', will be either left or right depending on the direction of the motion."],
|
|
376
380
|
'close_to_zero_speed_m': ["","Sum for all keypoints: about 50 px/frame or 0.2 m/frame"],
|
|
377
|
-
'multiperson': ["", "multiperson involves tracking: will be faster if set to false. true if not specified"],
|
|
381
|
+
'multiperson': ["", "multiperson involves tracking: will be faster if set to false. true if not specified"],
|
|
382
|
+
'tracking_mode': ["", "sports2d or rtmlib. sports2d is generally much more accurate and comparable in speed. sports2d if not specified"],
|
|
383
|
+
'deepsort_params': ["", 'Deepsort tracking parameters: """{dictionary between 3 double quotes}""". \n\
|
|
384
|
+
More information there: https://github.com/levan92/deep_sort_realtime/blob/master/deep_sort_realtime/deepsort_tracker.py#L51'],
|
|
378
385
|
'input_size': ["", "width, height. 1280, 720 if not specified. Lower resolution will be faster but less precise"],
|
|
379
386
|
'keypoint_likelihood_threshold': ["", "detected keypoints are not retained if likelihood is below this threshold. 0.3 if not specified"],
|
|
380
387
|
'average_likelihood_threshold': ["", "detected persons are not retained if average keypoint likelihood is below this threshold. 0.5 if not specified"],
|
|
@@ -419,7 +426,7 @@ Sports2D:
|
|
|
419
426
|
|
|
420
427
|
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.
|
|
421
428
|
|
|
422
|
-
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.
|
|
429
|
+
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.
|
|
423
430
|
|
|
424
431
|
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.
|
|
425
432
|
|
|
@@ -54,7 +54,7 @@ mode = 'balanced' # 'lightweight', 'balanced', 'performance', or """{dictionary}
|
|
|
54
54
|
|
|
55
55
|
# 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).
|
|
56
56
|
# Models can be local paths or URLs.
|
|
57
|
-
# Make sure the input_sizes are within
|
|
57
|
+
# Make sure the input_sizes are within square brackets, and that they are in the opposite order from the one in the model path (for example, it would be [192,256] for rtmpose-m_simcc-body7_pt-body7-halpe26_700e-256x192-4d3e73dd_20230605.zip).
|
|
58
58
|
# If your pose_model is not provided in skeletons.py, you may have to create your own one (see example at the end of the file).
|
|
59
59
|
# Example, equivalent to mode='balanced':
|
|
60
60
|
# mode = """{'det_class':'YOLOX',
|
|
@@ -68,17 +68,20 @@ mode = 'balanced' # 'lightweight', 'balanced', 'performance', or """{dictionary}
|
|
|
68
68
|
# 'pose_model':'https://download.openmmlab.com/mmpose/v1/projects/rtmo/onnx_sdk/rtmo-m_16xb16-600e_body7-640x640-39e78cc4_20231211.zip',
|
|
69
69
|
# 'pose_input_size':[640, 640]}"""
|
|
70
70
|
|
|
71
|
-
det_frequency =
|
|
71
|
+
det_frequency = 4 # Run person detection only every N frames, and inbetween track previously detected bounding boxes (keypoint detection is still run on all frames).
|
|
72
72
|
# Equal to or greater than 1, can be as high as you want in simple uncrowded cases. Much faster, but might be less accurate.
|
|
73
73
|
device = 'auto' # 'auto', 'CPU', 'CUDA', 'MPS', 'ROCM'
|
|
74
74
|
backend = 'auto' # 'auto', 'openvino', 'onnxruntime', 'opencv'
|
|
75
|
-
tracking_mode = 'sports2d' # '
|
|
75
|
+
tracking_mode = 'sports2d' # 'sports2d' or 'deepsort'. 'deepsort' is slower but more robust in difficult configurations
|
|
76
|
+
deepsort_params = """{'max_age':30, 'n_init':3, 'nms_max_overlap':0.8, 'max_cosine_distance':0.3, 'nn_budget':200, 'max_iou_distance':0.8, 'embedder_gpu': True}""" # """{dictionary between 3 double quotes}"""
|
|
77
|
+
# More robust in crowded scenes but Can be tricky to parametrize. More information there: https://github.com/levan92/deep_sort_realtime/blob/master/deep_sort_realtime/deepsort_tracker.py#L51
|
|
78
|
+
# Note: For even more robust tracking, use 'embedder':'torchreid', which runs osnet_ain_x1_0 by default. Install additional dependencies with: `pip install torchreid gdown tensorboard`
|
|
76
79
|
|
|
77
80
|
|
|
78
81
|
# Processing parameters
|
|
79
82
|
keypoint_likelihood_threshold = 0.3 # Keypoints whose likelihood is lower will not be taken into account
|
|
80
83
|
average_likelihood_threshold = 0.5 # Person will be ignored if average likelihood of good keypoints is lower than this value
|
|
81
|
-
keypoint_number_threshold = 0.3 # Person will be ignored if the number of good keypoints is less than this fraction
|
|
84
|
+
keypoint_number_threshold = 0.3 # Person will be ignored if the number of good keypoints (above keypoint_likelihood_threshold) is less than this fraction
|
|
82
85
|
|
|
83
86
|
|
|
84
87
|
[px_to_meters_conversion]
|
|
@@ -146,6 +146,7 @@ DEFAULT_CONFIG = {'project': {'video_input': ['demo.mp4'],
|
|
|
146
146
|
'device': 'auto',
|
|
147
147
|
'backend': 'auto',
|
|
148
148
|
'tracking_mode': 'sports2d',
|
|
149
|
+
'deepsort_params': """{'max_age':30, 'n_init':3, 'nms_max_overlap':0.8, 'max_cosine_distance':0.3, 'nn_budget':200, 'max_iou_distance':0.8, 'embedder_gpu': True}""",
|
|
149
150
|
'keypoint_likelihood_threshold': 0.3,
|
|
150
151
|
'average_likelihood_threshold': 0.5,
|
|
151
152
|
'keypoint_number_threshold': 0.3
|
|
@@ -248,7 +249,10 @@ CONFIG_HELP = {'config': ["C", "path to a toml configuration file"],
|
|
|
248
249
|
'osim_setup_path': ["", "path to OpenSim setup. '../OpenSim_setup' if not specified"],
|
|
249
250
|
'person_orientation': ["", "front, back, left, right, auto, or none. 'front none left' if not specified. If 'auto', will be either left or right depending on the direction of the motion."],
|
|
250
251
|
'close_to_zero_speed_m': ["","Sum for all keypoints: about 50 px/frame or 0.2 m/frame"],
|
|
251
|
-
'multiperson': ["", "multiperson involves tracking: will be faster if set to false. true if not specified"],
|
|
252
|
+
'multiperson': ["", "multiperson involves tracking: will be faster if set to false. true if not specified"],
|
|
253
|
+
'tracking_mode': ["", "sports2d or rtmlib. sports2d is generally much more accurate and comparable in speed. sports2d if not specified"],
|
|
254
|
+
'deepsort_params': ["", 'Deepsort tracking parameters: """{dictionary between 3 double quotes}""". \n\
|
|
255
|
+
More information there: https://github.com/levan92/deep_sort_realtime/blob/master/deep_sort_realtime/deepsort_tracker.py#L51'], #
|
|
252
256
|
'input_size': ["", "width, height. 1280, 720 if not specified. Lower resolution will be faster but less precise"],
|
|
253
257
|
'keypoint_likelihood_threshold': ["", "detected keypoints are not retained if likelihood is below this threshold. 0.3 if not specified"],
|
|
254
258
|
'average_likelihood_threshold': ["", "detected persons are not retained if average keypoint likelihood is below this threshold. 0.5 if not specified"],
|
|
@@ -20,7 +20,9 @@ import sys
|
|
|
20
20
|
import toml
|
|
21
21
|
import subprocess
|
|
22
22
|
from pathlib import Path
|
|
23
|
+
import itertools as it
|
|
23
24
|
import logging
|
|
25
|
+
from anytree import PreOrderIter
|
|
24
26
|
|
|
25
27
|
import numpy as np
|
|
26
28
|
import pandas as pd
|
|
@@ -28,6 +30,7 @@ from scipy import interpolate
|
|
|
28
30
|
import imageio_ffmpeg as ffmpeg
|
|
29
31
|
import cv2
|
|
30
32
|
|
|
33
|
+
import matplotlib.pyplot as plt
|
|
31
34
|
from PyQt5.QtWidgets import QMainWindow, QApplication, QWidget, QTabWidget, QVBoxLayout
|
|
32
35
|
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
|
|
33
36
|
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
|
|
@@ -466,7 +469,7 @@ def add_neck_hip_coords(kpt_name, p_X, p_Y, p_scores, kpt_ids, kpt_names):
|
|
|
466
469
|
return p_X, p_Y, p_scores
|
|
467
470
|
|
|
468
471
|
|
|
469
|
-
def best_coords_for_measurements(
|
|
472
|
+
def best_coords_for_measurements(Q_coords, keypoints_names, fastest_frames_to_remove_percent=0.2, close_to_zero_speed=0.2, large_hip_knee_angles=45):
|
|
470
473
|
'''
|
|
471
474
|
Compute the best coordinates for measurements, after removing:
|
|
472
475
|
- 20% fastest frames (may be outliers)
|
|
@@ -474,7 +477,7 @@ def best_coords_for_measurements(trc_data, keypoints_names, fastest_frames_to_re
|
|
|
474
477
|
- frames when hip and knee angle below 45° (imprecise coordinates when person is crouching)
|
|
475
478
|
|
|
476
479
|
INPUTS:
|
|
477
|
-
-
|
|
480
|
+
- Q_coords: pd.DataFrame. The XYZ coordinates of each marker
|
|
478
481
|
- keypoints_names: list. The list of marker names
|
|
479
482
|
- fastest_frames_to_remove_percent: float
|
|
480
483
|
- close_to_zero_speed: float (sum for all keypoints: about 50 px/frame or 0.2 m/frame)
|
|
@@ -482,44 +485,46 @@ def best_coords_for_measurements(trc_data, keypoints_names, fastest_frames_to_re
|
|
|
482
485
|
- trimmed_extrema_percent
|
|
483
486
|
|
|
484
487
|
OUTPUT:
|
|
485
|
-
-
|
|
488
|
+
- Q_coords_low_speeds_low_angles: pd.DataFrame. The best coordinates for measurements
|
|
486
489
|
'''
|
|
487
490
|
|
|
488
491
|
# Add MidShoulder column
|
|
489
|
-
df_MidShoulder = pd.DataFrame((
|
|
492
|
+
df_MidShoulder = pd.DataFrame((Q_coords['RShoulder'].values + Q_coords['LShoulder'].values) /2)
|
|
490
493
|
df_MidShoulder.columns = ['MidShoulder']*3
|
|
491
|
-
|
|
494
|
+
Q_coords = pd.concat((Q_coords.reset_index(drop=True), df_MidShoulder), axis=1)
|
|
492
495
|
|
|
493
496
|
# Add Hip column if not present
|
|
494
497
|
n_markers_init = len(keypoints_names)
|
|
495
498
|
if 'Hip' not in keypoints_names:
|
|
496
|
-
df_Hip = pd.DataFrame((
|
|
499
|
+
df_Hip = pd.DataFrame((Q_coords['RHip'].values + Q_coords['LHip'].values) /2)
|
|
497
500
|
df_Hip.columns = ['Hip']*3
|
|
498
|
-
|
|
501
|
+
Q_coords = pd.concat((Q_coords.reset_index(drop=True), df_Hip), axis=1)
|
|
499
502
|
n_markers = len(keypoints_names)
|
|
500
503
|
|
|
501
504
|
# Using 80% slowest frames
|
|
502
|
-
sum_speeds = pd.Series(np.nansum([np.linalg.norm(
|
|
505
|
+
sum_speeds = pd.Series(np.nansum([np.linalg.norm(Q_coords.iloc[:,kpt:kpt+3].diff(), axis=1) for kpt in range(n_markers)], axis=0))
|
|
503
506
|
sum_speeds = sum_speeds[sum_speeds>close_to_zero_speed] # Removing when speeds close to zero (out of frame)
|
|
504
507
|
if len(sum_speeds)==0:
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
+
logging.warning('All frames have speed close to zero. Make sure the person is moving and correctly detected, or change close_to_zero_speed to a lower value. Not restricting the speeds to be above any threshold.')
|
|
509
|
+
Q_coords_low_speeds = Q_coords
|
|
510
|
+
else:
|
|
511
|
+
min_speed_indices = sum_speeds.abs().nsmallest(int(len(sum_speeds) * (1-fastest_frames_to_remove_percent))).index
|
|
512
|
+
Q_coords_low_speeds = Q_coords.iloc[min_speed_indices].reset_index(drop=True)
|
|
508
513
|
|
|
509
514
|
# Only keep frames with hip and knee flexion angles below 45%
|
|
510
515
|
# (if more than 50 of them, else take 50 smallest values)
|
|
511
516
|
try:
|
|
512
|
-
ang_mean = mean_angles(
|
|
513
|
-
|
|
514
|
-
if len(
|
|
515
|
-
|
|
517
|
+
ang_mean = mean_angles(Q_coords_low_speeds, ang_to_consider = ['right knee', 'left knee', 'right hip', 'left hip'])
|
|
518
|
+
Q_coords_low_speeds_low_angles = Q_coords_low_speeds[ang_mean < large_hip_knee_angles]
|
|
519
|
+
if len(Q_coords_low_speeds_low_angles) < 50:
|
|
520
|
+
Q_coords_low_speeds_low_angles = Q_coords_low_speeds.iloc[pd.Series(ang_mean).nsmallest(50).index]
|
|
516
521
|
except:
|
|
517
|
-
logging.warning(f"At least one among the RAnkle, RKnee, RHip, RShoulder, LAnkle, LKnee, LHip, LShoulder markers is missing for computing the knee and hip angles. Not restricting these
|
|
522
|
+
logging.warning(f"At least one among the RAnkle, RKnee, RHip, RShoulder, LAnkle, LKnee, LHip, LShoulder markers is missing for computing the knee and hip angles. Not restricting these angles to be below {large_hip_knee_angles}°.")
|
|
518
523
|
|
|
519
524
|
if n_markers_init < n_markers:
|
|
520
|
-
|
|
525
|
+
Q_coords_low_speeds_low_angles = Q_coords_low_speeds_low_angles.iloc[:,:-3]
|
|
521
526
|
|
|
522
|
-
return
|
|
527
|
+
return Q_coords_low_speeds_low_angles
|
|
523
528
|
|
|
524
529
|
|
|
525
530
|
def compute_height(trc_data, keypoints_names, fastest_frames_to_remove_percent=0.1, close_to_zero_speed=50, large_hip_knee_angles=45, trimmed_extrema_percent=0.5):
|
|
@@ -547,7 +552,7 @@ def compute_height(trc_data, keypoints_names, fastest_frames_to_remove_percent=0
|
|
|
547
552
|
try:
|
|
548
553
|
rfoot, lfoot = [euclidean_distance(trc_data_low_speeds_low_angles[pair[0]],trc_data_low_speeds_low_angles[pair[1]]) for pair in feet_pairs]
|
|
549
554
|
except:
|
|
550
|
-
rfoot, lfoot = 10, 10
|
|
555
|
+
rfoot, lfoot = 0.10, 0.10
|
|
551
556
|
logging.warning('The Heel marker is missing from your model. Considering Foot to Heel size as 10 cm.')
|
|
552
557
|
|
|
553
558
|
ankle_to_shoulder_pairs = [['RAnkle', 'RKnee'], ['RKnee', 'RHip'], ['RHip', 'RShoulder'],
|
|
@@ -688,4 +693,350 @@ def write_calibration(calib_params, toml_path):
|
|
|
688
693
|
fish_str = f'fisheye = false\n\n'
|
|
689
694
|
cal_f.write(cam_str + name_str + size_str + mat_str + dist_str + rot_str + tran_str + fish_str)
|
|
690
695
|
meta = '[metadata]\nadjusted = false\nerror = 0.0\n'
|
|
691
|
-
cal_f.write(meta)
|
|
696
|
+
cal_f.write(meta)
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
def pad_shape(arr, target_len, fill_value=np.nan):
|
|
700
|
+
'''
|
|
701
|
+
Pads an array to the target length with specified fill values
|
|
702
|
+
|
|
703
|
+
INPUTS:
|
|
704
|
+
- arr: Input array to be padded.
|
|
705
|
+
- target_len: The target length of the first dimension after padding.
|
|
706
|
+
- fill_value: The value to use for padding (default: np.nan).
|
|
707
|
+
|
|
708
|
+
OUTPUTS:
|
|
709
|
+
- Padded array with shape (target_len, ...) matching the input dimensions.
|
|
710
|
+
'''
|
|
711
|
+
|
|
712
|
+
if len(arr) < target_len:
|
|
713
|
+
pad_shape = (target_len - len(arr),) + arr.shape[1:]
|
|
714
|
+
padding = np.full(pad_shape, fill_value)
|
|
715
|
+
return np.concatenate((arr, padding))
|
|
716
|
+
|
|
717
|
+
return arr
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
def min_with_single_indices(L, T):
|
|
721
|
+
'''
|
|
722
|
+
Let L be a list (size s) with T associated tuple indices (size s).
|
|
723
|
+
Select the smallest values of L, considering that
|
|
724
|
+
the next smallest value cannot have the same numbers
|
|
725
|
+
in the associated tuple as any of the previous ones.
|
|
726
|
+
|
|
727
|
+
Example:
|
|
728
|
+
L = [ 20, 27, 51, 33, 43, 23, 37, 24, 4, 68, 84, 3 ]
|
|
729
|
+
T = list(it.product(range(2),range(3)))
|
|
730
|
+
= [(0,0),(0,1),(0,2),(0,3),(1,0),(1,1),(1,2),(1,3),(2,0),(2,1),(2,2),(2,3)]
|
|
731
|
+
|
|
732
|
+
- 1st smallest value: 3 with tuple (2,3), index 11
|
|
733
|
+
- 2nd smallest value when excluding indices (2,.) and (.,3), i.e. [(0,0),(0,1),(0,2),X,(1,0),(1,1),(1,2),X,X,X,X,X]:
|
|
734
|
+
20 with tuple (0,0), index 0
|
|
735
|
+
- 3rd smallest value when excluding [X,X,X,X,X,(1,1),(1,2),X,X,X,X,X]:
|
|
736
|
+
23 with tuple (1,1), index 5
|
|
737
|
+
|
|
738
|
+
INPUTS:
|
|
739
|
+
- L: list (size s)
|
|
740
|
+
- T: T associated tuple indices (size s)
|
|
741
|
+
|
|
742
|
+
OUTPUTS:
|
|
743
|
+
- minL: list of smallest values of L, considering constraints on tuple indices
|
|
744
|
+
- argminL: list of indices of smallest values of L (indices of best combinations)
|
|
745
|
+
- T_minL: list of tuples associated with smallest values of L
|
|
746
|
+
'''
|
|
747
|
+
|
|
748
|
+
minL = [np.nanmin(L)]
|
|
749
|
+
argminL = [np.nanargmin(L)]
|
|
750
|
+
T_minL = [T[argminL[0]]]
|
|
751
|
+
|
|
752
|
+
mask_tokeep = np.array([True for t in T])
|
|
753
|
+
i=0
|
|
754
|
+
while mask_tokeep.any()==True:
|
|
755
|
+
mask_tokeep = mask_tokeep & np.array([t[0]!=T_minL[i][0] and t[1]!=T_minL[i][1] for t in T])
|
|
756
|
+
if mask_tokeep.any()==True:
|
|
757
|
+
indicesL_tokeep = np.where(mask_tokeep)[0]
|
|
758
|
+
minL += [np.nanmin(np.array(L)[indicesL_tokeep]) if not np.isnan(np.array(L)[indicesL_tokeep]).all() else np.nan]
|
|
759
|
+
argminL += [indicesL_tokeep[np.nanargmin(np.array(L)[indicesL_tokeep])] if not np.isnan(minL[-1]) else indicesL_tokeep[0]]
|
|
760
|
+
T_minL += (T[argminL[i+1]],)
|
|
761
|
+
i+=1
|
|
762
|
+
|
|
763
|
+
return np.array(minL), np.array(argminL), np.array(T_minL)
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
def sort_people_sports2d(keyptpre, keypt, scores=None):
|
|
767
|
+
'''
|
|
768
|
+
Associate persons across frames (Sports2D method)
|
|
769
|
+
Persons' indices are sometimes swapped when changing frame
|
|
770
|
+
A person is associated to another in the next frame when they are at a small distance
|
|
771
|
+
|
|
772
|
+
N.B.: Requires min_with_single_indices and euclidian_distance function (see common.py)
|
|
773
|
+
|
|
774
|
+
INPUTS:
|
|
775
|
+
- keyptpre: (K, L, M) array of 2D coordinates for K persons in the previous frame, L keypoints, M 2D coordinates
|
|
776
|
+
- keypt: idem keyptpre, for current frame
|
|
777
|
+
- score: (K, L) array of confidence scores for K persons, L keypoints (optional)
|
|
778
|
+
|
|
779
|
+
OUTPUTS:
|
|
780
|
+
- sorted_prev_keypoints: array with reordered persons with values of previous frame if current is empty
|
|
781
|
+
- sorted_keypoints: array with reordered persons --> if scores is not None
|
|
782
|
+
- sorted_scores: array with reordered scores --> if scores is not None
|
|
783
|
+
- associated_tuples: list of tuples with correspondences between persons across frames --> if scores is None (for Pose2Sim.triangulation())
|
|
784
|
+
'''
|
|
785
|
+
|
|
786
|
+
# Generate possible person correspondences across frames
|
|
787
|
+
max_len = max(len(keyptpre), len(keypt))
|
|
788
|
+
keyptpre = pad_shape(keyptpre, max_len, fill_value=np.nan)
|
|
789
|
+
keypt = pad_shape(keypt, max_len, fill_value=np.nan)
|
|
790
|
+
if scores is not None:
|
|
791
|
+
scores = pad_shape(scores, max_len, fill_value=np.nan)
|
|
792
|
+
|
|
793
|
+
# Compute distance between persons from one frame to another
|
|
794
|
+
personsIDs_comb = sorted(list(it.product(range(len(keyptpre)), range(len(keypt)))))
|
|
795
|
+
frame_by_frame_dist = [euclidean_distance(keyptpre[comb[0]],keypt[comb[1]]) for comb in personsIDs_comb]
|
|
796
|
+
frame_by_frame_dist = np.mean(frame_by_frame_dist, axis=1)
|
|
797
|
+
|
|
798
|
+
# Sort correspondences by distance
|
|
799
|
+
_, _, associated_tuples = min_with_single_indices(frame_by_frame_dist, personsIDs_comb)
|
|
800
|
+
|
|
801
|
+
# Associate points to same index across frames, nan if no correspondence
|
|
802
|
+
sorted_keypoints = []
|
|
803
|
+
for i in range(len(keyptpre)):
|
|
804
|
+
id_in_old = associated_tuples[:,1][associated_tuples[:,0] == i].tolist()
|
|
805
|
+
if len(id_in_old) > 0: sorted_keypoints += [keypt[id_in_old[0]]]
|
|
806
|
+
else: sorted_keypoints += [keypt[i]]
|
|
807
|
+
sorted_keypoints = np.array(sorted_keypoints)
|
|
808
|
+
|
|
809
|
+
if scores is not None:
|
|
810
|
+
sorted_scores = []
|
|
811
|
+
for i in range(len(keyptpre)):
|
|
812
|
+
id_in_old = associated_tuples[:,1][associated_tuples[:,0] == i].tolist()
|
|
813
|
+
if len(id_in_old) > 0: sorted_scores += [scores[id_in_old[0]]]
|
|
814
|
+
else: sorted_scores += [scores[i]]
|
|
815
|
+
sorted_scores = np.array(sorted_scores)
|
|
816
|
+
|
|
817
|
+
# Keep track of previous values even when missing for more than one frame
|
|
818
|
+
sorted_prev_keypoints = np.where(np.isnan(sorted_keypoints) & ~np.isnan(keyptpre), keyptpre, sorted_keypoints)
|
|
819
|
+
|
|
820
|
+
if scores is not None:
|
|
821
|
+
return sorted_prev_keypoints, sorted_keypoints, sorted_scores
|
|
822
|
+
else: # For Pose2Sim.triangulation()
|
|
823
|
+
return sorted_keypoints, associated_tuples
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
def sort_people_rtmlib(pose_tracker, keypoints, scores):
|
|
827
|
+
'''
|
|
828
|
+
Associate persons across frames (RTMLib method)
|
|
829
|
+
|
|
830
|
+
INPUTS:
|
|
831
|
+
- pose_tracker: PoseTracker. The initialized RTMLib pose tracker object
|
|
832
|
+
- keypoints: array of shape K, L, M with K the number of detected persons,
|
|
833
|
+
L the number of detected keypoints, M their 2D coordinates
|
|
834
|
+
- scores: array of shape K, L with K the number of detected persons,
|
|
835
|
+
L the confidence of detected keypoints
|
|
836
|
+
|
|
837
|
+
OUTPUT:
|
|
838
|
+
- sorted_keypoints: array with reordered persons
|
|
839
|
+
- sorted_scores: array with reordered scores
|
|
840
|
+
'''
|
|
841
|
+
|
|
842
|
+
try:
|
|
843
|
+
desired_size = max(pose_tracker.track_ids_last_frame)+1
|
|
844
|
+
sorted_keypoints = np.full((desired_size, keypoints.shape[1], 2), np.nan)
|
|
845
|
+
sorted_keypoints[pose_tracker.track_ids_last_frame] = keypoints[:len(pose_tracker.track_ids_last_frame), :, :]
|
|
846
|
+
sorted_scores = np.full((desired_size, scores.shape[1]), np.nan)
|
|
847
|
+
sorted_scores[pose_tracker.track_ids_last_frame] = scores[:len(pose_tracker.track_ids_last_frame), :]
|
|
848
|
+
except:
|
|
849
|
+
sorted_keypoints, sorted_scores = keypoints, scores
|
|
850
|
+
|
|
851
|
+
return sorted_keypoints, sorted_scores
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
def sort_people_deepsort(keypoints, scores, deepsort_tracker, frame,frame_count):
|
|
855
|
+
'''
|
|
856
|
+
Associate persons across frames (DeepSort method)
|
|
857
|
+
|
|
858
|
+
INPUTS:
|
|
859
|
+
- keypoints: array of shape K, L, M with K the number of detected persons,
|
|
860
|
+
L the number of detected keypoints, M their 2D coordinates
|
|
861
|
+
- scores: array of shape K, L with K the number of detected persons,
|
|
862
|
+
L the confidence of detected keypoints
|
|
863
|
+
- deepsort_tracker: The initialized DeepSort tracker object
|
|
864
|
+
- frame: np.array. The current image opened with cv2.imread
|
|
865
|
+
|
|
866
|
+
OUTPUT:
|
|
867
|
+
- sorted_keypoints: array with reordered persons
|
|
868
|
+
- sorted_scores: array with reordered scores
|
|
869
|
+
'''
|
|
870
|
+
|
|
871
|
+
try:
|
|
872
|
+
# Compute bboxes from keypoints and create detections (bboxes, scores, class_ids)
|
|
873
|
+
bboxes_ltwh = bbox_ltwh_compute(keypoints, padding=20)
|
|
874
|
+
bbox_scores = np.mean(scores, axis=1)
|
|
875
|
+
class_ids = np.array(['person']*len(bboxes_ltwh))
|
|
876
|
+
detections = list(zip(bboxes_ltwh, bbox_scores, class_ids))
|
|
877
|
+
|
|
878
|
+
# Estimates the tracks and retrieve indexes of the original detections
|
|
879
|
+
det_ids = [i for i in range(len(detections))]
|
|
880
|
+
tracks = deepsort_tracker.update_tracks(detections, frame=frame, others=det_ids)
|
|
881
|
+
track_ids_frame, orig_det_ids = [], []
|
|
882
|
+
for track in tracks:
|
|
883
|
+
if not track.is_confirmed():
|
|
884
|
+
continue
|
|
885
|
+
track_ids_frame.append(int(track.track_id)-1) # ID of people
|
|
886
|
+
orig_det_ids.append(track.get_det_supplementary()) # ID of detections
|
|
887
|
+
|
|
888
|
+
# Correspondence between person IDs and original detection IDs
|
|
889
|
+
desired_size = max(track_ids_frame) + 1
|
|
890
|
+
sorted_keypoints = np.full((desired_size, keypoints.shape[1], 2), np.nan)
|
|
891
|
+
sorted_scores = np.full((desired_size, scores.shape[1]), np.nan)
|
|
892
|
+
for i,v in enumerate(track_ids_frame):
|
|
893
|
+
if orig_det_ids[i] is not None:
|
|
894
|
+
sorted_keypoints[v] = keypoints[orig_det_ids[i]]
|
|
895
|
+
sorted_scores[v] = scores[orig_det_ids[i]]
|
|
896
|
+
|
|
897
|
+
except Exception as e:
|
|
898
|
+
sorted_keypoints, sorted_scores = keypoints, scores
|
|
899
|
+
if frame_count > deepsort_tracker.tracker.n_init:
|
|
900
|
+
logging.warning(f"Tracking error: {e}. Sorting persons with DeepSort method failed for this frame.")
|
|
901
|
+
|
|
902
|
+
return sorted_keypoints, sorted_scores
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
def bbox_ltwh_compute(keypoints, padding=0):
|
|
906
|
+
'''
|
|
907
|
+
Compute bounding boxes in (x_min, y_min, width, height) format
|
|
908
|
+
Optionally add padding to the bounding boxes
|
|
909
|
+
as a percentage of the bounding box size (+padding% horizontally, +padding/2% vertically)
|
|
910
|
+
|
|
911
|
+
INPUTS:
|
|
912
|
+
- keypoints: array of shape K, L, M with K the number of detected persons,
|
|
913
|
+
L the number of detected keypoints, M their 2D coordinates
|
|
914
|
+
- padding: int. The padding to add to the bounding boxes, in perceptage
|
|
915
|
+
'''
|
|
916
|
+
|
|
917
|
+
x_coords = keypoints[:, :, 0]
|
|
918
|
+
y_coords = keypoints[:, :, 1]
|
|
919
|
+
|
|
920
|
+
x_min, x_max = np.min(x_coords, axis=1), np.max(x_coords, axis=1)
|
|
921
|
+
y_min, y_max = np.min(y_coords, axis=1), np.max(y_coords, axis=1)
|
|
922
|
+
width = x_max - x_min
|
|
923
|
+
height = y_max - y_min
|
|
924
|
+
|
|
925
|
+
if padding > 0:
|
|
926
|
+
x_min = x_min - width*padding/100
|
|
927
|
+
y_min = y_min - height/2*padding/100
|
|
928
|
+
width = width + 2*width*padding/100
|
|
929
|
+
height = height + height*padding/100
|
|
930
|
+
|
|
931
|
+
bbox_ltwh = np.stack((x_min, y_min, width, height), axis=1)
|
|
932
|
+
|
|
933
|
+
return bbox_ltwh
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
def draw_bounding_box(img, X, Y, colors=[(255, 0, 0), (0, 255, 0), (0, 0, 255)], fontSize=0.3, thickness=1):
|
|
937
|
+
'''
|
|
938
|
+
Draw bounding boxes and person ID around list of lists of X and Y coordinates.
|
|
939
|
+
Bounding boxes have a different color for each person.
|
|
940
|
+
|
|
941
|
+
INPUTS:
|
|
942
|
+
- img: opencv image
|
|
943
|
+
- X: list of list of x coordinates
|
|
944
|
+
- Y: list of list of y coordinates
|
|
945
|
+
- colors: list of colors to cycle through
|
|
946
|
+
|
|
947
|
+
OUTPUT:
|
|
948
|
+
- img: image with rectangles and person IDs
|
|
949
|
+
'''
|
|
950
|
+
|
|
951
|
+
color_cycle = it.cycle(colors)
|
|
952
|
+
|
|
953
|
+
for i,(x,y) in enumerate(zip(X,Y)):
|
|
954
|
+
color = next(color_cycle)
|
|
955
|
+
if not np.isnan(x).all():
|
|
956
|
+
x_min, y_min = np.nanmin(x).astype(int), np.nanmin(y).astype(int)
|
|
957
|
+
x_max, y_max = np.nanmax(x).astype(int), np.nanmax(y).astype(int)
|
|
958
|
+
if x_min < 0: x_min = 0
|
|
959
|
+
if x_max > img.shape[1]: x_max = img.shape[1]
|
|
960
|
+
if y_min < 0: y_min = 0
|
|
961
|
+
if y_max > img.shape[0]: y_max = img.shape[0]
|
|
962
|
+
|
|
963
|
+
# Draw rectangles
|
|
964
|
+
cv2.rectangle(img, (x_min-25, y_min-25), (x_max+25, y_max+25), color, thickness)
|
|
965
|
+
|
|
966
|
+
# Write person ID
|
|
967
|
+
cv2.putText(img, str(i), (x_min-30, y_min-30), cv2.FONT_HERSHEY_SIMPLEX, fontSize, color, 2, cv2.LINE_AA)
|
|
968
|
+
|
|
969
|
+
return img
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
def draw_skel(img, X, Y, model):
|
|
973
|
+
'''
|
|
974
|
+
Draws keypoints and skeleton for each person.
|
|
975
|
+
Skeletons have a different color for each person.
|
|
976
|
+
|
|
977
|
+
INPUTS:
|
|
978
|
+
- img: opencv image
|
|
979
|
+
- X: list of list of x coordinates
|
|
980
|
+
- Y: list of list of y coordinates
|
|
981
|
+
- model: skeleton model (from skeletons.py)
|
|
982
|
+
- colors: list of colors to cycle through
|
|
983
|
+
|
|
984
|
+
OUTPUT:
|
|
985
|
+
- img: image with keypoints and skeleton
|
|
986
|
+
'''
|
|
987
|
+
|
|
988
|
+
# Get (unique) pairs between which to draw a line
|
|
989
|
+
id_pairs, name_pairs = [], []
|
|
990
|
+
for data_i in PreOrderIter(model.root, filter_=lambda node: node.is_leaf):
|
|
991
|
+
node_branch_ids = [node_i.id for node_i in data_i.path]
|
|
992
|
+
node_branch_names = [node_i.name for node_i in data_i.path]
|
|
993
|
+
id_pairs += [[node_branch_ids[i],node_branch_ids[i+1]] for i in range(len(node_branch_ids)-1)]
|
|
994
|
+
name_pairs += [[node_branch_names[i],node_branch_names[i+1]] for i in range(len(node_branch_names)-1)]
|
|
995
|
+
node_pairs = {tuple(name_pair): id_pair for (name_pair,id_pair) in zip(name_pairs,id_pairs)}
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
# Draw lines
|
|
999
|
+
for (x,y) in zip(X,Y):
|
|
1000
|
+
if not np.isnan(x).all():
|
|
1001
|
+
for names, ids in node_pairs.items():
|
|
1002
|
+
if not None in ids and not (np.isnan(x[ids[0]]) or np.isnan(y[ids[0]]) or np.isnan(x[ids[1]]) or np.isnan(y[ids[1]])):
|
|
1003
|
+
if any(n.startswith('R') for n in names) and not any(n.startswith('L') for n in names):
|
|
1004
|
+
c = (255,128,0)
|
|
1005
|
+
elif any(n.startswith('L') for n in names) and not any(n.startswith('R') for n in names):
|
|
1006
|
+
c = (0,255,0)
|
|
1007
|
+
else:
|
|
1008
|
+
c = (51, 153, 255)
|
|
1009
|
+
cv2.line(img, (int(x[ids[0]]), int(y[ids[0]])), (int(x[ids[1]]), int(y[ids[1]])), c, thickness)
|
|
1010
|
+
|
|
1011
|
+
return img
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
def draw_keypts(img, X, Y, scores, cmap_str='RdYlGn'):
|
|
1015
|
+
'''
|
|
1016
|
+
Draws keypoints and skeleton for each person.
|
|
1017
|
+
Keypoints' colors depend on their score.
|
|
1018
|
+
|
|
1019
|
+
INPUTS:
|
|
1020
|
+
- img: opencv image
|
|
1021
|
+
- X: list of list of x coordinates
|
|
1022
|
+
- Y: list of list of y coordinates
|
|
1023
|
+
- scores: list of list of scores
|
|
1024
|
+
- cmap_str: colormap name
|
|
1025
|
+
|
|
1026
|
+
OUTPUT:
|
|
1027
|
+
- img: image with keypoints and skeleton
|
|
1028
|
+
'''
|
|
1029
|
+
|
|
1030
|
+
scores = np.where(np.isnan(scores), 0, scores)
|
|
1031
|
+
# scores = (scores - 0.4) / (1-0.4) # to get a red color for scores lower than 0.4
|
|
1032
|
+
scores = np.where(scores>0.99, 0.99, scores)
|
|
1033
|
+
scores = np.where(scores<0, 0, scores)
|
|
1034
|
+
|
|
1035
|
+
cmap = plt.get_cmap(cmap_str)
|
|
1036
|
+
for (x,y,s) in zip(X,Y,scores):
|
|
1037
|
+
c_k = np.array(cmap(s))[:,:-1]*255
|
|
1038
|
+
[cv2.circle(img, (int(x[i]), int(y[i])), thickness+4, c_k[i][::-1], -1)
|
|
1039
|
+
for i in range(len(x))
|
|
1040
|
+
if not (np.isnan(x[i]) or np.isnan(y[i]))]
|
|
1041
|
+
|
|
1042
|
+
return img
|
|
@@ -60,7 +60,7 @@ from functools import partial
|
|
|
60
60
|
from datetime import datetime
|
|
61
61
|
import itertools as it
|
|
62
62
|
from tqdm import tqdm
|
|
63
|
-
from anytree import RenderTree
|
|
63
|
+
from anytree import RenderTree
|
|
64
64
|
|
|
65
65
|
import numpy as np
|
|
66
66
|
import pandas as pd
|
|
@@ -68,6 +68,7 @@ import cv2
|
|
|
68
68
|
import matplotlib as mpl
|
|
69
69
|
import matplotlib.pyplot as plt
|
|
70
70
|
from rtmlib import PoseTracker, BodyWithFeet, Wholebody, Body, Custom
|
|
71
|
+
from deep_sort_realtime.deepsort_tracker import DeepSort
|
|
71
72
|
|
|
72
73
|
from Sports2D.Utilities import filter
|
|
73
74
|
from Sports2D.Utilities.common import *
|
|
@@ -337,161 +338,6 @@ def compute_angle(ang_name, person_X_flipped, person_Y, angle_dict, keypoints_id
|
|
|
337
338
|
return ang
|
|
338
339
|
|
|
339
340
|
|
|
340
|
-
def min_with_single_indices(L, T):
|
|
341
|
-
'''
|
|
342
|
-
Let L be a list (size s) with T associated tuple indices (size s).
|
|
343
|
-
Select the smallest values of L, considering that
|
|
344
|
-
the next smallest value cannot have the same numbers
|
|
345
|
-
in the associated tuple as any of the previous ones.
|
|
346
|
-
|
|
347
|
-
Example:
|
|
348
|
-
L = [ 20, 27, 51, 33, 43, 23, 37, 24, 4, 68, 84, 3 ]
|
|
349
|
-
T = list(it.product(range(2),range(3)))
|
|
350
|
-
= [(0,0),(0,1),(0,2),(0,3),(1,0),(1,1),(1,2),(1,3),(2,0),(2,1),(2,2),(2,3)]
|
|
351
|
-
|
|
352
|
-
- 1st smallest value: 3 with tuple (2,3), index 11
|
|
353
|
-
- 2nd smallest value when excluding indices (2,.) and (.,3), i.e. [(0,0),(0,1),(0,2),X,(1,0),(1,1),(1,2),X,X,X,X,X]:
|
|
354
|
-
20 with tuple (0,0), index 0
|
|
355
|
-
- 3rd smallest value when excluding [X,X,X,X,X,(1,1),(1,2),X,X,X,X,X]:
|
|
356
|
-
23 with tuple (1,1), index 5
|
|
357
|
-
|
|
358
|
-
INPUTS:
|
|
359
|
-
- L: list (size s)
|
|
360
|
-
- T: T associated tuple indices (size s)
|
|
361
|
-
|
|
362
|
-
OUTPUTS:
|
|
363
|
-
- minL: list of smallest values of L, considering constraints on tuple indices
|
|
364
|
-
- argminL: list of indices of smallest values of L (indices of best combinations)
|
|
365
|
-
- T_minL: list of tuples associated with smallest values of L
|
|
366
|
-
'''
|
|
367
|
-
|
|
368
|
-
minL = [np.nanmin(L)]
|
|
369
|
-
argminL = [np.nanargmin(L)]
|
|
370
|
-
T_minL = [T[argminL[0]]]
|
|
371
|
-
|
|
372
|
-
mask_tokeep = np.array([True for t in T])
|
|
373
|
-
i=0
|
|
374
|
-
while mask_tokeep.any()==True:
|
|
375
|
-
mask_tokeep = mask_tokeep & np.array([t[0]!=T_minL[i][0] and t[1]!=T_minL[i][1] for t in T])
|
|
376
|
-
if mask_tokeep.any()==True:
|
|
377
|
-
indicesL_tokeep = np.where(mask_tokeep)[0]
|
|
378
|
-
minL += [np.nanmin(np.array(L)[indicesL_tokeep]) if not np.isnan(np.array(L)[indicesL_tokeep]).all() else np.nan]
|
|
379
|
-
argminL += [indicesL_tokeep[np.nanargmin(np.array(L)[indicesL_tokeep])] if not np.isnan(minL[-1]) else indicesL_tokeep[0]]
|
|
380
|
-
T_minL += (T[argminL[i+1]],)
|
|
381
|
-
i+=1
|
|
382
|
-
|
|
383
|
-
return np.array(minL), np.array(argminL), np.array(T_minL)
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
def pad_shape(arr, target_len, fill_value=np.nan):
|
|
387
|
-
'''
|
|
388
|
-
Pads an array to the target length with specified fill values
|
|
389
|
-
|
|
390
|
-
INPUTS:
|
|
391
|
-
- arr: Input array to be padded.
|
|
392
|
-
- target_len: The target length of the first dimension after padding.
|
|
393
|
-
- fill_value: The value to use for padding (default: np.nan).
|
|
394
|
-
|
|
395
|
-
OUTPUTS:
|
|
396
|
-
- Padded array with shape (target_len, ...) matching the input dimensions.
|
|
397
|
-
'''
|
|
398
|
-
|
|
399
|
-
if len(arr) < target_len:
|
|
400
|
-
pad_shape = (target_len - len(arr),) + arr.shape[1:]
|
|
401
|
-
padding = np.full(pad_shape, fill_value)
|
|
402
|
-
return np.concatenate((arr, padding))
|
|
403
|
-
|
|
404
|
-
return arr
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
def sort_people_sports2d(keyptpre, keypt, scores=None):
|
|
408
|
-
'''
|
|
409
|
-
Associate persons across frames (Sports2D method)
|
|
410
|
-
Persons' indices are sometimes swapped when changing frame
|
|
411
|
-
A person is associated to another in the next frame when they are at a small distance
|
|
412
|
-
|
|
413
|
-
N.B.: Requires min_with_single_indices and euclidian_distance function (see common.py)
|
|
414
|
-
|
|
415
|
-
INPUTS:
|
|
416
|
-
- keyptpre: (K, L, M) array of 2D coordinates for K persons in the previous frame, L keypoints, M 2D coordinates
|
|
417
|
-
- keypt: idem keyptpre, for current frame
|
|
418
|
-
- score: (K, L) array of confidence scores for K persons, L keypoints (optional)
|
|
419
|
-
|
|
420
|
-
OUTPUTS:
|
|
421
|
-
- sorted_prev_keypoints: array with reordered persons with values of previous frame if current is empty
|
|
422
|
-
- sorted_keypoints: array with reordered persons --> if scores is not None
|
|
423
|
-
- sorted_scores: array with reordered scores --> if scores is not None
|
|
424
|
-
- associated_tuples: list of tuples with correspondences between persons across frames --> if scores is None (for Pose2Sim.triangulation())
|
|
425
|
-
'''
|
|
426
|
-
|
|
427
|
-
# Generate possible person correspondences across frames
|
|
428
|
-
max_len = max(len(keyptpre), len(keypt))
|
|
429
|
-
keyptpre = pad_shape(keyptpre, max_len, fill_value=np.nan)
|
|
430
|
-
keypt = pad_shape(keypt, max_len, fill_value=np.nan)
|
|
431
|
-
if scores is not None:
|
|
432
|
-
scores = pad_shape(scores, max_len, fill_value=np.nan)
|
|
433
|
-
|
|
434
|
-
# Compute distance between persons from one frame to another
|
|
435
|
-
personsIDs_comb = sorted(list(it.product(range(len(keyptpre)), range(len(keypt)))))
|
|
436
|
-
frame_by_frame_dist = [euclidean_distance(keyptpre[comb[0]],keypt[comb[1]]) for comb in personsIDs_comb]
|
|
437
|
-
frame_by_frame_dist = np.mean(frame_by_frame_dist, axis=1)
|
|
438
|
-
|
|
439
|
-
# Sort correspondences by distance
|
|
440
|
-
_, _, associated_tuples = min_with_single_indices(frame_by_frame_dist, personsIDs_comb)
|
|
441
|
-
|
|
442
|
-
# Associate points to same index across frames, nan if no correspondence
|
|
443
|
-
sorted_keypoints = []
|
|
444
|
-
for i in range(len(keyptpre)):
|
|
445
|
-
id_in_old = associated_tuples[:,1][associated_tuples[:,0] == i].tolist()
|
|
446
|
-
if len(id_in_old) > 0: sorted_keypoints += [keypt[id_in_old[0]]]
|
|
447
|
-
else: sorted_keypoints += [keypt[i]]
|
|
448
|
-
sorted_keypoints = np.array(sorted_keypoints)
|
|
449
|
-
|
|
450
|
-
if scores is not None:
|
|
451
|
-
sorted_scores = []
|
|
452
|
-
for i in range(len(keyptpre)):
|
|
453
|
-
id_in_old = associated_tuples[:,1][associated_tuples[:,0] == i].tolist()
|
|
454
|
-
if len(id_in_old) > 0: sorted_scores += [scores[id_in_old[0]]]
|
|
455
|
-
else: sorted_scores += [scores[i]]
|
|
456
|
-
sorted_scores = np.array(sorted_scores)
|
|
457
|
-
|
|
458
|
-
# Keep track of previous values even when missing for more than one frame
|
|
459
|
-
sorted_prev_keypoints = np.where(np.isnan(sorted_keypoints) & ~np.isnan(keyptpre), keyptpre, sorted_keypoints)
|
|
460
|
-
|
|
461
|
-
if scores is not None:
|
|
462
|
-
return sorted_prev_keypoints, sorted_keypoints, sorted_scores
|
|
463
|
-
else: # For Pose2Sim.triangulation()
|
|
464
|
-
return sorted_keypoints, associated_tuples
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
def sort_people_rtmlib(pose_tracker, keypoints, scores):
|
|
468
|
-
'''
|
|
469
|
-
Associate persons across frames (RTMLib method)
|
|
470
|
-
|
|
471
|
-
INPUTS:
|
|
472
|
-
- pose_tracker: PoseTracker. The initialized RTMLib pose tracker object
|
|
473
|
-
- keypoints: array of shape K, L, M with K the number of detected persons,
|
|
474
|
-
L the number of detected keypoints, M their 2D coordinates
|
|
475
|
-
- scores: array of shape K, L with K the number of detected persons,
|
|
476
|
-
L the confidence of detected keypoints
|
|
477
|
-
|
|
478
|
-
OUTPUT:
|
|
479
|
-
- sorted_keypoints: array with reordered persons
|
|
480
|
-
- sorted_scores: array with reordered scores
|
|
481
|
-
'''
|
|
482
|
-
|
|
483
|
-
try:
|
|
484
|
-
desired_size = max(pose_tracker.track_ids_last_frame)+1
|
|
485
|
-
sorted_keypoints = np.full((desired_size, keypoints.shape[1], 2), np.nan)
|
|
486
|
-
sorted_keypoints[pose_tracker.track_ids_last_frame] = keypoints[:len(pose_tracker.track_ids_last_frame), :, :]
|
|
487
|
-
sorted_scores = np.full((desired_size, scores.shape[1]), np.nan)
|
|
488
|
-
sorted_scores[pose_tracker.track_ids_last_frame] = scores[:len(pose_tracker.track_ids_last_frame), :]
|
|
489
|
-
except:
|
|
490
|
-
sorted_keypoints, sorted_scores = keypoints, scores
|
|
491
|
-
|
|
492
|
-
return sorted_keypoints, sorted_scores
|
|
493
|
-
|
|
494
|
-
|
|
495
341
|
def draw_dotted_line(img, start, direction, length, color=(0, 255, 0), gap=7, dot_length=3, thickness=thickness):
|
|
496
342
|
'''
|
|
497
343
|
Draw a dotted line with on a cv2 image
|
|
@@ -516,109 +362,6 @@ def draw_dotted_line(img, start, direction, length, color=(0, 255, 0), gap=7, do
|
|
|
516
362
|
cv2.line(img, tuple(line_start.astype(int)), tuple(line_end.astype(int)), color, thickness)
|
|
517
363
|
|
|
518
364
|
|
|
519
|
-
def draw_bounding_box(img, X, Y, colors=[(255, 0, 0), (0, 255, 0), (0, 0, 255)], fontSize=0.3, thickness=1):
|
|
520
|
-
'''
|
|
521
|
-
Draw bounding boxes and person ID around list of lists of X and Y coordinates.
|
|
522
|
-
Bounding boxes have a different color for each person.
|
|
523
|
-
|
|
524
|
-
INPUTS:
|
|
525
|
-
- img: opencv image
|
|
526
|
-
- X: list of list of x coordinates
|
|
527
|
-
- Y: list of list of y coordinates
|
|
528
|
-
- colors: list of colors to cycle through
|
|
529
|
-
|
|
530
|
-
OUTPUT:
|
|
531
|
-
- img: image with rectangles and person IDs
|
|
532
|
-
'''
|
|
533
|
-
|
|
534
|
-
color_cycle = it.cycle(colors)
|
|
535
|
-
|
|
536
|
-
for i,(x,y) in enumerate(zip(X,Y)):
|
|
537
|
-
color = next(color_cycle)
|
|
538
|
-
if not np.isnan(x).all():
|
|
539
|
-
x_min, y_min = np.nanmin(x).astype(int), np.nanmin(y).astype(int)
|
|
540
|
-
x_max, y_max = np.nanmax(x).astype(int), np.nanmax(y).astype(int)
|
|
541
|
-
if x_min < 0: x_min = 0
|
|
542
|
-
if x_max > img.shape[1]: x_max = img.shape[1]
|
|
543
|
-
if y_min < 0: y_min = 0
|
|
544
|
-
if y_max > img.shape[0]: y_max = img.shape[0]
|
|
545
|
-
|
|
546
|
-
# Draw rectangles
|
|
547
|
-
cv2.rectangle(img, (x_min-25, y_min-25), (x_max+25, y_max+25), color, thickness)
|
|
548
|
-
|
|
549
|
-
# Write person ID
|
|
550
|
-
cv2.putText(img, str(i), (x_min-30, y_min-30), cv2.FONT_HERSHEY_SIMPLEX, fontSize+1, color, 2, cv2.LINE_AA)
|
|
551
|
-
|
|
552
|
-
return img
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
def draw_skel(img, X, Y, model, colors=[(255, 0, 0), (0, 255, 0), (0, 0, 255)]):
|
|
556
|
-
'''
|
|
557
|
-
Draws keypoints and skeleton for each person.
|
|
558
|
-
Skeletons have a different color for each person.
|
|
559
|
-
|
|
560
|
-
INPUTS:
|
|
561
|
-
- img: opencv image
|
|
562
|
-
- X: list of list of x coordinates
|
|
563
|
-
- Y: list of list of y coordinates
|
|
564
|
-
- model: skeleton model (from skeletons.py)
|
|
565
|
-
- colors: list of colors to cycle through
|
|
566
|
-
|
|
567
|
-
OUTPUT:
|
|
568
|
-
- img: image with keypoints and skeleton
|
|
569
|
-
'''
|
|
570
|
-
|
|
571
|
-
# Get (unique) pairs between which to draw a line
|
|
572
|
-
node_pairs = []
|
|
573
|
-
for data_i in PreOrderIter(model.root, filter_=lambda node: node.is_leaf):
|
|
574
|
-
node_branches = [node_i.id for node_i in data_i.path]
|
|
575
|
-
node_pairs += [[node_branches[i],node_branches[i+1]] for i in range(len(node_branches)-1)]
|
|
576
|
-
node_pairs = [list(x) for x in set(tuple(x) for x in node_pairs)]
|
|
577
|
-
|
|
578
|
-
# Draw lines
|
|
579
|
-
color_cycle = it.cycle(colors)
|
|
580
|
-
for (x,y) in zip(X,Y):
|
|
581
|
-
c = next(color_cycle)
|
|
582
|
-
if not np.isnan(x).all():
|
|
583
|
-
[cv2.line(img,
|
|
584
|
-
(int(x[n[0]]), int(y[n[0]])), (int(x[n[1]]), int(y[n[1]])), c, thickness)
|
|
585
|
-
for n in node_pairs
|
|
586
|
-
if not None in n and not (np.isnan(x[n[0]]) or np.isnan(y[n[0]]) or np.isnan(x[n[1]]) or np.isnan(y[n[1]]))] # IF NOT NONE
|
|
587
|
-
|
|
588
|
-
return img
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
def draw_keypts(img, X, Y, scores, cmap_str='RdYlGn'):
|
|
592
|
-
'''
|
|
593
|
-
Draws keypoints and skeleton for each person.
|
|
594
|
-
Keypoints' colors depend on their score.
|
|
595
|
-
|
|
596
|
-
INPUTS:
|
|
597
|
-
- img: opencv image
|
|
598
|
-
- X: list of list of x coordinates
|
|
599
|
-
- Y: list of list of y coordinates
|
|
600
|
-
- scores: list of list of scores
|
|
601
|
-
- cmap_str: colormap name
|
|
602
|
-
|
|
603
|
-
OUTPUT:
|
|
604
|
-
- img: image with keypoints and skeleton
|
|
605
|
-
'''
|
|
606
|
-
|
|
607
|
-
scores = np.where(np.isnan(scores), 0, scores)
|
|
608
|
-
# scores = (scores - 0.4) / (1-0.4) # to get a red color for scores lower than 0.4
|
|
609
|
-
scores = np.where(scores>0.99, 0.99, scores)
|
|
610
|
-
scores = np.where(scores<0, 0, scores)
|
|
611
|
-
|
|
612
|
-
cmap = plt.get_cmap(cmap_str)
|
|
613
|
-
for (x,y,s) in zip(X,Y,scores):
|
|
614
|
-
c_k = np.array(cmap(s))[:,:-1]*255
|
|
615
|
-
[cv2.circle(img, (int(x[i]), int(y[i])), thickness+4, c_k[i][::-1], -1)
|
|
616
|
-
for i in range(len(x))
|
|
617
|
-
if not (np.isnan(x[i]) or np.isnan(y[i]))]
|
|
618
|
-
|
|
619
|
-
return img
|
|
620
|
-
|
|
621
|
-
|
|
622
365
|
def draw_angles(img, valid_X, valid_Y, valid_angles, valid_X_flipped, keypoints_ids, keypoints_names, angle_names, display_angle_values_on= ['body', 'list'], colors=[(255, 0, 0), (0, 255, 0), (0, 0, 255)], fontSize=0.3, thickness=1):
|
|
623
366
|
'''
|
|
624
367
|
Draw angles on the image.
|
|
@@ -1184,6 +927,16 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
1184
927
|
mode = config_dict.get('pose').get('mode')
|
|
1185
928
|
det_frequency = config_dict.get('pose').get('det_frequency')
|
|
1186
929
|
tracking_mode = config_dict.get('pose').get('tracking_mode')
|
|
930
|
+
if tracking_mode == 'deepsort':
|
|
931
|
+
deepsort_params = config_dict.get('pose').get('deepsort_params')
|
|
932
|
+
try:
|
|
933
|
+
deepsort_params = ast.literal_eval(deepsort_params)
|
|
934
|
+
except: # if within single quotes instead of double quotes when run with sports2d --mode """{dictionary}"""
|
|
935
|
+
deepsort_params = deepsort_params.strip("'").replace('\n', '').replace(" ", "").replace(",", '", "').replace(":", '":"').replace("{", '{"').replace("}", '"}').replace('":"/',':/').replace('":"\\',':\\')
|
|
936
|
+
deepsort_params = re.sub(r'"\[([^"]+)",\s?"([^"]+)\]"', r'[\1,\2]', deepsort_params) # changes "[640", "640]" to [640,640]
|
|
937
|
+
deepsort_params = json.loads(deepsort_params)
|
|
938
|
+
deepsort_tracker = DeepSort(**deepsort_params)
|
|
939
|
+
deepsort_tracker.tracker.tracks.clear()
|
|
1187
940
|
backend = config_dict.get('pose').get('backend')
|
|
1188
941
|
device = config_dict.get('pose').get('device')
|
|
1189
942
|
|
|
@@ -1321,8 +1074,8 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
1321
1074
|
logging.warning("\nInvalid mode. Must be 'lightweight', 'balanced', 'performance', or '''{dictionary}''' of parameters within triple quotes. Make sure input_sizes are within square brackets.")
|
|
1322
1075
|
logging.warning('Using the default "balanced" mode.')
|
|
1323
1076
|
mode = 'balanced'
|
|
1324
|
-
|
|
1325
1077
|
|
|
1078
|
+
|
|
1326
1079
|
# Skip pose estimation or set it up:
|
|
1327
1080
|
if load_trc:
|
|
1328
1081
|
if not '_px' in str(load_trc):
|
|
@@ -1341,12 +1094,21 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
1341
1094
|
keypoints_ids = [node.id for _, _, node in RenderTree(pose_model) if node.id!=None]
|
|
1342
1095
|
keypoints_names = [node.name for _, _, node in RenderTree(pose_model) if node.id!=None]
|
|
1343
1096
|
|
|
1344
|
-
|
|
1345
|
-
|
|
1097
|
+
# Set up pose tracker
|
|
1098
|
+
try:
|
|
1099
|
+
pose_tracker = setup_pose_tracker(ModelClass, det_frequency, mode, False, backend, device)
|
|
1100
|
+
except:
|
|
1101
|
+
logging.error('Error: Pose estimation failed. Check in Config.toml that pose_model and mode are valid.')
|
|
1102
|
+
raise ValueError('Error: Pose estimation failed. Check in Config.toml that pose_model and mode are valid.')
|
|
1103
|
+
|
|
1104
|
+
if tracking_mode not in ['deepsort', 'sports2d']:
|
|
1105
|
+
logging.warning(f"Tracking mode {tracking_mode} not recognized. Using sports2d method.")
|
|
1106
|
+
tracking_mode = 'sports2d'
|
|
1346
1107
|
logging.info(f'\nPose tracking set up for "{pose_model_name}" model.')
|
|
1347
1108
|
logging.info(f'Mode: {mode}.\n')
|
|
1348
|
-
logging.info(f'Persons are detected every {det_frequency} frames and tracked inbetween. Multi-person is {"" if multiperson else "not "}selected.')
|
|
1349
|
-
logging.info(f
|
|
1109
|
+
logging.info(f'Persons are detected every {det_frequency} frames and tracked inbetween. Multi-person is {"" if multiperson else "not "}selected. Tracking is done with {tracking_mode}.')
|
|
1110
|
+
if tracking_mode == 'deepsort': logging.info(f'Deepsort parameters: {deepsort_params}.')
|
|
1111
|
+
logging.info(f"{keypoint_likelihood_threshold=}, {average_likelihood_threshold=}, {keypoint_number_threshold=}")
|
|
1350
1112
|
|
|
1351
1113
|
if flip_left_right:
|
|
1352
1114
|
try:
|
|
@@ -1383,22 +1145,22 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
1383
1145
|
for frame_nb in frame_iterator:
|
|
1384
1146
|
start_time = datetime.now()
|
|
1385
1147
|
success, frame = cap.read()
|
|
1148
|
+
frame_count += 1
|
|
1386
1149
|
|
|
1387
1150
|
# If frame not grabbed
|
|
1388
1151
|
if not success:
|
|
1389
|
-
logging.warning(f"Failed to grab frame {frame_count}.")
|
|
1152
|
+
logging.warning(f"Failed to grab frame {frame_count-1}.")
|
|
1390
1153
|
if save_pose:
|
|
1391
1154
|
all_frames_X.append([])
|
|
1392
1155
|
all_frames_Y.append([])
|
|
1393
1156
|
all_frames_scores.append([])
|
|
1394
1157
|
if save_angles:
|
|
1395
1158
|
all_frames_angles.append([])
|
|
1396
|
-
frame_count += 1
|
|
1397
1159
|
continue
|
|
1398
1160
|
else:
|
|
1399
1161
|
cv2.putText(frame, f"Press 'q' to quit", (cam_width-int(400*fontSize), cam_height-20), cv2.FONT_HERSHEY_SIMPLEX, fontSize+0.2, (255,255,255), thickness+1, cv2.LINE_AA)
|
|
1400
1162
|
cv2.putText(frame, f"Press 'q' to quit", (cam_width-int(400*fontSize), cam_height-20), cv2.FONT_HERSHEY_SIMPLEX, fontSize+0.2, (0,0,255), thickness, cv2.LINE_AA)
|
|
1401
|
-
|
|
1163
|
+
|
|
1402
1164
|
|
|
1403
1165
|
# Retrieve pose or Estimate pose and track people
|
|
1404
1166
|
if load_trc:
|
|
@@ -1409,13 +1171,14 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
1409
1171
|
else:
|
|
1410
1172
|
# Detect poses
|
|
1411
1173
|
keypoints, scores = pose_tracker(frame)
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1174
|
+
|
|
1175
|
+
# Track poses across frames
|
|
1176
|
+
if tracking_mode == 'deepsort':
|
|
1177
|
+
keypoints, scores = sort_people_deepsort(keypoints, scores, deepsort_tracker, frame, frame_count)
|
|
1178
|
+
if tracking_mode == 'sports2d':
|
|
1416
1179
|
if 'prev_keypoints' not in locals(): prev_keypoints = keypoints
|
|
1417
1180
|
prev_keypoints, keypoints, scores = sort_people_sports2d(prev_keypoints, keypoints, scores=scores)
|
|
1418
|
-
|
|
1181
|
+
|
|
1419
1182
|
|
|
1420
1183
|
# Process coordinates and compute angles
|
|
1421
1184
|
valid_X, valid_Y, valid_scores = [], [], []
|
|
@@ -1478,7 +1241,7 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
1478
1241
|
img = frame.copy()
|
|
1479
1242
|
img = draw_bounding_box(img, valid_X, valid_Y, colors=colors, fontSize=fontSize, thickness=thickness)
|
|
1480
1243
|
img = draw_keypts(img, valid_X, valid_Y, valid_scores, cmap_str='RdYlGn')
|
|
1481
|
-
img = draw_skel(img, valid_X, valid_Y, pose_model
|
|
1244
|
+
img = draw_skel(img, valid_X, valid_Y, pose_model)
|
|
1482
1245
|
if calculate_angles:
|
|
1483
1246
|
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)
|
|
1484
1247
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[metadata]
|
|
2
2
|
name = sports2d
|
|
3
|
-
version = 0.6.
|
|
3
|
+
version = 0.6.2
|
|
4
4
|
author = David Pagnon
|
|
5
5
|
author_email = contact@david-pagnon.com
|
|
6
6
|
description = Detect pose and compute 2D joint angles from a video.
|
|
@@ -41,10 +41,11 @@ install_requires =
|
|
|
41
41
|
matplotlib
|
|
42
42
|
PyQt5
|
|
43
43
|
statsmodels
|
|
44
|
-
|
|
44
|
+
rtmlib
|
|
45
45
|
openvino
|
|
46
46
|
tqdm
|
|
47
47
|
imageio_ffmpeg
|
|
48
|
+
deep-sort-realtime
|
|
48
49
|
packages = find:
|
|
49
50
|
|
|
50
51
|
[options.entry_points]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: sports2d
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.2
|
|
4
4
|
Summary: Detect pose and compute 2D joint angles from a video.
|
|
5
5
|
Home-page: https://github.com/davidpagnon/Sports2D
|
|
6
6
|
Author: David Pagnon
|
|
@@ -33,10 +33,11 @@ Requires-Dist: opencv-python
|
|
|
33
33
|
Requires-Dist: matplotlib
|
|
34
34
|
Requires-Dist: PyQt5
|
|
35
35
|
Requires-Dist: statsmodels
|
|
36
|
-
Requires-Dist:
|
|
36
|
+
Requires-Dist: rtmlib
|
|
37
37
|
Requires-Dist: openvino
|
|
38
38
|
Requires-Dist: tqdm
|
|
39
39
|
Requires-Dist: imageio_ffmpeg
|
|
40
|
+
Requires-Dist: deep-sort-realtime
|
|
40
41
|
|
|
41
42
|
|
|
42
43
|
[](https://github.com/davidpagnon/sports2d/actions/workflows/continuous-integration.yml)
|
|
@@ -212,6 +213,9 @@ Note that it does not take distortions into account, and that it will be less ac
|
|
|
212
213
|
``` cmd
|
|
213
214
|
sports2d --multiperson false --pose_model Body --mode lightweight --det_frequency 50
|
|
214
215
|
```
|
|
216
|
+
``` cmd
|
|
217
|
+
sports2d --tracking_mode deepsort --deepsort_params """{'max_age':30, 'n_init':3, 'nms_max_overlap':0.8, 'max_cosine_distance':0.3, 'nn_budget':200, 'max_iou_distance':0.8, 'embedder_gpu': True}"""
|
|
218
|
+
```
|
|
215
219
|
<br>
|
|
216
220
|
|
|
217
221
|
#### Run with a toml configuration file:
|
|
@@ -249,6 +253,7 @@ Note that any detection and pose models can be used (first [deploy them with MMP
|
|
|
249
253
|
- Use `--det_frequency 50`: Will detect poses only every 50 frames, and track keypoints in between, which is faster.
|
|
250
254
|
- Use `--multiperson false`: Can be used if one single person is present in the video. Otherwise, persons' IDs may be mixed up.
|
|
251
255
|
- Use `--load_trc <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.
|
|
256
|
+
- 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.
|
|
252
257
|
|
|
253
258
|
<br>
|
|
254
259
|
|
|
@@ -369,7 +374,7 @@ sports2d --time_range 1.2 2.7 --ik true --person_orientation front none left
|
|
|
369
374
|
|
|
370
375
|
### All the parameters
|
|
371
376
|
|
|
372
|
-
|
|
377
|
+
For a full list of the available parameters, have a look at the [Config_Demo.toml](https://github.com/davidpagnon/Sports2D/blob/main/Sports2D/Demo/Config_demo.toml) file or type:
|
|
373
378
|
|
|
374
379
|
``` cmd
|
|
375
380
|
sports2d --help
|
|
@@ -414,7 +419,10 @@ sports2d --help
|
|
|
414
419
|
'osim_setup_path': ["", "path to OpenSim setup. '../OpenSim_setup' if not specified"],
|
|
415
420
|
'person_orientation': ["", "front, back, left, right, auto, or none. 'front none left' if not specified. If 'auto', will be either left or right depending on the direction of the motion."],
|
|
416
421
|
'close_to_zero_speed_m': ["","Sum for all keypoints: about 50 px/frame or 0.2 m/frame"],
|
|
417
|
-
'multiperson': ["", "multiperson involves tracking: will be faster if set to false. true if not specified"],
|
|
422
|
+
'multiperson': ["", "multiperson involves tracking: will be faster if set to false. true if not specified"],
|
|
423
|
+
'tracking_mode': ["", "sports2d or rtmlib. sports2d is generally much more accurate and comparable in speed. sports2d if not specified"],
|
|
424
|
+
'deepsort_params': ["", 'Deepsort tracking parameters: """{dictionary between 3 double quotes}""". \n\
|
|
425
|
+
More information there: https://github.com/levan92/deep_sort_realtime/blob/master/deep_sort_realtime/deepsort_tracker.py#L51'],
|
|
418
426
|
'input_size': ["", "width, height. 1280, 720 if not specified. Lower resolution will be faster but less precise"],
|
|
419
427
|
'keypoint_likelihood_threshold': ["", "detected keypoints are not retained if likelihood is below this threshold. 0.3 if not specified"],
|
|
420
428
|
'average_likelihood_threshold': ["", "detected persons are not retained if average keypoint likelihood is below this threshold. 0.5 if not specified"],
|
|
@@ -459,7 +467,7 @@ Sports2D:
|
|
|
459
467
|
|
|
460
468
|
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.
|
|
461
469
|
|
|
462
|
-
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.
|
|
470
|
+
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.
|
|
463
471
|
|
|
464
472
|
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.
|
|
465
473
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|