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.
@@ -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(trc_data, keypoints_names, fastest_frames_to_remove_percent=0.2, close_to_zero_speed=0.2, large_hip_knee_angles=45):
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
- - trc_data: pd.DataFrame. The XYZ coordinates of each marker
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
- - trc_data_low_speeds_low_angles: pd.DataFrame. The best coordinates for measurements
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((trc_data['RShoulder'].values + trc_data['LShoulder'].values) /2)
598
+ df_MidShoulder = pd.DataFrame((Q_coords['RShoulder'].values + Q_coords['LShoulder'].values) /2)
490
599
  df_MidShoulder.columns = ['MidShoulder']*3
491
- trc_data = pd.concat((trc_data.reset_index(drop=True), df_MidShoulder), axis=1)
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((trc_data['RHip'].values + trc_data['LHip'].values) /2)
605
+ df_Hip = pd.DataFrame((Q_coords['RHip'].values + Q_coords['LHip'].values) /2)
497
606
  df_Hip.columns = ['Hip']*3
498
- trc_data = pd.concat((trc_data.reset_index(drop=True), df_Hip), axis=1)
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(trc_data.iloc[:,kpt:kpt+3].diff(), axis=1) for kpt in range(n_markers)], axis=0))
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
- raise ValueError('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.')
506
- min_speed_indices = sum_speeds.abs().nsmallest(int(len(sum_speeds) * (1-fastest_frames_to_remove_percent))).index
507
- trc_data_low_speeds = trc_data.iloc[min_speed_indices].reset_index(drop=True)
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(trc_data_low_speeds, ang_to_consider = ['right knee', 'left knee', 'right hip', 'left hip'])
513
- trc_data_low_speeds_low_angles = trc_data_low_speeds[ang_mean < large_hip_knee_angles]
514
- if len(trc_data_low_speeds_low_angles) < 50:
515
- trc_data_low_speeds_low_angles = trc_data_low_speeds.iloc[pd.Series(ang_mean).nsmallest(50).index]
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 agles to be below {large_hip_knee_angles}°.")
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
- trc_data_low_speeds_low_angles = trc_data_low_speeds_low_angles.iloc[:,:-3]
631
+ Q_coords_low_speeds_low_angles = Q_coords_low_speeds_low_angles.iloc[:,:-3]
521
632
 
522
- return trc_data_low_speeds_low_angles
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
@@ -85,10 +85,10 @@ HALPE_26 = Node("Hip", id=19, children=[
85
85
  ])
86
86
 
87
87
 
88
- '''COCO_133_wrist (full-body with hands and face, from AlphaPose, MMPose, etc.)
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
- COCO_133_wrist = Node("CHip", id=None, children=[
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("CHip", id=None, children=[
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, children=[
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("CHip", id=None, children=[
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("CHip", id=4, children=[
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),
@@ -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')