sports2d 0.7.2__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
@@ -58,6 +58,7 @@ import json
58
58
  import ast
59
59
  import shutil
60
60
  import os
61
+ from importlib.metadata import version
61
62
  from functools import partial
62
63
  from datetime import datetime
63
64
  import itertools as it
@@ -70,13 +71,16 @@ import pandas as pd
70
71
  import cv2
71
72
  import matplotlib as mpl
72
73
  import matplotlib.pyplot as plt
74
+ from matplotlib.widgets import Slider, Button
75
+ from matplotlib import patheffects
76
+
73
77
  from rtmlib import PoseTracker, BodyWithFeet, Wholebody, Body, Custom
74
78
  from deep_sort_realtime.deepsort_tracker import DeepSort
75
- import opensim as osim
76
79
 
77
80
  from Sports2D.Utilities import filter
78
81
  from Sports2D.Utilities.common import *
79
- from Sports2D.Utilities.skeletons import *
82
+ from Pose2Sim.common import *
83
+ from Pose2Sim.skeletons import *
80
84
 
81
85
  DEFAULT_MASS = 70
82
86
  DEFAULT_HEIGHT = 1.7
@@ -86,7 +90,7 @@ __author__ = "David Pagnon, HunMin Kim"
86
90
  __copyright__ = "Copyright 2023, Sports2D"
87
91
  __credits__ = ["David Pagnon"]
88
92
  __license__ = "BSD 3-Clause License"
89
- __version__ = "0.4.0"
93
+ __version__ = version("sports2d")
90
94
  __maintainer__ = "David Pagnon"
91
95
  __email__ = "contact@david-pagnon.com"
92
96
  __status__ = "Development"
@@ -624,8 +628,10 @@ def trc_data_from_XYZtime(X, Y, Z, time):
624
628
  - trc_data: pd.DataFrame. Dataframe of trc data
625
629
  '''
626
630
 
627
- 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)
628
- 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)
629
635
 
630
636
  return trc_data
631
637
 
@@ -776,21 +782,308 @@ def angle_plots(angle_data_unfiltered, angle_data, person_id):
776
782
  pw.show()
777
783
 
778
784
 
779
- def get_personID_with_highest_scores(all_frames_scores):
785
+ def get_personIDs_with_highest_scores(all_frames_scores, nb_persons_to_detect):
780
786
  '''
781
787
  Get the person ID with the highest scores
782
788
 
783
789
  INPUTS:
784
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
785
792
 
786
793
  OUTPUT:
787
- - person_id: int. The person ID with the highest scores
794
+ - selected_persons: list of int. The person IDs with the highest scores
788
795
  '''
789
796
 
790
797
  # Get the person with the highest scores over all frames and all keypoints
791
- 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
792
833
 
793
- 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
794
1087
 
795
1088
 
796
1089
  def compute_floor_line(trc_data, keypoint_names = ['LBigToe', 'RBigToe'], toe_speed_below = 7, tot_speed_above=2.0):
@@ -812,7 +1105,9 @@ def compute_floor_line(trc_data, keypoint_names = ['LBigToe', 'RBigToe'], toe_sp
812
1105
  '''
813
1106
 
814
1107
  # Remove frames where the person is mostly not moving (outlier)
815
- 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])
816
1111
  trc_data = trc_data[av_speeds>tot_speed_above]
817
1112
 
818
1113
  # Retrieve zero-speed coordinates for the foot
