sports2d 0.5.4__py3-none-any.whl → 0.5.6__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.
@@ -21,7 +21,7 @@ compare = false # Not implemented yet
21
21
 
22
22
  # Video parameters
23
23
  time_range = [] # [] for the whole video, or [start_time, end_time] (in seconds), or [[start_time1, end_time1], [start_time2, end_time2], ...]
24
- video_dir = '' # If empty, result dir is current dir
24
+ video_dir = '' # If empty, video dir is current dir
25
25
 
26
26
  # Webcam parameters
27
27
  webcam_id = 0 # your webcam id (0 is default)
Sports2D/process.py CHANGED
@@ -1101,7 +1101,7 @@ def get_personID_with_highest_scores(all_frames_scores):
1101
1101
  return person_id
1102
1102
 
1103
1103
 
1104
- def compute_floor_line(trc_data, keypoint_names = ['LBigToe', 'RBigToe'], speed_threshold = 2.5):
1104
+ def compute_floor_line(trc_data, keypoint_names = ['LBigToe', 'RBigToe'], toe_speed_below = 1.0, tot_speed_above=2.0):
1105
1105
  '''
1106
1106
  Compute the floor line equation and angle
1107
1107
  from the feet keypoints when they have zero speed.
@@ -1111,19 +1111,23 @@ def compute_floor_line(trc_data, keypoint_names = ['LBigToe', 'RBigToe'], speed_
1111
1111
  INPUTS:
1112
1112
  - trc_data: pd.DataFrame. The trc data
1113
1113
  - keypoint_names: list of str. The names of the keypoints to use
1114
- - speed_threshold: float. The speed threshold below which the keypoints are considered as not moving
1114
+ - toe_speed_below: float. The speed threshold (px/frame) below which the keypoints are considered as not moving
1115
1115
 
1116
1116
  OUTPUT:
1117
1117
  - angle: float. The angle of the floor line in radians
1118
1118
  - xy_origin: list. The origin of the floor line
1119
1119
  '''
1120
1120
 
1121
+ # Remove frames where the person is mostly not moving (outlier)
1122
+ av_speeds = np.nanmean([np.insert(np.linalg.norm(trc_data[kpt].diff(), axis=1)[1:],0,0) for kpt in trc_data.columns.unique()[1:]], axis=0)
1123
+ trc_data = trc_data[av_speeds>tot_speed_above]
1124
+
1121
1125
  # Retrieve zero-speed coordinates for the foot
1122
1126
  low_speeds_X, low_speeds_Y = [], []
1123
1127
  for kpt in keypoint_names:
1124
1128
  speeds = np.linalg.norm(trc_data[kpt].diff(), axis=1)
1125
1129
 
1126
- low_speed_frames = trc_data[speeds<speed_threshold].index
1130
+ low_speed_frames = trc_data[speeds<toe_speed_below].index
1127
1131
  low_speeds_coords = trc_data[kpt].loc[low_speed_frames]
1128
1132
  low_speeds_coords = low_speeds_coords[low_speeds_coords!=0]
1129
1133
 
@@ -1416,7 +1420,6 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1416
1420
  gaussian_filter_kernel = config_dict.get('post-processing').get('gaussian').get('sigma_kernel')
1417
1421
  loess_filter_kernel = config_dict.get('post-processing').get('loess').get('nb_values_used')
1418
1422
  median_filter_kernel = config_dict.get('post-processing').get('median').get('kernel_size')
1419
- butterworth_filter_cutoff /= slowmo_factor
1420
1423
  filter_options = [do_filter, filter_type,
1421
1424
  butterworth_filter_order, butterworth_filter_cutoff, frame_rate,
1422
1425
  gaussian_filter_kernel, loess_filter_kernel, median_filter_kernel]
@@ -1453,6 +1456,7 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1453
1456
  logging.warning('Webcam input: the framerate may vary. If results are filtered, Sports2D will use the average framerate as input.')
1454
1457
  else:
1455
1458
  cap, out_vid, cam_width, cam_height, fps = setup_video(video_file_path, save_vid, vid_output_path)
1459
+ fps *= slowmo_factor
1456
1460
  start_time = get_start_time_ffmpeg(video_file_path)
1457
1461
  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))]
