sports2d 0.7.3__py3-none-any.whl → 0.8.0__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/process.py CHANGED
@@ -71,13 +71,16 @@ import pandas as pd
71
71
  import cv2
72
72
  import matplotlib as mpl
73
73
  import matplotlib.pyplot as plt
74
+ from matplotlib.widgets import Slider, Button
75
+ from matplotlib import patheffects
76
+
74
77
  from rtmlib import PoseTracker, BodyWithFeet, Wholebody, Body, Custom
75
78
  from deep_sort_realtime.deepsort_tracker import DeepSort
76
79
 
77
80
  from Sports2D.Utilities import filter
78
81
  from Sports2D.Utilities.common import *
79
- from Sports2D.Utilities.skeletons import *
80
82
  from Pose2Sim.common import *
83
+ from Pose2Sim.skeletons import *
81
84
 
82
85
  DEFAULT_MASS = 70
83
86
  DEFAULT_HEIGHT = 1.7
@@ -625,8 +628,10 @@ def trc_data_from_XYZtime(X, Y, Z, time):
625
628
  - trc_data: pd.DataFrame. Dataframe of trc data
626
629
  '''
627
630
 
628
- trc_data = pd.concat([pd.concat([X.iloc[:,kpt], Y.iloc[:,kpt], Z.iloc[:,kpt]], axis=1) for kpt in range(len(X.columns))], axis=1)
629
- trc_data.insert(0, 'time', time)
631
+ columns_to_concat = []
632
+ for kpt in range(len(X.columns)):
633
+ columns_to_concat.extend([X.iloc[:,kpt], Y.iloc[:,kpt], Z.iloc[:,kpt]])
634
+ trc_data = pd.concat([time] + columns_to_concat, axis=1)
630
635
 
631
636
  return trc_data
632
637
 
@@ -777,21 +782,308 @@ def angle_plots(angle_data_unfiltered, angle_data, person_id):
777
782
  pw.show()
778
783
 
779
784
 
780
- def get_personID_with_highest_scores(all_frames_scores):
785
+ def get_personIDs_with_highest_scores(all_frames_scores, nb_persons_to_detect):
781
786
  '''
782
787
  Get the person ID with the highest scores
783
788
 
784
789
  INPUTS:
785
790
  - all_frames_scores: array of scores for all frames, all persons, all keypoints
791
+ - nb_persons_to_detect: int or 'all'. The number of persons to detect
786
792
 
787
793
  OUTPUT:
788
- - person_id: int. The person ID with the highest scores
794
+ - selected_persons: list of int. The person IDs with the highest scores
789
795
  '''
790
796
 
791
797
  # Get the person with the highest scores over all frames and all keypoints
792
- person_id = np.argmax(np.nansum(np.nansum(all_frames_scores, axis=0), axis=1))
798
+ score_means = np.nansum(np.nanmean(all_frames_scores, axis=0), axis=1)
799
+ selected_persons = (-score_means).argsort()[:nb_persons_to_detect]
800
+
801
+ return selected_persons
802
+
803
+
804
+ def get_personIDs_in_detection_order(nb_persons_to_detect, reverse=False):
805
+ '''
806
+ Get the person IDs in the order of detection
807
+
808
+ INPUTS:
809
+ - nb_persons_to_detect: int. The number of persons to detect
810
+ - reverse: bool. Whether to reverse the order of detection
811
+
812
+ OUTPUT:
813
+ - selected_persons: list of int. The person IDs in the order of detection
814
+ '''
815
+
816
+ selected_persons = list(range(nb_persons_to_detect))
817
+ if reverse:
818
+ selected_persons = selected_persons[::-1]
819
+
820
+ return selected_persons
821
+
822
+
823
+ def get_personIDs_with_greatest_displacement(all_frames_X_homog, all_frames_Y_homog, nb_persons_to_detect, reverse=False, horizontal=True):
824
+ '''
825
+ Get the person ID with the greatest displacement
826
+
827
+ INPUTS:
828
+ - all_frames_X_homog: shape (Nframes, Npersons, Nkpts)
829
+ - all_frames_Y_homog: shape (Nframes, Npersons, Nkpts)
830
+ - nb_persons_to_detect: int. The number of persons to detect
831
+ - reverse: bool. Whether to reverse the order of detection
832
+ - horizontal: bool. Whether to compute the displacement in the horizontal direction
793
833
 
