sports2d 0.6.1__py3-none-any.whl → 0.6.2__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.
@@ -54,7 +54,7 @@ mode = 'balanced' # 'lightweight', 'balanced', 'performance', or """{dictionary}
54
54
 
55
55
  # A dictionary (WITHIN THREE DOUBLE QUOTES) allows you to manually select the person detection (if top_down approach) and/or pose estimation models (see https://github.com/Tau-J/rtmlib).
56
56
  # Models can be local paths or URLs.
57
- # Make sure the input_sizes are within triple quotes, and that they are in the opposite order from the one in the model path (for example, it would be [192,256] for rtmpose-m_simcc-body7_pt-body7-halpe26_700e-256x192-4d3e73dd_20230605.zip).
57
+ # Make sure the input_sizes are within square brackets, and that they are in the opposite order from the one in the model path (for example, it would be [192,256] for rtmpose-m_simcc-body7_pt-body7-halpe26_700e-256x192-4d3e73dd_20230605.zip).
58
58
  # If your pose_model is not provided in skeletons.py, you may have to create your own one (see example at the end of the file).
59
59
  # Example, equivalent to mode='balanced':
60
60
  # mode = """{'det_class':'YOLOX',
@@ -68,17 +68,20 @@ mode = 'balanced' # 'lightweight', 'balanced', 'performance', or """{dictionary}
68
68
  # 'pose_model':'https://download.openmmlab.com/mmpose/v1/projects/rtmo/onnx_sdk/rtmo-m_16xb16-600e_body7-640x640-39e78cc4_20231211.zip',
69
69
  # 'pose_input_size':[640, 640]}"""
70
70
 
71
- det_frequency = 1 # Run person detection only every N frames, and inbetween track previously detected bounding boxes (keypoint detection is still run on all frames).
71
+ det_frequency = 4 # Run person detection only every N frames, and inbetween track previously detected bounding boxes (keypoint detection is still run on all frames).
72
72
  # Equal to or greater than 1, can be as high as you want in simple uncrowded cases. Much faster, but might be less accurate.
73
73
  device = 'auto' # 'auto', 'CPU', 'CUDA', 'MPS', 'ROCM'
74
74
  backend = 'auto' # 'auto', 'openvino', 'onnxruntime', 'opencv'
75
- tracking_mode = 'sports2d' # 'rtmlib' or 'sports2d'. 'sports2d' is generally much more accurate and comparable in speed
75
+ tracking_mode = 'sports2d' # 'sports2d' or 'deepsort'. 'deepsort' is slower but more robust in difficult configurations
76
+ deepsort_params = """{'max_age':30, 'n_init':3, 'nms_max_overlap':0.8, 'max_cosine_distance':0.3, 'nn_budget':200, 'max_iou_distance':0.8, 'embedder_gpu': True}""" # """{dictionary between 3 double quotes}"""
77
+ # More robust in crowded scenes but Can be tricky to parametrize. More information there: https://github.com/levan92/deep_sort_realtime/blob/master/deep_sort_realtime/deepsort_tracker.py#L51
78
+ # Note: For even more robust tracking, use 'embedder':'torchreid', which runs osnet_ain_x1_0 by default. Install additional dependencies with: `pip install torchreid gdown tensorboard`
76
79
 
77
80
 
78
81
  # Processing parameters
79
82
  keypoint_likelihood_threshold = 0.3 # Keypoints whose likelihood is lower will not be taken into account
80
83
  average_likelihood_threshold = 0.5 # Person will be ignored if average likelihood of good keypoints is lower than this value
81
- keypoint_number_threshold = 0.3 # Person will be ignored if the number of good keypoints is less than this fraction
84
+ keypoint_number_threshold = 0.3 # Person will be ignored if the number of good keypoints (above keypoint_likelihood_threshold) is less than this fraction
82
85
 
83
86
 
84
87
  [px_to_meters_conversion]