@@ -845,13 +1140,13 @@ def compute_floor_line(trc_data, keypoint_names = ['LBigToe', 'RBigToe'], toe_sp
845
1140
  return angle, xy_origin, gait_direction
846
1141
 
847
1142
 
848
- 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'):
849
1144
  '''
850
1145
  Convert pixel coordinates to meters.
851
1146
 
852
1147
  INPUTS:
853
1148
  - Q_coords_kpt: pd.DataFrame. The xyz coordinates of a keypoint in pixels, with z filled with zeros
854
- - 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
855
1150
  - height_px: float. The height of the person in pixels
856
1151
  - cx, cy: float. The origin of the image in pixels
857
1152
  - floor_angle: float. The angle of the floor in radians
@@ -864,11 +1159,11 @@ def convert_px_to_meters(Q_coords_kpt, px_to_m_person_height_m, height_px, cx, c
864
1159
  u = Q_coords_kpt.iloc[:,0]
865
1160
  v = Q_coords_kpt.iloc[:,1]
866
1161
 
867
- X = px_to_m_person_height_m / height_px * ((u-cx) + (v-cy)*np.sin(floor_angle))
868
- 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))
869
1164
 
870
- if 'marker_Z_positions' in globals() and visible_side!='none':
871
- 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():
872
1167
  Z = X.copy()
873
1168
  Z[:] = marker_Z_positions[visible_side][marker_name]
874
1169
  else:
@@ -924,28 +1219,42 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
924
1219
  '''
925
1220
 
926
1221
  # Base parameters
927
- video_dir = Path(config_dict.get('project').get('video_dir'))
928
- px_to_m_from_person_id = int(config_dict.get('project').get('px_to_m_from_person_id'))
929
- px_to_m_person_height_m = config_dict.get('project').get('px_to_m_person_height')
930
- 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')
931
1239
  if isinstance(visible_side, str): visible_side = [visible_side]
1240
+
932
1241
  # Pose from file
933
- load_trc_px = config_dict.get('project').get('load_trc_px')
1242
+ load_trc_px = config_dict.get('base').get('load_trc_px')
934
1243
  if load_trc_px == '': load_trc_px = None
935
1244
  else: load_trc_px = Path(load_trc_px).resolve()
936
- compare = config_dict.get('project').get('compare')
1245
+ compare = config_dict.get('base').get('compare')
1246
+
937
1247
  # Webcam settings
938
- webcam_id = config_dict.get('project').get('webcam_id')
939
- input_size = config_dict.get('project').get('input_size')
940
-
941
- # Process settings
942
- multiperson = config_dict.get('process').get('multiperson')
943
- show_realtime_results = config_dict.get('process').get('show_realtime_results')
944
- save_vid = config_dict.get('process').get('save_vid')
945
- save_img = config_dict.get('process').get('save_img')
946
- save_pose = config_dict.get('process').get('save_pose')
947
- calculate_angles = config_dict.get('process').get('calculate_angles')
948
- 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')
949
1258
 
950
1259
  # Pose_advanced settings
951
1260
  slowmo_factor = config_dict.get('pose').get('slowmo_factor')
@@ -1041,7 +1350,8 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1041
1350
  close_to_zero_speed_px = config_dict.get('kinematics').get('close_to_zero_speed_px')
1042
1351
  close_to_zero_speed_m = config_dict.get('kinematics').get('close_to_zero_speed_m')
1043
1352
  if do_ik:
1044
- from Pose2Sim.markerAugmentation import augment_markers_all
1353
+ if use_augmentation:
1354
+ from Pose2Sim.markerAugmentation import augment_markers_all
1045
1355
  from Pose2Sim.kinematics import kinematics_all
1046
1356
  # Create a Pose2Sim dictionary and fill in missing keys
1047
1357
  recursivedict = lambda: defaultdict(recursivedict)
@@ -1129,9 +1439,10 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1129
1439
  logging.info(f'\nUsing a pose file instead of running pose estimation and tracking: {load_trc_px}.')
1130
1440
  # Load pose file in px
1131
1441
  Q_coords, _, time_col, keypoints_names, _ = read_trc(load_trc_px)
1132
-
1442
+ t0 = time_col[0]
1133
1443
  keypoints_ids = [i for i in range(len(keypoints_names))]
1134
1444
  keypoints_all, scores_all = load_pose_file(Q_coords)
1445
+
1135
1446
  for pre, _, node in RenderTree(pose_model):
1136
1447
  if node.name in keypoints_names:
1137
1448
  node.id = keypoints_names.index(node.name)
@@ -1145,6 +1456,7 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1145
1456
  # Retrieve keypoint names from model
1146
1457
  keypoints_ids = [node.id for _, _, node in RenderTree(pose_model) if node.id!=None]
1147
1458
  keypoints_names = [node.name for _, _, node in RenderTree(pose_model) if node.id!=None]
1459
+ t0 = 0
1148
1460
 
1149
1461
  # Set up pose tracker
1150
1462
  try:
@@ -1158,8 +1470,9 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1158
1470
  tracking_mode = 'sports2d'
1159
1471
  logging.info(f'\nPose tracking set up for "{pose_model_name}" model.')
1160
1472
  logging.info(f'Mode: {mode}.\n')
1161
- 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}.')
1162
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}.')
1163
1476
  logging.info(f"{keypoint_likelihood_threshold=}, {average_likelihood_threshold=}, {keypoint_number_threshold=}")
