diff --git a/Content/Calib2D.png b/Content/Calib2D.png deleted file mode 100644 index 4498986..0000000 Binary files a/Content/Calib2D.png and /dev/null differ diff --git a/Content/Filter3D.png b/Content/Filter3D.png deleted file mode 100644 index 72973df..0000000 Binary files a/Content/Filter3D.png and /dev/null differ diff --git a/Content/P2S_calibration.png b/Content/P2S_calibration.png new file mode 100644 index 0000000..c370cfd Binary files /dev/null and b/Content/P2S_calibration.png differ diff --git a/Content/P2S_filtering.png b/Content/P2S_filtering.png new file mode 100644 index 0000000..1b6b315 Binary files /dev/null and b/Content/P2S_filtering.png differ diff --git a/Content/P2S_markeraugmentation.png b/Content/P2S_markeraugmentation.png new file mode 100644 index 0000000..a284550 Binary files /dev/null and b/Content/P2S_markeraugmentation.png differ diff --git a/Content/P2S_personassociation.png b/Content/P2S_personassociation.png new file mode 100644 index 0000000..ac6a6c4 Binary files /dev/null and b/Content/P2S_personassociation.png differ diff --git a/Content/P2S_poseestimation.png b/Content/P2S_poseestimation.png new file mode 100644 index 0000000..02e024d Binary files /dev/null and b/Content/P2S_poseestimation.png differ diff --git a/Content/P2S_synchronization.png b/Content/P2S_synchronization.png new file mode 100644 index 0000000..9d8aecf Binary files /dev/null and b/Content/P2S_synchronization.png differ diff --git a/Content/P2S_triangulation.png b/Content/P2S_triangulation.png new file mode 100644 index 0000000..f1ae08c Binary files /dev/null and b/Content/P2S_triangulation.png differ diff --git a/Content/Pipeline.png b/Content/Pipeline.png deleted file mode 100644 index 4f2c92a..0000000 Binary files a/Content/Pipeline.png and /dev/null differ diff --git a/Content/Track2D.png b/Content/Track2D.png deleted file mode 100644 index e949963..0000000 Binary files a/Content/Track2D.png and /dev/null differ diff --git a/Content/Triangulate3D.png b/Content/Triangulate3D.png deleted file mode 100644 index 57a905f..0000000 Binary files a/Content/Triangulate3D.png and /dev/null differ diff --git a/Content/fSpy_calibration.png b/Content/fSpy_calibration.png deleted file mode 100644 index 32a43d6..0000000 Binary files a/Content/fSpy_calibration.png and /dev/null differ diff --git a/Content/synchro.jpg b/Content/synchro.jpg new file mode 100644 index 0000000..c5cedd4 Binary files /dev/null and b/Content/synchro.jpg differ diff --git a/Pose2Sim/Demo_Batch/Config.toml b/Pose2Sim/Demo_Batch/Config.toml index b026383..ea4559d 100644 --- a/Pose2Sim/Demo_Batch/Config.toml +++ b/Pose2Sim/Demo_Batch/Config.toml @@ -52,7 +52,7 @@ output_format = 'openpose' # 'openpose', 'mmpose', 'deeplabcut', 'none' or a lis [synchronization] -display_sync_plots = true # true or false (lowercase) +display_sync_plots = false # 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 # ['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) diff --git a/Pose2Sim/Pose2Sim.py b/Pose2Sim/Pose2Sim.py index ff456f5..d3bc5c8 100644 --- a/Pose2Sim/Pose2Sim.py +++ b/Pose2Sim/Pose2Sim.py @@ -104,7 +104,7 @@ def determine_level(config_dir): len_paths = [len(root.split(os.sep)) for root,dirs,files in os.walk(config_dir) if 'Config.toml' in files] if len_paths == []: - raise FileNotFoundError('Please run Pose2Sim from a Session, Participant, or Trial directory.') + raise FileNotFoundError('You need a Config.toml file in each trial or root folder.') level = max(len_paths) - min(len_paths) + 1 return level @@ -172,7 +172,7 @@ def calibration(config=None): config_dict = config_dicts[0] try: session_dir = os.path.realpath([os.getcwd() if level==2 else os.path.join(os.getcwd(), '..')][0]) - [os.path.join(session_dir, c) for c in os.listdir(session_dir) if 'calib' in c.lower() ][0] + [os.path.join(session_dir, c) for c in os.listdir(session_dir) if 'calib' in c.lower() and not c.lower().endswith('.py')][0] except: session_dir = os.path.realpath(os.getcwd()) config_dict.get("project").update({"project_dir":session_dir}) @@ -183,17 +183,17 @@ def calibration(config=None): # 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] - logging.info("\n\n---------------------------------------------------------------------") + logging.info("\n---------------------------------------------------------------------") logging.info("Camera calibration") logging.info(f"On {currentDateAndTime.strftime('%A %d. %B %Y, %H:%M:%S')}") - logging.info("---------------------------------------------------------------------") - logging.info(f"\nCalibration directory: {calib_dir}") + logging.info(f"Calibration directory: {calib_dir}") + logging.info("---------------------------------------------------------------------\n") start = time.time() calibrate_cams_all(config_dict) end = time.time() - logging.info(f'\nCalibration took {end-start:.2f} s.') + logging.info(f'\nCalibration took {end-start:.2f} s.\n') def poseEstimation(config=None): @@ -228,17 +228,17 @@ def poseEstimation(config=None): frame_range = config_dict.get('project').get('frame_range') frames = ["all frames" if not frame_range else f"frames {frame_range[0]} to {frame_range[1]}"][0] - logging.info("\n\n---------------------------------------------------------------------") + logging.info("\n---------------------------------------------------------------------") logging.info(f"Pose estimation for {seq_name}, for {frames}.") logging.info(f"On {currentDateAndTime.strftime('%A %d. %B %Y, %H:%M:%S')}") - logging.info("---------------------------------------------------------------------") - logging.info(f"\nProject directory: {project_dir}") + logging.info(f"Project directory: {project_dir}") + logging.info("---------------------------------------------------------------------\n") rtm_estimator(config_dict) end = time.time() elapsed = end - start - logging.info(f'\nPose estimation took {time.strftime("%Hh%Mm%Ss", time.gmtime(elapsed))}.') + logging.info(f'\nPose estimation took {time.strftime("%Hh%Mm%Ss", time.gmtime(elapsed))}.\n') def synchronization(config=None): @@ -253,7 +253,7 @@ def synchronization(config=None): # Import the function from Pose2Sim.synchronization import synchronize_cams_all - # Determine the level at which the function is called (session:3, participant:2, trial:1) + # Determine the level at which the function is called (root:2, trial:1) level, config_dicts = read_config_files(config) if type(config)==dict: @@ -272,17 +272,17 @@ def synchronization(config=None): currentDateAndTime = datetime.now() project_dir = os.path.realpath(config_dict.get('project').get('project_dir')) - logging.info("\n\n---------------------------------------------------------------------") + logging.info("\n---------------------------------------------------------------------") logging.info("Camera synchronization") logging.info(f"On {currentDateAndTime.strftime('%A %d. %B %Y, %H:%M:%S')}") - logging.info("---------------------------------------------------------------------") - logging.info(f"\nProject directory: {project_dir}") + logging.info(f"Project directory: {project_dir}") + 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))}.') + logging.info(f'\nSynchronization took {time.strftime("%Hh%Mm%Ss", time.gmtime(elapsed))}.\n') def personAssociation(config=None): @@ -297,7 +297,7 @@ def personAssociation(config=None): from Pose2Sim.personAssociation import track_2d_all - # Determine the level at which the function is called (session:3, participant:2, trial:1) + # Determine the level at which the function is called (root:2, trial:1) level, config_dicts = read_config_files(config) if type(config)==dict: @@ -319,17 +319,17 @@ def personAssociation(config=None): 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\n---------------------------------------------------------------------") + logging.info("\n---------------------------------------------------------------------") logging.info(f"Associating persons for {seq_name}, for {frames}.") logging.info(f"On {currentDateAndTime.strftime('%A %d. %B %Y, %H:%M:%S')}") - logging.info("---------------------------------------------------------------------") - logging.info(f"\nProject directory: {project_dir}") + logging.info(f"Project directory: {project_dir}") + logging.info("---------------------------------------------------------------------\n") track_2d_all(config_dict) end = time.time() elapsed = end-start - logging.info(f'\nAssociating persons took {time.strftime("%Hh%Mm%Ss", time.gmtime(elapsed))}.') + logging.info(f'\nAssociating persons took {time.strftime("%Hh%Mm%Ss", time.gmtime(elapsed))}.\n') def triangulation(config=None): @@ -343,7 +343,7 @@ def triangulation(config=None): from Pose2Sim.triangulation import triangulate_all - # Determine the level at which the function is called (session:3, participant:2, trial:1) + # Determine the level at which the function is called (root:2, trial:1) level, config_dicts = read_config_files(config) if type(config)==dict: @@ -365,17 +365,17 @@ def triangulation(config=None): 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\n---------------------------------------------------------------------") + logging.info("\n---------------------------------------------------------------------") logging.info(f"Triangulation of 2D points for {seq_name}, for {frames}.") logging.info(f"On {currentDateAndTime.strftime('%A %d. %B %Y, %H:%M:%S')}") - logging.info("---------------------------------------------------------------------") - logging.info(f"\nProject directory: {project_dir}") + logging.info(f"Project directory: {project_dir}") + logging.info("---------------------------------------------------------------------\n") triangulate_all(config_dict) end = time.time() elapsed = end-start - logging.info(f'\nTriangulation took {time.strftime("%Hh%Mm%Ss", time.gmtime(elapsed))}.') + logging.info(f'\nTriangulation took {time.strftime("%Hh%Mm%Ss", time.gmtime(elapsed))}.\n') def filtering(config=None): @@ -389,7 +389,7 @@ def filtering(config=None): from Pose2Sim.filtering import filter_all - # Determine the level at which the function is called (session:3, participant:2, trial:1) + # Determine the level at which the function is called (root:2, trial:1) level, config_dicts = read_config_files(config) if type(config)==dict: @@ -410,13 +410,15 @@ def filtering(config=None): 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\n---------------------------------------------------------------------") + logging.info("\n---------------------------------------------------------------------") 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("---------------------------------------------------------------------") - logging.info(f"\nProject directory: {project_dir}\n") + logging.info(f"Project directory: {project_dir}\n") + logging.info("---------------------------------------------------------------------\n") filter_all(config_dict) + + logging.info('\n') def markerAugmentation(config=None): @@ -449,17 +451,17 @@ def markerAugmentation(config=None): 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\n---------------------------------------------------------------------") + logging.info("\n---------------------------------------------------------------------") logging.info(f"Augmentation process for {seq_name}, for {frames}.") logging.info(f"On {currentDateAndTime.strftime('%A %d. %B %Y, %H:%M:%S')}") - logging.info("---------------------------------------------------------------------") - logging.info(f"\nProject directory: {project_dir}\n") + logging.info(f"Project directory: {project_dir}") + logging.info("---------------------------------------------------------------------\n") augmentTRC(config_dict) end = time.time() elapsed = end-start - logging.info(f'\nMarker augmentation took {time.strftime("%Hh%Mm%Ss", time.gmtime(elapsed))}.') + logging.info(f'\nMarker augmentation took {time.strftime("%Hh%Mm%Ss", time.gmtime(elapsed))}.\n') def opensimProcessing(config=None): @@ -477,7 +479,7 @@ def opensimProcessing(config=None): # # TODO # from Pose2Sim.opensimProcessing import opensim_processing_all - # # Determine the level at which the function is called (session:3, participant:2, trial:1) + # # Determine the level at which the function is called (root:2, trial:1) # level, config_dicts = read_config_files(config) # if type(config)==dict: @@ -499,23 +501,23 @@ def opensimProcessing(config=None): # 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\n---------------------------------------------------------------------") + # logging.info("\n---------------------------------------------------------------------") # # if static_file in project_dir: # # logging.info(f"Scaling model with .") # # else: # # logging.info(f"Running inverse kinematics .") # logging.info(f"On {currentDateAndTime.strftime('%A %d. %B %Y, %H:%M:%S')}") - # logging.info("---------------------------------------------------------------------") - # logging.info(f"\nOpenSim output directory: {project_dir}") + # logging.info(f"OpenSim output directory: {project_dir}") + # logging.info("---------------------------------------------------------------------\n") # opensim_processing_all(config_dict) # end = time.time() # elapsed = end-start # # if static_file in project_dir: - # # logging.info(f'Model scaling took {time.strftime("%Hh%Mm%Ss", time.gmtime(elapsed))}.') + # # logging.info(f'Model scaling took {time.strftime("%Hh%Mm%Ss", time.gmtime(elapsed))}.\n') # # else: - # # logging.info(f'Inverse kinematics took {time.strftime("%Hh%Mm%Ss", time.gmtime(elapsed))}.') + # # logging.info(f'Inverse kinematics 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): @@ -524,82 +526,101 @@ def runAll(config=None, do_calibration=True, do_poseEstimation=True, do_synchron and may even lead to worse results. Think carefully before running all. ''' - # Determine the level at which the function is called (session:3, participant:2, trial:1) - level, config_dicts = read_config_files(config) - - if type(config)==dict: - 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":""})') # Set up logging + level, config_dicts = read_config_files(config) session_dir = os.path.realpath(os.path.join(config_dicts[0].get('project').get('project_dir'), '..')) setup_logging(session_dir) - # Batch process all trials - for config_dict in config_dicts: - start = time.time() - currentDateAndTime = datetime.now() - project_dir = os.path.realpath(config_dict.get('project').get('project_dir')) - seq_name = os.path.basename(project_dir) - 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] - + currentDateAndTime = datetime.now() + start = time.time() + + logging.info("\n\n=====================================================================") + logging.info(f"RUNNING ALL.") + logging.info(f"On {currentDateAndTime.strftime('%A %d. %B %Y, %H:%M:%S')}") + logging.info(f"Project directory: {session_dir}\n") + logging.info("=====================================================================") + + if do_calibration: logging.info("\n\n=====================================================================") - logging.info(f"RUNNING ALL FOR {seq_name}, FOR {frames}.") - logging.info(f"On {currentDateAndTime.strftime('%A %d. %B %Y, %H:%M:%S')}") + logging.info('Running calibration...') + logging.info("=====================================================================") + calibration(config) + else: + logging.info("\n\n=====================================================================") + logging.info('Skipping calibration.') logging.info("=====================================================================") - logging.info(f"\nProject directory: {project_dir}\n") - if do_calibration: - logging.info('\nRUNNING CALIBRATION...') - calibration(config) - else: - logging.info('\nSKIPPING CALIBRATION.') + if do_poseEstimation: + logging.info("\n\n=====================================================================") + logging.info('Running pose estimation...') + logging.info("=====================================================================") + poseEstimation(config) + else: + logging.info("\n\n=====================================================================") + logging.info('Skipping pose estimation.') + logging.info("=====================================================================") - if do_poseEstimation: - logging.info('\nRUNNING POSE ESTIMATION...') - poseEstimation(config) - else: - logging.info('\nSKIPPING POSE ESTIMATION.') + if do_synchronization: + logging.info("\n\n=====================================================================") + logging.info('Running synchronization...') + logging.info("=====================================================================") + synchronization(config) + else: + logging.info("\n\n=====================================================================") + logging.info('Skipping synchronization.') + logging.info("=====================================================================") - if do_synchronization: - logging.info('\nRUNNING SYNCHRONIZATION...') - synchronization(config) - else: - logging.info('\nSKIPPING SYNCHRONIZATION.') + if do_personAssociation: + logging.info("\n\n=====================================================================") + logging.info('Running person association...') + logging.info("=====================================================================") + personAssociation(config) + else: + logging.info("\n\n=====================================================================") + logging.info('Skipping person association.') + logging.info("=====================================================================") - if do_personAssociation: - logging.info('\nRUNNING PERSON ASSOCIATION...') - personAssociation(config) - else: - logging.info('\nSKIPPING PERSON ASSOCIATION.') - - if do_triangulation: - logging.info('\nRUNNING TRIANGULATION...') - triangulation(config) - else: - logging.info('\nSKIPPING TRIANGULATION.') - - if do_filtering: - logging.info('\nRUNNING FILTERING...') - filtering(config) - else: - logging.info('\nSKIPPING FILTERING.') - - if do_markerAugmentation: - logging.info('\nRUNNING MARKER AUGMENTATION.') - markerAugmentation(config) - else: - logging.info('\nSKIPPING MARKER AUGMENTATION.') + if do_triangulation: + logging.info("\n\n=====================================================================") + logging.info('Running triangulation...') + logging.info("=====================================================================") + triangulation(config) + else: + logging.info("\n\n=====================================================================") + logging.info('Skipping triangulation.') + logging.info("=====================================================================") - # if do_opensimProcessing: - # logging.info('\nRUNNING OPENSIM PROCESSING.') - # opensimProcessing(config) - # else: - # logging.info('\nSKIPPING OPENSIM PROCESSING.') + if do_filtering: + logging.info("\n\n=====================================================================") + logging.info('Running filtering...') + logging.info("=====================================================================") + filtering(config) + else: + logging.info("\n\n=====================================================================") + logging.info('Skipping filtering.') + logging.info("=====================================================================") - end = time.time() - elapsed = end-start - logging.info(f'\nRUNNING ALL FUNCTIONS TOOK {time.strftime("%Hh%Mm%Ss", time.gmtime(elapsed))}.') \ No newline at end of file + if do_markerAugmentation: + logging.info("\n\n=====================================================================") + logging.info('Running marker augmentation.') + logging.info("=====================================================================") + markerAugmentation(config) + else: + logging.info("\n\n=====================================================================") + logging.info('Skipping marker augmentation.') + 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("=====================================================================") + + end = time.time() + elapsed = end-start + logging.info(f'\nRUNNING ALL FUNCTIONS TOOK {time.strftime("%Hh%Mm%Ss", time.gmtime(elapsed))}.\n') \ No newline at end of file diff --git a/Pose2Sim/Utilities/tests.py b/Pose2Sim/Utilities/tests.py index 8868d63..76a8f47 100644 --- a/Pose2Sim/Utilities/tests.py +++ b/Pose2Sim/Utilities/tests.py @@ -66,7 +66,7 @@ class TestWorkflow(unittest.TestCase): @patch('builtins.input', return_value='no') # Mock input() to return 'no' def test_workflow(self, mock_input): ''' - SINGLE-PERSON and MULTI-PERSON: + SINGLE-PERSON, MULTI-PERSON, BATCH PROCESSING: - calibration - pose estimation - synchronization @@ -74,6 +74,7 @@ class TestWorkflow(unittest.TestCase): - triangulation - filtering - marker augmentation + - run all N.B.: Calibration from scene dimensions is not tested, as it requires the user to click points on the image. @@ -86,9 +87,9 @@ class TestWorkflow(unittest.TestCase): ''' - ################## - # SINGLE-PERSON # - ################## + ################### + # SINGLE-PERSON # + ################### project_dir = '../Demo_SinglePerson' config_dict = toml.load(os.path.join(project_dir, 'Config.toml')) @@ -109,11 +110,14 @@ class TestWorkflow(unittest.TestCase): Pose2Sim.filtering(config_dict) Pose2Sim.markerAugmentation(config_dict) # Pose2Sim.kinematics(config_dict) + + config_dict.get("pose").update({"overwrite_pose":False}) + Pose2Sim.runAll(config_dict) - ################## - # MULTI-PERSON # - ################## + #################### + # MULTI-PERSON # + #################### project_dir = '../Demo_MultiPerson' config_dict = toml.load(os.path.join(project_dir, 'Config.toml')) @@ -134,6 +138,19 @@ class TestWorkflow(unittest.TestCase): Pose2Sim.markerAugmentation(config_dict) # Pose2Sim.kinematics(config_dict) + config_dict.get("pose").update({"overwrite_pose":False}) + Pose2Sim.runAll(config_dict) + + + #################### + # BATCH PROCESSING # + #################### + + project_dir = '../Demo_Batch' + os.chdir(project_dir) + + Pose2Sim.runAll() + if __name__ == '__main__': unittest.main() diff --git a/Pose2Sim/synchronization.py b/Pose2Sim/synchronization.py index 8afff57..909e57f 100644 --- a/Pose2Sim/synchronization.py +++ b/Pose2Sim/synchronization.py @@ -266,7 +266,7 @@ def synchronize_cams_all(config_dict): # Warning if multi_person if multi_person: - logging.warning('\nYou set your project as a multi-person one: make sure you set `approx_time_maxspeed` and `time_range_around_maxspeed` at times where one single persons are in the scene, you you may get inaccurate results.') + logging.warning('\nYou set your project as a multi-person one: make sure you set `approx_time_maxspeed` and `time_range_around_maxspeed` at times where one single person is in the scene, or you may get inaccurate results.') do_synchro = input('Do you want to continue? (y/n)') if do_synchro.lower() not in ["y","yes"]: logging.warning('Synchronization cancelled.') diff --git a/README.md b/README.md index cf4135d..643bbdd 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ > **_News_: Version 0.9:**\ > **Pose estimation with RTMPose is now included in Pose2Sim!**\ -> **Other recently added features**: Automatic camera synchronization, multi-person analysis, Blender visualization, Marker augmentation. +> **Other recently added features**: Automatic camera synchronization, multi-person analysis, Blender visualization, Marker augmentation, Batch processing. > To upgrade, type `pip install pose2sim --upgrade` (note that you need Python 3.9 or higher). @@ -65,9 +65,7 @@ If you can only use one single camera and don't mind losing some accuracy, pleas 5. [Demonstration Part-4 (optional): Try multi-person analysis](#demonstration-part-4-optional-try-multi-person-analysis) 6. [Demonstration Part-5 (optional): Try batch processing](#demonstration-part-5-optional-try-batch-processing) 2. [Use on your own data](#use-on-your-own-data) - 1. [Setting your project up](#setting-your-project-up) - 1. [Retrieve the folder structure](#retrieve-the-folder-structure) - 2. [Single Trial vs. Batch processing](#single-trial-vs-batch-processing) + 1. [Setting up your project](#setting-up-your-project) 2. [2D pose estimation](#2d-pose-estimation) 1. [With RTMPose (default)](#with-rtmpose-default) 2. [With MMPose (coming soon)](#with-mmpose-coming-soon) @@ -167,12 +165,19 @@ Pose2Sim.markerAugmentation() ``` 3D results are stored as .trc files in each trial folder in the `pose-3d` directory. -*N.B.:* Default parameters have been provided in [Config.toml](https://github.com/perfanalytics/pose2sim/blob/main/Pose2Sim/Demo_SinglePerson/Config.toml) but can be edited. -
-__*GO FURTHER:*__\ -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)). +**Note:** +- Default parameters have been provided in [Config.toml](https://github.com/perfanalytics/pose2sim/blob/main/Pose2Sim/Demo_SinglePerson/Config.toml) but can be edited. +- You can run all stages at once: + ``` python + from Pose2Sim import Pose2Sim + 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)). +
+ +
@@ -225,14 +230,13 @@ https://github.com/perfanalytics/pose2sim/assets/54667644/5d7c858f-7e46-40c1-928 Go to the Multi-participant Demo folder: `cd \Pose2Sim\Demo_MultiPerson`. \ Type `ipython`, and try the following code: + ``` python from Pose2Sim import Pose2Sim -Pose2Sim.personAssociation() -Pose2Sim.triangulation() -Pose2Sim.filtering() -Pose2Sim.markerAugmentation() +Pose2Sim.runAll(do_synchronization=False) # Synchronization possible, but tricky with multiple persons ``` + One .trc file per participant will be generated and stored in the `pose-3d` directory.\ You can then run OpenSim scaling and inverse kinematics for each resulting .trc file as in [Demonstration Part-2](#demonstration-part-2-obtain-3d-joint-angles-with-opensim).\ You can also visualize your results with Blender as in [Demonstration Part-3](#demonstration-part-3-optional-visualize-your-results-with-blender). @@ -241,67 +245,48 @@ You can also visualize your results with Blender as in [Demonstration Part-3](#d Set *[triangulation]* `reorder_trc = true` if you need to run OpenSim and to match the generated .trc files with the static trials.\ Make sure that the order of *[markerAugmentation]* `participant_height` and `participant_mass` matches the order of the static trials. -*N.B.:* Note that in the case of our floating ghost participant, marker augmentation may worsen the results. See [Marker augmentation](#marker-augmentation) for instruction on when and when not to use it. +
+ +## Demonstration Part-5 (optional): Try batch processing +> _**Run numerous analysis with different parameters and minimal friction.**_ + +Go to the Batch Demo folder: `cd \Pose2Sim\Demo_Batch`. \ +Type `ipython`, and try the following code: + +``` python +from Pose2Sim import Pose2Sim +Pose2Sim.runAll() +``` + +The batch processing structure requires a `Config.toml` file in each of the trial directories. Global parameters are given in the `Config.toml` file of the `BatchSession` folder. They can be altered for specific or `Trials` by uncommenting keys and their values in their respective `Config.toml` files. + +Run Pose2Sim from the `BatchSession` folder if you want to batch process the whole session, or from a `Trial` folder if you want to process only a specific trial. + + +| SingleTrial | BatchSession | +|-----------------|--------------------| +|
SingleTrial                    
├── calibration
├── videos
└── Config.toml
|
BatchSession                     
├── calibration
├── Trial_1
│ ├── videos
│ └── Config.toml
├── Trial_2
│ ├── videos
│ └── Config.toml
└── Config.toml
| + + +For example, try uncommenting `[project]` and set `frame_range = [10,99]`, or uncomment `[pose]` and set `mode = 'lightweight'` in the `Config.toml` file of `Trial_1`.

# Use on your own data -> _**Deeper explanations and instructions are given below.**_ \ -> N.B.: If a step is not relevant for your use case (synchronization, person association, marker augmentation...), you can skip it. +> **N.B.: If a step is not relevant for your use case (synchronization, person association, marker augmentation...), you can skip it.** -
- -## Setting your project up +## Setting up your project > _**Get ready for automatic batch processing.**_ -### Retrieve the folder structure 1. Open a terminal, enter `pip show pose2sim`, report package location. \ Copy this path and do `cd \pose2sim`. - 2. Copy the *Demo_SinglePerson* or *Demo_MultiPerson* folder wherever you like, and rename it as you wish. + 2. Copy-paste the *Demo_SinglePerson*, *Demo_MultiPerson*, or *Demo_Batch* folder wherever you like, and rename it as you wish. 3. The rest of the tutorial will explain to you how to populate the `Calibration` and `videos` folders, edit the [Config.toml](https://github.com/perfanalytics/pose2sim/blob/main/Pose2Sim/Demo_SinglePerson/Config.toml) files, and run each Pose2Sim step.
-### Single Trial vs. Batch processing - -> _**Copy and edit either the [Demo_SinglePerson](https://github.com/perfanalytics/pose2sim/blob/main/Pose2Sim/Demo_SinglePerson) folder or the [S00_Demo_BatchSession](https://github.com/perfanalytics/pose2sim/blob/main/Pose2Sim/S00_Demo_BatchSession) one.**_ -> - Single trial is more straight-forward to set up for isolated experiments -> - Batch processing allows you to run numerous analysis with different parameters and minimal friction - - - -#### Single trial - -The single trial folder should contain a `Config.toml` file, a `calibration` folder, and a `pose` folder, the latter including one subfolder for each camera. - -
-SingleTrial \
-├── calibration \
-├── pose \
-└── Config.toml
-
- -#### Batch processing - -For batch processing, each session directory should follow a `Session -> Participant -> Trial` structure, with a `Config.toml` file in each of the directory levels. - -
-Session_s1         \ Config.toml
-├── Calibration\ 
-└── Participant_p1 \ Config.toml
-    └── Trial_t1   \ Config.toml
-        └── pose \
-
- -Run Pose2Sim from the `Session` folder if you want to batch process the whole session, from the `Participant` folder if you want to batch process all the trials of a participant, or from the `Trial` folder if you want to process a single trial. There should be one `Calibration` folder per session. - -Global parameters are given in the `Config.toml` file of the `Session` folder, and can be altered for specific `Participants` or `Trials` by uncommenting keys and their values in their respective Config.toml files.\ -Try uncommenting `[project]` and set `frame_range = [10,300]` for a Participant for example, or uncomment `[filtering.butterworth]` and set `cut_off_frequency = 10` for a Trial. - -
- ## 2D pose estimation > _**Estimate 2D pose from images with RTMPose or another pose estimation solution.**_ @@ -318,6 +303,10 @@ from Pose2Sim import Pose2Sim Pose2Sim.poseEstimation() ``` + + +
+ *N.B.:* The `GPU` will be used with ONNX backend if a valid CUDA installation is found (or MPS with MacOS), otherwise the `CPU` will be used with OpenVINO backend.\ *N.B.:* Pose estimation can be run in `lightweight`, `balanced`, or `performance` mode.\ *N.B.:* Pose estimation can be dramatically sped up by increasing the value of `det_frequency`. In that case, the detection is only done every `det_frequency` frames, and bounding boxes are tracked inbetween (keypoint detection is still performed on all frames).\ @@ -407,8 +396,12 @@ from Pose2Sim import Pose2Sim Pose2Sim.calibration() ``` -Output:\ - + + + +
+Output file: + @@ -508,10 +501,19 @@ from Pose2Sim import Pose2Sim Pose2Sim.synchronization() ``` + + +
+ For each camera, this computes mean vertical speed for the chosen keypoints, and finds the time offset for which their correlation is highest.\ All keypoints can be taken into account, or a subset of them. The user can also specify a time for each camera when only one participant is in the scene, preferably performing a clear vertical motion. -*N.B.:* Works best when only one participant is in the scene, at a roughly equal distance from all cameras and when the capture is at least 5-10 seconds long. + + +*N.B.:* Works best when: +- only one participant is in the scene (set `approx_time_maxspeed` and `time_range_around_maxspeed` accordingly) +- the participant is at a roughly equal distance from all cameras +- when the capture is at least 5 seconds long *N.B.:* Alternatively, use a flashlight, a clap, or a clear event to synchronize cameras. GoPro cameras can also be synchronized with a timecode, by GPS (outdoors) or with their app (slightly less reliable). @@ -521,8 +523,9 @@ All keypoints can be taken into account, or a subset of them. The user can also ### Associate persons across cameras > _**If `multi_person` is set to `false`, the algorithm chooses the person for whom the reprojection error is smallest.\ - If `multi_person` is set to `true`, it associates across views the people for whom the distances between epipolar lines are the smallest. People are then associated across frames according to their displacement speed.**_ \ -***N.B.:** Skip this step if only one person is in the field of view.* + If `multi_person` is set to `true`, it associates across views the people for whom the distances between epipolar lines are the smallest. People are then associated across frames according to their displacement speed.**_ + +> ***N.B.:** Skip this step if only one person is in the field of view.* Open an Anaconda prompt or a terminal in a `Session`, `Participant`, or `Trial` folder.\ Type `ipython`. @@ -531,11 +534,12 @@ from Pose2Sim import Pose2Sim Pose2Sim.personAssociation() ``` + + +
+ Check printed output. If results are not satisfying, try and release the constraints in the [Config.toml](https://github.com/perfanalytics/pose2sim/blob/main/Pose2Sim/S00_Demo_Session/Config.toml) file. -Output:\ - -
### Triangulating keypoints @@ -551,12 +555,13 @@ from Pose2Sim import Pose2Sim Pose2Sim.triangulation() ``` + + +
+ Check printed output, and visualize your trc in OpenSim: `File -> Preview experimental data`.\ If your triangulation is not satisfying, try and release the constraints in the `Config.toml` file. -Output:\ - -
### Filtering 3D coordinates @@ -571,13 +576,15 @@ from Pose2Sim import Pose2Sim Pose2Sim.filtering() ``` + + +
+ Check your filtration with the displayed figures, and visualize your .trc file in OpenSim. If your filtering is not satisfying, try and change the parameters in the `Config.toml` file. Output:\ - -
### Marker Augmentation @@ -604,6 +611,8 @@ from Pose2Sim import Pose2Sim Pose2Sim.markerAugmentation() ``` + +
## OpenSim kinematics