sports2d 0.8.18__py3-none-any.whl → 0.8.19__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/Utilities/tests.py +5 -5
- Sports2D/process.py +200 -138
- {sports2d-0.8.18.dist-info → sports2d-0.8.19.dist-info}/METADATA +11 -9
- {sports2d-0.8.18.dist-info → sports2d-0.8.19.dist-info}/RECORD +8 -8
- {sports2d-0.8.18.dist-info → sports2d-0.8.19.dist-info}/WHEEL +0 -0
- {sports2d-0.8.18.dist-info → sports2d-0.8.19.dist-info}/entry_points.txt +0 -0
- {sports2d-0.8.18.dist-info → sports2d-0.8.19.dist-info}/licenses/LICENSE +0 -0
- {sports2d-0.8.18.dist-info → sports2d-0.8.19.dist-info}/top_level.txt +0 -0
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
|
@@ -83,6 +83,7 @@ from Sports2D.Utilities.common import *
|
|
|
83
83
|
from Pose2Sim.common import *
|
|
84
84
|
from Pose2Sim.skeletons import *
|
|
85
85
|
from Pose2Sim.triangulation import indices_of_first_last_non_nan_chunks
|
|
86
|
+
from Pose2Sim.personAssociation import *
|
|
86
87
|
from Pose2Sim.filtering import *
|
|
87
88
|
|
|
88
89
|
# Not safe, but to be used until OpenMMLab/RTMlib's SSL certificates are updated
|
|
@@ -106,7 +107,7 @@ __status__ = "Development"
|
|
|
106
107
|
|
|
107
108
|
|
|
108
109
|
# FUNCTIONS
|
|
109
|
-
def setup_webcam(webcam_id,
|
|
110
|
+
def setup_webcam(webcam_id, vid_output_path, input_size):
|
|
110
111
|
'''
|
|
111
112
|
Set up webcam capture with OpenCV.
|
|
112
113
|
|
|
@@ -132,29 +133,28 @@ def setup_webcam(webcam_id, save_vid, vid_output_path, input_size):
|
|
|
132
133
|
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, input_size[1])
|
|
133
134
|
cam_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
134
135
|
cam_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
136
|
+
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
|
|
135
137
|
fps = round(cap.get(cv2.CAP_PROP_FPS))
|
|
136
138
|
if fps == 0: fps = 30
|
|
137
139
|
|
|
138
140
|
if cam_width != input_size[0] or cam_height != input_size[1]:
|
|
139
141
|
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
142
|
|
|
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.")
|
|
143
|
+
# fourcc MJPG produces very large files but is faster. If it is too slow, consider using it and then converting the video to h264
|
|
144
|
+
# try:
|
|
145
|
+
# fourcc = cv2.VideoWriter_fourcc(*'avc1') # =h264. better compression and quality but may fail on some systems
|
|
146
|
+
# out_vid = cv2.VideoWriter(vid_output_path, fourcc, fps, (cam_width, cam_height))
|
|
147
|
+
# if not out_vid.isOpened():
|
|
148
|
+
# raise ValueError("Failed to open video writer with 'avc1' (h264)")
|
|
149
|
+
# except Exception:
|
|
150
|
+
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
|
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.")
|
|
153
153
|
|
|
154
154
|
return cap, out_vid, cam_width, cam_height, fps
|
|
155
155
|
|
|
156
156
|
|
|
157
|
-
def setup_video(video_file_path,
|
|
157
|
+
def setup_video(video_file_path, vid_output_path, save_vid):
|
|
158
158
|
'''
|
|
159
159
|
Set up video capture with OpenCV.
|
|
160
160
|
|
|
@@ -985,12 +985,13 @@ def get_personIDs_with_greatest_displacement(all_frames_X_homog, all_frames_Y_ho
|
|
|
985
985
|
return selected_persons
|
|
986
986
|
|
|
987
987
|
|
|
988
|
-
def get_personIDs_on_click(
|
|
988
|
+
def get_personIDs_on_click(video_file_path, frame_range, all_frames_X_homog, all_frames_Y_homog):
|
|
989
989
|
'''
|
|
990
990
|
Get the person IDs on click in the image
|
|
991
991
|
|
|
992
992
|
INPUTS:
|
|
993
|
-
-
|
|
993
|
+
- video_file_path: path to video file
|
|
994
|
+
- frame_range: tuple (start_frame, end_frame)
|
|
994
995
|
- all_frames_X_homog: shape (Nframes, Npersons, Nkpts)
|
|
995
996
|
- all_frames_Y_homog: shape (Nframes, Npersons, Nkpts)
|
|
996
997
|
|
|
@@ -1001,23 +1002,19 @@ def get_personIDs_on_click(frames, all_frames_X_homog, all_frames_Y_homog):
|
|
|
1001
1002
|
# Reorganize the coordinates to shape (Nframes, Npersons, Nkpts, Ndims)
|
|
1002
1003
|
all_pose_coords = np.stack((all_frames_X_homog, all_frames_Y_homog), axis=-1)
|
|
1003
1004
|
|
|
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
1005
|
# Select person IDs on click on video/image
|
|
1010
|
-
selected_persons = select_persons_on_vid(
|
|
1006
|
+
selected_persons = select_persons_on_vid(video_file_path, frame_range, all_pose_coords)
|
|
1011
1007
|
|
|
1012
1008
|
return selected_persons
|
|
1013
1009
|
|
|
1014
1010
|
|
|
1015
|
-
def select_persons_on_vid(
|
|
1011
|
+
def select_persons_on_vid(video_file_path, frame_range, all_pose_coords):
|
|
1016
1012
|
'''
|
|
1017
1013
|
Interactive UI to select persons from a video by clicking on their bounding boxes.
|
|
1018
1014
|
|
|
1019
1015
|
INPUTS:
|
|
1020
|
-
-
|
|
1016
|
+
- video_file_path: path to video file
|
|
1017
|
+
- frame_range: tuple (start_frame, end_frame)
|
|
1021
1018
|
- all_pose_coords: keypoints coordinates. shape (Nframes, Npersons, Nkpts, Ndims)
|
|
1022
1019
|
|
|
1023
1020
|
OUTPUT:
|
|
@@ -1031,93 +1028,42 @@ def select_persons_on_vid(frames, all_pose_coords):
|
|
|
1031
1028
|
LINE_UNSELECTED_COLOR = 'white'
|
|
1032
1029
|
LINE_SELECTED_COLOR = 'darkorange'
|
|
1033
1030
|
|
|
1034
|
-
selected_persons = []
|
|
1035
1031
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
#
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
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
|
-
|
|
1113
|
-
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])
|
|
1114
|
-
ok_button = Button(ax_button, 'OK', color=BACKGROUND_COLOR)
|
|
1032
|
+
def get_frame(frame_idx):
|
|
1033
|
+
"""Get frame with caching"""
|
|
1034
|
+
actual_frame_idx = start_frame + frame_idx
|
|
1035
|
+
|
|
1036
|
+
# Check cache first
|
|
1037
|
+
if actual_frame_idx in frame_cache:
|
|
1038
|
+
# Move to end of cache order (recently used)
|
|
1039
|
+
cache_order.remove(actual_frame_idx)
|
|
1040
|
+
cache_order.append(actual_frame_idx)
|
|
1041
|
+
return frame_cache[actual_frame_idx]
|
|
1042
|
+
|
|
1043
|
+
# Load from video
|
|
1044
|
+
cap.set(cv2.CAP_PROP_POS_FRAMES, actual_frame_idx)
|
|
1045
|
+
success, frame = cap.read()
|
|
1046
|
+
if not success:
|
|
1047
|
+
raise ValueError(f"Could not read frame {actual_frame_idx}")
|
|
1048
|
+
|
|
1049
|
+
# Add to cache
|
|
1050
|
+
frame_cache[actual_frame_idx] = frame.copy()
|
|
1051
|
+
cache_order.append(actual_frame_idx)
|
|
1052
|
+
|
|
1053
|
+
# Remove old frames if cache too large
|
|
1054
|
+
while len(frame_cache) > cache_size:
|
|
1055
|
+
oldest_frame = cache_order.pop(0)
|
|
1056
|
+
if oldest_frame in frame_cache:
|
|
1057
|
+
del frame_cache[oldest_frame]
|
|
1058
|
+
|
|
1059
|
+
return frame
|
|
1115
1060
|
|
|
1116
1061
|
|
|
1117
1062
|
def update_frame(val):
|
|
1118
1063
|
# Update image
|
|
1119
1064
|
frame_idx = int(frame_slider.val)
|
|
1120
|
-
|
|
1065
|
+
frame = get_frame(frame_idx)
|
|
1066
|
+
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
|
1121
1067
|
|
|
1122
1068
|
# Update bboxes and annotations
|
|
1123
1069
|
for items in [rects, annotations]:
|
|
@@ -1210,6 +1156,101 @@ def select_persons_on_vid(frames, all_pose_coords):
|
|
|
1210
1156
|
plt.close(fig)
|
|
1211
1157
|
|
|
1212
1158
|
|
|
1159
|
+
# Open video
|
|
1160
|
+
cap = cv2.VideoCapture(video_file_path)
|
|
1161
|
+
if not cap.isOpened():
|
|
1162
|
+
raise ValueError(f"Could not open video: {video_file_path}")
|
|
1163
|
+
start_frame, end_frame = frame_range
|
|
1164
|
+
|
|
1165
|
+
|
|
1166
|
+
# Frame cache for efficiency - only keep recently accessed frames
|
|
1167
|
+
frame_cache = {}
|
|
1168
|
+
cache_size = 20 # Keep last 20 frames in memory
|
|
1169
|
+
cache_order = []
|
|
1170
|
+
|
|
1171
|
+
# Calculate bounding boxes for each person in each frame
|
|
1172
|
+
selected_persons = []
|
|
1173
|
+
n_frames, n_persons = all_pose_coords.shape[0], all_pose_coords.shape[1]
|
|
1174
|
+
all_bboxes = []
|
|
1175
|
+
for frame_idx in range(n_frames):
|
|
1176
|
+
frame_bboxes = []
|
|
1177
|
+
for person_idx in range(n_persons):
|
|
1178
|
+
# Get keypoints for current person
|
|
1179
|
+
keypoints = all_pose_coords[frame_idx, person_idx]
|
|
1180
|
+
valid_keypoints = keypoints[~np.isnan(keypoints).all(axis=1)]
|
|
1181
|
+
if len(valid_keypoints) > 0:
|
|
1182
|
+
# Calculate bounding box
|
|
1183
|
+
x_min, y_min = np.min(valid_keypoints, axis=0)
|
|
1184
|
+
x_max, y_max = np.max(valid_keypoints, axis=0)
|
|
1185
|
+
frame_bboxes.append((x_min, y_min, x_max, y_max))
|
|
1186
|
+
else:
|
|
1187
|
+
frame_bboxes.append((np.nan, np.nan, np.nan, np.nan)) # No valid bounding box for this person
|
|
1188
|
+
all_bboxes.append(frame_bboxes)
|
|
1189
|
+
all_bboxes = np.array(all_bboxes) # Shape: (Nframes, Npersons, 4)
|
|
1190
|
+
|
|
1191
|
+
# Create figure, axes, and slider
|
|
1192
|
+
first_frame = get_frame(0)
|
|
1193
|
+
frame_height, frame_width = first_frame.shape[:2]
|
|
1194
|
+
is_vertical = frame_height > frame_width
|
|
1195
|
+
if is_vertical:
|
|
1196
|
+
fig_height = frame_height / 250 # For vertical videos
|
|
1197
|
+
else:
|
|
1198
|
+
fig_height = max(frame_height / 300, 6) # For horizontal videos
|
|
1199
|
+
fig = plt.figure(figsize=(8, fig_height), num=f'Select the persons to analyze in the desired order')
|
|
1200
|
+
fig.patch.set_facecolor(BACKGROUND_COLOR)
|
|
1201
|
+
|
|
1202
|
+
video_axes_height = 0.7 if is_vertical else 0.6
|
|
1203
|
+
ax_video = plt.axes([0.1, 0.2, 0.8, video_axes_height])
|
|
1204
|
+
ax_video.axis('off')
|
|
1205
|
+
ax_video.set_facecolor(BACKGROUND_COLOR)
|
|
1206
|
+
|
|
1207
|
+
# First image
|
|
1208
|
+
frame_rgb = cv2.cvtColor(first_frame, cv2.COLOR_BGR2RGB)
|
|
1209
|
+
rects, annotations = [], []
|
|
1210
|
+
for person_idx, bbox in enumerate(all_bboxes[0]):
|
|
1211
|
+
if ~np.isnan(bbox).any():
|
|
1212
|
+
x_min, y_min, x_max, y_max = bbox.astype(int)
|
|
1213
|
+
rect = plt.Rectangle(
|
|
1214
|
+
(x_min, y_min), x_max - x_min, y_max - y_min,
|
|
1215
|
+
linewidth=1, edgecolor=LINE_UNSELECTED_COLOR, facecolor=UNSELECTED_COLOR,
|
|
1216
|
+
linestyle='-', path_effects=[patheffects.withSimplePatchShadow()], zorder=2
|
|
1217
|
+
)
|
|
1218
|
+
ax_video.add_patch(rect)
|
|
1219
|
+
annotation = ax_video.text(
|
|
1220
|
+
x_min, y_min - 10, f'{person_idx}', color=LINE_UNSELECTED_COLOR, fontsize=7, fontweight='normal',
|
|
1221
|
+
bbox=dict(facecolor=UNSELECTED_COLOR, edgecolor=LINE_UNSELECTED_COLOR, boxstyle='square,pad=0.3', path_effects=[patheffects.withSimplePatchShadow()]), zorder=3
|
|
1222
|
+
)
|
|
1223
|
+
rects.append(rect)
|
|
1224
|
+
annotations.append(annotation)
|
|
1225
|
+
img_plot = ax_video.imshow(frame_rgb)
|
|
1226
|
+
|
|
1227
|
+
# Slider
|
|
1228
|
+
ax_slider = plt.axes([ax_video.get_position().x0, ax_video.get_position().y0-0.05, ax_video.get_position().width, 0.04])
|
|
1229
|
+
ax_slider.set_facecolor(BACKGROUND_COLOR)
|
|
1230
|
+
frame_slider = Slider(
|
|
1231
|
+
ax=ax_slider,
|
|
1232
|
+
label='',
|
|
1233
|
+
valmin=0,
|
|
1234
|
+
valmax=len(all_pose_coords)-1,
|
|
1235
|
+
valinit=0,
|
|
1236
|
+
valstep=1,
|
|
1237
|
+
valfmt=None
|
|
1238
|
+
)
|
|
1239
|
+
frame_slider.poly.set_edgecolor(SLIDER_EDGE_COLOR)
|
|
1240
|
+
frame_slider.poly.set_facecolor(SLIDER_COLOR)
|
|
1241
|
+
frame_slider.poly.set_linewidth(1)
|
|
1242
|
+
frame_slider.valtext.set_visible(False)
|
|
1243
|
+
|
|
1244
|
+
|
|
1245
|
+
# Status text and OK button
|
|
1246
|
+
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])
|
|
1247
|
+
ax_status.axis('off')
|
|
1248
|
+
status_text = ax_status.text(0.0, 0.5, f"Selected: None", color='black', fontsize=10)
|
|
1249
|
+
|
|
1250
|
+
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])
|
|
1251
|
+
ok_button = Button(ax_button, 'OK', color=BACKGROUND_COLOR)
|
|
1252
|
+
|
|
1253
|
+
|
|
1213
1254
|
# Connect events
|
|
1214
1255
|
frame_slider.on_changed(update_frame)
|
|
1215
1256
|
fig.canvas.mpl_connect('button_press_event', on_click)
|
|
@@ -1465,11 +1506,12 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
1465
1506
|
# Create output directories
|
|
1466
1507
|
if video_file == "webcam":
|
|
1467
1508
|
current_date = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
1468
|
-
output_dir_name = f'webcam_{current_date}'
|
|
1509
|
+
output_dir_name = f'webcam_{current_date}_Sports2D'
|
|
1510
|
+
video_file_path = result_dir / output_dir_name / f'webcam_{current_date}_raw.mp4'
|
|
1469
1511
|
else:
|
|
1470
|
-
video_file_path = video_dir / video_file
|
|
1471
1512
|
video_file_stem = video_file.stem
|
|
1472
1513
|
output_dir_name = f'{video_file_stem}_Sports2D'
|
|
1514
|
+
video_file_path = video_dir / video_file
|
|
1473
1515
|
output_dir = result_dir / output_dir_name
|
|
1474
1516
|
img_output_dir = output_dir / f'{output_dir_name}_img'
|
|
1475
1517
|
vid_output_path = output_dir / f'{output_dir_name}.mp4'
|
|
@@ -1491,7 +1533,10 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
1491
1533
|
trimmed_extrema_percent = config_dict.get('kinematics').get('trimmed_extrema_percent')
|
|
1492
1534
|
close_to_zero_speed_px = config_dict.get('kinematics').get('close_to_zero_speed_px')
|
|
1493
1535
|
close_to_zero_speed_m = config_dict.get('kinematics').get('close_to_zero_speed_m')
|
|
1494
|
-
|
|
1536
|
+
# Create a Pose2Sim dictionary and fill in missing keys
|
|
1537
|
+
recursivedict = lambda: defaultdict(recursivedict)
|
|
1538
|
+
Pose2Sim_config_dict = recursivedict()
|
|
1539
|
+
if do_ik or use_augmentation:
|
|
1495
1540
|
try:
|
|
1496
1541
|
if use_augmentation:
|
|
1497
1542
|
from Pose2Sim.markerAugmentation import augment_markers_all
|
|
@@ -1501,9 +1546,6 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
1501
1546
|
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
1547
|
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
1548
|
|
|
1504
|
-
# Create a Pose2Sim dictionary and fill in missing keys
|
|
1505
|
-
recursivedict = lambda: defaultdict(recursivedict)
|
|
1506
|
-
Pose2Sim_config_dict = recursivedict()
|
|
1507
1549
|
# Fill Pose2Sim dictionary (height and mass will be filled later)
|
|
1508
1550
|
Pose2Sim_config_dict['project']['project_dir'] = str(output_dir)
|
|
1509
1551
|
Pose2Sim_config_dict['markerAugmentation']['make_c3d'] = make_c3d
|
|
@@ -1534,12 +1576,13 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
1534
1576
|
|
|
1535
1577
|
# Set up video capture
|
|
1536
1578
|
if video_file == "webcam":
|
|
1537
|
-
cap, out_vid, cam_width, cam_height, fps = setup_webcam(webcam_id,
|
|
1579
|
+
cap, out_vid, cam_width, cam_height, fps = setup_webcam(webcam_id, vid_output_path, input_size)
|
|
1580
|
+
frame_rate = fps
|
|
1538
1581
|
frame_range = [0,sys.maxsize]
|
|
1539
1582
|
frame_iterator = range(*frame_range)
|
|
1540
1583
|
logging.warning('Webcam input: the framerate may vary. If results are filtered, Sports2D will use the average framerate as input.')
|
|
1541
1584
|
else:
|
|
1542
|
-
cap, out_vid, cam_width, cam_height, fps = setup_video(video_file_path,
|
|
1585
|
+
cap, out_vid, cam_width, cam_height, fps = setup_video(video_file_path, vid_output_path, save_vid)
|
|
1543
1586
|
fps *= slowmo_factor
|
|
1544
1587
|
start_time = get_start_time_ffmpeg(video_file_path)
|
|
1545
1588
|
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 +1679,11 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
1636
1679
|
all_frames_X, all_frames_X_flipped, all_frames_Y, all_frames_scores, all_frames_angles = [], [], [], [], []
|
|
1637
1680
|
frame_processing_times = []
|
|
1638
1681
|
frame_count = 0
|
|
1639
|
-
|
|
1682
|
+
first_frame = max(int(t0 * fps), frame_range[0])
|
|
1683
|
+
# frames = []
|
|
1640
1684
|
while cap.isOpened():
|
|
1641
1685
|
# Skip to the starting frame
|
|
1642
|
-
if frame_count
|
|
1686
|
+
if frame_count < first_frame:
|
|
1643
1687
|
cap.read()
|
|
1644
1688
|
frame_count += 1
|
|
1645
1689
|
continue
|
|
@@ -1659,9 +1703,9 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
1659
1703
|
if save_angles:
|
|
1660
1704
|
all_frames_angles.append([])
|
|
1661
1705
|
continue
|
|
1662
|
-
else: # does not store all frames in memory if they are not saved or used for ordering
|
|
1663
|
-
|
|
1664
|
-
|
|
1706
|
+
# else: # does not store all frames in memory if they are not saved or used for ordering
|
|
1707
|
+
# if save_img or save_vid or person_ordering_method == 'on_click':
|
|
1708
|
+
# frames.append(frame.copy())
|
|
1665
1709
|
|
|
1666
1710
|
# Retrieve pose or Estimate pose and track people
|
|
1667
1711
|
if load_trc_px:
|
|
@@ -1670,6 +1714,10 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
1670
1714
|
keypoints = keypoints_all[frame_nb]
|
|
1671
1715
|
scores = scores_all[frame_nb]
|
|
1672
1716
|
else:
|
|
1717
|
+
# Save video on the fly if the input is a webcam
|
|
1718
|
+
if video_file == "webcam":
|
|
1719
|
+
out_vid.write(frame)
|
|
1720
|
+
|
|
1673
1721
|
# Detect poses
|
|
1674
1722
|
keypoints, scores = pose_tracker(frame)
|
|
1675
1723
|
|
|
@@ -1775,8 +1823,11 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
1775
1823
|
# End of the video is reached
|
|
1776
1824
|
cap.release()
|
|
1777
1825
|
logging.info(f"Video processing completed.")
|
|
1778
|
-
if save_vid:
|
|
1826
|
+
if save_vid or video_file == "webcam":
|
|
1779
1827
|
out_vid.release()
|
|
1828
|
+
if video_file == "webcam":
|
|
1829
|
+
vid_output_path.absolute().rename(video_file_path)
|
|
1830
|
+
|
|
1780
1831
|
if show_realtime_results:
|
|
1781
1832
|
cv2.destroyAllWindows()
|
|
1782
1833
|
|
|
@@ -1813,7 +1864,7 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
1813
1864
|
nb_persons_to_detect = nb_detected_persons
|
|
1814
1865
|
|
|
1815
1866
|
if person_ordering_method == 'on_click':
|
|
1816
|
-
selected_persons = get_personIDs_on_click(
|
|
1867
|
+
selected_persons = get_personIDs_on_click(video_file_path, frame_range, all_frames_X_homog, all_frames_Y_homog)
|
|
1817
1868
|
if len(selected_persons) == 0:
|
|
1818
1869
|
logging.warning('No persons selected. Analyzing all detected persons.')
|
|
1819
1870
|
selected_persons = list(range(nb_detected_persons))
|
|
@@ -1890,8 +1941,13 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
1890
1941
|
all_frames_Y_person_interp.replace(np.nan, 0, inplace=True)
|
|
1891
1942
|
|
|
1892
1943
|
# Filter
|
|
1944
|
+
# if handle_LR_swap:
|
|
1945
|
+
# logging.info(f'Handling left-right swaps.')
|
|
1946
|
+
# all_frames_X_person_interp = all_frames_X_person_interp.apply(LR_unswap, axis=0)
|
|
1947
|
+
# all_frames_Y_person_interp = all_frames_Y_person_interp.apply(LR_unswap, axis=0)
|
|
1948
|
+
|
|
1893
1949
|
if reject_outliers:
|
|
1894
|
-
logging.info('Rejecting outliers with Hampel filter.')
|
|
1950
|
+
logging.info('Rejecting outliers with a Hampel filter.')
|
|
1895
1951
|
all_frames_X_person_interp = all_frames_X_person_interp.apply(hampel_filter, axis=0, args = [round(7*frame_rate/30), 2])
|
|
1896
1952
|
all_frames_Y_person_interp = all_frames_Y_person_interp.apply(hampel_filter, axis=0, args = [round(7*frame_rate/30), 2])
|
|
1897
1953
|
|
|
@@ -2140,7 +2196,7 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
2140
2196
|
|
|
2141
2197
|
# Filter
|
|
2142
2198
|
if reject_outliers:
|
|
2143
|
-
logging.info(f'Rejecting outliers with Hampel filter.')
|
|
2199
|
+
logging.info(f'Rejecting outliers with a Hampel filter.')
|
|
2144
2200
|
all_frames_angles_person_interp = all_frames_angles_person_interp.apply(hampel_filter, axis=0)
|
|
2145
2201
|
|
|
2146
2202
|
if not do_filter:
|
|
@@ -2172,7 +2228,7 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
2172
2228
|
logging.error(f"Invalid filter_type: {filter_type}. Must be 'butterworth', 'gcv_spline', 'kalman', 'gaussian', 'loess', or 'median'.")
|
|
2173
2229
|
raise ValueError(f"Invalid filter_type: {filter_type}. Must be 'butterworth', 'gcv_spline', 'kalman', 'gaussian', 'loess', or 'median'.")
|
|
2174
2230
|
|
|
2175
|
-
logging.info(f'Filtering with {args}
|
|
2231
|
+
logging.info(f'Filtering with {args}')
|
|
2176
2232
|
all_frames_angles_person_filt = all_frames_angles_person_interp.apply(filter1d, axis=0, args = [Pose2Sim_config_dict, filter_type, frame_rate])
|
|
2177
2233
|
|
|
2178
2234
|
# Add floor_angle_estim to segment angles
|
|
@@ -2228,22 +2284,28 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
|
|
|
2228
2284
|
new_keypoints_ids = list(range(len(new_keypoints_ids)))
|
|
2229
2285
|
|
|
2230
2286
|
# Draw pose and angles
|
|
2287
|
+
first_frame, last_frame = frame_range
|
|
2231
2288
|
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
|
-
|
|
2289
|
+
first_trim, last_trim = first_frame, last_frame
|
|
2290
|
+
cap = cv2.VideoCapture(video_file_path)
|
|
2291
|
+
cap.set(cv2.CAP_PROP_POS_FRAMES, first_frame+first_trim)
|
|
2292
|
+
for i in range(first_trim, last_trim):
|
|
2293
|
+
success, frame = cap.read()
|
|
2294
|
+
if not success:
|
|
2295
|
+
raise ValueError(f"Could not read frame {i}")
|
|
2296
|
+
img = frame.copy()
|
|
2297
|
+
img = draw_bounding_box(img, all_frames_X_processed[i], all_frames_Y_processed[i], colors=colors, fontSize=fontSize, thickness=thickness)
|
|
2298
|
+
img = draw_keypts(img, all_frames_X_processed[i], all_frames_Y_processed[i], all_frames_scores_processed[i], cmap_str='RdYlGn')
|
|
2299
|
+
img = draw_skel(img, all_frames_X_processed[i], all_frames_Y_processed[i], pose_model_with_new_ids)
|
|
2300
|
+
if calculate_angles:
|
|
2301
|
+
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)
|
|
2302
|
+
|
|
2303
|
+
# Save video or images
|
|
2304
|
+
if save_vid:
|
|
2305
|
+
out_vid.write(img)
|
|
2306
|
+
if save_img:
|
|
2307
|
+
cv2.imwrite(str((img_output_dir / f'{output_dir_name}_{(i+frame_range[0]):06d}.png')), img)
|
|
2308
|
+
cap.release()
|
|
2247
2309
|
|
|
2248
2310
|
if save_vid:
|
|
2249
2311
|
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.19
|
|
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,7 +35,7 @@ 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
41
|
Requires-Dist: Pose2Sim>=0.10.33
|
|
@@ -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:**
|
|
@@ -11,15 +11,15 @@ Content/sports2d_opensim.gif,sha256=XP1AcjqhbGcJknXUoNJjPWAwaM9ahZafbDgLWvzKJs4,
|
|
|
11
11
|
Sports2D/Sports2D.ipynb,sha256=VnOVjIl6ndnCJTT13L4W5qTw4T-TQDF3jt3-wxnXDqM,2427047
|
|
12
12
|
Sports2D/Sports2D.py,sha256=3Mcc_jFaD5Zv4ArB-jKYhgpMlFT0XBifTlSe70volzk,35385
|
|
13
13
|
Sports2D/__init__.py,sha256=BuUkPEdItxlkeqz4dmoiPwZLkgAfABJK3KWQ1ujTGwE,153
|
|
14
|
-
Sports2D/process.py,sha256=
|
|
14
|
+
Sports2D/process.py,sha256=hw9En4j6ROPmow0YmPK8Ohuc8Li8CoEEUXdtUTN_5zg,122898
|
|
15
15
|
Sports2D/Demo/Config_demo.toml,sha256=YescEgeQq3ojGqEAFWgXN142HL-YaVcRty9LbJgScoM,15577
|
|
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.19.dist-info/licenses/LICENSE,sha256=f4qe3nE0Y7ltJho5w-xAR0jI5PUox5Xl-MsYiY7ZRM8,1521
|
|
21
|
+
sports2d-0.8.19.dist-info/METADATA,sha256=F5rmn0eQhNwzk_6IYmY8nrtPZv7NHFCqgcnGeAdYHhI,41277
|
|
22
|
+
sports2d-0.8.19.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
23
|
+
sports2d-0.8.19.dist-info/entry_points.txt,sha256=V8dFDIXatz9VvoGgoHzb2wE71C9-f85K6_OjnEQlxww,108
|
|
24
|
+
sports2d-0.8.19.dist-info/top_level.txt,sha256=cWWBiDD2WbQXMoIoN6-9et9U2t2c_ZKo2JtBqO5uN-k,17
|
|
25
|
+
sports2d-0.8.19.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|