794
- return person_id
834
+ OUTPUT:
835
+ - selected_persons: list of int. The person IDs with the greatest displacement
836
+ '''
837
+
838
+ # Average position over all keypoints to shape (Npersons, Nframes, Ndims)
839
+ mean_pos_X_kpts = np.nanmean(all_frames_X_homog, axis=2)
840
+
841
+ # Compute sum of distances from one frame to the next
842
+ if horizontal:
843
+ max_dist_traveled = abs(np.nansum(np.diff(mean_pos_X_kpts, axis=0), axis=0))
844
+ else:
845
+ mean_pos_Y_kpts = np.nanmean(all_frames_Y_homog, axis=2)
846
+ pos_XY = np.stack((mean_pos_X_kpts.T, mean_pos_Y_kpts.T), axis=-1)
847
+ max_dist_traveled = np.nansum([euclidean_distance(m,p) for (m,p) in zip(pos_XY[:,1:,:], pos_XY[:,:-1,:])], axis=1)
848
+ max_dist_traveled = np.where(np.isinf(max_dist_traveled), 0, max_dist_traveled)
849
+
850
+ selected_persons = (-max_dist_traveled).argsort()[:nb_persons_to_detect]
851
+ if reverse:
852
+ selected_persons = selected_persons[::-1]
853
+
854
+ return selected_persons
855
+
856
+
857
+ def get_personIDs_on_click(frames, all_frames_X_homog, all_frames_Y_homog):
858
+ '''
859
+ Get the person IDs on click in the image
860
+
861
+ INPUTS:
862
+ - frames: list of images read by cv2.imread. shape (Nframes, H, W, 3)
863
+ - all_frames_X_homog: shape (Nframes, Npersons, Nkpts)
864
+ - all_frames_Y_homog: shape (Nframes, Npersons, Nkpts)
865
+
866
+ OUTPUT:
867
+ - selected_persons: list of int. The person IDs selected by the user
868
+ '''
869
+
870
+ # Reorganize the coordinates to shape (Nframes, Npersons, Nkpts, Ndims)
871
+ all_pose_coords = np.stack((all_frames_X_homog, all_frames_Y_homog), axis=-1)
872
+
873
+ # Trim all_pose_coords and frames to the same size
874
+ min_frames = min(all_pose_coords.shape[0], len(frames))
875
+ all_pose_coords = all_pose_coords[:min_frames]
876
+ frames = frames[:min_frames]
877
+
878
+ # Select person IDs on click on video/image
879
+ selected_persons = select_persons_on_vid(frames, all_pose_coords)
880
+
881
+ return selected_persons
882
+
883
+
884
+ def select_persons_on_vid(frames, all_pose_coords):
885
+ '''
886
+ Interactive UI to select persons from a video by clicking on their bounding boxes.
887
+
888
+ INPUTS:
889
+ - frames: list of images read by cv2.imread. shape (Nframes, H, W, 3)
890
+ - all_pose_coords: keypoints coordinates. shape (Nframes, Npersons, Nkpts, Ndims)
891
+
892
+ OUTPUT:
893
+ - selected_persons : list with indices of selected persons
894
+ '''
895
+
896
+ BACKGROUND_COLOR = 'white'
897
+ SLIDER_COLOR = '#4682B4'
898
+ SLIDER_EDGE_COLOR = (0.5, 0.5, 0.5, 0.5)
899
+ UNSELECTED_COLOR = (1, 1, 1, 0.1)
900
+ LINE_UNSELECTED_COLOR = 'white'
901
+ LINE_SELECTED_COLOR = 'darkorange'
902
+
903
+ selected_persons = []
904
+
905
+ # Calculate bounding boxes for each person in each frame
906
+ n_frames, n_persons = all_pose_coords.shape[0], all_pose_coords.shape[1]
907
+ all_bboxes = []
908
+ for frame_idx in range(n_frames):
909
+ frame_bboxes = []
910
+ for person_idx in range(n_persons):
911
+ # Get keypoints for current person
912
+ keypoints = all_pose_coords[frame_idx, person_idx]
913
+ valid_keypoints = keypoints[~np.isnan(keypoints).all(axis=1)]
914
+ if len(valid_keypoints) > 0:
915
+ # Calculate bounding box
916
+ x_min, y_min = np.min(valid_keypoints, axis=0)
917
+ x_max, y_max = np.max(valid_keypoints, axis=0)
918
+ frame_bboxes.append((x_min, y_min, x_max, y_max))
919
+ else:
920
+ frame_bboxes.append((np.nan, np.nan, np.nan, np.nan)) # No valid bounding box for this person
921
+ all_bboxes.append(frame_bboxes)
922
+ all_bboxes = np.array(all_bboxes) # Shape: (Nframes, Npersons, 4)
923
+
924
+ # Create figure, axes, and slider
925
+ frame_height, frame_width = frames[0].shape[:2]
926
+ is_vertical = frame_height > frame_width
927
+ if is_vertical:
928
+ fig_height = frame_height / 250 # For vertical videos
929
+ else:
930
+ fig_height = max(frame_height / 300, 6) # For horizontal videos
931
+ fig = plt.figure(figsize=(8, fig_height), num=f'Select the persons to analyze in the desired order')
932
+ fig.patch.set_facecolor(BACKGROUND_COLOR)
933
+
934
+ video_axes_height = 0.7 if is_vertical else 0.6
935
+ ax_video = plt.axes([0.1, 0.2, 0.8, video_axes_height])
936
+ ax_video.axis('off')
937
+ ax_video.set_facecolor(BACKGROUND_COLOR)
938
+
939
+ # First image
940
+ frame_rgb = cv2.cvtColor(frames[0], cv2.COLOR_BGR2RGB)
941
+ rects, annotations = [], []
942
+ for person_idx, bbox in enumerate(all_bboxes[0]):
943
+ if ~np.isnan(bbox).any():
944
+ x_min, y_min, x_max, y_max = bbox.astype(int)
945
+ rect = plt.Rectangle(
946
+ (x_min, y_min), x_max - x_min, y_max - y_min,
947
+ linewidth=1, edgecolor=LINE_UNSELECTED_COLOR, facecolor=UNSELECTED_COLOR,
948
+ linestyle='-', path_effects=[patheffects.withSimplePatchShadow()], zorder=2
949
+ )
950
+ ax_video.add_patch(rect)
951
+ annotation = ax_video.text(
952
+ x_min, y_min - 10, f'{person_idx}', color=LINE_UNSELECTED_COLOR, fontsize=7, fontweight='normal',
953
+ bbox=dict(facecolor=UNSELECTED_COLOR, edgecolor=LINE_UNSELECTED_COLOR, boxstyle='square,pad=0.3', path_effects=[patheffects.withSimplePatchShadow()]), zorder=3
954
+ )
955
+ rects.append(rect)
956
+ annotations.append(annotation)
957
+ img_plot = ax_video.imshow(frame_rgb)
958
+
959
+ # Slider
960
+ ax_slider = plt.axes([ax_video.get_position().x0, ax_video.get_position().y0-0.05, ax_video.get_position().width, 0.04])
961
+ ax_slider.set_facecolor(BACKGROUND_COLOR)
962
+ frame_slider = Slider(
963
+ ax=ax_slider,
964
+ label='',
965
+ valmin=0,
966
+ valmax=len(all_pose_coords)-1,
967
+ valinit=0,
968
+ valstep=1,
969
+ valfmt=None
970
+ )
971
+ frame_slider.poly.set_edgecolor(SLIDER_EDGE_COLOR)
972
+ frame_slider.poly.set_facecolor(SLIDER_COLOR)
973
+ frame_slider.poly.set_linewidth(1)
974
+ frame_slider.valtext.set_visible(False)
975
+
976
+
977
+ # Status text and OK button
978
+ ax_status = plt.axes([ax_video.get_position().x0, ax_video.get_position().y0-0.1, 2*ax_video.get_position().width/3, 0.04])
979
+ ax_status.axis('off')
980
+ status_text = ax_status.text(0.0, 0.5, f"Selected: None", color='black', fontsize=10)
981
+
982
+ ax_button = plt.axes([ax_video.get_position().x0 + 3*ax_video.get_position().width/4, ax_video.get_position().y0-0.1, ax_video.get_position().width/4, 0.04])
983
+ ok_button = Button(ax_button, 'OK', color=BACKGROUND_COLOR)
984
+
985
+
986
+ def update_frame(val):
987
+ # Update image
988
+ frame_idx = int(frame_slider.val)
989
+ frame_rgb = cv2.cvtColor(frames[frame_idx], cv2.COLOR_BGR2RGB)
990
+
991
+ # Update bboxes and annotations
992
+ for items in [rects, annotations]:
993
+ for item in items:
994
+ item.remove()
995
+ items.clear()
996
+
997
+ for person_idx, bbox in enumerate(all_bboxes[frame_idx]):
998
+ if ~np.isnan(bbox).any():
999
+ x_min, y_min, x_max, y_max = bbox.astype(int)
1000
+ rect = plt.Rectangle(
1001
+ (x_min, y_min), x_max - x_min, y_max - y_min,
1002
+ linewidth=1, edgecolor='white', facecolor=UNSELECTED_COLOR,
1003
+ linestyle='-', path_effects=[patheffects.withSimplePatchShadow()], zorder=2
1004
+ )
1005
+ ax_video.add_patch(rect)
1006
+ rects.append(rect)
1007
+
1008
+ annotation = ax_video.text(
1009
+ x_min, y_min - 10, f'{person_idx}', color=LINE_UNSELECTED_COLOR, fontsize=7, fontweight='normal',
1010
+ bbox=dict(facecolor=UNSELECTED_COLOR, edgecolor=LINE_UNSELECTED_COLOR, boxstyle='square,pad=0.3'), path_effects=[patheffects.withSimplePatchShadow()], zorder=3
1011
+ )
1012
+ annotations.append(annotation)
1013
+
1014
+ # Update plot
1015
+ img_plot.set_data(frame_rgb)
1016
+ fig.canvas.draw_idle()
1017
+
1018
+
1019
+ def on_click(event):
1020
+ if event.inaxes != ax_video:
1021
+ return
1022
+
1023
+ frame_idx = int(frame_slider.val)
1024
+ x, y = event.xdata, event.ydata
1025
+
1026
+ # Check if click is inside any bounding box
1027
+ for person_idx, bbox in enumerate(all_bboxes[frame_idx]):
1028
+ if ~np.isnan(bbox).any():
1029
+ x_min, y_min, x_max, y_max = bbox.astype(int)
1030
+ if x_min <= x <= x_max and y_min <= y <= y_max:
1031
+ # Toggle selection
1032
+ if person_idx in selected_persons:
1033
+ rects[person_idx].set_linewidth(1)
1034
+ rects[person_idx].set_edgecolor(LINE_UNSELECTED_COLOR)
1035
+ selected_persons.remove(person_idx)
1036
+ else:
1037
+ rects[person_idx].set_linewidth(2)
1038
+ rects[person_idx].set_edgecolor(LINE_SELECTED_COLOR)
1039
+ selected_persons.append(person_idx)
1040
+
1041
+ # Update display
1042
+ status_text.set_text(f"Selected: {selected_persons}")
1043
+ # draw_bounding_boxes(frame_idx)
1044
+ fig.canvas.draw_idle()
1045
+ break
1046
+
1047
+
1048
+ def on_hover(event):
1049
+ if event.inaxes != ax_video:
1050
+ return
1051
+
1052
+ frame_idx = int(frame_slider.val)
1053
+ x, y = event.xdata, event.ydata
1054
+
1055
+ # Change color on hover
1056
+ for person_idx, bbox in enumerate(all_bboxes[frame_idx]):
1057
+ if ~np.isnan(bbox).any():
1058
+ x_min, y_min, x_max, y_max = bbox.astype(int)
1059
+ if x_min <= x <= x_max and y_min <= y <= y_max:
1060
+ rects[person_idx].set_linewidth(2)
1061
+ rects[person_idx].set_edgecolor(LINE_SELECTED_COLOR)
1062
+ rects[person_idx].set_facecolor((1, 1, 0, 0.2))
1063
+ else:
1064
+ rects[person_idx].set_facecolor(UNSELECTED_COLOR)
1065
+ if person_idx in selected_persons:
1066
+ rects[person_idx].set_linewidth(2)
1067
+ rects[person_idx].set_edgecolor(LINE_SELECTED_COLOR)
1068
+ else:
1069
+ rects[person_idx].set_linewidth(1)
1070
+ rects[person_idx].set_edgecolor(LINE_UNSELECTED_COLOR)
1071
+ fig.canvas.draw_idle()
1072
+
1073
+
1074
+ def on_ok(event):
1075
+ plt.close(fig)
1076
+
1077
+
1078
+ # Connect events
1079
+ frame_slider.on_changed(update_frame)
1080
+ fig.canvas.mpl_connect('button_press_event', on_click)
1081
+ fig.canvas.mpl_connect('motion_notify_event', on_hover)
1082
+ ok_button.on_clicked(on_ok)
1083
+
1084
+ plt.show()
1085
+
1086
+ return selected_persons
795
1087
 
796
1088
 
797
1089
  def compute_floor_line(trc_data, keypoint_names = ['LBigToe', 'RBigToe'], toe_speed_below = 7, tot_speed_above=2.0):
@@ -813,7 +1105,9 @@ def compute_floor_line(trc_data, keypoint_names = ['LBigToe', 'RBigToe'], toe_sp
813
1105
  '''
