sports2d 0.6.1__py3-none-any.whl → 0.6.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Sports2D/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.
@@ -1044,9 +787,9 @@ def get_personID_with_highest_scores(all_frames_scores):
1044
787
  return person_id
1045
788
 
1046
789
 
1047
- def compute_floor_line(trc_data, keypoint_names = ['LBigToe', 'RBigToe'], toe_speed_below = 1.0, tot_speed_above=2.0):
790
+ def compute_floor_line(trc_data, keypoint_names = ['LBigToe', 'RBigToe'], toe_speed_below = 7, tot_speed_above=2.0):
1048
791
  '''
1049
- Compute the floor line equation and angle
792
+ Compute the floor line equation, angle, and direction
1050
793
  from the feet keypoints when they have zero speed.
1051
794
 
1052
795
  N.B.: Y coordinates point downwards
@@ -1059,6 +802,7 @@ def compute_floor_line(trc_data, keypoint_names = ['LBigToe', 'RBigToe'], toe_sp
1059
802
  OUTPUT:
1060
803
  - angle: float. The angle of the floor line in radians
1061
804
  - xy_origin: list. The origin of the floor line
805
+ - gait_direction: float. Left if < 0, 'right' otherwise
1062
806
  '''
1063
807
 
1064
808
  # Remove frames where the person is mostly not moving (outlier)
