Skip to content

Commit 7428e7d

Browse files
committed
Reorganizing files for Pose2Sim consistency
1 parent 5ae7eaf commit 7428e7d

File tree

2 files changed

+207
-203
lines changed

2 files changed

+207
-203
lines changed

Sports2D/Utilities/common.py

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import logging
2424

2525
import numpy as np
26+
import pandas as pd
2627
from scipy import interpolate
2728
import imageio_ffmpeg as ffmpeg
2829
import cv2
@@ -43,6 +44,49 @@
4344
__status__ = "Development"
4445

4546

47+
## CONSTANTS
48+
angle_dict = { # lowercase!
49+
# joint angles
50+
'right ankle': [['RKnee', 'RAnkle', 'RBigToe', 'RHeel'], 'dorsiflexion', 90, 1],
51+
'left ankle': [['LKnee', 'LAnkle', 'LBigToe', 'LHeel'], 'dorsiflexion', 90, 1],
52+
'right knee': [['RAnkle', 'RKnee', 'RHip'], 'flexion', -180, 1],
53+
'left knee': [['LAnkle', 'LKnee', 'LHip'], 'flexion', -180, 1],
54+
'right hip': [['RKnee', 'RHip', 'Hip', 'Neck'], 'flexion', 0, -1],
55+
'left hip': [['LKnee', 'LHip', 'Hip', 'Neck'], 'flexion', 0, -1],
56+
# 'lumbar': [['Neck', 'Hip', 'RHip', 'LHip'], 'flexion', -180, -1],
57+
# 'neck': [['Head', 'Neck', 'RShoulder', 'LShoulder'], 'flexion', -180, -1],
58+
'right shoulder': [['RElbow', 'RShoulder', 'Hip', 'Neck'], 'flexion', 0, -1],
59+
'left shoulder': [['LElbow', 'LShoulder', 'Hip', 'Neck'], 'flexion', 0, -1],
60+
'right elbow': [['RWrist', 'RElbow', 'RShoulder'], 'flexion', 180, -1],
61+
'left elbow': [['LWrist', 'LElbow', 'LShoulder'], 'flexion', 180, -1],
62+
'right wrist': [['RElbow', 'RWrist', 'RIndex'], 'flexion', -180, 1],
63+
'left wrist': [['LElbow', 'LIndex', 'LWrist'], 'flexion', -180, 1],
64+
65+
# segment angles
66+
'right foot': [['RBigToe', 'RHeel'], 'horizontal', 0, -1],
67+
'left foot': [['LBigToe', 'LHeel'], 'horizontal', 0, -1],
68+
'right shank': [['RAnkle', 'RKnee'], 'horizontal', 0, -1],
69+
'left shank': [['LAnkle', 'LKnee'], 'horizontal', 0, -1],
70+
'right thigh': [['RKnee', 'RHip'], 'horizontal', 0, -1],
71+
'left thigh': [['LKnee', 'LHip'], 'horizontal', 0, -1],
72+
'pelvis': [['LHip', 'RHip'], 'horizontal', 0, -1],
73+
'trunk': [['Neck', 'Hip'], 'horizontal', 0, -1],
74+
'shoulders': [['LShoulder', 'RShoulder'], 'horizontal', 0, -1],
75+
'head': [['Head', 'Neck'], 'horizontal', 0, -1],
76+
'right arm': [['RElbow', 'RShoulder'], 'horizontal', 0, -1],
77+
'left arm': [['LElbow', 'LShoulder'], 'horizontal', 0, -1],
78+
'right forearm': [['RWrist', 'RElbow'], 'horizontal', 0, -1],
79+
'left forearm': [['LWrist', 'LElbow'], 'horizontal', 0, -1],
80+
'right hand': [['RIndex', 'RWrist'], 'horizontal', 0, -1],
81+
'left hand': [['LIndex', 'LWrist'], 'horizontal', 0, -1]
82+
}
83+
84+
colors = [(255, 0, 0), (0, 0, 255), (255, 255, 0), (255, 0, 255), (0, 255, 255), (0, 0, 0), (255, 255, 255),
85+
(125, 0, 0), (0, 125, 0), (0, 0, 125), (125, 125, 0), (125, 0, 125), (0, 125, 125),
86+
(255, 125, 125), (125, 255, 125), (125, 125, 255), (255, 255, 125), (255, 125, 255), (125, 255, 255), (125, 125, 125),
87+
(255, 0, 125), (255, 125, 0), (0, 125, 255), (0, 255, 125), (125, 0, 255), (125, 255, 0), (0, 255, 0)]
88+
thickness = 1
89+
4690
## CLASSES
4791
class plotWindow():
4892
'''
@@ -96,6 +140,34 @@ def show(self):
96140
self.app.exec_()
97141

98142
## FUNCTIONS
143+
def read_trc(trc_path):
144+
'''
145+
Read a TRC file and extract its contents.
146+
147+
INPUTS:
148+
- trc_path (str): The path to the TRC file.
149+
150+
OUTPUTS:
151+
- tuple: A tuple containing the Q coordinates, frames column, time column, marker names, and header.
152+
'''
153+
154+
try:
155+
with open(trc_path, 'r') as trc_file:
156+
header = [next(trc_file) for _ in range(5)]
157+
markers = header[3].split('\t')[2::3]
158+
markers = [m.strip() for m in markers if m.strip()] # remove last \n character
159+
160+
trc_df = pd.read_csv(trc_path, sep="\t", skiprows=4, encoding='utf-8')
161+
frames_col, time_col = trc_df.iloc[:, 0], trc_df.iloc[:, 1]
162+
Q_coords = trc_df.drop(trc_df.columns[[0, 1]], axis=1)
163+
Q_coords = Q_coords.loc[:, ~Q_coords.columns.str.startswith('Unnamed')] # remove unnamed columns
164+
165+
return Q_coords, frames_col, time_col, markers, header
166+
167+
except Exception as e:
168+
raise ValueError(f"Error reading TRC file at {trc_path}: {e}")
169+
170+
99171
def interpolate_zeros_nans(col, *args):
100172
'''
101173
Interpolate missing points (of value zero),
@@ -288,6 +360,140 @@ def points_to_angles(points_list):
288360
return ang_deg
289361