1458
1462
  frame_iterator = tqdm(range(*frame_range)) # use a progress bar
@@ -1466,7 +1470,7 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1466
1470
  if load_trc:
1467
1471
  if not '_px' in str(load_trc):
1468
1472
  logging.error(f'\n{load_trc} file needs to be in px, not in meters.')
1469
- logging.info(f'\nUsing a pose file instead of running pose tracking {load_trc}.')
1473
+ logging.info(f'\nUsing a pose file instead of running pose estimation and tracking: {load_trc}.')
1470
1474
  # Load pose file in px
1471
1475
  Q_coords, _, _, keypoints_names, _ = read_trc(load_trc)
1472
1476
  keypoints_ids = [i for i in range(len(keypoints_names))]
@@ -1501,7 +1505,7 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1501
1505
  frame_count = 0
1502
1506
  while cap.isOpened():
1503
1507
  # Skip to the starting frame
1504
- if frame_count < frame_range[0]:
1508
+ if frame_count < frame_range[0] and not load_trc:
1505
1509
  cap.read()
1506
1510
  frame_count += 1
1507
1511
  continue
@@ -1528,6 +1532,8 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1528
1532
 
1529
1533
  # Retrieve pose or Estimate pose and track people
1530
1534
  if load_trc:
1535
+ if frame_nb >= len(keypoints_all):
1536
+ break
1531
1537
  keypoints = keypoints_all[frame_nb]
1532
1538
  scores = scores_all[frame_nb]
1533
1539
  else:
@@ -1545,18 +1551,23 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1545
1551
  valid_X, valid_Y, valid_scores = [], [], []
1546
1552
  valid_X_flipped, valid_angles = [], []
1547
1553
  for person_idx in range(len(keypoints)):
1548
- # Retrieve keypoints and scores for the person, remove low-confidence keypoints
1549
- person_X, person_Y = np.where(scores[person_idx][:, np.newaxis] < keypoint_likelihood_threshold, np.nan, keypoints[person_idx]).T
1550
- person_scores = np.where(scores[person_idx] < keypoint_likelihood_threshold, np.nan, scores[person_idx])
1551
-
1552
- # Skip person if the fraction of valid detected keypoints is too low
1553
- enough_good_keypoints = len(person_scores[~np.isnan(person_scores)]) >= len(person_scores) * keypoint_number_threshold
1554
- scores_of_good_keypoints = person_scores[~np.isnan(person_scores)]
1555
- average_score_of_remaining_keypoints_is_enough = (np.nanmean(scores_of_good_keypoints) if len(scores_of_good_keypoints)>0 else 0) >= average_likelihood_threshold
1556
- if not enough_good_keypoints or not average_score_of_remaining_keypoints_is_enough:
1557
- person_X = np.full_like(person_X, np.nan)
1558
- person_Y = np.full_like(person_Y, np.nan)
1559
- person_scores = np.full_like(person_scores, np.nan)
1554
+ if load_trc:
1555
+ person_X = keypoints[person_idx][:,0]
1556
+ person_Y = keypoints[person_idx][:,1]
1557
+ person_scores = scores[person_idx]
1558
+ else:
1559
+ # Retrieve keypoints and scores for the person, remove low-confidence keypoints
1560
+ person_X, person_Y = np.where(scores[person_idx][:, np.newaxis] < keypoint_likelihood_threshold, np.nan, keypoints[person_idx]).T
1561
+ person_scores = np.where(scores[person_idx] < keypoint_likelihood_threshold, np.nan, scores[person_idx])
1562
+
1563
+ # Skip person if the fraction of valid detected keypoints is too low
1564
+ enough_good_keypoints = len(person_scores[~np.isnan(person_scores)]) >= len(person_scores) * keypoint_number_threshold
1565
+ scores_of_good_keypoints = person_scores[~np.isnan(person_scores)]
1566
+ average_score_of_remaining_keypoints_is_enough = (np.nanmean(scores_of_good_keypoints) if len(scores_of_good_keypoints)>0 else 0) >= average_likelihood_threshold
1567
+ if not enough_good_keypoints or not average_score_of_remaining_keypoints_is_enough:
1568
+ person_X = np.full_like(person_X, np.nan)
1569
+ person_Y = np.full_like(person_Y, np.nan)
1570
+ person_scores = np.full_like(person_scores, np.nan)
1560
1571
  valid_X.append(person_X)
