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/Demo/Config_demo.toml +9 -10
- Sports2D/Sports2D.py +24 -21
- Sports2D/Utilities/common.py +0 -1
- Sports2D/Utilities/tests.py +38 -20
- Sports2D/process.py +533 -140
- {sports2d-0.7.3.dist-info → sports2d-0.8.0.dist-info}/METADATA +98 -80
- sports2d-0.8.0.dist-info/RECORD +15 -0
- sports2d-0.8.0.dist-info/entry_points.txt +3 -0
- Sports2D/Utilities/skeletons.py +0 -1000
- sports2d-0.7.3.dist-info/RECORD +0 -16
- sports2d-0.7.3.dist-info/entry_points.txt +0 -2
- {sports2d-0.7.3.dist-info → sports2d-0.8.0.dist-info}/WHEEL +0 -0
- {sports2d-0.7.3.dist-info → sports2d-0.8.0.dist-info}/licenses/LICENSE +0 -0
- {sports2d-0.7.3.dist-info → sports2d-0.8.0.dist-info}/top_level.txt +0 -0
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
|
-
|
|
629
|
-
|
|
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
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
-
|
|
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 =
|
|
869
|
-
Y = -
|
|
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
|
-
|
|
872
|
-
|
|
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('
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
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('
|
|
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('
|
|
1245
|
+
compare = config_dict.get('base').get('compare')
|
|
1246
|
+
|
|
938
1247
|
# Webcam settings
|
|
939
|
-
webcam_id = config_dict.get('
|
|
940
|
-
input_size = config_dict.get('
|
|
941
|
-
|
|
942
|
-
#
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
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.
|
|
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]
|
|
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
|
|
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
|
|
1303
|
-
|
|
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
|
-
|
|
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[...,
|
|
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[...,
|
|
1344
|
-
all_frames_Z_homog = pd.DataFrame(np.zeros_like(all_frames_X_homog)[:,0,:], columns=
|
|
1345
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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[:,
|
|
1368
|
-
|
|
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
|
|
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 <=
|
|
1373
|
-
|
|
1374
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1433
|
-
|
|
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
|
-
|
|
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/
|
|
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[
|
|
1466
|
-
floor_angle_estim, xy_origin_estim, _ = compute_floor_line(trc_data[
|
|
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[
|
|
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
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
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 #
|
|
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.
|
|
1499
|
-
else 'left' if gait_direction < -0.
|
|
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
|
-
|
|
1513
|
-
trc_data_m_i.
|
|
1514
|
-
|
|
1515
|
-
trc_data_unfiltered_m_i.
|
|
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
|
-
|
|
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 /
|
|
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
|
-
|
|
1578
|
-
|
|
1912
|
+
|
|
1579
1913
|
# unwrap angles
|
|
1580
|
-
#
|
|
1581
|
-
for i in range(
|
|
1582
|
-
for j in range(
|
|
1583
|
-
valid_mask = ~np.isnan(
|
|
1584
|
-
|
|
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
|
|
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(
|
|
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):')
|