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/Demo/Config_demo.toml +12 -13
- Sports2D/Sports2D.py +27 -23
- Sports2D/Utilities/__init__.py +2 -1
- Sports2D/Utilities/common.py +2 -940
- Sports2D/Utilities/filter.py +2 -1
- Sports2D/Utilities/tests.py +40 -21
- Sports2D/__init__.py +2 -1
- Sports2D/process.py +540 -144
- {sports2d-0.7.2.dist-info → sports2d-0.8.0.dist-info}/METADATA +105 -83
- sports2d-0.8.0.dist-info/RECORD +15 -0
- {sports2d-0.7.2.dist-info → sports2d-0.8.0.dist-info}/WHEEL +1 -1
- sports2d-0.8.0.dist-info/entry_points.txt +3 -0
- Sports2D/Utilities/skeletons.py +0 -1000
- sports2d-0.7.2.dist-info/RECORD +0 -16
- sports2d-0.7.2.dist-info/entry_points.txt +0 -2
- {sports2d-0.7.2.dist-info → sports2d-0.8.0.dist-info/licenses}/LICENSE +0 -0
- {sports2d-0.7.2.dist-info → sports2d-0.8.0.dist-info}/top_level.txt +0 -0
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
|
|
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__ = "
|
|
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
|
-
|
|
628
|
-
|
|
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
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
-
|
|
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 =
|
|
868
|
-
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))
|
|
869
1164
|
|
|
870
|
-
|
|
871
|
-
|
|
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('
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
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('
|
|
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('
|
|
1245
|
+
compare = config_dict.get('base').get('compare')
|
|
1246
|
+
|
|
937
1247
|
# Webcam settings
|
|
938
|
-
webcam_id = config_dict.get('
|
|
939
|
-
input_size = config_dict.get('
|
|
940
|
-
|
|
941
|
-
#
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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
|
-
|
|
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.
|
|
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]
|
|
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
|
|
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
|
|
1301
|
-
|
|
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
|
-
|
|
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[...,
|
|
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[...,
|
|
1342
|
-
all_frames_Z_homog = pd.DataFrame(np.zeros_like(all_frames_X_homog)[:,0,:], columns=
|
|
1343
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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[:,
|
|
1366
|
-
|
|
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
|
|
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 <=
|
|
1371
|
-
|
|
1372
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1431
|
-
|
|
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
|
-
|
|
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/
|
|
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[
|
|
1464
|
-
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)
|
|
1465
1802
|
else:
|
|
1466
|
-
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)
|
|
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
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
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 #
|
|
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.
|
|
1497
|
-
else 'left' if gait_direction < -0.
|
|
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
|
-
|
|
1511
|
-
trc_data_m_i.
|
|
1512
|
-
|
|
1513
|
-
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)
|
|
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
|
-
|
|
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 /
|
|
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
|
-
|
|
1576
|
-
|
|
1912
|
+
|
|
1577
1913
|
# unwrap angles
|
|
1578
|
-
#
|
|
1579
|
-
for i in range(
|
|
1580
|
-
for j in range(
|
|
1581
|
-
valid_mask = ~np.isnan(
|
|
1582
|
-
|
|
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
|
|
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(
|
|
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.')
|