1561
1572
  valid_Y.append(person_Y)
1562
1573
  valid_scores.append(person_scores)
@@ -1632,9 +1643,10 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1632
1643
  all_frames_scores = make_homogeneous(all_frames_scores)
1633
1644
 
1634
1645
  frame_range = [0,frame_count] if video_file == 'webcam' else frame_range
1635
- all_frames_time = pd.Series(np.linspace(frame_range[0]/fps/slowmo_factor, frame_range[1]/fps/slowmo_factor, frame_count+1), name='time')
1646
+ all_frames_time = pd.Series(np.linspace(frame_range[0]/fps, frame_range[1]/fps, frame_count+1), name='time')
1636
1647
  if not multiperson:
1637
- detected_persons = [get_personID_with_highest_scores(all_frames_scores)]
1648
+ calib_on_person_id = get_personID_with_highest_scores(all_frames_scores)
1649
+ detected_persons = [calib_on_person_id]
1638
1650
  else:
1639
1651
  detected_persons = range(all_frames_X_homog.shape[1])
1640
1652
 
@@ -1685,15 +1697,13 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1685
1697
  else:
1686
1698
  filter_type = filter_options[1]
1687
1699
  if filter_type == 'butterworth':
1700
+ cutoff = filter_options[3]
1688
1701
  if video_file == 'webcam':
1689
- cutoff = filter_options[3]
1690
1702
  if cutoff / (fps / 2) >= 1:
1691
1703
  cutoff_old = cutoff
1692
1704
  cutoff = fps/(2+0.001)
1693
1705
  args = f'\n{cutoff_old:.1f} Hz cut-off framerate too large for a real-time framerate of {fps:.1f} Hz. Using a cut-off framerate of {cutoff:.1f} Hz instead.'
1694
1706
  filter_options[3] = cutoff
1695
- else:
1696
- args = ''
1697
1707
  args = f'Butterworth filter, {filter_options[2]}th order, {filter_options[3]} Hz.'
1698
1708
  filter_options[4] = fps
1699
1709
  if filter_type == 'gaussian':
@@ -1730,38 +1740,44 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1730
1740
 
1731
1741
  else:
1732
1742
  # Compute calibration parameters
1743
+ if not multiperson:
1744
+ selected_person_id = calib_on_person_id
1745
+ calib_on_person_id = 0
1733
1746
  height_px = compute_height(trc_data[calib_on_person_id].iloc[:,1:], keypoints_names,
1734
1747
  fastest_frames_to_remove_percent=fastest_frames_to_remove_percent, close_to_zero_speed=close_to_zero_speed_px, large_hip_knee_angles=large_hip_knee_angles, trimmed_extrema_percent=trimmed_extrema_percent)
1735
1748
 
1736
1749
  if floor_angle == 'auto' or xy_origin == 'auto':
1737
1750
  # estimated from the line formed by the toes when they are on the ground (where speed = 0)
1738
- floor_angle_estim, xy_origin_estim = compute_floor_line(trc_data[calib_on_person_id], keypoint_names=['LBigToe', 'RBigToe'])
1751
+ toe_speed_below = 1 # m/s (below which the foot is considered to be stationary)
1752
+ px_per_m = height_px/person_height_m
1753
+ toe_speed_below_px_frame = toe_speed_below * px_per_m / fps
1754
+ floor_angle_estim, xy_origin_estim = compute_floor_line(trc_data[calib_on_person_id], keypoint_names=['LBigToe', 'RBigToe'], toe_speed_below=toe_speed_below_px_frame)
1739
1755
  if not floor_angle == 'auto':
1740
1756
  floor_angle_estim = floor_angle
1741
- floor_angle_estim = -floor_angle_estim # Y points downwards
1742
1757
  if xy_origin == 'auto':
1743
1758
  cx, cy = xy_origin_estim
1744
1759
  else:
1745
1760
  cx, cy = xy_origin