1164
1477
 
1165
1478
  if flip_left_right:
@@ -1181,15 +1494,18 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1181
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]}.")
1182
1495
 
1183
1496
 
1497
+ # ====================================================
1184
1498
  # Process video or webcam feed
1499
+ # ====================================================
1185
1500
  logging.info(f"\nProcessing video stream...")
1186
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}.")
1187
- 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 = [], [], [], [], []
1188
1503
  frame_processing_times = []
1189
1504
  frame_count = 0
1505
+ frames = []
1190
1506
  while cap.isOpened():
1191
1507
  # Skip to the starting frame
1192
- if frame_count < frame_range[0] and not load_trc_px:
1508
+ if frame_count <= int(t0 * fps) or frame_count < frame_range[0]:
1193
1509
  cap.read()
1194
1510
  frame_count += 1
1195
1511
  continue
@@ -1210,6 +1526,8 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1210
1526
  all_frames_angles.append([])
1211
1527
  continue
1212
1528
  else:
1529
+ frames.append(frame.copy())
1530
+
1213
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)
1214
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)
1215
1533
 
@@ -1230,7 +1548,7 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1230
1548
  if tracking_mode == 'sports2d':
1231
1549
  if 'prev_keypoints' not in locals(): prev_keypoints = keypoints
1232
1550
  prev_keypoints, keypoints, scores = sort_people_sports2d(prev_keypoints, keypoints, scores=scores)
1233
-
1551
+
1234
1552
 
1235
1553
  # Process coordinates and compute angles
1236
1554
  valid_X, valid_Y, valid_scores = [], [], []
@@ -1254,26 +1572,24 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1254
1572
  person_Y = np.full_like(person_Y, np.nan)
1255
1573
  person_scores = np.full_like(person_scores, np.nan)
1256
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)
1257
1589
 
1258
1590
  # Compute angles
1259
1591
  if calculate_angles:
1260
- # Check whether the person is looking to the left or right
1261
- if flip_left_right:
1262
- person_X_flipped = flip_left_right_direction(person_X, L_R_direction_idx, keypoints_names, keypoints_ids)
1263
- else:
1264
- person_X_flipped = person_X.copy()
1265
-
1266
- # Compute angles
1267
1592
  person_angles = []
1268
- # Add Neck and Hip if not provided
1269
- new_keypoints_names, new_keypoints_ids = keypoints_names.copy(), keypoints_ids.copy()
1270
- for kpt in ['Neck', 'Hip']:
1271
- if kpt not in new_keypoints_names:
1272
- 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)
1273
- person_X, _, _ = add_neck_hip_coords(kpt, person_X, person_Y, person_scores, new_keypoints_ids, new_keypoints_names)
1274
- new_keypoints_names.append(kpt)
1275
- new_keypoints_ids.append(len(person_X_flipped)-1)
1276
-
1277
1593
  for ang_name in angle_names:
1278
1594
  ang_params = angle_dict.get(ang_name)
1279
1595
  kpts = ang_params[0]
@@ -1289,24 +1605,19 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1289
1605
  valid_scores.append(person_scores)
1290
1606
 
1291
1607
  # Draw keypoints and skeleton
1292
- if show_realtime_results or save_vid or save_img:
1608
+ if show_realtime_results:
1293
1609
  img = frame.copy()
1294
1610
  img = draw_bounding_box(img, valid_X, valid_Y, colors=colors, fontSize=fontSize, thickness=thickness)
1295
1611
  img = draw_keypts(img, valid_X, valid_Y, valid_scores, cmap_str='RdYlGn')
1296
1612
  img = draw_skel(img, valid_X, valid_Y, pose_model)
1297
1613
  if calculate_angles:
1298
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)
1299
-
1300
- if show_realtime_results:
1301
- cv2.imshow(f'{video_file} Sports2D', img)
1302
- if (cv2.waitKey(1) & 0xFF) == ord('q') or (cv2.waitKey(1) & 0xFF) == 27:
1303
- break
1304
- if save_vid:
1305
- out_vid.write(img)
1306
- if save_img:
1307
- 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
1308
1618
 
1309
1619
  all_frames_X.append(np.array(valid_X))
