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.
- Sports2D/Demo/Config_demo.toml +12 -9
- Sports2D/Sports2D.py +12 -8
- Sports2D/Utilities/common.py +3 -0
- Sports2D/Utilities/tests.py +3 -2
- Sports2D/process.py +278 -114
- {sports2d-0.8.24.dist-info → sports2d-0.8.25.dist-info}/METADATA +43 -34
- {sports2d-0.8.24.dist-info → sports2d-0.8.25.dist-info}/RECORD +11 -11
- {sports2d-0.8.24.dist-info → sports2d-0.8.25.dist-info}/WHEEL +0 -0
- {sports2d-0.8.24.dist-info → sports2d-0.8.25.dist-info}/entry_points.txt +0 -0
- {sports2d-0.8.24.dist-info → sports2d-0.8.25.dist-info}/licenses/LICENSE +0 -0
- {sports2d-0.8.24.dist-info → sports2d-0.8.25.dist-info}/top_level.txt +0 -0
Sports2D/Demo/Config_demo.toml
CHANGED
|
@@ -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
|
+
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
|
-
#
|
|
109
|
-
|
|
110
|
-
|
|
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':
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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\
|
Sports2D/Utilities/common.py
CHANGED
|
@@ -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],
|
Sports2D/Utilities/tests.py
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
1509
|
+
Z = u.copy()
|
|
1362
1510
|
Z[:] = marker_Z_positions[visible_side][marker_name]
|
|
1363
1511
|
else:
|
|
1364
|
-
Z = np.zeros_like(
|
|
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
|
-
|
|
1472
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
2121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
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
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
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
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
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[
|
|
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[
|
|
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.
|
|
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
|
-
> -
|
|
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
|
-
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
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
|
-
|
|
319
|
-
|
|
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.
|
|
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
|
-
'
|
|
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
|
|
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
|
|
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=
|
|
12
|
+
Sports2D/Sports2D.py,sha256=kayxoFmZyekSoO6YDipLMPUW9cHSCRReaymd_Va8AQA,36761
|
|
13
13
|
Sports2D/__init__.py,sha256=BuUkPEdItxlkeqz4dmoiPwZLkgAfABJK3KWQ1ujTGwE,153
|
|
14
|
-
Sports2D/process.py,sha256=
|
|
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=
|
|
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=
|
|
20
|
-
Sports2D/Utilities/tests.py,sha256=
|
|
21
|
-
sports2d-0.8.
|
|
22
|
-
sports2d-0.8.
|
|
23
|
-
sports2d-0.8.
|
|
24
|
-
sports2d-0.8.
|
|
25
|
-
sports2d-0.8.
|
|
26
|
-
sports2d-0.8.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|