1746
1761
  logging.info(f'Using height of person #{calib_on_person_id} ({person_height_m}m) to convert coordinates in meters. '
1747
- f'Floor angle: {np.degrees(-floor_angle_estim) if not floor_angle=="auto" else f"auto (estimation: {round(np.degrees(-floor_angle_estim),2)}°)"}, '
1748
- f'xy_origin: {xy_origin if not xy_origin=="auto" else f"auto (estimation: {[round(c,2) for c in xy_origin_estim]})"}.')
1762
+ f'Floor angle: {np.degrees(floor_angle_estim) if not floor_angle=="auto" else f"auto (estimation: {round(np.degrees(floor_angle_estim),2)}°)"}, '
1763
+ f'xy_origin: {xy_origin if not xy_origin=="auto" else f"auto (estimation: {[round(c) for c in xy_origin_estim]})"}.')
1749
1764
 
1750
1765
  # Coordinates in m
1751
1766
  for i in range(len(trc_data)):
1752
1767
  if not np.array(trc_data[i].iloc[:,1:] ==0).all():
1753
- trc_data_m_i = pd.concat([convert_px_to_meters(trc_data[i][kpt_name], person_height_m, height_px, cx, cy, floor_angle_estim) for kpt_name in keypoints_names], axis=1)
1768
+ trc_data_m_i = pd.concat([convert_px_to_meters(trc_data[i][kpt_name], person_height_m, height_px, cx, cy, -floor_angle_estim) for kpt_name in keypoints_names], axis=1)
1754
1769
  trc_data_m_i.insert(0, 't', all_frames_time)
1755
- trc_data_unfiltered_m_i = pd.concat([convert_px_to_meters(trc_data_unfiltered[i][kpt_name], person_height_m, height_px, cx, cy, floor_angle_estim) for kpt_name in keypoints_names], axis=1)
1770
+ trc_data_unfiltered_m_i = pd.concat([convert_px_to_meters(trc_data_unfiltered[i][kpt_name], person_height_m, height_px, cx, cy, -floor_angle_estim) for kpt_name in keypoints_names], axis=1)
1756
1771
  trc_data_unfiltered_m_i.insert(0, 't', all_frames_time)
1757
1772
 
1758
1773
  if to_meters and show_plots:
1759
1774
  pose_plots(trc_data_unfiltered_m_i, trc_data_m_i, i)
1760
1775
 
1761
1776
  # Write to trc file
1762
- pose_path_person_m_i = (pose_output_path.parent / (pose_output_path_m.stem + f'_person{i:02d}.trc'))
1777
+ idx_path = selected_person_id if not multiperson and not calib_file else i
1778
+ pose_path_person_m_i = (pose_output_path.parent / (pose_output_path_m.stem + f'_person{idx_path:02d}.trc'))
1763
1779
  make_trc_with_trc_data(trc_data_m_i, pose_path_person_m_i)
1764
- logging.info(f'Person {i}: Pose in meters saved to {pose_path_person_m_i.resolve()}.')
1780
+ logging.info(f'Person {idx_path}: Pose in meters saved to {pose_path_person_m_i.resolve()}.')
1765
1781
 
1766
1782
 
1767
1783
 
@@ -1815,6 +1831,13 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1815
1831
  if save_angles and calculate_angles:
1816
1832
  logging.info('\nPost-processing angles:')
1817
1833
  all_frames_angles = make_homogeneous(all_frames_angles)
1834
+
1835
+ # unwrap angles
1836
+ # all_frames_angles = np.unwrap(all_frames_angles, axis=0, period=180) # This give all nan values -> need to mask nans
1837
+ for i in range(all_frames_angles.shape[1]): # for each person
1838
+ for j in range(all_frames_angles.shape[2]): # for each angle
1839
+ valid_mask = ~np.isnan(all_frames_angles[:, i, j])
1840
+ all_frames_angles[valid_mask, i, j] = np.unwrap(all_frames_angles[valid_mask, i, j], period=180)
1818
1841
 
1819
1842
  # Process angles for each person
1820
1843
  for i in detected_persons:
@@ -1846,16 +1869,14 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1846
1869
  else:
1847
1870
  filter_type = filter_options[1]
1848
1871
  if filter_type == 'butterworth':
1872
+ cutoff = filter_options[3]
1849
1873
  if video_file == 'webcam':