1620
+ all_frames_X_flipped.append(np.array(valid_X_flipped))
1310
1621
  all_frames_Y.append(np.array(valid_Y))
1311
1622
  all_frames_scores.append(np.array(valid_scores))
1312
1623
 
@@ -1316,64 +1627,92 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1316
1627
  elapsed_time = (datetime.now() - start_time).total_seconds()
1317
1628
  frame_processing_times.append(elapsed_time)
1318
1629
 
1319
-
1320
1630
  # End of the video is reached
1321
1631
  cap.release()
1322
1632
  logging.info(f"Video processing completed.")
1323
1633
  if save_vid:
1324
1634
  out_vid.release()
1325
- if video_file == 'webcam':
1326
- actual_framerate = len(frame_processing_times) / sum(frame_processing_times)
1327
- logging.info(f"Rewriting webcam video based on the averate framerate {actual_framerate}.")
1328
- resample_video(vid_output_path, fps, actual_framerate)
1329
- fps = actual_framerate
1330
- logging.info(f"Processed video saved to {vid_output_path.resolve()}.")
1331
- if save_img:
1332
- logging.info(f"Processed images saved to {img_output_dir.resolve()}.")
1333
1635
  if show_realtime_results:
1334
1636
  cv2.destroyAllWindows()
1335
-
1336
1637
 
1337
- # Post-processing: Interpolate, filter, and save pose and angles
1638
+
1639
+ # ====================================================
1640
+ # Post-processing: Select persons, Interpolate, filter, and save pose and angles
1641
+ # ====================================================
1338
1642
  all_frames_X_homog = make_homogeneous(all_frames_X)
1339
- 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]
1340
1646
  all_frames_Y_homog = make_homogeneous(all_frames_Y)
1341
- all_frames_Y_homog = all_frames_Y_homog[...,keypoints_ids]
1342
- all_frames_Z_homog = pd.DataFrame(np.zeros_like(all_frames_X_homog)[:,0,:], columns=keypoints_names)
1343
- 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)
1344
1652
 
1345
1653
  frame_range = [0,frame_count] if video_file == 'webcam' else frame_range
1346
- if not load_trc_px:
1347
- all_frames_time = pd.Series(np.linspace(frame_range[0]/fps, frame_range[1]/fps, frame_count-frame_range[0]+1), name='time')
1348
- else:
1654
+ if load_trc_px:
1349
1655
  all_frames_time = time_col
1350
- if not multiperson:
1351
- px_to_m_from_person_id = get_personID_with_highest_scores(all_frames_scores)
1352
- detected_persons = [px_to_m_from_person_id]
1656
+ selected_persons = [0]
1353
1657
  else:
1354
- 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
+
1355
1689
 
1690
+ # ====================================================
1356
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()
1357
1694
  if save_pose:
1358
1695
  logging.info('\nPost-processing pose:')
1359
-
1360
1696
  # Process pose for each person
1361
- trc_data = []
1362
- trc_data_unfiltered = []
1363
- for i in detected_persons:
1697
+ trc_data, trc_data_unfiltered = [], []
1698
+ for i, idx_person in enumerate(selected_persons):
1364
1699
  pose_path_person = pose_output_path.parent / (pose_output_path.stem + f'_person{i:02d}.trc')
1365
- all_frames_X_person = pd.DataFrame(all_frames_X_homog[:,i,:], columns=keypoints_names)
1366
- 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)
1367
1703
 
1368
- # Delete person if less than 4 valid frames
1704
+ # Delete person if less than 10 valid frames
1369
1705
  pose_nan_count = len(np.where(all_frames_X_person.sum(axis=1)==0)[0])
1370
- if frame_count - frame_range[0] - pose_nan_count <= 4:
1371
- 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())
1372
- 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
1373
1711
  trc_data.append(trc_data_i)
1374
1712
  trc_data_unfiltered_i = trc_data_i.copy()
1375
1713
  trc_data_unfiltered.append(trc_data_unfiltered_i)
1376
- 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.')
1377
1716
 
1378
1717
  else:
1379
1718
  # Interpolate
@@ -1427,20 +1766,21 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1427
1766
  logging.info(f'Pose in pixels saved to {pose_path_person.resolve()}.')
1428
1767
 
1429
1768
  # Plotting coordinates before and after interpolation and filtering
1430
- 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)
1431
- 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)
1432
1773
  trc_data_unfiltered.append(trc_data_unfiltered_i)
