sports2d 0.8.18__py3-none-any.whl → 0.8.20__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 +3 -2
- Sports2D/Sports2D.py +5 -3
- Sports2D/Utilities/tests.py +5 -5
- Sports2D/process.py +258 -155
- {sports2d-0.8.18.dist-info → sports2d-0.8.20.dist-info}/METADATA +12 -10
- {sports2d-0.8.18.dist-info → sports2d-0.8.20.dist-info}/RECORD +10 -10
- {sports2d-0.8.18.dist-info → sports2d-0.8.20.dist-info}/WHEEL +0 -0
- {sports2d-0.8.18.dist-info → sports2d-0.8.20.dist-info}/entry_points.txt +0 -0
- {sports2d-0.8.18.dist-info → sports2d-0.8.20.dist-info}/licenses/LICENSE +0 -0
- {sports2d-0.8.18.dist-info → sports2d-0.8.20.dist-info}/top_level.txt +0 -0
Sports2D/Demo/Config_demo.toml
CHANGED
|
@@ -60,8 +60,8 @@ pose_model = 'Body_with_feet' #With RTMLib:
|
|
|
60
60
|
# - Hand (HAND_21, only lightweight mode. Potentially better results with Whole_body),
|
|
61
61
|
# - Face (FACE_106),
|
|
62
62
|
# - Animal (ANIMAL2D_17)
|
|
63
|
-
#
|
|
64
|
-
#
|
|
63
|
+
# ⚠ Only RTMPose is natively embeded in Pose2Sim. For all other pose estimation methods, you will have to run them yourself, and then refer to the documentation to convert the output files if needed
|
|
64
|
+
# ⚠ For Face and Animal, use mode="""{dictionary}""", and find the corresponding .onnx model there https://github.com/open-mmlab/mmpose/tree/main/projects/rtmpose
|
|
65
65
|
mode = 'balanced' # 'lightweight', 'balanced', 'performance', or """{dictionary}""" (see below)
|
|
66
66
|
|
|
67
67
|
# A dictionary (WITHIN THREE DOUBLE QUOTES) allows you to manually select the person detection (if top_down approach) and/or pose estimation models (see https://github.com/Tau-J/rtmlib).
|
|
@@ -139,6 +139,7 @@ reject_outliers = true # Hampel filter for outlier rejection before other f
|
|
|
139
139
|
|
|
140
140
|
filter = true
|
|
141
141
|
show_graphs = true # Show plots of raw and processed results
|
|
142
|
+
save_graphs = false # Save position and angle plots of raw and processed results
|
|
142
143
|
filter_type = 'butterworth' # butterworth, kalman, gcv_spline, gaussian, loess, median, butterworth_on_speed
|
|
143
144
|
|
|
144
145
|
# Most intuitive and standard filter in biomechanics
|
Sports2D/Sports2D.py
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
- Run on webcam with default parameters:
|
|
29
29
|
sports2d --video_input webcam
|
|
30
30
|
- Run with custom parameters (all non specified are set to default):
|
|
31
|
-
sports2d --
|
|
31
|
+
sports2d --show_graphs False --time_range 0 2.1 --result_dir path_to_result_dir
|
|
32
32
|
sports2d --person_detection_method highest_likelihood --mode lightweight --det_frequency 50
|
|
33
33
|
- Run with a toml configuration file:
|
|
34
34
|
sports2d --config path_to_config.toml
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
pip install .
|
|
45
45
|
|
|
46
46
|
-----
|
|
47
|
-
|
|
47
|
+
⚠ Warning ⚠
|
|
48
48
|
-----
|
|
49
49
|
- The angle estimation is only as good as the pose estimation algorithm, i.e., it is not perfect.
|
|
50
50
|
- It will only lead to acceptable results if the persons move in the 2D plane (sagittal plane).
|
|
@@ -236,6 +236,7 @@ DEFAULT_CONFIG = {'base': {'video_input': ['demo.mp4'],
|
|
|
236
236
|
'reject_outliers': True,
|
|
237
237
|
'filter': True,
|
|
238
238
|
'show_graphs': True,
|
|
239
|
+
'save_graphs': False,
|
|
239
240
|
'filter_type': 'butterworth',
|
|
240
241
|
'butterworth': {'order': 4, 'cut_off_frequency': 6.0},
|
|
241
242
|
'kalman': {'trust_ratio': 500.0, 'smooth':True},
|
|
@@ -279,6 +280,7 @@ CONFIG_HELP = {'config': ["C", "path to a toml configuration file"],
|
|
|
279
280
|
'show_realtime_results': ["R", "show results in real-time. true if not specified"],
|
|
280
281
|
'display_angle_values_on': ["a", '"body", "list", "body" "list", or "none". body list if not specified'],
|
|
281
282
|
'show_graphs': ["G", "show plots of raw and processed results. true if not specified"],
|
|
283
|
+
'save_graphs': ["", "save position and angle plots of raw and processed results. false if not specified"],
|
|
282
284
|
'joint_angles': ["j", '"Right ankle" "Left ankle" "Right knee" "Left knee" "Right hip" "Left hip" "Right shoulder" "Left shoulder" "Right elbow" "Left elbow" if not specified'],
|
|
283
285
|
'segment_angles': ["s", '"Right foot" "Left foot" "Right shank" "Left shank" "Right thigh" "Left thigh" "Pelvis" "Trunk" "Shoulders" "Head" "Right arm" "Left arm" "Right forearm" "Left forearm" if not specified'],
|
|
284
286
|
'save_vid': ["V", "save processed video. true if not specified"],
|
|
@@ -546,7 +548,7 @@ def main():
|
|
|
546
548
|
- Run on webcam with default parameters:
|
|
547
549
|
sports2d --video_input webcam
|
|
548
550
|
- Run with custom parameters (all non specified are set to default):
|
|
549
|
-
sports2d --
|
|
551
|
+
sports2d --show_graphs False --time_range 0 2.1 --result_dir path_to_result_dir
|
|
550
552
|
sports2d --mode lightweight --det_frequency 50
|
|
551
553
|
- Run with a toml configuration file:
|
|
552
554
|
sports2d --config path_to_config.toml
|
Sports2D/Utilities/tests.py
CHANGED
|
@@ -63,14 +63,14 @@ def test_workflow():
|
|
|
63
63
|
|
|
64
64
|
# Default
|
|
65
65
|
demo_cmd = ["sports2d", "--person_ordering_method", "highest_likelihood", "--show_realtime_results", "False", "--show_graphs", "False"]
|
|
66
|
-
subprocess.run(demo_cmd, check=True, capture_output=True, text=True, encoding='utf-8')
|
|
66
|
+
subprocess.run(demo_cmd, check=True, capture_output=True, text=True, encoding='utf-8', errors='replace')
|
|
67
67
|
|
|
68
68
|
# With loading a trc file, visible_side 'front', first_person_height '1.76", floor_angle 0, xy_origin [0, 928]
|
|
69
69
|
demo_cmd2 = ["sports2d", "--show_realtime_results", "False", "--show_graphs", "False",
|
|
70
70
|
"--load_trc_px", os.path.join(root_dir, "demo_Sports2D", "demo_Sports2D_px_person01.trc"),
|
|
71
71
|
"--visible_side", "front", "--first_person_height", "1.76", "--time_range", "1.2", "2.7",
|
|
72
72
|
"--floor_angle", "0", "--xy_origin", "0", "928"]
|
|
73
|
-
subprocess.run(demo_cmd2, check=True, capture_output=True, text=True, encoding='utf-8')
|
|
73
|
+
subprocess.run(demo_cmd2, check=True, capture_output=True, text=True, encoding='utf-8', errors='replace')
|
|
74
74
|
|
|
75
75
|
# With no pixels to meters conversion, one person to select, lightweight mode, detection frequency, slowmo factor, gaussian filter, RTMO body pose model
|
|
76
76
|
demo_cmd3 = ["sports2d", "--show_realtime_results", "False", "--show_graphs", "False",
|
|
@@ -80,7 +80,7 @@ def test_workflow():
|
|
|
80
80
|
"--slowmo_factor", "4",
|
|
81
81
|
"--filter_type", "gaussian",
|
|
82
82
|
"--pose_model", "body", "--mode", """{'pose_class':'RTMO', 'pose_model':'https://download.openmmlab.com/mmpose/v1/projects/rtmo/onnx_sdk/rtmo-m_16xb16-600e_body7-640x640-39e78cc4_20231211.zip', 'pose_input_size':[640, 640]}"""]
|
|
83
|
-
subprocess.run(demo_cmd3, check=True, capture_output=True, text=True, encoding='utf-8')
|
|
83
|
+
subprocess.run(demo_cmd3, check=True, capture_output=True, text=True, encoding='utf-8', errors='replace')
|
|
84
84
|
|
|
85
85
|
# With a time range, inverse kinematics, marker augmentation
|
|
86
86
|
demo_cmd4 = ["sports2d", "--person_ordering_method", "greatest_displacement", "--show_realtime_results", "False", "--show_graphs", "False",
|
|
@@ -88,7 +88,7 @@ def test_workflow():
|
|
|
88
88
|
"--do_ik", "True", "--use_augmentation", "True",
|
|
89
89
|
"--nb_persons_to_detect", "all", "--first_person_height", "1.65",
|
|
90
90
|
"--visible_side", "auto", "front", "--participant_mass", "55.0", "67.0"]
|
|
91
|
-
subprocess.run(demo_cmd4, check=True, capture_output=True, text=True, encoding='utf-8')
|
|
91
|
+
subprocess.run(demo_cmd4, check=True, capture_output=True, text=True, encoding='utf-8', errors='replace')
|
|
92
92
|
|
|
93
93
|
# From config file
|
|
94
94
|
config_path = Path(__file__).resolve().parent.parent / 'Demo' / 'Config_demo.toml'
|
|
@@ -98,7 +98,7 @@ def test_workflow():
|
|
|
98
98
|
config_dict.get("base").update({"person_ordering_method": "highest_likelihood"})
|
|
99
99
|
with open(config_path, 'w') as f: toml.dump(config_dict, f)
|
|
100
100
|
demo_cmd5 = ["sports2d", "--config", str(config_path), "--show_realtime_results", "False", "--show_graphs", "False"]
|
|
101
|
-
subprocess.run(demo_cmd5, check=True, capture_output=True, text=True, encoding='utf-8')
|
|
101
|
+
subprocess.run(demo_cmd5, check=True, capture_output=True, text=True, encoding='utf-8', errors='replace')
|
|
102
102
|
|
|
103
103
|
|
|
104
104
|
if __name__ == "__main__":
|
Sports2D/process.py
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
- optionally plots pose and angle data before and after processing for comparison
|
|
30
30
|
- optionally saves poses for each person as a trc file, and angles as a mot file
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
⚠ Warning ⚠
|
|
33
33
|
- The pose detection is only as good as the pose estimation algorithm, i.e., it is not perfect.
|
|
34
34
|
- It will lead to reliable results only if the persons move in the 2D plane (sagittal or frontal plane).
|
|
35
35
|
- The persons need to be filmed as perpendicularly as possible from their side.
|
|
@@ -77,12 +77,14 @@ from matplotlib.widgets import Slider, Button
|
|
|
77
77
|
from matplotlib import patheffects
|
|
78
78
|
|
|
79
79
|
from rtmlib import PoseTracker, BodyWithFeet, Wholebody, Body, Hand, Custom
|
|
80
|
+
from rtmlib.tools.object_detection.post_processings import nms
|
|
80
81
|
from deep_sort_realtime.deepsort_tracker import DeepSort
|
|
81
82
|
|
|
82
83
|
from Sports2D.Utilities.common import *
|
|
83
84
|
from Pose2Sim.common import *
|
|
84
85
|
from Pose2Sim.skeletons import *
|
|
85
86
|
from Pose2Sim.triangulation import indices_of_first_last_non_nan_chunks
|
|
87
|
+
from Pose2Sim.personAssociation import *
|
|
86
88
|
from Pose2Sim.filtering import *
|
|
87
89
|
|
|
88
90
|
# Not safe, but to be used until OpenMMLab/RTMlib's SSL certificates are updated
|
|
@@ -106,7 +108,7 @@ __status__ = "Development"
|
|
|
106
108
|
|
|
107
109
|
|
|
108
110
|
# FUNCTIONS
|
|
109
|
-
def setup_webcam(webcam_id,
|
|
111
|
+
def setup_webcam(webcam_id, vid_output_path, input_size):
|
|
110
112
|
'''
|
|
111
113
|
Set up webcam capture with OpenCV.
|
|
112
114
|
|
|
@@ -132,29 +134,28 @@ def setup_webcam(webcam_id, save_vid, vid_output_path, input_size):
|
|
|
132
134
|
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, input_size[1])
|
|
133
135
|
cam_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
134
136
|
cam_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
137
|
+
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
|
|
135
138
|
fps = round(cap.get(cv2.CAP_PROP_FPS))
|
|
136
139
|
if fps == 0: fps = 30
|
|
137
140
|
|
|
138
141
|
if cam_width != input_size[0] or cam_height != input_size[1]:
|
|
139
142
|
logging.warning(f"Warning: Your webcam does not support {input_size[0]}x{input_size[1]} resolution. Resolution set to the closest supported one: {cam_width}x{cam_height}.")
|
|
140
143
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
out_vid = cv2.VideoWriter(vid_output_path, fourcc, fps, (cam_width, cam_height))
|
|
152
|
-
# logging.info("Failed to open video writer with 'avc1' (h264). Using 'mp4v' instead.")
|
|
144
|
+
# fourcc MJPG produces very large files but is faster. If it is too slow, consider using it and then converting the video to h264
|
|
145
|
+
# try:
|
|
146
|
+
# fourcc = cv2.VideoWriter_fourcc(*'avc1') # =h264. better compression and quality but may fail on some systems
|
|
147
|
+
# out_vid = cv2.VideoWriter(vid_output_path, fourcc, fps, (cam_width, cam_height))
|
|
148
|
+
# if not out_vid.isOpened():
|
|
149
|
+
# raise ValueError("Failed to open video writer with 'avc1' (h264)")
|
|
150
|
+
# except Exception:
|
|
151
|
+
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
|
152
|
+
out_vid = cv2.VideoWriter(vid_output_path, fourcc, fps, (cam_width, cam_height))
|
|
153
|
+
# logging.info("Failed to open video writer with 'avc1' (h264). Using 'mp4v' instead.")
|
|
153
154
|
|
|
154
155
|
return cap, out_vid, cam_width, cam_height, fps
|
|
155
156
|
|
|
156
157
|
|
|
157
|
-
def setup_video(video_file_path,
|
|
158
|
+
def setup_video(video_file_path, vid_output_path, save_vid):
|
|
158
159
|
'''
|
|
159
160
|
Set up video capture with OpenCV.
|
|
160
161
|
|
|
@@ -789,10 +790,10 @@ def make_mot_with_angles(angles, time, mot_path):
|
|
|
789
790
|
return angles
|
|
790
791
|
|
|
791
792
|
|
|
792
|
-
def pose_plots(trc_data_unfiltered, trc_data, person_id):
|
|
793
|
+
def pose_plots(trc_data_unfiltered, trc_data, person_id, show=True):
|
|
793
794
|
'''
|
|
794
795
|
Displays trc filtered and unfiltered data for comparison
|
|
795
|
-
|
|
796
|
+
⚠ Often crashes on the third window...
|
|
796
797
|
|
|
797
798
|
INPUTS:
|
|
798
799
|
- trc_data_unfiltered: pd.DataFrame. The unfiltered trc data
|
|
@@ -835,13 +836,16 @@ def pose_plots(trc_data_unfiltered, trc_data, person_id):
|
|
|
835
836
|
|
|
836
837
|
pw.addPlot(keypoint, f)
|
|
837
838
|
|
|
838
|
-
|
|
839
|
+
if show:
|
|
840
|
+
pw.show()
|
|
841
|
+
|
|
842
|
+
return pw
|
|
839
843
|
|
|
840
844
|
|
|
841
|
-
def angle_plots(angle_data_unfiltered, angle_data, person_id):
|
|
845
|
+
def angle_plots(angle_data_unfiltered, angle_data, person_id, show=True):
|
|
842
846
|
'''
|
|
843
847
|
Displays angle filtered and unfiltered data for comparison
|
|
844
|
-
|
|
848
|
+
⚠ Often crashes on the third window...
|
|
845
849
|
|
|
846
850
|
INPUTS:
|
|
847
851
|
- angle_data_unfiltered: pd.DataFrame. The unfiltered angle data
|
|
@@ -878,7 +882,10 @@ def angle_plots(angle_data_unfiltered, angle_data, person_id):
|
|
|
878
882
|
|
|
879
883
|
pw.addPlot(angle, f)
|
|
880
884
|
|
|
881
|
-
|
|
885
|
+
if show:
|
|
886
|
+
pw.show()
|
|
887
|
+
|
|
888
|
+
return pw
|
|
882
889
|
|
|
883
890
|
|
|
884
891
|
def get_personIDs_with_highest_scores(all_frames_scores, nb_persons_to_detect):
|
|
@@ -985,12 +992,13 @@ def get_personIDs_with_greatest_displacement(all_frames_X_homog, all_frames_Y_ho
|
|
|
985
992
|
return selected_persons
|
|
986
993
|
|
|
987
994
|
|
|
988
|
-
def get_personIDs_on_click(
|
|
995
|
+
def get_personIDs_on_click(video_file_path, frame_range, all_frames_X_homog, all_frames_Y_homog):
|
|
989
996
|
'''
|
|
990
997
|
Get the person IDs on click in the image
|
|
991
998
|
|
|
992
999
|
INPUTS:
|
|
993
|
-
-
|
|
1000
|
+
- video_file_path: path to video file
|
|
1001
|
+
- frame_range: tuple (start_frame, end_frame)
|
|
994
1002
|
- all_frames_X_homog: shape (Nframes, Npersons, Nkpts)
|
|
995
1003
|
- all_frames_Y_homog: shape (Nframes, Npersons, Nkpts)
|
|
996
1004
|
|
|
@@ -1001,23 +1009,19 @@ def get_personIDs_on_click(frames, all_frames_X_homog, all_frames_Y_homog):
|
|
|
1001
1009
|
# Reorganize the coordinates to shape (Nframes, Npersons, Nkpts, Ndims)
|
|
1002
1010
|
all_pose_coords = np.stack((all_frames_X_homog, all_frames_Y_homog), axis=-1)
|
|
1003
1011
|
|
|
1004
|
-
# Trim all_pose_coords and frames to the same size
|
|
1005
|
-
min_frames = min(all_pose_coords.shape[0], len(frames))
|
|
1006
|
-
all_pose_coords = all_pose_coords[:min_frames]
|
|
1007
|
-
frames = frames[:min_frames]
|
|
1008
|
-
|
|
1009
1012
|
# Select person IDs on click on video/image
|
|
1010
|
-
selected_persons = select_persons_on_vid(
|
|
1013
|
+
selected_persons = select_persons_on_vid(video_file_path, frame_range, all_pose_coords)
|
|
1011
1014
|
|
|
1012
1015
|
return selected_persons
|
|
1013
1016
|
|
|
1014
1017
|
|
|
1015
|
-
def select_persons_on_vid(
|
|
1018
|
+
def select_persons_on_vid(video_file_path, frame_range, all_pose_coords):
|
|
1016
1019
|
'''
|
|
1017
1020
|
Interactive UI to select persons from a video by clicking on their bounding boxes.
|
|
1018
1021
|
|
|
1019
1022
|
INPUTS:
|
|
1020
|
-
-
|
|
1023
|
+
- video_file_path: path to video file
|
|
1024
|
+
- frame_range: tuple (start_frame, end_frame)
|
|
1021
1025
|
- all_pose_coords: keypoints coordinates. shape (Nframes, Npersons, Nkpts, Ndims)
|
|
1022
1026
|
|
|
1023
1027
|
OUTPUT:
|
|
@@ -1031,93 +1035,42 @@ def select_persons_on_vid(frames, all_pose_coords):
|
|
|
1031
1035
|
LINE_UNSELECTED_COLOR = 'white'
|
|
1032
1036
|
LINE_SELECTED_COLOR = 'darkorange'
|
|
1033
1037
|
|
|
1034
|
-
selected_persons = []
|
|
1035
|
-
|
|
1036
|
-
# Calculate bounding boxes for each person in each frame
|
|
1037
|
-
n_frames, n_persons = all_pose_coords.shape[0], all_pose_coords.shape[1]
|
|
1038
|
-
all_bboxes = []
|
|
1039
|
-
for frame_idx in range(n_frames):
|
|
1040
|
-
frame_bboxes = []
|
|
1041
|
-
for person_idx in range(n_persons):
|
|
1042
|
-
# Get keypoints for current person
|
|
1043
|
-
keypoints = all_pose_coords[frame_idx, person_idx]
|
|
1044
|
-
valid_keypoints = keypoints[~np.isnan(keypoints).all(axis=1)]
|
|
1045
|
-
if len(valid_keypoints) > 0:
|
|
1046
|
-
# Calculate bounding box
|
|
1047
|
-
x_min, y_min = np.min(valid_keypoints, axis=0)
|
|
1048
|
-
x_max, y_max = np.max(valid_keypoints, axis=0)
|
|
1049
|
-
frame_bboxes.append((x_min, y_min, x_max, y_max))
|
|
1050
|
-
else:
|
|
1051
|
-
frame_bboxes.append((np.nan, np.nan, np.nan, np.nan)) # No valid bounding box for this person
|
|
1052
|
-
all_bboxes.append(frame_bboxes)
|
|
1053
|
-
all_bboxes = np.array(all_bboxes) # Shape: (Nframes, Npersons, 4)
|
|
1054
|
-
|
|
1055
|
-
# Create figure, axes, and slider
|
|
1056
|
-
frame_height, frame_width = frames[0].shape[:2]
|
|
1057
|
-
is_vertical = frame_height > frame_width
|
|
1058
|
-
if is_vertical:
|
|
1059
|
-
fig_height = frame_height / 250 # For vertical videos
|
|
1060
|
-
else:
|
|
1061
|
-
fig_height = max(frame_height / 300, 6) # For horizontal videos
|
|
1062
|
-
fig = plt.figure(figsize=(8, fig_height), num=f'Select the persons to analyze in the desired order')
|
|
1063
|
-
fig.patch.set_facecolor(BACKGROUND_COLOR)
|
|
1064
|
-
|
|
1065
|
-
video_axes_height = 0.7 if is_vertical else 0.6
|
|
1066
|
-
ax_video = plt.axes([0.1, 0.2, 0.8, video_axes_height])
|
|
1067
|
-
ax_video.axis('off')
|
|
1068
|
-
ax_video.set_facecolor(BACKGROUND_COLOR)
|
|
1069
|
-
|
|
1070
|
-
# First image
|
|
1071
|
-
frame_rgb = cv2.cvtColor(frames[0], cv2.COLOR_BGR2RGB)
|
|
1072
|
-
rects, annotations = [], []
|
|
1073
|
-
for person_idx, bbox in enumerate(all_bboxes[0]):
|
|
1074
|
-
if ~np.isnan(bbox).any():
|
|
1075
|
-
x_min, y_min, x_max, y_max = bbox.astype(int)
|
|
1076
|
-
rect = plt.Rectangle(
|
|
1077
|
-
(x_min, y_min), x_max - x_min, y_max - y_min,
|
|
1078
|
-
linewidth=1, edgecolor=LINE_UNSELECTED_COLOR, facecolor=UNSELECTED_COLOR,
|
|
1079
|
-
linestyle='-', path_effects=[patheffects.withSimplePatchShadow()], zorder=2
|
|
1080
|
-
)
|
|
1081
|
-
ax_video.add_patch(rect)
|
|
1082
|
-
annotation = ax_video.text(
|
|
1083
|
-
x_min, y_min - 10, f'{person_idx}', color=LINE_UNSELECTED_COLOR, fontsize=7, fontweight='normal',
|
|
1084
|
-
bbox=dict(facecolor=UNSELECTED_COLOR, edgecolor=LINE_UNSELECTED_COLOR, boxstyle='square,pad=0.3', path_effects=[patheffects.withSimplePatchShadow()]), zorder=3
|
|
1085
|
-
)
|
|
1086
|
-
rects.append(rect)
|
|
1087
|
-
annotations.append(annotation)
|
|
1088
|
-
img_plot = ax_video.imshow(frame_rgb)
|
|
1089
|
-
|
|
1090
|
-
# Slider
|
|
1091
|
-
ax_slider = plt.axes([ax_video.get_position().x0, ax_video.get_position().y0-0.05, ax_video.get_position().width, 0.04])
|
|
1092
|
-
ax_slider.set_facecolor(BACKGROUND_COLOR)
|
|
1093
|
-
frame_slider = Slider(
|
|
1094
|
-
ax=ax_slider,
|
|
1095
|
-
label='',
|
|
1096
|
-
valmin=0,
|
|
1097
|
-
valmax=len(all_pose_coords)-1,
|
|
1098
|
-
valinit=0,
|
|
1099
|
-
valstep=1,
|
|
1100
|
-
valfmt=None
|
|
1101
|
-
)
|
|
1102
|
-
frame_slider.poly.set_edgecolor(SLIDER_EDGE_COLOR)
|
|
1103
|
-
frame_slider.poly.set_facecolor(SLIDER_COLOR)
|
|
1104
|
-
frame_slider.poly.set_linewidth(1)
|
|
1105
|
-
frame_slider.valtext.set_visible(False)
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
# Status text and OK button
|
|
1109
|
-
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])
|
|
1110
|
-
ax_status.axis('off')
|
|
1111
|
-
status_text = ax_status.text(0.0, 0.5, f"Selected: None", color='black', fontsize=10)
|
|
1112
1038
|
|
|
1113
|
-
|
|
1114
|
-
|
|
1039
|
+
def get_frame(frame_idx):
|
|
1040
|
+
"""Get frame with caching"""
|
|
1041
|
+
actual_frame_idx = start_frame + frame_idx
|
|
1042
|
+
|
|
1043
|
+
# Check cache first
|
|
1044
|
+
if actual_frame_idx in frame_cache:
|
|
1045
|
+
# Move to end of cache order (recently used)
|
|
1046
|
+
cache_order.remove(actual_frame_idx)
|
|
1047
|
+
cache_order.append(actual_frame_idx)
|
|
1048
|
+
return frame_cache[actual_frame_idx]
|
|
1049
|
+
|
|
1050
|
+
# Load from video
|
|
1051
|
+
cap.set(cv2.CAP_PROP_POS_FRAMES, actual_frame_idx)
|
|
1052
|
+
success, frame = cap.read()
|
|
1053
|
+
if not success:
|
|
1054
|
+
raise ValueError(f"Could not read frame {actual_frame_idx}")
|
|
1055
|
+
|
|
1056
|
+
# Add to cache
|
|
1057
|
+
frame_cache[actual_frame_idx] = frame.copy()
|
|
1058
|
+
cache_order.append(actual_frame_idx)
|
|
1059
|
+
|
|
1060
|
+
# Remove old frames if cache too large
|
|
1061
|
+
while len(frame_cache) > cache_size:
|
|
1062
|
+
oldest_frame = cache_order.pop(0)
|
|
1063
|
+
if oldest_frame in frame_cache:
|
|
1064
|
+
del frame_cache[oldest_frame]
|
|
1065
|
+
|
|
1066
|
+
return frame
|
|
1115
1067
|
|
|
1116
1068
|
|
|
1117
1069
|
def update_frame(val):
|
|
1118
1070
|
# Update image
|
|
1119
1071
|
frame_idx = int(frame_slider.val)
|
|
1120
|
-
|
|
1072
|
+
frame = get_frame(frame_idx)
|
|
1073
|
+
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
|
1121
1074
|
|
|
1122
1075
|
# Update bboxes and annotations
|
|
1123
1076
|
for items in [rects, annotations]:
|
|
@@ -1210,6 +1163,101 @@ def select_persons_on_vid(frames, all_pose_coords):
|
|
|
1210
1163
|
plt.close(fig)
|
|
1211
1164
|
|
|
1212
1165
|
|
|
1166
|
+
# Open video
|
|
1167
|
+
cap = cv2.VideoCapture(video_file_path)
|
|
1168
|
+
if not cap.isOpened():
|
|
1169
|
+
raise ValueError(f"Could not open video: {video_file_path}")
|
|
1170
|
+
start_frame, end_frame = frame_range
|
|
1171
|
+
|
|
1172
|
+
|
|
1173
|
+
# Frame cache for efficiency - only keep recently accessed frames
|
|
1174
|
+
frame_cache = {}
|
|
1175
|
+
cache_size = 20 # Keep last 20 frames in memory
|
|
1176
|
+
cache_order = []
|
|
1177
|
+
|
|
1178
|
+
# Calculate bounding boxes for each person in each frame
|
|
1179
|
+
selected_persons = []
|
|
1180
|
+
n_frames, n_persons = all_pose_coords.shape[0], all_pose_coords.shape[1]
|
|
1181
|
+
all_bboxes = []
|
|
1182
|
+
for frame_idx in range(n_frames):
|
|
1183
|
+
frame_bboxes = []
|
|
1184
|
+
for person_idx in range(n_persons):
|
|
1185
|
+
# Get keypoints for current person
|
|
1186
|
+
keypoints = all_pose_coords[frame_idx, person_idx]
|
|
1187
|
+
valid_keypoints = keypoints[~np.isnan(keypoints).all(axis=1)]
|
|
1188
|
+
if len(valid_keypoints) > 0:
|
|
1189
|
+
# Calculate bounding box
|
|
1190
|
+
x_min, y_min = np.min(valid_keypoints, axis=0)
|
|
1191
|
+
x_max, y_max = np.max(valid_keypoints, axis=0)
|
|
1192
|
+
frame_bboxes.append((x_min, y_min, x_max, y_max))
|
|
1193
|
+
else:
|
|
1194
|
+
frame_bboxes.append((np.nan, np.nan, np.nan, np.nan)) # No valid bounding box for this person
|
|
1195
|
+
all_bboxes.append(frame_bboxes)
|
|
1196
|
+
all_bboxes = np.array(all_bboxes) # Shape: (Nframes, Npersons, 4)
|
|
1197
|
+
|
|
1198
|
+
# Create figure, axes, and slider
|
|
1199
|
+
first_frame = get_frame(0)
|
|
1200
|
+
frame_height, frame_width = first_frame.shape[:2]
|
|
1201
|
+
is_vertical = frame_height > frame_width
|
|
1202
|
+
if is_vertical:
|
|
1203
|
+
fig_height = frame_height / 250 # For vertical videos
|
|
1204
|
+
else:
|
|
1205
|
+
fig_height = max(frame_height / 300, 6) # For horizontal videos
|
|
1206
|
+
fig = plt.figure(figsize=(8, fig_height), num=f'Select the persons to analyze in the desired order')
|
|
1207
|
+
fig.patch.set_facecolor(BACKGROUND_COLOR)
|
|
1208
|
+
|
|
1209
|
+
video_axes_height = 0.7 if is_vertical else 0.6
|
|
1210
|
+
ax_video = plt.axes([0.1, 0.2, 0.8, video_axes_height])
|
|
1211
|
+
ax_video.axis('off')
|
|
1212
|
+
ax_video.set_facecolor(BACKGROUND_COLOR)
|
|
1213
|
+
|
|
1214
|
+
# First image
|
|
1215
|
+
frame_rgb = cv2.cvtColor(first_frame, cv2.COLOR_BGR2RGB)
|
|
1216
|
+
rects, annotations = [], []
|
|
1217
|
+
for person_idx, bbox in enumerate(all_bboxes[0]):
|
|
1218
|
+
if ~np.isnan(bbox).any():
|
|
1219
|
+
x_min, y_min, x_max, y_max = bbox.astype(int)
|
|
1220
|
+
rect = plt.Rectangle(
|
|
1221
|
+
(x_min, y_min), x_max - x_min, y_max - y_min,
|
|
1222
|
+
linewidth=1, edgecolor=LINE_UNSELECTED_COLOR, facecolor=UNSELECTED_COLOR,
|
|
1223
|
+
linestyle='-', path_effects=[patheffects.withSimplePatchShadow()], zorder=2
|
|
1224
|
+
)
|
|
1225
|
+
ax_video.add_patch(rect)
|
|
1226
|
+
annotation = ax_video.text(
|
|
1227
|
+
x_min, y_min - 10, f'{person_idx}', color=LINE_UNSELECTED_COLOR, fontsize=7, fontweight='normal',
|
|
1228
|
+
bbox=dict(facecolor=UNSELECTED_COLOR, edgecolor=LINE_UNSELECTED_COLOR, boxstyle='square,pad=0.3', path_effects=[patheffects.withSimplePatchShadow()]), zorder=3
|
|
1229
|
+
)
|
|
1230
|
+
rects.append(rect)
|
|
1231
|
+
annotations.append(annotation)
|
|
1232
|
+
img_plot = ax_video.imshow(frame_rgb)
|
|
1233
|
+
|
|
1234
|
+
# Slider
|
|
1235
|
+
ax_slider = plt.axes([ax_video.get_position().x0, ax_video.get_position().y0-0.05, ax_video.get_position().width, 0.04])
|
|
1236
|
+
ax_slider.set_facecolor(BACKGROUND_COLOR)
|
|
1237
|
+
frame_slider = Slider(
|
|
1238
|
+
ax=ax_slider,
|
|
1239
|
+
label='',
|
|
1240
|
+
valmin=0,
|
|
1241
|
+
valmax=len(all_pose_coords)-1,
|
|
1242
|
+
valinit=0,
|
|
1243
|
+
valstep=1,
|
|
1244
|
+
valfmt=None
|
|
1245
|
+
)
|
|
1246
|
+
frame_slider.poly.set_edgecolor(SLIDER_EDGE_COLOR)
|
|
1247
|
+
frame_slider.poly.set_facecolor(SLIDER_COLOR)
|
|
1248
|
+
frame_slider.poly.set_linewidth(1)
|
|
1249
|
+
frame_slider.valtext.set_visible(False)
|
|
1250
|
+
|
|
1251
|
+
|
|
1252
|
+
# Status text and OK button
|
|
1253
|
+
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])
|
|
1254
|
+
ax_status.axis('off')
|
|
1255
|
+
status_text = ax_status.text(0.0, 0.5, f"Selected: None", color='black', fontsize=10)
|
|
1256
|
+
|
|
1257
|
+
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])
|
|
1258
|
+
ok_button = Button(ax_button, 'OK', color=BACKGROUND_COLOR)
|
|
1259
|
+
|
|
1260
|
+
|
|
1213
1261
|
# Connect events
|
|
1214
1262
|
frame_slider.on_changed(update_frame)
|
|
1215
1263
|
fig.canvas.mpl_connect('button_press_event', on_click)
|
|
@@ -1333,7 +1381,7 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
1333
1381
|
- optionally plots pose and angle data before and after processing for comparison
|
|
1334
1382
|
- optionally saves poses for each person as a trc file, and angles as a mot file
|
|
1335
1383
|
|
|
1336
|
-
|
|
1384
|
+
⚠ Warning ⚠
|
|
1337
1385
|
- The pose detection is only as good as the pose estimation algorithm, i.e., it is not perfect.
|
|
1338
1386
|
- It will lead to reliable results only if the persons move in the 2D plane (sagittal or frontal plane).
|
|
1339
1387
|
- The persons need to be filmed as perpendicularly as possible from their side.
|
|
@@ -1449,6 +1497,7 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
1449
1497
|
handle_LR_swap = config_dict.get('post-processing').get('handle_LR_swap', False)
|
|
1450
1498
|
reject_outliers = config_dict.get('post-processing').get('reject_outliers', False)
|
|
1451
1499
|
show_plots = config_dict.get('post-processing').get('show_graphs')
|
|
1500
|
+
save_plots = config_dict.get('post-processing').get('save_graphs')
|
|
1452
1501
|
filter_type = config_dict.get('post-processing').get('filter_type')
|
|
1453
1502
|
butterworth_filter_order = config_dict.get('post-processing').get('butterworth', {}).get('order')
|
|
1454
1503
|
butterworth_filter_cutoff = config_dict.get('post-processing').get('butterworth', {}).get('cut_off_frequency')
|
|
@@ -1465,12 +1514,14 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
1465
1514
|
# Create output directories
|
|
1466
1515
|
if video_file == "webcam":
|
|
1467
1516
|
current_date = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
1468
|
-
output_dir_name = f'webcam_{current_date}'
|
|
1517
|
+
output_dir_name = f'webcam_{current_date}_Sports2D'
|
|
1518
|
+
video_file_path = result_dir / output_dir_name / f'webcam_{current_date}_raw.mp4'
|
|
1469
1519
|
else:
|
|
1470
|
-
video_file_path = video_dir / video_file
|
|
1471
1520
|
video_file_stem = video_file.stem
|
|
1472
1521
|
output_dir_name = f'{video_file_stem}_Sports2D'
|
|
1522
|
+
video_file_path = video_dir / video_file
|
|
1473
1523
|
output_dir = result_dir / output_dir_name
|
|
1524
|
+
plots_output_dir = output_dir / f'{output_dir_name}_graphs'
|
|
1474
1525
|
img_output_dir = output_dir / f'{output_dir_name}_img'
|
|
1475
1526
|
vid_output_path = output_dir / f'{output_dir_name}.mp4'
|
|
1476
1527
|
pose_output_path = output_dir / f'{output_dir_name}_px.trc'
|
|
@@ -1479,6 +1530,8 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
1479
1530
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
1480
1531
|
if save_img:
|
|
1481
1532
|
img_output_dir.mkdir(parents=True, exist_ok=True)
|
|
1533
|
+
if save_plots:
|
|
1534
|
+
plots_output_dir.mkdir(parents=True, exist_ok=True)
|
|
1482
1535
|
|
|
1483
1536
|
# Inverse kinematics settings
|
|
1484
1537
|
do_ik = config_dict.get('kinematics').get('do_ik')
|
|
@@ -1491,7 +1544,10 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
1491
1544
|
trimmed_extrema_percent = config_dict.get('kinematics').get('trimmed_extrema_percent')
|
|
1492
1545
|
close_to_zero_speed_px = config_dict.get('kinematics').get('close_to_zero_speed_px')
|
|
1493
1546
|
close_to_zero_speed_m = config_dict.get('kinematics').get('close_to_zero_speed_m')
|
|
1494
|
-
|
|
1547
|
+
# Create a Pose2Sim dictionary and fill in missing keys
|
|
1548
|
+
recursivedict = lambda: defaultdict(recursivedict)
|
|
1549
|
+
Pose2Sim_config_dict = recursivedict()
|
|
1550
|
+
if do_ik or use_augmentation:
|
|
1495
1551
|
try:
|
|
1496
1552
|
if use_augmentation:
|
|
1497
1553
|
from Pose2Sim.markerAugmentation import augment_markers_all
|
|
@@ -1501,9 +1557,6 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
1501
1557
|
logging.error("OpenSim package is not installed. Please install it to use inverse kinematics or marker augmentation features (see 'Full install' section of the documentation).")
|
|
1502
1558
|
raise ImportError("OpenSim package is not installed. Please install it to use inverse kinematics or marker augmentation features (see 'Full install' section of the documentation).")
|
|
1503
1559
|
|
|
1504
|
-
# Create a Pose2Sim dictionary and fill in missing keys
|
|
1505
|
-
recursivedict = lambda: defaultdict(recursivedict)
|
|
1506
|
-
Pose2Sim_config_dict = recursivedict()
|
|
1507
1560
|
# Fill Pose2Sim dictionary (height and mass will be filled later)
|
|
1508
1561
|
Pose2Sim_config_dict['project']['project_dir'] = str(output_dir)
|
|
1509
1562
|
Pose2Sim_config_dict['markerAugmentation']['make_c3d'] = make_c3d
|
|
@@ -1534,12 +1587,13 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
1534
1587
|
|
|
1535
1588
|
# Set up video capture
|
|
1536
1589
|
if video_file == "webcam":
|
|
1537
|
-
cap, out_vid, cam_width, cam_height, fps = setup_webcam(webcam_id,
|
|
1590
|
+
cap, out_vid, cam_width, cam_height, fps = setup_webcam(webcam_id, vid_output_path, input_size)
|
|
1591
|
+
frame_rate = fps
|
|
1538
1592
|
frame_range = [0,sys.maxsize]
|
|
1539
1593
|
frame_iterator = range(*frame_range)
|
|
1540
1594
|
logging.warning('Webcam input: the framerate may vary. If results are filtered, Sports2D will use the average framerate as input.')
|
|
1541
1595
|
else:
|
|
1542
|
-
cap, out_vid, cam_width, cam_height, fps = setup_video(video_file_path,
|
|
1596
|
+
cap, out_vid, cam_width, cam_height, fps = setup_video(video_file_path, vid_output_path, save_vid)
|
|
1543
1597
|
fps *= slowmo_factor
|
|
1544
1598
|
start_time = get_start_time_ffmpeg(video_file_path)
|
|
1545
1599
|
frame_range = [int((time_range[0]-start_time) * frame_rate), int((time_range[1]-start_time) * frame_rate)] if time_range else [0, int(cap.get(cv2.CAP_PROP_FRAME_COUNT))]
|
|
@@ -1636,10 +1690,11 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
1636
1690
|
all_frames_X, all_frames_X_flipped, all_frames_Y, all_frames_scores, all_frames_angles = [], [], [], [], []
|
|
1637
1691
|
frame_processing_times = []
|
|
1638
1692
|
frame_count = 0
|
|
1639
|
-
|
|
1693
|
+
first_frame = max(int(t0 * fps), frame_range[0])
|
|
1694
|
+
# frames = []
|
|
1640
1695
|
while cap.isOpened():
|
|
1641
1696
|
# Skip to the starting frame
|
|
1642
|
-
if frame_count
|
|
1697
|
+
if frame_count < first_frame:
|
|
1643
1698
|
cap.read()
|
|
1644
1699
|
frame_count += 1
|
|
1645
1700
|
continue
|
|
@@ -1659,9 +1714,9 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
1659
1714
|
if save_angles:
|
|
1660
1715
|
all_frames_angles.append([])
|
|
1661
1716
|
continue
|
|
1662
|
-
else: # does not store all frames in memory if they are not saved or used for ordering
|
|
1663
|
-
|
|
1664
|
-
|
|
1717
|
+
# else: # does not store all frames in memory if they are not saved or used for ordering
|
|
1718
|
+
# if save_img or save_vid or person_ordering_method == 'on_click':
|
|
1719
|
+
# frames.append(frame.copy())
|
|
1665
1720
|
|
|
1666
1721
|
# Retrieve pose or Estimate pose and track people
|
|
1667
1722
|
if load_trc_px:
|
|
@@ -1670,9 +1725,20 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
1670
1725
|
keypoints = keypoints_all[frame_nb]
|
|
1671
1726
|
scores = scores_all[frame_nb]
|
|
1672
1727
|
else:
|
|
1728
|
+
# Save video on the fly if the input is a webcam
|
|
1729
|
+
if video_file == "webcam":
|
|
1730
|
+
out_vid.write(frame)
|
|
1731
|
+
|
|
1673
1732
|
# Detect poses
|
|
1674
1733
|
keypoints, scores = pose_tracker(frame)
|
|
1675
1734
|
|
|
1735
|
+
# Non maximum suppression (at pose level, not detection)
|
|
1736
|
+
frame_shape = frame.shape
|
|
1737
|
+
bboxes = bbox_xyxy_compute(frame_shape, keypoints, padding=0)
|
|
1738
|
+
score_bboxes = np.array([np.mean(s) for s in scores])
|
|
1739
|
+
keep = nms(bboxes, score_bboxes, nms_thr=0.45)
|
|
1740
|
+
keypoints, scores = keypoints[keep], scores[keep]
|
|
1741
|
+
|
|
1676
1742
|
# Track poses across frames
|
|
1677
1743
|
if tracking_mode == 'deepsort':
|
|
1678
1744
|
keypoints, scores = sort_people_deepsort(keypoints, scores, deepsort_tracker, frame, frame_count)
|
|
@@ -1775,8 +1841,11 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
1775
1841
|
# End of the video is reached
|
|
1776
1842
|
cap.release()
|
|
1777
1843
|
logging.info(f"Video processing completed.")
|
|
1778
|
-
if save_vid:
|
|
1844
|
+
if save_vid or video_file == "webcam":
|
|
1779
1845
|
out_vid.release()
|
|
1846
|
+
if video_file == "webcam":
|
|
1847
|
+
vid_output_path.absolute().rename(video_file_path)
|
|
1848
|
+
|
|
1780
1849
|
if show_realtime_results:
|
|
1781
1850
|
cv2.destroyAllWindows()
|
|
1782
1851
|
|
|
@@ -1813,7 +1882,7 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
1813
1882
|
nb_persons_to_detect = nb_detected_persons
|
|
1814
1883
|
|
|
1815
1884
|
if person_ordering_method == 'on_click':
|
|
1816
|
-
selected_persons = get_personIDs_on_click(
|
|
1885
|
+
selected_persons = get_personIDs_on_click(video_file_path, frame_range, all_frames_X_homog, all_frames_Y_homog)
|
|
1817
1886
|
if len(selected_persons) == 0:
|
|
1818
1887
|
logging.warning('No persons selected. Analyzing all detected persons.')
|
|
1819
1888
|
selected_persons = list(range(nb_detected_persons))
|
|
@@ -1890,8 +1959,13 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
1890
1959
|
all_frames_Y_person_interp.replace(np.nan, 0, inplace=True)
|
|
1891
1960
|
|
|
1892
1961
|
# Filter
|
|
1962
|
+
# if handle_LR_swap:
|
|
1963
|
+
# logging.info(f'Handling left-right swaps.')
|
|
1964
|
+
# all_frames_X_person_interp = all_frames_X_person_interp.apply(LR_unswap, axis=0)
|
|
1965
|
+
# all_frames_Y_person_interp = all_frames_Y_person_interp.apply(LR_unswap, axis=0)
|
|
1966
|
+
|
|
1893
1967
|
if reject_outliers:
|
|
1894
|
-
logging.info('Rejecting outliers with Hampel filter.')
|
|
1968
|
+
logging.info('Rejecting outliers with a Hampel filter.')
|
|
1895
1969
|
all_frames_X_person_interp = all_frames_X_person_interp.apply(hampel_filter, axis=0, args = [round(7*frame_rate/30), 2])
|
|
1896
1970
|
all_frames_Y_person_interp = all_frames_Y_person_interp.apply(hampel_filter, axis=0, args = [round(7*frame_rate/30), 2])
|
|
1897
1971
|
|
|
@@ -1943,9 +2017,17 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
1943
2017
|
columns_to_concat.extend([all_frames_X_person.iloc[:,kpt], all_frames_Y_person.iloc[:,kpt], all_frames_Z_homog.iloc[:,kpt]])
|
|
1944
2018
|
trc_data_unfiltered_i = pd.concat([all_frames_time] + columns_to_concat, axis=1)
|
|
1945
2019
|
trc_data_unfiltered.append(trc_data_unfiltered_i)
|
|
1946
|
-
if
|
|
1947
|
-
pose_plots(trc_data_unfiltered_i, trc_data_i, i)
|
|
1948
|
-
|
|
2020
|
+
if not to_meters and (show_plots or save_plots):
|
|
2021
|
+
pw = pose_plots(trc_data_unfiltered_i, trc_data_i, i, show=show_plots)
|
|
2022
|
+
if save_plots:
|
|
2023
|
+
for n, f in enumerate(pw.figure_handles):
|
|
2024
|
+
dpi = pw.canvases[i].figure.dpi
|
|
2025
|
+
f.set_size_inches(1280/dpi, 720/dpi)
|
|
2026
|
+
title = pw.tabs.tabText(n)
|
|
2027
|
+
plot_path = plots_output_dir / (pose_output_path.stem + f'_person{i:02d}_px_{title.replace(" ","_").replace("/","_")}.png')
|
|
2028
|
+
f.savefig(plot_path, dpi=dpi, bbox_inches='tight')
|
|
2029
|
+
logging.info(f'Pose plots (px) saved in {plots_output_dir}.')
|
|
2030
|
+
|
|
1949
2031
|
all_frames_X_processed[:,idx_person,:], all_frames_Y_processed[:,idx_person,:] = all_frames_X_person_filt, all_frames_Y_person_filt
|
|
1950
2032
|
if calculate_angles or save_angles:
|
|
1951
2033
|
all_frames_X_flipped_processed[:,idx_person,:] = all_frames_X_flipped_person
|
|
@@ -2031,9 +2113,17 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
2031
2113
|
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]
|
|
2032
2114
|
trc_data_unfiltered_m_i = pd.concat([all_frames_time.rename('time')]+px_to_m_unfiltered_i, axis=1)
|
|
2033
2115
|
|
|
2034
|
-
if to_meters and show_plots:
|
|
2035
|
-
pose_plots(trc_data_unfiltered_m_i, trc_data_m_i, i)
|
|
2036
|
-
|
|
2116
|
+
if to_meters and (show_plots or save_plots):
|
|
2117
|
+
pw = pose_plots(trc_data_unfiltered_m_i, trc_data_m_i, i, show=show_plots)
|
|
2118
|
+
if save_plots:
|
|
2119
|
+
for n, f in enumerate(pw.figure_handles):
|
|
2120
|
+
dpi = pw.canvases[i].figure.dpi
|
|
2121
|
+
f.set_size_inches(1280/dpi, 720/dpi)
|
|
2122
|
+
title = pw.tabs.tabText(n)
|
|
2123
|
+
plot_path = plots_output_dir / (pose_output_path_m.stem + f'_person{i:02d}_m_{title.replace(" ","_").replace("/","_")}.png')
|
|
2124
|
+
f.savefig(plot_path, dpi=dpi, bbox_inches='tight')
|
|
2125
|
+
logging.info(f'Pose plots (m) saved in {plots_output_dir}.')
|
|
2126
|
+
|
|
2037
2127
|
# Write to trc file
|
|
2038
2128
|
trc_data_m.append(trc_data_m_i)
|
|
2039
2129
|
pose_path_person_m_i = (pose_output_path.parent / (pose_output_path_m.stem + f'_person{i:02d}.trc'))
|
|
@@ -2140,7 +2230,7 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
2140
2230
|
|
|
2141
2231
|
# Filter
|
|
2142
2232
|
if reject_outliers:
|
|
2143
|
-
logging.info(f'Rejecting outliers with Hampel filter.')
|
|
2233
|
+
logging.info(f'Rejecting outliers with a Hampel filter.')
|
|
2144
2234
|
all_frames_angles_person_interp = all_frames_angles_person_interp.apply(hampel_filter, axis=0)
|
|
2145
2235
|
|
|
2146
2236
|
if not do_filter:
|
|
@@ -2172,7 +2262,7 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
2172
2262
|
logging.error(f"Invalid filter_type: {filter_type}. Must be 'butterworth', 'gcv_spline', 'kalman', 'gaussian', 'loess', or 'median'.")
|
|
2173
2263
|
raise ValueError(f"Invalid filter_type: {filter_type}. Must be 'butterworth', 'gcv_spline', 'kalman', 'gaussian', 'loess', or 'median'.")
|
|
2174
2264
|
|
|
2175
|
-
logging.info(f'Filtering with {args}
|
|
2265
|
+
logging.info(f'Filtering with {args}')
|
|
2176
2266
|
all_frames_angles_person_filt = all_frames_angles_person_interp.apply(filter1d, axis=0, args = [Pose2Sim_config_dict, filter_type, frame_rate])
|
|
2177
2267
|
|
|
2178
2268
|
# Add floor_angle_estim to segment angles
|
|
@@ -2192,9 +2282,16 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
2192
2282
|
logging.info(f'Angles saved to {angles_path_person.resolve()}.')
|
|
2193
2283
|
|
|
2194
2284
|
# Plotting angles before and after interpolation and filtering
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
angle_plots(all_frames_angles_person, angle_data, i) # i = current person
|
|
2285
|
+
all_frames_angles_person.insert(0, 'time', all_frames_time)
|
|
2286
|
+
if save_plots and (show_plots or save_plots):
|
|
2287
|
+
pw = angle_plots(all_frames_angles_person, angle_data, i, show=show_plots) # i = current person
|
|
2288
|
+
for n, f in enumerate(pw.figure_handles):
|
|
2289
|
+
dpi = pw.canvases[i].figure.dpi
|
|
2290
|
+
f.set_size_inches(1280/dpi, 720/dpi)
|
|
2291
|
+
title = pw.tabs.tabText(n)
|
|
2292
|
+
plot_path = plots_output_dir / (pose_output_path_m.stem + f'_person{i:02d}_ang_{title.replace(" ","_").replace("/","_")}.png')
|
|
2293
|
+
f.savefig(plot_path, dpi=dpi, bbox_inches='tight')
|
|
2294
|
+
logging.info(f'Pose plots (m) saved in {plots_output_dir}.')
|
|
2198
2295
|
|
|
2199
2296
|
|
|
2200
2297
|
#%% ==================================================
|
|
@@ -2228,22 +2325,28 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
2228
2325
|
new_keypoints_ids = list(range(len(new_keypoints_ids)))
|
|
2229
2326
|
|
|
2230
2327
|
# Draw pose and angles
|
|
2328
|
+
first_frame, last_frame = frame_range
|
|
2231
2329
|
if 'first_trim' not in locals():
|
|
2232
|
-
first_trim, last_trim =
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2330
|
+
first_trim, last_trim = first_frame, last_frame
|
|
2331
|
+
cap = cv2.VideoCapture(video_file_path)
|
|
2332
|
+
cap.set(cv2.CAP_PROP_POS_FRAMES, first_frame+first_trim)
|
|
2333
|
+
for i in range(first_trim, last_trim):
|
|
2334
|
+
success, frame = cap.read()
|
|
2335
|
+
if not success:
|
|
2336
|
+
raise ValueError(f"Could not read frame {i}")
|
|
2337
|
+
img = frame.copy()
|
|
2338
|
+
img = draw_bounding_box(img, all_frames_X_processed[i], all_frames_Y_processed[i], colors=colors, fontSize=fontSize, thickness=thickness)
|
|
2339
|
+
img = draw_keypts(img, all_frames_X_processed[i], all_frames_Y_processed[i], all_frames_scores_processed[i], cmap_str='RdYlGn')
|
|
2340
|
+
img = draw_skel(img, all_frames_X_processed[i], all_frames_Y_processed[i], pose_model_with_new_ids)
|
|
2341
|
+
if calculate_angles:
|
|
2342
|
+
img = draw_angles(img, all_frames_X_processed[i], all_frames_Y_processed[i], all_frames_angles_processed[i], all_frames_X_flipped_processed[i], new_keypoints_ids, new_keypoints_names, angle_names, display_angle_values_on=display_angle_values_on, colors=colors, fontSize=fontSize, thickness=thickness)
|
|
2343
|
+
|
|
2344
|
+
# Save video or images
|
|
2345
|
+
if save_vid:
|
|
2346
|
+
out_vid.write(img)
|
|
2347
|
+
if save_img:
|
|
2348
|
+
cv2.imwrite(str((img_output_dir / f'{output_dir_name}_{(i+frame_range[0]):06d}.png')), img)
|
|
2349
|
+
cap.release()
|
|
2247
2350
|
|
|
2248
2351
|
if save_vid:
|
|
2249
2352
|
out_vid.release()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sports2d
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.20
|
|
4
4
|
Summary: Compute 2D human pose and angles from a video or a webcam.
|
|
5
5
|
Author-email: David Pagnon <contact@david-pagnon.com>
|
|
6
6
|
Maintainer-email: David Pagnon <contact@david-pagnon.com>
|
|
@@ -35,10 +35,10 @@ Requires-Dist: ipython
|
|
|
35
35
|
Requires-Dist: c3d
|
|
36
36
|
Requires-Dist: rtmlib
|
|
37
37
|
Requires-Dist: openvino
|
|
38
|
-
Requires-Dist: opencv-python
|
|
38
|
+
Requires-Dist: opencv-python<4.12
|
|
39
39
|
Requires-Dist: imageio_ffmpeg
|
|
40
40
|
Requires-Dist: deep-sort-realtime
|
|
41
|
-
Requires-Dist: Pose2Sim>=0.10.
|
|
41
|
+
Requires-Dist: Pose2Sim>=0.10.36
|
|
42
42
|
Dynamic: license-file
|
|
43
43
|
|
|
44
44
|
|
|
@@ -145,7 +145,7 @@ If you need 3D research-grade markerless joint kinematics, consider using severa
|
|
|
145
145
|
|
|
146
146
|
> N.B.: Full install is required for OpenSim inverse kinematics.
|
|
147
147
|
|
|
148
|
-
Open a terminal. Type `python -V` to make sure python >=3.10 <=3.
|
|
148
|
+
Open a terminal. Type `python -V` to make sure python >=3.10 <=3.12 is installed. If not, install it [from there](https://www.python.org/downloads/).
|
|
149
149
|
|
|
150
150
|
Run:
|
|
151
151
|
``` cmd
|
|
@@ -169,7 +169,7 @@ pip install .
|
|
|
169
169
|
- Install Anaconda or [Miniconda](https://docs.conda.io/en/latest/miniconda.html):\
|
|
170
170
|
Open an Anaconda prompt and create a virtual environment:
|
|
171
171
|
``` cmd
|
|
172
|
-
conda create -n Sports2D python=3.
|
|
172
|
+
conda create -n Sports2D python=3.12 -y
|
|
173
173
|
conda activate Sports2D
|
|
174
174
|
```
|
|
175
175
|
- **Install OpenSim**:\
|
|
@@ -568,7 +568,7 @@ Note that any detection and pose models can be used (first [deploy them with MMP
|
|
|
568
568
|
'pose_model':'https://download.openmmlab.com/mmpose/v1/projects/rtmposev1/onnx_sdk/rtmpose-t_simcc-body7_pt-body7_420e-256x192-026a1439_20230504.zip',
|
|
569
569
|
'pose_input_size':[192,256]}"""
|
|
570
570
|
```
|
|
571
|
-
- Use `--det_frequency 50`:
|
|
571
|
+
- Use `--det_frequency 50`: Rtmlib is (by default) a top-down method: detects bounding boxes for every person in the frame, and then detects keypoints inside of each box. The person detection stage is much slower. You can choose to detect persons only every 50 frames (for example), and track bounding boxes inbetween, which is much faster.
|
|
572
572
|
- Use `--load_trc_px <path_to_file_px.trc>`: Will use pose estimation results from a file. Useful if you want to use different parameters for pixel to meter conversion or angle calculation without running detection and pose estimation all over.
|
|
573
573
|
- Make sure you use `--tracking_mode sports2d`: Will use the default Sports2D tracker. Unlike DeepSort, it is faster, does not require any parametrization, and is as good in non-crowded scenes.
|
|
574
574
|
|
|
@@ -637,13 +637,13 @@ Sports2D:
|
|
|
637
637
|
|
|
638
638
|
1. **Reads stream from a webcam, from one video, or from a list of videos**. Selects the specified time range to process.
|
|
639
639
|
|
|
640
|
-
2. **Sets up pose estimation with RTMLib.** It can be run in lightweight, balanced, or performance mode, and for faster inference,
|
|
640
|
+
2. **Sets up pose estimation with RTMLib.** It can be run in lightweight, balanced, or performance mode, and for faster inference, the person bounding boxes can be tracked instead of detected every frame. Any RTMPose model can be used.
|
|
641
641
|
|
|
642
642
|
3. **Tracks people** so that their IDs are consistent across frames. A person is associated to another in the next frame when they are at a small distance. IDs remain consistent even if the person disappears from a few frames. We crafted a 'sports2D' tracker which gives good results and runs in real time, but it is also possible to use `deepsort` in particularly challenging situations.
|
|
643
643
|
|
|
644
|
-
4. **Chooses
|
|
644
|
+
4. **Chooses which persons to analyze.** In single-person mode, only keeps the person with the highest average scores over the sequence. In multi-person mode, you can choose the number of persons to analyze (`nb_persons_to_detect`), and how to order them (`person_ordering_method`). The ordering method can be 'on_click', 'highest_likelihood', 'largest_size', 'smallest_size', 'greatest_displacement', 'least_displacement', 'first_detected', or 'last_detected'. `on_click` is default and lets the user click on the persons they are interested in, in the desired order.
|
|
645
645
|
|
|
646
|
-
4. **Converts the pixel coordinates to meters.** The user can provide
|
|
646
|
+
4. **Converts the pixel coordinates to meters.** The user can provide the size of a specified person to scale results accordingly. The floor angle and the coordinate origin can either be detected automatically from the gait sequence, or be manually specified. The depth coordinates are set to normative values, depending on whether the person is going left, right, facing the camera, or looking away.
|
|
647
647
|
|
|
648
648
|
5. **Computes the selected joint and segment angles**, and flips them on the left/right side if the respective foot is pointing to the left/right.
|
|
649
649
|
|
|
@@ -652,12 +652,14 @@ Sports2D:
|
|
|
652
652
|
Draws the skeleton and the keypoints, with a green to red color scale to account for their confidence\
|
|
653
653
|
Draws joint and segment angles on the body, and writes the values either near the joint/segment, or on the upper-left of the image with a progress bar
|
|
654
654
|
|
|
655
|
-
6. **Interpolates and filters results:** Missing pose and angle sequences are interpolated unless gaps are too large
|
|
655
|
+
6. **Interpolates and filters results:** (1) Swaps between right and left limbs are corrected, (2) Missing pose and angle sequences are interpolated unless gaps are too large, (3) Outliers are rejected with a Hampel filter, and finally (4) Results are filtered, by default with a 6 Hz Butterworth filter. All of the above can be configured or deactivated, and other filters such as Kalman, GCV, Gaussian, LOESS, Median, and Butterworth on speeds are also available (see [Config_Demo.toml](https://github.com/davidpagnon/Sports2D/blob/main/Sports2D/Demo/Config_demo.toml))
|
|
656
656
|
|
|
657
657
|
7. **Optionally show** processed images, saves them, or saves them as a video\
|
|
658
658
|
**Optionally plots** pose and angle data before and after processing for comparison\
|
|
659
659
|
**Optionally saves** poses for each person as a TRC file in pixels and meters, angles as a MOT file, and calibration data as a [Pose2Sim](https://github.com/perfanalytics/pose2sim) TOML file
|
|
660
660
|
|
|
661
|
+
8. **Optionally runs scaling and inverse kinematics** with OpenSim via [Pose2Sim](https://github.com/perfanalytics/pose2sim).
|
|
662
|
+
|
|
661
663
|
<br>
|
|
662
664
|
|
|
663
665
|
**Joint angle conventions:**
|
|
@@ -9,17 +9,17 @@ Content/paper.md,sha256=8rWSOLrKTysloZv0Fz2lr3nayxtHi7GlFMqwdgDVggY,11333
|
|
|
9
9
|
Content/sports2d_blender.gif,sha256=wgMuPRxhja3XtQn76_okGXsNnUT9Thp0pnD36GdW5_E,448786
|
|
10
10
|
Content/sports2d_opensim.gif,sha256=XP1AcjqhbGcJknXUoNJjPWAwaM9ahZafbDgLWvzKJs4,376656
|
|
11
11
|
Sports2D/Sports2D.ipynb,sha256=VnOVjIl6ndnCJTT13L4W5qTw4T-TQDF3jt3-wxnXDqM,2427047
|
|
12
|
-
Sports2D/Sports2D.py,sha256=
|
|
12
|
+
Sports2D/Sports2D.py,sha256=aCx3Hy1XltgBS7kuQ52b653hQtL_9sjvpQ0xGoPuoOw,35572
|
|
13
13
|
Sports2D/__init__.py,sha256=BuUkPEdItxlkeqz4dmoiPwZLkgAfABJK3KWQ1ujTGwE,153
|
|
14
|
-
Sports2D/process.py,sha256=
|
|
15
|
-
Sports2D/Demo/Config_demo.toml,sha256=
|
|
14
|
+
Sports2D/process.py,sha256=VrN8ZQimznjPAk_-1r9NpZnBCzDz9z0lV-E4wNJMCq0,125549
|
|
15
|
+
Sports2D/Demo/Config_demo.toml,sha256=GXlj4tuGvnEpzQm0FFOS2BWVHcGSHGxuudAAHhzsTQc,15665
|
|
16
16
|
Sports2D/Demo/demo.mp4,sha256=2aZkFxhWR7ESMEtXCT8MGA83p2jmoU2sp1ylQfO3gDk,3968304
|
|
17
17
|
Sports2D/Utilities/__init__.py,sha256=BuUkPEdItxlkeqz4dmoiPwZLkgAfABJK3KWQ1ujTGwE,153
|
|
18
18
|
Sports2D/Utilities/common.py,sha256=idMRmesFv5BPX-5g3z5dOVa7SpS_8tNgijvGrOZlR-k,11185
|
|
19
|
-
Sports2D/Utilities/tests.py,sha256=
|
|
20
|
-
sports2d-0.8.
|
|
21
|
-
sports2d-0.8.
|
|
22
|
-
sports2d-0.8.
|
|
23
|
-
sports2d-0.8.
|
|
24
|
-
sports2d-0.8.
|
|
25
|
-
sports2d-0.8.
|
|
19
|
+
Sports2D/Utilities/tests.py,sha256=bUcPoaIwa6ur13Njup5MjGY3N060Ropl_MCdAbCAbTc,4792
|
|
20
|
+
sports2d-0.8.20.dist-info/licenses/LICENSE,sha256=f4qe3nE0Y7ltJho5w-xAR0jI5PUox5Xl-MsYiY7ZRM8,1521
|
|
21
|
+
sports2d-0.8.20.dist-info/METADATA,sha256=PX4DMmISr1ZnSSKNHFqGF6F97_SuZBAPV5OOe8tgTPU,41277
|
|
22
|
+
sports2d-0.8.20.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
23
|
+
sports2d-0.8.20.dist-info/entry_points.txt,sha256=V8dFDIXatz9VvoGgoHzb2wE71C9-f85K6_OjnEQlxww,108
|
|
24
|
+
sports2d-0.8.20.dist-info/top_level.txt,sha256=cWWBiDD2WbQXMoIoN6-9et9U2t2c_ZKo2JtBqO5uN-k,17
|
|
25
|
+
sports2d-0.8.20.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|