1850
- cutoff = filter_options[3]
1851
1874
  if cutoff / (fps / 2) >= 1:
1852
1875
  cutoff_old = cutoff
1853
1876
  cutoff = fps/(2+0.001)
1854
1877
  args = f'\n{cutoff_old:.1f} Hz cut-off framerate too large for a real-time framerate of {fps:.1f} Hz. Using a cut-off framerate of {cutoff:.1f} Hz instead.'
1855
1878
  filter_options[3] = cutoff
1856
- else:
1857
- args = ''
1858
- args = f'Butterworth filter, {filter_options[2]}th order, {filter_options[3]} Hz. ' + args
1879
+ args = f'Butterworth filter, {filter_options[2]}th order, {filter_options[3]} Hz.'
1859
1880
  filter_options[4] = fps
1860
1881
  if filter_type == 'gaussian':
1861
1882
  args = f'Gaussian filter, Sigma kernel {filter_options[5]}.'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sports2d
3
- Version: 0.5.4
3
+ Version: 0.5.6
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
@@ -233,9 +233,10 @@ Note that it does not take distortions into account, and that it will be less ac
233
233
  ### Too slow for you?
234
234
 
235
235
  **Quick fixes:**
236
- - Use `--multiperson false`: Can be used if one single person is present in the video. Otherwise, persons' IDs may be mixed up.
236
+ - Use ` --save_vid false --save_img false --show_realtime_results false`: Will not save images or videos, and will not display the results in real time.
237
237
  - Use `--mode lightweight`: Will use a lighter version of RTMPose, which is faster but less accurate.
238
238
  - Use `--det_frequency 50`: Will detect poses only every 50 frames, and track keypoints in between, which is faster.
239
+ - Use `--multiperson false`: Can be used if one single person is present in the video. Otherwise, persons' IDs may be mixed up.
239
240
  - Use `--load_trc <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.
240
241
 
241
242
  <br>
@@ -1,16 +1,16 @@
1
1
  Sports2D/Sports2D.py,sha256=dZv4xglguFJZDu9Zv0AZKAGu1TQIW9ynmY8pMsNHw14,26377
2
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
3
+ Sports2D/process.py,sha256=uX35szjJ6T7tOcqXQsljnS6xD1T9ufo2_hhJFI29cKo,89117
4
+ Sports2D/Demo/Config_demo.toml,sha256=CeHY91RXrt26TzvtXnCq7Hp2gMdu3EX-flZxaH0DqyA,6768
5
5
  Sports2D/Demo/demo.mp4,sha256=2aZkFxhWR7ESMEtXCT8MGA83p2jmoU2sp1ylQfO3gDk,3968304
6
6
  Sports2D/Utilities/__init__.py,sha256=TyCP7Uuuy6CNklhPf8W84MbYoO1_-1dxowSYAJyk_OI,102
7
7
  Sports2D/Utilities/common.py,sha256=FEWmlq9HNlHzA2ioV5MPPOeC-5Py4JaDbIIxQgq9hGE,14128
8
8
  Sports2D/Utilities/filter.py,sha256=8mVefMjDzxmh9a30eNtIrUuK_mUKoOJ2Nr-OzcQKkKM,4922
9
9
  Sports2D/Utilities/skeletons.py,sha256=44IWpz47zjh_6YDqkwaJnSysaGi7ovgYE25ji-hC-Kw,15660
10
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,,
11
+ sports2d-0.5.6.dist-info/LICENSE,sha256=f4qe3nE0Y7ltJho5w-xAR0jI5PUox5Xl-MsYiY7ZRM8,1521
12
+ sports2d-0.5.6.dist-info/METADATA,sha256=X6YrqZQnm0k9MG59MJe3pguWBwxR0mJ7-c0SWc-67FY,23221
13
+ sports2d-0.5.6.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
14
+ sports2d-0.5.6.dist-info/entry_points.txt,sha256=h2CJTuydtNf8JyaLoWxWl5HTSIxx5Ra_FSiSGQsf7Sk,52
15
+ sports2d-0.5.6.dist-info/top_level.txt,sha256=DoURf9UDB8lQ_9lMUPQMQqhXCvWPFFjJco9NzPlHJ6I,9
16
+ sports2d-0.5.6.dist-info/RECORD,,