290362

363+
def mean_angles(Q_coords, markers, ang_to_consider = ['right knee', 'left knee', 'right hip', 'left hip']):
364+
'''
365+
Compute the mean angle time series from 3D points for a given list of angles.
366+
367+
INPUTS:
368+
- Q_coords (DataFrame): The triangulated coordinates of the markers.
369+
- markers (list): The list of marker names.
370+
- ang_to_consider (list): The list of angles to consider (requires angle_dict).
371+
372+
OUTPUTS:
373+
- ang_mean: The mean angle time series.
374+
'''
375+
376+
ang_to_consider = ['right knee', 'left knee', 'right hip', 'left hip']
377+
378+
angs = []
379+
for ang_name in ang_to_consider:
380+
ang_params = angle_dict[ang_name]
381+
ang_mk = ang_params[0]
382+
383+
pts_for_angles = []
384+
for pt in ang_mk:
385+
pts_for_angles.append(Q_coords.iloc[:,markers.index(pt)*3:markers.index(pt)*3+3])
386+
ang = points_to_angles(pts_for_angles)
387+
388+
ang += ang_params[2]
389+
ang *= ang_params[3]
390+
ang = np.abs(ang)
391+
392+
angs.append(ang)
393+
394+
ang_mean = np.mean(angs, axis=0)
395+
396+
return ang_mean
397+
398+
399+
def best_coords_for_measurements(Q_coords, keypoints_names, fastest_frames_to_remove_percent=0.2, close_to_zero_speed=0.2, large_hip_knee_angles=45):
400+
'''
401+
Compute the best coordinates for measurements, after removing:
402+
- 20% fastest frames (may be outliers)
403+
- frames when speed is close to zero (person is out of frame): 0.2 m/frame, or 50 px/frame
404+
- frames when hip and knee angle below 45° (imprecise coordinates when person is crouching)
405+
406+
INPUTS:
407+
- Q_coords: pd.DataFrame. The XYZ coordinates of each marker
408+
- keypoints_names: list. The list of marker names
409+
- fastest_frames_to_remove_percent: float
410+
- close_to_zero_speed: float (sum for all keypoints: about 50 px/frame or 0.2 m/frame)
411+
- large_hip_knee_angles: int
412+
- trimmed_extrema_percent
413+
414+
OUTPUT:
415+
- Q_coords_low_speeds_low_angles: pd.DataFrame. The best coordinates for measurements
416+
'''
417+
418+
# Add Hip column if not present
419+
n_markers_init = len(keypoints_names)
420+
if 'Hip' not in keypoints_names:
421+
RHip_df = Q_coords.iloc[:,keypoints_names.index('RHip')*3:keypoints_names.index('RHip')*3+3]
422+
LHip_df = Q_coords.iloc[:,keypoints_names.index('LHip')*3:keypoints_names.index('LHip')*3+3]
423+
Hip_df = RHip_df.add(LHip_df, fill_value=0) /2
424+
Hip_df.columns = [col+ str(int(Q_coords.columns[-1][1:])+1) for col in ['X','Y','Z']]
425+
keypoints_names += ['Hip']
426+
Q_coords = pd.concat([Q_coords, Hip_df], axis=1)
427+
n_markers = len(keypoints_names)
428+
429+
# Using 80% slowest frames
430+
sum_speeds = pd.Series(np.nansum([np.linalg.norm(Q_coords.iloc[:,kpt:kpt+3].diff(), axis=1) for kpt in range(n_markers)], axis=0))
431+
sum_speeds = sum_speeds[sum_speeds>close_to_zero_speed] # Removing when speeds close to zero (out of frame)
432+
if len(sum_speeds)==0:
433+
raise ValueError('All frames have speed close to zero. Make sure the person is moving and correctly detected, or change close_to_zero_speed to a lower value.')
434+
min_speed_indices = sum_speeds.abs().nsmallest(int(len(sum_speeds) * (1-fastest_frames_to_remove_percent))).index
435+
Q_coords_low_speeds = Q_coords.iloc[min_speed_indices].reset_index(drop=True)
436+
437+
# Only keep frames with hip and knee flexion angles below 45%
438+
# (if more than 50 of them, else take 50 smallest values)
439+
ang_mean = mean_angles(Q_coords_low_speeds, keypoints_names, ang_to_consider = ['right knee', 'left knee', 'right hip', 'left hip'])
440+
Q_coords_low_speeds_low_angles = Q_coords_low_speeds[ang_mean < large_hip_knee_angles]
441+
if len(Q_coords_low_speeds_low_angles) < 50:
442+
Q_coords_low_speeds_low_angles = Q_coords_low_speeds.iloc[pd.Series(ang_mean).nsmallest(50).index]
443+
444+
if n_markers_init < n_markers:
445+
Q_coords_low_speeds_low_angles = Q_coords_low_speeds_low_angles.iloc[:,:-3]
446+
447+
return Q_coords_low_speeds_low_angles
448+
449+
450+
def compute_height(Q_coords, keypoints_names, fastest_frames_to_remove_percent=0.1, close_to_zero_speed=50, large_hip_knee_angles=45, trimmed_extrema_percent=0.5):
451+
'''
452+
Compute the height of the person from the trc data.
453+
454+
INPUTS:
455+
- Q_coords: pd.DataFrame. The XYZ coordinates of each marker
456+
- keypoints_names: list. The list of marker names
457+
- fastest_frames_to_remove_percent: float. Frames with high speed are considered as outliers
458+
- close_to_zero_speed: float. Sum for all keypoints: about 50 px/frame or 0.2 m/frame
459+
- large_hip_knee_angles5: float. Hip and knee angles below this value are considered as imprecise
460+
- trimmed_extrema_percent: float. Proportion of the most extreme segment values to remove before calculating their mean)
461+
462+
OUTPUT:
463+
- height: float. The estimated height of the person
464+
'''
465+
466+
# Retrieve most reliable coordinates
467+
Q_coords_low_speeds_low_angles = best_coords_for_measurements(Q_coords, keypoints_names,
468+
fastest_frames_to_remove_percent=fastest_frames_to_remove_percent, close_to_zero_speed=close_to_zero_speed, large_hip_knee_angles=large_hip_knee_angles)
469+
Q_coords_low_speeds_low_angles.columns = np.array([[m]*3 for m in keypoints_names]).flatten()
470+
471+
# Add MidShoulder column
472+
df_MidShoulder = pd.DataFrame((Q_coords_low_speeds_low_angles['RShoulder'].values + Q_coords_low_speeds_low_angles['LShoulder'].values) /2)
473+
df_MidShoulder.columns = ['MidShoulder']*3
474+
Q_coords_low_speeds_low_angles = pd.concat((Q_coords_low_speeds_low_angles.reset_index(drop=True), df_MidShoulder), axis=1)
475+
476+
# Automatically compute the height of the person
477+
pairs_up_to_shoulders = [['RHeel', 'RAnkle'], ['RAnkle', 'RKnee'], ['RKnee', 'RHip'], ['RHip', 'RShoulder'],
478+
['LHeel', 'LAnkle'], ['LAnkle', 'LKnee'], ['LKnee', 'LHip'], ['LHip', 'LShoulder']]
479+
try:
480+
rfoot, rshank, rfemur, rback, lfoot, lshank, lfemur, lback = [euclidean_distance(Q_coords_low_speeds_low_angles[pair[0]],Q_coords_low_speeds_low_angles[pair[1]]) for pair in pairs_up_to_shoulders]
481+
except:
482+
raise ValueError('At least one of the following markers is missing for computing the height of the person:\
483+
RHeel, RAnkle, RKnee, RHip, RShoulder, LHeel, LAnkle, LKnee, LHip, LShoulder.\
484+
Make sure that the person is entirely visible, or use a calibration file instead, or set "to_meters=false".')
485+
if 'Head' in keypoints_names:
486+
head = euclidean_distance(Q_coords_low_speeds_low_angles['MidShoulder'], Q_coords_low_speeds_low_angles['Head'])
487+
else:
488+
head = euclidean_distance(Q_coords_low_speeds_low_angles['MidShoulder'], Q_coords_low_speeds_low_angles['Nose'])*1.33
489+
heights = (rfoot + lfoot)/2 + (rshank + lshank)/2 + (rfemur + lfemur)/2 + (rback + lback)/2 + head
490+
491+
# Remove the 20% most extreme values
492+
height = trimmed_mean(heights, trimmed_extrema_percent=trimmed_extrema_percent)
493+
494+
return height
495+
496+
291497
def euclidean_distance(q1, q2):
292498
'''
293499
Euclidean distance between 2 points (N-dim).

0 commit comments

Comments
 (0)