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.
@@ -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, save_vid, vid_output_path, input_size):
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
- out_vid = None
142
- if save_vid:
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.")
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, save_vid, vid_output_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(frames, all_frames_X_homog, all_frames_Y_homog):
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
- - frames: list of images read by cv2.imread. shape (Nframes, H, W, 3)
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(frames, all_pose_coords)
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(frames, all_pose_coords):
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
- - frames: list of images read by cv2.imread. shape (Nframes, H, W, 3)
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
- # 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
-
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
- frame_rgb = cv2.cvtColor(frames[frame_idx], cv2.COLOR_BGR2RGB)
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
- if do_ik or use_augmentation or do_filter:
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, save_vid, vid_output_path, input_size)
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, save_vid, vid_output_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
- frames = []
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 <= int(t0 * fps) or frame_count < frame_range[0]:
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
- if save_img or save_vid or person_ordering_method == 'on_click':
1664
- frames.append(frame.copy())
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(frames, all_frames_X_homog, all_frames_Y_homog)
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 = 0, frame_count-1
2233
- for frame_count, (frame, valid_X, valid_X_flipped, valid_Y, valid_scores, valid_angles) in enumerate(zip(frames, all_frames_X_processed, all_frames_X_flipped_processed, all_frames_Y_processed, all_frames_scores_processed, all_frames_angles_processed)):
2234
- if frame_count >= first_trim and frame_count <= last_trim:
2235
- img = frame.copy()
2236
- img = draw_bounding_box(img, valid_X, valid_Y, colors=colors, fontSize=fontSize, thickness=thickness)
2237
- img = draw_keypts(img, valid_X, valid_Y, valid_scores, cmap_str='RdYlGn')
2238
- img = draw_skel(img, valid_X, valid_Y, pose_model_with_new_ids)
2239
- if calculate_angles:
2240
- img = draw_angles(img, valid_X, valid_Y, valid_angles, valid_X_flipped, new_keypoints_ids, new_keypoints_names, angle_names, display_angle_values_on=display_angle_values_on, colors=colors, fontSize=fontSize, thickness=thickness)
2241
-
2242
- # Save video or images
2243
- if save_vid:
2244
- out_vid.write(img)
2245
- if save_img:
2246
- cv2.imwrite(str((img_output_dir / f'{output_dir_name}_{(frame_count+frame_range[0]):06d}.png')), img)
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.18
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.11 is installed. If not, install it [from there](https://www.python.org/downloads/).
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.10 -y
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`: Will detect poses only every 50 frames, and track keypoints in between, which is faster.
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, keypoints can be tracked instead of detected for a certain number of frames. Any RTMPose model can be used.
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 the right persons to keep.** In single-person mode, only keeps the person with the highest average scores over the sequence. In multi-person mode, only retrieves the keypoints with high enough confidence, and only keeps the persons with high enough average confidence over each frame.
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 a calibration file, or simply the size of a specified person. 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.
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. Outliers are rejected with a Hampel filter. Results are filtered with a 6 Hz Butterworth filter. Many other filters are available, and all of the above can be configured or deactivated (see [Config_Demo.toml](https://github.com/davidpagnon/Sports2D/blob/main/Sports2D/Demo/Config_demo.toml))
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=bDKhKftfDQucuwnVnoXrtHYrMe8qrOP33B6P1wy2wLE,120632
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=Ec504-4iuAvw5TDNT7upyoPRcs09EIe4Dteph3ybFJA,4702
20
- sports2d-0.8.18.dist-info/licenses/LICENSE,sha256=f4qe3nE0Y7ltJho5w-xAR0jI5PUox5Xl-MsYiY7ZRM8,1521
21
- sports2d-0.8.18.dist-info/METADATA,sha256=1X_RdFwHijFHWjOpFrmXkGTAnqCzVXnlAl-ZHnosNNQ,40492
22
- sports2d-0.8.18.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
- sports2d-0.8.18.dist-info/entry_points.txt,sha256=V8dFDIXatz9VvoGgoHzb2wE71C9-f85K6_OjnEQlxww,108
24
- sports2d-0.8.18.dist-info/top_level.txt,sha256=cWWBiDD2WbQXMoIoN6-9et9U2t2c_ZKo2JtBqO5uN-k,17
25
- sports2d-0.8.18.dist-info/RECORD,,
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,,