|
23 | 23 | import logging
|
24 | 24 |
|
25 | 25 | import numpy as np
|
| 26 | +import pandas as pd |
26 | 27 | from scipy import interpolate
|
27 | 28 | import imageio_ffmpeg as ffmpeg
|
28 | 29 | import cv2
|
|
43 | 44 | __status__ = "Development"
|
44 | 45 |
|
45 | 46 |
|
| 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 | + |
46 | 90 | ## CLASSES
|
47 | 91 | class plotWindow():
|
48 | 92 | '''
|
@@ -96,6 +140,34 @@ def show(self):
|
96 | 140 | self.app.exec_()
|
97 | 141 |
|
98 | 142 | ## 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 | + |
99 | 171 | def interpolate_zeros_nans(col, *args):
|
100 | 172 | '''
|
101 | 173 | Interpolate missing points (of value zero),
|
@@ -288,6 +360,140 @@ def points_to_angles(points_list):
|
288 | 360 | return ang_deg
|
289 | 361 |
|
290 | 362 |
|
| 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 | + |
291 | 497 | def euclidean_distance(q1, q2):
|
292 | 498 | '''
|
293 | 499 | Euclidean distance between 2 points (N-dim).
|
|
0 commit comments