814
1106
 
815
1107
  # Remove frames where the person is mostly not moving (outlier)
816
- av_speeds = np.nanmean([np.insert(np.linalg.norm(trc_data[kpt].diff(), axis=1)[1:],0,0) for kpt in trc_data.columns.unique()[1:]], axis=0)
1108
+ speeds_kpts = np.array([np.insert(np.linalg.norm(trc_data[kpt].diff(), axis=1)[1:],0,0)
1109
+ for kpt in trc_data.columns.unique()[1:]]).T
1110
+ av_speeds = np.array([np.nanmean(speed_kpt) if not np.isnan(speed_kpt).all() else 0 for speed_kpt in speeds_kpts])
817
1111
  trc_data = trc_data[av_speeds>tot_speed_above]
818
1112
 
819
1113
  # Retrieve zero-speed coordinates for the foot
@@ -846,13 +1140,13 @@ def compute_floor_line(trc_data, keypoint_names = ['LBigToe', 'RBigToe'], toe_sp
846
1140
  return angle, xy_origin, gait_direction
847
1141
 
848
1142
 
849
- def convert_px_to_meters(Q_coords_kpt, px_to_m_person_height_m, height_px, cx, cy, floor_angle, visible_side='none'):
1143
+ def convert_px_to_meters(Q_coords_kpt, first_person_height, height_px, cx, cy, floor_angle, visible_side='none'):
850
1144
  '''
851
1145
  Convert pixel coordinates to meters.
852
1146
 
853
1147
  INPUTS:
854
1148
  - Q_coords_kpt: pd.DataFrame. The xyz coordinates of a keypoint in pixels, with z filled with zeros
855
- - px_to_m_person_height_m: float. The height of the person in meters
1149
+ - first_person_height: float. The height of the person in meters
856
1150
  - height_px: float. The height of the person in pixels
857
1151
  - cx, cy: float. The origin of the image in pixels
858
1152
  - floor_angle: float. The angle of the floor in radians
@@ -865,11 +1159,11 @@ def convert_px_to_meters(Q_coords_kpt, px_to_m_person_height_m, height_px, cx, c
865
1159
  u = Q_coords_kpt.iloc[:,0]
866
1160
  v = Q_coords_kpt.iloc[:,1]
867
1161
 
868
- X = px_to_m_person_height_m / height_px * ((u-cx) + (v-cy)*np.sin(floor_angle))
869
- Y = - px_to_m_person_height_m / height_px * np.cos(floor_angle) * (v-cy - np.tan(floor_angle)*(u-cx))
1162
+ X = first_person_height / height_px * ((u-cx) + (v-cy)*np.sin(floor_angle))
1163
+ Y = - first_person_height / height_px * np.cos(floor_angle) * (v-cy - np.tan(floor_angle)*(u-cx))
870
1164
 
871
- if 'marker_Z_positions' in globals() and visible_side!='none':
872
- marker_name = Q_coords_kpt.columns[0]
1165
+ marker_name = Q_coords_kpt.columns[0]
1166
+ if 'marker_Z_positions' in globals() and visible_side!='none' and marker_name in marker_Z_positions[visible_side].keys():
873
1167
  Z = X.copy()
874
1168
  Z[:] = marker_Z_positions[visible_side][marker_name]
875
1169
  else:
@@ -925,28 +1219,42 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
925
1219
  '''
926
1220
 
927
1221
  # Base parameters
928
- video_dir = Path(config_dict.get('project').get('video_dir'))
929
- px_to_m_from_person_id = int(config_dict.get('project').get('px_to_m_from_person_id'))
930
- px_to_m_person_height_m = config_dict.get('project').get('px_to_m_person_height')
931
- visible_side = config_dict.get('project').get('visible_side')
1222
+ video_dir = Path(config_dict.get('base').get('video_dir'))
1223
+
1224
+ nb_persons_to_detect = config_dict.get('base').get('nb_persons_to_detect')
1225
+ if nb_persons_to_detect != 'all':
1226
+ try:
1227
+ nb_persons_to_detect = int(nb_persons_to_detect)
1228
+ if nb_persons_to_detect < 1:
1229
+ logging.warning('nb_persons_to_detect must be "all" or > 1. Detecting all persons instead.')
1230
+ nb_persons_to_detect = 'all'
1231
+ except:
1232
+ logging.warning('nb_persons_to_detect must be "all" or an integer. Detecting all persons instead.')
1233
+ nb_persons_to_detect = 'all'
1234
+
1235
+ person_ordering_method = config_dict.get('base').get('person_ordering_method')
1236
+
1237
+ first_person_height = config_dict.get('base').get('first_person_height')
1238
+ visible_side = config_dict.get('base').get('visible_side')
932
1239
  if isinstance(visible_side, str): visible_side = [visible_side]
1240
+
933
1241
  # Pose from file
934
- load_trc_px = config_dict.get('project').get('load_trc_px')
1242
+ load_trc_px = config_dict.get('base').get('load_trc_px')
935
1243
  if load_trc_px == '': load_trc_px = None
936
1244
  else: load_trc_px = Path(load_trc_px).resolve()
937
- compare = config_dict.get('project').get('compare')
1245
+ compare = config_dict.get('base').get('compare')
1246
+
938
1247
  # Webcam settings
939
- webcam_id = config_dict.get('project').get('webcam_id')
940
- input_size = config_dict.get('project').get('input_size')
941
-
942
- # Process settings
943
- multiperson = config_dict.get('process').get('multiperson')
944
- show_realtime_results = config_dict.get('process').get('show_realtime_results')
945
- save_vid = config_dict.get('process').get('save_vid')
946
- save_img = config_dict.get('process').get('save_img')
947
- save_pose = config_dict.get('process').get('save_pose')
948
- calculate_angles = config_dict.get('process').get('calculate_angles')
949
- save_angles = config_dict.get('process').get('save_angles')
1248
+ webcam_id = config_dict.get('base').get('webcam_id')
1249
+ input_size = config_dict.get('base').get('input_size')
1250
+
1251
+ # Output settings
1252
+ show_realtime_results = config_dict.get('base').get('show_realtime_results')
1253
+ save_vid = config_dict.get('base').get('save_vid')
1254
+ save_img = config_dict.get('base').get('save_img')
1255
+ save_pose = config_dict.get('base').get('save_pose')
1256
+ calculate_angles = config_dict.get('base').get('calculate_angles')
1257
+ save_angles = config_dict.get('base').get('save_angles')
950
1258
 
951
1259
  # Pose_advanced settings
952
1260
  slowmo_factor = config_dict.get('pose').get('slowmo_factor')
@@ -1131,9 +1439,10 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1131
1439
  logging.info(f'\nUsing a pose file instead of running pose estimation and tracking: {load_trc_px}.')
1132
1440
  # Load pose file in px
1133
1441
  Q_coords, _, time_col, keypoints_names, _ = read_trc(load_trc_px)
1134
-
1442
+ t0 = time_col[0]
1135
1443
  keypoints_ids = [i for i in range(len(keypoints_names))]
1136
1444
  keypoints_all, scores_all = load_pose_file(Q_coords)
1445
+
1137
1446
  for pre, _, node in RenderTree(pose_model):
1138
1447
  if node.name in keypoints_names:
1139
1448
  node.id = keypoints_names.index(node.name)
@@ -1147,6 +1456,7 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1147
1456
  # Retrieve keypoint names from model
1148
1457
  keypoints_ids = [node.id for _, _, node in RenderTree(pose_model) if node.id!=None]
1149
1458
  keypoints_names = [node.name for _, _, node in RenderTree(pose_model) if node.id!=None]
1459
+ t0 = 0
1150
1460
 
1151
1461
  # Set up pose tracker
1152
1462
  try:
@@ -1160,8 +1470,9 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1160
1470
  tracking_mode = 'sports2d'
1161
1471
  logging.info(f'\nPose tracking set up for "{pose_model_name}" model.')
1162
1472
  logging.info(f'Mode: {mode}.\n')
1163
- 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}.')
1473
+ logging.info(f'Persons are detected every {det_frequency} frames and tracked inbetween. Tracking is done with {tracking_mode}.')
1164
1474
  if tracking_mode == 'deepsort': logging.info(f'Deepsort parameters: {deepsort_params}.')
