implementing OpensimProcessing (#130)

* implementing opensimProcessing

* edited in cooperating with opensimProcessing

* Update scaling2IK.py

* implementing opensimProcessing

* Update and rename scaling2IK.py to kinematics.py

* Add files via upload

* Add files via upload

* Update Config.toml

* Update Config.toml

* Update Config.toml

* Update Config.toml

* Update Config.toml

* Update Config.toml

* implementing opensim processing

* OpenSim part addition

* OpenSim Processing function signature adjustment

* code logic and layout adjustments

* opensimProcessing test enabled

* Update Pose2Sim.py

* Update Pose2Sim.py

* docstring for opensimProcessing updated

* saved folder name changed to opensim

* opensim processing tests enabled

* Deleted a repetitive line at opensim kinematics section
This commit is contained in:
peterlololsss 2024-09-18 07:04:52 +08:00 committed by GitHub
parent 7af1584e01
commit 215c8a6e6c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 675 additions and 145 deletions

View File

@ -186,6 +186,9 @@ static_trial = ['S00_P00_Participant/S00_P00_T00_StaticTrial']
# # At the Participant level, specify the name of the static trial folder name, e.g. ['S00_P00_T00_StaticTrial']; # # At the Participant level, specify the name of the static trial folder name, e.g. ['S00_P00_T00_StaticTrial'];
# # At the Session level, add participant subdirectory, e.g. ['S00_P00_Participant/S00_P00_T00_StaticTrial', 'S00_P01_Participant/S00_P00_T00_StaticTrial'] # # At the Session level, add participant subdirectory, e.g. ['S00_P00_Participant/S00_P00_T00_StaticTrial', 'S00_P01_Participant/S00_P00_T00_StaticTrial']
opensim_bin_path = 'C:\OpenSim 4.4\bin' opensim_bin_path = 'C:\OpenSim 4.4\bin'
use_augmentation = false # If using augmented measurements then set it true
load_trc_name = 'filtered' # 'default' or 'filtered', if use_augmentation = true, this line will be ignored instead using __LSTM.trc
IK_timeRange = [] #left empty to IK full range or eg.[0.5,1.0]

View File

@ -181,11 +181,14 @@
# [opensim] # [opensim]
# static_trial = ['S00_P00_Participant/S00_P00_T00_StaticTrial'] #static_trial = ['S00_P00_Participant/S00_P00_T00_StaticTrial']
# # # If this Config.toml file is at the Trial level, set to true or false (lowercase); # # If this Config.toml file is at the Trial level, set to true or false (lowercase);
# # # At the Participant level, specify the name of the static trial folder name, e.g. ['S00_P00_T00_StaticTrial']; # # At the Participant level, specify the name of the static trial folder name, e.g. ['S00_P00_T00_StaticTrial'];
# # # At the Session level, add participant subdirectory, e.g. ['S00_P00_Participant/S00_P00_T00_StaticTrial', 'S00_P01_Participant/S00_P00_T00_StaticTrial'] # # At the Session level, add participant subdirectory, e.g. ['S00_P00_Participant/S00_P00_T00_StaticTrial', 'S00_P01_Participant/S00_P00_T00_StaticTrial']
# opensim_bin_path = 'C:\OpenSim 4.4\bin' #opensim_bin_path = 'C:\OpenSim 4.4\bin'
#use_augmentation = false # If using augmented measurements then set it true
#load_trc_name = 'filtered' # 'default' or 'filtered', if use_augmentation = true, this line will be ignored instead using __LSTM.trc
#IK_timeRange = [] #left empty to IK full range or eg.[0.5,1.0]

View File

@ -19,8 +19,8 @@
[project] [project]
multi_person = true # true for trials with multiple participants. If false, only the main person in scene is analyzed (and it run much faster). multi_person = true # true for trials with multiple participants. If false, only the main person in scene is analyzed (and it run much faster).
participant_height = [1.72, 1.40] # m # float if single person, list of float if multi-person (same order as the Static trials) # Only used for marker augmentation participant_height = [1.72, 1.40, 1.86] # m # float if single person, list of float if multi-person (same order as the Static trials) # Only used for marker augmentation
participant_mass = [70.0, 63.5] # kg # Only used for marker augmentation and scaling participant_mass = [70.0, 63.5, 88.0] # kg # Only used for marker augmentation and scaling
# frame_rate = 'auto' # fps # int or 'auto'. If 'auto', finds from video (or defaults to 60 fps if you work with images) # frame_rate = 'auto' # fps # int or 'auto'. If 'auto', finds from video (or defaults to 60 fps if you work with images)
# frame_range = [] # For example [10,300], or [] for all frames. # frame_range = [] # For example [10,300], or [] for all frames.
@ -181,11 +181,14 @@ keypoints_to_consider = 'all' # 'all' if all points should be considered, for ex
# [opensim] # [opensim]
# static_trial = ['S00_P00_Participant/S00_P00_T00_StaticTrial'] #static_trial = ['S00_P00_Participant/S00_P00_T00_StaticTrial']
# # # If this Config.toml file is at the Trial level, set to true or false (lowercase); # # If this Config.toml file is at the Trial level, set to true or false (lowercase);
# # # At the Participant level, specify the name of the static trial folder name, e.g. ['S00_P00_T00_StaticTrial']; # # At the Participant level, specify the name of the static trial folder name, e.g. ['S00_P00_T00_StaticTrial'];
# # # At the Session level, add participant subdirectory, e.g. ['S00_P00_Participant/S00_P00_T00_StaticTrial', 'S00_P01_Participant/S00_P00_T00_StaticTrial'] # # At the Session level, add participant subdirectory, e.g. ['S00_P00_Participant/S00_P00_T00_StaticTrial', 'S00_P01_Participant/S00_P00_T00_StaticTrial']
# opensim_bin_path = 'C:\OpenSim 4.4\bin' #opensim_bin_path = 'C:\OpenSim 4.4\bin'
#use_augmentation = false # If using augmented measurements then set it true
#load_trc_name = 'filtered' # 'default' or 'filtered', if use_augmentation = true, this line will be ignored instead using __LSTM.trc
#IK_timeRange = [] #left empty to IK full range or eg.[0.5,1.0]

View File

@ -19,8 +19,8 @@
[project] [project]
multi_person = true # true for trials with multiple participants. If false, only the main person in scene is analyzed (and it run much faster). multi_person = true # true for trials with multiple participants. If false, only the main person in scene is analyzed (and it run much faster).
participant_height = [1.72, 1.40] # m # float if single person, list of float if multi-person (same order as the Static trials) # Only used for marker augmentation participant_height = [1.72, 1.40, 1.90] # m # float if single person, list of float if multi-person (same order as the Static trials) # Only used for marker augmentation
participant_mass = [70.0, 63.5] # kg # Only used for marker augmentation and scaling participant_mass = [70.0, 63.5, 90.0] # kg # Only used for marker augmentation and scaling
frame_rate = 'auto' # fps # int or 'auto'. If 'auto', finds from video (or defaults to 60 fps if you work with images) frame_rate = 'auto' # fps # int or 'auto'. If 'auto', finds from video (or defaults to 60 fps if you work with images)
frame_range = [] # For example [10,300], or [] for all frames. frame_range = [] # For example [10,300], or [] for all frames.
@ -186,6 +186,9 @@ static_trial = ['S00_P00_Participant/S00_P00_T00_StaticTrial']
# # At the Participant level, specify the name of the static trial folder name, e.g. ['S00_P00_T00_StaticTrial']; # # At the Participant level, specify the name of the static trial folder name, e.g. ['S00_P00_T00_StaticTrial'];
# # At the Session level, add participant subdirectory, e.g. ['S00_P00_Participant/S00_P00_T00_StaticTrial', 'S00_P01_Participant/S00_P00_T00_StaticTrial'] # # At the Session level, add participant subdirectory, e.g. ['S00_P00_Participant/S00_P00_T00_StaticTrial', 'S00_P01_Participant/S00_P00_T00_StaticTrial']
opensim_bin_path = 'C:\OpenSim 4.4\bin' opensim_bin_path = 'C:\OpenSim 4.4\bin'
use_augmentation = false # If using augmented measurements then set it true
load_trc_name = 'filtered' # 'default' or 'filtered', if use_augmentation = true, this line will be ignored instead using __LSTM.trc
IK_timeRange = [] #left empty to IK full range or eg.[0.5,1.0]

View File

@ -53,9 +53,9 @@ output_format = 'openpose' # 'openpose', 'mmpose', 'deeplabcut', 'none' or a lis
[synchronization] [synchronization]
display_sync_plots = true # true or false (lowercase) display_sync_plots = true # true or false (lowercase)
keypoints_to_consider = ['RWrist'] # 'all' if all points should be considered, for example if the participant did not perform any particicular sharp movement. In this case, the capture needs to be 5-10 seconds long at least keypoints_to_consider = ['RWrist'] # 'all' if all points should be considered, for example if the participant did not perform any particicular sharp movement. In this case, the capture needs to be 5-10 seconds long at least
# ['RWrist', 'RElbow'] list of keypoint names if you want to specify keypoints with a sharp vertical motion. # ['RWrist', 'RElbow'] list of keypoint names if you want to specify the keypoints to consider.
approx_time_maxspeed = 'auto' # 'auto' if you want to consider the whole capture (default, slower if long sequences) approx_time_maxspeed = 'auto' # 'auto' if you want to consider the whole capture (default, slower if long sequences)
# [10.0, 2.0, 8.0, 11.0] list of times (seconds) if you want to specify the approximate time of a clear vertical event for each camera # [10.0, 2.0, 8.0, 11.0] list of times in seconds, one value per camera if you want to specify the approximate time of a clear vertical event by one person standing alone in the scene
time_range_around_maxspeed = 2.0 # Search for best correlation in the range [approx_time_maxspeed - time_range_around_maxspeed, approx_time_maxspeed + time_range_around_maxspeed] time_range_around_maxspeed = 2.0 # Search for best correlation in the range [approx_time_maxspeed - time_range_around_maxspeed, approx_time_maxspeed + time_range_around_maxspeed]
likelihood_threshold = 0.4 # Keypoints whose likelihood is below likelihood_threshold are filtered out likelihood_threshold = 0.4 # Keypoints whose likelihood is below likelihood_threshold are filtered out
filter_cutoff = 6 # time series are smoothed to get coherent time-lagged correlation filter_cutoff = 6 # time series are smoothed to get coherent time-lagged correlation
@ -186,6 +186,11 @@ static_trial = ['S00_P00_Participant/S00_P00_T00_StaticTrial']
# # At the Participant level, specify the name of the static trial folder name, e.g. ['S00_P00_T00_StaticTrial']; # # At the Participant level, specify the name of the static trial folder name, e.g. ['S00_P00_T00_StaticTrial'];
# # At the Session level, add participant subdirectory, e.g. ['S00_P00_Participant/S00_P00_T00_StaticTrial', 'S00_P01_Participant/S00_P00_T00_StaticTrial'] # # At the Session level, add participant subdirectory, e.g. ['S00_P00_Participant/S00_P00_T00_StaticTrial', 'S00_P01_Participant/S00_P00_T00_StaticTrial']
opensim_bin_path = 'C:\OpenSim 4.4\bin' opensim_bin_path = 'C:\OpenSim 4.4\bin'
use_augmentation = false # If using augmented measurements then set it true
load_trc_name = 'filtered' # 'default' or 'filtered', if use_augmentation = true, this line will be ignored instead using __LSTM.trc
IK_timeRange = [] #left empty to IK full range or eg.[0.5,1.0]

View File

@ -7,7 +7,7 @@
## POSE2SIM ## ## POSE2SIM ##
########################################################################### ###########################################################################
This repository offers a way to perform markerless kinematics, and gives an This repository offers a way to perform markerless kinematics, and gives an
example workflow from an Openpose input to an OpenSim result. example workflow from an Openpose input to an OpenSim result.
It offers tools for: It offers tools for:
@ -16,20 +16,20 @@ It offers tools for:
- Camera synchronization, - Camera synchronization,
- Tracking the person of interest, - Tracking the person of interest,
- Robust triangulation, - Robust triangulation,
- Filtration, - Filtration,
- Marker augmentation, - Marker augmentation,
- OpenSim scaling and inverse kinematics - OpenSim scaling and inverse kinematics
It has been tested on Windows, Linux and MacOS, and works for any Python version >= 3.9 It has been tested on Windows, Linux and MacOS, and works for any Python version >= 3.9
Installation: Installation:
# Open Anaconda prompt. Type: # Open Anaconda prompt. Type:
# - conda create -n Pose2Sim python=3.9 # - conda create -n Pose2Sim python=3.9
# - conda activate Pose2Sim # - conda activate Pose2Sim
# - conda install -c opensim-org opensim -y # - conda install -c opensim-org opensim -y
# - pip install Pose2Sim # - pip install Pose2Sim
Usage: Usage:
# First run Pose estimation and organize your directories (see Readme.md) # First run Pose estimation and organize your directories (see Readme.md)
from Pose2Sim import Pose2Sim from Pose2Sim import Pose2Sim
Pose2Sim.calibration() Pose2Sim.calibration()
@ -39,6 +39,7 @@ Pose2Sim.personAssociation()
Pose2Sim.triangulation() Pose2Sim.triangulation()
Pose2Sim.filtering() Pose2Sim.filtering()
Pose2Sim.markerAugmentation() Pose2Sim.markerAugmentation()
Pose2Sim.opensimProcessing()
# Then run OpenSim (see Readme.md) # Then run OpenSim (see Readme.md)
''' '''
@ -69,15 +70,15 @@ def setup_logging(session_dir):
Create logging file and stream handlers Create logging file and stream handlers
''' '''
logging.basicConfig(format='%(message)s', level=logging.INFO, logging.basicConfig(format='%(message)s', level=logging.INFO,
handlers = [logging.handlers.TimedRotatingFileHandler(os.path.join(session_dir, 'logs.txt'), when='D', interval=7), logging.StreamHandler()]) handlers = [logging.handlers.TimedRotatingFileHandler(os.path.join(session_dir, 'logs.txt'), when='D', interval=7), logging.StreamHandler()])
def recursive_update(dict_to_update, dict_with_new_values): def recursive_update(dict_to_update, dict_with_new_values):
''' '''
Update nested dictionaries without overwriting existing keys in any level of nesting Update nested dictionaries without overwriting existing keys in any level of nesting
Example: Example:
dict_to_update = {'key': {'key_1': 'val_1', 'key_2': 'val_2'}} dict_to_update = {'key': {'key_1': 'val_1', 'key_2': 'val_2'}}
dict_with_new_values = {'key': {'key_1': 'val_1_new'}} dict_with_new_values = {'key': {'key_1': 'val_1_new'}}
returns {'key': {'key_1': 'val_1_new', 'key_2': 'val_2'}} returns {'key': {'key_1': 'val_1_new', 'key_2': 'val_2'}}
@ -111,7 +112,7 @@ def determine_level(config_dir):
def read_config_files(config): def read_config_files(config):
''' '''
Read Root and Trial configuration files, Read Root and Trial configuration files,
and output a dictionary with all the parameters. and output a dictionary with all the parameters.
''' '''
@ -123,9 +124,9 @@ def read_config_files(config):
config_dict.get("project").update({"project_dir":"<YOUR_PROJECT_DIRECTORY>"})') config_dict.get("project").update({"project_dir":"<YOUR_PROJECT_DIRECTORY>"})')
else: else:
# if launched without an argument, config == None, else it is the path to the config directory # if launched without an argument, config == None, else it is the path to the config directory
config_dir = ['.' if config == None else config][0] config_dir = ['.' if config == None else config][0]
level = determine_level(config_dir) level = determine_level(config_dir)
# Trial level # Trial level
if level == 1: # Trial if level == 1: # Trial
try: try:
@ -138,7 +139,7 @@ def read_config_files(config):
session_config_dict = toml.load(os.path.join(config_dir, 'Config.toml')) session_config_dict = toml.load(os.path.join(config_dir, 'Config.toml'))
session_config_dict.get("project").update({"project_dir":config_dir}) session_config_dict.get("project").update({"project_dir":config_dir})
config_dicts = [session_config_dict] config_dicts = [session_config_dict]
# Root level # Root level
if level == 2: if level == 2:
session_config_dict = toml.load(os.path.join(config_dir, 'Config.toml')) session_config_dict = toml.load(os.path.join(config_dir, 'Config.toml'))
@ -157,10 +158,11 @@ def read_config_files(config):
return level, config_dicts return level, config_dicts
def calibration(config=None): def calibration(config=None):
''' '''
Cameras calibration from checkerboards or from qualisys files. Cameras calibration from checkerboards or from qualisys files.
config can be a dictionary, config can be a dictionary,
or a the directory path of a trial, participant, or session, or a the directory path of a trial, participant, or session,
or the function can be called without an argument, in which case it the config directory is the current one. or the function can be called without an argument, in which case it the config directory is the current one.
@ -178,9 +180,9 @@ def calibration(config=None):
config_dict.get("project").update({"project_dir":session_dir}) config_dict.get("project").update({"project_dir":session_dir})
# Set up logging # Set up logging
setup_logging(session_dir) setup_logging(session_dir)
currentDateAndTime = datetime.now() currentDateAndTime = datetime.now()
# Run calibration # Run calibration
calib_dir = [os.path.join(session_dir, c) for c in os.listdir(session_dir) if os.path.isdir(os.path.join(session_dir, c)) and 'calib' in c.lower()][0] calib_dir = [os.path.join(session_dir, c) for c in os.listdir(session_dir) if os.path.isdir(os.path.join(session_dir, c)) and 'calib' in c.lower()][0]
logging.info("\n---------------------------------------------------------------------") logging.info("\n---------------------------------------------------------------------")
@ -189,23 +191,22 @@ def calibration(config=None):
logging.info(f"Calibration directory: {calib_dir}") logging.info(f"Calibration directory: {calib_dir}")
logging.info("---------------------------------------------------------------------\n") logging.info("---------------------------------------------------------------------\n")
start = time.time() start = time.time()
calibrate_cams_all(config_dict) calibrate_cams_all(config_dict)
end = time.time() end = time.time()
logging.info(f'\nCalibration took {end-start:.2f} s.\n') logging.info(f'\nCalibration took {end-start:.2f} s.\n')
logging.shutdown()
def poseEstimation(config=None): def poseEstimation(config=None):
''' '''
Estimate pose using RTMLib Estimate pose using RTMLib
config can be a dictionary, config can be a dictionary,
or a the directory path of a trial, participant, or session, or a the directory path of a trial, participant, or session,
or the function can be called without an argument, in which case it the config directory is the current one. or the function can be called without an argument, in which case it the config directory is the current one.
''' '''
from Pose2Sim.poseEstimation import rtm_estimator # The name of the function might change from Pose2Sim.poseEstimation import rtm_estimator # The name of the function might change
level, config_dicts = read_config_files(config) level, config_dicts = read_config_files(config)
@ -234,25 +235,23 @@ def poseEstimation(config=None):
logging.info(f"On {currentDateAndTime.strftime('%A %d. %B %Y, %H:%M:%S')}") logging.info(f"On {currentDateAndTime.strftime('%A %d. %B %Y, %H:%M:%S')}")
logging.info(f"Project directory: {project_dir}") logging.info(f"Project directory: {project_dir}")
logging.info("---------------------------------------------------------------------\n") logging.info("---------------------------------------------------------------------\n")
rtm_estimator(config_dict) rtm_estimator(config_dict)
end = time.time() end = time.time()
elapsed = end - start elapsed = end - start
logging.info(f'\nPose estimation took {time.strftime("%Hh%Mm%Ss", time.gmtime(elapsed))}.\n') logging.info(f'\nPose estimation took {time.strftime("%Hh%Mm%Ss", time.gmtime(elapsed))}.\n')
logging.shutdown()
def synchronization(config=None): def synchronization(config=None):
''' '''
Synchronize cameras if needed. Synchronize cameras if needed.
config can be a dictionary, config can be a dictionary,
or a the directory path of a trial, participant, or session, or a the directory path of a trial, participant, or session,
or the function can be called without an argument, in which case it the config directory is the current one. or the function can be called without an argument, in which case it the config directory is the current one.
''' '''
# Import the function # Import the function
from Pose2Sim.synchronization import synchronize_cams_all from Pose2Sim.synchronization import synchronize_cams_all
@ -267,7 +266,7 @@ def synchronization(config=None):
# Set up logging # Set up logging
session_dir = os.path.realpath(os.path.join(config_dicts[0].get('project').get('project_dir'), '..')) session_dir = os.path.realpath(os.path.join(config_dicts[0].get('project').get('project_dir'), '..'))
setup_logging(session_dir) setup_logging(session_dir)
# Batch process all trials # Batch process all trials
for config_dict in config_dicts: for config_dict in config_dicts:
@ -280,26 +279,24 @@ def synchronization(config=None):
logging.info(f"On {currentDateAndTime.strftime('%A %d. %B %Y, %H:%M:%S')}") logging.info(f"On {currentDateAndTime.strftime('%A %d. %B %Y, %H:%M:%S')}")
logging.info(f"Project directory: {project_dir}") logging.info(f"Project directory: {project_dir}")
logging.info("---------------------------------------------------------------------\n") logging.info("---------------------------------------------------------------------\n")
synchronize_cams_all(config_dict)
end = time.time()
elapsed = end-start
logging.info(f'\nSynchronization took {time.strftime("%Hh%Mm%Ss", time.gmtime(elapsed))}.\n')
logging.shutdown() synchronize_cams_all(config_dict)
end = time.time()
elapsed = end-start
logging.info(f'\nSynchronization took {time.strftime("%Hh%Mm%Ss", time.gmtime(elapsed))}.\n')
def personAssociation(config=None): def personAssociation(config=None):
''' '''
Tracking one or several persons of interest. Tracking one or several persons of interest.
Needs a calibration file. Needs a calibration file.
config can be a dictionary, config can be a dictionary,
or a the directory path of a trial, participant, or session, or a the directory path of a trial, participant, or session,
or the function can be called without an argument, in which case it the config directory is the current one. or the function can be called without an argument, in which case it the config directory is the current one.
''' '''
from Pose2Sim.personAssociation import track_2d_all from Pose2Sim.personAssociation import track_2d_all
# Determine the level at which the function is called (root:2, trial:1) # Determine the level at which the function is called (root:2, trial:1)
@ -313,7 +310,7 @@ def personAssociation(config=None):
# Set up logging # Set up logging
session_dir = os.path.realpath(os.path.join(config_dicts[0].get('project').get('project_dir'), '..')) session_dir = os.path.realpath(os.path.join(config_dicts[0].get('project').get('project_dir'), '..'))
setup_logging(session_dir) setup_logging(session_dir)
# Batch process all trials # Batch process all trials
for config_dict in config_dicts: for config_dict in config_dicts:
@ -329,20 +326,18 @@ def personAssociation(config=None):
logging.info(f"On {currentDateAndTime.strftime('%A %d. %B %Y, %H:%M:%S')}") logging.info(f"On {currentDateAndTime.strftime('%A %d. %B %Y, %H:%M:%S')}")
logging.info(f"Project directory: {project_dir}") logging.info(f"Project directory: {project_dir}")
logging.info("---------------------------------------------------------------------\n") logging.info("---------------------------------------------------------------------\n")
track_2d_all(config_dict) track_2d_all(config_dict)
end = time.time() end = time.time()
elapsed = end-start elapsed = end-start
logging.info(f'\nAssociating persons took {time.strftime("%Hh%Mm%Ss", time.gmtime(elapsed))}.\n') logging.info(f'\nAssociating persons took {time.strftime("%Hh%Mm%Ss", time.gmtime(elapsed))}.\n')
logging.shutdown()
def triangulation(config=None): def triangulation(config=None):
''' '''
Robust triangulation of 2D points coordinates. Robust triangulation of 2D points coordinates.
config can be a dictionary, config can be a dictionary,
or a the directory path of a trial, participant, or session, or a the directory path of a trial, participant, or session,
or the function can be called without an argument, in which case it the config directory is the current one. or the function can be called without an argument, in which case it the config directory is the current one.
@ -361,7 +356,7 @@ def triangulation(config=None):
# Set up logging # Set up logging
session_dir = os.path.realpath(os.path.join(config_dicts[0].get('project').get('project_dir'), '..')) session_dir = os.path.realpath(os.path.join(config_dicts[0].get('project').get('project_dir'), '..'))
setup_logging(session_dir) setup_logging(session_dir)
# Batch process all trials # Batch process all trials
for config_dict in config_dicts: for config_dict in config_dicts:
@ -377,20 +372,18 @@ def triangulation(config=None):
logging.info(f"On {currentDateAndTime.strftime('%A %d. %B %Y, %H:%M:%S')}") logging.info(f"On {currentDateAndTime.strftime('%A %d. %B %Y, %H:%M:%S')}")
logging.info(f"Project directory: {project_dir}") logging.info(f"Project directory: {project_dir}")
logging.info("---------------------------------------------------------------------\n") logging.info("---------------------------------------------------------------------\n")
triangulate_all(config_dict) triangulate_all(config_dict)
end = time.time() end = time.time()
elapsed = end-start elapsed = end-start
logging.info(f'\nTriangulation took {time.strftime("%Hh%Mm%Ss", time.gmtime(elapsed))}.\n') logging.info(f'\nTriangulation took {time.strftime("%Hh%Mm%Ss", time.gmtime(elapsed))}.\n')
logging.shutdown()
def filtering(config=None): def filtering(config=None):
''' '''
Filter trc 3D coordinates. Filter trc 3D coordinates.
config can be a dictionary, config can be a dictionary,
or a the directory path of a trial, participant, or session, or a the directory path of a trial, participant, or session,
or the function can be called without an argument, in which case it the config directory is the current one. or the function can be called without an argument, in which case it the config directory is the current one.
@ -418,25 +411,23 @@ def filtering(config=None):
seq_name = os.path.basename(project_dir) seq_name = os.path.basename(project_dir)
frame_range = config_dict.get('project').get('frame_range') frame_range = config_dict.get('project').get('frame_range')
frames = ["all frames" if frame_range == [] else f"frames {frame_range[0]} to {frame_range[1]}"][0] frames = ["all frames" if frame_range == [] else f"frames {frame_range[0]} to {frame_range[1]}"][0]
logging.info("\n---------------------------------------------------------------------") logging.info("\n---------------------------------------------------------------------")
logging.info(f"Filtering 3D coordinates for {seq_name}, for {frames}.") logging.info(f"Filtering 3D coordinates for {seq_name}, for {frames}.")
logging.info(f"On {currentDateAndTime.strftime('%A %d. %B %Y, %H:%M:%S')}") logging.info(f"On {currentDateAndTime.strftime('%A %d. %B %Y, %H:%M:%S')}")
logging.info(f"Project directory: {project_dir}\n") logging.info(f"Project directory: {project_dir}\n")
logging.info("---------------------------------------------------------------------\n") logging.info("---------------------------------------------------------------------\n")
filter_all(config_dict) filter_all(config_dict)
logging.info('\n') logging.info('\n')
logging.shutdown()
def markerAugmentation(config=None): def markerAugmentation(config=None):
''' '''
Augment trc 3D coordinates. Augment trc 3D coordinates.
Estimate the position of 43 additional markers. Estimate the position of 43 additional markers.
config can be a dictionary, config can be a dictionary,
or a the directory path of a trial, participant, or session, or a the directory path of a trial, participant, or session,
or the function can be called without an argument, in which case it the config directory is the current one. or the function can be called without an argument, in which case it the config directory is the current one.
@ -471,73 +462,66 @@ def markerAugmentation(config=None):
augmentTRC(config_dict) augmentTRC(config_dict)
end = time.time() end = time.time()
elapsed = end-start elapsed = end-start
logging.info(f'\nMarker augmentation took {time.strftime("%Hh%Mm%Ss", time.gmtime(elapsed))}.\n') logging.info(f'\nMarker augmentation took {time.strftime("%Hh%Mm%Ss", time.gmtime(elapsed))}.\n')
logging.shutdown()
def opensimProcessing(config=None): def opensimProcessing(config=None):
''' '''
Uses OpenSim to run scaling based on a static trc pose Performing OpenSim scaling and inverse kinematics.
and inverse kinematics based on a trc motion file. Selected the 10% slowest frames from trc for scaling
Saved as .osim and .mot
config can be a dictionary, config can be a dictionary,
or a the directory path of a trial, participant, or session, or a the directory path of a trial, participant, or session,
or the function can be called without an argument, in which case it the config directory is the current one. or the function can be called without an argument, in which case it the config directory is the current one.
''' '''
raise NotImplementedError('This has not been implemented yet. \nPlease see README.md for further explanation')
# # TODO
# from Pose2Sim.opensimProcessing import opensim_processing_all
# # Determine the level at which the function is called (root:2, trial:1)
# level, config_dicts = read_config_files(config)
# if type(config)==dict: from Pose2Sim.kinematics import opensimProcessing
# config_dict = config_dicts[0]
# if config_dict.get('project').get('project_dir') == None:
# raise ValueError('Please specify the project directory in config_dict:\n \
# config_dict.get("project").update({"project_dir":"<YOUR_TRIAL_DIRECTORY>"})')
# # Set up logging # Read the configuration files
# session_dir = os.path.realpath(os.path.join(config_dicts[0].get('project').get('project_dir'), '..')) level, config_dicts = read_config_files(config)
# setup_logging(session_dir)
# # Batch process all trials # Ensure the configuration is properly structured
# for config_dict in config_dicts: if isinstance(config, dict):
# currentDateAndTime = datetime.now() config_dict = config_dicts[0]
# start = time.time() if config_dict.get('project').get('project_dir') is None:
# project_dir = os.path.realpath(config_dict.get('project').get('project_dir')) raise ValueError('Please specify the project directory in config_dict:\n \
# seq_name = os.path.basename(project_dir) config_dict.get("project").update({"project_dir":"<YOUR_TRIAL_DIRECTORY>"})')
# frame_range = config_dict.get('project').get('frame_range')
# frames = ["all frames" if frame_range == [] else f"frames {frame_range[0]} to {frame_range[1]}"][0]
# logging.info("\n---------------------------------------------------------------------") session_dir = os.path.realpath(os.path.join(config_dicts[0].get('project').get('project_dir'), '..'))
# # if static_file in project_dir: setup_logging(session_dir)
# # logging.info(f"Scaling model with <STATIC TRC FILE>.")
# # else: # Process each configuration dictionary
# # logging.info(f"Running inverse kinematics <MOTION TRC FILE>.") for config_dict in config_dicts:
# logging.info(f"On {currentDateAndTime.strftime('%A %d. %B %Y, %H:%M:%S')}") start = time.time()
# logging.info(f"OpenSim output directory: {project_dir}") currentDateAndTime = datetime.now()
# logging.info("---------------------------------------------------------------------\n") project_dir = os.path.realpath(config_dict.get('project').get('project_dir'))
seq_name = os.path.basename(project_dir)
# opensim_processing_all(config_dict) frame_range = config_dict.get('project').get('frame_range')
frames = ["all frames" if frame_range == [] else f"frames {frame_range[0]} to {frame_range[1]}"][0]
# end = time.time()
# elapsed = end-start logging.info("\n---------------------------------------------------------------------")
# # if static_file in project_dir: logging.info(f"OpenSim processing for {seq_name}, for {frames}.")
# # logging.info(f'Model scaling took {time.strftime("%Hh%Mm%Ss", time.gmtime(elapsed))}.\n') logging.info(f"On {currentDateAndTime.strftime('%A %d. %B %Y, %H:%M:%S')}")
# # else: logging.info(f"Project directory: {project_dir}")
# # logging.info(f'Inverse kinematics took {time.strftime("%Hh%Mm%Ss", time.gmtime(elapsed))}.\n') logging.info("---------------------------------------------------------------------\n")
# logging.shutdown() try:
opensimProcessing(config_dict)
except Exception as e:
logging.error(f"Error during OpenSim processing: {e}")
continue
end = time.time()
elapsed = end - start
logging.info(f'\nOpenSim processing took {time.strftime("%Hh%Mm%Ss", time.gmtime(elapsed))}.\n')
def runAll(config=None, do_calibration=True, do_poseEstimation=True, do_synchronization=True, do_personAssociation=True, do_triangulation=True, do_filtering=True, do_markerAugmentation=True, do_opensimProcessing=True): def runAll(config=None, do_calibration=True, do_poseEstimation=True, do_synchronization=True, do_personAssociation=True, do_triangulation=True, do_filtering=True, do_markerAugmentation=True, do_opensimProcessing=True):
''' '''
Run all functions at once. Beware that Synchronization, personAssociation, and markerAugmentation are not always necessary, Run all functions at once. Beware that Synchronization, personAssociation, and markerAugmentation are not always necessary,
and may even lead to worse results. Think carefully before running all. and may even lead to worse results. Think carefully before running all.
''' '''
@ -545,7 +529,7 @@ def runAll(config=None, do_calibration=True, do_poseEstimation=True, do_synchron
# Set up logging # Set up logging
level, config_dicts = read_config_files(config) level, config_dicts = read_config_files(config)
session_dir = os.path.realpath(os.path.join(config_dicts[0].get('project').get('project_dir'), '..')) session_dir = os.path.realpath(os.path.join(config_dicts[0].get('project').get('project_dir'), '..'))
setup_logging(session_dir) setup_logging(session_dir)
currentDateAndTime = datetime.now() currentDateAndTime = datetime.now()
start = time.time() start = time.time()
@ -561,7 +545,7 @@ def runAll(config=None, do_calibration=True, do_poseEstimation=True, do_synchron
logging.info('Running calibration...') logging.info('Running calibration...')
logging.info("=====================================================================") logging.info("=====================================================================")
calibration(config) calibration(config)
else: else:
logging.info("\n\n=====================================================================") logging.info("\n\n=====================================================================")
logging.info('Skipping calibration.') logging.info('Skipping calibration.')
logging.info("=====================================================================") logging.info("=====================================================================")
@ -605,7 +589,7 @@ def runAll(config=None, do_calibration=True, do_poseEstimation=True, do_synchron
logging.info("\n\n=====================================================================") logging.info("\n\n=====================================================================")
logging.info('Skipping triangulation.') logging.info('Skipping triangulation.')
logging.info("=====================================================================") logging.info("=====================================================================")
if do_filtering: if do_filtering:
logging.info("\n\n=====================================================================") logging.info("\n\n=====================================================================")
logging.info('Running filtering...') logging.info('Running filtering...')
@ -625,18 +609,18 @@ def runAll(config=None, do_calibration=True, do_poseEstimation=True, do_synchron
logging.info("\n\n=====================================================================") logging.info("\n\n=====================================================================")
logging.info('Skipping marker augmentation.') logging.info('Skipping marker augmentation.')
logging.info("\n\n=====================================================================") logging.info("\n\n=====================================================================")
# if do_opensimProcessing:
# logging.info("\n\n=====================================================================")
# logging.info('Running opensim processing.')
# logging.info("=====================================================================")
# opensimProcessing(config)
# else:
# logging.info("\n\n=====================================================================")
# logging.info('Skipping opensim processing.')
# logging.info("=====================================================================")
if do_opensimProcessing:
logging.info("\n\n=====================================================================")
logging.info("Running OpenSim processing.")
logging.info("=====================================================================")
opensimProcessing(config)
else:
logging.info("\n\n=====================================================================")
logging.info('Skipping OpenSim processing.')
logging.info("\n\n=====================================================================")
logging.info("Pose2Sim pipeline completed.")
end = time.time() end = time.time()
elapsed = end-start elapsed = end-start
logging.info(f'\nRUNNING ALL FUNCTIONS TOOK {time.strftime("%Hh%Mm%Ss", time.gmtime(elapsed))}.\n') logging.info(f'\nRUNNING ALL FUNCTIONS TOOK {time.strftime("%Hh%Mm%Ss", time.gmtime(elapsed))}.\n')
logging.shutdown()

View File

@ -15,12 +15,14 @@
- triangulation - triangulation
- filtering - filtering
- marker augmentation - marker augmentation
- opensim processing
- Multi-person: - Multi-person:
- synchronization - synchronization
- person association - person association
- triangulation - triangulation
- filtering - filtering
- marker augmentation - marker augmentation
- opensim processing
- SINGLE TRIAL: - SINGLE TRIAL:
- calibration - calibration
@ -29,6 +31,7 @@
- triangulation - triangulation
- filtering - filtering
- marker augmentation - marker augmentation
- opensim processing
N.B.: N.B.:
1. Calibration from scene dimensions is not tested, as it requires the 1. Calibration from scene dimensions is not tested, as it requires the
@ -74,6 +77,7 @@ class TestWorkflow(unittest.TestCase):
- triangulation - triangulation
- filtering - filtering
- marker augmentation - marker augmentation
- OpenSim processing
- run all - run all
N.B.: Calibration from scene dimensions is not tested, as it requires the N.B.: Calibration from scene dimensions is not tested, as it requires the
@ -109,7 +113,7 @@ class TestWorkflow(unittest.TestCase):
Pose2Sim.triangulation(config_dict) Pose2Sim.triangulation(config_dict)
Pose2Sim.filtering(config_dict) Pose2Sim.filtering(config_dict)
Pose2Sim.markerAugmentation(config_dict) Pose2Sim.markerAugmentation(config_dict)
# Pose2Sim.kinematics(config_dict) Pose2Sim.opensimProcessing(config_dict)
config_dict.get("pose").update({"overwrite_pose":False}) config_dict.get("pose").update({"overwrite_pose":False})
Pose2Sim.runAll(config_dict) Pose2Sim.runAll(config_dict)
@ -137,7 +141,7 @@ class TestWorkflow(unittest.TestCase):
Pose2Sim.triangulation(config_dict) Pose2Sim.triangulation(config_dict)
Pose2Sim.filtering(config_dict) Pose2Sim.filtering(config_dict)
Pose2Sim.markerAugmentation(config_dict) Pose2Sim.markerAugmentation(config_dict)
# Pose2Sim.kinematics(config_dict) Pose2Sim.opensimProcessing(config_dict)
# Run all # Run all
config_dict.get("pose").update({"overwrite_pose":False}) config_dict.get("pose").update({"overwrite_pose":False})

511
Pose2Sim/kinematics.py Normal file
View File

@ -0,0 +1,511 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
###########################################################################
## KINEMATICS PROCESSING ##
###########################################################################
Process kinematic data using OpenSim tools.
This script performs scaling, inverse kinematics, and related processing
on 3D motion capture data (TRC files). The scaling process adjusts the
generic model to match the subject's physical dimensions, while inverse
kinematics computes the joint angles based on the motion data.
Set your parameters in Config.toml.
INPUTS:
- a directory containing TRC files
- kinematic processing parameters in Config.toml
OUTPUT:
- scaled OpenSim model files (.osim)
- joint angle data files (.mot)
'''
import os
import sys
from collections import defaultdict
from pathlib import Path
import numpy as np
import pandas as pd
from lxml import etree
import logging
import opensim
## FUNCTIONS
def find_config_and_pose3d(project_dir):
"""
Find configuration files and associated pose-3d directories in the project directory.
Args:
project_dir (str): The root directory of the project.
Returns:
list: A list of tuples containing the config path and the corresponding pose-3d directory.
"""
config_paths = []
for root, dirs, files in os.walk(project_dir):
if 'Config.toml' in files:
config_path = Path(root) / 'Config.toml'
possible_pose3d_dir = Path(root) / 'pose-3d'
if not possible_pose3d_dir.exists():
possible_pose3d_dir = Path(root).parent / 'pose-3d'
if possible_pose3d_dir.exists():
config_paths.append((config_path, possible_pose3d_dir))
else:
logging.warning(f"No pose-3d directory found for config: {config_path}")
return config_paths
def get_grouped_files(directory, pattern='*.trc'):
"""
Group TRC files by person ID or treat them as single-person if no ID is found.
Args:
directory (str): The directory containing TRC files.
pattern (str): The file pattern to search for.
Returns:
dict: A dictionary grouping TRC files by person ID.
"""
files = list(Path(directory).glob(pattern))
grouped_files = defaultdict(list)
for file in files:
parts = file.stem.split('_')
if len(parts) > 2 and 'P' in parts[2]: # Multi-person file naming convention
person_id = parts[2]
else:
person_id = "SinglePerson"
grouped_files[person_id].append(file)
return grouped_files
def process_all_groups(config_dict):
"""
Process all groups (single or multi-person) based on the configuration.
Args:
config_dict (dict): The configuration dictionary containing project details.
"""
logging.info("Processing all groups in the project.")
project_dir = config_dict.get('project', {}).get('project_dir')
config_and_pose3d_paths = find_config_and_pose3d(project_dir)
for config_path, pose3d_dir in config_and_pose3d_paths:
logging.info(f"Processing setup with config: {config_path}")
trc_groups = get_grouped_files(pose3d_dir)
trial_name = Path(pose3d_dir).parent.name # Use the parent directory name as the trial name
for person_id, trc_files in trc_groups.items():
filtered_trc_files = load_trc(config_dict, trc_files)
# Ensure output directory includes the trial name
trial_output_dir = get_output_dir(Path(config_dict['project']['project_dir']).parent / trial_name, person_id)
perform_scaling(config_dict, person_id, filtered_trc_files, trial_output_dir)
perform_inverse_kinematics(config_dict, person_id, filtered_trc_files, trial_output_dir)
def load_trc(config_dict, trc_files):
"""
Load and filter TRC files according to the configuration.
Args:
config_dict (dict): The configuration dictionary.
trc_files (list): A list of TRC file paths.
Returns:
list: A list of filtered TRC files based on the criteria specified in the configuration.
"""
opensim_config = config_dict.get('opensim', {})
use_lstm = opensim_config.get('use_augmentation', False)
load_trc_name = opensim_config.get('load_trc_name', 'default')
# Filter out any scaled TRC files
unscaled_trc_files = [file for file in trc_files if '_scaling' not in str(file)]
logging.info(f"Starting TRC file filtering with criteria: use_lstm = {use_lstm}, load_trc_name = {load_trc_name}")
logging.info(f"Initial list of TRC files: {unscaled_trc_files}")
# Initialize the list to store filtered TRC files
trc_files = []
# Check for LSTM files if LSTM is being used
if use_lstm:
lstm_files = [file for file in unscaled_trc_files if '_LSTM.trc' in str(file)]
if not lstm_files:
raise FileNotFoundError("No LSTM TRC file found in the provided list.")
trc_files.extend(lstm_files)
# Check for default or filtered TRC files
if load_trc_name == 'default':
default_files = [file for file in unscaled_trc_files if '_LSTM' not in str(file) and '_filt_butterworth' not in str(file)]
trc_files.extend(default_files)
elif load_trc_name == 'filtered':
filtered_files = [file for file in unscaled_trc_files if '_filt_butterworth' in str(file) and '_LSTM' not in str(file)]
trc_files.extend(filtered_files)
# If no TRC files are found after filtering, raise an error
if not trc_files:
logging.error(f"No suitable TRC files found with the specified criteria: use_lstm = {use_lstm}, load_trc_name = {load_trc_name}")
raise FileNotFoundError(f"No suitable TRC files found in the provided list with the specified criteria: use_lstm = {use_lstm}, load_trc_name = {load_trc_name}")
logging.info(f"Filtered TRC files: {trc_files}")
return trc_files
def read_trc(trc_path):
"""
Read a TRC file and extract its contents.
Args:
trc_path (str): The path to the TRC file.
Returns:
tuple: A tuple containing the Q coordinates, frames column, time column, and header.
"""
try:
logging.info(f"Attempting to read TRC file: {trc_path}")
with open(trc_path, 'r') as trc_file:
header = [next(trc_file) for _ in range(5)]
trc_df = pd.read_csv(trc_path, sep="\t", skiprows=4, encoding='utf-8')
frames_col, time_col = trc_df.iloc[:, 0], trc_df.iloc[:, 1]
Q_coords = trc_df.drop(trc_df.columns[[0, 1]], axis=1)
return Q_coords, frames_col, time_col, header
except Exception as e:
logging.error(f"Error reading TRC file at {trc_path}: {e}")
raise
def make_trc_with_Q(Q, header, trc_path):
"""
Write the processed Q coordinates back to a TRC file.
Args:
Q (pd.DataFrame): The Q coordinates data.
header (list): The header of the original TRC file.
trc_path (str): Path to save the new TRC file.
"""
header_2_split = header[2].split('\t')
header_2_split[2] = str(len(Q))
header_2_split[-1] = str(len(Q))
header[2] = '\t'.join(header_2_split) + '\n'
time = pd.Series(np.arange(len(Q)) / float(header_2_split[0]), name='t')
Q.insert(0, 't', time)
with open(trc_path, 'w') as trc_o:
[trc_o.write(line) for line in header]
Q.to_csv(trc_o, sep='\t', index=True, header=None, lineterminator='\n')
def get_key(config_dict):
"""
Determine the key for the OpenSim model and setup files based on the configuration.
Args:
config_dict (dict): The configuration dictionary.
Returns:
str: The key used to select the model and setup files.
"""
use_augmentation = config_dict.get('opensim', {}).get('use_augmentation', False)
if use_augmentation:
return 'LSTM'
pose_model = config_dict.get('pose', {}).get('pose_model', '').upper()
if not pose_model:
raise ValueError(f"Invalid or missing 'pose_model' in config: {pose_model}")
return pose_model
def get_OpenSim_Setup():
"""
Locate the OpenSim setup directory within the Pose2Sim package.
Returns:
Path: The path to the OpenSim setup directory.
"""
pose2sim_path = Path(sys.modules['Pose2Sim'].__file__).resolve().parent
setup_dir = pose2sim_path / 'OpenSim_Setup'
return setup_dir
def get_Model(config_dict):
"""
Retrieve the OpenSim model file path based on the configuration.
Args:
config_dict (dict): The configuration dictionary.
Returns:
str: The path to the OpenSim model file.
"""
setup_key = get_key(config_dict)
setup_dir = get_OpenSim_Setup()
if setup_key == 'LSTM':
pose_model_file = 'Model_Pose2Sim_LSTM.osim'
elif setup_key == 'BLAZEPOSE':
pose_model_file = 'Model_Pose2Sim_Blazepose.osim'
elif setup_key == 'BODY_25':
pose_model_file = 'Model_Pose2Sim_Body25.osim'
elif setup_key == 'BODY_25B':
pose_model_file = 'Model_Setup_Pose2Sim_Body25b.osim'
elif setup_key == 'BODY_135':
pose_model_file = 'Model_Pose2Sim_Body135.osim'
elif setup_key == 'COCO_17':
pose_model_file = 'Model_Pose2Sim_Coco17.osim'
elif setup_key == 'COCO_133':
pose_model_file = 'Model_Pose2Sim_Coco133.osim'
elif setup_key == 'HALPE_26':
pose_model_file = 'Model_Pose2Sim_Halpe26.osim'
elif setup_key == 'HALPE_68':
pose_model_file = 'Model_Pose2Sim_Halpe68_136.osim'
else:
raise ValueError(f"pose_model '{setup_key}' not found.")
pose_model_path = os.path.join(setup_dir, pose_model_file)
return pose_model_path
def get_Scale_Setup(config_dict):
"""
Retrieve the OpenSim scaling setup file path based on the configuration.
Args:
config_dict (dict): The configuration dictionary.
Returns:
str: The path to the OpenSim scaling setup file.
"""
setup_key = get_key(config_dict)
setup_dir = get_OpenSim_Setup()
if setup_key == 'LSTM':
scale_setup_file = 'Scaling_Setup_Pose2Sim_LSTM.xml'
elif setup_key == 'BLAZEPOSE':
scale_setup_file = 'Scaling_Setup_Pose2Sim_Blazepose.xml'
elif setup_key == 'BODY_25':
scale_setup_file = 'Scaling_Setup_Pose2Sim_Body25.xml'
elif setup_key == 'BODY_25B':
scale_setup_file = 'Scaling_Setup_Pose2Sim_Body25b.xml'
elif setup_key == 'BODY_135':
scale_setup_file = 'Scaling_Setup_Pose2Sim_Body135.xml'
elif setup_key == 'COCO_17':
scale_setup_file = 'Scaling_Setup_Pose2Sim_Coco17.xml'
elif setup_key == 'COCO_133':
scale_setup_file = 'Scaling_Setup_Pose2Sim_Coco133.xml'
elif setup_key == 'HALPE_26':
scale_setup_file = 'Scaling_Setup_Pose2Sim_Halpe26.xml'
elif setup_key == 'HALPE_68':
scale_setup_file = 'Scaling_Setup_Pose2Sim_Halpe68_136.xml'
else:
raise ValueError(f"pose_model '{setup_key}' not found.")
scale_setup_path = os.path.join(setup_dir, scale_setup_file)
return scale_setup_path
def get_IK_Setup(config_dict):
"""
Retrieve the OpenSim inverse kinematics setup file path based on the configuration.
Args:
config_dict (dict): The configuration dictionary.
Returns:
str: The path to the OpenSim inverse kinematics setup file.
"""
setup_key = get_key(config_dict)
setup_dir = get_OpenSim_Setup()
if setup_key == 'LSTM':
ik_setup_file = 'IK_Setup_Pose2Sim_LSTM.xml'
elif setup_key == 'BLAZEPOSE':
ik_setup_file = 'IK_Setup_Pose2Sim_Blazepose.xml'
elif setup_key == 'BODY_25':
ik_setup_file = 'IK_Setup_Pose2Sim_Body25.xml'
elif setup_key == 'BODY_25B':
ik_setup_file = 'IK_Setup_Pose2Sim_Body25b.xml'
elif setup_key == 'BODY_135':
ik_setup_file = 'IK_Setup_Pose2Sim_Body135.xml'
elif setup_key == 'COCO_17':
ik_setup_file = 'IK_Setup_Pose2Sim_Coco17.xml'
elif setup_key == 'COCO_133':
ik_setup_file = 'IK_Setup_Pose2Sim_Coco133.xml'
elif setup_key == 'HALPE_26':
ik_setup_file = 'IK_Setup_Pose2Sim_Halpe26.xml'
elif setup_key == 'HALPE_68':
ik_setup_file = 'IK_Setup_Pose2Sim_Halpe68_136.xml'
else:
raise ValueError(f"pose_model '{setup_key}' not found.")
ik_setup_path = os.path.join(setup_dir, ik_setup_file)
return ik_setup_path
def get_output_dir(config_dir, person_id):
"""
Determines the correct output directory based on the configuration and the person identifier.
Args:
config_dir (Path): The root directory where the configuration file is located.
person_id (str): Identifier for the person (e.g., 'SinglePerson', 'P1').
Returns:
Path: The path where the output files should be stored.
"""
output_dir = config_dir / 'opensim' # Assuming 'opensim' as the default output subdirectory
# Append the person_id to the output directory if it's a multi-person setup
if person_id != "SinglePerson":
output_dir = output_dir / person_id
logging.debug(f"Output directory determined as: {output_dir}")
# Create the directory if it does not exist
if not output_dir.exists():
output_dir.mkdir(parents=True, exist_ok=True)
return output_dir
def perform_scaling(config_dict, person_id, trc_files, output_dir):
"""
Perform scaling on the TRC files according to the OpenSim configuration.
Args:
config_dict (dict): The configuration dictionary.
person_id (str): The person identifier (e.g., 'SinglePerson', 'P1').
trc_files (list): List of TRC files to be processed.
output_dir (Path): The directory where the output files should be saved.
"""
geometry_path = Path(get_OpenSim_Setup()) / 'Geometry'
geometry_path_str = str(geometry_path)
opensim.ModelVisualizer.addDirToGeometrySearchPaths(geometry_path_str)
try:
athlete_config = config_dict.get('project', {})
athlete_height = athlete_config.get('participant_height', -1)
athlete_weight = athlete_config.get('participant_mass', -1)
if person_id == "SinglePerson":
if not isinstance(athlete_height, float) or not isinstance(athlete_weight, float):
raise ValueError("For a single person configuration, 'participant_height' and 'participant_mass' must be floats.")
else:
if person_id.startswith("P"):
try:
person_idx = int(person_id.replace('P', '')) - 1
athlete_height = athlete_height[person_idx]
athlete_weight = athlete_weight[person_idx]
except (ValueError, IndexError) as e:
raise ValueError(f"Error processing multi-person data for '{person_id}': {e}")
else:
raise ValueError(f"Unexpected person_id format: '{person_id}'")
logging.debug(f"Performing scaling. Output directory: {output_dir}")
pose_model = get_Model(config_dict)
if not pose_model:
raise ValueError(f"Model path not found for pose_model: {pose_model}")
for trc_file in trc_files:
trc_file = Path(trc_file)
scaling_path = get_Scale_Setup(config_dict)
Q_coords, _, _, header = read_trc(trc_file)
Q_diff = Q_coords.diff(axis=0).sum(axis=1)
Q_diff = Q_diff[Q_diff != 0]
min_speed_indices = Q_diff.abs().nsmallest(int(len(Q_diff) * 0.1)).index
Q_coords_scaling = Q_coords.iloc[min_speed_indices].reset_index(drop=True)
trc_scaling_path = trc_file.parent / (trc_file.stem + '_scaling.trc')
make_trc_with_Q(Q_coords_scaling, header, str(trc_scaling_path))
scaling_file_path = str(trc_file.parent / (trc_file.stem + '_' + Path(scaling_path).name))
scaled_model_path = (output_dir / (trc_file.stem + '_scaled.osim')).resolve()
scaling_tree = etree.parse(str(scaling_path))
scaling_root = scaling_tree.getroot()
scaling_root[0].find('mass').text = str(athlete_weight)
scaling_root[0].find('height').text = str(athlete_height)
scaling_root[0].find('GenericModelMaker').find('model_file').text = str(pose_model)
scaling_root[0].find('ModelScaler').find('marker_file').text = trc_scaling_path.name
scaling_root[0].find('ModelScaler').find('time_range').text = '0 ' + str(Q_coords_scaling['t'].iloc[-1])
scaling_root[0].find('ModelScaler').find('output_model_file').text = str(scaled_model_path)
scaling_root[0].find('MarkerPlacer').find('marker_file').text = trc_scaling_path.name
scaling_root[0].find('MarkerPlacer').find('time_range').text = '0 ' + str(Q_coords_scaling['t'].iloc[-1])
scaling_root[0].find('MarkerPlacer').find('output_model_file').text = str(scaled_model_path)
scaling_tree.write(scaling_file_path)
logging.debug(f"Running ScaleTool with scaling file: {scaling_file_path}")
opensim.ScaleTool(scaling_file_path).run()
except Exception as e:
logging.error(f"Error during scaling for {person_id}: {e}")
raise
def perform_inverse_kinematics(config_dict, person_id, trc_files, output_dir):
"""
Perform inverse kinematics on the TRC files according to the OpenSim configuration.
Args:
config_dict (dict): The configuration dictionary.
person_id (str): The person identifier (e.g., 'SinglePerson', 'P1').
trc_files (list): List of TRC files to be processed.
output_dir (Path): The directory where the output files should be saved.
"""
try:
logging.debug(f"Performing inverse kinematics. Output directory: {output_dir}")
for trc_file in trc_files:
trc_file_path = Path(trc_file).resolve()
scaled_model_path = Path(output_dir) / (trc_file_path.stem + '_scaled.osim')
ik_setup_path = get_IK_Setup(config_dict)
Q_coords, frames_col, time_col, header = read_trc(trc_file_path)
ik_time_range = config_dict.get('opensim', {}).get('IK_timeRange', [])
if not ik_time_range:
start_time = time_col.iloc[0]
end_time = time_col.iloc[-1]
else:
start_time, end_time = ik_time_range[0], ik_time_range[1]
ik_file_path = Path(trc_file_path.parent / (trc_file_path.stem + '_' + Path(ik_setup_path).name)).resolve()
scaled_model_path = scaled_model_path.resolve()
output_motion_file = Path(output_dir, trc_file_path.stem + '.mot').resolve()
ik_tree = etree.parse(ik_setup_path)
ik_root = ik_tree.getroot()
ik_root.find('.//model_file').text = str(scaled_model_path)
ik_root.find('.//time_range').text = f'{start_time} {end_time}'
ik_root.find('.//output_motion_file').text = str(output_motion_file)
ik_root.find('.//marker_file').text = str(trc_file_path)
ik_tree.write(ik_file_path)
logging.info(f"Running InverseKinematicsTool with TRC file: {trc_file_path}")
if not trc_file_path.exists():
raise FileNotFoundError(f"TRC file does not exist: {trc_file_path}")
logging.debug(f"Running InverseKinematicsTool with IK setup file: {ik_file_path}")
opensim.InverseKinematicsTool(str(ik_file_path)).run()
except Exception as e:
logging.error(f"Error during IK for {person_id}: {e}")
raise
def opensimProcessing(config_dict):
logging.info("Starting OpenSim processing...")
process_all_groups(config_dict)
logging.info("OpenSim processing completed successfully.")

View File

@ -175,9 +175,13 @@ Pose2Sim.personAssociation()
Pose2Sim.triangulation() Pose2Sim.triangulation()
Pose2Sim.filtering() Pose2Sim.filtering()
Pose2Sim.markerAugmentation() Pose2Sim.markerAugmentation()
Pose2Sim.opensimProcessing()
``` ```
3D results are stored as .trc files in each trial folder in the `pose-3d` directory. 3D results are stored as .trc files in each trial folder in the `pose-3d` directory.
OpenSim results are stored as scaled model .osim and .mot in each trial folder in the `opensim` directory.
</br> </br>
**Note:** **Note:**
@ -188,6 +192,7 @@ Pose2Sim.markerAugmentation()
Pose2Sim.runAll(do_calibration=True, do_poseEstimation=True, do_synchronization=True, do_personAssociation=True, do_triangulation=True, do_filtering=True, do_markerAugmentation=True, do_opensimProcessing=True) Pose2Sim.runAll(do_calibration=True, do_poseEstimation=True, do_synchronization=True, do_personAssociation=True, do_triangulation=True, do_filtering=True, do_markerAugmentation=True, do_opensimProcessing=True)
``` ```
- Try the calibration tool by changing `calibration_type` to `calculate` instead of `convert` in [Config.toml](https://github.com/perfanalytics/pose2sim/blob/main/Pose2Sim/Demo_SinglePerson/Config.toml) (more info [there](#calculate-from-scratch)). - Try the calibration tool by changing `calibration_type` to `calculate` instead of `convert` in [Config.toml](https://github.com/perfanalytics/pose2sim/blob/main/Pose2Sim/Demo_SinglePerson/Config.toml) (more info [there](#calculate-from-scratch)).
- If the results are not convincing, refer to Section [OpenSim-kinematics](#OpenSim-kinematics) in the document.
</br> </br>
@ -652,6 +657,15 @@ Pose2Sim.markerAugmentation()
</br> </br>
### If kinematics results are not convicing:
> _***Explanation on choosing the best frames for scaling (L437-448):***_
>
> On difficult trials, some points are not well triangulated, which can lead to bad scaling. For example, if a point of the foot is very far from the rest of the body on some frames, scaling will consider that the foot is very large. Consequently, we need to scale only on the frames that are best triangulated. Now, how to find these best frames?
>
>
> My reasoning was that the points of badly triangulated frames would go all over the place, and thus that their speeds would be fast. So I only selected the 10% slowest frames for scaling. I think that in addition, we should take the median scale factor for these frames, because we might have slow frames that are still bad. -> This last step has not been done.
### Command line ### Command line
Alternatively, you can use command-line tools: Alternatively, you can use command-line tools: