sports2d 0.8.24__py3-none-any.whl → 0.8.25__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.
@@ -97,7 +97,8 @@ tracking_mode = 'sports2d' # 'sports2d' or 'deepsort'. 'deepsort' is slower, har
97
97
  keypoint_likelihood_threshold = 0.3 # Keypoints whose likelihood is lower will not be taken into account
98
98
  average_likelihood_threshold = 0.5 # Person will be ignored if average likelihood of good keypoints is lower than this value
99
99
  keypoint_number_threshold = 0.3 # Person will be ignored if the number of good keypoints (above keypoint_likelihood_threshold) is less than this fraction
100
- max_distance = 100 # in px or None # If a person is detected further than max_distance from its position on the previous frame, it will be considered as a new one
100
+ max_distance = 250 # in px or None # If a person is detected further than max_distance from its position on the previous frame, it will be considered as a new one
101
+
101
102
 
102
103
  [px_to_meters_conversion]
103
104
  # Pixel to meters conversion
@@ -105,15 +106,17 @@ to_meters = true
105
106
  make_c3d = true
106
107
  save_calib = true
107
108
 
108
- # If conversion from first_person_height
109
- floor_angle = 'auto' # 'auto' or a value in degrees, eg 2.3. If 'auto', estimated from the line formed by the toes when they are on the ground (where speed = 0)
110
- xy_origin = ['auto'] # ['auto'] or [px_x,px_y]. N.B.: px_y points downwards. If ['auto'], direction estimated from the start to the end of the line formed by the toes when they are on the ground
109
+ # Compensate for perspective effects, which make the further limb look smaller. 1-2% coordinate error at 10 m, less if the camera is further away
110
+ perspective_value = 10 # Either camera-to-person distance (m), or focal length (px), or field-of-view (degrees or radians), or '' if perspective_unit=='from_calib'
111
+ perspective_unit = 'distance_m' # 'distance_m', 'f_px', 'fov_deg', 'fov_rad', or 'from_calib'
112
+
113
+ # Compensate for camera horizon
114
+ floor_angle = 'auto' # float, 'from_kinematics', 'from_calib', or 'auto' # 'auto' is equivalent to 'from_kinematics', ie angle calculated from foot contacts. 'from_calib' calculates it from a toml calibration file. Use float to manually specify it in degrees
115
+ xy_origin = ['auto'] # [px_x,px_y], or ['from kinematics'], ['from_calib'], or ['auto']. # BETWEEN BRACKETS! # ['auto'] is equivalent to ['from_kinematics'], ie origin estimated at first foot contact, direction is direction of motion. ['from_calib'] calculates it from a calibration file. Use [px_x,px_y] to manually specify it in pixels (px_y points downwards)
116
+
117
+ # Optional calibration file
118
+ calib_file = '' # Calibration file in the Pose2Sim toml format, or '' if not available
111
119
 
112
- # If conversion from a calibration file
113
- calib_file = '' # Calibration in the Pose2Sim format. 'calib_demo.toml', or '' if not available
114
- # subject_distance
115
- # focal_distance
116
- # recalculate_extrinsics
117
120
 
118
121
  [angles]
119
122
  display_angle_values_on = ['body', 'list'] # 'body', 'list', ['body', 'list'], 'none'. Display angle values on the body, as a list in the upper left of the image, both, or do not display them.