1475
+ logging.info(f'{"All persons are" if nb_persons_to_detect=="all" else f"{nb_persons_to_detect} persons are" if nb_persons_to_detect>1 else "1 person is"} analyzed. Person ordering method is {person_ordering_method}.')
1165
1476
  logging.info(f"{keypoint_likelihood_threshold=}, {average_likelihood_threshold=}, {keypoint_number_threshold=}")
1166
1477
 
1167
1478
  if flip_left_right:
@@ -1183,15 +1494,18 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1183
1494
  logging.warning(f"Skipping {ang_name} angle computation because at least one of the following keypoints is not provided by the model: {ang_params[0]}.")
1184
1495
 
1185
1496
 
1497
+ # ====================================================
1186
1498
  # Process video or webcam feed
1499
+ # ====================================================
1187
1500
  logging.info(f"\nProcessing video stream...")
1188
1501
  # logging.info(f"{'Video, ' if save_vid else ''}{'Images, ' if save_img else ''}{'Pose, ' if save_pose else ''}{'Angles ' if save_angles else ''}{'and ' if save_angles or save_img or save_pose or save_vid else ''}Logs will be saved in {result_dir}.")
1189
- all_frames_X, all_frames_Y, all_frames_scores, all_frames_angles = [], [], [], []
1502
+ all_frames_X, all_frames_X_flipped, all_frames_Y, all_frames_scores, all_frames_angles = [], [], [], [], []
1190
1503
  frame_processing_times = []
1191
1504
  frame_count = 0
1505
+ frames = []
1192
1506
  while cap.isOpened():
1193
1507
  # Skip to the starting frame
1194
- if frame_count < frame_range[0] and not load_trc_px:
1508
+ if frame_count <= int(t0 * fps) or frame_count < frame_range[0]:
1195
1509
  cap.read()
1196
1510
  frame_count += 1
1197
1511
  continue
@@ -1212,6 +1526,8 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1212
1526
  all_frames_angles.append([])
1213
1527
  continue
1214
1528
  else:
1529
+ frames.append(frame.copy())
1530
+
1215
1531
  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)
1216
1532
  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)
1217
1533
 
@@ -1232,7 +1548,7 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1232
1548
  if tracking_mode == 'sports2d':
1233
1549
  if 'prev_keypoints' not in locals(): prev_keypoints = keypoints
1234
1550
  prev_keypoints, keypoints, scores = sort_people_sports2d(prev_keypoints, keypoints, scores=scores)
1235
-
1551
+
1236
1552
 
1237
1553
  # Process coordinates and compute angles
1238
1554
  valid_X, valid_Y, valid_scores = [], [], []
@@ -1256,26 +1572,24 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1256
1572
  person_Y = np.full_like(person_Y, np.nan)
1257
1573
  person_scores = np.full_like(person_scores, np.nan)
1258
1574
 
1575
+ # Check whether the person is looking to the left or right
1576
+ if flip_left_right:
1577
+ person_X_flipped = flip_left_right_direction(person_X, L_R_direction_idx, keypoints_names, keypoints_ids)
1578
+ else:
1579
+ person_X_flipped = person_X.copy()
1580
+
1581
+ # Add Neck and Hip if not provided
1582
+ new_keypoints_names, new_keypoints_ids = keypoints_names.copy(), keypoints_ids.copy()
1583
+ for kpt in ['Hip', 'Neck']:
1584
+ if kpt not in new_keypoints_names:
1585
+ person_X_flipped, person_Y, person_scores = add_neck_hip_coords(kpt, person_X_flipped, person_Y, person_scores, new_keypoints_ids, new_keypoints_names)
1586
+ person_X, _, _ = add_neck_hip_coords(kpt, person_X, person_Y, person_scores, new_keypoints_ids, new_keypoints_names)
1587
+ new_keypoints_names.append(kpt)
1588
+ new_keypoints_ids.append(len(person_X_flipped)-1)
1259
1589
 
1260
1590
  # Compute angles
1261
1591
  if calculate_angles:
1262
- # Check whether the person is looking to the left or right
1263
- if flip_left_right:
1264
- person_X_flipped = flip_left_right_direction(person_X, L_R_direction_idx, keypoints_names, keypoints_ids)
1265
- else:
1266
- person_X_flipped = person_X.copy()
1267
-
1268
- # Compute angles
1269
1592
  person_angles = []
