sports2d 0.6.1__py3-none-any.whl → 0.6.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- Sports2D/Demo/Config_demo.toml +40 -22
- Sports2D/Sports2D.py +39 -13
- Sports2D/Utilities/common.py +477 -20
- Sports2D/Utilities/skeletons.py +7 -8
- Sports2D/Utilities/tests.py +3 -3
- Sports2D/process.py +162 -326
- {sports2d-0.6.1.dist-info → sports2d-0.6.3.dist-info}/METADATA +306 -182
- sports2d-0.6.3.dist-info/RECORD +16 -0
- sports2d-0.6.1.dist-info/RECORD +0 -16
- {sports2d-0.6.1.dist-info → sports2d-0.6.3.dist-info}/LICENSE +0 -0
- {sports2d-0.6.1.dist-info → sports2d-0.6.3.dist-info}/WHEEL +0 -0
- {sports2d-0.6.1.dist-info → sports2d-0.6.3.dist-info}/entry_points.txt +0 -0
- {sports2d-0.6.1.dist-info → sports2d-0.6.3.dist-info}/top_level.txt +0 -0
Sports2D/Utilities/common.py
CHANGED
|
@@ -20,14 +20,18 @@ 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
|
|
27
29
|
from scipy import interpolate
|
|
28
30
|
import imageio_ffmpeg as ffmpeg
|
|
29
31
|
import cv2
|
|
32
|
+
import c3d
|
|
30
33
|
|
|
34
|
+
import matplotlib.pyplot as plt
|
|
31
35
|
from PyQt5.QtWidgets import QMainWindow, QApplication, QWidget, QTabWidget, QVBoxLayout
|
|
32
36
|
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
|
|
33
37
|
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
|
|
@@ -81,6 +85,32 @@ angle_dict = { # lowercase!
|
|
|
81
85
|
'left hand': [['LIndex', 'LWrist'], 'horizontal', 0, -1]
|
|
82
86
|
}
|
|
83
87
|
|
|
88
|
+
marker_Z_positions = {'right':
|
|
89
|
+
{"RHip": 0.105, "RKnee": 0.0886, "RAnkle": 0.0972, "RBigToe":0.0766, "RHeel":0.0883, "RSmallToe": 0.1200,
|
|
90
|
+
"RShoulder": 0.2016, "RElbow": 0.1613, "RWrist": 0.120, "RThumb": 0.1625, "RIndex": 0.1735, "RPinky": 0.1740, "REye": 0.0311,
|
|
91
|
+
"LHip": -0.105, "LKnee": -0.0886, "LAnkle": -0.0972, "LBigToe": -0.0766, "LHeel": -0.0883, "LSmallToe": -0.1200,
|
|
92
|
+
"LShoulder": -0.2016, "LElbow": -0.1613, "LWrist": -0.120, "LThumb": -0.1625, "LIndex": -0.1735, "LPinky": -0.1740, "LEye": -0.0311,
|
|
93
|
+
"Hip": 0.0, "Neck": 0.0, "Head":0.0, "Nose": 0.0},
|
|
94
|
+
'left':
|
|
95
|
+
{"RHip": -0.105, "RKnee": -0.0886, "RAnkle": -0.0972, "RBigToe": -0.0766, "RHeel": -0.0883, "RSmallToe": -0.1200,
|
|
96
|
+
"RShoulder": -0.2016, "RElbow": -0.1613, "RWrist": -0.120, "RThumb": -0.1625, "RIndex": -0.1735, "RPinky": -0.1740, "REye": -0.0311,
|
|
97
|
+
"LHip": 0.105, "LKnee": 0.0886, "LAnkle": 0.0972, "LBigToe":0.0766, "LHeel":0.0883, "LSmallToe": 0.1200,
|
|
98
|
+
"LShoulder": 0.2016, "LElbow": 0.1613, "LWrist": 0.120, "LThumb": 0.1625, "LIndex": 0.1735, "LPinky": 0.1740, "LEye": 0.0311,
|
|
99
|
+
"Hip": 0.0, "Neck": 0.0, "Head":0.0, "Nose": 0.0},
|
|
100
|
+
'front':
|
|
101
|
+
{"RHip": 0.0301, "RKnee": 0.0179, "RAnkle": 0.0230, "RBigToe": 0.2179, "RHeel": -0.0119, "RSmallToe": 0.1804,
|
|
102
|
+
"RShoulder": -0.01275, "RElbow": 0.0119, "RWrist": 0.0002, "RThumb": 0.0106, "RIndex": -0.0004, "RPinky": -0.0009, "REye": 0.0702,
|
|
103
|
+
"LHip": -0.0301, "LKnee": -0.0179, "LAnkle": 0.0230, "LBigToe": 0.2179, "LHeel": -0.0119, "LSmallToe": 0.1804,
|
|
104
|
+
"LShoulder": 0.01275, "LElbow": -0.0119, "LWrist": -0.0002, "LThumb": -0.0106, "LIndex": 0.0004, "LPinky": 0.0009, "LEye": -0.0702,
|
|
105
|
+
"Hip": 0.0301, "Neck": -0.0008, "Head": 0.0655, "Nose": 0.1076},
|
|
106
|
+
'back':
|
|
107
|
+
{"RHip": -0.0301, "RKnee": -0.0179, "RAnkle": -0.0230, "RBigToe": -0.2179, "RHeel": 0.0119, "RSmallToe": -0.1804,
|
|
108
|
+
"RShoulder": 0.01275, "RElbow": -0.0119, "RWrist": -0.0002, "RThumb": -0.0106, "RIndex": 0.0004, "RPinky": 0.0009, "REye": -0.0702,
|
|
109
|
+
"LHip": 0.0301, "LKnee": 0.0179, "LAnkle": -0.0230, "LBigToe": -0.2179, "LHeel": 0.0119, "LSmallToe": -0.1804,
|
|
110
|
+
"LShoulder": -0.01275, "LElbow": 0.0119, "LWrist": 0.0002, "LThumb": 0.0106, "LIndex": -0.0004, "LPinky": -0.0009, "LEye": 0.0702,
|
|
111
|
+
"Hip": 0.0301, "Neck": -0.0008, "Head": -0.0655, "Nose": 0.1076},
|
|
112
|
+
}
|
|
113
|
+
|
|
84
114
|
colors = [(255, 0, 0), (0, 0, 255), (255, 255, 0), (255, 0, 255), (0, 255, 255), (0, 0, 0), (255, 255, 255),
|
|
85
115
|
(125, 0, 0), (0, 125, 0), (0, 0, 125), (125, 125, 0), (125, 0, 125), (0, 125, 125),
|
|
86
116
|
(255, 125, 125), (125, 255, 125), (125, 125, 255), (255, 255, 125), (255, 125, 255), (125, 255, 255), (125, 125, 125),
|
|
@@ -169,6 +199,85 @@ def read_trc(trc_path):
|
|
|
169
199
|
raise ValueError(f"Error reading TRC file at {trc_path}: {e}")
|
|
170
200
|
|
|
171
201
|
|
|
202
|
+
def extract_trc_data(trc_path):
|
|
203
|
+
'''
|
|
204
|
+
Extract marker names and coordinates from a trc file.
|
|
205
|
+
|
|
206
|
+
INPUTS:
|
|
207
|
+
- trc_path: Path to the trc file
|
|
208
|
+
|
|
209
|
+
OUTPUTS:
|
|
210
|
+
- marker_names: List of marker names
|
|
211
|
+
- marker_coords: Array of marker coordinates (n_frames, t+3*n_markers)
|
|
212
|
+
'''
|
|
213
|
+
|
|
214
|
+
# marker names
|
|
215
|
+
with open(trc_path, 'r') as file:
|
|
216
|
+
lines = file.readlines()
|
|
217
|
+
marker_names_line = lines[3]
|
|
218
|
+
marker_names = marker_names_line.strip().split('\t')[2::3]
|
|
219
|
+
|
|
220
|
+
# time and marker coordinates
|
|
221
|
+
trc_data_np = np.genfromtxt(trc_path, skip_header=5, delimiter = '\t')[:,1:]
|
|
222
|
+
|
|
223
|
+
return marker_names, trc_data_np
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def create_c3d_file(c3d_path, marker_names, trc_data_np):
|
|
227
|
+
'''
|
|
228
|
+
Create a c3d file from the data extracted from a trc file.
|
|
229
|
+
|
|
230
|
+
INPUTS:
|
|
231
|
+
- c3d_path: Path to the c3d file
|
|
232
|
+
- marker_names: List of marker names
|
|
233
|
+
- trc_data_np: Array of marker coordinates (n_frames, t+3*n_markers)
|
|
234
|
+
|
|
235
|
+
OUTPUTS:
|
|
236
|
+
- c3d file
|
|
237
|
+
'''
|
|
238
|
+
|
|
239
|
+
# retrieve frame rate
|
|
240
|
+
times = trc_data_np[:,0]
|
|
241
|
+
frame_rate = round((len(times)-1) / (times[-1] - times[0]))
|
|
242
|
+
|
|
243
|
+
# write c3d file
|
|
244
|
+
writer = c3d.Writer(point_rate=frame_rate, analog_rate=0, point_scale=1.0, point_units='mm', gen_scale=-1.0)
|
|
245
|
+
writer.set_point_labels(marker_names)
|
|
246
|
+
writer.set_screen_axis(X='+Z', Y='+Y')
|
|
247
|
+
|
|
248
|
+
for frame in trc_data_np:
|
|
249
|
+
residuals = np.full((len(marker_names), 1), 0.0)
|
|
250
|
+
cameras = np.zeros((len(marker_names), 1))
|
|
251
|
+
coords = frame[1:].reshape(-1,3)*1000
|
|
252
|
+
points = np.hstack((coords, residuals, cameras))
|
|
253
|
+
writer.add_frames([(points, np.array([]))])
|
|
254
|
+
|
|
255
|
+
writer.set_start_frame(0)
|
|
256
|
+
writer._set_last_frame(len(trc_data_np)-1)
|
|
257
|
+
|
|
258
|
+
with open(c3d_path, 'wb') as handle:
|
|
259
|
+
writer.write(handle)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def convert_to_c3d(trc_path):
|
|
263
|
+
'''
|
|
264
|
+
Make Visual3D compatible c3d files from a trc path
|
|
265
|
+
|
|
266
|
+
INPUT:
|
|
267
|
+
- trc_path: string, trc file to convert
|
|
268
|
+
|
|
269
|
+
OUTPUT:
|
|
270
|
+
- c3d file
|
|
271
|
+
'''
|
|
272
|
+
|
|
273
|
+
trc_path = str(trc_path)
|
|
274
|
+
c3d_path = trc_path.replace('.trc', '.c3d')
|
|
275
|
+
marker_names, trc_data_np = extract_trc_data(trc_path)
|
|
276
|
+
create_c3d_file(c3d_path, marker_names, trc_data_np)
|
|
277
|
+
|
|
278
|
+
return c3d_path
|
|
279
|
+
|
|
280
|
+
|
|
172
281
|
def interpolate_zeros_nans(col, *args):
|
|
173
282
|
'''
|
|
174
283
|
Interpolate missing points (of value zero),
|
|
@@ -466,7 +575,7 @@ def add_neck_hip_coords(kpt_name, p_X, p_Y, p_scores, kpt_ids, kpt_names):
|
|
|
466
575
|
return p_X, p_Y, p_scores
|
|
467
576
|
|
|
468
577
|
|
|
469
|
-
def best_coords_for_measurements(
|
|
578
|
+
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
579
|
'''
|
|
471
580
|
Compute the best coordinates for measurements, after removing:
|
|
472
581
|
- 20% fastest frames (may be outliers)
|
|
@@ -474,7 +583,7 @@ def best_coords_for_measurements(trc_data, keypoints_names, fastest_frames_to_re
|
|
|
474
583
|
- frames when hip and knee angle below 45° (imprecise coordinates when person is crouching)
|
|
475
584
|
|
|
476
585
|
INPUTS:
|
|
477
|
-
-
|
|
586
|
+
- Q_coords: pd.DataFrame. The XYZ coordinates of each marker
|
|
478
587
|
- keypoints_names: list. The list of marker names
|
|
479
588
|
- fastest_frames_to_remove_percent: float
|
|
480
589
|
- close_to_zero_speed: float (sum for all keypoints: about 50 px/frame or 0.2 m/frame)
|
|
@@ -482,44 +591,46 @@ def best_coords_for_measurements(trc_data, keypoints_names, fastest_frames_to_re
|
|
|
482
591
|
- trimmed_extrema_percent
|
|
483
592
|
|
|
484
593
|
OUTPUT:
|
|
485
|
-
-
|
|
594
|
+
- Q_coords_low_speeds_low_angles: pd.DataFrame. The best coordinates for measurements
|
|
486
595
|
'''
|
|
487
596
|
|
|
488
597
|
# Add MidShoulder column
|
|
489
|
-
df_MidShoulder = pd.DataFrame((
|
|
598
|
+
df_MidShoulder = pd.DataFrame((Q_coords['RShoulder'].values + Q_coords['LShoulder'].values) /2)
|
|
490
599
|
df_MidShoulder.columns = ['MidShoulder']*3
|
|
491
|
-
|
|
600
|
+
Q_coords = pd.concat((Q_coords.reset_index(drop=True), df_MidShoulder), axis=1)
|
|
492
601
|
|
|
493
602
|
# Add Hip column if not present
|
|
494
603
|
n_markers_init = len(keypoints_names)
|
|
495
604
|
if 'Hip' not in keypoints_names:
|
|
496
|
-
df_Hip = pd.DataFrame((
|
|
605
|
+
df_Hip = pd.DataFrame((Q_coords['RHip'].values + Q_coords['LHip'].values) /2)
|
|
497
606
|
df_Hip.columns = ['Hip']*3
|
|
498
|
-
|
|
607
|
+
Q_coords = pd.concat((Q_coords.reset_index(drop=True), df_Hip), axis=1)
|
|
499
608
|
n_markers = len(keypoints_names)
|
|
500
609
|
|
|
501
610
|
# Using 80% slowest frames
|
|
502
|
-
sum_speeds = pd.Series(np.nansum([np.linalg.norm(
|
|
611
|
+
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
612
|
sum_speeds = sum_speeds[sum_speeds>close_to_zero_speed] # Removing when speeds close to zero (out of frame)
|
|
504
613
|
if len(sum_speeds)==0:
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
614
|
+
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.')
|
|
615
|
+
Q_coords_low_speeds = Q_coords
|
|
616
|
+
else:
|
|
617
|
+
min_speed_indices = sum_speeds.abs().nsmallest(int(len(sum_speeds) * (1-fastest_frames_to_remove_percent))).index
|
|
618
|
+
Q_coords_low_speeds = Q_coords.iloc[min_speed_indices].reset_index(drop=True)
|
|
508
619
|
|
|
509
620
|
# Only keep frames with hip and knee flexion angles below 45%
|
|
510
621
|
# (if more than 50 of them, else take 50 smallest values)
|
|
511
622
|
try:
|
|
512
|
-
ang_mean = mean_angles(
|
|
513
|
-
|
|
514
|
-
if len(
|
|
515
|
-
|
|
623
|
+
ang_mean = mean_angles(Q_coords_low_speeds, ang_to_consider = ['right knee', 'left knee', 'right hip', 'left hip'])
|
|
624
|
+
Q_coords_low_speeds_low_angles = Q_coords_low_speeds[ang_mean < large_hip_knee_angles]
|
|
625
|
+
if len(Q_coords_low_speeds_low_angles) < 50:
|
|
626
|
+
Q_coords_low_speeds_low_angles = Q_coords_low_speeds.iloc[pd.Series(ang_mean).nsmallest(50).index]
|
|
516
627
|
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
|
|
628
|
+
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
629
|
|
|
519
630
|
if n_markers_init < n_markers:
|
|
520
|
-
|
|
631
|
+
Q_coords_low_speeds_low_angles = Q_coords_low_speeds_low_angles.iloc[:,:-3]
|
|
521
632
|
|
|
522
|
-
return
|
|
633
|
+
return Q_coords_low_speeds_low_angles
|
|
523
634
|
|
|
524
635
|
|
|
525
636
|
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 +658,7 @@ def compute_height(trc_data, keypoints_names, fastest_frames_to_remove_percent=0
|
|
|
547
658
|
try:
|
|
548
659
|
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
660
|
except:
|
|
550
|
-
rfoot, lfoot = 10, 10
|
|
661
|
+
rfoot, lfoot = 0.10, 0.10
|
|
551
662
|
logging.warning('The Heel marker is missing from your model. Considering Foot to Heel size as 10 cm.')
|
|
552
663
|
|
|
553
664
|
ankle_to_shoulder_pairs = [['RAnkle', 'RKnee'], ['RKnee', 'RHip'], ['RHip', 'RShoulder'],
|
|
@@ -688,4 +799,350 @@ def write_calibration(calib_params, toml_path):
|
|
|
688
799
|
fish_str = f'fisheye = false\n\n'
|
|
689
800
|
cal_f.write(cam_str + name_str + size_str + mat_str + dist_str + rot_str + tran_str + fish_str)
|
|
690
801
|
meta = '[metadata]\nadjusted = false\nerror = 0.0\n'
|
|
691
|
-
cal_f.write(meta)
|
|
802
|
+
cal_f.write(meta)
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
def pad_shape(arr, target_len, fill_value=np.nan):
|
|
806
|
+
'''
|
|
807
|
+
Pads an array to the target length with specified fill values
|
|
808
|
+
|
|
809
|
+
INPUTS:
|
|
810
|
+
- arr: Input array to be padded.
|
|
811
|
+
- target_len: The target length of the first dimension after padding.
|
|
812
|
+
- fill_value: The value to use for padding (default: np.nan).
|
|
813
|
+
|
|
814
|
+
OUTPUTS:
|
|
815
|
+
- Padded array with shape (target_len, ...) matching the input dimensions.
|
|
816
|
+
'''
|
|
817
|
+
|
|
818
|
+
if len(arr) < target_len:
|
|
819
|
+
pad_shape = (target_len - len(arr),) + arr.shape[1:]
|
|
820
|
+
padding = np.full(pad_shape, fill_value)
|
|
821
|
+
return np.concatenate((arr, padding))
|
|
822
|
+
|
|
823
|
+
return arr
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
def min_with_single_indices(L, T):
|
|
827
|
+
'''
|
|
828
|
+
Let L be a list (size s) with T associated tuple indices (size s).
|
|
829
|
+
Select the smallest values of L, considering that
|
|
830
|
+
the next smallest value cannot have the same numbers
|
|
831
|
+
in the associated tuple as any of the previous ones.
|
|
832
|
+
|
|
833
|
+
Example:
|
|
834
|
+
L = [ 20, 27, 51, 33, 43, 23, 37, 24, 4, 68, 84, 3 ]
|
|
835
|
+
T = list(it.product(range(2),range(3)))
|
|
836
|
+
= [(0,0),(0,1),(0,2),(0,3),(1,0),(1,1),(1,2),(1,3),(2,0),(2,1),(2,2),(2,3)]
|
|
837
|
+
|
|
838
|
+
- 1st smallest value: 3 with tuple (2,3), index 11
|
|
839
|
+
- 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]:
|
|
840
|
+
20 with tuple (0,0), index 0
|
|
841
|
+
- 3rd smallest value when excluding [X,X,X,X,X,(1,1),(1,2),X,X,X,X,X]:
|
|
842
|
+
23 with tuple (1,1), index 5
|
|
843
|
+
|
|
844
|
+
INPUTS:
|
|
845
|
+
- L: list (size s)
|
|
846
|
+
- T: T associated tuple indices (size s)
|
|
847
|
+
|
|
848
|
+
OUTPUTS:
|
|
849
|
+
- minL: list of smallest values of L, considering constraints on tuple indices
|
|
850
|
+
- argminL: list of indices of smallest values of L (indices of best combinations)
|
|
851
|
+
- T_minL: list of tuples associated with smallest values of L
|
|
852
|
+
'''
|
|
853
|
+
|
|
854
|
+
minL = [np.nanmin(L)]
|
|
855
|
+
argminL = [np.nanargmin(L)]
|
|
856
|
+
T_minL = [T[argminL[0]]]
|
|
857
|
+
|
|
858
|
+
mask_tokeep = np.array([True for t in T])
|
|
859
|
+
i=0
|
|
860
|
+
while mask_tokeep.any()==True:
|
|
861
|
+
mask_tokeep = mask_tokeep & np.array([t[0]!=T_minL[i][0] and t[1]!=T_minL[i][1] for t in T])
|
|
862
|
+
if mask_tokeep.any()==True:
|
|
863
|
+
indicesL_tokeep = np.where(mask_tokeep)[0]
|
|
864
|
+
minL += [np.nanmin(np.array(L)[indicesL_tokeep]) if not np.isnan(np.array(L)[indicesL_tokeep]).all() else np.nan]
|
|
865
|
+
argminL += [indicesL_tokeep[np.nanargmin(np.array(L)[indicesL_tokeep])] if not np.isnan(minL[-1]) else indicesL_tokeep[0]]
|
|
866
|
+
T_minL += (T[argminL[i+1]],)
|
|
867
|
+
i+=1
|
|
868
|
+
|
|
869
|
+
return np.array(minL), np.array(argminL), np.array(T_minL)
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
def sort_people_sports2d(keyptpre, keypt, scores=None):
|
|
873
|
+
'''
|
|
874
|
+
Associate persons across frames (Sports2D method)
|
|
875
|
+
Persons' indices are sometimes swapped when changing frame
|
|
876
|
+
A person is associated to another in the next frame when they are at a small distance
|
|
877
|
+
|
|
878
|
+
N.B.: Requires min_with_single_indices and euclidian_distance function (see common.py)
|
|
879
|
+
|
|
880
|
+
INPUTS:
|
|
881
|
+
- keyptpre: (K, L, M) array of 2D coordinates for K persons in the previous frame, L keypoints, M 2D coordinates
|
|
882
|
+
- keypt: idem keyptpre, for current frame
|
|
883
|
+
- score: (K, L) array of confidence scores for K persons, L keypoints (optional)
|
|
884
|
+
|
|
885
|
+
OUTPUTS:
|
|
886
|
+
- sorted_prev_keypoints: array with reordered persons with values of previous frame if current is empty
|
|
887
|
+
- sorted_keypoints: array with reordered persons --> if scores is not None
|
|
888
|
+
- sorted_scores: array with reordered scores --> if scores is not None
|
|
889
|
+
- associated_tuples: list of tuples with correspondences between persons across frames --> if scores is None (for Pose2Sim.triangulation())
|
|
890
|
+
'''
|
|
891
|
+
|
|
892
|
+
# Generate possible person correspondences across frames
|
|
893
|
+
max_len = max(len(keyptpre), len(keypt))
|
|
894
|
+
keyptpre = pad_shape(keyptpre, max_len, fill_value=np.nan)
|
|
895
|
+
keypt = pad_shape(keypt, max_len, fill_value=np.nan)
|
|
896
|
+
if scores is not None:
|
|
897
|
+
scores = pad_shape(scores, max_len, fill_value=np.nan)
|
|
898
|
+
|
|
899
|
+
# Compute distance between persons from one frame to another
|
|
900
|
+
personsIDs_comb = sorted(list(it.product(range(len(keyptpre)), range(len(keypt)))))
|
|
901
|
+
frame_by_frame_dist = [euclidean_distance(keyptpre[comb[0]],keypt[comb[1]]) for comb in personsIDs_comb]
|
|
902
|
+
frame_by_frame_dist = np.mean(frame_by_frame_dist, axis=1)
|
|
903
|
+
|
|
904
|
+
# Sort correspondences by distance
|
|
905
|
+
_, _, associated_tuples = min_with_single_indices(frame_by_frame_dist, personsIDs_comb)
|
|
906
|
+
|
|
907
|
+
# Associate points to same index across frames, nan if no correspondence
|
|
908
|
+
sorted_keypoints = []
|
|
909
|
+
for i in range(len(keyptpre)):
|
|
910
|
+
id_in_old = associated_tuples[:,1][associated_tuples[:,0] == i].tolist()
|
|
911
|
+
if len(id_in_old) > 0: sorted_keypoints += [keypt[id_in_old[0]]]
|
|
912
|
+
else: sorted_keypoints += [keypt[i]]
|
|
913
|
+
sorted_keypoints = np.array(sorted_keypoints)
|
|
914
|
+
|
|
915
|
+
if scores is not None:
|
|
916
|
+
sorted_scores = []
|
|
917
|
+
for i in range(len(keyptpre)):
|
|
918
|
+
id_in_old = associated_tuples[:,1][associated_tuples[:,0] == i].tolist()
|
|
919
|
+
if len(id_in_old) > 0: sorted_scores += [scores[id_in_old[0]]]
|
|
920
|
+
else: sorted_scores += [scores[i]]
|
|
921
|
+
sorted_scores = np.array(sorted_scores)
|
|
922
|
+
|
|
923
|
+
# Keep track of previous values even when missing for more than one frame
|
|
924
|
+
sorted_prev_keypoints = np.where(np.isnan(sorted_keypoints) & ~np.isnan(keyptpre), keyptpre, sorted_keypoints)
|
|
925
|
+
|
|
926
|
+
if scores is not None:
|
|
927
|
+
return sorted_prev_keypoints, sorted_keypoints, sorted_scores
|
|
928
|
+
else: # For Pose2Sim.triangulation()
|
|
929
|
+
return sorted_keypoints, associated_tuples
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
def sort_people_rtmlib(pose_tracker, keypoints, scores):
|
|
933
|
+
'''
|
|
934
|
+
Associate persons across frames (RTMLib method)
|
|
935
|
+
|
|
936
|
+
INPUTS:
|
|
937
|
+
- pose_tracker: PoseTracker. The initialized RTMLib pose tracker object
|
|
938
|
+
- keypoints: array of shape K, L, M with K the number of detected persons,
|
|
939
|
+
L the number of detected keypoints, M their 2D coordinates
|
|
940
|
+
- scores: array of shape K, L with K the number of detected persons,
|
|
941
|
+
L the confidence of detected keypoints
|
|
942
|
+
|
|
943
|
+
OUTPUT:
|
|
944
|
+
- sorted_keypoints: array with reordered persons
|
|
945
|
+
- sorted_scores: array with reordered scores
|
|
946
|
+
'''
|
|
947
|
+
|
|
948
|
+
try:
|
|
949
|
+
desired_size = max(pose_tracker.track_ids_last_frame)+1
|
|
950
|
+
sorted_keypoints = np.full((desired_size, keypoints.shape[1], 2), np.nan)
|
|
951
|
+
sorted_keypoints[pose_tracker.track_ids_last_frame] = keypoints[:len(pose_tracker.track_ids_last_frame), :, :]
|
|
952
|
+
sorted_scores = np.full((desired_size, scores.shape[1]), np.nan)
|
|
953
|
+
sorted_scores[pose_tracker.track_ids_last_frame] = scores[:len(pose_tracker.track_ids_last_frame), :]
|
|
954
|
+
except:
|
|
955
|
+
sorted_keypoints, sorted_scores = keypoints, scores
|
|
956
|
+
|
|
957
|
+
return sorted_keypoints, sorted_scores
|
|
958
|
+
|
|
959
|
+
|
|
960
|
+
def sort_people_deepsort(keypoints, scores, deepsort_tracker, frame,frame_count):
|
|
961
|
+
'''
|
|
962
|
+
Associate persons across frames (DeepSort method)
|
|
963
|
+
|
|
964
|
+
INPUTS:
|
|
965
|
+
- keypoints: array of shape K, L, M with K the number of detected persons,
|
|
966
|
+
L the number of detected keypoints, M their 2D coordinates
|
|
967
|
+
- scores: array of shape K, L with K the number of detected persons,
|
|
968
|
+
L the confidence of detected keypoints
|
|
969
|
+
- deepsort_tracker: The initialized DeepSort tracker object
|
|
970
|
+
- frame: np.array. The current image opened with cv2.imread
|
|
971
|
+
|
|
972
|
+
OUTPUT:
|
|
973
|
+
- sorted_keypoints: array with reordered persons
|
|
974
|
+
- sorted_scores: array with reordered scores
|
|
975
|
+
'''
|
|
976
|
+
|
|
977
|
+
try:
|
|
978
|
+
# Compute bboxes from keypoints and create detections (bboxes, scores, class_ids)
|
|
979
|
+
bboxes_ltwh = bbox_ltwh_compute(keypoints, padding=20)
|
|
980
|
+
bbox_scores = np.mean(scores, axis=1)
|
|
981
|
+
class_ids = np.array(['person']*len(bboxes_ltwh))
|
|
982
|
+
detections = list(zip(bboxes_ltwh, bbox_scores, class_ids))
|
|
983
|
+
|
|
984
|
+
# Estimates the tracks and retrieve indexes of the original detections
|
|
985
|
+
det_ids = [i for i in range(len(detections))]
|
|
986
|
+
tracks = deepsort_tracker.update_tracks(detections, frame=frame, others=det_ids)
|
|
987
|
+
track_ids_frame, orig_det_ids = [], []
|
|
988
|
+
for track in tracks:
|
|
989
|
+
if not track.is_confirmed():
|
|
990
|
+
continue
|
|
991
|
+
track_ids_frame.append(int(track.track_id)-1) # ID of people
|
|
992
|
+
orig_det_ids.append(track.get_det_supplementary()) # ID of detections
|
|
993
|
+
|
|
994
|
+
# Correspondence between person IDs and original detection IDs
|
|
995
|
+
desired_size = max(track_ids_frame) + 1
|
|
996
|
+
sorted_keypoints = np.full((desired_size, keypoints.shape[1], 2), np.nan)
|
|
997
|
+
sorted_scores = np.full((desired_size, scores.shape[1]), np.nan)
|
|
998
|
+
for i,v in enumerate(track_ids_frame):
|
|
999
|
+
if orig_det_ids[i] is not None:
|
|
1000
|
+
sorted_keypoints[v] = keypoints[orig_det_ids[i]]
|
|
1001
|
+
sorted_scores[v] = scores[orig_det_ids[i]]
|
|
1002
|
+
|
|
1003
|
+
except Exception as e:
|
|
1004
|
+
sorted_keypoints, sorted_scores = keypoints, scores
|
|
1005
|
+
if frame_count > deepsort_tracker.tracker.n_init:
|
|
1006
|
+
logging.warning(f"Tracking error: {e}. Sorting persons with DeepSort method failed for this frame.")
|
|
1007
|
+
|
|
1008
|
+
return sorted_keypoints, sorted_scores
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
def bbox_ltwh_compute(keypoints, padding=0):
|
|
1012
|
+
'''
|
|
1013
|
+
Compute bounding boxes in (x_min, y_min, width, height) format
|
|
1014
|
+
Optionally add padding to the bounding boxes
|
|
1015
|
+
as a percentage of the bounding box size (+padding% horizontally, +padding/2% vertically)
|
|
1016
|
+
|
|
1017
|
+
INPUTS:
|
|
1018
|
+
- keypoints: array of shape K, L, M with K the number of detected persons,
|
|
1019
|
+
L the number of detected keypoints, M their 2D coordinates
|
|
1020
|
+
- padding: int. The padding to add to the bounding boxes, in perceptage
|
|
1021
|
+
'''
|
|
1022
|
+
|
|
1023
|
+
x_coords = keypoints[:, :, 0]
|
|
1024
|
+
y_coords = keypoints[:, :, 1]
|
|
1025
|
+
|
|
1026
|
+
x_min, x_max = np.min(x_coords, axis=1), np.max(x_coords, axis=1)
|
|
1027
|
+
y_min, y_max = np.min(y_coords, axis=1), np.max(y_coords, axis=1)
|
|
1028
|
+
width = x_max - x_min
|
|
1029
|
+
height = y_max - y_min
|
|
1030
|
+
|
|
1031
|
+
if padding > 0:
|
|
1032
|
+
x_min = x_min - width*padding/100
|
|
1033
|
+
y_min = y_min - height/2*padding/100
|
|
1034
|
+
width = width + 2*width*padding/100
|
|
1035
|
+
height = height + height*padding/100
|
|
1036
|
+
|
|
1037
|
+
bbox_ltwh = np.stack((x_min, y_min, width, height), axis=1)
|
|
1038
|
+
|
|
1039
|
+
return bbox_ltwh
|
|
1040
|
+
|
|
1041
|
+
|
|
1042
|
+
def draw_bounding_box(img, X, Y, colors=[(255, 0, 0), (0, 255, 0), (0, 0, 255)], fontSize=0.3, thickness=1):
|
|
1043
|
+
'''
|
|
1044
|
+
Draw bounding boxes and person ID around list of lists of X and Y coordinates.
|
|
1045
|
+
Bounding boxes have a different color for each person.
|
|
1046
|
+
|
|
1047
|
+
INPUTS:
|
|
1048
|
+
- img: opencv image
|
|
1049
|
+
- X: list of list of x coordinates
|
|
1050
|
+
- Y: list of list of y coordinates
|
|
1051
|
+
- colors: list of colors to cycle through
|
|
1052
|
+
|
|
1053
|
+
OUTPUT:
|
|
1054
|
+
- img: image with rectangles and person IDs
|
|
1055
|
+
'''
|
|
1056
|
+
|
|
1057
|
+
color_cycle = it.cycle(colors)
|
|
1058
|
+
|
|
1059
|
+
for i,(x,y) in enumerate(zip(X,Y)):
|
|
1060
|
+
color = next(color_cycle)
|
|
1061
|
+
if not np.isnan(x).all():
|
|
1062
|
+
x_min, y_min = np.nanmin(x).astype(int), np.nanmin(y).astype(int)
|
|
1063
|
+
x_max, y_max = np.nanmax(x).astype(int), np.nanmax(y).astype(int)
|
|
1064
|
+
if x_min < 0: x_min = 0
|
|
1065
|
+
if x_max > img.shape[1]: x_max = img.shape[1]
|
|
1066
|
+
if y_min < 0: y_min = 0
|
|
1067
|
+
if y_max > img.shape[0]: y_max = img.shape[0]
|
|
1068
|
+
|
|
1069
|
+
# Draw rectangles
|
|
1070
|
+
cv2.rectangle(img, (x_min-25, y_min-25), (x_max+25, y_max+25), color, thickness)
|
|
1071
|
+
|
|
1072
|
+
# Write person ID
|
|
1073
|
+
cv2.putText(img, str(i), (x_min-30, y_min-30), cv2.FONT_HERSHEY_SIMPLEX, fontSize, color, 2, cv2.LINE_AA)
|
|
1074
|
+
|
|
1075
|
+
return img
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
def draw_skel(img, X, Y, model):
|
|
1079
|
+
'''
|
|
1080
|
+
Draws keypoints and skeleton for each person.
|
|
1081
|
+
Skeletons have a different color for each person.
|
|
1082
|
+
|
|
1083
|
+
INPUTS:
|
|
1084
|
+
- img: opencv image
|
|
1085
|
+
- X: list of list of x coordinates
|
|
1086
|
+
- Y: list of list of y coordinates
|
|
1087
|
+
- model: skeleton model (from skeletons.py)
|
|
1088
|
+
- colors: list of colors to cycle through
|
|
1089
|
+
|
|
1090
|
+
OUTPUT:
|
|
1091
|
+
- img: image with keypoints and skeleton
|
|
1092
|
+
'''
|
|
1093
|
+
|
|
1094
|
+
# Get (unique) pairs between which to draw a line
|
|
1095
|
+
id_pairs, name_pairs = [], []
|
|
1096
|
+
for data_i in PreOrderIter(model.root, filter_=lambda node: node.is_leaf):
|
|
1097
|
+
node_branch_ids = [node_i.id for node_i in data_i.path]
|
|
1098
|
+
node_branch_names = [node_i.name for node_i in data_i.path]
|
|
1099
|
+
id_pairs += [[node_branch_ids[i],node_branch_ids[i+1]] for i in range(len(node_branch_ids)-1)]
|
|
1100
|
+
name_pairs += [[node_branch_names[i],node_branch_names[i+1]] for i in range(len(node_branch_names)-1)]
|
|
1101
|
+
node_pairs = {tuple(name_pair): id_pair for (name_pair,id_pair) in zip(name_pairs,id_pairs)}
|
|
1102
|
+
|
|
1103
|
+
|
|
1104
|
+
# Draw lines
|
|
1105
|
+
for (x,y) in zip(X,Y):
|
|
1106
|
+
if not np.isnan(x).all():
|
|
1107
|
+
for names, ids in node_pairs.items():
|
|
1108
|
+
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]])):
|
|
1109
|
+
if any(n.startswith('R') for n in names) and not any(n.startswith('L') for n in names):
|
|
1110
|
+
c = (255,128,0)
|
|
1111
|
+
elif any(n.startswith('L') for n in names) and not any(n.startswith('R') for n in names):
|
|
1112
|
+
c = (0,255,0)
|
|
1113
|
+
else:
|
|
1114
|
+
c = (51, 153, 255)
|
|
1115
|
+
cv2.line(img, (int(x[ids[0]]), int(y[ids[0]])), (int(x[ids[1]]), int(y[ids[1]])), c, thickness)
|
|
1116
|
+
|
|
1117
|
+
return img
|
|
1118
|
+
|
|
1119
|
+
|
|
1120
|
+
def draw_keypts(img, X, Y, scores, cmap_str='RdYlGn'):
|
|
1121
|
+
'''
|
|
1122
|
+
Draws keypoints and skeleton for each person.
|
|
1123
|
+
Keypoints' colors depend on their score.
|
|
1124
|
+
|
|
1125
|
+
INPUTS:
|
|
1126
|
+
- img: opencv image
|
|
1127
|
+
- X: list of list of x coordinates
|
|
1128
|
+
- Y: list of list of y coordinates
|
|
1129
|
+
- scores: list of list of scores
|
|
1130
|
+
- cmap_str: colormap name
|
|
1131
|
+
|
|
1132
|
+
OUTPUT:
|
|
1133
|
+
- img: image with keypoints and skeleton
|
|
1134
|
+
'''
|
|
1135
|
+
|
|
1136
|
+
scores = np.where(np.isnan(scores), 0, scores)
|
|
1137
|
+
# scores = (scores - 0.4) / (1-0.4) # to get a red color for scores lower than 0.4
|
|
1138
|
+
scores = np.where(scores>0.99, 0.99, scores)
|
|
1139
|
+
scores = np.where(scores<0, 0, scores)
|
|
1140
|
+
|
|
1141
|
+
cmap = plt.get_cmap(cmap_str)
|
|
1142
|
+
for (x,y,s) in zip(X,Y,scores):
|
|
1143
|
+
c_k = np.array(cmap(s))[:,:-1]*255
|
|
1144
|
+
[cv2.circle(img, (int(x[i]), int(y[i])), thickness+4, c_k[i][::-1], -1)
|
|
1145
|
+
for i in range(len(x))
|
|
1146
|
+
if not (np.isnan(x[i]) or np.isnan(y[i]))]
|
|
1147
|
+
|
|
1148
|
+
return img
|
Sports2D/Utilities/skeletons.py
CHANGED
|
@@ -85,10 +85,10 @@ HALPE_26 = Node("Hip", id=19, children=[
|
|
|
85
85
|
])
|
|
86
86
|
|
|
87
87
|
|
|
88
|
-
'''
|
|
88
|
+
'''COCO_133_WRIST (full-body with hands and face, from AlphaPose, MMPose, etc.)
|
|
89
89
|
https://github.com/MVIG-SJTU/AlphaPose/blob/master/docs/MODEL_ZOO.md
|
|
90
90
|
https://github.com/open-mmlab/mmpose/tree/main/projects/rtmpose'''
|
|
91
|
-
|
|
91
|
+
COCO_133_WRIST = Node("Hip", id=None, children=[
|
|
92
92
|
Node("RHip", id=12, children=[
|
|
93
93
|
Node("RKnee", id=14, children=[
|
|
94
94
|
Node("RAnkle", id=16, children=[
|
|
@@ -139,7 +139,7 @@ COCO_133_wrist = Node("CHip", id=None, children=[
|
|
|
139
139
|
'''COCO_133 (full-body with hands and face, from AlphaPose, MMPose, etc.)
|
|
140
140
|
https://github.com/MVIG-SJTU/AlphaPose/blob/master/docs/MODEL_ZOO.md
|
|
141
141
|
https://github.com/open-mmlab/mmpose/tree/main/projects/rtmpose'''
|
|
142
|
-
COCO_133 = Node("
|
|
142
|
+
COCO_133 = Node("Hip", id=None, children=[
|
|
143
143
|
Node("RHip", id=12, children=[
|
|
144
144
|
Node("RKnee", id=14, children=[
|
|
145
145
|
Node("RAnkle", id=16, children=[
|
|
@@ -359,9 +359,7 @@ COCO_133 = Node("CHip", id=None, children=[
|
|
|
359
359
|
Node("Mouth17", id=87, children=[
|
|
360
360
|
Node("Mouth18", id=88, children=[
|
|
361
361
|
Node("Mouth19", id=89, children=[
|
|
362
|
-
Node("Mouth20", id=90
|
|
363
|
-
Node("Mouth21", id=91)
|
|
364
|
-
]),
|
|
362
|
+
Node("Mouth20", id=90)
|
|
365
363
|
]),
|
|
366
364
|
]),
|
|
367
365
|
]),
|
|
@@ -387,7 +385,7 @@ COCO_133 = Node("CHip", id=None, children=[
|
|
|
387
385
|
|
|
388
386
|
'''COCO_17 (full-body without hands and feet, from OpenPose, AlphaPose, OpenPifPaf, YOLO-pose, MMPose, etc.)
|
|
389
387
|
https://github.com/open-mmlab/mmpose/tree/main/projects/rtmpose'''
|
|
390
|
-
COCO_17 = Node("
|
|
388
|
+
COCO_17 = Node("Hip", id=None, children=[
|
|
391
389
|
Node("RHip", id=12, children=[
|
|
392
390
|
Node("RKnee", id=14, children=[
|
|
393
391
|
Node("RAnkle", id=16),
|
|
@@ -645,9 +643,10 @@ FACE_106 = Node("root", id=None, children=[
|
|
|
645
643
|
]),
|
|
646
644
|
])
|
|
647
645
|
|
|
646
|
+
|
|
648
647
|
'''ANIMAL2D_17 (full-body animal)
|
|
649
648
|
https://github.com/AlexTheBad/AP-10K/'''
|
|
650
|
-
ANIMAL2D_17 = Node("
|
|
649
|
+
ANIMAL2D_17 = Node("Hip", id=4, children=[
|
|
651
650
|
Node("RHip", id=14, children=[
|
|
652
651
|
Node("RKnee", id=15, children=[
|
|
653
652
|
Node("RAnkle", id=16),
|
Sports2D/Utilities/tests.py
CHANGED
|
@@ -56,11 +56,11 @@ def test_workflow():
|
|
|
56
56
|
|
|
57
57
|
# Default
|
|
58
58
|
demo_cmd = ["sports2d", "--show_realtime_results", "False", "--show_graphs", "False"]
|
|
59
|
-
subprocess.run(demo_cmd, check=True, capture_output=True, text=True)
|
|
59
|
+
subprocess.run(demo_cmd, check=True, capture_output=True, text=True, encoding='utf-8')
|
|
60
60
|
|
|
61
61
|
# With no pixels to meters conversion, no multiperson, lightweight mode, detection frequency, time range and slowmo factor
|
|
62
62
|
demo_cmd2 = ["sports2d", "--to_meters", "False", "--multiperson", "False", "--mode", "lightweight", "--det_frequency", "50", "--time_range", "1.2", "2.7", "--slowmo_factor", "4", "--show_realtime_results", "False", "--show_graphs", "False"]
|
|
63
|
-
subprocess.run(demo_cmd2, check=True, capture_output=True, text=True)
|
|
63
|
+
subprocess.run(demo_cmd2, check=True, capture_output=True, text=True, encoding='utf-8')
|
|
64
64
|
|
|
65
65
|
# With inverse kinematics, body pose_model and custom RTMO mode
|
|
66
66
|
# demo_cmd3 = ["sports2d", "--do_ik", "--person_orientation", "front none left", "--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]}", "--show_realtime_results", "False", "--show_graphs", "False"]
|
|
@@ -74,4 +74,4 @@ def test_workflow():
|
|
|
74
74
|
with open(cli_config_path, 'w') as f: toml.dump(config_dict, f)
|
|
75
75
|
|
|
76
76
|
demo_cmd4 = ["sports2d", "--config", str(cli_config_path), "--show_realtime_results", "False", "--show_graphs", "False"]
|
|
77
|
-
subprocess.run(demo_cmd4, check=True, capture_output=True, text=True)
|
|
77
|
+
subprocess.run(demo_cmd4, check=True, capture_output=True, text=True, encoding='utf-8')
|