Sports2D/Sports2D.py CHANGED
@@ -152,7 +152,7 @@ DEFAULT_CONFIG = {'base': {'video_input': ['demo.mp4'],
152
152
  'keypoint_likelihood_threshold': 0.3,
153
153
  'average_likelihood_threshold': 0.5,
154
154
  'keypoint_number_threshold': 0.3,
155
- 'max_distance': 100,
155
+ 'max_distance': 250,
156
156
  'CUSTOM': { 'name': 'Hip',
157
157
  'id': 19,
158
158
  'children': [{'name': 'RHip',
@@ -194,10 +194,12 @@ DEFAULT_CONFIG = {'base': {'video_input': ['demo.mp4'],
194
194
  'px_to_meters_conversion': {
195
195
  'to_meters': True,
196
196
  'make_c3d': True,
197
- 'calib_file': '',
197
+ 'save_calib': True,
198
+ 'perspective_value': 10.0,
199
+ 'perspective_unit': 'distance_m',
198
200
  'floor_angle': 'auto',
199
201
  'xy_origin': ['auto'],
200
- 'save_calib': True
202
+ 'calib_file': '',
201
203
  },
202
204
  'angles': {'display_angle_values_on': ['body', 'list'],
203
205
  'fontSize': 0.3,
@@ -269,16 +271,21 @@ DEFAULT_CONFIG = {'base': {'video_input': ['demo.mp4'],
269
271
 
270
272
  CONFIG_HELP = {'config': ["C", "path to a toml configuration file"],
271
273
  'video_input': ["i", "webcam, or video_path.mp4, or video1_path.avi video2_path.mp4 ... Beware that images won't be saved if paths contain non ASCII characters"],
274
+ 'time_range': ["t", "start_time end_time. In seconds. Whole video if not specified. start_time1 end_time1 start_time2 end_time2 ... if multiple videos with different time ranges"],
272
275
  'nb_persons_to_detect': ["n", "number of persons to detect. int or 'all'. 'all' if not specified"],
273
276
  'person_ordering_method': ["", "'on_click', 'highest_likelihood', 'largest_size', 'smallest_size', 'greatest_displacement', 'least_displacement', 'first_detected', or 'last_detected'. 'on_click' if not specified"],
274
277
  'first_person_height': ["H", "height of the reference person in meters. 1.65 if not specified. Not used if a calibration file is provided"],
275
278
  'visible_side': ["", "front, back, left, right, auto, or none. 'auto front none' if not specified. If 'auto', will be either left or right depending on the direction of the motion. If 'none', no IK for this person"],
279
+ 'participant_mass': ["", "mass of the participant in kg or none. Defaults to 70 if not provided. No influence on kinematics (motion), only on kinetics (forces)"],
280
+ 'perspective_value': ["", "Either camera-to-person distance (m), or focal length (px), or field-of-view (degrees or radians), or '' if perspective_unit=='from_calib'"],
281
+ 'perspective_unit': ["", "'distance_m', 'f_px', 'fov_deg', 'fov_rad', or 'from_calib'"],
282
+ 'do_ik': ["", "do inverse kinematics. false if not specified"],
283
+ 'use_augmentation': ["", "Use LSTM marker augmentation. false if not specified"],
276
284
  'load_trc_px': ["", "load trc file to avaid running pose estimation again. false if not specified"],
277
285
  'compare': ["", "visually compare motion with trc file. false if not specified"],
278
- 'webcam_id': ["w", "webcam ID. 0 if not specified"],
279
- 'time_range': ["t", "start_time end_time. In seconds. Whole video if not specified. start_time1 end_time1 start_time2 end_time2 ... if multiple videos with different time ranges"],
280
286
  'video_dir': ["d", "current directory if not specified"],
281
287
  'result_dir': ["r", "current directory if not specified"],
288
+ 'webcam_id': ["w", "webcam ID. 0 if not specified"],
282
289
  'show_realtime_results': ["R", "show results in real-time. true if not specified"],
283
290
  'display_angle_values_on': ["a", '"body", "list", "body" "list", or "none". body list if not specified'],
284
291
  'show_graphs': ["G", "show plots of raw and processed results. true if not specified"],
@@ -303,11 +310,8 @@ CONFIG_HELP = {'config': ["C", "path to a toml configuration file"],
303
310
  'xy_origin': ["", "origin of the xy plane. 'auto' if not specified"],
304
311
  'calib_file': ["", "path to calibration file. '' if not specified, eg no calibration file"],
305
312
  'save_calib': ["", "save calibration file. true if not specified"],
306
- 'do_ik': ["", "do inverse kinematics. false if not specified"],
307
- 'use_augmentation': ["", "Use LSTM marker augmentation. false if not specified"],
308
313
  'feet_on_floor': ["", "offset marker augmentation results so that feet are at floor level. true if not specified"],
309
314
  'use_simple_model': ["", "IK 10+ times faster, but no muscles or flexible spine, no patella. false if not specified"],
310
- 'participant_mass': ["", "mass of the participant in kg or none. Defaults to 70 if not provided. No influence on kinematics (motion), only on kinetics (forces)"],
311
315
  'close_to_zero_speed_m': ["","Sum for all keypoints: about 50 px/frame or 0.2 m/frame"],
312
316
  'tracking_mode': ["", "'sports2d' or 'deepsort'. 'deepsort' is slower, harder to parametrize but can be more robust if correctly tuned"],
313
317
  'deepsort_params': ["", 'Deepsort tracking parameters: """{dictionary between 3 double quotes}""". \n\
@@ -36,6 +36,9 @@ __status__ = "Development"
36
36
 
37
37
 
38
38
  ## CONSTANTS
39
+ # 4 points joint angle: between knee and ankle, and toe and heel. Add 90° offset and multiply by 1
40
+ # 3 points joint angle: between ankle, knee, hip. -180° offset, multiply by -1
41
+ # 2 points segment angle: between horizontal and ankle and knee, 0° offset, multiply by -1
39
42
  angle_dict = { # lowercase!
40
43
  # joint angles
41
44
  'right ankle': [['RKnee', 'RAnkle', 'RBigToe', 'RHeel'], 'dorsiflexion', 90, 1],
@@ -96,7 +96,7 @@ def test_workflow():
96
96
 
97
97
  # With no pixels to meters conversion, one person to select, lightweight mode, detection frequency, slowmo factor, gaussian filter, RTMO body pose model
98
98
  demo_cmd3 = ["sports2d", "--show_realtime_results", "False", "--show_graphs", "False", "--save_graphs", "False",
99
- # "--calib_file", "calib_demo.toml",
99
+ "--floor_angle", "from_calib", "--xy_origin", "from_calib", "--perspective_unit", "from_calib", "--calib_file", os.path.join(root_dir, "demo_Sports2D", "demo_Sports2D_calib.toml"),
100
100
  "--nb_persons_to_detect", "1", "--person_ordering_method", "greatest_displacement",
101
101
  "--mode", "lightweight", "--det_frequency", "50",
102
102
  "--slowmo_factor", "4",
@@ -104,9 +104,10 @@ def test_workflow():
104
104
  "--pose_model", "body", "--mode", """{'pose_class':'RTMO', 'pose_model':'https://download.openmmlab.com/mmpose/v1/projects/rtmo/onnx_sdk/rtmo-m_16xb16-600e_body7-640x640-39e78cc4_20231211.zip', 'pose_input_size':[640, 640]}"""]
105
105
  subprocess.run(demo_cmd3, check=True, capture_output=True, text=True, encoding='utf-8', errors='replace')
106
106
 
107
- # With a time range, inverse kinematics, marker augmentation
107
+ # With a time range, inverse kinematics, marker augmentation, perspective value in fov
108
108
  demo_cmd4 = ["sports2d", "--person_ordering_method", "greatest_displacement", "--show_realtime_results", "False", "--show_graphs", "False", "--save_graphs", "False",
109
109
  "--time_range", "1.2", "2.7",
110
+ "--perspective_value", "40", "--perspective_unit", "fov_deg",
110
111
  "--do_ik", "True", "--use_augmentation", "True",
111
112
  "--nb_persons_to_detect", "all", "--first_person_height", "1.65",
112
113
  "--visible_side", "auto", "front", "--participant_mass", "55.0", "67.0"]
Sports2D/process.py CHANGED
@@ -90,6 +90,8 @@ from Pose2Sim.filtering import *
90
90
  # Silence numpy "RuntimeWarning: Mean of empty slice"
91
91
  import warnings
92
92
  warnings.filterwarnings("ignore", category=RuntimeWarning, message="Mean of empty slice")
93
+ warnings.filterwarnings("ignore", category=RuntimeWarning, message="All-NaN slice encountered")
94
+ warnings.filterwarnings("ignore", category=RuntimeWarning, message="invalid value encountered in scalar divide")
93
95
 
94
96
  # Not safe, but to be used until OpenMMLab/RTMlib's SSL certificates are updated
95
97
  import ssl
@@ -1275,7 +1277,7 @@ def select_persons_on_vid(video_file_path, frame_range, all_pose_coords):
1275
1277
  return selected_persons
1276
1278
 
1277
1279
 
1278
- def compute_floor_line(trc_data, score_data, keypoint_names = ['LBigToe', 'RBigToe'], toe_speed_below = 7, score_threshold=0.5):
1280
+ def compute_floor_line(trc_data, score_data, keypoint_names = ['LBigToe', 'RBigToe'], toe_speed_below = 7, score_threshold=0.3):
1279
1281
  '''
1280
1282
  Compute the floor line equation, angle, and direction
1281
1283
  from the feet keypoints when they have zero speed.
@@ -1325,7 +1327,7 @@ def compute_floor_line(trc_data, score_data, keypoint_names = ['LBigToe', 'RBigT
1325
1327
 
1326
1328
  # Fit a line to the zero-speed coordinates
1327
1329
  floor_line = np.polyfit(low_speeds_X, low_speeds_Y, 1) # (slope, intercept)
1328
- angle = -np.arctan(floor_line[0]) # angle of the floor line in degrees
1330
+ angle = -np.arctan(floor_line[0]) # angle of the floor line in radians
1329
1331
  xy_origin = [0, floor_line[1]] # origin of the floor line
1330
1332
 
1331
1333
  # Gait direction
@@ -1334,14 +1336,159 @@ def compute_floor_line(trc_data, score_data, keypoint_names = ['LBigToe', 'RBigT
1334
1336
  return angle, xy_origin, gait_direction
1335
1337
 
1336
1338
 
1337
- def convert_px_to_meters(Q_coords_kpt, first_person_height, height_px, cx, cy, floor_angle, visible_side='none'):
1339
+ def get_distance_from_camera(perspective_value=10, perspective_unit='distance_m', calib_file =None, height_px=1, height_m=1, cam_width=1, cam_height=1):
1340
+ '''
1341
+ Compute the distance between the camera and the person based on the chosen perspective unit.
1342
+
1343
+ INPUTS:
1344
+ - perspective_value: Value associated with the chosen perspective unit.
1345
+ - perspective_unit: Unit used to compute the distance. Can be 'distance_m', 'f_px', 'fov_rad', 'fov_deg', or 'from_calib'.
1346
+ - calib_file: Path to the toml calibration file.
1347
+ - height_px: Height of the person in pixels.
1348
+ - height_m: Height of the first person in meters.
1349
+ - cam_width: Width of the camera frame in pixels.
1350
+ - cam_height: Height of the camera frame in pixels.
1351
+
1352
+ OUTPUTS:
1353
+ - distance_m: Distance between the camera and the person in meters.
1354
+ '''
1355
+
1356
+ if perspective_unit == 'from_calib':
1357
+ if not calib_file:
1358
+ perspective_unit = 'distance_m'
1359
+ distance_m = 10.0
1360
+ logging.warning(f'No calibration file provided. Using a default distance of {distance_m} m between the camera and the person to convert px to meters.')
1361
+ else:
1362
+ calib_params_dict = retrieve_calib_params(calib_file)
1363
+ f_px = calib_params_dict['K'][0][0][0]
1364
+ distance_m = f_px / height_px * height_m
1365
+ elif perspective_unit == 'distance_m':
1366
+ distance_m = perspective_value
1367
+ elif perspective_unit == 'f_px':
1368
+ f_px = perspective_value
1369
+ distance_m = f_px / height_px * height_m
1370
+ elif perspective_unit == 'fov_rad':
1371
+ fov_rad = perspective_value
1372
+ f_px = max(cam_width, cam_height)/2 / np.tan(fov_rad / 2)
1373
+ distance_m = f_px / height_px * height_m
1374
+ elif perspective_unit == 'fov_deg':
1375
+ fov_rad = np.radians(perspective_value)
1376
+ f_px = max(cam_width, cam_height)/2 / np.tan(fov_rad / 2)
1377
+ distance_m = f_px / height_px * height_m
1378
+
1379
+ return distance_m
1380
+
1381
+
1382
+ def get_floor_params(floor_angle='auto', xy_origin=['auto'],
1383
+ calib_file=None, height_px=1, height_m=1,
1384
+ fps=30, trc_data=pd.DataFrame(), score_data=pd.DataFrame(), toe_speed_below=1, score_threshold=0.5,
1385
+ cam_width=1, cam_height=1):
1386
+ '''
1387
+ Compute the floor angle and the xy_origin based on calibration file, kinematics, or user input.
1388
+
1389
+ INPUTS:
1390
+ - floor_angle: Method to compute the floor angle. Can be 'auto', 'from_calib', 'from_kinematics', or a numeric value in degrees.
1391
+ - xy_origin: Method to compute the xy_origin. Can be ['auto'], ['from_calib'], ['from_kinematics'], or a list of two numeric values in pixels [cx, cy].
1392
+ - calib_file: Path to a toml calibration file.
1393
+ - height_px: Height of the person in pixels.
1394
+ - height_m: Height of the first person in meters.
1395
+ - fps: Framerate of the video in frames per second. Used if estimating floor line from kinematics.
1396
+ - trc_data: DataFrame containing the pose data in pixels for one person. Used if estimating floor line from kinematics.
1397
+ - score_data: DataFrame containing the keypoint scores for one person. Used if estimating floor line from kinematics.
1398
+ - toe_speed_below: Speed below which the foot is considered to be stationary, in m/s. Used if estimating floor line from kinematics.
1399
+ - score_threshold: Minimum average keypoint score to consider a frame for floor line estimation.
1400
+ - cam_width: Width of the camera frame in pixels. Used if failed to estimate floor line from kinematics.
1401
+ - cam_height: Height of the camera frame in pixels. Used if failed to estimate floor line from kinematics.
1402
+
1403
+ OUTPUTS:
1404
+ - floor_angle_estim: Estimated floor angle in radians.
1405
+ - xy_origin_estim: Estimated xy_origin as a list of two numeric values in pixels [cx, cy].
1406
+ - gait_direction: Estimated gait direction. 'left' if < 0, 'right' otherwise.
1407
+ '''
1408
+
1409
+ # Estimate floor angle from the calibration file
1410
+ if floor_angle == 'from_calib' or xy_origin == ['from_calib']:
1411
+ if not calib_file:
1412
+ if floor_angle == 'from_calib':
1413
+ floor_angle = 'auto'
1414
+ if xy_origin == ['from_calib']:
1415
+ xy_origin = ['auto']
1416
+ logging.warning(f'No calibration file provided. Estimating floor angle and xy_origin from the pose of the first selected person.')
1417
+ else:
1418
+ calib_params_dict = retrieve_calib_params(calib_file)
1419
+
1420
+ R90z = np.array([[0.0, -1.0, 0.0],
1421
+ [1.0, 0.0, 0.0],
1422
+ [0.0, 0.0, 1.0]])
1423
+ R270x = np.array([[1.0, 0.0, 0.0],
1424
+ [0.0, 0.0, 1.0],
1425
+ [0.0, -1.0, 0.0]])
1426
+
1427
+ R_cam = cv2.Rodrigues(calib_params_dict['R'][0])[0]
1428
+ T_cam = np.array(calib_params_dict['T'][0])
1429
+ R_world, T_world = world_to_camera_persp(R_cam, T_cam)
1430
+ Rfloory = R90z.T @ R_world @ R270x.T
1431
+ T_world = R90z.T @ T_world
1432
+ floor_angle_calib = np.arctan2(Rfloory[0,2], Rfloory[0,0])
1433
+
1434
+ cu = calib_params_dict['K'][0][0][2]
1435
+ cv = calib_params_dict['K'][0][1][2]
1436
+ cx = 0.0
1437
+ cy = cv + T_world[2]* height_px / height_m
1438
+ xy_origin_calib = [cx, cy]
1439
+
1440
+ # Estimate xy_origin from the line formed by the toes when they are on the ground (where speed = 0)
1441
+ px_per_m = height_px/height_m
1442
+ toe_speed_below_px_frame = toe_speed_below * px_per_m / fps # speed below which the foot is considered to be stationary
1443
+ try:
1444
+ if all(key in trc_data for key in ['LBigToe', 'RBigToe']):
1445
+ floor_angle_kin, xy_origin_kin, gait_direction = compute_floor_line(trc_data, score_data, keypoint_names=['LBigToe', 'RBigToe'], toe_speed_below=toe_speed_below_px_frame, score_threshold=score_threshold)
1446
+ else:
1447
+ floor_angle_kin, xy_origin_estim, gait_direction = compute_floor_line(trc_data, score_data, keypoint_names=['LAnkle', 'RAnkle'], toe_speed_below=toe_speed_below_px_frame, score_threshold=score_threshold)
1448
+ xy_origin_kin[1] = xy_origin_kin[1] + 0.13*px_per_m # approx. height of the ankle above the floor
1449
+ logging.warning(f'The RBigToe and LBigToe are missing from your pose estimation model. Using ankles - 13 cm to compute the floor line.')
1450
+ except:
1451
+ floor_angle_kin = 0
1452
+ xy_origin_kin = cam_width/2, cam_height/2
1453
+ logging.warning(f'Could not estimate the floor angle and xy_origin from person {0}. Make sure that the full body is visible. Using floor angle = 0° and xy_origin = [{cam_width/2}, {cam_height/2}] px.')
1454
+
1455
+ # Determine final floor angle estimation
1456
+ if floor_angle == 'from_calib':
1457
+ floor_angle_estim = floor_angle_calib
1458
+ elif floor_angle in ['auto', 'from_kinematics']:
1459
+ floor_angle_estim = floor_angle_kin
1460
+ else:
1461
+ try:
1462
+ floor_angle_estim = np.radians(float(floor_angle))
1463
+ except:
1464
+ raise ValueError(f'Invalid floor_angle: {floor_angle}. Must be "auto", "from_calib", "from_kinematics", or a numeric value in degrees.')
1465
+
1466
+ # Determine final xy_origin estimation
1467
+ if xy_origin == ['from_calib']:
1468
+ xy_origin_estim = xy_origin_calib
1469
+ elif xy_origin in [['auto'], ['from_kinematics']]:
1470
+ xy_origin_estim = xy_origin_kin
1471
+ else:
1472
+ try:
1473
+ xy_origin_estim = [float(v) for v in xy_origin]
1474
+ except:
1475
+ raise ValueError(f'Invalid xy_origin: {xy_origin}. Must be "auto", "from_calib", "from_kinematics", or a list of two numeric values in pixels.')
1476
+
1477
+ return floor_angle_estim, xy_origin_estim, gait_direction
1478
+
1479
+
1480
+ def convert_px_to_meters(Q_coords_kpt, first_person_height, height_px, distance_m, cam_width, cam_height, cx, cy, floor_angle, visible_side='none'):
1338
1481
  '''
1339
1482
  Convert pixel coordinates to meters.
1483
+ Corrects for floor angle, floor level, and depth perspective errors.
1340
1484
 
1341
1485
  INPUTS:
1342
1486
  - Q_coords_kpt: pd.DataFrame. The xyz coordinates of a keypoint in pixels, with z filled with zeros
1343
1487
  - first_person_height: float. The height of the person in meters
1344
1488
  - height_px: float. The height of the person in pixels
1489
+ - cam_width: float. The width of the camera frame in pixels
1490
+ - cam_height: float. The height of the camera frame in pixels
1491
+ - distance_m: float. The distance between the camera and the person in meters
1345
1492
  - cx, cy: float. The origin of the image in pixels
1346
1493
  - floor_angle: float. The angle of the floor in radians
1347
1494
  - visible_side: str. The side of the person that is visible ('right', 'left', 'front', 'back', 'none')
@@ -1350,19 +1497,36 @@ def convert_px_to_meters(Q_coords_kpt, first_person_height, height_px, cx, cy, f
1350
1497
  - Q_coords_kpt_m: pd.DataFrame. The XYZ coordinates of a keypoint in meters
1351
1498
  '''
1352
1499
 
1500
+ # u,v coordinates
1353
1501
  u = Q_coords_kpt.iloc[:,0]
1354
1502
  v = Q_coords_kpt.iloc[:,1]
1503
+ cu = cam_width / 2
1504
+ cv = cam_height / 2
1355
1505
 
1356
- X = first_person_height / height_px * ((u-cx) + (v-cy)*np.sin(floor_angle))
1357
- Y = - first_person_height / height_px * np.cos(floor_angle) * (v-cy - np.tan(floor_angle)*(u-cx))
1358
-
1506
+ # Normative Z coordinates
1359
1507
  marker_name = Q_coords_kpt.columns[0]
1360
1508
  if 'marker_Z_positions' in globals() and visible_side!='none' and marker_name in marker_Z_positions[visible_side].keys():
1361
- Z = X.copy()
1509
+ Z = u.copy()
1362
1510
  Z[:] = marker_Z_positions[visible_side][marker_name]
1363
1511
  else:
1364
- Z = np.zeros_like(X)
1365
-
1512
+ Z = np.zeros_like(u)
1513
+
1514
+ ## Compute X and Y coordinates in meters
1515
+ # X = first_person_height / height_px * (u-cu)
1516
+ # Y = - first_person_height / height_px * (v-cv)
1517
+ ## With floor angle and level correction:
1518
+ # X = first_person_height / height_px * ((u-cx)*np.cos(floor_angle) + (v-cy)*np.sin(floor_angle))
1519
+ # Y = - first_person_height / height_px * ((v-cy)*np.cos(floor_angle) + (u-cx)*np.sin(floor_angle))
1520
+ ## With floor angle and level correction, and depth perspective correction:
1521
+ scaling_factor = first_person_height / height_px
1522
+ X = scaling_factor * ( \
1523
+ ((u-cx) - Z/distance_m * (u-cu)) * np.cos(floor_angle) + \
1524
+ ((v-cy) - Z/distance_m * (v-cv)) * np.sin(floor_angle) )
1525
+ Y = - scaling_factor * ( \
1526
+ ((v-cy) - Z/distance_m * (v-cv)) * np.cos(floor_angle) - \
1527
+ ((u-cx) - Z/distance_m * (u-cu)) * np.sin(floor_angle) )
1528
+
1529
+ # Assemble results
1366
1530
  Q_coords_kpt_m = pd.DataFrame(np.array([X, Y, Z]).T, columns=Q_coords_kpt.columns)
1367
1531
 
1368
1532
  return Q_coords_kpt_m
@@ -1455,8 +1619,9 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1455
1619
  pose_model = config_dict.get('pose').get('pose_model')
1456
1620
  mode = config_dict.get('pose').get('mode')
1457
1621
  det_frequency = config_dict.get('pose').get('det_frequency')
1622
+ backend = config_dict.get('pose').get('backend')
1623
+ device = config_dict.get('pose').get('device')
1458
1624
  tracking_mode = config_dict.get('pose').get('tracking_mode')
1459
- max_distance = config_dict.get('pose').get('max_distance', None)
1460
1625
  if tracking_mode == 'deepsort':
1461
1626
  from deep_sort_realtime.deepsort_tracker import DeepSort
1462
1627
  deepsort_params = config_dict.get('pose').get('deepsort_params')
@@ -1468,39 +1633,39 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1468
1633
  deepsort_params = json.loads(deepsort_params)
1469
1634
  deepsort_tracker = DeepSort(**deepsort_params)
1470
1635
  deepsort_tracker.tracker.tracks.clear()
1471
- backend = config_dict.get('pose').get('backend')
1472
- device = config_dict.get('pose').get('device')
1473
-
1636
+
1637
+ keypoint_likelihood_threshold = config_dict.get('pose').get('keypoint_likelihood_threshold')
1638
+ average_likelihood_threshold = config_dict.get('pose').get('average_likelihood_threshold')
1639
+ keypoint_number_threshold = config_dict.get('pose').get('keypoint_number_threshold')
1640
+ max_distance = config_dict.get('pose').get('max_distance', None)
1641
+
1474
1642
  # Pixel to meters conversion
1475
1643
  to_meters = config_dict.get('px_to_meters_conversion').get('to_meters')
1476
1644
  make_c3d = config_dict.get('px_to_meters_conversion').get('make_c3d')
1477
1645
  save_calib = config_dict.get('px_to_meters_conversion').get('save_calib')
1646
+ # Correct perspective effects
1647
+ perspective_value = config_dict.get('px_to_meters_conversion', {}).get('perspective_value', 10.0)
1648
+ perspective_unit = config_dict.get('px_to_meters_conversion', {}).get('perspective_unit', 'distance_m')
1649
+ # Calibration from person height
1650
+ floor_angle = config_dict.get('px_to_meters_conversion').get('floor_angle', 'auto') # 'auto' or float
1651
+ xy_origin = config_dict.get('px_to_meters_conversion').get('xy_origin', ['auto']) # ['auto'] or [x, y]
1478
1652
  # Calibration from file
1479
1653
  calib_file = config_dict.get('px_to_meters_conversion').get('calib_file')
1480
1654
  if calib_file == '':
1481
1655
  calib_file = None
1482
1656
  else:
1483
- calib_file = video_dir / calib_file
1657
+ calib_file = Path(calib_file).resolve()
1484
1658
  if not calib_file.is_file():
1485
1659
  raise FileNotFoundError(f'Error: Could not find calibration file {calib_file}. Check that the file exists.')
1486
- # Calibration from person height
1487
- floor_angle = config_dict.get('px_to_meters_conversion').get('floor_angle') # 'auto' or float
1488
- floor_angle = np.radians(float(floor_angle)) if floor_angle != 'auto' else floor_angle
1489
- xy_origin = config_dict.get('px_to_meters_conversion').get('xy_origin') # ['auto'] or [x, y]
1490
- xy_origin = [float(o) for o in xy_origin] if xy_origin != ['auto'] else 'auto'
1491
-
1492
- keypoint_likelihood_threshold = config_dict.get('pose').get('keypoint_likelihood_threshold')
1493
- average_likelihood_threshold = config_dict.get('pose').get('average_likelihood_threshold')
1494
- keypoint_number_threshold = config_dict.get('pose').get('keypoint_number_threshold')
1495
1660
 
1496
1661
  # Angles advanced settings
1662
+ display_angle_values_on = config_dict.get('angles').get('display_angle_values_on')
1663
+ fontSize = config_dict.get('angles').get('fontSize')
1664
+ thickness = 1 if fontSize < 0.8 else 2
1497
1665
  joint_angle_names = config_dict.get('angles').get('joint_angles')
1498
1666
  segment_angle_names = config_dict.get('angles').get('segment_angles')
1499
1667
  angle_names = joint_angle_names + segment_angle_names
1500
1668
  angle_names = [angle_name.lower() for angle_name in angle_names]
1501
- display_angle_values_on = config_dict.get('angles').get('display_angle_values_on')
1502
- fontSize = config_dict.get('angles').get('fontSize')
1503
- thickness = 1 if fontSize < 0.8 else 2
1504
1669
  flip_left_right = config_dict.get('angles').get('flip_left_right')
1505
1670
  correct_segment_angles_with_floor_angle = config_dict.get('angles').get('correct_segment_angles_with_floor_angle')
1506
1671
 
@@ -1672,7 +1837,7 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
1672
1837
  except:
1673
1838
  logging.error('Error: Pose estimation failed. Check in Config.toml that pose_model and mode are valid.')
1674
1839
  raise ValueError('Error: Pose estimation failed. Check in Config.toml that pose_model and mode are valid.')
1675
- logging.info(f'Persons are detected every {det_frequency} frames and tracked inbetween. Tracking is done with {tracking_mode}.')
1840
+ logging.info(f'Persons detection is run every {det_frequency} frames (pose estimation is run at every frame). Tracking is done with {tracking_mode}.')
1676
1841
 
1677
1842
  if tracking_mode == 'deepsort':
1678
1843
  logging.info(f'Deepsort parameters: {deepsort_params}.')
@@ -2112,102 +2277,100 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
2112
2277
  trc_data_m = []
2113
2278
  if to_meters and save_pose:
2114
2279
  logging.info('\nConverting pose to meters:')
2115
-
2280
+
2116
2281
  # Compute height in px of the first person
2117
2282
  height_px = compute_height(trc_data[0].iloc[:,1:], new_keypoints_names,
2118
2283
  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)
2119
2284
 
2120
- if calib_file or save_calib:
2121
- dist_to_cam = 10.0 # arbitrary distance between the camera and the person (m)
2285
+ # Compute distance from camera to compensate for perspective effects
2286
+ distance_m = get_distance_from_camera(perspective_value=perspective_value, perspective_unit=perspective_unit,
2287
+ calib_file=calib_file, height_px=height_px, height_m=first_person_height,
2288
+ cam_width=cam_width, cam_height=cam_height)
2289
+
2290
+ # Compute floor angle and xy_origin to compensate for camera horizon and person position
2291
+ floor_angle_estim, xy_origin_estim, gait_direction = get_floor_params(floor_angle=floor_angle, xy_origin=xy_origin,
2292
+ calib_file=calib_file, height_px=height_px, height_m=first_person_height,
2293
+ fps=fps, trc_data=trc_data[0], score_data=score_data[0], toe_speed_below=1, score_threshold=average_likelihood_threshold,
2294
+ cam_width=cam_width, cam_height=cam_height)
2295
+ cx, cy = xy_origin_estim
2296
+ direction_person0 = 'right' if gait_direction > 0.3 \
2297
+ else 'left' if gait_direction < -0.3 \
2298
+ else 'front'
2299
+
2300
+ logging.info(f'Converting from pixels to meters using a person height of {first_person_height:.2f} in meters (manual input), and of {height_px:.2f} in pixels (calculated).')
2301
+
2302
+ perspective_messages = {
2303
+ "distance_m": f"(obtained from a manual input).",
2304
+ "f_px": f"(calculated from a focal length of {perspective_value:.2f} m).",
2305
+ "fov_deg": f"(calculated from a field of view of {perspective_value:.2f} deg).",
2306
+ "fov_rad": f"(calculated from a field of view of {perspective_value:.2f} rad).",
2307
+ "from_calib": f"(calculated from a calibration file: {calib_file}).",
2308
+ }
2309
+ message = perspective_messages.get(perspective_unit, "")
2310
+ logging.info(f'Perspective effects corrected using a camera-to-person distance of {distance_m:.2f} m {message}')
2311
+
2312
+ floor_angle_messages = {
2313
+ "manual": "manual input.",
2314
+ "auto": "gait kinematics.",
2315
+ "from_kinematics": "gait kinematics.",
2316
+ "from_calib": "a calibration file.",
2317
+ }
2318
+ if isinstance(floor_angle, (int, float)):
2319
+ key = "manual"
2320
+ else:
2321
+ key = floor_angle
2322
+ message = floor_angle_messages.get(key, "")
2323
+ logging.info(f'Camera horizon: {np.degrees(floor_angle_estim):.2f}°, corrected using {message}')
2324
+
2325
+ def get_correction_message(xy_origin):
2326
+ if all(isinstance(o, (int, float)) for o in xy_origin) and len(xy_origin) == 2:
2327
+ return "manual input."
2328
+ elif xy_origin == ["auto"] or xy_origin == ["from_kinematics"]:
2329
+ return "gait kinematics."
2330
+ elif xy_origin == ["from_calib"]:
2331
+ return "a calibration file."
2332
+ else:
2333
+ return "."
2334
+ message = get_correction_message(xy_origin)
2335
+ logging.info(f'Floor level: {cy:.2f} px (from the top of the image), gait starting at {cx:.2f} px in the {direction_person0} direction for the first person. Corrected using {message}\n')
2336
+
2337
+
2338
+ # Save calibration file
2339
+ if save_calib and not calib_file:
2122
2340
  R90z = np.array([[0.0, -1.0, 0.0],
2123
2341
  [1.0, 0.0, 0.0],
2124
2342
  [0.0, 0.0, 1.0]])
2125
2343
  R270x = np.array([[1.0, 0.0, 0.0],
2126
2344
  [0.0, 0.0, 1.0],
2127
2345
  [0.0, -1.0, 0.0]])
2128
-
2129
- # Compute px to meter parameters from calibration file
2130
- if calib_file:
2131
- calib_params_dict = retrieve_calib_params(calib_file)
2132
-
2133
- f = calib_params_dict['K'][0][0][0]
2134
- first_person_height = height_px / f * dist_to_cam
2135
-
2136
- R_cam = cv2.Rodrigues(calib_params_dict['R'][0])[0]
2137
- T_cam = np.array(calib_params_dict['T'][0])
2138
- R_world, T_world = world_to_camera_persp(R_cam, T_cam)
2139
- Rfloory = R90z.T @ R_world @ R270x.T
2140
- T_world = R90z.T @ T_world
2141
- floor_angle_estim = np.arctan2(Rfloory[0,2], Rfloory[0,0])
2142
2346
 
2143
- cu = calib_params_dict['K'][0][0][2]
2144
- cv = calib_params_dict['K'][0][1][2]
2145
- cx = 0.0
2146
- cy = cv + T_world[2]*f/dist_to_cam
2147
- xy_origin_estim = [cx, cy]
2148
-
2149
- logging.info(f'Using calibration file to convert coordinates in meters: {calib_file}.\n'
2150
- f'Floor angle: {np.degrees(floor_angle_estim):.2f}°, '
2151
- f'xy_origin: [{cx:.2f}, {cy:.2f}] px.')
2152
-
2347
+ calib_file_path = output_dir / f'{video_file_stem}_Sports2D_calib.toml'
2153
2348
 
2154
- # Compute px to meter parameters from scene
2155
- else:
2156
- toe_speed_below = 1 # m/s (below which the foot is considered to be stationary)
2157
- px_per_m = height_px/first_person_height
2158
- toe_speed_below_px_frame = toe_speed_below * px_per_m / fps
2159
- if floor_angle == 'auto' or xy_origin == 'auto':
2160
- # estimated from the line formed by the toes when they are on the ground (where speed = 0)
2161
- try:
2162
- if all(key in trc_data[0] for key in ['LBigToe', 'RBigToe']):
2163
- floor_angle_estim, xy_origin_estim, _ = compute_floor_line(trc_data[0], score_data[0], keypoint_names=['LBigToe', 'RBigToe'], toe_speed_below=toe_speed_below_px_frame, score_threshold=average_likelihood_threshold)
2164
- else:
2165
- floor_angle_estim, xy_origin_estim, _ = compute_floor_line(trc_data[0], score_data[0], keypoint_names=['LAnkle', 'RAnkle'], toe_speed_below=toe_speed_below_px_frame, score_threshold=average_likelihood_threshold)
2166
- xy_origin_estim[1] = xy_origin_estim[1] + 0.13*px_per_m # approx. height of the ankle above the floor
2167
- logging.warning(f'The RBigToe and LBigToe are missing from your pose estimation model. Using ankles - 13 cm to compute the floor line.')
2168
- except:
2169
- floor_angle_estim = 0
2170
- xy_origin_estim = cam_width/2, cam_height/2
2171
- logging.warning(f'Could not estimate the floor angle and xy_origin from person {0}. Make sure that the full body is visible. Using floor angle = 0° and xy_origin = [{cam_width/2}, {cam_height/2}] px.')
2172
- if not floor_angle == 'auto':
2173
- floor_angle_estim = floor_angle
2174
- if xy_origin == 'auto':
2175
- cx, cy = xy_origin_estim
2176
- else:
2177
- cx, cy = xy_origin
2178
- logging.info(f'Using height of person #0 ({first_person_height}m) to convert coordinates in meters.\n'
2179
- f'Floor angle: {np.degrees(floor_angle_estim) if not floor_angle=="auto" else f"auto (estimation: {round(np.degrees(floor_angle_estim),2)}°)"}, '
2180
- f'xy_origin: {xy_origin if not xy_origin=="auto" else f"auto (estimation: {[round(c) for c in xy_origin_estim]})"} px.')
2349
+ # name, size, distortions
2350
+ N = [video_file_stem]
2351
+ S = [[cam_width, cam_height]]
2352
+ D = [[0.0, 0.0, 0.0, 0.0]]
2353
+
2354
+ # Intrinsics
2355
+ f = height_px / first_person_height * distance_m
2356
+ cu = cam_width/2
2357
+ cv = cam_height/2
2358
+ K = np.array([[[f, 0.0, cu], [0.0, f, cv], [0.0, 0.0, 1.0]]])
2359
+
2360
+ # Extrinsics
2361
+ Rfloory = np.array([[np.cos(floor_angle_estim), 0.0, np.sin(floor_angle_estim)],
2362
+ [0.0, 1.0, 0.0],
2363
+ [-np.sin(floor_angle_estim), 0.0, np.cos(floor_angle_estim)]])
2364
+ R_world = R90z @ Rfloory @ R270x
2365
+ T_world = R90z @ np.array([-(cx-cu)/f*distance_m, -distance_m, (cy-cv)/f*distance_m])
2181
2366
 
2182
- # Save calibration file
2183
- if save_calib:
2184
- calib_file_path = output_dir / f'{video_file_stem}_Sports2D_calib.toml'
2185
-
2186
- # name, size, distortions
2187
- N = [video_file_stem]
2188
- S = [[cam_width, cam_height]]
2189
- D = [[0.0, 0.0, 0.0, 0.0]]
2190
-
2191
- # Intrinsics
2192
- f = height_px / first_person_height * dist_to_cam
2193
- cu = cam_width/2
2194
- cv = cam_height/2
2195
- K = np.array([[[f, 0.0, cu], [0.0, f, cv], [0.0, 0.0, 1.0]]])
2196
-
2197
- # Extrinsics
2198
- Rfloory = np.array([[np.cos(floor_angle_estim), 0.0, np.sin(floor_angle_estim)],
2199
- [0.0, 1.0, 0.0],
2200
- [-np.sin(floor_angle_estim), 0.0, np.cos(floor_angle_estim)]])
2201
- R_world = R90z @ Rfloory @ R270x
2202
- T_world = R90z @ np.array([-(cx-cu)/f*dist_to_cam, -dist_to_cam, (cy-cv)/f*dist_to_cam])
2203
-
2204
- R_cam, T_cam = world_to_camera_persp(R_world, T_world)
2205
- Tvec_cam = T_cam.reshape(1,3).tolist()
2206
- Rvec_cam = cv2.Rodrigues(R_cam)[0].reshape(1,3).tolist()
2367
+ R_cam, T_cam = world_to_camera_persp(R_world, T_world)
2368
+ Tvec_cam = T_cam.reshape(1,3).tolist()
2369
+ Rvec_cam = cv2.Rodrigues(R_cam)[0].reshape(1,3).tolist()
2207
2370
 
2208
- # Write calibration file
2209
- toml_write(calib_file_path, N, S, D, K, Rvec_cam, Tvec_cam)
2210
- logging.info(f'Calibration saved to {calib_file_path}.')
2371
+ # Write calibration file
2372
+ toml_write(calib_file_path, N, S, D, K, Rvec_cam, Tvec_cam)
2373
+ logging.info(f'Calibration saved to {calib_file_path}.')
2211
2374
 
2212
2375
 
2213
2376
  # Coordinates in m
@@ -2220,9 +2383,9 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
2220
2383
  if visible_side_i == 'auto':
2221
2384
  try:
2222
2385
  if all(key in trc_data[i] for key in ['LBigToe', 'RBigToe']):
2223
- _, _, gait_direction = compute_floor_line(trc_data[i], score_data[0], keypoint_names=['LBigToe', 'RBigToe'], toe_speed_below=toe_speed_below_px_frame, score_threshold=average_likelihood_threshold)
2386
+ _, _, gait_direction = compute_floor_line(trc_data[i], score_data[i], keypoint_names=['LBigToe', 'RBigToe'], score_threshold=keypoint_likelihood_threshold) # toe_speed_below=1 bu default
2224
2387
  else:
2225
- _, _, gait_direction = compute_floor_line(trc_data[i], score_data[0], keypoint_names=['LAnkle', 'RAnkle'], toe_speed_below=toe_speed_below_px_frame, score_threshold=average_likelihood_threshold)
2388
+ _, _, gait_direction = compute_floor_line(trc_data[i], score_data[i], keypoint_names=['LAnkle', 'RAnkle'], score_threshold=keypoint_likelihood_threshold)
2226
2389
  logging.warning(f'The RBigToe and LBigToe are missing from your model. Gait direction will be determined from the ankle points.')
2227
2390
  visible_side_i = 'right' if gait_direction > 0.3 \
2228
2391
  else 'left' if gait_direction < -0.3 \
@@ -2237,8 +2400,9 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
2237
2400
  else:
2238
2401
  logging.info(f'- Person {i}: Seen from the {visible_side_i}.')
2239
2402
 
2403
+
2240
2404
  # Convert to meters
2241
- px_to_m_i = [convert_px_to_meters(trc_data[i][kpt_name], first_person_height, height_px, cx, cy, -floor_angle_estim, visible_side=visible_side_i) for kpt_name in new_keypoints_names]
2405
+ px_to_m_i = [convert_px_to_meters(trc_data[i][kpt_name], first_person_height, height_px, distance_m, cam_width, cam_height, cx, cy, -floor_angle_estim, visible_side=visible_side_i) for kpt_name in new_keypoints_names]
2242
2406
  trc_data_m_i = pd.concat([all_frames_time.rename('time')]+px_to_m_i, axis=1)
2243
2407
  for c_id, c in enumerate(3*np.arange(len(trc_data_m_i.columns[3::3]))+1): # only X coordinates
2244
2408
  first_run_start, last_run_end = first_run_starts_everyone[i][c_id], last_run_ends_everyone[i][c_id]
@@ -2247,7 +2411,7 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
2247
2411
  trc_data_m_i.iloc[first_run_start:last_run_end,c+2] = trc_data_m_i.iloc[first_run_start:last_run_end,c+2].ffill().bfill()
2248
2412
  first_trim, last_trim = trc_data_m_i.isnull().any(axis=1).idxmin(), trc_data_m_i[::-1].isnull().any(axis=1).idxmin()
2249
2413
  trc_data_m_i = trc_data_m_i.iloc[first_trim:last_trim+1,:]
2250
- px_to_m_unfiltered_i = [convert_px_to_meters(trc_data_unfiltered[i][kpt_name], first_person_height, height_px, cx, cy, -floor_angle_estim) for kpt_name in new_keypoints_names]
2414
+ px_to_m_unfiltered_i = [convert_px_to_meters(trc_data_unfiltered[i][kpt_name], first_person_height, height_px, distance_m, cam_width, cam_height, cx, cy, -floor_angle_estim, visible_side=visible_side_i) for kpt_name in new_keypoints_names]
2251
2415
  trc_data_unfiltered_m_i = pd.concat([all_frames_time.rename('time')]+px_to_m_unfiltered_i, axis=1)
2252
2416
 
2253
2417
  if to_meters and (show_plots or save_plots):
@@ -2531,7 +2695,7 @@ def process_fun(config_dict, video_file, time_range, frame_rate, result_dir):
2531
2695
  if (mot_file.parent/(mot_file.stem+'_ik.mot')).exists():
2532
2696
  os.remove(mot_file.parent/(mot_file.stem+'_ik.mot'))
2533
2697
  os.rename(mot_file, mot_file.parent/(mot_file.stem+'_ik.mot'))
2534
- logging.info(f'.osim model and .mot motion file results saved to {kinematics_dir.resolve()}.\n')
2698
+ logging.info(f'.osim model and .mot motion file results saved to {kinematics_dir.resolve().parent}.\n')
2535
2699
 
2536
2700
  # Move all files in pose-3d and kinematics to the output_dir
2537
2701
  osim.Logger.removeFileSink()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sports2d
3
- Version: 0.8.24
3
+ Version: 0.8.25
4
4
  Summary: Compute 2D human pose and angles from a video or a webcam.
5
5
  Author-email: David Pagnon <contact@david-pagnon.com>
6
6
  Maintainer-email: David Pagnon <contact@david-pagnon.com>
@@ -52,7 +52,7 @@ Dynamic: license-file
52
52
  </br>
53
53
 
54
54
  > **`Announcements:`**
55
- > - Generate or import a calibration file, OpenSim skeleton overlay **New in v0.9!**
55
+ > - Compensate for floor angle, floor height, depth perspective effects, generate a calibration file **New in v0.9!**
56
56
  > - Select only the persons you want to analyze **New in v0.8!**
57
57
  > - MarkerAugmentation and Inverse Kinematics for accurate 3D motion with OpenSim. **New in v0.7!**
58
58
  > - Any detector and pose estimation model can be used. **New in v0.6!**
@@ -294,30 +294,35 @@ sports2d --person_ordering_method on_click
294
294
  #### Get coordinates in meters:
295
295
  > **N.B.:** The Z coordinate (depth) should not be overly trusted.
296
296
 
297
- You may want coordinates in meters rather than pixels. 2 options to do so:
297
+ To convert from pixels to meters, you need a minima the height of a participant. Better results can be obtained by also providing an information on depth. The camera horizon angle and the floor height are generally automatically estimated. **N.B.: A calibration file will be generated.**
298
298
 
299
- 1. **Just provide the height of a reference person**:
300
- - Their height in meters is be compared with their height in pixels to get a pixel-to-meter conversion factor.
301
- - To estimate the depth coordinates, specify which side of the person is visible: `left`, `right`, `front`, or `back`. Use `auto` if you want it to be automatically determined (only works for motions in the sagittal plane), or `none` if you want to keep 2D coordinates instead of 3D (if the person turns around, for example).
302
- - The floor angle is automatically estimated from gait, as well as the origin of the xy axis. The person trajectory is corrected accordingly. You can use the `--floor_angle` and `--xy_origin` parameters to manually specify them if your subject is not travelling horizontally or if you want the origin not to be under their feet (note that the `y` axis points down).
303
-
304
- **N.B.: A calibration file will be generated.** By convention, the camera-to-subject distance is set to 10 meters.
299
+ - The pixel-to-meters scale is computed from the ratio between the height of the participant in meters and in pixels. The height in pixels is automatically calculated; use the `--first_person_height` parameter to specify the height in meters.
300
+ - Depth perspective effects can be compensated either with the camera-to-person distance (m), or focal length (px), or field-of-view (degrees or radians), or from a calibration file. Use the `--perspective_unit` ('distance_m', 'f_px', 'fov_deg', 'fov_rad', or 'from_calib') and `--perspective_value` parameters (resp. in m, px, deg, rad, or '').
301
+ - The camera horizon angle can be estimated from kinematics (`auto`), from a calibration file (`from_calib`), or manually (float). Use the `--floor_angle` parameter.
302
+ - Likewise for the floor level. Use the `--xy_origin` parameter.
305
303
 
306
- ``` cmd
307
- sports2d --first_person_height 1.65 --visible_side auto front none
308
- ```
309
- ``` cmd
310
- sports2d --first_person_height 1.65 --visible_side auto front none `
311
- --person_ordering_method on_click `
312
- --floor_angle 0 --xy_origin 0 940
313
- ```
304
+ If one of these parameters is set to `from_calib`, then use `--calib_file`.
314
305
 
315
- 2. **Or use a calibration file**:\
316
- It can either be a `.toml` calibration file previously generated by Sports2D, or a more accurate one coming from another system. For example, [Pose2Sim](https://github.com/perfanalytics/pose2sim) can be used to accurately calculate calibration, or to convert calibration files from Qualisys, Vicon, OpenCap, FreeMoCap, etc.
317
306
 
318
- ``` cmd
319
- sports2d --calib_file Calib_demo.toml --visible_side auto front none
320
- ```
307
+ ``` cmd
308
+ sports2d --first_person_height 1.65
309
+ ```
310
+ ``` cmd
311
+ sports2d --first_person_height 1.65 `
312
+ --floor_angle auto `
313
+ --xy_origin auto`
314
+ --perspective_unit distance_m --perspective_value 10
315
+ ```
316
+ ``` cmd
317
+ sports2d --first_person_height 1.65 `
318
+ --floor_angle 0 `
319
+ --xy_origin from_calib`
320
+ --perspective_unit from_calib --calib_file Sports2D\Demo\Calib_demo.toml
321
+ ```
322
+ ``` cmd
323
+ sports2d --first_person_height 1.65 `
324
+ --perspective_unit f_px --perspective_value 2520
325
+ ```
321
326
 
322
327
  <br>
323
328
 
@@ -480,20 +485,25 @@ sports2d --help
480
485
  'config': ["C", "path to a toml configuration file"],
481
486
 
482
487
  'video_input': ["i", "webcam, or video_path.mp4, or video1_path.avi video2_path.mp4 ... Beware that images won't be saved if paths contain non ASCII characters"],
488
+ 'time_range': ["t", "start_time end_time. In seconds. Whole video if not specified. start_time1 end_time1 start_time2 end_time2 ... if multiple videos with different time ranges"],
483
489
  'nb_persons_to_detect': ["n", "number of persons to detect. int or 'all'. 'all' if not specified"],
484
490
  'person_ordering_method': ["", "'on_click', 'highest_likelihood', 'largest_size', 'smallest_size', 'greatest_displacement', 'least_displacement', 'first_detected', or 'last_detected'. 'on_click' if not specified"],
485
491
  'first_person_height': ["H", "height of the reference person in meters. 1.65 if not specified. Not used if a calibration file is provided"],
486
492
  'visible_side': ["", "front, back, left, right, auto, or none. 'auto front none' if not specified. If 'auto', will be either left or right depending on the direction of the motion. If 'none', no IK for this person"],
493
+ 'participant_mass': ["", "mass of the participant in kg or none. Defaults to 70 if not provided. No influence on kinematics (motion), only on kinetics (forces)"],
494
+ 'perspective_value': ["", "Either camera-to-person distance (m), or focal length (px), or field-of-view (degrees or radians), or '' if perspective_unit=='from_calib'"],
495
+ 'perspective_unit': ["", "'distance_m', 'f_px', 'fov_deg', 'fov_rad', or 'from_calib'"],
496
+ 'do_ik': ["", "do inverse kinematics. false if not specified"],
497
+ 'use_augmentation': ["", "Use LSTM marker augmentation. false if not specified"],
487
498
  'load_trc_px': ["", "load trc file to avaid running pose estimation again. false if not specified"],
488
499
  'compare': ["", "visually compare motion with trc file. false if not specified"],
489
- 'webcam_id': ["w", "webcam ID. 0 if not specified"],
490
- 'time_range': ["t", "start_time end_time. In seconds. Whole video if not specified. start_time1 end_time1 start_time2 end_time2 ... if multiple videos with different time ranges"],
491
500
  'video_dir': ["d", "current directory if not specified"],
492
501
  'result_dir': ["r", "current directory if not specified"],
502
+ 'webcam_id': ["w", "webcam ID. 0 if not specified"],
493
503
  'show_realtime_results': ["R", "show results in real-time. true if not specified"],
494
504
  'display_angle_values_on': ["a", '"body", "list", "body" "list", or "none". body list if not specified'],
495
505
  'show_graphs': ["G", "show plots of raw and processed results. true if not specified"],
496
- 'save_graphs': ["", "save position and angle plots of raw and processed results. false if not specified"],
506
+ 'save_graphs': ["", "save position and angle plots of raw and processed results. true if not specified"],
497
507
  'joint_angles': ["j", '"Right ankle" "Left ankle" "Right knee" "Left knee" "Right hip" "Left hip" "Right shoulder" "Left shoulder" "Right elbow" "Left elbow" if not specified'],
498
508
  'segment_angles': ["s", '"Right foot" "Left foot" "Right shank" "Left shank" "Right thigh" "Left thigh" "Pelvis" "Trunk" "Shoulders" "Head" "Right arm" "Left arm" "Right forearm" "Left forearm" if not specified'],
499
509
  'save_vid': ["V", "save processed video. true if not specified"],
@@ -514,11 +524,8 @@ sports2d --help
514
524
  'xy_origin': ["", "origin of the xy plane. 'auto' if not specified"],
515
525
  'calib_file': ["", "path to calibration file. '' if not specified, eg no calibration file"],
516
526
  'save_calib': ["", "save calibration file. true if not specified"],
517
- 'do_ik': ["", "do inverse kinematics. false if not specified"],
518
- 'use_augmentation': ["", "Use LSTM marker augmentation. false if not specified"],
519
527
  'feet_on_floor': ["", "offset marker augmentation results so that feet are at floor level. true if not specified"],
520
- 'use_simple_model': ["", "IK 10+ times faster, but no muscles or flexible spine. false if not specified"],
521
- 'participant_mass': ["", "mass of the participant in kg or none. Defaults to 70 if not provided. No influence on kinematics (motion), only on kinetics (forces)"],
528
+ 'use_simple_model': ["", "IK 10+ times faster, but no muscles or flexible spine, no patella. false if not specified"],
522
529
  'close_to_zero_speed_m': ["","Sum for all keypoints: about 50 px/frame or 0.2 m/frame"],
523
530
  'tracking_mode': ["", "'sports2d' or 'deepsort'. 'deepsort' is slower, harder to parametrize but can be more robust if correctly tuned"],
524
531
  'deepsort_params': ["", 'Deepsort tracking parameters: """{dictionary between 3 double quotes}""". \n\
@@ -528,6 +535,7 @@ sports2d --help
528
535
  'keypoint_likelihood_threshold': ["", "detected keypoints are not retained if likelihood is below this threshold. 0.3 if not specified"],
529
536
  'average_likelihood_threshold': ["", "detected persons are not retained if average keypoint likelihood is below this threshold. 0.5 if not specified"],
530
537
  'keypoint_number_threshold': ["", "detected persons are not retained if number of detected keypoints is below this threshold. 0.3 if not specified, i.e., i.e., 30 percent"],
538
+ 'max_distance': ["", "If a person is detected further than max_distance from its position on the previous frame, it will be considered as a new one. in px or None, 100 by default."],
531
539
  'fastest_frames_to_remove_percent': ["", "Frames with high speed are considered as outliers. Defaults to 0.1"],
532
540
  'close_to_zero_speed_px': ["", "Sum for all keypoints: about 50 px/frame or 0.2 m/frame. Defaults to 50"],
533
541
  'large_hip_knee_angles': ["", "Hip and knee angles below this value are considered as imprecise. Defaults to 45"],
@@ -539,15 +547,16 @@ sports2d --help
539
547
  'interp_gap_smaller_than': ["", "interpolate sequences of missing data if they are less than N frames long. 10 if not specified"],
540
548
  'fill_large_gaps_with': ["", "last_value, nan, or zeros. last_value if not specified"],
541
549
  'sections_to_keep': ["", "all, largest, first, or last. Keep 'all' valid sections even when they are interspersed with undetected chunks, or the 'largest' valid section, or the 'first' one, or the 'last' one"],
550
+ 'min_chunk_size': ["", "Minimum number of valid frames in a row to keep a chunk of data for a person. 10 if not specified"],
542
551
  'reject_outliers': ["", "reject outliers with Hampel filter before other filtering methods. true if not specified"],
543
552
  'filter': ["", "filter results. true if not specified"],
544
553
  'filter_type': ["", "butterworth, kalman, gcv_spline, gaussian, median, or loess. butterworth if not specified"],
554
+ 'cut_off_frequency': ["", "cut-off frequency of the Butterworth filter. 6 if not specified"],
545
555
  'order': ["", "order of the Butterworth filter. 4 if not specified"],
546
- 'cut_off_frequency': ["", "cut-off frequency of the Butterworth filter. 3 if not specified"],
556
+ 'gcv_cut_off_frequency': ["", "cut-off frequency of the GCV spline filter. 'auto' is usually better, unless the signal is too short (noise can then be considered as signal -> trajectories not filtered). 'auto' if not specified"],
557
+ 'gcv_smoothing_factor': ["", "smoothing factor of the GCV spline filter (>=0). Ignored if cut_off_frequency != 'auto'. Biases results towards more smoothing (>1) or more fidelity to data (<1). 1.0 if not specified"],
547
558
  'trust_ratio': ["", "trust ratio of the Kalman filter: How much more do you trust triangulation results (measurements), than the assumption of constant acceleration(process)? 500 if not specified"],
548
559
  'smooth': ["", "dual Kalman smoothing. true if not specified"],
549
- 'gcv_cut_off_frequency': ["", "cut-off frequency of the GCV spline filter. 'auto' if not specified"],
550
- 'smoothing_factor': ["", "smoothing factor of the GCV spline filter (>=0). Ignored if cut_off_frequency != 'auto'. Biases results towards more smoothing (>1) or more fidelity to data (<1). 0.1 if not specified"],
551
560
  'sigma_kernel': ["", "sigma of the gaussian filter. 1 if not specified"],
552
561
  'nb_values_used': ["", "number of values used for the loess filter. 5 if not specified"],
553
562
  'kernel_size': ["", "kernel size of the median filter. 3 if not specified"],
@@ -656,11 +665,11 @@ Sports2D:
656
665
 
657
666
  2. **Sets up pose estimation with RTMLib.** It can be run in lightweight, balanced, or performance mode, and for faster inference, the person bounding boxes can be tracked instead of detected every frame. Any RTMPose model can be used.
658
667
 
659
- 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.
668
+ 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, thanks to the 'sports2D' tracker. [See Release notes of v0.8.22 for more information](https://github.com/davidpagnon/Sports2D/releases/tag/v0.8.22).
660
669
 
661
670
  4. **Chooses which persons to analyze.** In single-person mode, only keeps the person with the highest average scores over the sequence. In multi-person mode, you can choose the number of persons to analyze (`nb_persons_to_detect`), and how to order them (`person_ordering_method`). The ordering method can be 'on_click', 'highest_likelihood', 'largest_size', 'smallest_size', 'greatest_displacement', 'least_displacement', 'first_detected', or 'last_detected'. `on_click` is default and lets the user click on the persons they are interested in, in the desired order.
662
671
 
663
- 4. **Converts the pixel coordinates to meters.** The user can provide the size of a specified person to scale results accordingly. The floor angle and the coordinate origin can either be detected automatically from the gait sequence, or be manually specified. The depth coordinates are set to normative values, depending on whether the person is going left, right, facing the camera, or looking away.
672
+ 4. **Converts the pixel coordinates to meters.** The user can provide the size of a specified person to scale results accordingly. The camera horizon angle and the floor level can either be detected automatically from the gait sequence, be manually specified, or obtained frmm a calibration file. The depth perspective effects are compensated thanks with the distance from the camera to the subject, the focal length, the field of view, or from a calibration file. [See Release notes of v0.8.25 for more information](https://github.com/davidpagnon/Sports2D/releases/tag/v0.8.25).
664
673
 
665
674
  5. **Computes the selected joint and segment angles**, and flips them on the left/right side if the respective foot is pointing to the left/right.
666
675
 
@@ -9,18 +9,18 @@ Content/paper.md,sha256=8rWSOLrKTysloZv0Fz2lr3nayxtHi7GlFMqwdgDVggY,11333
9
9
  Content/sports2d_blender.gif,sha256=wgMuPRxhja3XtQn76_okGXsNnUT9Thp0pnD36GdW5_E,448786
10
10
  Content/sports2d_opensim.gif,sha256=XP1AcjqhbGcJknXUoNJjPWAwaM9ahZafbDgLWvzKJs4,376656
11
11
  Sports2D/Sports2D.ipynb,sha256=VnOVjIl6ndnCJTT13L4W5qTw4T-TQDF3jt3-wxnXDqM,2427047
12
- Sports2D/Sports2D.py,sha256=dZR16Id9rhrifEU91Y6wZxxmCCQDuGEfa3Ri9kLSiR4,36345
12
+ Sports2D/Sports2D.py,sha256=kayxoFmZyekSoO6YDipLMPUW9cHSCRReaymd_Va8AQA,36761
13
13
  Sports2D/__init__.py,sha256=BuUkPEdItxlkeqz4dmoiPwZLkgAfABJK3KWQ1ujTGwE,153
14
- Sports2D/process.py,sha256=TweN8PsBuA8FG-7-yAQEGh_PJYpmhGDeNYtde40XXcc,131637
14
+ Sports2D/process.py,sha256=TX9ne0Wp1WxJPtSqzAkV-0rZcWWPlLVdDUqIPkqo5Wo,140354
15
15
  Sports2D/Demo/Calib_demo.toml,sha256=d6myoOkhhz3c5LOwCEJQBWT9eyqr6RSYoaPbFjBMizc,369
16
- Sports2D/Demo/Config_demo.toml,sha256=ZCyGybTRPi3YNTigPZkyokQz5gbEsbqGAS4O7Z69K4M,15972
16
+ Sports2D/Demo/Config_demo.toml,sha256=w9PaAZFPMHLKmWtPpEV_vWrUReyAZpH6iLa2-D8qhJ8,16545
17
17
  Sports2D/Demo/demo.mp4,sha256=2aZkFxhWR7ESMEtXCT8MGA83p2jmoU2sp1ylQfO3gDk,3968304
18
18
  Sports2D/Utilities/__init__.py,sha256=BuUkPEdItxlkeqz4dmoiPwZLkgAfABJK3KWQ1ujTGwE,153
19
- Sports2D/Utilities/common.py,sha256=idMRmesFv5BPX-5g3z5dOVa7SpS_8tNgijvGrOZlR-k,11185
20
- Sports2D/Utilities/tests.py,sha256=BcZ02nVcX04PpTQ2t0g5dPGHbAXY_028Y2lxn_3udjQ,5570
21
- sports2d-0.8.24.dist-info/licenses/LICENSE,sha256=f4qe3nE0Y7ltJho5w-xAR0jI5PUox5Xl-MsYiY7ZRM8,1521
22
- sports2d-0.8.24.dist-info/METADATA,sha256=6DjTjB4xYYmTItINASrlzONHW65ww6Py3bhhxjGSR9g,42938
23
- sports2d-0.8.24.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
24
- sports2d-0.8.24.dist-info/entry_points.txt,sha256=V8dFDIXatz9VvoGgoHzb2wE71C9-f85K6_OjnEQlxww,108
25
- sports2d-0.8.24.dist-info/top_level.txt,sha256=cWWBiDD2WbQXMoIoN6-9et9U2t2c_ZKo2JtBqO5uN-k,17
26
- sports2d-0.8.24.dist-info/RECORD,,
19
+ Sports2D/Utilities/common.py,sha256=RbS7kFCAT7SLbCxJJM0ULsqm7G4TMCnOVeZDTRgkrwk,11457
20
+ Sports2D/Utilities/tests.py,sha256=DYQ5ZdO1L7ZxxSb5hx8rTbF9VaKBeyh16hK_j_6Q9hQ,5819
21
+ sports2d-0.8.25.dist-info/licenses/LICENSE,sha256=f4qe3nE0Y7ltJho5w-xAR0jI5PUox5Xl-MsYiY7ZRM8,1521
22
+ sports2d-0.8.25.dist-info/METADATA,sha256=0q1giYFQLJRODB4XskE2bHce6EcpmKV2v-O5287rVxM,43757
23
+ sports2d-0.8.25.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
24
+ sports2d-0.8.25.dist-info/entry_points.txt,sha256=V8dFDIXatz9VvoGgoHzb2wE71C9-f85K6_OjnEQlxww,108
25
+ sports2d-0.8.25.dist-info/top_level.txt,sha256=cWWBiDD2WbQXMoIoN6-9et9U2t2c_ZKo2JtBqO5uN-k,17
26
+ sports2d-0.8.25.dist-info/RECORD,,