1270
- # Add Neck and Hip if not provided
1271
- new_keypoints_names, new_keypoints_ids = keypoints_names.copy(), keypoints_ids.copy()
1272
- for kpt in ['Neck', 'Hip']:
1273
- if kpt not in new_keypoints_names:
1274
- person_X_flipped, person_Y, person_scores = add_neck_hip_coords(kpt, person_X_flipped, person_Y, person_scores, new_keypoints_ids, new_keypoints_names)
1275
- person_X, _, _ = add_neck_hip_coords(kpt, person_X, person_Y, person_scores, new_keypoints_ids, new_keypoints_names)
1276
- new_keypoints_names.append(kpt)
1277
- new_keypoints_ids.append(len(person_X_flipped)-1)
1278
-
1279
1593
  for ang_name in angle_names:
1280
1594
  ang_params = angle_dict.get(ang_name)
1281
1595
  kpts = ang_params[0]
@@ -1291,24 +1605,19 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1291
1605
  valid_scores.append(person_scores)
1292
1606
 
1293
1607
  # Draw keypoints and skeleton
1294
- if show_realtime_results or save_vid or save_img:
1608
+ if show_realtime_results:
1295
1609
  img = frame.copy()
1296
1610
  img = draw_bounding_box(img, valid_X, valid_Y, colors=colors, fontSize=fontSize, thickness=thickness)
1297
1611
  img = draw_keypts(img, valid_X, valid_Y, valid_scores, cmap_str='RdYlGn')
1298
1612
  img = draw_skel(img, valid_X, valid_Y, pose_model)
1299
1613
  if calculate_angles:
1300
1614
  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)
1301
-
1302
- if show_realtime_results:
1303
- cv2.imshow(f'{video_file} Sports2D', img)
1304
- if (cv2.waitKey(1) & 0xFF) == ord('q') or (cv2.waitKey(1) & 0xFF) == 27:
1305
- break
1306
- if save_vid:
1307
- out_vid.write(img)
1308
- if save_img:
1309
- cv2.imwrite(str((img_output_dir / f'{output_dir_name}_{(frame_count-1):06d}.png')), img)
1615
+ cv2.imshow(f'{video_file} Sports2D', img)
1616
+ if (cv2.waitKey(1) & 0xFF) == ord('q') or (cv2.waitKey(1) & 0xFF) == 27:
1617
+ break
1310
1618
 
1311
1619
  all_frames_X.append(np.array(valid_X))
1620
+ all_frames_X_flipped.append(np.array(valid_X_flipped))
1312
1621
  all_frames_Y.append(np.array(valid_Y))
1313
1622
  all_frames_scores.append(np.array(valid_scores))
1314
1623
 
@@ -1318,64 +1627,92 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1318
1627
  elapsed_time = (datetime.now() - start_time).total_seconds()
1319
1628
  frame_processing_times.append(elapsed_time)
1320
1629
 
1321
-
1322
1630
  # End of the video is reached
1323
1631
  cap.release()
1324
1632
  logging.info(f"Video processing completed.")
1325
1633
  if save_vid:
1326
1634
  out_vid.release()
1327
- if video_file == 'webcam':
1328
- actual_framerate = len(frame_processing_times) / sum(frame_processing_times)
1329
- logging.info(f"Rewriting webcam video based on the averate framerate {actual_framerate}.")
1330
- resample_video(vid_output_path, fps, actual_framerate)
1331
- fps = actual_framerate
1332
- logging.info(f"Processed video saved to {vid_output_path.resolve()}.")
1333
- if save_img:
1334
- logging.info(f"Processed images saved to {img_output_dir.resolve()}.")
1335
1635
  if show_realtime_results:
1336
1636
  cv2.destroyAllWindows()
1337
-
1338
1637
 
1339
- # Post-processing: Interpolate, filter, and save pose and angles
1638
+
1639
+ # ====================================================
1640
+ # Post-processing: Select persons, Interpolate, filter, and save pose and angles
1641
+ # ====================================================
1340
1642
  all_frames_X_homog = make_homogeneous(all_frames_X)
1341
- all_frames_X_homog = all_frames_X_homog[...,keypoints_ids]
1643
+ all_frames_X_homog = all_frames_X_homog[...,new_keypoints_ids]
1644
+ all_frames_X_flipped_homog = make_homogeneous(all_frames_X_flipped)
1645
+ all_frames_X_flipped_homog = all_frames_X_flipped_homog[...,new_keypoints_ids]
1342
1646
  all_frames_Y_homog = make_homogeneous(all_frames_Y)
1343
- all_frames_Y_homog = all_frames_Y_homog[...,keypoints_ids]
1344
- all_frames_Z_homog = pd.DataFrame(np.zeros_like(all_frames_X_homog)[:,0,:], columns=keypoints_names)
1345
- all_frames_scores = make_homogeneous(all_frames_scores)
1647
+ all_frames_Y_homog = all_frames_Y_homog[...,new_keypoints_ids]
1648
+ all_frames_Z_homog = pd.DataFrame(np.zeros_like(all_frames_X_homog)[:,0,:], columns=new_keypoints_names)
1649
+ all_frames_scores_homog = make_homogeneous(all_frames_scores)
1650
+ all_frames_scores_homog = all_frames_scores_homog[...,new_keypoints_ids]
1651
+ all_frames_angles_homog = make_homogeneous(all_frames_angles)
1346
1652
 
1347
1653
  frame_range = [0,frame_count] if video_file == 'webcam' else frame_range
1348
- if not load_trc_px:
1349
- all_frames_time = pd.Series(np.linspace(frame_range[0]/fps, frame_range[1]/fps, frame_count-frame_range[0]+1), name='time')
1350
- else:
1654
+ if load_trc_px:
1351
1655
  all_frames_time = time_col
1352
- if not multiperson:
1353
- px_to_m_from_person_id = get_personID_with_highest_scores(all_frames_scores)
1354
- detected_persons = [px_to_m_from_person_id]
1656
+ selected_persons = [0]
1355
1657
  else:
1356
- detected_persons = range(all_frames_X_homog.shape[1])
1658
+ # Select persons
1659
+ all_frames_time = pd.Series(np.linspace(frame_range[0]/fps, frame_range[1]/fps, frame_count-frame_range[0]), name='time')
1660
+ nb_detected_persons = all_frames_scores_homog.shape[1]
1661
+ if nb_persons_to_detect == 'all':
1662
+ nb_persons_to_detect = all_frames_scores_homog.shape[1]
1663
+ if nb_detected_persons < nb_persons_to_detect:
1664
+ logging.warning(f'Less than the {nb_persons_to_detect} required persons were detected. Analyzing all {nb_detected_persons} persons.')
1665
+ nb_persons_to_detect = nb_detected_persons
1666
+
1667
+ if person_ordering_method == 'on_click':
1668
+ selected_persons = get_personIDs_on_click(frames, all_frames_X_homog, all_frames_Y_homog)
1669
+ if len(selected_persons) == 0:
1670
+ logging.warning('No persons selected. Analyzing all detected persons.')
1671
+ selected_persons = list(range(nb_detected_persons))
1672
+ if len(selected_persons) != nb_persons_to_detect:
1673
+ logging.warning(f'You selected more (or less) than the required {nb_persons_to_detect} persons. "nb_persons_to_detect" will be set to {len(selected_persons)}.')
1674
+ nb_persons_to_detect = len(selected_persons)
1675
+ elif person_ordering_method == 'highest_likelihood':
1676
+ selected_persons = get_personIDs_with_highest_scores(all_frames_scores_homog, nb_persons_to_detect)
1677
+ elif person_ordering_method == 'first_detected':
1678
+ selected_persons = get_personIDs_in_detection_order(nb_persons_to_detect)
1679
+ elif person_ordering_method == 'last_detected':
1680
+ selected_persons = get_personIDs_in_detection_order(nb_persons_to_detect, reverse=True)
1681
+ elif person_ordering_method == 'greatest_displacement':
1682
+ selected_persons = get_personIDs_with_greatest_displacement(all_frames_X_homog, all_frames_Y_homog, nb_persons_to_detect=nb_persons_to_detect, horizontal=True)
1683
+ elif person_ordering_method == 'least_displacement':
1684
+ selected_persons = get_personIDs_with_greatest_displacement(all_frames_X_homog, all_frames_Y_homog, nb_persons_to_detect=nb_persons_to_detect, reverse=True, horizontal=True)
1685
+ else:
1686
+ raise ValueError(f"Invalid person_ordering_method: {person_ordering_method}. Must be 'on_click', 'highest_likelihood', 'greatest_displacement', 'first_detected', or 'last_detected'.")
1687
+ logging.info(f'Reordered persons: IDs of persons {selected_persons} become {list(range(len(selected_persons)))}.')
1688
+
1357
1689
 