Sports2D/Sports2D.py CHANGED
@@ -146,6 +146,7 @@ DEFAULT_CONFIG = {'project': {'video_input': ['demo.mp4'],
146
146
  'device': 'auto',
147
147
  'backend': 'auto',
148
148
  'tracking_mode': 'sports2d',
149
+ 'deepsort_params': """{'max_age':30, 'n_init':3, 'nms_max_overlap':0.8, 'max_cosine_distance':0.3, 'nn_budget':200, 'max_iou_distance':0.8, 'embedder_gpu': True}""",
149
150
  'keypoint_likelihood_threshold': 0.3,
150
151
  'average_likelihood_threshold': 0.5,
151
152
  'keypoint_number_threshold': 0.3
@@ -248,7 +249,10 @@ CONFIG_HELP = {'config': ["C", "path to a toml configuration file"],
248
249
  'osim_setup_path': ["", "path to OpenSim setup. '../OpenSim_setup' if not specified"],
249
250
  'person_orientation': ["", "front, back, left, right, auto, or none. 'front none left' if not specified. If 'auto', will be either left or right depending on the direction of the motion."],
250
251
  'close_to_zero_speed_m': ["","Sum for all keypoints: about 50 px/frame or 0.2 m/frame"],
251
- 'multiperson': ["", "multiperson involves tracking: will be faster if set to false. true if not specified"], 'tracking_mode': ["", "sports2d or rtmlib. sports2d is generally much more accurate and comparable in speed. sports2d if not specified"],
252
+ 'multiperson': ["", "multiperson involves tracking: will be faster if set to false. true if not specified"],
253
+ 'tracking_mode': ["", "sports2d or rtmlib. sports2d is generally much more accurate and comparable in speed. sports2d if not specified"],
254
+ 'deepsort_params': ["", 'Deepsort tracking parameters: """{dictionary between 3 double quotes}""". \n\
255
+ More information there: https://github.com/levan92/deep_sort_realtime/blob/master/deep_sort_realtime/deepsort_tracker.py#L51'], #
252
256
  'input_size': ["", "width, height. 1280, 720 if not specified. Lower resolution will be faster but less precise"],
253
257
  'keypoint_likelihood_threshold': ["", "detected keypoints are not retained if likelihood is below this threshold. 0.3 if not specified"],
254
258
  'average_likelihood_threshold': ["", "detected persons are not retained if average keypoint likelihood is below this threshold. 0.5 if not specified"],
@@ -20,7 +20,9 @@ import sys
20
20
  import toml
21
21
  import subprocess
22
22
  from pathlib import Path
23
+ import itertools as it
23
24
  import logging
25
+ from anytree import PreOrderIter
24
26
 
25
27
  import numpy as np
26
28
  import pandas as pd
@@ -28,6 +30,7 @@ from scipy import interpolate
28
30
  import imageio_ffmpeg as ffmpeg
29
31
  import cv2
30
32
 
33
+ import matplotlib.pyplot as plt
31
34
  from PyQt5.QtWidgets import QMainWindow, QApplication, QWidget, QTabWidget, QVBoxLayout
32
35
  from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
33
36
  from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
@@ -466,7 +469,7 @@ def add_neck_hip_coords(kpt_name, p_X, p_Y, p_scores, kpt_ids, kpt_names):
466
469
  return p_X, p_Y, p_scores
467
470
 
468
471
 
469
- def best_coords_for_measurements(trc_data, keypoints_names, fastest_frames_to_remove_percent=0.2, close_to_zero_speed=0.2, large_hip_knee_angles=45):
472
+ def best_coords_for_measurements(Q_coords, keypoints_names, fastest_frames_to_remove_percent=0.2, close_to_zero_speed=0.2, large_hip_knee_angles=45):
470
473
  '''
471
474
  Compute the best coordinates for measurements, after removing:
472
475
  - 20% fastest frames (may be outliers)
@@ -474,7 +477,7 @@ def best_coords_for_measurements(trc_data, keypoints_names, fastest_frames_to_re
474
477
  - frames when hip and knee angle below 45° (imprecise coordinates when person is crouching)
475
478
 
476
479
  INPUTS:
477
- - trc_data: pd.DataFrame. The XYZ coordinates of each marker
480
+ - Q_coords: pd.DataFrame. The XYZ coordinates of each marker
478
481
  - keypoints_names: list. The list of marker names
479
482
  - fastest_frames_to_remove_percent: float
480
483
  - close_to_zero_speed: float (sum for all keypoints: about 50 px/frame or 0.2 m/frame)
@@ -482,44 +485,46 @@ def best_coords_for_measurements(trc_data, keypoints_names, fastest_frames_to_re
482
485
  - trimmed_extrema_percent
483
486
 
484
487
  OUTPUT:
485
- - trc_data_low_speeds_low_angles: pd.DataFrame. The best coordinates for measurements
488
+ - Q_coords_low_speeds_low_angles: pd.DataFrame. The best coordinates for measurements
486
489
  '''
487
490
 
488
491
  # Add MidShoulder column
489
- df_MidShoulder = pd.DataFrame((trc_data['RShoulder'].values + trc_data['LShoulder'].values) /2)
492
+ df_MidShoulder = pd.DataFrame((Q_coords['RShoulder'].values + Q_coords['LShoulder'].values) /2)
490
493
  df_MidShoulder.columns = ['MidShoulder']*3
491
- trc_data = pd.concat((trc_data.reset_index(drop=True), df_MidShoulder), axis=1)
494
+ Q_coords = pd.concat((Q_coords.reset_index(drop=True), df_MidShoulder), axis=1)
492
495
 
493
496
  # Add Hip column if not present
494
497
  n_markers_init = len(keypoints_names)
495
498
  if 'Hip' not in keypoints_names:
496
- df_Hip = pd.DataFrame((trc_data['RHip'].values + trc_data['LHip'].values) /2)
499
+ df_Hip = pd.DataFrame((Q_coords['RHip'].values + Q_coords['LHip'].values) /2)
497
500
  df_Hip.columns = ['Hip']*3
498
- trc_data = pd.concat((trc_data.reset_index(drop=True), df_Hip), axis=1)
501
+ Q_coords = pd.concat((Q_coords.reset_index(drop=True), df_Hip), axis=1)
499
502
  n_markers = len(keypoints_names)
500
503
 
501
504
  # Using 80% slowest frames
502
- sum_speeds = pd.Series(np.nansum([np.linalg.norm(trc_data.iloc[:,kpt:kpt+3].diff(), axis=1) for kpt in range(n_markers)], axis=0))
505
+ sum_speeds = pd.Series(np.nansum([np.linalg.norm(Q_coords.iloc[:,kpt:kpt+3].diff(), axis=1) for kpt in range(n_markers)], axis=0))
503
506
  sum_speeds = sum_speeds[sum_speeds>close_to_zero_speed] # Removing when speeds close to zero (out of frame)
504
507
  if len(sum_speeds)==0:
505
- raise ValueError('All frames have speed close to zero. Make sure the person is moving and correctly detected, or change close_to_zero_speed to a lower value.')
506
- min_speed_indices = sum_speeds.abs().nsmallest(int(len(sum_speeds) * (1-fastest_frames_to_remove_percent))).index
507
- trc_data_low_speeds = trc_data.iloc[min_speed_indices].reset_index(drop=True)
508
+ logging.warning('All frames have speed close to zero. Make sure the person is moving and correctly detected, or change close_to_zero_speed to a lower value. Not restricting the speeds to be above any threshold.')
509
+ Q_coords_low_speeds = Q_coords
510
+ else:
511
+ min_speed_indices = sum_speeds.abs().nsmallest(int(len(sum_speeds) * (1-fastest_frames_to_remove_percent))).index
512
+ Q_coords_low_speeds = Q_coords.iloc[min_speed_indices].reset_index(drop=True)
508
513
 
509
514
  # Only keep frames with hip and knee flexion angles below 45%
510
515
  # (if more than 50 of them, else take 50 smallest values)
511
516
  try:
512
- ang_mean = mean_angles(trc_data_low_speeds, ang_to_consider = ['right knee', 'left knee', 'right hip', 'left hip'])
513
- trc_data_low_speeds_low_angles = trc_data_low_speeds[ang_mean < large_hip_knee_angles]
514
- if len(trc_data_low_speeds_low_angles) < 50:
515
- trc_data_low_speeds_low_angles = trc_data_low_speeds.iloc[pd.Series(ang_mean).nsmallest(50).index]
517
+ ang_mean = mean_angles(Q_coords_low_speeds, ang_to_consider = ['right knee', 'left knee', 'right hip', 'left hip'])
518
+ Q_coords_low_speeds_low_angles = Q_coords_low_speeds[ang_mean < large_hip_knee_angles]
519
+ if len(Q_coords_low_speeds_low_angles) < 50:
520
+ Q_coords_low_speeds_low_angles = Q_coords_low_speeds.iloc[pd.Series(ang_mean).nsmallest(50).index]
516
521
  except:
517
- logging.warning(f"At least one among the RAnkle, RKnee, RHip, RShoulder, LAnkle, LKnee, LHip, LShoulder markers is missing for computing the knee and hip angles. Not restricting these agles to be below {large_hip_knee_angles}°.")
522
+ logging.warning(f"At least one among the RAnkle, RKnee, RHip, RShoulder, LAnkle, LKnee, LHip, LShoulder markers is missing for computing the knee and hip angles. Not restricting these angles to be below {large_hip_knee_angles}°.")
518
523
 
519
524
  if n_markers_init < n_markers:
520
- trc_data_low_speeds_low_angles = trc_data_low_speeds_low_angles.iloc[:,:-3]
525
+ Q_coords_low_speeds_low_angles = Q_coords_low_speeds_low_angles.iloc[:,:-3]
521
526
 
522
- return trc_data_low_speeds_low_angles
527
+ return Q_coords_low_speeds_low_angles
523
528
 
524
529
 
525
530
  def compute_height(trc_data, keypoints_names, fastest_frames_to_remove_percent=0.1, close_to_zero_speed=50, large_hip_knee_angles=45, trimmed_extrema_percent=0.5):
@@ -547,7 +552,7 @@ def compute_height(trc_data, keypoints_names, fastest_frames_to_remove_percent=0
547
552
  try:
548
553
  rfoot, lfoot = [euclidean_distance(trc_data_low_speeds_low_angles[pair[0]],trc_data_low_speeds_low_angles[pair[1]]) for pair in feet_pairs]
549
554
  except:
550
- rfoot, lfoot = 10, 10
555
+ rfoot, lfoot = 0.10, 0.10
551
556
  logging.warning('The Heel marker is missing from your model. Considering Foot to Heel size as 10 cm.')
552
557
 
553
558
  ankle_to_shoulder_pairs = [['RAnkle', 'RKnee'], ['RKnee', 'RHip'], ['RHip', 'RShoulder'],
@@ -688,4 +693,350 @@ def write_calibration(calib_params, toml_path):
688
693
  fish_str = f'fisheye = false\n\n'
689
694
  cal_f.write(cam_str + name_str + size_str + mat_str + dist_str + rot_str + tran_str + fish_str)
690
695
  meta = '[metadata]\nadjusted = false\nerror = 0.0\n'
691
- cal_f.write(meta)
696
+ cal_f.write(meta)
697
+
698
+
699
+ def pad_shape(arr, target_len, fill_value=np.nan):
700
+ '''
701
+ Pads an array to the target length with specified fill values
702
+
703
+ INPUTS:
704
+ - arr: Input array to be padded.
705
+ - target_len: The target length of the first dimension after padding.
706
+ - fill_value: The value to use for padding (default: np.nan).
707
+
708
+ OUTPUTS:
709
+ - Padded array with shape (target_len, ...) matching the input dimensions.
710
+ '''
711
+
712
+ if len(arr) < target_len:
713
+ pad_shape = (target_len - len(arr),) + arr.shape[1:]
714
+ padding = np.full(pad_shape, fill_value)
715
+ return np.concatenate((arr, padding))
716
+
717
+ return arr
718
+
719
+
720
+ def min_with_single_indices(L, T):
721
+ '''
722
+ Let L be a list (size s) with T associated tuple indices (size s).
723
+ Select the smallest values of L, considering that
724
+ the next smallest value cannot have the same numbers
725
+ in the associated tuple as any of the previous ones.
726
+
727
+ Example:
728
+ L = [ 20, 27, 51, 33, 43, 23, 37, 24, 4, 68, 84, 3 ]
729
+ T = list(it.product(range(2),range(3)))
730
+ = [(0,0),(0,1),(0,2),(0,3),(1,0),(1,1),(1,2),(1,3),(2,0),(2,1),(2,2),(2,3)]
731
+
732
+ - 1st smallest value: 3 with tuple (2,3), index 11
733
+ - 2nd smallest value when excluding indices (2,.) and (.,3), i.e. [(0,0),(0,1),(0,2),X,(1,0),(1,1),(1,2),X,X,X,X,X]:
734
+ 20 with tuple (0,0), index 0
735
+ - 3rd smallest value when excluding [X,X,X,X,X,(1,1),(1,2),X,X,X,X,X]:
736
+ 23 with tuple (1,1), index 5
737
+
738
+ INPUTS:
739
+ - L: list (size s)
740
+ - T: T associated tuple indices (size s)
741
+
742
+ OUTPUTS:
743
+ - minL: list of smallest values of L, considering constraints on tuple indices
744
+ - argminL: list of indices of smallest values of L (indices of best combinations)
745
+ - T_minL: list of tuples associated with smallest values of L
746
+ '''
747
+
748
+ minL = [np.nanmin(L)]
749
+ argminL = [np.nanargmin(L)]
750
+ T_minL = [T[argminL[0]]]
751
+
752
+ mask_tokeep = np.array([True for t in T])
753
+ i=0
754
+ while mask_tokeep.any()==True:
755
+ mask_tokeep = mask_tokeep & np.array([t[0]!=T_minL[i][0] and t[1]!=T_minL[i][1] for t in T])
756
+ if mask_tokeep.any()==True:
757
+ indicesL_tokeep = np.where(mask_tokeep)[0]
758
+ minL += [np.nanmin(np.array(L)[indicesL_tokeep]) if not np.isnan(np.array(L)[indicesL_tokeep]).all() else np.nan]
759
+ argminL += [indicesL_tokeep[np.nanargmin(np.array(L)[indicesL_tokeep])] if not np.isnan(minL[-1]) else indicesL_tokeep[0]]
760
+ T_minL += (T[argminL[i+1]],)
761
+ i+=1
762
+
763
+ return np.array(minL), np.array(argminL), np.array(T_minL)
764
+
765
+
766
+ def sort_people_sports2d(keyptpre, keypt, scores=None):
767
+ '''
768
+ Associate persons across frames (Sports2D method)
769
+ Persons' indices are sometimes swapped when changing frame
770
+ A person is associated to another in the next frame when they are at a small distance
771
+
772
+ N.B.: Requires min_with_single_indices and euclidian_distance function (see common.py)
773
+
774
+ INPUTS:
775
+ - keyptpre: (K, L, M) array of 2D coordinates for K persons in the previous frame, L keypoints, M 2D coordinates
776
+ - keypt: idem keyptpre, for current frame
777
+ - score: (K, L) array of confidence scores for K persons, L keypoints (optional)
778
+
779
+ OUTPUTS:
780
+ - sorted_prev_keypoints: array with reordered persons with values of previous frame if current is empty
781
+ - sorted_keypoints: array with reordered persons --> if scores is not None
782
+ - sorted_scores: array with reordered scores --> if scores is not None
783
+ - associated_tuples: list of tuples with correspondences between persons across frames --> if scores is None (for Pose2Sim.triangulation())
784
+ '''
785
+
786
+ # Generate possible person correspondences across frames
787
+ max_len = max(len(keyptpre), len(keypt))
788
+ keyptpre = pad_shape(keyptpre, max_len, fill_value=np.nan)
789
+ keypt = pad_shape(keypt, max_len, fill_value=np.nan)
790
+ if scores is not None:
791
+ scores = pad_shape(scores, max_len, fill_value=np.nan)
792
+
793
+ # Compute distance between persons from one frame to another
794
+ personsIDs_comb = sorted(list(it.product(range(len(keyptpre)), range(len(keypt)))))
795
+ frame_by_frame_dist = [euclidean_distance(keyptpre[comb[0]],keypt[comb[1]]) for comb in personsIDs_comb]
796
+ frame_by_frame_dist = np.mean(frame_by_frame_dist, axis=1)
797
+
798
+ # Sort correspondences by distance
799
+ _, _, associated_tuples = min_with_single_indices(frame_by_frame_dist, personsIDs_comb)
800
+
801
+ # Associate points to same index across frames, nan if no correspondence
802
+ sorted_keypoints = []
803
+ for i in range(len(keyptpre)):
804
+ id_in_old = associated_tuples[:,1][associated_tuples[:,0] == i].tolist()
805
+ if len(id_in_old) > 0: sorted_keypoints += [keypt[id_in_old[0]]]
806
+ else: sorted_keypoints += [keypt[i]]
807
+ sorted_keypoints = np.array(sorted_keypoints)
808
+
809
+ if scores is not None:
810
+ sorted_scores = []
811
+ for i in range(len(keyptpre)):
812
+ id_in_old = associated_tuples[:,1][associated_tuples[:,0] == i].tolist()
813
+ if len(id_in_old) > 0: sorted_scores += [scores[id_in_old[0]]]
814
+ else: sorted_scores += [scores[i]]
815
+ sorted_scores = np.array(sorted_scores)
816
+
817
+ # Keep track of previous values even when missing for more than one frame
818
+ sorted_prev_keypoints = np.where(np.isnan(sorted_keypoints) & ~np.isnan(keyptpre), keyptpre, sorted_keypoints)
819
+
820
+ if scores is not None:
821
+ return sorted_prev_keypoints, sorted_keypoints, sorted_scores
822
+ else: # For Pose2Sim.triangulation()
823
+ return sorted_keypoints, associated_tuples
824
+
825
+
826
+ def sort_people_rtmlib(pose_tracker, keypoints, scores):
827
+ '''
828
+ Associate persons across frames (RTMLib method)
829
+
830
+ INPUTS:
831
+ - pose_tracker: PoseTracker. The initialized RTMLib pose tracker object
832
+ - keypoints: array of shape K, L, M with K the number of detected persons,
833
+ L the number of detected keypoints, M their 2D coordinates
834
+ - scores: array of shape K, L with K the number of detected persons,
835
+ L the confidence of detected keypoints
836
+
837
+ OUTPUT:
838
+ - sorted_keypoints: array with reordered persons
839
+ - sorted_scores: array with reordered scores
840
+ '''
841
+
842
+ try:
843
+ desired_size = max(pose_tracker.track_ids_last_frame)+1
844
+ sorted_keypoints = np.full((desired_size, keypoints.shape[1], 2), np.nan)
845
+ sorted_keypoints[pose_tracker.track_ids_last_frame] = keypoints[:len(pose_tracker.track_ids_last_frame), :, :]
846
+ sorted_scores = np.full((desired_size, scores.shape[1]), np.nan)
847
+ sorted_scores[pose_tracker.track_ids_last_frame] = scores[:len(pose_tracker.track_ids_last_frame), :]
848
+ except:
849
+ sorted_keypoints, sorted_scores = keypoints, scores
850
+
851
+ return sorted_keypoints, sorted_scores
852
+
853
+
854
+ def sort_people_deepsort(keypoints, scores, deepsort_tracker, frame,frame_count):
855
+ '''
856
+ Associate persons across frames (DeepSort method)
857
+
858
+ INPUTS:
859
+ - keypoints: array of shape K, L, M with K the number of detected persons,
860
+ L the number of detected keypoints, M their 2D coordinates
861
+ - scores: array of shape K, L with K the number of detected persons,
862
+ L the confidence of detected keypoints
863
+ - deepsort_tracker: The initialized DeepSort tracker object
864
+ - frame: np.array. The current image opened with cv2.imread
865
+
866
+ OUTPUT:
867
+ - sorted_keypoints: array with reordered persons
868
+ - sorted_scores: array with reordered scores
869
+ '''
870
+
871
+ try:
872
+ # Compute bboxes from keypoints and create detections (bboxes, scores, class_ids)
873
+ bboxes_ltwh = bbox_ltwh_compute(keypoints, padding=20)
874
+ bbox_scores = np.mean(scores, axis=1)
875
+ class_ids = np.array(['person']*len(bboxes_ltwh))
876
+ detections = list(zip(bboxes_ltwh, bbox_scores, class_ids))
877
+
878
+ # Estimates the tracks and retrieve indexes of the original detections
879
+ det_ids = [i for i in range(len(detections))]
880
+ tracks = deepsort_tracker.update_tracks(detections, frame=frame, others=det_ids)
881
+ track_ids_frame, orig_det_ids = [], []
882
+ for track in tracks:
883
+ if not track.is_confirmed():
884
+ continue
885
+ track_ids_frame.append(int(track.track_id)-1) # ID of people
886
+ orig_det_ids.append(track.get_det_supplementary()) # ID of detections
887
+
888
+ # Correspondence between person IDs and original detection IDs
889
+ desired_size = max(track_ids_frame) + 1
890
+ sorted_keypoints = np.full((desired_size, keypoints.shape[1], 2), np.nan)
891
+ sorted_scores = np.full((desired_size, scores.shape[1]), np.nan)
892
+ for i,v in enumerate(track_ids_frame):
893
+ if orig_det_ids[i] is not None:
894
+ sorted_keypoints[v] = keypoints[orig_det_ids[i]]
895
+ sorted_scores[v] = scores[orig_det_ids[i]]
896
+
897
+ except Exception as e:
898
+ sorted_keypoints, sorted_scores = keypoints, scores
899
+ if frame_count > deepsort_tracker.tracker.n_init:
900
+ logging.warning(f"Tracking error: {e}. Sorting persons with DeepSort method failed for this frame.")
901
+
902
+ return sorted_keypoints, sorted_scores
903
+
904
+
905
+ def bbox_ltwh_compute(keypoints, padding=0):
906
+ '''
907
+ Compute bounding boxes in (x_min, y_min, width, height) format
908
+ Optionally add padding to the bounding boxes
909
+ as a percentage of the bounding box size (+padding% horizontally, +padding/2% vertically)
910
+
911
+ INPUTS:
912
+ - keypoints: array of shape K, L, M with K the number of detected persons,
913
+ L the number of detected keypoints, M their 2D coordinates
914
+ - padding: int. The padding to add to the bounding boxes, in perceptage
915
+ '''
916
+
917
+ x_coords = keypoints[:, :, 0]
918
+ y_coords = keypoints[:, :, 1]
919
+
920
+ x_min, x_max = np.min(x_coords, axis=1), np.max(x_coords, axis=1)
921
+ y_min, y_max = np.min(y_coords, axis=1), np.max(y_coords, axis=1)
922
+ width = x_max - x_min
923
+ height = y_max - y_min
924
+
925
+ if padding > 0:
926
+ x_min = x_min - width*padding/100
927
+ y_min = y_min - height/2*padding/100
928
+ width = width + 2*width*padding/100
929
+ height = height + height*padding/100
930
+
931
+ bbox_ltwh = np.stack((x_min, y_min, width, height), axis=1)
932
+
933
+ return bbox_ltwh
934
+
935
+
936
+ def draw_bounding_box(img, X, Y, colors=[(255, 0, 0), (0, 255, 0), (0, 0, 255)], fontSize=0.3, thickness=1):
937
+ '''
938
+ Draw bounding boxes and person ID around list of lists of X and Y coordinates.
939
+ Bounding boxes have a different color for each person.
940
+
941
+ INPUTS:
942
+ - img: opencv image
943
+ - X: list of list of x coordinates
944
+ - Y: list of list of y coordinates
945
+ - colors: list of colors to cycle through
946
+
947
+ OUTPUT:
948
+ - img: image with rectangles and person IDs
949
+ '''
950
+
951
+ color_cycle = it.cycle(colors)
952
+
953
+ for i,(x,y) in enumerate(zip(X,Y)):
954
+ color = next(color_cycle)
955
+ if not np.isnan(x).all():
956
+ x_min, y_min = np.nanmin(x).astype(int), np.nanmin(y).astype(int)
957
+ x_max, y_max = np.nanmax(x).astype(int), np.nanmax(y).astype(int)
958
+ if x_min < 0: x_min = 0
959
+ if x_max > img.shape[1]: x_max = img.shape[1]
960
+ if y_min < 0: y_min = 0
961
+ if y_max > img.shape[0]: y_max = img.shape[0]
962
+
963
+ # Draw rectangles
964
+ cv2.rectangle(img, (x_min-25, y_min-25), (x_max+25, y_max+25), color, thickness)
965
+
966
+ # Write person ID
967
+ cv2.putText(img, str(i), (x_min-30, y_min-30), cv2.FONT_HERSHEY_SIMPLEX, fontSize, color, 2, cv2.LINE_AA)
968
+
969
+ return img
970
+
971
+
972
+ def draw_skel(img, X, Y, model):
973
+ '''
974
+ Draws keypoints and skeleton for each person.
975
+ Skeletons have a different color for each person.
976
+
977
+ INPUTS:
978
+ - img: opencv image
979
+ - X: list of list of x coordinates
980
+ - Y: list of list of y coordinates
981
+ - model: skeleton model (from skeletons.py)
982
+ - colors: list of colors to cycle through
983
+
984
+ OUTPUT:
985
+ - img: image with keypoints and skeleton
986
+ '''
987
+
988
+ # Get (unique) pairs between which to draw a line
989
+ id_pairs, name_pairs = [], []
990
+ for data_i in PreOrderIter(model.root, filter_=lambda node: node.is_leaf):
991
+ node_branch_ids = [node_i.id for node_i in data_i.path]
992
+ node_branch_names = [node_i.name for node_i in data_i.path]
993
+ id_pairs += [[node_branch_ids[i],node_branch_ids[i+1]] for i in range(len(node_branch_ids)-1)]
994
+ name_pairs += [[node_branch_names[i],node_branch_names[i+1]] for i in range(len(node_branch_names)-1)]
995
+ node_pairs = {tuple(name_pair): id_pair for (name_pair,id_pair) in zip(name_pairs,id_pairs)}
996
+
997
+
998
+ # Draw lines
999
+ for (x,y) in zip(X,Y):
1000
+ if not np.isnan(x).all():
1001
+ for names, ids in node_pairs.items():
1002
+ if not None in ids and not (np.isnan(x[ids[0]]) or np.isnan(y[ids[0]]) or np.isnan(x[ids[1]]) or np.isnan(y[ids[1]])):
1003
+ if any(n.startswith('R') for n in names) and not any(n.startswith('L') for n in names):
1004
+ c = (255,128,0)
1005
+ elif any(n.startswith('L') for n in names) and not any(n.startswith('R') for n in names):
1006
+ c = (0,255,0)
1007
+ else:
1008
+ c = (51, 153, 255)
1009
+ cv2.line(img, (int(x[ids[0]]), int(y[ids[0]])), (int(x[ids[1]]), int(y[ids[1]])), c, thickness)
1010
+
1011
+ return img
1012
+
1013
+
1014
+ def draw_keypts(img, X, Y, scores, cmap_str='RdYlGn'):
1015
+ '''
1016
+ Draws keypoints and skeleton for each person.
1017
+ Keypoints' colors depend on their score.
1018
+
1019
+ INPUTS:
1020
+ - img: opencv image
1021
+ - X: list of list of x coordinates
1022
+ - Y: list of list of y coordinates
1023
+ - scores: list of list of scores
1024
+ - cmap_str: colormap name
1025
+
1026
+ OUTPUT:
1027
+ - img: image with keypoints and skeleton
1028
+ '''
1029
+
1030
+ scores = np.where(np.isnan(scores), 0, scores)
1031
+ # scores = (scores - 0.4) / (1-0.4) # to get a red color for scores lower than 0.4
1032
+ scores = np.where(scores>0.99, 0.99, scores)
1033
+ scores = np.where(scores<0, 0, scores)
1034
+
1035
+ cmap = plt.get_cmap(cmap_str)
1036
+ for (x,y,s) in zip(X,Y,scores):
1037
+ c_k = np.array(cmap(s))[:,:-1]*255
1038
+ [cv2.circle(img, (int(x[i]), int(y[i])), thickness+4, c_k[i][::-1], -1)
1039
+ for i in range(len(x))
1040
+ if not (np.isnan(x[i]) or np.isnan(y[i]))]
1041
+
1042
+ return img
Sports2D/process.py CHANGED
@@ -60,7 +60,7 @@ from functools import partial
60
60
  from datetime import datetime
61
61
  import itertools as it
62
62
  from tqdm import tqdm
63
- from anytree import RenderTree, PreOrderIter
63
+ from anytree import RenderTree
64
64
 
65
65
  import numpy as np
66
66
  import pandas as pd
@@ -68,6 +68,7 @@ import cv2
68
68
  import matplotlib as mpl
69
69
  import matplotlib.pyplot as plt
70
70
  from rtmlib import PoseTracker, BodyWithFeet, Wholebody, Body, Custom
71
+ from deep_sort_realtime.deepsort_tracker import DeepSort
71
72
 
72
73
  from Sports2D.Utilities import filter
73
74
  from Sports2D.Utilities.common import *
@@ -337,161 +338,6 @@ def compute_angle(ang_name, person_X_flipped, person_Y, angle_dict, keypoints_id
337
338
  return ang
338
339
 
339
340
 
340
- def min_with_single_indices(L, T):
341
- '''
342
- Let L be a list (size s) with T associated tuple indices (size s).
343
- Select the smallest values of L, considering that
344
- the next smallest value cannot have the same numbers
345
- in the associated tuple as any of the previous ones.
346
-
347
- Example:
348
- L = [ 20, 27, 51, 33, 43, 23, 37, 24, 4, 68, 84, 3 ]
349
- T = list(it.product(range(2),range(3)))
350
- = [(0,0),(0,1),(0,2),(0,3),(1,0),(1,1),(1,2),(1,3),(2,0),(2,1),(2,2),(2,3)]
351
-
352
- - 1st smallest value: 3 with tuple (2,3), index 11
353
- - 2nd smallest value when excluding indices (2,.) and (.,3), i.e. [(0,0),(0,1),(0,2),X,(1,0),(1,1),(1,2),X,X,X,X,X]:
354
- 20 with tuple (0,0), index 0
355
- - 3rd smallest value when excluding [X,X,X,X,X,(1,1),(1,2),X,X,X,X,X]:
356
- 23 with tuple (1,1), index 5
357
-
358
- INPUTS:
359
- - L: list (size s)
360
- - T: T associated tuple indices (size s)
361
-
362
- OUTPUTS:
363
- - minL: list of smallest values of L, considering constraints on tuple indices
364
- - argminL: list of indices of smallest values of L (indices of best combinations)
365
- - T_minL: list of tuples associated with smallest values of L
366
- '''
367
-
368
- minL = [np.nanmin(L)]
369
- argminL = [np.nanargmin(L)]
370
- T_minL = [T[argminL[0]]]
371
-
372
- mask_tokeep = np.array([True for t in T])
373
- i=0
374
- while mask_tokeep.any()==True:
375
- mask_tokeep = mask_tokeep & np.array([t[0]!=T_minL[i][0] and t[1]!=T_minL[i][1] for t in T])
376
- if mask_tokeep.any()==True:
377
- indicesL_tokeep = np.where(mask_tokeep)[0]
378
- minL += [np.nanmin(np.array(L)[indicesL_tokeep]) if not np.isnan(np.array(L)[indicesL_tokeep]).all() else np.nan]
379
- argminL += [indicesL_tokeep[np.nanargmin(np.array(L)[indicesL_tokeep])] if not np.isnan(minL[-1]) else indicesL_tokeep[0]]
380
- T_minL += (T[argminL[i+1]],)
381
- i+=1
382
-
383
- return np.array(minL), np.array(argminL), np.array(T_minL)
384
-
385
-
386
- def pad_shape(arr, target_len, fill_value=np.nan):
387
- '''
388
- Pads an array to the target length with specified fill values
389
-
390
- INPUTS:
391
- - arr: Input array to be padded.
392
- - target_len: The target length of the first dimension after padding.
393
- - fill_value: The value to use for padding (default: np.nan).
394
-
395
- OUTPUTS:
396
- - Padded array with shape (target_len, ...) matching the input dimensions.
397
- '''
398
-
399
- if len(arr) < target_len:
400
- pad_shape = (target_len - len(arr),) + arr.shape[1:]
401
- padding = np.full(pad_shape, fill_value)
402
- return np.concatenate((arr, padding))
403
-
404
- return arr
405
-
406
-
407
- def sort_people_sports2d(keyptpre, keypt, scores=None):
408
- '''
409
- Associate persons across frames (Sports2D method)
410
- Persons' indices are sometimes swapped when changing frame
411
- A person is associated to another in the next frame when they are at a small distance
412
-
413
- N.B.: Requires min_with_single_indices and euclidian_distance function (see common.py)
414
-
415
- INPUTS:
416
- - keyptpre: (K, L, M) array of 2D coordinates for K persons in the previous frame, L keypoints, M 2D coordinates
417
- - keypt: idem keyptpre, for current frame
418
- - score: (K, L) array of confidence scores for K persons, L keypoints (optional)
419
-
420
- OUTPUTS:
421
- - sorted_prev_keypoints: array with reordered persons with values of previous frame if current is empty
422
- - sorted_keypoints: array with reordered persons --> if scores is not None
423
- - sorted_scores: array with reordered scores --> if scores is not None
424
- - associated_tuples: list of tuples with correspondences between persons across frames --> if scores is None (for Pose2Sim.triangulation())
425
- '''
426
-
427
- # Generate possible person correspondences across frames
428
- max_len = max(len(keyptpre), len(keypt))
429
- keyptpre = pad_shape(keyptpre, max_len, fill_value=np.nan)
430
- keypt = pad_shape(keypt, max_len, fill_value=np.nan)
431
- if scores is not None:
432
- scores = pad_shape(scores, max_len, fill_value=np.nan)
433
-
434
- # Compute distance between persons from one frame to another
435
- personsIDs_comb = sorted(list(it.product(range(len(keyptpre)), range(len(keypt)))))
436
- frame_by_frame_dist = [euclidean_distance(keyptpre[comb[0]],keypt[comb[1]]) for comb in personsIDs_comb]
437
- frame_by_frame_dist = np.mean(frame_by_frame_dist, axis=1)
438
-
439
- # Sort correspondences by distance
440
- _, _, associated_tuples = min_with_single_indices(frame_by_frame_dist, personsIDs_comb)
441
-
442
- # Associate points to same index across frames, nan if no correspondence
443
- sorted_keypoints = []
444
- for i in range(len(keyptpre)):
445
- id_in_old = associated_tuples[:,1][associated_tuples[:,0] == i].tolist()
446
- if len(id_in_old) > 0: sorted_keypoints += [keypt[id_in_old[0]]]
447
- else: sorted_keypoints += [keypt[i]]
448
- sorted_keypoints = np.array(sorted_keypoints)
449
-
450
- if scores is not None:
451
- sorted_scores = []
452
- for i in range(len(keyptpre)):
453
- id_in_old = associated_tuples[:,1][associated_tuples[:,0] == i].tolist()
454
- if len(id_in_old) > 0: sorted_scores += [scores[id_in_old[0]]]
455
- else: sorted_scores += [scores[i]]
456
- sorted_scores = np.array(sorted_scores)
457
-
458
- # Keep track of previous values even when missing for more than one frame
459
- sorted_prev_keypoints = np.where(np.isnan(sorted_keypoints) & ~np.isnan(keyptpre), keyptpre, sorted_keypoints)
460
-
461
- if scores is not None:
462
- return sorted_prev_keypoints, sorted_keypoints, sorted_scores
463
- else: # For Pose2Sim.triangulation()
464
- return sorted_keypoints, associated_tuples
465
-
466
-
467
- def sort_people_rtmlib(pose_tracker, keypoints, scores):
468
- '''
469
- Associate persons across frames (RTMLib method)
470
-
471
- INPUTS:
472
- - pose_tracker: PoseTracker. The initialized RTMLib pose tracker object
473
- - keypoints: array of shape K, L, M with K the number of detected persons,
474
- L the number of detected keypoints, M their 2D coordinates
475
- - scores: array of shape K, L with K the number of detected persons,
476
- L the confidence of detected keypoints
477
-
478
- OUTPUT:
479
- - sorted_keypoints: array with reordered persons
480
- - sorted_scores: array with reordered scores
481
- '''
482
-
483
- try:
484
- desired_size = max(pose_tracker.track_ids_last_frame)+1
485
- sorted_keypoints = np.full((desired_size, keypoints.shape[1], 2), np.nan)
486
- sorted_keypoints[pose_tracker.track_ids_last_frame] = keypoints[:len(pose_tracker.track_ids_last_frame), :, :]
487
- sorted_scores = np.full((desired_size, scores.shape[1]), np.nan)
488
- sorted_scores[pose_tracker.track_ids_last_frame] = scores[:len(pose_tracker.track_ids_last_frame), :]
489
- except:
490
- sorted_keypoints, sorted_scores = keypoints, scores
491
-
492
- return sorted_keypoints, sorted_scores
493
-
494
-
495
341
  def draw_dotted_line(img, start, direction, length, color=(0, 255, 0), gap=7, dot_length=3, thickness=thickness):
496
342
  '''
497
343
  Draw a dotted line with on a cv2 image
@@ -516,109 +362,6 @@ def draw_dotted_line(img, start, direction, length, color=(0, 255, 0), gap=7, do
516
362
  cv2.line(img, tuple(line_start.astype(int)), tuple(line_end.astype(int)), color, thickness)
517
363
 
518
364
 
519
- def draw_bounding_box(img, X, Y, colors=[(255, 0, 0), (0, 255, 0), (0, 0, 255)], fontSize=0.3, thickness=1):
520
- '''
521
- Draw bounding boxes and person ID around list of lists of X and Y coordinates.
522
- Bounding boxes have a different color for each person.
523
-
524
- INPUTS:
525
- - img: opencv image
526
- - X: list of list of x coordinates
527
- - Y: list of list of y coordinates
528
- - colors: list of colors to cycle through
529
-
530
- OUTPUT:
531
- - img: image with rectangles and person IDs
532
- '''
533
-
534
- color_cycle = it.cycle(colors)
535
-
536
- for i,(x,y) in enumerate(zip(X,Y)):
537
- color = next(color_cycle)
538
- if not np.isnan(x).all():
539
- x_min, y_min = np.nanmin(x).astype(int), np.nanmin(y).astype(int)
540
- x_max, y_max = np.nanmax(x).astype(int), np.nanmax(y).astype(int)
541
- if x_min < 0: x_min = 0
542
- if x_max > img.shape[1]: x_max = img.shape[1]
543
- if y_min < 0: y_min = 0
544
- if y_max > img.shape[0]: y_max = img.shape[0]
545
-
546
- # Draw rectangles
547
- cv2.rectangle(img, (x_min-25, y_min-25), (x_max+25, y_max+25), color, thickness)
548
-
549
- # Write person ID
550
- cv2.putText(img, str(i), (x_min-30, y_min-30), cv2.FONT_HERSHEY_SIMPLEX, fontSize+1, color, 2, cv2.LINE_AA)
551
-
552
- return img
553
-
554
-
555
- def draw_skel(img, X, Y, model, colors=[(255, 0, 0), (0, 255, 0), (0, 0, 255)]):
556
- '''
557
- Draws keypoints and skeleton for each person.
558
- Skeletons have a different color for each person.
559
-
560
- INPUTS:
561
- - img: opencv image
562
- - X: list of list of x coordinates
563
- - Y: list of list of y coordinates
564
- - model: skeleton model (from skeletons.py)
565
- - colors: list of colors to cycle through
566
-
567
- OUTPUT:
568
- - img: image with keypoints and skeleton
569
- '''
570
-
571
- # Get (unique) pairs between which to draw a line
572
- node_pairs = []
573
- for data_i in PreOrderIter(model.root, filter_=lambda node: node.is_leaf):
574
- node_branches = [node_i.id for node_i in data_i.path]
575
- node_pairs += [[node_branches[i],node_branches[i+1]] for i in range(len(node_branches)-1)]
576
- node_pairs = [list(x) for x in set(tuple(x) for x in node_pairs)]
577
-
578
- # Draw lines
579
- color_cycle = it.cycle(colors)
580
- for (x,y) in zip(X,Y):
581
- c = next(color_cycle)
582
- if not np.isnan(x).all():
583
- [cv2.line(img,
584
- (int(x[n[0]]), int(y[n[0]])), (int(x[n[1]]), int(y[n[1]])), c, thickness)
585
- for n in node_pairs
586
- if not None in n and not (np.isnan(x[n[0]]) or np.isnan(y[n[0]]) or np.isnan(x[n[1]]) or np.isnan(y[n[1]]))] # IF NOT NONE
587
-
588
- return img
589
-
590
-
591
- def draw_keypts(img, X, Y, scores, cmap_str='RdYlGn'):
592
- '''
593
- Draws keypoints and skeleton for each person.
594
- Keypoints' colors depend on their score.
595
-
596
- INPUTS:
597
- - img: opencv image
598
- - X: list of list of x coordinates
599
- - Y: list of list of y coordinates
600
- - scores: list of list of scores
601
- - cmap_str: colormap name
602
-
603
- OUTPUT:
604
- - img: image with keypoints and skeleton
605
- '''
606
-
607
- scores = np.where(np.isnan(scores), 0, scores)
608
- # scores = (scores - 0.4) / (1-0.4) # to get a red color for scores lower than 0.4
609
- scores = np.where(scores>0.99, 0.99, scores)
610
- scores = np.where(scores<0, 0, scores)
611
-
612
- cmap = plt.get_cmap(cmap_str)
613
- for (x,y,s) in zip(X,Y,scores):
614
- c_k = np.array(cmap(s))[:,:-1]*255
615
- [cv2.circle(img, (int(x[i]), int(y[i])), thickness+4, c_k[i][::-1], -1)
616
- for i in range(len(x))
617
- if not (np.isnan(x[i]) or np.isnan(y[i]))]
618
-
619
- return img
620
-
621
-
622
365
  def draw_angles(img, valid_X, valid_Y, valid_angles, valid_X_flipped, keypoints_ids, keypoints_names, angle_names, display_angle_values_on= ['body', 'list'], colors=[(255, 0, 0), (0, 255, 0), (0, 0, 255)], fontSize=0.3, thickness=1):
623
366
  '''
624
367
  Draw angles on the image.
@@ -1184,6 +927,16 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1184
927
  mode = config_dict.get('pose').get('mode')
1185
928
  det_frequency = config_dict.get('pose').get('det_frequency')
1186
929
  tracking_mode = config_dict.get('pose').get('tracking_mode')
930
+ if tracking_mode == 'deepsort':
931
+ deepsort_params = config_dict.get('pose').get('deepsort_params')
932
+ try:
933
+ deepsort_params = ast.literal_eval(deepsort_params)
934
+ except: # if within single quotes instead of double quotes when run with sports2d --mode """{dictionary}"""
935
+ deepsort_params = deepsort_params.strip("'").replace('\n', '').replace(" ", "").replace(",", '", "').replace(":", '":"').replace("{", '{"').replace("}", '"}').replace('":"/',':/').replace('":"\\',':\\')
936
+ deepsort_params = re.sub(r'"\[([^"]+)",\s?"([^"]+)\]"', r'[\1,\2]', deepsort_params) # changes "[640", "640]" to [640,640]
937
+ deepsort_params = json.loads(deepsort_params)
938
+ deepsort_tracker = DeepSort(**deepsort_params)
939
+ deepsort_tracker.tracker.tracks.clear()
1187
940
  backend = config_dict.get('pose').get('backend')
1188
941
  device = config_dict.get('pose').get('device')
1189
942
 
@@ -1321,8 +1074,8 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1321
1074
  logging.warning("\nInvalid mode. Must be 'lightweight', 'balanced', 'performance', or '''{dictionary}''' of parameters within triple quotes. Make sure input_sizes are within square brackets.")
1322
1075
  logging.warning('Using the default "balanced" mode.')
1323
1076
  mode = 'balanced'
1324
-
1325
1077
 
1078
+
1326
1079
  # Skip pose estimation or set it up:
1327
1080
  if load_trc:
1328
1081
  if not '_px' in str(load_trc):
@@ -1341,12 +1094,21 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1341
1094
  keypoints_ids = [node.id for _, _, node in RenderTree(pose_model) if node.id!=None]
1342
1095
  keypoints_names = [node.name for _, _, node in RenderTree(pose_model) if node.id!=None]
1343
1096
 
1344
- tracking_rtmlib = True if (tracking_mode == 'rtmlib' and multiperson) else False
1345
- pose_tracker = setup_pose_tracker(ModelClass, det_frequency, mode, tracking_rtmlib, backend, device)
1097
+ # Set up pose tracker
1098
+ try:
1099
+ pose_tracker = setup_pose_tracker(ModelClass, det_frequency, mode, False, backend, device)
1100
+ except:
1101
+ logging.error('Error: Pose estimation failed. Check in Config.toml that pose_model and mode are valid.')
1102
+ raise ValueError('Error: Pose estimation failed. Check in Config.toml that pose_model and mode are valid.')
1103
+
1104
+ if tracking_mode not in ['deepsort', 'sports2d']:
1105
+ logging.warning(f"Tracking mode {tracking_mode} not recognized. Using sports2d method.")
1106
+ tracking_mode = 'sports2d'
1346
1107
  logging.info(f'\nPose tracking set up for "{pose_model_name}" model.')
1347
1108
  logging.info(f'Mode: {mode}.\n')
1348
- logging.info(f'Persons are detected every {det_frequency} frames and tracked inbetween. Multi-person is {"" if multiperson else "not "}selected.')
1349
- logging.info(f"Parameters: {keypoint_likelihood_threshold=}, {average_likelihood_threshold=}, {keypoint_number_threshold=}")
1109
+ logging.info(f'Persons are detected every {det_frequency} frames and tracked inbetween. Multi-person is {"" if multiperson else "not "}selected. Tracking is done with {tracking_mode}.')
1110
+ if tracking_mode == 'deepsort': logging.info(f'Deepsort parameters: {deepsort_params}.')
1111
+ logging.info(f"{keypoint_likelihood_threshold=}, {average_likelihood_threshold=}, {keypoint_number_threshold=}")
1350
1112
 
1351
1113
  if flip_left_right:
1352
1114
  try:
@@ -1383,22 +1145,22 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1383
1145
  for frame_nb in frame_iterator:
1384
1146
  start_time = datetime.now()
1385
1147
  success, frame = cap.read()
1148
+ frame_count += 1
1386
1149
 
1387
1150
  # If frame not grabbed
1388
1151
  if not success:
1389
- logging.warning(f"Failed to grab frame {frame_count}.")
1152
+ logging.warning(f"Failed to grab frame {frame_count-1}.")
1390
1153
  if save_pose:
1391
1154
  all_frames_X.append([])
1392
1155
  all_frames_Y.append([])
1393
1156
  all_frames_scores.append([])
1394
1157
  if save_angles:
1395
1158
  all_frames_angles.append([])
1396
- frame_count += 1
1397
1159
  continue
1398
1160
  else:
1399
1161
  cv2.putText(frame, f"Press 'q' to quit", (cam_width-int(400*fontSize), cam_height-20), cv2.FONT_HERSHEY_SIMPLEX, fontSize+0.2, (255,255,255), thickness+1, cv2.LINE_AA)
1400
1162
  cv2.putText(frame, f"Press 'q' to quit", (cam_width-int(400*fontSize), cam_height-20), cv2.FONT_HERSHEY_SIMPLEX, fontSize+0.2, (0,0,255), thickness, cv2.LINE_AA)
1401
- frame_count += 1
1163
+
1402
1164
 
1403
1165
  # Retrieve pose or Estimate pose and track people
1404
1166
  if load_trc:
@@ -1409,13 +1171,14 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1409
1171
  else:
1410
1172
  # Detect poses
1411
1173
  keypoints, scores = pose_tracker(frame)
1412
- # Track persons
1413
- if tracking_rtmlib:
1414
- keypoints, scores = sort_people_rtmlib(pose_tracker, keypoints, scores)
1415
- else:
1174
+
1175
+ # Track poses across frames
1176
+ if tracking_mode == 'deepsort':
1177
+ keypoints, scores = sort_people_deepsort(keypoints, scores, deepsort_tracker, frame, frame_count)
1178
+ if tracking_mode == 'sports2d':
1416
1179
  if 'prev_keypoints' not in locals(): prev_keypoints = keypoints
1417
1180
  prev_keypoints, keypoints, scores = sort_people_sports2d(prev_keypoints, keypoints, scores=scores)
1418
-
1181
+
1419
1182
 
1420
1183
  # Process coordinates and compute angles
1421
1184
  valid_X, valid_Y, valid_scores = [], [], []
@@ -1478,7 +1241,7 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1478
1241
  img = frame.copy()
1479
1242
  img = draw_bounding_box(img, valid_X, valid_Y, colors=colors, fontSize=fontSize, thickness=thickness)
1480
1243
  img = draw_keypts(img, valid_X, valid_Y, valid_scores, cmap_str='RdYlGn')
1481
- img = draw_skel(img, valid_X, valid_Y, pose_model, colors=colors)
1244
+ img = draw_skel(img, valid_X, valid_Y, pose_model)
1482
1245
  if calculate_angles:
1483
1246
  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)
1484
1247
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: sports2d
3
- Version: 0.6.1
3
+ Version: 0.6.2
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
@@ -33,10 +33,11 @@ Requires-Dist: opencv-python
33
33
  Requires-Dist: matplotlib
34
34
  Requires-Dist: PyQt5
35
35
  Requires-Dist: statsmodels
36
- Requires-Dist: rtmlib_pose2sim
36
+ Requires-Dist: rtmlib
37
37
  Requires-Dist: openvino
38
38
  Requires-Dist: tqdm
39
39
  Requires-Dist: imageio_ffmpeg
40
+ Requires-Dist: deep-sort-realtime
40
41
 
41
42
 
42
43
  [![Continuous integration](https://github.com/davidpagnon/sports2d/actions/workflows/continuous-integration.yml/badge.svg?branch=main)](https://github.com/davidpagnon/sports2d/actions/workflows/continuous-integration.yml)
@@ -212,6 +213,9 @@ Note that it does not take distortions into account, and that it will be less ac
212
213
  ``` cmd
213
214
  sports2d --multiperson false --pose_model Body --mode lightweight --det_frequency 50
214
215
  ```
216
+ ``` cmd
217
+ sports2d --tracking_mode deepsort --deepsort_params """{'max_age':30, 'n_init':3, 'nms_max_overlap':0.8, 'max_cosine_distance':0.3, 'nn_budget':200, 'max_iou_distance':0.8, 'embedder_gpu': True}"""
218
+ ```
215
219
  <br>
216
220
 
217
221
  #### Run with a toml configuration file:
@@ -249,6 +253,7 @@ Note that any detection and pose models can be used (first [deploy them with MMP
249
253
  - Use `--det_frequency 50`: Will detect poses only every 50 frames, and track keypoints in between, which is faster.
250
254
  - Use `--multiperson false`: Can be used if one single person is present in the video. Otherwise, persons' IDs may be mixed up.
251
255
  - 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.
256
+ - 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.
252
257
 
253
258
  <br>
254
259
 
@@ -369,7 +374,7 @@ sports2d --time_range 1.2 2.7 --ik true --person_orientation front none left
369
374
 
370
375
  ### All the parameters
371
376
 
372
- Have a look at the [Config_Demo.toml](https://github.com/davidpagnon/Sports2D/blob/main/Sports2D/Demo/Config_demo.toml) file or type for a full list of the available parameters:
377
+ For a full list of the available parameters, have a look at the [Config_Demo.toml](https://github.com/davidpagnon/Sports2D/blob/main/Sports2D/Demo/Config_demo.toml) file or type:
373
378
 
374
379
  ``` cmd
375
380
  sports2d --help
@@ -414,7 +419,10 @@ sports2d --help
414
419
  'osim_setup_path': ["", "path to OpenSim setup. '../OpenSim_setup' if not specified"],
415
420
  'person_orientation': ["", "front, back, left, right, auto, or none. 'front none left' if not specified. If 'auto', will be either left or right depending on the direction of the motion."],
416
421
  'close_to_zero_speed_m': ["","Sum for all keypoints: about 50 px/frame or 0.2 m/frame"],
417
- 'multiperson': ["", "multiperson involves tracking: will be faster if set to false. true if not specified"], 'tracking_mode': ["", "sports2d or rtmlib. sports2d is generally much more accurate and comparable in speed. sports2d if not specified"],
422
+ 'multiperson': ["", "multiperson involves tracking: will be faster if set to false. true if not specified"],
423
+ 'tracking_mode': ["", "sports2d or rtmlib. sports2d is generally much more accurate and comparable in speed. sports2d if not specified"],
424
+ 'deepsort_params': ["", 'Deepsort tracking parameters: """{dictionary between 3 double quotes}""". \n\
425
+ More information there: https://github.com/levan92/deep_sort_realtime/blob/master/deep_sort_realtime/deepsort_tracker.py#L51'],
418
426
  'input_size': ["", "width, height. 1280, 720 if not specified. Lower resolution will be faster but less precise"],
419
427
  'keypoint_likelihood_threshold': ["", "detected keypoints are not retained if likelihood is below this threshold. 0.3 if not specified"],
420
428
  'average_likelihood_threshold': ["", "detected persons are not retained if average keypoint likelihood is below this threshold. 0.5 if not specified"],
@@ -459,7 +467,7 @@ Sports2D:
459
467
 
460
468
  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.
461
469
 
462
- 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. This carefully crafted `sports2d` tracker runs at a comparable speed as the RTMlib one but is much more robust. The user can still choose the RTMLib method if they need it by specifying it in the Config.toml file.
470
+ 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.
463
471
 
464
472
  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.
465
473
 
@@ -0,0 +1,16 @@
1
+ Sports2D/Sports2D.py,sha256=8h8LJiClAPMTrgNyu5MXJaLMk0H3cuCVb5AfjlNqcnQ,27881
2
+ Sports2D/__init__.py,sha256=TyCP7Uuuy6CNklhPf8W84MbYoO1_-1dxowSYAJyk_OI,102
3
+ Sports2D/process.py,sha256=Q2kOkijxT-vxc2bVNHlNmW89G5AMUxUAgR3Ld0_Gbx8,77761
4
+ Sports2D/Demo/Config_demo.toml,sha256=D9DKslAExcjeGyGM96Iergd9GzABbDfkrvIZ6WkR5qA,12039
5
+ Sports2D/Demo/demo.mp4,sha256=2aZkFxhWR7ESMEtXCT8MGA83p2jmoU2sp1ylQfO3gDk,3968304
6
+ Sports2D/Utilities/__init__.py,sha256=TyCP7Uuuy6CNklhPf8W84MbYoO1_-1dxowSYAJyk_OI,102
7
+ Sports2D/Utilities/common.py,sha256=Pmv_meJaQ-H4deWtr3y5paLEq4kc4w1W_L94eQVTtvg,42723
8
+ Sports2D/Utilities/filter.py,sha256=8mVefMjDzxmh9a30eNtIrUuK_mUKoOJ2Nr-OzcQKkKM,4922
9
+ Sports2D/Utilities/skeletons.py,sha256=-EtpcoGxwAtJLr02_svLhdkFoNaQiUGj7cfK_aazgB0,40290
10
+ Sports2D/Utilities/tests.py,sha256=U0uwhPgcDY7HavI5f3HmfWydFi8eOfn_h4FIRCRhFcc,3104
11
+ sports2d-0.6.2.dist-info/LICENSE,sha256=f4qe3nE0Y7ltJho5w-xAR0jI5PUox5Xl-MsYiY7ZRM8,1521
12
+ sports2d-0.6.2.dist-info/METADATA,sha256=crFUyUqG5CrRZ6RxVZaqnPrgg9QQiX6tPrbg1JX6muo,31884
13
+ sports2d-0.6.2.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
14
+ sports2d-0.6.2.dist-info/entry_points.txt,sha256=h2CJTuydtNf8JyaLoWxWl5HTSIxx5Ra_FSiSGQsf7Sk,52
15
+ sports2d-0.6.2.dist-info/top_level.txt,sha256=DoURf9UDB8lQ_9lMUPQMQqhXCvWPFFjJco9NzPlHJ6I,9
16
+ sports2d-0.6.2.dist-info/RECORD,,
@@ -1,16 +0,0 @@
1
- Sports2D/Sports2D.py,sha256=ASgy0qsSDceBq4XFYo99ZFyJdNED72yDfgyMCJkhs-Q,27380
2
- Sports2D/__init__.py,sha256=TyCP7Uuuy6CNklhPf8W84MbYoO1_-1dxowSYAJyk_OI,102
3
- Sports2D/process.py,sha256=yAOS5nMAW0osCa-1wUEElNjy9hDCHhp7D_ug4b3A7DY,86671
4
- Sports2D/Demo/Config_demo.toml,sha256=0OXf19HlAWjeev6D6EaADREhdvONp74HrjBxXBt1Keo,11378
5
- Sports2D/Demo/demo.mp4,sha256=2aZkFxhWR7ESMEtXCT8MGA83p2jmoU2sp1ylQfO3gDk,3968304
6
- Sports2D/Utilities/__init__.py,sha256=TyCP7Uuuy6CNklhPf8W84MbYoO1_-1dxowSYAJyk_OI,102
7
- Sports2D/Utilities/common.py,sha256=Ak8ovbU4zInqMhqRw2CIz50diZyqtZ9nhVtpI1zamCQ,28303
8
- Sports2D/Utilities/filter.py,sha256=8mVefMjDzxmh9a30eNtIrUuK_mUKoOJ2Nr-OzcQKkKM,4922
9
- Sports2D/Utilities/skeletons.py,sha256=-EtpcoGxwAtJLr02_svLhdkFoNaQiUGj7cfK_aazgB0,40290
10
- Sports2D/Utilities/tests.py,sha256=U0uwhPgcDY7HavI5f3HmfWydFi8eOfn_h4FIRCRhFcc,3104
11
- sports2d-0.6.1.dist-info/LICENSE,sha256=f4qe3nE0Y7ltJho5w-xAR0jI5PUox5Xl-MsYiY7ZRM8,1521
12
- sports2d-0.6.1.dist-info/METADATA,sha256=-GBzyA-5TSJfBBzP4fmYTsLkSpgKH9Z_2guBiIoezc8,31271
13
- sports2d-0.6.1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
14
- sports2d-0.6.1.dist-info/entry_points.txt,sha256=h2CJTuydtNf8JyaLoWxWl5HTSIxx5Ra_FSiSGQsf7Sk,52
15
- sports2d-0.6.1.dist-info/top_level.txt,sha256=DoURf9UDB8lQ_9lMUPQMQqhXCvWPFFjJco9NzPlHJ6I,9
16
- sports2d-0.6.1.dist-info/RECORD,,