From 36b89ba6b2b47b9aaf7cd0163206d99411f2ec98 Mon Sep 17 00:00:00 2001 From: Seeeeeyo Date: Fri, 23 Jan 2026 15:14:57 -0700 Subject: [PATCH 1/4] Monocular session and trial handling + fixed risky operation (modifying dictionnary while iterating on it) --- utils.py | 172 +++++++++++++++++++++++++++++---------------- utilsKinematics.py | 40 +++++++++-- 2 files changed, 149 insertions(+), 63 deletions(-) diff --git a/utils.py b/utils.py index 7fc9b89..c8213fa 100644 --- a/utils.py +++ b/utils.py @@ -105,6 +105,8 @@ def get_trial_json(trial_id): def get_neutral_trial_id(session_id): session = get_session_json(session_id) + if session['isMono']: + return None neutral_ids = [t['id'] for t in session['trials'] if t['name']=='neutral'] if len(neutral_ids)>0: @@ -141,28 +143,36 @@ def get_camera_mapping(session_id, session_path): if not os.path.exists(mappingPath): mappingURL = trial['results'][resultTags.index('camera_mapping')]['media'] download_file(mappingURL, mappingPath) - -def get_model_and_metadata(session_id, session_path): - neutral_id = get_neutral_trial_id(session_id) - trial = get_trial_json(neutral_id) - resultTags = [res['tag'] for res in trial['results']] - - # Metadata. + +def get_metadata(session_path, trial, resultTags): metadataPath = os.path.join(session_path,'sessionMetadata.yaml') - if not os.path.exists(metadataPath) : + if not os.path.exists(metadataPath): metadataURL = trial['results'][resultTags.index('session_metadata')]['media'] download_file(metadataURL, metadataPath) - # Model. + +def get_model(session_path, trial, resultTags, isMono=False): modelURL = trial['results'][resultTags.index('opensim_model')]['media'] modelName = modelURL[modelURL.rfind('-')+1:modelURL.rfind('?')] modelFolder = os.path.join(session_path, 'OpenSimData', 'Model') + if isMono: + modelFolder = os.path.join(modelFolder, trial['name']) modelPath = os.path.join(modelFolder, modelName) if not os.path.exists(modelPath): os.makedirs(modelFolder, exist_ok=True) download_file(modelURL, modelPath) - + return modelName + + +def get_model_and_metadata(session_id, session_path): + neutral_id = get_neutral_trial_id(session_id) + trial = get_trial_json(neutral_id) + resultTags = [res['tag'] for res in trial['results']] + + get_metadata(session_path, trial, resultTags) + modelName = get_model(session_path, trial, resultTags) + return modelName def get_main_settings(session_folder,trial_name): @@ -185,7 +195,7 @@ def get_model_name_from_metadata(sessionFolder,appendText='_scaled'): return modelName -def get_motion_data(trial_id, session_path): +def get_motion_data(trial_id, session_path, isMono=False): trial = get_trial_json(trial_id) trial_name = trial['name'] resultTags = [res['tag'] for res in trial['results']] @@ -207,6 +217,10 @@ def get_motion_data(trial_id, session_path): if not os.path.exists(ikPath): ikURL = trial['results'][resultTags.index('ik_results')]['media'] download_file(ikURL, ikPath) + + # Model data if mono trial (isMono = True in session JSON) + if isMono: + get_model(session_path, trial, resultTags, isMono=True) # Main settings if 'main_settings' in resultTags: @@ -272,16 +286,19 @@ def download_kinematics(session_id, folder=None, trialNames=None): if folder is None: folder = os.getcwd() os.makedirs(folder, exist_ok=True) + + sessionJson = get_session_json(session_id) + isMono = sessionJson['isMono'] - # Model and metadata. - neutral_id = get_neutral_trial_id(session_id) - get_motion_data(neutral_id, folder) - modelName = get_model_and_metadata(session_id, folder) - # Remove extension from modelName - modelName = modelName.replace('.osim','') + if not isMono: + # Model and metadata. + neutral_id = get_neutral_trial_id(session_id) + get_motion_data(neutral_id, folder) + modelName = get_model_and_metadata(session_id, folder) + # Remove extension from modelName + modelName = modelName.replace('.osim','') # Session trial names. - sessionJson = get_session_json(session_id) sessionTrialNames = [t['name'] for t in sessionJson['trials']] if trialNames != None: [print(t + ' not in session trial names.') @@ -293,7 +310,7 @@ def download_kinematics(session_id, folder=None, trialNames=None): if trialNames is not None and trialDict['name'] not in trialNames: continue trial_id = trialDict['id'] - get_motion_data(trial_id,folder) + get_motion_data(trial_id,folder, isMono=isMono) loadedTrialNames.append(trialDict['name']) # Remove 'calibration' and 'neutral' from loadedTrialNames. @@ -312,12 +329,20 @@ def download_trial(trial_id, folder, session_id=None): session_id = trial['session_id'] os.makedirs(folder,exist_ok=True) + + # check if it is a mono trial + session = get_session_json(session_id) + isMono = session['isMono'] - # download model - get_model_and_metadata(session_id, folder) - - # download trc and mot - get_motion_data(trial_id,folder) + if isMono: + resultTags = [res['tag'] for res in trial['results']] + get_metadata(folder, trial, resultTags) + else: + # download model + get_model_and_metadata(session_id, folder) + + # download trc and mot + model if mono trial + get_motion_data(trial_id,folder, isMono=isMono) return trial['name'] @@ -494,8 +519,7 @@ def download_videos_from_server(session_id,trial_id, with open(os.path.join(session_path, "Videos", 'mappingCamDevice.pickle'), 'rb') as handle: mappingCamDevice = pickle.load(handle) # ensure upper on deviceID - for dID in mappingCamDevice.keys(): - mappingCamDevice[dID.upper()] = mappingCamDevice.pop(dID) + mappingCamDevice = {k.upper(): v for k, v in mappingCamDevice.items()} for video in trial["videos"]: k = mappingCamDevice[video["device_id"].replace('-', '').upper()] videoDir = os.path.join(session_path, "Videos", "Cam{}".format(k), "InputMedia", trial_name) @@ -650,44 +674,57 @@ def download_session(session_id, sessionBasePath= None, session = get_session_json(session_id) session_path = os.path.join(sessionBasePath,'OpenCapData_' + session_id) + + os.makedirs(session_path, exist_ok=True) + + isMono = session['isMono'] - calib_id = get_calibration_trial_id(session_id) - neutral_id = get_neutral_trial_id(session_id) + if not isMono: + calib_id = get_calibration_trial_id(session_id) + neutral_id = get_neutral_trial_id(session_id) + + # Calibration + try: + get_camera_mapping(session_id, session_path) + if downloadVideos: + download_videos_from_server(session_id,calib_id, + isCalibration=True,isStaticPose=False, + session_path = session_path) + + get_calibration(session_id,session_path) + except: + pass + + # Neutral + try: + modelName = get_model_and_metadata(session_id,session_path) + get_motion_data(neutral_id,session_path) + if downloadVideos: + download_videos_from_server(session_id,neutral_id, + isCalibration=False,isStaticPose=True, + session_path = session_path) + + get_syncd_videos(neutral_id,session_path) + except: + pass + dynamic_ids = [t['id'] for t in session['trials'] if (t['name'] != 'calibration' and t['name'] !='neutral')] - - # Calibration - try: - get_camera_mapping(session_id, session_path) - if downloadVideos: - download_videos_from_server(session_id,calib_id, - isCalibration=True,isStaticPose=False, - session_path = session_path) - get_calibration(session_id,session_path) - except: - pass - - # Neutral - try: - modelName = get_model_and_metadata(session_id,session_path) - get_motion_data(neutral_id,session_path) - if downloadVideos: - download_videos_from_server(session_id,neutral_id, - isCalibration=False,isStaticPose=True, - session_path = session_path) - - get_syncd_videos(neutral_id,session_path) - except: - pass + # Metadata for mono session + if isMono: + # hand the first dynamic trial + first_trial = get_trial_json(dynamic_ids[0]) + resultTags = [res['tag'] for res in first_trial['results']] + get_metadata(session_path, first_trial, resultTags) # Dynamic for dynamic_id in dynamic_ids: try: - get_motion_data(dynamic_id,session_path) + get_motion_data(dynamic_id,session_path, isMono=isMono) if downloadVideos: download_videos_from_server(session_id,dynamic_id, isCalibration=False,isStaticPose=False, - session_path = session_path) + session_path=session_path) get_syncd_videos(dynamic_id,session_path) except: @@ -705,16 +742,33 @@ def download_session(session_id, sessionBasePath= None, # Geometry try: - if 'Lai' in modelName: - modelType = 'LaiArnold' + if isMono: + # get all names of .osim files in subfolders of Model folder + modelDir = os.path.join(session_path, 'OpenSimData', 'Model') + modelNames = [] + for subfolder in os.listdir(modelDir): + subfolderPath = os.path.join(modelDir, subfolder) + if os.path.isdir(subfolderPath): + modelNames.extend([f for f in os.listdir(subfolderPath) if f.endswith('.osim')]) + # check if any of the model names contain 'Lai', assuming the same model type is used for all trials of the session + if any('Lai' in name for name in modelNames): + modelType = 'LaiArnold' + else: + raise ValueError("Geometries not available for this model, please contact us") + modelName = modelNames[0] + else: - raise ValueError("Geometries not available for this model, please contact us") + if 'Lai' in modelName: + modelType = 'LaiArnold' + else: + raise ValueError("Geometries not available for this model, please contact us") + if platform.system() == 'Windows': geometryDir = os.path.join(repoDir, 'tmp', modelType, 'Geometry') else: geometryDir = "/tmp/{}/Geometry".format(modelType) - # If not in cache, download from s3. - if not os.path.exists(geometryDir): + # If not in cache or empty, download from s3. + if not os.path.exists(geometryDir) or not os.listdir(geometryDir): os.makedirs(geometryDir, exist_ok=True) get_geometries(session_path, modelName=modelName) geometryDirEnd = os.path.join(session_path, 'OpenSimData', 'Model', 'Geometry') diff --git a/utilsKinematics.py b/utilsKinematics.py index f2629a6..9ad3622 100644 --- a/utilsKinematics.py +++ b/utilsKinematics.py @@ -46,18 +46,50 @@ def __init__(self, sessionDir, trialName, opensim.Logger.setLevelString('error') modelBasePath = os.path.join(sessionDir, 'OpenSimData', 'Model') + + # Check if this is a mono session (models stored in trial subfolders) + # Check specifically for a subfolder matching the trial name + isMono = False + if os.path.exists(modelBasePath): + trialModelPath = os.path.join(modelBasePath, trialName) + if os.path.isdir(trialModelPath): + isMono = True + # Load model if specified, otherwise load the one that was on server if modelName is None: - modelName = utils.get_model_name_from_metadata(sessionDir) - modelPath = os.path.join(modelBasePath,modelName) + if isMono: + # For mono sessions, look in the trial subfolder + trialModelPath = os.path.join(modelBasePath, trialName) + if os.path.exists(trialModelPath): + # Find .osim file in the trial subfolder + osimFiles = [f for f in os.listdir(trialModelPath) if f.endswith('.osim')] + if osimFiles: + modelPath = os.path.join(trialModelPath, osimFiles[0]) + else: + raise Exception('No .osim file found in ' + trialModelPath) + else: + raise Exception('Trial model folder does not exist: ' + trialModelPath) + else: + modelName = utils.get_model_name_from_metadata(sessionDir) + modelPath = os.path.join(modelBasePath, modelName) else: - modelPath = os.path.join(modelBasePath, - '{}.osim'.format(modelName)) + if isMono: + # For mono sessions, look in the trial subfolder + trialModelPath = os.path.join(modelBasePath, trialName) + if not modelName.endswith('.osim'): + modelName = modelName + '.osim' + modelPath = os.path.join(trialModelPath, modelName) + else: + if not modelName.endswith('.osim'): + modelPath = os.path.join(modelBasePath, '{}.osim'.format(modelName)) + else: + modelPath = os.path.join(modelBasePath, modelName) # make sure model exists if not os.path.exists(modelPath): raise Exception('Model path: ' + modelPath + ' does not exist.') + self.modelPath = modelPath self.model = opensim.Model(modelPath) self.model.initSystem() From 3ec7a6296d049ec95a19559dbcf72637debe6449 Mon Sep 17 00:00:00 2001 From: Seeeeeyo Date: Fri, 23 Jan 2026 15:25:58 -0700 Subject: [PATCH 2/4] isMono downloadKinematics: get metadata and geometries with modelName --- utils.py | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/utils.py b/utils.py index c8213fa..580e76b 100644 --- a/utils.py +++ b/utils.py @@ -291,7 +291,7 @@ def download_kinematics(session_id, folder=None, trialNames=None): isMono = sessionJson['isMono'] if not isMono: - # Model and metadata. + # Model and metadata from neutral trial neutral_id = get_neutral_trial_id(session_id) get_motion_data(neutral_id, folder) modelName = get_model_and_metadata(session_id, folder) @@ -304,7 +304,18 @@ def download_kinematics(session_id, folder=None, trialNames=None): [print(t + ' not in session trial names.') for t in trialNames if t not in sessionTrialNames] - # Motion data. + # Get dynamic trial IDs + dynamic_ids = [t['id'] for t in sessionJson['trials'] if (t['name'] != 'calibration' and t['name'] !='neutral')] + + # Metadata for mono session + if isMono: + # Get metadata from the first dynamic trial + if dynamic_ids: + first_trial = get_trial_json(dynamic_ids[0]) + resultTags = [res['tag'] for res in first_trial['results']] + get_metadata(folder, first_trial, resultTags) + + # Motion data for all dynamic trials loadedTrialNames = [] for trialDict in sessionJson['trials']: if trialNames is not None and trialDict['name'] not in trialNames: @@ -315,8 +326,25 @@ def download_kinematics(session_id, folder=None, trialNames=None): # Remove 'calibration' and 'neutral' from loadedTrialNames. loadedTrialNames = [i for i in loadedTrialNames if i!='neutral' and i!='calibration'] - + # Geometries. + if isMono: + # For mono sessions, find model names in subfolders + modelDir = os.path.join(folder, 'OpenSimData', 'Model') + if os.path.exists(modelDir): + modelNames = [] + for subfolder in os.listdir(modelDir): + subfolderPath = os.path.join(modelDir, subfolder) + if os.path.isdir(subfolderPath): + modelNames.extend([f for f in os.listdir(subfolderPath) if f.endswith('.osim')]) + if modelNames: + # Use first model name found (assuming same model type for all trials) + modelName = modelNames[0].replace('.osim', '') + else: + raise ValueError("No model files found in mono session subfolders") + else: + raise ValueError("Model directory does not exist") + get_geometries(folder, modelName=modelName) return loadedTrialNames, modelName From f72f98a4ab403e86b0eda53f3b542498b3a7c722 Mon Sep 17 00:00:00 2001 From: Seeeeeyo Date: Fri, 23 Jan 2026 16:16:06 -0700 Subject: [PATCH 3/4] marker mapping and project root path form import --- ActivityAnalyses/gait_analysis.py | 9 ++- marker_name_mapping.py | 99 +++++++++++++++++++++++++++++++ utilsKinematics.py | 26 ++++++++ 3 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 marker_name_mapping.py diff --git a/ActivityAnalyses/gait_analysis.py b/ActivityAnalyses/gait_analysis.py index f4e4949..839230d 100644 --- a/ActivityAnalyses/gait_analysis.py +++ b/ActivityAnalyses/gait_analysis.py @@ -19,7 +19,11 @@ """ import sys -sys.path.append('../') +import os +# Add project root to path +project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +if project_root not in sys.path: + sys.path.insert(0, project_root) import numpy as np import copy @@ -1046,5 +1050,4 @@ def detect_correct_order(rHS, rTO, lHS, lTO): 'eventNamesContralateral':['TO','HS'], 'ipsilateralLeg':leg} - return gaitEvents - + return gaitEvents \ No newline at end of file diff --git a/marker_name_mapping.py b/marker_name_mapping.py new file mode 100644 index 0000000..fd5dd62 --- /dev/null +++ b/marker_name_mapping.py @@ -0,0 +1,99 @@ +""" +Marker name mapping dictionary for converting from expected format (with '_study' suffix) +to actual format (without '_study' suffix, lowercase). + +This mapping is used to rename markers in TRC files to match the expected format +used by the gait_analysis class. + +Expected format: markers end with '_study' (e.g., 'r_calc_study', 'r.ASIS_study') +Actual format: markers without '_study' suffix, lowercase (e.g., 'r_calc', 'r_ASIS') +""" + +MARKER_NAME_MAPPING = { + # Pelvis markers + 'r.ASIS_study': 'r_ASIS', + 'L.ASIS_study': 'l_ASIS', + 'r.PSIS_study': 'r_PSIS', + 'L.PSIS_study': 'l_PSIS', + + # Right leg markers + 'r_knee_study': 'r_knee', + 'r_mknee_study': 'r_mknee', + 'r_ankle_study': 'r_ankle', + 'r_mankle_study': 'r_mankle', + 'r_toe_study': 'r_toe', + 'r_5meta_study': 'r_5meta', + 'r_calc_study': 'r_calc', + + # Left leg markers + 'L_knee_study': 'l_knee', + 'L_mknee_study': 'l_mknee', + 'L_ankle_study': 'l_ankle', + 'L_mankle_study': 'l_mankle', + 'L_toe_study': 'l_toe', + 'L_calc_study': 'l_calc', + 'L_5meta_study': 'l_5meta', + + # Shoulder markers + 'r_shoulder_study': 'r_shoulder', + 'L_shoulder_study': 'l_shoulder', + + # Spine markers + 'C7_study': 'C7', + + # Thigh markers (may not exist in all files) + # 'r_thigh1_study': 'r_thigh1', # Check if exists in actual file + # 'r_thigh2_study': 'r_thigh2', # Check if exists in actual file + # 'r_thigh3_study': 'r_thigh3', # Check if exists in actual file + # 'L_thigh1_study': 'l_thigh1', # Check if exists in actual file + # 'L_thigh2_study': 'l_thigh2', # Check if exists in actual file + # 'L_thigh3_study': 'l_thigh3', # Check if exists in actual file + + # Shank markers (may not exist in all files) + # 'r_sh1_study': 'r_sh1', # Check if exists in actual file + # 'r_sh2_study': 'r_sh2', # Check if exists in actual file + # 'r_sh3_study': 'r_sh3', # Check if exists in actual file + # 'L_sh1_study': 'l_sh1', # Check if exists in actual file + # 'L_sh2_study': 'l_sh2', # Check if exists in actual file + # 'L_sh3_study': 'l_sh3', # Check if exists in actual file + + # Hip joint centers + 'RHJC_study': 'RHJC', # Check if exists in actual file + 'LHJC_study': 'LHJC', # Check if exists in actual file + + # Elbow markers + # 'r_lelbow_study': 'r_elbow', # Approximate mapping + 'r_melbow_study': 'r_melbow', + # 'L_lelbow_study': 'l_elbow', # Approximate mapping + 'L_melbow_study': 'l_melbow', + + # Wrist markers + # 'r_lwrist_study': 'r_wrist_radius', # Approximate mapping + # 'r_mwrist_study': 'r_wrist_ulna', # Approximate mapping + # 'L_lwrist_study': 'l_wrist_radius', # Approximate mapping + # 'L_mwrist_study': 'l_wrist_ulna', # Approximate mapping +} + +# Reverse mapping (actual -> expected) for renaming markers in TRC files +REVERSE_MARKER_NAME_MAPPING = {v: k for k, v in MARKER_NAME_MAPPING.items()} + +# if __name__ == '__main__': +# """ +# Print the mapping dictionary in a format suitable for use in cloud functions. +# """ +# import json + +# print("Marker name mapping (expected -> actual):") +# print("=" * 60) +# for expected, actual in sorted(MARKER_NAME_MAPPING.items()): +# print(f" '{expected}' -> '{actual}'") + +# print("\n\nReverse mapping (actual -> expected) for renaming:") +# print("=" * 60) +# for actual, expected in sorted(REVERSE_MARKER_NAME_MAPPING.items()): +# print(f" '{actual}' -> '{expected}'") + +# print("\n\nJSON format (for cloud function):") +# print("=" * 60) +# print(json.dumps(REVERSE_MARKER_NAME_MAPPING, indent=2)) + diff --git a/utilsKinematics.py b/utilsKinematics.py index 9ad3622..f82bc37 100644 --- a/utilsKinematics.py +++ b/utilsKinematics.py @@ -32,6 +32,13 @@ import numpy as np from scipy.spatial.transform import Rotation +# Import marker name mapping for conversion +try: + from marker_name_mapping import REVERSE_MARKER_NAME_MAPPING +except ImportError: + # If mapping file doesn't exist, use empty dict (no conversion) + REVERSE_MARKER_NAME_MAPPING = {} + class kinematics: @@ -207,6 +214,25 @@ def get_marker_dict(self, session_dir, trial_name, '{}.trc'.format(trial_name)) markerDict = trc_2_dict(trcFilePath) + + # Convert marker names from actual format to expected format (with _study suffix) + if REVERSE_MARKER_NAME_MAPPING: + converted_markers = {} + # First pass: add markers that are already in correct format (prioritize these) + for marker_name, marker_data in markerDict['markers'].items(): + if marker_name not in REVERSE_MARKER_NAME_MAPPING: + # Already in correct format or unknown marker - keep as-is + converted_markers[marker_name] = marker_data + # Second pass: convert markers that need renaming (only if target doesn't exist) + for marker_name, marker_data in markerDict['markers'].items(): + if marker_name in REVERSE_MARKER_NAME_MAPPING: + new_name = REVERSE_MARKER_NAME_MAPPING[marker_name] + # Only convert if the target name doesn't already exist + # (avoids overwriting markers already in correct format) + if new_name not in converted_markers: + converted_markers[new_name] = marker_data + markerDict['markers'] = converted_markers + if lowpass_cutoff_frequency > 0: markerDict['markers'] = { marker_name: lowPassFilter(self.time, data, lowpass_cutoff_frequency) From 832bd1ae3ebf555117875b4d6dd9cdccbdce5285 Mon Sep 17 00:00:00 2001 From: Seeeeeyo Date: Fri, 23 Jan 2026 16:57:37 -0700 Subject: [PATCH 4/4] api url using function --- utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils.py b/utils.py index 580e76b..78932c4 100644 --- a/utils.py +++ b/utils.py @@ -587,7 +587,7 @@ def get_calibration(session_id,session_path): def download_and_switch_calibration(session_id,session_path,calibTrialID = None): if calibTrialID == None: calibTrialID = get_calibration_trial_id(session_id) - resp = requests.get("https://api.opencap.ai/trials/{}/".format(calibTrialID), + resp = requests.get("{}trials/{}/".format(API_URL, calibTrialID), headers = {"Authorization": "Token {}".format(API_TOKEN)}) trial = resp.json()