1690
+ # ====================================================
1358
1691
  # Post-processing pose
1692
+ # ====================================================
1693
+ all_frames_X_processed, all_frames_X_flipped_processed, all_frames_Y_processed, all_frames_scores_processed, all_frames_angles_processed = all_frames_X_homog.copy(), all_frames_X_flipped_homog.copy(), all_frames_Y_homog.copy(), all_frames_scores_homog.copy(), all_frames_angles_homog.copy()
1359
1694
  if save_pose:
1360
1695
  logging.info('\nPost-processing pose:')
1361
-
1362
1696
  # Process pose for each person
1363
- trc_data = []
1364
- trc_data_unfiltered = []
1365
- for i in detected_persons:
1697
+ trc_data, trc_data_unfiltered = [], []
1698
+ for i, idx_person in enumerate(selected_persons):
1366
1699
  pose_path_person = pose_output_path.parent / (pose_output_path.stem + f'_person{i:02d}.trc')
1367
- all_frames_X_person = pd.DataFrame(all_frames_X_homog[:,i,:], columns=keypoints_names)
1368
- all_frames_Y_person = pd.DataFrame(all_frames_Y_homog[:,i,:], columns=keypoints_names)
1700
+ all_frames_X_person = pd.DataFrame(all_frames_X_homog[:,idx_person,:], columns=new_keypoints_names)
1701
+ all_frames_X_flipped_person = pd.DataFrame(all_frames_X_flipped_homog[:,idx_person,:], columns=new_keypoints_names)
1702
+ all_frames_Y_person = pd.DataFrame(all_frames_Y_homog[:,idx_person,:], columns=new_keypoints_names)
1369
1703
 
1370
- # Delete person if less than 4 valid frames
1704
+ # Delete person if less than 10 valid frames
1371
1705
  pose_nan_count = len(np.where(all_frames_X_person.sum(axis=1)==0)[0])
1372
- if frame_count - frame_range[0] - pose_nan_count <= 4:
1373
- trc_data_i = pd.DataFrame(0, index=all_frames_X_person.index, columns=np.array([[c]*3 for c in all_frames_X_person.columns]).flatten())
1374
- trc_data_i.insert(0, 't', all_frames_time)
1706
+ if frame_count - frame_range[0] - pose_nan_count <= 10:
1707
+ all_frames_X_processed[:,idx_person,:], all_frames_X_flipped_processed[:,idx_person,:], all_frames_Y_processed[:,idx_person,:] = np.nan, np.nan, np.nan
1708
+ columns=np.array([[c]*3 for c in all_frames_X_person.columns]).flatten()
1709
+ trc_data_i = pd.DataFrame(0, index=all_frames_X_person.index, columns=['t']+list(columns))
1710
+ trc_data_i['t'] = all_frames_time
1375
1711
  trc_data.append(trc_data_i)
1376
1712
  trc_data_unfiltered_i = trc_data_i.copy()
1377
1713
  trc_data_unfiltered.append(trc_data_unfiltered_i)
1378
- logging.info(f'- Person {i}: Less than 4 valid frames. Deleting person.')
1714
+
1715
+ logging.info(f'- Person {i}: Less than 10 valid frames. Deleting person.')
1379
1716
 
1380
1717
  else:
1381
1718
  # Interpolate
@@ -1429,20 +1766,21 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1429
1766
  logging.info(f'Pose in pixels saved to {pose_path_person.resolve()}.')
1430
1767
 
1431
1768
  # Plotting coordinates before and after interpolation and filtering
1432
- trc_data_unfiltered_i = pd.concat([pd.concat([all_frames_X_person.iloc[:,kpt], all_frames_Y_person.iloc[:,kpt], all_frames_Z_homog.iloc[:,kpt]], axis=1) for kpt in range(len(all_frames_X_person.columns))], axis=1)
1433
- trc_data_unfiltered_i.insert(0, 't', all_frames_time)
1769
+ columns_to_concat = []
1770
+ for kpt in range(len(all_frames_X_person.columns)):
1771
+ columns_to_concat.extend([all_frames_X_person.iloc[:,kpt], all_frames_Y_person.iloc[:,kpt], all_frames_Z_homog.iloc[:,kpt]])
1772
+ trc_data_unfiltered_i = pd.concat([all_frames_time] + columns_to_concat, axis=1)
1434
1773
  trc_data_unfiltered.append(trc_data_unfiltered_i)
1435
1774
  if show_plots and not to_meters:
1436
1775
  pose_plots(trc_data_unfiltered_i, trc_data_i, i)
1437
-
1776
+
1777
+ all_frames_X_processed[:,idx_person,:], all_frames_X_flipped_processed[:,idx_person,:], all_frames_Y_processed[:,idx_person,:] = all_frames_X_person_filt, all_frames_X_flipped_person, all_frames_Y_person_filt
1778
+
1438
1779
 
1439
1780
  # Convert px to meters
1440
1781
  trc_data_m = []
1441
- if to_meters:
1782
+ if to_meters and save_pose:
1442
1783
  logging.info('\nConverting pose to meters:')
1443
- if px_to_m_from_person_id>=len(trc_data):
1444
- logging.warning(f'Person #{px_to_m_from_person_id} not detected in the video. Calibrating on person #0 instead.')
1445
- px_to_m_from_person_id = 0
1446
1784
  if calib_file:
1447
1785
  logging.info(f'Using calibration file to convert coordinates in meters: {calib_file}.')
1448
1786
  calib_params_dict = retrieve_calib_params(calib_file)
@@ -1450,35 +1788,32 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1450
1788
 
1451
1789
  else:
1452
1790
  # Compute calibration parameters