@@ -1067,36 +811,45 @@ def compute_floor_line(trc_data, keypoint_names = ['LBigToe', 'RBigToe'], toe_sp
1067
811
 
1068
812
  # Retrieve zero-speed coordinates for the foot
1069
813
  low_speeds_X, low_speeds_Y = [], []
814
+ gait_direction_val = []
1070
815
  for kpt in keypoint_names:
1071
816
  speeds = np.linalg.norm(trc_data[kpt].diff(), axis=1)
1072
-
817
+
1073
818
  low_speed_frames = trc_data[speeds<toe_speed_below].index
1074
819
  low_speeds_coords = trc_data[kpt].loc[low_speed_frames]
1075
820
  low_speeds_coords = low_speeds_coords[low_speeds_coords!=0]
1076
821
 
1077
- low_speeds_X += low_speeds_coords.iloc[:,0].tolist()
822
+ low_speeds_X_kpt = low_speeds_coords.iloc[:,0].tolist()
823
+ low_speeds_X += low_speeds_X_kpt
1078
824
  low_speeds_Y += low_speeds_coords.iloc[:,1].tolist()
1079
825
 
826
+ # gait direction (between [-1,1])
827
+ X_trend_val = np.polyfit(range(len(low_speeds_X_kpt)), low_speeds_X_kpt, 1)[0]
828
+ gait_direction_kpt = X_trend_val * len(low_speeds_X_kpt) / (np.max(low_speeds_X_kpt) - np.min(low_speeds_X_kpt))
829
+ gait_direction_val.append(gait_direction_kpt)
830
+
1080
831
  # Fit a line to the zero-speed coordinates
1081
832
  floor_line = np.polyfit(low_speeds_X, low_speeds_Y, 1) # (slope, intercept)
1082
- xy_origin = [0, floor_line[1]]
1083
-
1084
- # Compute the angle of the floor line in degrees
1085
- angle = -np.arctan(floor_line[0])
833
+ angle = -np.arctan(floor_line[0]) # angle of the floor line in degrees
834
+ xy_origin = [0, floor_line[1]] # origin of the floor line
1086
835
 
1087
- return angle, xy_origin
836
+ # Gait direction
837
+ gait_direction = np.mean(gait_direction_val)
838
+
839
+ return angle, xy_origin, gait_direction
1088
840
 
1089
841
 
1090
- def convert_px_to_meters(Q_coords_kpt, person_height_m, height_px, cx, cy, floor_angle):
842
+ def convert_px_to_meters(Q_coords_kpt, px_to_m_person_height_m, height_px, cx, cy, floor_angle, visible_side='none'):
1091
843
  '''
1092
844
  Convert pixel coordinates to meters.
1093
845
 
1094
846
  INPUTS:
1095
847
  - Q_coords_kpt: pd.DataFrame. The xyz coordinates of a keypoint in pixels, with z filled with zeros
1096
- - person_height_m: float. The height of the person in meters
848
+ - px_to_m_person_height_m: float. The height of the person in meters
1097
849
  - height_px: float. The height of the person in pixels
1098
850
  - cx, cy: float. The origin of the image in pixels
1099
851
  - floor_angle: float. The angle of the floor in radians
852
+ - visible_side: str. The side of the person that is visible ('right', 'left', 'front', 'back', 'none')
1100
853
 
1101
854
  OUTPUT:
1102
855
  - Q_coords_kpt_m: pd.DataFrame. The XYZ coordinates of a keypoint in meters
@@ -1105,10 +858,17 @@ def convert_px_to_meters(Q_coords_kpt, person_height_m, height_px, cx, cy, floor
1105
858
  u = Q_coords_kpt.iloc[:,0]
1106
859
  v = Q_coords_kpt.iloc[:,1]
1107
860
 
1108
- X = person_height_m / height_px * ((u-cx) + (v-cy)*np.sin(floor_angle))
1109
- Y = - person_height_m / height_px * np.cos(floor_angle) * (v-cy - np.tan(floor_angle)*(u-cx))
861
+ X = px_to_m_person_height_m / height_px * ((u-cx) + (v-cy)*np.sin(floor_angle))
862
+ Y = - px_to_m_person_height_m / height_px * np.cos(floor_angle) * (v-cy - np.tan(floor_angle)*(u-cx))
863
+
864
+ if 'marker_Z_positions' in globals() and visible_side!='none':
865
+ marker_name = Q_coords_kpt.columns[0]
866
+ Z = X.copy()
867
+ Z[:] = marker_Z_positions[visible_side][marker_name]
868
+ else:
869
+ Z = np.zeros_like(X)
1110
870
 
1111
- Q_coords_kpt_m = pd.DataFrame(np.array([X, Y, np.zeros_like(X)]).T, columns=Q_coords_kpt.columns)
871
+ Q_coords_kpt_m = pd.DataFrame(np.array([X, Y, Z]).T, columns=Q_coords_kpt.columns)
1112
872
 
1113
873
  return Q_coords_kpt_m
1114
874
 
@@ -1159,11 +919,13 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1159
919
 
1160
920
  # Base parameters
1161
921
  video_dir = Path(config_dict.get('project').get('video_dir'))
1162
- person_height_m = config_dict.get('project').get('person_height')
922
+ px_to_m_from_person_id = int(config_dict.get('project').get('px_to_m_from_person_id'))
923
+ px_to_m_person_height_m = config_dict.get('project').get('px_to_m_person_height')
924
+ visible_side = config_dict.get('project').get('visible_side')
1163
925
  # Pose from file
1164
- load_trc = config_dict.get('project').get('load_trc')
1165
- if load_trc == '': load_trc = None
1166
- else: load_trc = Path(load_trc).resolve()
926
+ load_trc_px = config_dict.get('project').get('load_trc_px')
927
+ if load_trc_px == '': load_trc_px = None
928
+ else: load_trc_px = Path(load_trc_px).resolve()
1167
929
  compare = config_dict.get('project').get('compare')
1168
930
  # Webcam settings
1169
931
  webcam_id = config_dict.get('project').get('webcam_id')
@@ -1184,18 +946,28 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1184
946
  mode = config_dict.get('pose').get('mode')
1185
947
  det_frequency = config_dict.get('pose').get('det_frequency')
1186
948
  tracking_mode = config_dict.get('pose').get('tracking_mode')
949
+ if tracking_mode == 'deepsort':
950
+ deepsort_params = config_dict.get('pose').get('deepsort_params')
951
+ try:
952
+ deepsort_params = ast.literal_eval(deepsort_params)
953
+ except: # if within single quotes instead of double quotes when run with sports2d --mode """{dictionary}"""
954
+ deepsort_params = deepsort_params.strip("'").replace('\n', '').replace(" ", "").replace(",", '", "').replace(":", '":"').replace("{", '{"').replace("}", '"}').replace('":"/',':/').replace('":"\\',':\\')
955
+ deepsort_params = re.sub(r'"\[([^"]+)",\s?"([^"]+)\]"', r'[\1,\2]', deepsort_params) # changes "[640", "640]" to [640,640]
956
+ deepsort_params = json.loads(deepsort_params)
957
+ deepsort_tracker = DeepSort(**deepsort_params)
958
+ deepsort_tracker.tracker.tracks.clear()
1187
959
  backend = config_dict.get('pose').get('backend')
1188
960
  device = config_dict.get('pose').get('device')
1189
961
 
1190
962
  # Pixel to meters conversion
1191
963
  to_meters = config_dict.get('px_to_meters_conversion').get('to_meters')
964
+ make_c3d = config_dict.get('px_to_meters_conversion').get('make_c3d')
1192
965
  save_calib = config_dict.get('px_to_meters_conversion').get('save_calib')
1193
966
  # Calibration from file
1194
967
  calib_file = config_dict.get('px_to_meters_conversion').get('calib_file')
1195
968
  if calib_file == '': calib_file = None
1196
969
  else: calib_file = Path(calib_file).resolve()
1197
970
  # Calibration from person height
1198
- calib_on_person_id = int(config_dict.get('px_to_meters_conversion').get('calib_on_person_id'))
1199
971
  floor_angle = config_dict.get('px_to_meters_conversion').get('floor_angle') # 'auto' or float
1200
972
  floor_angle = np.radians(float(floor_angle)) if floor_angle != 'auto' else floor_angle
1201
973
  xy_origin = config_dict.get('px_to_meters_conversion').get('xy_origin') # ['auto'] or [x, y]
@@ -1239,9 +1011,21 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1239
1011
  gaussian_filter_kernel, loess_filter_kernel, median_filter_kernel]
1240
1012
 
1241
1013
  # Inverse kinematics settings
1242
- do_ik = config_dict.get('inverse-kinematics').get('do_ik')
1243
- osim_setup_path = config_dict.get('inverse-kinematics').get('osim_setup_path')
1244
- person_orientations = config_dict.get('inverse-kinematics').get('person_orientation')
1014
+ do_ik = config_dict.get('kinematics').get('do_ik')
1015
+ use_augmentation = config_dict.get('kinematics').get('use_augmentation')
1016
+ use_contacts_muscles = config_dict.get('kinematics').get('use_contacts_muscles')
1017
+
1018
+ osim_setup_path = config_dict.get('kinematics').get('osim_setup_path')
1019
+ right_left_symmetry = config_dict.get('kinematics').get('right_left_symmetry')
1020
+ default_height = config_dict.get('kinematics').get('default_height')
1021
+ remove_scaling_setup = config_dict.get('kinematics').get('remove_individual_scaling_setup')
1022
+ remove_ik_setup = config_dict.get('kinematics').get('remove_individual_ik_setup')
1023
+ fastest_frames_to_remove_percent = config_dict.get('kinematics').get('fastest_frames_to_remove_percent')
1024
+ large_hip_knee_angles = config_dict.get('kinematics').get('large_hip_knee_angles')
1025
+ trimmed_extrema_percent = config_dict.get('kinematics').get('trimmed_extrema_percent')
1026
+ close_to_zero_speed = config_dict.get('kinematics').get('close_to_zero_speed_m')
1027
+
1028
+ if do_ik: from Pose2Sim import Pose2Sim
1245
1029
 
1246
1030
  # Create output directories
1247
1031
  if video_file == "webcam":
@@ -1321,15 +1105,15 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1321
1105
  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
1106
  logging.warning('Using the default "balanced" mode.')
1323
1107
  mode = 'balanced'
1324
-
1325
1108
 
1109
+
1326
1110
  # Skip pose estimation or set it up:
1327
- if load_trc:
1328
- if not '_px' in str(load_trc):
1329
- logging.error(f'\n{load_trc} file needs to be in px, not in meters.')
1330
- logging.info(f'\nUsing a pose file instead of running pose estimation and tracking: {load_trc}.')
1111
+ if load_trc_px:
1112
+ if not '_px' in str(load_trc_px):
1113
+ logging.error(f'\n{load_trc_px} file needs to be in px, not in meters.')
1114
+ logging.info(f'\nUsing a pose file instead of running pose estimation and tracking: {load_trc_px}.')
1331
1115
  # Load pose file in px
1332
- Q_coords, _, _, keypoints_names, _ = read_trc(load_trc)
1116
+ Q_coords, _, _, keypoints_names, _ = read_trc(load_trc_px)
1333
1117
  keypoints_ids = [i for i in range(len(keypoints_names))]
1334
1118
  keypoints_all, scores_all = load_pose_file(Q_coords)
1335
1119
  for pre, _, node in RenderTree(model_name):
@@ -1341,12 +1125,21 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1341
1125
  keypoints_ids = [node.id for _, _, node in RenderTree(pose_model) if node.id!=None]
1342
1126
  keypoints_names = [node.name for _, _, node in RenderTree(pose_model) if node.id!=None]
1343
1127
 
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)
1128
+ # Set up pose tracker
1129
+ try:
1130
+ pose_tracker = setup_pose_tracker(ModelClass, det_frequency, mode, False, backend, device)
1131
+ except:
1132
+ logging.error('Error: Pose estimation failed. Check in Config.toml that pose_model and mode are valid.')
1133
+ raise ValueError('Error: Pose estimation failed. Check in Config.toml that pose_model and mode are valid.')
1134
+
1135
+ if tracking_mode not in ['deepsort', 'sports2d']:
1136
+ logging.warning(f"Tracking mode {tracking_mode} not recognized. Using sports2d method.")
1137
+ tracking_mode = 'sports2d'
1346
1138
  logging.info(f'\nPose tracking set up for "{pose_model_name}" model.')
1347
1139
  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=}")
1140
+ 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}.')
1141
+ if tracking_mode == 'deepsort': logging.info(f'Deepsort parameters: {deepsort_params}.')
1142
+ logging.info(f"{keypoint_likelihood_threshold=}, {average_likelihood_threshold=}, {keypoint_number_threshold=}")
1350
1143
 
1351
1144
  if flip_left_right:
1352
1145
  try:
@@ -1375,7 +1168,7 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1375
1168
  frame_count = 0
1376
1169
  while cap.isOpened():
1377
1170
  # Skip to the starting frame
1378
- if frame_count < frame_range[0] and not load_trc:
1171
+ if frame_count < frame_range[0] and not load_trc_px:
1379
1172
  cap.read()
1380
1173
  frame_count += 1
1381
1174
  continue
@@ -1383,25 +1176,25 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1383
1176
  for frame_nb in frame_iterator:
1384
1177
  start_time = datetime.now()
1385
1178
  success, frame = cap.read()
1179
+ frame_count += 1
1386
1180
 
1387
1181
  # If frame not grabbed
1388
1182
  if not success:
1389
- logging.warning(f"Failed to grab frame {frame_count}.")
1183
+ logging.warning(f"Failed to grab frame {frame_count-1}.")
1390
1184
  if save_pose:
1391
1185
  all_frames_X.append([])
1392
1186
  all_frames_Y.append([])
1393
1187
  all_frames_scores.append([])
1394
1188
  if save_angles:
1395
1189
  all_frames_angles.append([])
1396
- frame_count += 1
1397
1190
  continue
1398
1191
  else:
1399
1192
  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
1193
  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
1194
+
1402
1195
 
1403
1196
  # Retrieve pose or Estimate pose and track people
1404
- if load_trc:
1197
+ if load_trc_px:
1405
1198
  if frame_nb >= len(keypoints_all):
1406
1199
  break
1407
1200
  keypoints = keypoints_all[frame_nb]
@@ -1409,19 +1202,20 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1409
1202
  else:
1410
1203
  # Detect poses
1411
1204
  keypoints, scores = pose_tracker(frame)
1412
- # Track persons
1413
- if tracking_rtmlib:
1414
- keypoints, scores = sort_people_rtmlib(pose_tracker, keypoints, scores)
1415
- else:
1205
+
1206
+ # Track poses across frames
1207
+ if tracking_mode == 'deepsort':
1208
+ keypoints, scores = sort_people_deepsort(keypoints, scores, deepsort_tracker, frame, frame_count)
1209
+ if tracking_mode == 'sports2d':
1416
1210
  if 'prev_keypoints' not in locals(): prev_keypoints = keypoints
1417
1211
  prev_keypoints, keypoints, scores = sort_people_sports2d(prev_keypoints, keypoints, scores=scores)
1418
-
1212
+
1419
1213
 
1420
1214
  # Process coordinates and compute angles
1421
1215
  valid_X, valid_Y, valid_scores = [], [], []
1422
1216
  valid_X_flipped, valid_angles = [], []
1423
1217
  for person_idx in range(len(keypoints)):
1424
- if load_trc:
1218
+ if load_trc_px:
1425
1219
  person_X = keypoints[person_idx][:,0]
1426
1220
  person_Y = keypoints[person_idx][:,1]
1427
1221
  person_scores = scores[person_idx]
@@ -1478,7 +1272,7 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1478
1272
  img = frame.copy()
1479
1273
  img = draw_bounding_box(img, valid_X, valid_Y, colors=colors, fontSize=fontSize, thickness=thickness)
1480
1274
  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)
1275
+ img = draw_skel(img, valid_X, valid_Y, pose_model)
1482
1276
  if calculate_angles:
1483
1277
  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
1278
 
@@ -1530,8 +1324,8 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1530
1324
  frame_range = [0,frame_count] if video_file == 'webcam' else frame_range
1531
1325
  all_frames_time = pd.Series(np.linspace(frame_range[0]/fps, frame_range[1]/fps, frame_count+1), name='time')
1532
1326
  if not multiperson:
1533
- calib_on_person_id = get_personID_with_highest_scores(all_frames_scores)
1534
- detected_persons = [calib_on_person_id]
1327
+ px_to_m_from_person_id = get_personID_with_highest_scores(all_frames_scores)
1328
+ detected_persons = [px_to_m_from_person_id]
1535
1329
  else:
1536
1330
  detected_persons = range(all_frames_X_homog.shape[1])
1537
1331
 
@@ -1604,7 +1398,7 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1604
1398
  # Build TRC file
1605
1399
  trc_data_i = trc_data_from_XYZtime(all_frames_X_person_filt, all_frames_Y_person_filt, all_frames_Z_homog, all_frames_time)
1606
1400
  trc_data.append(trc_data_i)
1607
- if not load_trc:
1401
+ if not load_trc_px:
1608
1402
  make_trc_with_trc_data(trc_data_i, str(pose_path_person), fps=fps)
1609
1403
  logging.info(f'Pose in pixels saved to {pose_path_person.resolve()}.')
1610
1404
 
@@ -1619,9 +1413,9 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1619
1413
  # Convert px to meters
1620
1414
  if to_meters:
1621
1415
  logging.info('\nConverting pose to meters:')
1622
- if calib_on_person_id>=len(trc_data):
1623
- logging.warning(f'Person #{calib_on_person_id} not detected in the video. Calibrating on person #0 instead.')
1624
- calib_on_person_id = 0
1416
+ if px_to_m_from_person_id>=len(trc_data):
1417
+ logging.warning(f'Person #{px_to_m_from_person_id} not detected in the video. Calibrating on person #0 instead.')
1418
+ px_to_m_from_person_id = 0
1625
1419
  if calib_file:
1626
1420
  logging.info(f'Using calibration file to convert coordinates in meters: {calib_file}.')
1627
1421
  calib_params_dict = retrieve_calib_params(calib_file)
@@ -1630,43 +1424,68 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1630
1424
  else:
1631
1425
  # Compute calibration parameters
1632
1426
  if not multiperson:
1633
- selected_person_id = calib_on_person_id
1634
- calib_on_person_id = 0
1635
- height_px = compute_height(trc_data[calib_on_person_id].iloc[:,1:], keypoints_names,
1427
+ selected_person_id = px_to_m_from_person_id
1428
+ px_to_m_from_person_id = 0
1429
+ height_px = compute_height(trc_data[px_to_m_from_person_id].iloc[:,1:], keypoints_names,
1636
1430
  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)
1637
1431
 
1432
+ toe_speed_below = 1 # m/s (below which the foot is considered to be stationary)
1433
+ px_per_m = height_px/px_to_m_person_height_m
1434
+ toe_speed_below_px_frame = toe_speed_below * px_per_m / fps
1638
1435
  if floor_angle == 'auto' or xy_origin == 'auto':
1639
1436
  # estimated from the line formed by the toes when they are on the ground (where speed = 0)
1640
1437
  try:
1641
- toe_speed_below = 1 # m/s (below which the foot is considered to be stationary)
1642
- px_per_m = height_px/person_height_m
1643
- toe_speed_below_px_frame = toe_speed_below * px_per_m / fps
1644
- try:
1645
- 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)
1646
- except: # no feet points
1647
- floor_angle_estim, xy_origin_estim = compute_floor_line(trc_data[calib_on_person_id], keypoint_names=['LAnkle', 'RAnkle'], toe_speed_below=toe_speed_below_px_frame)
1438
+ if all(key in trc_data[px_to_m_from_person_id] for key in ['LBigToe', 'RBigToe']):
1439
+ floor_angle_estim, xy_origin_estim, _ = compute_floor_line(trc_data[px_to_m_from_person_id], keypoint_names=['LBigToe', 'RBigToe'], toe_speed_below=toe_speed_below_px_frame)
1440
+ else:
1441
+ floor_angle_estim, xy_origin_estim, _ = compute_floor_line(trc_data[px_to_m_from_person_id], keypoint_names=['LAnkle', 'RAnkle'], toe_speed_below=toe_speed_below_px_frame)
1648
1442
  xy_origin_estim[0] = xy_origin_estim[0]-0.13
1649
1443
  logging.warning(f'The RBigToe and LBigToe are missing from your model. Using ankles - 13 cm to compute the floor line.')
1650
1444
  except:
1651
1445
  floor_angle_estim = 0
1652
1446
  xy_origin_estim = cam_width/2, cam_height/2
1653
- logging.warning(f'Could not estimate the floor angle and xy_origin. Make sure that the full body is visible. Using floor angle = 0° and xy_origin = [{cam_width/2}, {cam_height/2}].')
1447
+ logging.warning(f'Could not estimate the floor angle and xy_origin for person {px_to_m_from_person_id}. Make sure that the full body is visible. Using floor angle = 0° and xy_origin = [{cam_width/2}, {cam_height/2}].')
1654
1448
  if not floor_angle == 'auto':
1655
1449
  floor_angle_estim = floor_angle
1656
1450
  if xy_origin == 'auto':
1657
1451
  cx, cy = xy_origin_estim
1658
1452
  else:
1659
1453
  cx, cy = xy_origin
1660
- logging.info(f'Using height of person #{calib_on_person_id} ({person_height_m}m) to convert coordinates in meters. '
1454
+ logging.info(f'Using height of person #{px_to_m_from_person_id} ({px_to_m_person_height_m}m) to convert coordinates in meters. '
1661
1455
  f'Floor angle: {np.degrees(floor_angle_estim) if not floor_angle=="auto" else f"auto (estimation: {round(np.degrees(floor_angle_estim),2)}°)"}, '
1662
1456
  f'xy_origin: {xy_origin if not xy_origin=="auto" else f"auto (estimation: {[round(c) for c in xy_origin_estim]})"}.')
1663
1457
 
1664
1458
  # Coordinates in m
1665
1459
  for i in range(len(trc_data)):
1460
+ # print(i)
1666
1461
  if not np.array(trc_data[i].iloc[:,1:] ==0).all():
1667
- 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)
1462
+ # Automatically determine visible side
1463
+ visible_side_i = visible_side[i] if len(visible_side)>i else 'auto' # set to 'auto' if list too short
1464
+
1465
+ # Set to 'front' if slope of X values between [-5,5]
1466
+ if visible_side_i == 'auto':
1467
+ try:
1468
+ if all(key in trc_data[i] for key in ['LBigToe', 'RBigToe']):
1469
+ _, _, gait_direction = compute_floor_line(trc_data[i], keypoint_names=['LBigToe', 'RBigToe'], toe_speed_below=toe_speed_below_px_frame)
1470
+ else:
1471
+ _, _, gait_direction = compute_floor_line(trc_data[i], keypoint_names=['LAnkle', 'RAnkle'], toe_speed_below=toe_speed_below_px_frame)
1472
+ logging.warning(f'The RBigToe and LBigToe are missing from your model. Gait direction will be determined from the ankle points.')
1473
+ visible_side_i = 'right' if gait_direction > 0.6 \
1474
+ else 'left' if gait_direction < -0.6 \
1475
+ else 'front'
1476
+ except:
1477
+ visible_side_i = 'none'
1478
+ logging.warning(f'Could not automatically find gait direction for person {i}. Please set visible_side to "front", "back", "left", or "right" for this person. Setting to "none".')
1479
+
1480
+ # skip if none
1481
+ if visible_side_i == 'none':
1482
+ logging.info(f'Skipping because "visible_side" is set to none for person {i}.')
1483
+ continue
1484
+
1485
+ # Convert to meters
1486
+ trc_data_m_i = pd.concat([convert_px_to_meters(trc_data[i][kpt_name], px_to_m_person_height_m, height_px, cx, cy, -floor_angle_estim, visible_side=visible_side_i) for kpt_name in keypoints_names], axis=1)
1668
1487
  trc_data_m_i.insert(0, 't', all_frames_time)
1669
- 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)
1488
+ trc_data_unfiltered_m_i = pd.concat([convert_px_to_meters(trc_data_unfiltered[i][kpt_name], px_to_m_person_height_m, height_px, cx, cy, -floor_angle_estim) for kpt_name in keypoints_names], axis=1)
1670
1489
  trc_data_unfiltered_m_i.insert(0, 't', all_frames_time)
1671
1490
 
1672
1491
  if to_meters and show_plots:
@@ -1676,7 +1495,9 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1676
1495
  idx_path = selected_person_id if not multiperson and not calib_file else i
1677
1496
  pose_path_person_m_i = (pose_output_path.parent / (pose_output_path_m.stem + f'_person{idx_path:02d}.trc'))
1678
1497
  make_trc_with_trc_data(trc_data_m_i, pose_path_person_m_i, fps=fps)
1679
- logging.info(f'Person {idx_path}: Pose in meters saved to {pose_path_person_m_i.resolve()}.')
1498
+ if make_c3d:
1499
+ c3d_path = convert_to_c3d(pose_path_person_m_i)
1500
+ logging.info(f'Person {idx_path}: Pose in meters saved to {pose_path_person_m_i.resolve()}. {"Also saved in c3d format." if make_c3d else ""}')
1680
1501
 
1681
1502
 
1682
1503
 
@@ -1694,7 +1515,7 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1694
1515
 
1695
1516
 
1696
1517
  # z = 3.0 # distance between the camera and the person. Required in the calibration file but simplified in the equations
1697
- # f = height_px / person_height_m * z
1518
+ # f = height_px / px_to_m_person_height_m * z
1698
1519
 
1699
1520
 
1700
1521
  # # Name
@@ -1728,7 +1549,7 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1728
1549
 
1729
1550
  # Post-processing angles
1730
1551
  if save_angles and calculate_angles:
1731
- logging.info('\nPost-processing angles:')
1552
+ logging.info('\nPost-processing angles (without inverse kinematics):')
1732
1553
  all_frames_angles = make_homogeneous(all_frames_angles)
1733
1554
 
1734
1555
  # unwrap angles
@@ -1805,3 +1626,18 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1805
1626
  if show_plots:
1806
1627
  all_frames_angles_person.insert(0, 't', all_frames_time)
1807
1628
  angle_plots(all_frames_angles_person, angle_data, i) # i = current person
1629
+
1630
+
1631
+ # # Run scaling and inverse kinematics
1632
+ # if save_angles and calculate_angles and do_ik:
1633
+ # logging.info('\nPost-processing angles (with inverse kinematics):')
1634
+ # if not to_meters:
1635
+ # logging.error('IK requires positions in meters rather than in pixels. Set to_meters to True.')
1636
+ # raise ValueError('IK requires positions in meters rather than in pixels. Set to_meters to True.')
1637
+
1638
+
1639
+ # marker_Z_positions
1640
+ # if 'none': No IK possible.
1641
+ # visible_side=='auto'
1642
+
1643
+ # convert_to_c3d(trc_path)