1433
1774
  if show_plots and not to_meters:
1434
1775
  pose_plots(trc_data_unfiltered_i, trc_data_i, i)
1435
-
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
+
1436
1779
 
1437
1780
  # Convert px to meters
1438
1781
  trc_data_m = []
1439
- if to_meters:
1782
+ if to_meters and save_pose:
1440
1783
  logging.info('\nConverting pose to meters:')
1441
- if px_to_m_from_person_id>=len(trc_data):
1442
- logging.warning(f'Person #{px_to_m_from_person_id} not detected in the video. Calibrating on person #0 instead.')
1443
- px_to_m_from_person_id = 0
1444
1784
  if calib_file:
1445
1785
  logging.info(f'Using calibration file to convert coordinates in meters: {calib_file}.')
1446
1786
  calib_params_dict = retrieve_calib_params(calib_file)
@@ -1448,35 +1788,32 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1448
1788
 
1449
1789
  else:
1450
1790
  # Compute calibration parameters
1451
- if not multiperson:
1452
- selected_person_id = px_to_m_from_person_id
1453
- px_to_m_from_person_id = 0
1454
- 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,
1455
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)
1456
1793
 
1457
1794
  toe_speed_below = 1 # m/s (below which the foot is considered to be stationary)
1458
- px_per_m = height_px/px_to_m_person_height_m
1795
+ px_per_m = height_px/first_person_height
1459
1796
  toe_speed_below_px_frame = toe_speed_below * px_per_m / fps
1460
1797
  if floor_angle == 'auto' or xy_origin == 'auto':
1461
1798
  # estimated from the line formed by the toes when they are on the ground (where speed = 0)
1462
- try:
1463
- if all(key in trc_data[px_to_m_from_person_id] for key in ['LBigToe', 'RBigToe']):
1464
- 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)
1465
1802
  else:
1466
- 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)
1467
1804
  xy_origin_estim[0] = xy_origin_estim[0]-0.13
1468
1805
  logging.warning(f'The RBigToe and LBigToe are missing from your model. Using ankles - 13 cm to compute the floor line.')
1469
- except:
1470
- floor_angle_estim = 0
1471
- xy_origin_estim = cam_width/2, cam_height/2
1472
- 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.')
1473
1810
  if not floor_angle == 'auto':
1474
1811
  floor_angle_estim = floor_angle
1475
1812
  if xy_origin == 'auto':
1476
1813
  cx, cy = xy_origin_estim
1477
1814
  else:
1478
1815
  cx, cy = xy_origin
1479
- 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. '
1480
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)}°)"}, '
1481
1818
  f'xy_origin: {xy_origin if not xy_origin=="auto" else f"auto (estimation: {[round(c) for c in xy_origin_estim]})"} px.')
1482
1819
 
@@ -1493,8 +1830,8 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1493
1830
  else:
1494
1831
  _, _, gait_direction = compute_floor_line(trc_data[i], keypoint_names=['LAnkle', 'RAnkle'], toe_speed_below=toe_speed_below_px_frame)
1495
1832
  logging.warning(f'The RBigToe and LBigToe are missing from your model. Gait direction will be determined from the ankle points.')
1496
- visible_side_i = 'right' if gait_direction > 0.6 \
1497
- 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 \
1498
1835
  else 'front'
1499
1836
  logging.info(f'- Person {i}: Seen from the {visible_side_i}.')
1500
1837
  except:
@@ -1507,21 +1844,20 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1507
1844
  logging.info(f'- Person {i}: Seen from the {visible_side_i}.')
1508
1845
 
1509
1846
  # Convert to meters
1510
- 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)
1511
- trc_data_m_i.insert(0, 't', all_frames_time)
1512
- 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)
1513
- 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)
1514
1851
 
1515
1852
  if to_meters and show_plots:
1516
1853
  pose_plots(trc_data_unfiltered_m_i, trc_data_m_i, i)
1517
1854
 
1518
1855
  # Write to trc file
1519
1856
  trc_data_m.append(trc_data_m_i)
1520
- idx_path = selected_person_id if not multiperson and not calib_file else i
1521
- 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'))
1522
1858
  make_trc_with_trc_data(trc_data_m_i, pose_path_person_m_i, fps=fps)
1523
1859
  if make_c3d:
