From 7242702d1c8d40e5ea820d2b4c4cc0c852e49c43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandrine=20B=C3=A9dard?= <71230552+sandrinebedard@users.noreply.github.com> Date: Sat, 18 May 2024 16:46:37 -0400 Subject: [PATCH] Add options for sub-folder and for func sesssions (#88) * add options for sub-folder and for func sesssions * Update utils.py --------- Co-authored-by: Rohan Banerjee --- manual_correction.py | 126 +++++++++++++++++++++++++++++++++++++++++++ utils.py | 12 +++-- 2 files changed, 135 insertions(+), 3 deletions(-) diff --git a/manual_correction.py b/manual_correction.py index 7d3e6a5..915d074 100644 --- a/manual_correction.py +++ b/manual_correction.py @@ -215,6 +215,12 @@ def get_parser(): type=str, default=None ) + parser.add_argument( #TODO + '-add-subfolder', + help="Add a subfolder to find labels or segmentation to correct", + type=str, + default=None + ) parser.add_argument( '-qc-only', help="Only output QC report based on the manually-corrected files already present in the 'derivatives' folder. " @@ -584,6 +590,8 @@ def ask_if_modify(fname_out, fname_label, do_labeling_always=False): create_empty_mask = False # If the output file does not exist, copy it from label folder + print(os.path.isfile(fname_label)) + print('fname_label', fname_label) elif not os.path.isfile(fname_out) and os.path.isfile(fname_label): do_labeling = True copy = True @@ -780,6 +788,123 @@ def main(): # TODO: address "none" issue if no file present under a key # Perform manual corrections for task, files in dict_yml.items(): + # Get the list of segmentation files to add to derivatives, excluding the manually corrected files in -config. + # TODO: probably extend also for other tasks (such as FILES_GMSEG) + if args.add_seg_only and task == 'FILES_SEG': + # Remove the files in the -config list + for file in files: + # Remove the file suffix (e.g., '_RPI_r') to match the list of files in -path-img + file = utils.remove_suffix(file, args.suffix_files_in) + if file in file_list: + file_list.remove(file) + files = file_list # Rename to use those files instead of the ones to exclude + if len(files) > 0: + # Handle regex (i.e., iterate over all subjects) + if '*' in files[0] and len(files) == 1: + subject, ses, filename, contrast = utils.fetch_subject_and_session(files[0]) + # Get list of files recursively + files = sorted(glob.glob(os.path.join(path_img, '**', filename), + recursive=True)) + # Loop across files + for file in tqdm.tqdm(files, desc="{}".format(task), unit="file"): + # Print empty line to not overlay with tqdm progress bar + time.sleep(0.1) + print("") + # build file names + subject, ses, filename, contrast = utils.fetch_subject_and_session(file) + if args.add_subfolder: + subfolder = args.add_subfolder + else: + subfolder = False + # Construct absolute path to the input file + # For example: '/Users/user/dataset/data_processed/sub-001/anat/sub-001_T2w.nii.gz' + if subfolder: + fname = os.path.join(path_img, subject, ses, contrast, subfolder, filename) + else: + fname = os.path.join(path_img, subject, ses, contrast, filename) + # Construct absolute path to the other contrast file + if args.load_other_contrast: + # Do not include session in the filename + if ses == '': + other_contrast_filename = subject + '_' + args.load_other_contrast + '.nii.gz' + # Include session in the filename + else: + other_contrast_filename = subject + '_' + ses + '_' + args.load_other_contrast + '.nii.gz' + fname_other_contrast = os.path.join(path_img, subject, ses, contrast, other_contrast_filename) + else: + fname_other_contrast = None + # Construct absolute path to the input label (segmentation, labeling etc.) file + # For example: '/Users/user/dataset/data_processed/sub-001/anat/sub-001_T2w_seg.nii.gz' + if subfolder: + fname_label = utils.add_suffix(os.path.join(path_label, subject, ses, contrast, subfolder, filename), suffix_dict[task]) + else: + fname_label = utils.add_suffix(os.path.join(path_label, subject, ses, contrast, filename), suffix_dict[task]) + # Construct absolute path to the output file (i.e., path where manually corrected file will be saved) + # For example: '/Users/user/dataset/derivatives/labels/sub-001/anat/sub-001_T2w_seg.nii.gz' + # The information regarding the modified data will be stored within the sidecar .json file + fname_out = utils.add_suffix(os.path.join(path_out, subject, ses, contrast, filename), suffix_dict[task]) + + # Create subject folder in output if they do not exist + os.makedirs(os.path.join(path_out, subject, ses, contrast), exist_ok=True) + if not args.qc_only: + # Check if the output file already exists. If so, asks user if they want to modify it. + do_labeling, copy, create_empty_mask, do_labeling_always = \ + ask_if_modify(fname_out=fname_out, + fname_label=fname_label, + do_labeling_always=do_labeling_always) + # Perform labeling (i.e., segmentation correction, labeling correction etc.) for the specific task + if do_labeling: + if args.denoise: + # Denoise the input file + fname = denoise_image(fname) + # Copy file to derivatives folder + if copy: + shutil.copyfile(fname_label, fname_out) + print(f'Copying: {fname_label} to {fname_out}') + # Create empty mask in derivatives folder + elif create_empty_mask: + utils.create_empty_mask(fname, fname_out) + + if task in ['FILES_SEG', 'FILES_GMSEG']: + if not args.add_seg_only: + time_one = get_modification_time(fname_out) + correct_segmentation(fname, fname_out, fname_other_contrast, args.viewer, param_fsleyes) + time_two = get_modification_time(fname_out) + elif task == 'FILES_LESION': + time_one = get_modification_time(fname_out) + correct_segmentation(fname, fname_out, fname_other_contrast, args.viewer, param_fsleyes) + time_two = get_modification_time(fname_out) + elif task == 'FILES_LABEL': + time_one = get_modification_time(fname_out) + correct_vertebral_labeling(fname, fname_out, args.label_disc_list) + time_two = get_modification_time(fname_out) + elif task == 'FILES_COMPRESSION': + time_one = get_modification_time(fname_out) + # Note: be aware of possibility to create compression labels also using + # 'sct_label_utils -create-viewer' + # Context: https://github.com/spinalcordtoolbox/spinalcordtoolbox/issues/3984 + correct_segmentation(fname, fname_out, fname_other_contrast, 'fsleyes', param_fsleyes) + time_two = get_modification_time(fname_out) + elif task == 'FILES_PMJ': + time_one = get_modification_time(fname_out) + correct_pmj_label(fname, fname_out) + time_two = get_modification_time(fname_out) + elif task == 'FILES_CENTERLINE': + time_one = get_modification_time(fname_out) + correct_centerline(fname, fname_out) + time_two = get_modification_time(fname_out) + else: + sys.exit('Task not recognized from the YAML file: {}'.format(task)) + if args.denoise: + # Remove the denoised file (we do not need it anymore) + remove_denoised_file(fname) + + # Add segmentation only (skip generating QC report) + if args.add_seg_only: + # We are passing modified=True because we are adding a new segmentation and we want + # to create a JSON file + update_json(fname_out, name_rater, modified=True) + # Generate QC report if task.startswith('FILES'): # Check if task is in suffix_dict.keys(), if not, skip it # Note that this check is done after the task.startswith('FILES') check because the manual-correction @@ -897,6 +1022,7 @@ def main(): # a JSON file update_json(fname_out, name_rater, json_metadata) # Generate QC report + #generate_qc(fname, fname_out, task, fname_qc, subject, args.config, args.qc_lesion_plane, suffix_dict) else: update_json(fname_out, name_rater, json_metadata) # Generate QC report diff --git a/utils.py b/utils.py index f838ac5..246064e 100644 --- a/utils.py +++ b/utils.py @@ -35,6 +35,15 @@ def fetch_subject_and_session(filename_path): _, filename = os.path.split(filename_path) # Get just the filename (i.e., remove the path) subject = re.search('sub-(.*?)[_/]', filename_path) # [_/] means either underscore or slash subjectID = subject.group(0)[:-1] if subject else "" # [:-1] removes the last underscore or slash + session = re.findall(r'ses-.*', filename_path) + sessionID = session[0].split('_')[0] if session else "" # Return None if there is no session + if 'dwi' in filename_path: + contrast = 'dwi' + elif 'bold' in filename_path: + contrast = 'func' + else: + contrast = 'anat' + session = re.search('ses-(.*?)[_/]', filename_path) # [_/] means either underscore or slash sessionID = session.group(0)[:-1] if session else "" # [:-1] removes the last underscore or slash @@ -42,9 +51,6 @@ def fetch_subject_and_session(filename_path): # . - match any character (except newline) # *? - match the previous element as few times as possible (zero or more times) - # TODO - add support for func (fMRI) - contrast = 'dwi' if 'dwi' in filename_path else 'anat' # Return contrast (dwi or anat) - return subjectID, sessionID, filename, contrast