sports2d 0.5.2__py3-none-any.whl → 0.5.4__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.
@@ -102,7 +102,7 @@ show_graphs = true # Show plots of raw and processed results
102
102
  filter_type = 'butterworth' # butterworth, gaussian, LOESS, median
103
103
  [post-processing.butterworth]
104
104
  order = 4
105
- cut_off_frequency = 3 # Hz
105
+ cut_off_frequency = 6 # Hz # Will be divided by slowmo_factor to be equivalent to non slowed-down video
106
106
  [post-processing.gaussian]
107
107
  sigma_kernel = 1 #px
108
108
  [post-processing.loess]
@@ -119,4 +119,7 @@ person_orientation = ['front', 'none', 'left'] # Choose among 'auto', 'none', 'f
119
119
  # Example with one person on one video: ['front']
120
120
  # Or ['front', 'none', 'left'] with 3 persons on one video
121
121
  osim_setup_path = '../OpenSim_setup' # Path to the OpenSim setup folder
122
- close_to_zero_speed_m = 0.2 # Sum for all keypoints: about 50 px/frame or 0.2 m/frame
122
+ close_to_zero_speed_m = 0.2 # Sum for all keypoints: about 50 px/frame or 0.2 m/frame
123
+
124
+ [logging]
125
+ use_custom_logging = false # if integrated in an API that already has logging
Sports2D/Sports2D.py CHANGED
@@ -194,7 +194,7 @@ DEFAULT_CONFIG = {'project': {'video_input': ['demo.mp4'],
194
194
  'filter': True,
195
195
  'show_graphs': True,
196
196
  'filter_type': 'butterworth',
197
- 'butterworth': {'order': 4, 'cut_off_frequency': 3},
197
+ 'butterworth': {'order': 4, 'cut_off_frequency': 6},
198
198
  'gaussian': {'sigma_kernel': 1},
199
199
  'loess': {'nb_values_used': 5},
200
200
  'median': {'kernel_size': 3}
@@ -203,7 +203,8 @@ DEFAULT_CONFIG = {'project': {'video_input': ['demo.mp4'],
203
203
  'person_orientation': ['front', '', 'left'],
204
204
  'osim_setup_path': '../OpenSim_setup',
205
205
  'close_to_zero_speed_m': 0.2
206
- }
206
+ },
207
+ 'logging': {'use_custom_logging': False}
207
208
  }
208
209
 
209
210
  CONFIG_HELP = {'config': ["C", "path to a toml configuration file"],
@@ -260,7 +261,8 @@ CONFIG_HELP = {'config': ["C", "path to a toml configuration file"],
260
261
  'cut_off_frequency': ["", "cut-off frequency of the Butterworth filter. 3 if not specified"],
261
262
  'sigma_kernel': ["", "sigma of the gaussian filter. 1 if not specified"],
262
263
  'nb_values_used': ["", "number of values used for the loess filter. 5 if not specified"],
263
- 'kernel_size': ["", "kernel size of the median filter. 3 if not specified"]
264
+ 'kernel_size': ["", "kernel size of the median filter. 3 if not specified"],
265
+ 'use_custom_logging': ["", "use custom logging. false if not specified"]
264
266
  }
265
267
 
266
268
 
@@ -414,11 +416,13 @@ def process(config='Config_demo.toml'):
414
416
  else:
415
417
  config_dict = read_config_file(config)
416
418
  video_dir, video_files, frame_rates, time_ranges, result_dir = base_params(config_dict)
419
+ use_custom_logging = config_dict.get('logging').get('use_custom_logging')
417
420
 
418
421
  result_dir.mkdir(parents=True, exist_ok=True)
419
- with open(result_dir / 'logs.txt', 'a+') as log_f: pass
420
- logging.basicConfig(format='%(message)s', level=logging.INFO, force=True,
421
- handlers = [logging.handlers.TimedRotatingFileHandler(result_dir / 'logs.txt', when='D', interval=7), logging.StreamHandler()])
422
+ if not use_custom_logging:
423
+ with open(result_dir / 'logs.txt', 'a+') as log_f: pass
424
+ logging.basicConfig(format='%(message)s', level=logging.INFO, force=True,
425
+ handlers = [logging.handlers.TimedRotatingFileHandler(result_dir / 'logs.txt', when='D', interval=7), logging.StreamHandler()])
422
426
 
423
427
  for video_file, time_range, frame_rate in zip(video_files, time_ranges, frame_rates):
424
428
  currentDateAndTime = datetime.now()
@@ -20,6 +20,7 @@ import sys
20
20
  import toml
21
21
  import subprocess
22
22
  from pathlib import Path
23
+ import logging
23
24
 
24
25
  import numpy as np
25
26
  from scipy import interpolate
@@ -164,21 +165,31 @@ def make_homogeneous(list_of_arrays):
164
165
  '''
165
166
 
166
167
  def get_max_shape(list_of_arrays):
168
+ '''
169
+ Recursively determine the maximum shape of a list of arrays.
170
+ '''
167
171
  if isinstance(list_of_arrays[0], list):
168
172
  # Maximum length at the current level plus the max shape at the next level
169
173
  return [max(len(arr) for arr in list_of_arrays)] + get_max_shape(
170
174
  [item for sublist in list_of_arrays for item in sublist])
171
175
  else:
172
176
  # Determine the maximum shape across all list_of_arrays at this level
173
- return [len(list_of_arrays)] + [max(arr.shape[i] for arr in list_of_arrays) for i in range(list_of_arrays[0].ndim)]
177
+ return [len(list_of_arrays)] + [max(arr.shape[i] for arr in list_of_arrays if arr.size > 0) for i in range(list_of_arrays[0].ndim)]
174
178
 
175
179
  def pad_with_nans(list_of_arrays, target_shape):
176
180
  '''
177
181
  Recursively pad list_of_arrays with nans to match the target shape.
178
182
  '''
179
- if isinstance(list_of_arrays, np.ndarray):
180
- # Pad the current array to the target shape
181
- pad_width = [(0, max_dim - curr_dim) for curr_dim, max_dim in zip(list_of_arrays.shape, target_shape)]
183
+ if isinstance(list_of_arrays, np.ndarray):
184
+ # Pad the current array to the target shape
185
+ pad_width = []
186
+ for dim_index in range(0, len(target_shape)):
187
+ if dim_index == len(list_of_arrays.shape) or dim_index > len(list_of_arrays.shape):
188
+ list_of_arrays = np.expand_dims(list_of_arrays, 0)
189
+ for dim_index in range(0, len(target_shape)):
190
+ max_dim = target_shape[dim_index]
191
+ curr_dim = list_of_arrays.shape[dim_index]
192
+ pad_width.append((0, max_dim - curr_dim))
182
193
  return np.pad(list_of_arrays.astype(float), pad_width, constant_values=np.nan)
183
194
  # Recursively pad each array in the list
184
195
  return [pad_with_nans(array, target_shape[1:]) for array in list_of_arrays]
@@ -191,6 +202,28 @@ def make_homogeneous(list_of_arrays):
191
202
  return np.array(list_of_arrays)
192
203
 
193
204
 
205
+ def get_start_time_ffmpeg(video_path):
206
+ '''
207
+ Get the start time of a video using FFmpeg.
208
+ '''
209
+
210
+ try:
211
+ ffmpeg_path = ffmpeg.get_ffmpeg_exe()
212
+ except Exception as e:
213
+ logging.warning(f"No ffmpeg exe could be found. Starting time set to 0.0. Error: {e}")
214
+ return 0.0
215
+
216
+ cmd = [ffmpeg_path, "-i", video_path]
217
+ result = subprocess.run(cmd, stderr=subprocess.PIPE, stdout=subprocess.DEVNULL, text=True)
218
+ for line in result.stderr.splitlines():
219
+ if "start:" in line:
220
+ parts = line.split("start:")
221
+ if len(parts) > 1:
222
+ start_time = parts[1].split(",")[0].strip()
223
+ return float(start_time)
224
+ return 0.0 # Default to 0 if not found
225
+
226
+
194
227
  def resample_video(vid_output_path, fps, desired_framerate):
195
228
  '''
196
229
  Resample video to the desired fps using ffmpeg.
Sports2D/process.py CHANGED
@@ -195,7 +195,7 @@ def setup_video(video_file_path, save_vid, vid_output_path):
195
195
  if video_file_path.name == video_file_path.stem:
196
196
  raise ValueError("Please set video_input to 'webcam' or to a video file (with extension) in Config.toml")
197
197
  try:
198
- cap = cv2.VideoCapture(video_file_path)
198
+ cap = cv2.VideoCapture(str(video_file_path.absolute()))
199
199
  if not cap.isOpened():
200
200
  raise
201
201
  except:
@@ -210,12 +210,12 @@ def setup_video(video_file_path, save_vid, vid_output_path):
210
210
  if save_vid:
211
211
  # try:
212
212
  # fourcc = cv2.VideoWriter_fourcc(*'avc1') # =h264. better compression and quality but may fail on some systems
213
- # out_vid = cv2.VideoWriter(vid_output_path, fourcc, fps, (cam_width, cam_height))
213
+ # out_vid = cv2.VideoWriter(str(vid_output_path.absolute()), fourcc, fps, (cam_width, cam_height))
214
214
  # if not out_vid.isOpened():
215
215
  # raise ValueError("Failed to open video writer with 'avc1' (h264)")
216
216
  # except Exception:
217
217
  fourcc = cv2.VideoWriter_fourcc(*'mp4v')
218
- out_vid = cv2.VideoWriter(vid_output_path, fourcc, fps, (cam_width, cam_height))
218
+ out_vid = cv2.VideoWriter(str(vid_output_path.absolute()), fourcc, fps, (cam_width, cam_height))
219
219
  # logging.info("Failed to open video writer with 'avc1' (h264). Using 'mp4v' instead.")
220
220
 
221
221
  return cap, out_vid, cam_width, cam_height, fps
@@ -1195,6 +1195,15 @@ def best_coords_for_measurements(Q_coords, keypoints_names, fastest_frames_to_re
1195
1195
  - Q_coords_low_speeds_low_angles: pd.DataFrame. The best coordinates for measurements
1196
1196
  '''
1197
1197
 
1198
+ # Add Hip column if not present
1199
+ n_markers_init = len(keypoints_names)
1200
+ if 'Hip' not in keypoints_names:
1201
+ RHip_df = Q_coords.iloc[:,keypoints_names.index('RHip')*3:keypoints_names.index('RHip')*3+3]
1202
+ LHip_df = Q_coords.iloc[:,keypoints_names.index('LHip')*3:keypoints_names.index('RHip')*3+3]
1203
+ Hip_df = RHip_df.add(LHip_df, fill_value=0) /2
1204
+ Hip_df.columns = [col+ str(int(Q_coords.columns[-1][1:])+1) for col in ['X','Y','Z']]
1205
+ keypoints_names += ['Hip']
1206
+ Q_coords = pd.concat([Q_coords, Hip_df], axis=1)
1198
1207
  n_markers = len(keypoints_names)
1199
1208
 
1200
1209
  # Using 80% slowest frames
@@ -1210,6 +1219,9 @@ def best_coords_for_measurements(Q_coords, keypoints_names, fastest_frames_to_re
1210
1219
  if len(Q_coords_low_speeds_low_angles) < 50:
1211
1220
  Q_coords_low_speeds_low_angles = Q_coords_low_speeds.iloc[pd.Series(ang_mean).nsmallest(50).index]
1212
1221
 
1222
+ if n_markers_init < n_markers:
1223
+ Q_coords_low_speeds_low_angles = Q_coords_low_speeds_low_angles.iloc[:,:-3]
1224
+
1213
1225
  return Q_coords_low_speeds_low_angles
1214
1226
 
1215
1227
 
@@ -1404,6 +1416,7 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1404
1416
  gaussian_filter_kernel = config_dict.get('post-processing').get('gaussian').get('sigma_kernel')
1405
1417
  loess_filter_kernel = config_dict.get('post-processing').get('loess').get('nb_values_used')
1406
1418
  median_filter_kernel = config_dict.get('post-processing').get('median').get('kernel_size')
1419
+ butterworth_filter_cutoff /= slowmo_factor
1407
1420
  filter_options = [do_filter, filter_type,
1408
1421
  butterworth_filter_order, butterworth_filter_cutoff, frame_rate,
1409
1422
  gaussian_filter_kernel, loess_filter_kernel, median_filter_kernel]
@@ -1440,7 +1453,8 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1440
1453
  logging.warning('Webcam input: the framerate may vary. If results are filtered, Sports2D will use the average framerate as input.')
1441
1454
  else:
1442
1455
  cap, out_vid, cam_width, cam_height, fps = setup_video(video_file_path, save_vid, vid_output_path)
1443
- frame_range = [int(time_range[0] * frame_rate), int(time_range[1] * frame_rate)] if time_range else [0, int(cap.get(cv2.CAP_PROP_FRAME_COUNT))]
1456
+ start_time = get_start_time_ffmpeg(video_file_path)
1457
+ 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))]
1444
1458
  frame_iterator = tqdm(range(*frame_range)) # use a progress bar
1445
1459
  if show_realtime_results:
1446
1460
  cv2.namedWindow(f'{video_file} Sports2D', cv2.WINDOW_NORMAL + cv2.WINDOW_KEEPRATIO)
@@ -1486,6 +1500,7 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1486
1500
  frame_processing_times = []
1487
1501
  frame_count = 0
1488
1502
  while cap.isOpened():
1503
+ # Skip to the starting frame
1489
1504
  if frame_count < frame_range[0]:
1490
1505
  cap.read()
1491
1506
  frame_count += 1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sports2d
3
- Version: 0.5.2
3
+ Version: 0.5.4
4
4
  Summary: Detect pose and compute 2D joint angles from a video.
5
5
  Home-page: https://github.com/davidpagnon/Sports2D
6
6
  Author: David Pagnon
@@ -0,0 +1,16 @@
1
+ Sports2D/Sports2D.py,sha256=dZv4xglguFJZDu9Zv0AZKAGu1TQIW9ynmY8pMsNHw14,26377
2
+ Sports2D/__init__.py,sha256=TyCP7Uuuy6CNklhPf8W84MbYoO1_-1dxowSYAJyk_OI,102
3
+ Sports2D/process.py,sha256=6rFjME0pSJXZpYD9t7Ai8P5Ly2MVpmKgZAo912j24kM,87663
4
+ Sports2D/Demo/Config_demo.toml,sha256=kp2iqohOLlN3vzFBDgz69BB8kpaYqcGXDQpchlxeO9w,6769
5
+ Sports2D/Demo/demo.mp4,sha256=2aZkFxhWR7ESMEtXCT8MGA83p2jmoU2sp1ylQfO3gDk,3968304
6
+ Sports2D/Utilities/__init__.py,sha256=TyCP7Uuuy6CNklhPf8W84MbYoO1_-1dxowSYAJyk_OI,102
7
+ Sports2D/Utilities/common.py,sha256=FEWmlq9HNlHzA2ioV5MPPOeC-5Py4JaDbIIxQgq9hGE,14128
8
+ Sports2D/Utilities/filter.py,sha256=8mVefMjDzxmh9a30eNtIrUuK_mUKoOJ2Nr-OzcQKkKM,4922
9
+ Sports2D/Utilities/skeletons.py,sha256=44IWpz47zjh_6YDqkwaJnSysaGi7ovgYE25ji-hC-Kw,15660
10
+ Sports2D/Utilities/tests.py,sha256=g06HBExGkvZrhZpNXN19G9Shisfgp1cqjAp0kFxiKEc,2574
11
+ sports2d-0.5.4.dist-info/LICENSE,sha256=f4qe3nE0Y7ltJho5w-xAR0jI5PUox5Xl-MsYiY7ZRM8,1521
12
+ sports2d-0.5.4.dist-info/METADATA,sha256=0EOJSFWnNfbndViXV-gYWglHkzkTeYvg5CIpBEK4pVs,23067
13
+ sports2d-0.5.4.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
14
+ sports2d-0.5.4.dist-info/entry_points.txt,sha256=h2CJTuydtNf8JyaLoWxWl5HTSIxx5Ra_FSiSGQsf7Sk,52
15
+ sports2d-0.5.4.dist-info/top_level.txt,sha256=DoURf9UDB8lQ_9lMUPQMQqhXCvWPFFjJco9NzPlHJ6I,9
16
+ sports2d-0.5.4.dist-info/RECORD,,
@@ -1,16 +0,0 @@
1
- Sports2D/Sports2D.py,sha256=toqCATE3D3JDYKp_SnCUdQekW45F_KsIHK1vurDS6n8,26103
2
- Sports2D/__init__.py,sha256=TyCP7Uuuy6CNklhPf8W84MbYoO1_-1dxowSYAJyk_OI,102
3
- Sports2D/process.py,sha256=EQcLyjxyn-oKysZ3iCZ1eSvY8LITTuxXDKmfVTKRNZ4,86767
4
- Sports2D/Demo/Config_demo.toml,sha256=03HL-4nMRVrSzL_NFRA6rjzSqirP6GfHjb1bAt8VeKU,6603
5
- Sports2D/Demo/demo.mp4,sha256=2aZkFxhWR7ESMEtXCT8MGA83p2jmoU2sp1ylQfO3gDk,3968304
6
- Sports2D/Utilities/__init__.py,sha256=TyCP7Uuuy6CNklhPf8W84MbYoO1_-1dxowSYAJyk_OI,102
7
- Sports2D/Utilities/common.py,sha256=-LoJGs_nyNPcj6cQfVQI9X5mQmv7ibTcYUfaEwFN2uU,12880
8
- Sports2D/Utilities/filter.py,sha256=8mVefMjDzxmh9a30eNtIrUuK_mUKoOJ2Nr-OzcQKkKM,4922
9
- Sports2D/Utilities/skeletons.py,sha256=44IWpz47zjh_6YDqkwaJnSysaGi7ovgYE25ji-hC-Kw,15660
10
- Sports2D/Utilities/tests.py,sha256=g06HBExGkvZrhZpNXN19G9Shisfgp1cqjAp0kFxiKEc,2574
11
- sports2d-0.5.2.dist-info/LICENSE,sha256=f4qe3nE0Y7ltJho5w-xAR0jI5PUox5Xl-MsYiY7ZRM8,1521
12
- sports2d-0.5.2.dist-info/METADATA,sha256=46UdMkJj9kmxHjsLaxBhyBo_ubE1cRZ8FDD0lFimfeA,23067
13
- sports2d-0.5.2.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
14
- sports2d-0.5.2.dist-info/entry_points.txt,sha256=h2CJTuydtNf8JyaLoWxWl5HTSIxx5Ra_FSiSGQsf7Sk,52
15
- sports2d-0.5.2.dist-info/top_level.txt,sha256=DoURf9UDB8lQ_9lMUPQMQqhXCvWPFFjJco9NzPlHJ6I,9
16
- sports2d-0.5.2.dist-info/RECORD,,