1524
- c3d_path = convert_to_c3d(pose_path_person_m_i)
1860
+ c3d_path = convert_to_c3d(str(pose_path_person_m_i))
1525
1861
  logging.info(f'Pose in meters saved to {pose_path_person_m_i.resolve()}. {"Also saved in c3d format." if make_c3d else ""}')
1526
1862
 
1527
1863
 
@@ -1537,7 +1873,7 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1537
1873
 
1538
1874
 
1539
1875
  # z = 3.0 # distance between the camera and the person. Required in the calibration file but simplified in the equations
1540
- # f = height_px / px_to_m_person_height_m * z
1876
+ # f = height_px / first_person_height * z
1541
1877
 
1542
1878
 
1543
1879
  # # Name
@@ -1568,27 +1904,28 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1568
1904
 
1569
1905
 
1570
1906
 
1571
-
1907
+ # ====================================================
1572
1908
  # Post-processing angles
1909
+ # ====================================================
1573
1910
  if save_angles and calculate_angles:
1574
1911
  logging.info('\nPost-processing angles (without inverse kinematics):')
1575
- all_frames_angles = make_homogeneous(all_frames_angles)
1576
-
1912
+
1577
1913
  # unwrap angles
1578
- # all_frames_angles = np.unwrap(all_frames_angles, axis=0, period=180) # This give all nan values -> need to mask nans
1579
- for i in range(all_frames_angles.shape[1]): # for each person
1580
- for j in range(all_frames_angles.shape[2]): # for each angle
1581
- valid_mask = ~np.isnan(all_frames_angles[:, i, j])
1582
- 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)
1583
1919
 
1584
1920
  # Process angles for each person
1585
- for i in detected_persons:
1921
+ for i, idx_person in enumerate(selected_persons):
1586
1922
  angles_path_person = angles_output_path.parent / (angles_output_path.stem + f'_person{i:02d}.mot')
1587
- 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)
1588
1924
 
1589
1925
  # Delete person if less than 4 valid frames
1590
1926
  angle_nan_count = len(np.where(all_frames_angles_person.sum(axis=1)==0)[0])
1591
1927
  if frame_count - frame_range[0] - angle_nan_count <= 4:
1928
+ all_frames_angles_processed[:,idx_person,:] = np.nan
1592
1929
  logging.info(f'- Person {i}: Less than 4 valid frames. Deleting person.')
1593
1930
 
1594
1931
  else:
@@ -1629,17 +1966,18 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1629
1966
  logging.info(f'Filtering with {args}')
1630
1967
  all_frames_angles_person_filt = all_frames_angles_person_interp.apply(filter.filter1d, axis=0, args=filter_options)
1631
1968
 
1632
- # Remove columns with all nan values
1633
- all_frames_angles_person_filt.dropna(axis=1, how='all', inplace=True)
1634
- all_frames_angles_person = all_frames_angles_person[all_frames_angles_person_filt.columns]
1635
-
1636
1969
  # Add floor_angle_estim to segment angles
1637
- if correct_segment_angles_with_floor_angle and to_meters:
1970
+ if correct_segment_angles_with_floor_angle and to_meters:
1638
1971
  logging.info(f'Correcting segment angles by removing the {round(np.degrees(floor_angle_estim),2)}° floor angle.')
1639
1972
  for ang_name in all_frames_angles_person_filt.columns:
1640
1973
  if 'horizontal' in angle_dict[ang_name][1]:
1641
1974
  all_frames_angles_person_filt[ang_name] -= np.degrees(floor_angle_estim)
1642
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
+
1643
1981
  # Build mot file
1644
1982
  angle_data = make_mot_with_angles(all_frames_angles_person_filt, all_frames_time, str(angles_path_person))
1645
1983
  logging.info(f'Angles saved to {angles_path_person.resolve()}.')
@@ -1650,8 +1988,66 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1650
1988
  angle_plots(all_frames_angles_person, angle_data, i) # i = current person
1651
1989
 
1652
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
+ # ====================================================
1653
2047
  # OpenSim inverse kinematics (and optional marker augmentation)
2048
+ # ====================================================
1654
2049
  if do_ik or use_augmentation:
2050
+ import opensim as osim
1655
2051
  logging.info('\nPost-processing angles (with inverse kinematics):')
1656
2052
  if not to_meters:
1657
2053
  logging.warning('Skipping marker augmentation and inverse kinematics as to_meters was set to False.')