diff --git a/app.py b/app.py index 86a2644d..bef1b48d 100644 --- a/app.py +++ b/app.py @@ -121,10 +121,11 @@ headers = {"Authorization": "Token {}".format(API_TOKEN)}) continue - if any([v["video"] is None for v in trial["videos"]]): - r = requests.patch(trial_url, data={"status": "error"}, - headers = {"Authorization": "Token {}".format(API_TOKEN)}) - continue + # The following is now done in main, to allow reprocessing trials with missing videos + # if any([v["video"] is None for v in trial["videos"]]): + # r = requests.patch(trial_url, data={"status": "error"}, + # headers = {"Authorization": "Token {}".format(API_TOKEN)}) + # continue trial_type = "dynamic" if trial["name"] == "calibration": diff --git a/main.py b/main.py index 57ffece0..5e9bd34d 100644 --- a/main.py +++ b/main.py @@ -31,7 +31,7 @@ from utilsAugmenter import augmentTRC from utilsOpenSim import runScaleTool, getScaleTimeRange, runIKTool, generateVisualizerJson -def main(sessionName, trialName, trial_id, camerasToUse=['all'], +def main(sessionName, trialName, trial_id, cameras_to_use=['all'], intrinsicsFinalFolder='Deployed', isDocker=False, extrinsicsTrial=False, alternateExtrinsics=None, calibrationOptions=None, @@ -41,7 +41,8 @@ def main(sessionName, trialName, trial_id, camerasToUse=['all'], genericFolderNames=False, offset=True, benchmark=False, dataDir=None, overwriteAugmenterModel=False, filter_frequency='default', overwriteFilterFrequency=False, - scaling_setup='upright_standing_pose', overwriteScalingSetup=False): + scaling_setup='upright_standing_pose', overwriteScalingSetup=False, + overwriteCamerasToUse=False): # %% High-level settings. # Camera calibration. @@ -121,6 +122,15 @@ def main(sessionName, trialName, trial_id, camerasToUse=['all'], else: scalingSetup = scaling_setup + # If camerastouse is in sessionMetadata, reprocess with specified cameras. + # This allows reprocessing trials with missing videos. If + # overwriteCamerasToUse is True, the camera selection is the one + # passed as an argument to main(). This is useful for local testing. + if 'camerastouse' in sessionMetadata and not overwriteCamerasToUse: + camerasToUse = sessionMetadata['camerastouse'] + else: + camerasToUse = cameras_to_use + # %% Paths to pose detector folder for local testing. if poseDetector == 'OpenPose': poseDetectorDirectory = getOpenPoseDirectory(isDocker) @@ -316,13 +326,59 @@ def main(sessionName, trialName, trial_id, camerasToUse=['all'], else: raise Exception('checkerBoard placement value in\ sessionMetadata.yaml is not currently supported') + + # Detect all available cameras (ie, cameras with existing videos). + cameras_available = [] + for camName in cameraDirectories: + camDir = cameraDirectories[camName] + pathVideoWithoutExtension = os.path.join(camDir, 'InputMedia', trialName, trial_id) + if len(glob.glob(pathVideoWithoutExtension + '*')) == 0: + print(f"Camera {camName} does not have a video for trial {trial_id}") + else: + if os.path.exists(os.path.join(pathVideoWithoutExtension + getVideoExtension(pathVideoWithoutExtension))): + cameras_available.append(camName) + else: + print(f"Camera {camName} does not have a video for trial {trial_id}") + + if camerasToUse[0] == 'all': + cameras_all = list(cameraDirectories.keys()) + if not all([cam in cameras_available for cam in cameras_all]): + exception = 'Not all cameras have uploaded videos; one or more cameras might have turned off or lost connection' + raise Exception(exception, exception) + else: + camerasToUse_c = camerasToUse + elif camerasToUse[0] == 'all_available': + camerasToUse_c = cameras_available + print(f"Using available cameras: {camerasToUse_c}") + else: + if not all([cam in cameras_available for cam in camerasToUse]): + raise Exception('Not all specified cameras in camerasToUse have videos; verify the camera names or consider setting camerasToUse to ["all_available"]') + else: + camerasToUse_c = camerasToUse + print(f"Using cameras: {camerasToUse_c}") + settings['camerasToUse'] = camerasToUse_c + if camerasToUse_c[0] != 'all' and len(camerasToUse_c) < 2: + exception = 'At least two videos are required for 3D reconstruction, video upload likely failed for one or more cameras.' + raise Exception(exception, exception) + + # For neutral, we do not allow reprocessing with not all cameras. + # The reason is that it affects extrinsics selection, and then you can only process + # dynamic trials with the same camera selection (ie, potentially not all cameras). + # This might be addressable, but I (Antoine) do not see an immediate need + this + # would be a significant change in the code base. In practice, a data collection + # will not go through neutral if not all cameras are available. + if scaleModel: + if camerasToUse_c[0] != 'all' and len(camerasToUse_c) < len(cameraDirectories): + exception = 'All cameras are required for calibration and neutral pose.' + raise Exception(exception, exception) + # Run pose detection algorithm. try: videoExtension = runPoseDetector( cameraDirectories, trialRelativePath, poseDetectorDirectory, trialName, CamParamDict=CamParamDict, resolutionPoseDetection=resolutionPoseDetection, - generateVideo=generateVideo, cams2Use=camerasToUse, + generateVideo=generateVideo, cams2Use=camerasToUse_c, poseDetector=poseDetector, bbox_thr=bbox_thr) trialRelativePath += videoExtension except Exception as e: @@ -333,16 +389,16 @@ def main(sessionName, trialName, trial_id, camerasToUse=['all'], Visit https://www.opencap.ai/best-pratices to learn more about data collection and https://www.opencap.ai/troubleshooting for potential causes for a failed trial.""" raise Exception(exception, traceback.format_exc()) - + if runSynchronization: - # Synchronize videos. + # Synchronize videos. try: keypoints2D, confidence, keypointNames, frameRate, nansInOut, startEndFrames, cameras2Use = ( synchronizeVideos( cameraDirectories, trialRelativePath, poseDetectorDirectory, undistortPoints=True, CamParamDict=CamParamDict, filtFreqs=filtFreqs, confidenceThreshold=0.4, - imageBasedTracker=False, cams2Use=camerasToUse, + imageBasedTracker=False, cams2Use=camerasToUse_c, poseDetector=poseDetector, trialName=trialName, resolutionPoseDetection=resolutionPoseDetection)) except Exception as e: @@ -357,10 +413,18 @@ def main(sessionName, trialName, trial_id, camerasToUse=['all'], potential causes for a failed trial.""" raise Exception(exception, traceback.format_exc()) + # Note: this should not be necessary, because we prevent reprocessing the neutral trial + # with not all cameras, but keeping it in there in case we would want to. + if calibrationOptions is not None: + allCams = list(calibrationOptions.keys()) + for cam_t in allCams: + if not cam_t in cameras2Use: + calibrationOptions.pop(cam_t) + if scaleModel and calibrationOptions is not None and alternateExtrinsics is None: # Automatically select the camera calibration to use CamParamDict = autoSelectExtrinsicSolution(sessionDir,keypoints2D,confidence,calibrationOptions) - + if runTriangulation: # Triangulate. try: @@ -549,7 +613,8 @@ def main(sessionName, trialName, trial_id, camerasToUse=['all'], vertical_offset=vertical_offset) # %% Rewrite settings, adding offset - if not extrinsicsTrial and offset: - settings['verticalOffset'] = vertical_offset_settings + if not extrinsicsTrial: + if offset: + settings['verticalOffset'] = vertical_offset_settings with open(pathSettings, 'w') as file: yaml.dump(settings, file) diff --git a/opensimPipeline/Models/LaiUhlrich2022.osim b/opensimPipeline/Models/LaiUhlrich2022.osim index 70d71436..ec3e7b0b 100644 --- a/opensimPipeline/Models/LaiUhlrich2022.osim +++ b/opensimPipeline/Models/LaiUhlrich2022.osim @@ -3177,7 +3177,7 @@ 0 - -6.283185307179586 6.283185307179586 + -100 100 true diff --git a/opensimPipeline/Models/LaiUhlrich2022_shoulder.osim b/opensimPipeline/Models/LaiUhlrich2022_shoulder.osim index a26ce33f..2307d880 100644 --- a/opensimPipeline/Models/LaiUhlrich2022_shoulder.osim +++ b/opensimPipeline/Models/LaiUhlrich2022_shoulder.osim @@ -3257,7 +3257,7 @@ 0 - -6.283185307179586 6.283185307179586 + -100 100 true diff --git a/utils.py b/utils.py index 05aba40e..e45a1bba 100644 --- a/utils.py +++ b/utils.py @@ -671,7 +671,7 @@ def changeSessionMetadata(session_ids,newMetaDict): for newMeta in newMetaDict: if not newMeta in addedKey: print("Could not find {} in existing metadata, trying to add it.".format(newMeta)) - settings_fields = ['framerate', 'posemodel', 'openSimModel', 'augmentermodel', 'filterfrequency', 'scalingsetup'] + settings_fields = ['framerate', 'posemodel', 'openSimModel', 'augmentermodel', 'filterfrequency', 'scalingsetup', 'camerastouse'] if newMeta in settings_fields: if 'settings' not in existingMeta: existingMeta['settings'] = {} @@ -770,9 +770,11 @@ def postMotionData(trial_id,session_path,trial_name=None,isNeutral=False, camDirs = glob.glob(os.path.join(session_path,'Videos','Cam*')) for camDir in camDirs: outputPklFolder = os.path.join(camDir,pklDir) - pklPath = glob.glob(os.path.join(outputPklFolder,'*_pp.pkl'))[0] - _,camName = os.path.split(camDir) - postFileToTrial(pklPath,trial_id,tag='pose_pickle',device_id=camName) + pickle_files = glob.glob(os.path.join(outputPklFolder,'*_pp.pkl')) + if pickle_files: + pklPath = pickle_files[0] + _,camName = os.path.split(camDir) + postFileToTrial(pklPath,trial_id,tag='pose_pickle',device_id=camName) # post marker data deleteResult(trial_id, tag='marker_data') diff --git a/utilsServer.py b/utilsServer.py index 865ceaae..352a8419 100644 --- a/utilsServer.py +++ b/utilsServer.py @@ -40,7 +40,8 @@ def processTrial(session_id, trial_id, trial_type = 'dynamic', deleteLocalFolder = True, hasWritePermissions = True, use_existing_pose_pickle = False, - batchProcess = False): + batchProcess = False, + cameras_to_use=['all']): # Get session directory session_name = session_id @@ -61,7 +62,8 @@ def processTrial(session_id, trial_id, trial_type = 'dynamic', # run calibration try: main(session_name, trial_name, trial_id, isDocker=isDocker, extrinsicsTrial=True, - imageUpsampleFactor=imageUpsampleFactor,genericFolderNames = True) + imageUpsampleFactor=imageUpsampleFactor,genericFolderNames = True, + cameras_to_use=cameras_to_use) except Exception as e: error_msg = {} error_msg['error_msg'] = e.args[0] @@ -122,7 +124,8 @@ def processTrial(session_id, trial_id, trial_type = 'dynamic', resolutionPoseDetection = resolutionPoseDetection, genericFolderNames = True, bbox_thr = bbox_thr, - calibrationOptions = calibrationOptions) + calibrationOptions = calibrationOptions, + cameras_to_use=cameras_to_use) except Exception as e: # Try to post pose pickles so can be used offline. This function will # error at kinematics most likely, but if pose estimation completed, @@ -211,7 +214,8 @@ def processTrial(session_id, trial_id, trial_type = 'dynamic', imageUpsampleFactor=imageUpsampleFactor, resolutionPoseDetection = resolutionPoseDetection, genericFolderNames = True, - bbox_thr = bbox_thr) + bbox_thr = bbox_thr, + cameras_to_use=cameras_to_use) except Exception as e: # Try to post pose pickles so can be used offline. This function will # error at kinematics most likely, but if pose estimation completed, @@ -331,7 +335,8 @@ def newSessionSameSetup(session_id_old,session_id_new,extrinsicTrialName='calibr def batchReprocess(session_ids,calib_id,static_id,dynamic_trialNames,poseDetector='OpenPose', resolutionPoseDetection='1x736',deleteLocalFolder=True, - isServer=False, use_existing_pose_pickle=True): + isServer=False, use_existing_pose_pickle=True, + cameras_to_use=['all']): # extract trial ids from trial names if dynamic_trialNames is not None and len(dynamic_trialNames)>0: @@ -366,7 +371,8 @@ def batchReprocess(session_ids,calib_id,static_id,dynamic_trialNames,poseDetecto poseDetector = poseDetector, deleteLocalFolder = deleteLocalFolder, isDocker=isServer, - hasWritePermissions = hasWritePermissions) + hasWritePermissions = hasWritePermissions, + cameras_to_use=cameras_to_use) statusData = {'status':'done'} _ = requests.patch(API_URL + "trials/{}/".format(calib_id_toProcess), data=statusData, headers = {"Authorization": "Token {}".format(API_TOKEN)}) @@ -392,7 +398,8 @@ def batchReprocess(session_ids,calib_id,static_id,dynamic_trialNames,poseDetecto isDocker=isServer, hasWritePermissions = hasWritePermissions, use_existing_pose_pickle = use_existing_pose_pickle, - batchProcess = True) + batchProcess = True, + cameras_to_use=cameras_to_use) statusData = {'status':'done'} _ = requests.patch(API_URL + "trials/{}/".format(static_id_toProcess), data=statusData, headers = {"Authorization": "Token {}".format(API_TOKEN)}) @@ -423,7 +430,8 @@ def batchReprocess(session_ids,calib_id,static_id,dynamic_trialNames,poseDetecto isDocker=isServer, hasWritePermissions = hasWritePermissions, use_existing_pose_pickle = use_existing_pose_pickle, - batchProcess = True) + batchProcess = True, + cameras_to_use=cameras_to_use) statusData = {'status':'done'} _ = requests.patch(API_URL + "trials/{}/".format(dID), data=statusData,