1453
- if not multiperson:
1454
- selected_person_id = px_to_m_from_person_id
1455
- px_to_m_from_person_id = 0
1456
- height_px = compute_height(trc_data[px_to_m_from_person_id].iloc[:,1:], keypoints_names,
1791
+ height_px = compute_height(trc_data[0].iloc[:,1:], new_keypoints_names,
1457
1792
  fastest_frames_to_remove_percent=fastest_frames_to_remove_percent, close_to_zero_speed=close_to_zero_speed_px, large_hip_knee_angles=large_hip_knee_angles, trimmed_extrema_percent=trimmed_extrema_percent)
1458
1793
 
1459
1794
  toe_speed_below = 1 # m/s (below which the foot is considered to be stationary)
1460
- px_per_m = height_px/px_to_m_person_height_m
1795
+ px_per_m = height_px/first_person_height
1461
1796
  toe_speed_below_px_frame = toe_speed_below * px_per_m / fps
1462
1797
  if floor_angle == 'auto' or xy_origin == 'auto':
1463
1798
  # estimated from the line formed by the toes when they are on the ground (where speed = 0)
1464
- try:
1465
- if all(key in trc_data[px_to_m_from_person_id] for key in ['LBigToe', 'RBigToe']):
1466
- floor_angle_estim, xy_origin_estim, _ = compute_floor_line(trc_data[px_to_m_from_person_id], keypoint_names=['LBigToe', 'RBigToe'], toe_speed_below=toe_speed_below_px_frame)
1799
+ # try:
1800
+ if all(key in trc_data[0] for key in ['LBigToe', 'RBigToe']):
1801
+ floor_angle_estim, xy_origin_estim, _ = compute_floor_line(trc_data[0], keypoint_names=['LBigToe', 'RBigToe'], toe_speed_below=toe_speed_below_px_frame)
1467
1802
  else:
1468
- floor_angle_estim, xy_origin_estim, _ = compute_floor_line(trc_data[px_to_m_from_person_id], keypoint_names=['LAnkle', 'RAnkle'], toe_speed_below=toe_speed_below_px_frame)
1803
+ floor_angle_estim, xy_origin_estim, _ = compute_floor_line(trc_data[0], keypoint_names=['LAnkle', 'RAnkle'], toe_speed_below=toe_speed_below_px_frame)
1469
1804
  xy_origin_estim[0] = xy_origin_estim[0]-0.13
1470
1805
  logging.warning(f'The RBigToe and LBigToe are missing from your model. Using ankles - 13 cm to compute the floor line.')
1471
- except:
1472
- floor_angle_estim = 0
1473
- xy_origin_estim = cam_width/2, cam_height/2
1474
- logging.warning(f'Could not estimate the floor angle and xy_origin for person {px_to_m_from_person_id}. Make sure that the full body is visible. Using floor angle = 0° and xy_origin = [{cam_width/2}, {cam_height/2}] px.')
1806
+ # except:
1807
+ # floor_angle_estim = 0
1808
+ # xy_origin_estim = cam_width/2, cam_height/2
1809
+ # logging.warning(f'Could not estimate the floor angle and xy_origin from person {0}. Make sure that the full body is visible. Using floor angle = 0° and xy_origin = [{cam_width/2}, {cam_height/2}] px.')
1475
1810
  if not floor_angle == 'auto':
1476
1811
  floor_angle_estim = floor_angle
1477
1812
  if xy_origin == 'auto':
1478
1813
  cx, cy = xy_origin_estim
1479
1814
  else:
1480
1815
  cx, cy = xy_origin
1481
- logging.info(f'Using height of person #{px_to_m_from_person_id} ({px_to_m_person_height_m}m) to convert coordinates in meters. '
1816
+ logging.info(f'Using height of person #0 ({first_person_height}m) to convert coordinates in meters. '
1482
1817
  f'Floor angle: {np.degrees(floor_angle_estim) if not floor_angle=="auto" else f"auto (estimation: {round(np.degrees(floor_angle_estim),2)}°)"}, '
1483
1818
  f'xy_origin: {xy_origin if not xy_origin=="auto" else f"auto (estimation: {[round(c) for c in xy_origin_estim]})"} px.')
1484
1819
 
@@ -1495,8 +1830,8 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1495
1830
  else:
1496
1831
  _, _, gait_direction = compute_floor_line(trc_data[i], keypoint_names=['LAnkle', 'RAnkle'], toe_speed_below=toe_speed_below_px_frame)
1497
1832
  logging.warning(f'The RBigToe and LBigToe are missing from your model. Gait direction will be determined from the ankle points.')
1498
- visible_side_i = 'right' if gait_direction > 0.6 \
1499
- else 'left' if gait_direction < -0.6 \
1833
+ visible_side_i = 'right' if gait_direction > 0.3 \
1834
+ else 'left' if gait_direction < -0.3 \
1500
1835
  else 'front'
1501
1836
  logging.info(f'- Person {i}: Seen from the {visible_side_i}.')
1502
1837
  except:
@@ -1509,18 +1844,17 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1509
1844
  logging.info(f'- Person {i}: Seen from the {visible_side_i}.')
1510
1845
 
1511
1846
  # Convert to meters
1512
- trc_data_m_i = pd.concat([convert_px_to_meters(trc_data[i][kpt_name], px_to_m_person_height_m, height_px, cx, cy, -floor_angle_estim, visible_side=visible_side_i) for kpt_name in keypoints_names], axis=1)
1513
- trc_data_m_i.insert(0, 't', all_frames_time)
1514
- trc_data_unfiltered_m_i = pd.concat([convert_px_to_meters(trc_data_unfiltered[i][kpt_name], px_to_m_person_height_m, height_px, cx, cy, -floor_angle_estim) for kpt_name in keypoints_names], axis=1)
1515
- trc_data_unfiltered_m_i.insert(0, 't', all_frames_time)
1847
+ px_to_m_i = [convert_px_to_meters(trc_data[i][kpt_name], first_person_height, height_px, cx, cy, -floor_angle_estim, visible_side=visible_side_i) for kpt_name in new_keypoints_names]
1848
+ trc_data_m_i = pd.concat([all_frames_time.rename('t')]+px_to_m_i, axis=1)
1849
+ px_to_m_unfiltered_i = [convert_px_to_meters(trc_data_unfiltered[i][kpt_name], first_person_height, height_px, cx, cy, -floor_angle_estim) for kpt_name in new_keypoints_names]
1850
+ trc_data_unfiltered_m_i = pd.concat([all_frames_time.rename('t')]+px_to_m_unfiltered_i, axis=1)
1516
1851
 
1517
1852
  if to_meters and show_plots:
1518
1853
  pose_plots(trc_data_unfiltered_m_i, trc_data_m_i, i)
1519
1854
 
1520
1855
  # Write to trc file
1521
1856
  trc_data_m.append(trc_data_m_i)
1522
- idx_path = selected_person_id if not multiperson and not calib_file else i
1523
- pose_path_person_m_i = (pose_output_path.parent / (pose_output_path_m.stem + f'_person{idx_path:02d}.trc'))
1857
+ pose_path_person_m_i = (pose_output_path.parent / (pose_output_path_m.stem + f'_person{i:02d}.trc'))
1524
1858
  make_trc_with_trc_data(trc_data_m_i, pose_path_person_m_i, fps=fps)
1525
1859
  if make_c3d:
1526
1860
  c3d_path = convert_to_c3d(str(pose_path_person_m_i))
@@ -1539,7 +1873,7 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1539
1873
 
1540
1874
 
1541
1875
  # z = 3.0 # distance between the camera and the person. Required in the calibration file but simplified in the equations
1542
- # f = height_px / px_to_m_person_height_m * z
1876
+ # f = height_px / first_person_height * z
1543
1877
 
1544
1878
 
1545
1879
  # # Name
@@ -1570,27 +1904,28 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1570
1904
 
1571
1905
 
1572
1906
 
1573
-
1907
+ # ====================================================
1574
1908
  # Post-processing angles
1909
+ # ====================================================
1575
1910
  if save_angles and calculate_angles:
1576
1911
  logging.info('\nPost-processing angles (without inverse kinematics):')
1577
- all_frames_angles = make_homogeneous(all_frames_angles)
1578
-
1912
+
1579
1913
  # unwrap angles
1580
- # all_frames_angles = np.unwrap(all_frames_angles, axis=0, period=180) # This give all nan values -> need to mask nans
1581
- for i in range(all_frames_angles.shape[1]): # for each person
1582
- for j in range(all_frames_angles.shape[2]): # for each angle
1583
- valid_mask = ~np.isnan(all_frames_angles[:, i, j])
1584
- all_frames_angles[valid_mask, i, j] = np.unwrap(all_frames_angles[valid_mask, i, j], period=180)
1914
+ # all_frames_angles_homog = np.unwrap(all_frames_angles_homog, axis=0, period=180) # This give all nan values -> need to mask nans
1915
+ for i in range(all_frames_angles_homog.shape[1]): # for each person
1916
+ for j in range(all_frames_angles_homog.shape[2]): # for each angle
1917
+ valid_mask = ~np.isnan(all_frames_angles_homog[:, i, j])
1918
+ all_frames_angles_homog[valid_mask, i, j] = np.unwrap(all_frames_angles_homog[valid_mask, i, j], period=180)
1585
1919
 
1586
1920
  # Process angles for each person
1587
- for i in detected_persons:
1921
+ for i, idx_person in enumerate(selected_persons):
1588
1922
  angles_path_person = angles_output_path.parent / (angles_output_path.stem + f'_person{i:02d}.mot')
1589
- all_frames_angles_person = pd.DataFrame(all_frames_angles[:,i,:], columns=angle_names)
1923
+ all_frames_angles_person = pd.DataFrame(all_frames_angles_homog[:,idx_person,:], columns=angle_names)
1590
1924
 
1591
1925
  # Delete person if less than 4 valid frames
1592
1926
  angle_nan_count = len(np.where(all_frames_angles_person.sum(axis=1)==0)[0])
1593
1927
  if frame_count - frame_range[0] - angle_nan_count <= 4:
1928
+ all_frames_angles_processed[:,idx_person,:] = np.nan
1594
1929
  logging.info(f'- Person {i}: Less than 4 valid frames. Deleting person.')
1595
1930
 
1596
1931
  else:
@@ -1631,17 +1966,18 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1631
1966
  logging.info(f'Filtering with {args}')
1632
1967
  all_frames_angles_person_filt = all_frames_angles_person_interp.apply(filter.filter1d, axis=0, args=filter_options)
1633
1968
 
1634
- # Remove columns with all nan values
1635
- all_frames_angles_person_filt.dropna(axis=1, how='all', inplace=True)
1636
- all_frames_angles_person = all_frames_angles_person[all_frames_angles_person_filt.columns]
1637
-
1638
1969
  # Add floor_angle_estim to segment angles
1639
- if correct_segment_angles_with_floor_angle and to_meters:
1970
+ if correct_segment_angles_with_floor_angle and to_meters:
1640
1971
  logging.info(f'Correcting segment angles by removing the {round(np.degrees(floor_angle_estim),2)}° floor angle.')
1641
1972
  for ang_name in all_frames_angles_person_filt.columns:
1642
1973
  if 'horizontal' in angle_dict[ang_name][1]:
1643
1974
  all_frames_angles_person_filt[ang_name] -= np.degrees(floor_angle_estim)
1644
1975
 
1976
+ # Remove columns with all nan values
1977
+ all_frames_angles_processed[:,idx_person,:] = all_frames_angles_person_filt
1978
+ all_frames_angles_person_filt.dropna(axis=1, how='all', inplace=True)
1979
+ all_frames_angles_person = all_frames_angles_person[all_frames_angles_person_filt.columns]
1980
+
1645
1981
  # Build mot file
1646
1982
  angle_data = make_mot_with_angles(all_frames_angles_person_filt, all_frames_time, str(angles_path_person))
1647
1983
  logging.info(f'Angles saved to {angles_path_person.resolve()}.')
@@ -1652,7 +1988,64 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1652
1988
  angle_plots(all_frames_angles_person, angle_data, i) # i = current person
1653
1989
 
1654
1990
 
1991
+ # ====================================================
1992
+ # Save images/video with processed pose and angles
1993
+ # ====================================================
1994
+ if save_vid or save_img:
1995
+ logging.info('\nSaving images of processed pose and angles:')
1996
+ if save_vid:
1997
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
1998
+ out_vid = cv2.VideoWriter(str(vid_output_path.absolute()), fourcc, fps, (cam_width, cam_height))
1999
+
2000
+ # Reorder persons
2001
+ all_frames_X_processed, all_frames_X_flipped_processed, all_frames_Y_processed = all_frames_X_processed[:,selected_persons,:], all_frames_X_flipped_processed[:,selected_persons,:], all_frames_Y_processed[:,selected_persons,:]
2002
+ all_frames_scores_processed = all_frames_scores_processed[:,selected_persons,:]
2003
+ all_frames_angles_processed = all_frames_angles_processed[:,selected_persons,:]
2004
+
2005
+ # Reorder keypoints ids
2006
+ pose_model_with_new_ids = pose_model
2007
+ new_id = 0
2008
+ for node in PreOrderIter(pose_model_with_new_ids):
2009
+ if node.id!=None:
2010
+ node.id = new_id
2011
+ new_id+=1
2012
+ max_id = max(node.id for node in PreOrderIter(pose_model_with_new_ids) if node.id is not None)
2013
+ for node in PreOrderIter(pose_model_with_new_ids):
2014
+ if node.id==None:
2015
+ node.id = max_id+1
2016
+ max_id+=1
2017
+ new_keypoints_ids = list(range(len(new_keypoints_ids)))
2018
+
2019
+ # Draw pose and angles
2020
+ for frame_count, (frame, valid_X, valid_X_flipped, valid_Y, valid_scores, valid_angles) in enumerate(zip(frames, all_frames_X_processed, all_frames_X_flipped_processed, all_frames_Y_processed, all_frames_scores_processed, all_frames_angles_processed)):
2021
+ img = frame.copy()
2022
+ img = draw_bounding_box(img, valid_X, valid_Y, colors=colors, fontSize=fontSize, thickness=thickness)
2023
+ img = draw_keypts(img, valid_X, valid_Y, valid_scores, cmap_str='RdYlGn')
2024
+ img = draw_skel(img, valid_X, valid_Y, pose_model)
2025
+ if calculate_angles:
2026
+ 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)
2027
+
2028
+ # Save video or images
2029
+ if save_vid:
2030
+ out_vid.write(img)
2031
+ if save_img:
2032
+ cv2.imwrite(str((img_output_dir / f'{output_dir_name}_{(frame_count-1):06d}.png')), img)
2033
+
2034
+ if save_vid:
2035
+ out_vid.release()
2036
+ if video_file == 'webcam':
2037
+ actual_framerate = len(frame_processing_times) / sum(frame_processing_times)
2038
+ logging.info(f"Rewriting webcam video based on the averate framerate {actual_framerate}.")
2039
+ resample_video(vid_output_path, fps, actual_framerate)
2040
+ fps = actual_framerate
2041
+ logging.info(f"Processed video saved to {vid_output_path.resolve()}.")
2042
+ if save_img:
2043
+ logging.info(f"Processed images saved to {img_output_dir.resolve()}.")
2044
+
2045
+
2046
+ # ====================================================
1655
2047
  # OpenSim inverse kinematics (and optional marker augmentation)
2048
+ # ====================================================
1656
2049
  if do_ik or use_augmentation:
1657
2050
  import opensim as osim
1658
2051
  logging.info('\nPost-processing angles (with inverse kinematics):')