From c562652c3a586efeae745907f03d8ae76908d630 Mon Sep 17 00:00:00 2001 From: valosekj Date: Thu, 19 Jan 2023 13:13:20 -0500 Subject: [PATCH 01/41] Update description, imports, and help --- manual_correction.py | 48 ++++++++++++++++++++------------------- package_for_correction.py | 37 ++++++++++++++++++------------ utils.py | 9 ++++++-- 3 files changed, 54 insertions(+), 40 deletions(-) diff --git a/manual_correction.py b/manual_correction.py index 71aa581..f293fca 100644 --- a/manual_correction.py +++ b/manual_correction.py @@ -1,11 +1,12 @@ #!/usr/bin/env python # -# Script to perform manual correction of segmentations and vertebral labeling. +# Script to perform manual correction of spinal cord segmentation, gray matter segmentation, vertebral labeling, and +# pontomedullary junction labeling. # # For usage, type: python manual_correction.py -h # -# Authors: Jan Valosek, Julien Cohen-Adad -# Adapted by Sandrine Bédard for cord CSA project UK Biobank +# Authors: Jan Valosek, Sandrine Bédard, Julien Cohen-Adad +# import argparse import coloredlogs @@ -16,12 +17,7 @@ import shutil from textwrap import dedent import time -import yaml -import pipeline_ukbiobank.utils as utils - -# Folder where to output manual labels, at the root of a BIDS dataset. -# TODO: make it an input argument (with default value) -FOLDER_DERIVATIVES = os.path.join('derivatives', 'labels') +import utils def get_parser(): @@ -29,8 +25,9 @@ def get_parser(): parser function """ parser = argparse.ArgumentParser( - description='Manual correction of spinal cord segmentation, vertebral and pontomedullary junction labeling. ' - 'Manually corrected files are saved under derivatives/ folder (BIDS standard).', + description='Manual correction of spinal cord segmentation, gray matter segmentation, vertebral labeling, and ' + 'pontomedullary junction labeling.' + 'Manually corrected files are saved under derivatives/ folder (according to BIDS standard).', formatter_class=utils.SmartFormatter, prog=os.path.basename(__file__).strip('.py') ) @@ -41,35 +38,40 @@ def get_parser(): help= "R|Config yaml file listing images that require manual corrections for segmentation and vertebral " "labeling. 'FILES_SEG' lists images associated with spinal cord segmentation " + ",'FILES_GMSEG' lists images associated with gray matter segmentation " ",'FILES_LABEL' lists images associated with vertebral labeling " "and 'FILES_PMJ' lists images associated with pontomedullary junction labeling" "You can validate your .yml file at this website: http://www.yamllint.com/." - " If you want to correct segmentation only, ommit 'FILES_LABEL' in the list. Below is an example .yml file:\n" + "Below is an example .yml file:\n" + dedent( """ FILES_SEG: - - sub-1000032_T1w.nii.gz - - sub-1000083_T2w.nii.gz + - sub-001_T1w.nii.gz + - sub-002_T2w.nii.gz + FILES_GMSEG: + - sub-001_T1w.nii.gz + - sub-002_T2w.nii.gz FILES_LABEL: - - sub-1000032_T1w.nii.gz - - sub-1000710_T1w.nii.gz + - sub-001_T1w.nii.gz + - sub-002_T1w.nii.gz FILES_PMJ: - - sub-1000032_T1w.nii.gz - - sub-1000710_T1w.nii.gz\n + - sub-001_T1w.nii.gz + - sub-002_T1w.nii.gz\n """) ) parser.add_argument( '-path-in', metavar="", - help='Path to the processed data. Example: ~/ukbiobank_results/data_processed', - default='./' + required=True, + help='Path to the processed data. Example: ~//data_processed', ) parser.add_argument( '-path-out', metavar="", - help="Path to the BIDS dataset where the corrected labels will be generated. Note: if the derivatives/ folder " - "does not already exist, it will be created." - "Example: ~/data-ukbiobank", + help= + "R|Path to the output folder where the corrected labels will be saved. Example: ~//" + "Note: The path provided within this flag will be combined with the path provided within the " + "'-path-derivatives' flag. ", default='./' ) parser.add_argument( diff --git a/package_for_correction.py b/package_for_correction.py index ed7b12d..befdf00 100644 --- a/package_for_correction.py +++ b/package_for_correction.py @@ -1,10 +1,11 @@ #!/usr/bin/env python # -# Script to package data for manual correction from SpineGeneric adapted for ukbiobank cord CSA project. +# Script to package data for manual correction from SpineGeneric adapted for canproco project. # # For usage, type: python package_for_correction.py -h # -# Author: Julien Cohen-Adad +# Authors: Jan Valosek, Sandrine Bédard, Julien Cohen-Adad +# import os @@ -13,10 +14,8 @@ import tempfile from textwrap import dedent import argparse -import yaml import coloredlogs - -import pipeline_ukbiobank.utils as utils +import utils def get_parser(): @@ -35,26 +34,34 @@ def get_parser(): metavar="", required=True, help= - "R|Config .yml file listing images that require manual corrections for segmentation and vertebral " - "labeling. 'FILES_SEG' lists images associated with spinal cord segmentation," - "and 'FILES_LABEL' lists images associated with vertebral labeling. " - "You can validate your .yml file at this website: http://www.yamllint.com/. Below is an example .yml file:\n" + "R|Config yaml file listing images that require manual corrections for segmentation and vertebral " + "labeling. 'FILES_SEG' lists images associated with spinal cord segmentation " + ",'FILES_GMSEG' lists images associated with gray matter segmentation " + ",'FILES_LABEL' lists images associated with vertebral labeling " + "and 'FILES_PMJ' lists images associated with pontomedullary junction labeling" + "You can validate your .yml file at this website: http://www.yamllint.com/." + "Below is an example .yml file:\n" + dedent( """ FILES_SEG: - - sub-1000032_T1w.nii.gz - - sub-1000083_T2w.nii.gz + - sub-001_T1w.nii.gz + - sub-002_T2w.nii.gz + FILES_GMSEG: + - sub-001_T1w.nii.gz + - sub-002_T2w.nii.gz FILES_LABEL: - - sub-1000032_T1w.nii.gz - - sub-1000710_T1w.nii.gz\n + - sub-001_T1w.nii.gz + - sub-002_T1w.nii.gz + FILES_PMJ: + - sub-001_T1w.nii.gz + - sub-002_T1w.nii.gz\n """) ) parser.add_argument( '-path-in', metavar="", required=True, - help='Path to the processed data. Example: ~/ukbiobank_results/data_processed', - default='./' + help='Path to the processed data. Example: ~//data_processed', ) parser.add_argument( '-o', diff --git a/utils.py b/utils.py index 7a669c0..833f2b7 100644 --- a/utils.py +++ b/utils.py @@ -1,16 +1,21 @@ #!/usr/bin/env python # -*- coding: utf-8 +# # Collection of useful functions from spine_generic, used for manual segmentation +# +# Authors: Jan Valosek, Sandrine Bédard, Julien Cohen-Adad +# import os import re import logging +import sys import textwrap import argparse import subprocess import shutil -from pathlib import Path -from enum import Enum +import yaml + # BIDS utility tool def get_subject(file): From 49b13664215f9493cb35eeb0f9ecd678fbfde16f Mon Sep 17 00:00:00 2001 From: valosekj Date: Thu, 19 Jan 2023 13:14:19 -0500 Subject: [PATCH 02/41] Add new input flags --- manual_correction.py | 49 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/manual_correction.py b/manual_correction.py index f293fca..05ace34 100644 --- a/manual_correction.py +++ b/manual_correction.py @@ -74,6 +74,55 @@ def get_parser(): "'-path-derivatives' flag. ", default='./' ) + parser.add_argument( + '-path-derivatives', + metavar="", + help= + "R|Path to the 'derivatives' BIDS-complaint folder where the corrected labels will be saved. " + "Example: derivatives/labels" + "Note: if the provided folder (e.g., 'derivatives/labels') does not already exist, it will be created." + "Note: if segmentation or labels files already exist and you would like to correct them, provide path to them " + "within this flag.", + default=os.path.join('derivatives', 'labels') + ) + parser.add_argument( + '-suffix-files-in', + help= + "R|Suffix of the input files. For example: '_RPI_r'." + "Note: this flag is useful in cases when the input files have been processed and thus contains a specific " + "suffix.", + default='' + ) + parser.add_argument( + '-suffix-files-seg', + help="FILES-SEG suffix. Available options: '_seg' (default), '_label-SC_mask'.", + choices=['_seg', '_label-SC_mask'], + default='_seg' + ) + parser.add_argument( + '-suffix-files-gmseg', + help="FILES-GMSEG suffix. Available options: '_gmseg' (default), '_label-GM_mask'.", + choices=['_gmseg', '_label-GM_mask'], + default='_gmseg' + ) + parser.add_argument( + '-suffix-files-label', + help="FILES-LABEL suffix. Available options: '_labels' (default), '_labels-disc'.", + choices=['_labels', '_labels-disc'], + default='_labels' + ) + parser.add_argument( + '-label-list', + help="Provide a comma-separated list containing individual values and/or intervals. Example: '1:4,6,8' or 1:20 " + "(default)", + default='1:20' + ) + parser.add_argument( + '-viewer', + help="Viewer used for manual correction. Available options: 'itksnap' (default), 'fsleyes', 'slicer'.", + choices=['fsleyes', 'itksnap', 'slicer'], + default='itksnap' + ) parser.add_argument( '-qc-only', help="Only output QC report based on the manually-corrected files already present in the derivatives folder. " From 291be5f2257af344e1066934698037181be923ad Mon Sep 17 00:00:00 2001 From: valosekj Date: Thu, 19 Jan 2023 13:14:53 -0500 Subject: [PATCH 03/41] Include also GM segmentation ('FILES_GMSEG') --- manual_correction.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/manual_correction.py b/manual_correction.py index 05ace34..a03f4b7 100644 --- a/manual_correction.py +++ b/manual_correction.py @@ -144,9 +144,17 @@ def get_parser(): return parser -def get_function(task): +# TODO: add also sct_get_centerline +def get_function_for_qc(task): + """ + Get the function to use for QC based on the task. + :param task: + :return: + """ if task == 'FILES_SEG': return 'sct_deepseg_sc' + elif task == "FILES_GMSEG": + return "sct_deepseg_gm" elif task == 'FILES_LABEL': return 'sct_label_utils' elif task == 'FILES_PMJ': From 44a54f6937600365f545335cb033ca80bdf881cc Mon Sep 17 00:00:00 2001 From: valosekj Date: Thu, 19 Jan 2023 13:16:57 -0500 Subject: [PATCH 04/41] Allow to select a viewer (FSLeyes, ITKsnap, 3D Slicer). Add viewer_not_found function --- manual_correction.py | 64 +++++++++++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/manual_correction.py b/manual_correction.py index a03f4b7..ee324f7 100644 --- a/manual_correction.py +++ b/manual_correction.py @@ -163,37 +163,51 @@ def get_function_for_qc(task): raise ValueError("This task is not recognized: {}".format(task)) -def get_suffix(task, suffix=''): - if task == 'FILES_SEG': - return '_seg'+suffix - elif task == 'FILES_LABEL': - return '_labels'+suffix - elif task == 'FILES_PMJ': - return '_pmj'+suffix - - else: - raise ValueError("This task is not recognized: {}".format(task)) - - -def correct_segmentation(fname, fname_seg_out): +def correct_segmentation(fname, fname_seg_out, viewer): """ - Copy fname_seg in fname_seg_out, then open ITK-SNAP with fname and fname_seg_out. + Open viewer (ITK-SNAP, FSLeyes, or 3D Slicer) with fname and fname_seg_out. :param fname: - :param fname_seg: :param fname_seg_out: - :param name_rater: + :param viewer: :return: """ # launch ITK-SNAP - # Note: command line differs for macOs/Linux and Windows - print("In ITK-SNAP, correct the segmentation, then save it with the same name (overwrite).") - if shutil.which('itksnap') is not None: # Check if command 'itksnap' exists - os.system('itksnap -g ' + fname + ' -s ' + fname_seg_out) # for macOS and Linux - elif shutil.which('ITK-SNAP') is not None: # Check if command 'ITK-SNAP' exists - os.system('ITK-SNAP -g ' + fname + ' -s ' + fname_seg_out) # For windows - else: - sys.exit("ITK-SNAP not found. Please install it before using this program or check if it was added to PATH variable. Exit program.") - + if viewer == 'itksnap': + print("In ITK-SNAP, correct the segmentation, then save it with the same name (overwrite).") + # Note: command line differs for macOs/Linux and Windows + if shutil.which('itksnap') is not None: # Check if command 'itksnap' exists + # macOS and Linux + os.system('itksnap -g {} -s {}'.format(fname, fname_seg_out)) + elif shutil.which('ITK-SNAP') is not None: # Check if command 'ITK-SNAP' exists + # Windows + os.system('ITK-SNAP -g {} -s {}'.format(fname, fname_seg_out)) + else: + viewer_not_found(viewer) + # launch FSLeyes + elif viewer == 'fsleyes': + if shutil.which('fsleyes') is not None: # Check if command 'fsleyes' exists + print("In FSLeyes, click on 'Edit mode', correct the segmentation, and then save it with the same name " + "(overwrite).") + os.system('fsleyes {} {} -cm red'.format(fname, fname_seg_out)) + else: + viewer_not_found(viewer) + # launch 3D Slicer + elif viewer == 'slicer': + if shutil.which('slicer') is not None: + # TODO: Add instructions for 3D Slicer + pass + else: + viewer_not_found(viewer) + + +def viewer_not_found(viewer): + """ + Print that viewer is not installed and exit the program. + :param viewer: + :return: + """ + sys.exit("{} not found. Please install it before using this program or check if it was added to PATH variable. " + "You can also use another viewer by using the flag -viewer.".format(viewer)) def correct_vertebral_labeling(fname, fname_label): """ From 74da357301e48beb1d66d9b9dbc901a4a632e347 Mon Sep 17 00:00:00 2001 From: valosekj Date: Thu, 19 Jan 2023 13:18:08 -0500 Subject: [PATCH 05/41] Implement viewer_not_found to correct_vertebral_labeling. Allow passing 'label_list' specifying labels to correct. --- manual_correction.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/manual_correction.py b/manual_correction.py index ee324f7..cd0a743 100644 --- a/manual_correction.py +++ b/manual_correction.py @@ -209,16 +209,23 @@ def viewer_not_found(viewer): sys.exit("{} not found. Please install it before using this program or check if it was added to PATH variable. " "You can also use another viewer by using the flag -viewer.".format(viewer)) -def correct_vertebral_labeling(fname, fname_label): + +def correct_vertebral_labeling(fname, fname_label, label_list, viewer='sct_label_utils'): """ Open sct_label_utils to manually label vertebral levels. :param fname: :param fname_label: - :param name_rater: + :param label_list: Comma-separated list containing individual values and/or intervals. Example: '1:4,6,8' or 1:20 :return: """ - message = "Click at the posterior tip of the disc between C1-C2, C2-C3 and C3-C4 vertebral levels, then click 'Save and Quit'." - os.system('sct_label_utils -i {} -create-viewer 2,3,4 -o {} -msg "{}"'.format(fname, fname_label, message)) + if shutil.which(viewer) is not None: # Check if command 'sct_label_utils' exists + message = "Click at the posterior tip of the disc(s). Then click 'Save and Quit'." + if os.path.exists(fname_label): + os.system('sct_label_utils -i {} -create-viewer {} -o {} -ilabel {} -msg "{}"'.format(fname, label_list, fname_label, fname_label, message)) + else: + os.system('sct_label_utils -i {} -create-viewer {} -o {} -msg "{}"'.format(fname, label_list, fname_label, message)) + else: + viewer_not_found(viewer) def correct_pmj_label(fname, fname_label): From feb8eaa4e0eb6430eef97bfd116e5c7c99773f9f Mon Sep 17 00:00:00 2001 From: valosekj Date: Thu, 19 Jan 2023 13:18:31 -0500 Subject: [PATCH 06/41] Implement viewer_not_found to correct_pmj_label. --- manual_correction.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/manual_correction.py b/manual_correction.py index cd0a743..00b19d0 100644 --- a/manual_correction.py +++ b/manual_correction.py @@ -228,16 +228,18 @@ def correct_vertebral_labeling(fname, fname_label, label_list, viewer='sct_label viewer_not_found(viewer) -def correct_pmj_label(fname, fname_label): +def correct_pmj_label(fname, fname_label, viewer='sct_label_utils'): """ Open sct_label_utils to manually label PMJ. :param fname: :param fname_label: - :param name_rater: :return: """ - message = "Click at the posterior tip of the pontomedullary junction (PMJ) then click 'Save and Quit'." - os.system('sct_label_utils -i {} -create-viewer 50 -o {} -msg "{}"'.format(fname, fname_label, message)) + if shutil.which(viewer) is not None: # Check if command 'sct_label_utils' exists + message = "Click at the posterior tip of the pontomedullary junction (PMJ). Then click 'Save and Quit'." + os.system('sct_label_utils -i {} -create-viewer 50 -o {} -msg "{}"'.format(fname, fname_label, message)) + else: + viewer_not_found(viewer) def create_json(fname_nifti, name_rater): From fae12ade826107fc363b752f8433401acbdbe73a Mon Sep 17 00:00:00 2001 From: valosekj Date: Thu, 19 Jan 2023 13:19:42 -0500 Subject: [PATCH 07/41] Move the question if to modify labels to a separate function. --- manual_correction.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/manual_correction.py b/manual_correction.py index 00b19d0..1d06d4b 100644 --- a/manual_correction.py +++ b/manual_correction.py @@ -253,6 +253,34 @@ def create_json(fname_nifti, name_rater): fname_json = fname_nifti.rstrip('.nii').rstrip('.nii.gz') + '.json' with open(fname_json, 'w') as outfile: json.dump(metadata, outfile, indent=4) + # Add last newline + outfile.write("\n") + + +def ask_if_modify(fname_label): + """ + Check if file under derivatives already exists. If so, asks user if they want to modify it. + :param fname_label: + :return: + """ + if os.path.isfile(fname_label): + answer = None + while answer not in ("y", "n"): + answer = input("WARNING! The file {} already exists. " + "Would you like to modify it? [y/n] ".format(fname_label)) + if answer == "y": + do_labeling = True + overwrite = False + elif answer == "n": + do_labeling = False + overwrite = False + else: + print("Please answer with 'y' or 'n'") + else: + do_labeling = True + overwrite = True + + return do_labeling, overwrite def main(): From 272c4c9cba622172e70edfd027a578a821ea1cfa Mon Sep 17 00:00:00 2001 From: valosekj Date: Thu, 19 Jan 2023 13:20:35 -0500 Subject: [PATCH 08/41] Introduce fetch_yaml_config function to simplify code among scripts. --- manual_correction.py | 14 ++------------ package_for_correction.py | 16 +++------------- utils.py | 23 +++++++++++++++++++++++ 3 files changed, 28 insertions(+), 25 deletions(-) diff --git a/manual_correction.py b/manual_correction.py index 1d06d4b..e414c53 100644 --- a/manual_correction.py +++ b/manual_correction.py @@ -295,18 +295,8 @@ def main(): else: coloredlogs.install(fmt='%(message)s', level='INFO') - # check if input yml file exists - if os.path.isfile(args.config): - fname_yml = args.config - else: - sys.exit("ERROR: Input yml file {} does not exist or path is wrong.".format(args.config)) - - # fetch input yml file as dict - with open(fname_yml, 'r') as stream: - try: - dict_yml = yaml.safe_load(stream) - except yaml.YAMLError as exc: - print(exc) + # Fetch configuration from YAML file + dict_yml = utils.fetch_yaml_config(args.config) # Curate dict_yml to only have filenames instead of absolute path dict_yml = utils.curate_dict_yml(dict_yml) diff --git a/package_for_correction.py b/package_for_correction.py index befdf00..bc2d561 100644 --- a/package_for_correction.py +++ b/package_for_correction.py @@ -97,19 +97,9 @@ def main(): else: coloredlogs.install(fmt='%(message)s', level='INFO') - # Check if input yml file exists - if os.path.isfile(args.config): - fname_yml = args.config - else: - sys.exit("ERROR: Input yml file {} does not exist or path is wrong.".format(args.config)) - - # Fetch input yml file as dict - with open(fname_yml, 'r') as stream: - try: - dict_yml = yaml.safe_load(stream) - except yaml.YAMLError as exc: - print(exc) - + # Fetch configuration from YAML file + dict_yml = utils.fetch_yaml_config(args.config) + # Curate dict_yml to only have filenames instead of absolute path dict_yml = utils.curate_dict_yml(dict_yml) diff --git a/utils.py b/utils.py index 833f2b7..b39d8d3 100644 --- a/utils.py +++ b/utils.py @@ -141,6 +141,29 @@ def remove_suffix(fname, suffix): return os.path.join(stem.replace(suffix, '') + ext) +def fetch_yaml_config(config_file): + """ + Fetch configuration from YAML file + :param config_file: YAML file + :return: dictionary with configuration + """ + config_file = get_full_path(config_file) + # Check if input yml file exists + if os.path.isfile(config_file): + fname_yml = config_file + else: + sys.exit("ERROR: Input yml file {} does not exist or path is wrong.".format(config_file)) + + # Fetch input yml file as dict + with open(fname_yml, 'r') as stream: + try: + dict_yml = yaml.safe_load(stream) + except yaml.YAMLError as exc: + print(exc) + + return dict_yml + + def curate_dict_yml(dict_yml): """ Curate dict_yml to only have filenames instead of absolute path From abe52af7d37db6e5eae002030409784bd9109a84 Mon Sep 17 00:00:00 2001 From: valosekj Date: Thu, 19 Jan 2023 13:23:19 -0500 Subject: [PATCH 09/41] Introduce get_full_path function (to return full path and deal with ~ symbol). --- utils.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/utils.py b/utils.py index b39d8d3..4e84f07 100644 --- a/utils.py +++ b/utils.py @@ -176,6 +176,15 @@ def curate_dict_yml(dict_yml): return dict_yml_curate +def get_full_path(path): + """ + Return full path. If ~ is passed, expand it to home directory. + :param path: str: Input path + :return: str: Full path + """ + return os.path.abspath(os.path.expanduser(path)) + + def check_files_exist(dict_files, path_data): """ Check if all files listed in the input dictionary exist From 0e312aa402f19c4dda09aafc2118c406eae5b84c Mon Sep 17 00:00:00 2001 From: valosekj Date: Thu, 19 Jan 2023 13:39:09 -0500 Subject: [PATCH 10/41] Introduce fetch_subject_and_session function (to unify get_subject and get_session under a single function). --- utils.py | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/utils.py b/utils.py index 4e84f07..d56fb63 100644 --- a/utils.py +++ b/utils.py @@ -18,22 +18,29 @@ # BIDS utility tool -def get_subject(file): - """ - Get subject from BIDS file name - :param file: - :return: subject - """ - return file.split('_')[0] - - -def get_contrast(file): - """ - Get contrast from BIDS file name - :param file: - :return: - """ - return 'dwi' if (file.split('_')[-1]).split('.')[0] == 'dwi' else 'anat' +def fetch_subject_and_session(filename_path): + """ + Get subject ID, session ID and filename from the input BIDS-compatible filename or file path + The function works both on absolute file path as well as filename + :param filename_path: input nifti filename (e.g., sub-001_ses-01_T1w.nii.gz) or file path + (e.g., /home/user/MRI/bids/derivatives/labels/sub-001/ses-01/anat/sub-001_ses-01_T1w.nii.gz + :return: subjectID: subject ID (e.g., sub-001) + :return: sessionID: session ID (e.g., ses-01) + :return: filename: nii filename (e.g., sub-001_ses-01_T1w.nii.gz) + """ + + _, filename = os.path.split(filename_path) # Get just the filename (i.e., remove the path) + subject = re.search('sub-(.*?)[_/]', filename_path) + 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] if session else "" # Return None if there is no session + contrast = 'dwi' if 'dwi' in filename_path else 'anat' # Return contrast (dwi or anat) + # REGEX explanation + # \d - digit + # \d? - no or one occurrence of digit + # *? - match the previous element as few times as possible (zero or more times) + + return subjectID, sessionID, filename, contrast class SmartFormatter(argparse.HelpFormatter): From daf74dcd7ed889f84b326533ff5fdae3df79757c Mon Sep 17 00:00:00 2001 From: valosekj Date: Thu, 19 Jan 2023 13:39:58 -0500 Subject: [PATCH 11/41] Simplify code using fetch_subject_and_session --- manual_correction.py | 55 +++++++++++++++++++------------------------- 1 file changed, 24 insertions(+), 31 deletions(-) diff --git a/manual_correction.py b/manual_correction.py index e414c53..2283946 100644 --- a/manual_correction.py +++ b/manual_correction.py @@ -334,41 +334,34 @@ def main(): if files is not None: for file in files: # build file names - subject = file.split('_')[0] - contrast = utils.get_contrast(file) - fname = os.path.join(args.path_in, subject, contrast, file) - fname_label = os.path.join( - path_out_deriv, subject, contrast, utils.add_suffix(file, get_suffix(task, '-manual'))) - os.makedirs(os.path.join(path_out_deriv, subject, contrast), exist_ok=True) + subject, ses, filename, contrast = utils.fetch_subject_and_session(file) + # Construct absolute path to the input file + # For example: '/Users/user/dataset/data_processed/sub-001/anat/sub-001_T2w.nii.gz' + fname = os.path.join(utils.get_full_path(args.path_in), subject, ses, contrast, filename) + # 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' + fname_seg = utils.add_suffix(fname, suffix_dict[task]) + # Construct absolute path to the derivative 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-manual.nii.gz' + fname_label = os.path.join(path_out_deriv, subject, ses, contrast, + utils.add_suffix(utils.remove_suffix(filename, args.suffix_files_in), + suffix_dict[task] + '-manual')) + # Create output folders under derivative if they do not exist + os.makedirs(os.path.join(path_out_deriv, subject, ses, contrast), exist_ok=True) if not args.qc_only: - if os.path.isfile(fname_label): - # if corrected file already exists, asks user if they want to overwrite it - answer = None - while answer not in ("y", "n"): - answer = input("WARNING! The file {} already exists. " - "Would you like to modify it? [y/n] ".format(fname_label)) - if answer == "y": - do_labeling = True - overwrite = False - elif answer == "n": - do_labeling = False - else: - print("Please answer with 'y' or 'n'") - else: - do_labeling = True - overwrite = True - # Perform labeling for the specific task + # Check if file under derivatives already exists. If so, asks user if they want to modify it. + do_labeling, overwrite = ask_if_modify(fname_label) + # Perform labeling (i.e., segmentation correction, labeling correction etc.) for the specific task if do_labeling: - if task in ['FILES_SEG']: - fname_seg = utils.add_suffix(fname, get_suffix(task)) - if overwrite: - shutil.copyfile(fname_seg, fname_label) + if overwrite: + # Copy file to derivatives folder + shutil.copyfile(fname_seg, fname_label) + print(f'Copying: {fname_seg} to {fname_label}') + if task in ['FILES_SEG', 'FILES_GMSEG']: if not args.add_seg_only: - correct_segmentation(fname, fname_label) + correct_segmentation(fname, fname_label, args.viewer) elif task == 'FILES_LABEL': - if not utils.check_software_installed(): - sys.exit("Some required software are not installed. Exit program.") - correct_vertebral_labeling(fname, fname_label) + correct_vertebral_labeling(fname, fname_label, args.label_list) elif task == 'FILES_PMJ': if not utils.check_software_installed(): sys.exit("Some required software are not installed. Exit program.") From 6a1147a34a78caada361d4e0a1a6cdf2c03571d1 Mon Sep 17 00:00:00 2001 From: valosekj Date: Thu, 19 Jan 2023 13:41:22 -0500 Subject: [PATCH 12/41] Introduce suffix_dict (which is built from suffixes passed by the user). --- manual_correction.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/manual_correction.py b/manual_correction.py index 2283946..5c82723 100644 --- a/manual_correction.py +++ b/manual_correction.py @@ -304,6 +304,14 @@ def main(): # check for missing files before starting the whole process utils.check_files_exist(dict_yml, args.path_in) + suffix_dict = { + 'FILES_SEG': args.suffix_files_seg, # e.g., _seg or _label-SC_mask + 'FILES_GMSEG': args.suffix_files_gmseg, # e.g., _gmseg or _label-GM_mask + 'FILES_LABEL': args.suffix_files_label, # e.g., _labels or _labels-disc + 'FILES_PMJ': '_pmj' + } + + path_out = utils.get_full_path(args.path_out) # check that output folder exists and has write permission path_out_deriv = utils.check_output_folder(args.path_out, FOLDER_DERIVATIVES) From 0f010c1e16b7ad99cfcda6336444c6540360f1f6 Mon Sep 17 00:00:00 2001 From: valosekj Date: Thu, 19 Jan 2023 13:45:06 -0500 Subject: [PATCH 13/41] Update paths for several variables --- manual_correction.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/manual_correction.py b/manual_correction.py index 5c82723..e968797 100644 --- a/manual_correction.py +++ b/manual_correction.py @@ -301,8 +301,8 @@ def main(): # Curate dict_yml to only have filenames instead of absolute path dict_yml = utils.curate_dict_yml(dict_yml) - # check for missing files before starting the whole process - utils.check_files_exist(dict_yml, args.path_in) + # Check for missing files before starting the whole process + utils.check_files_exist(dict_yml, utils.get_full_path(args.path_in)) suffix_dict = { 'FILES_SEG': args.suffix_files_seg, # e.g., _seg or _label-SC_mask @@ -313,7 +313,7 @@ def main(): path_out = utils.get_full_path(args.path_out) # check that output folder exists and has write permission - path_out_deriv = utils.check_output_folder(args.path_out, FOLDER_DERIVATIVES) + path_out_deriv = utils.check_output_folder(path_out, args.path_derivatives) # Get name of expert rater (skip if -qc-only is true) if not args.qc_only: @@ -321,7 +321,7 @@ def main(): "corrected file: ") # Build QC report folder name - fname_qc = 'qc_corr_' + time.strftime('%Y%m%d%H%M%S') + fname_qc = os.path.join(path_out, 'qc_corr_' + time.strftime('%Y%m%d%H%M%S')) # Get list of segmentations files for all subjects in -path-in (if -add-seg-only) if args.add_seg_only: @@ -371,8 +371,6 @@ def main(): elif task == 'FILES_LABEL': correct_vertebral_labeling(fname, fname_label, args.label_list) elif task == 'FILES_PMJ': - if not utils.check_software_installed(): - sys.exit("Some required software are not installed. Exit program.") correct_pmj_label(fname, fname_label) else: sys.exit('Task not recognized from yml file: {}'.format(task)) @@ -382,9 +380,9 @@ def main(): # generate QC report (only for vertebral labeling or for qc only) if args.qc_only or task != 'FILES_SEG': os.system('sct_qc -i {} -s {} -p {} -qc {} -qc-subject {}'.format( - fname, fname_label, get_function(task), fname_qc, subject)) + fname, fname_label, get_function_for_qc(task), fname_qc, subject)) # Archive QC folder - shutil.copy(fname_yml, fname_qc) + shutil.copy(utils.get_full_path(args.config), fname_qc) shutil.make_archive(fname_qc, 'zip', fname_qc) print("Archive created:\n--> {}".format(fname_qc+'.zip')) From ab791d46c940a4e997e1fbf9a33b3ca6a7c893cd Mon Sep 17 00:00:00 2001 From: valosekj Date: Thu, 19 Jan 2023 13:46:14 -0500 Subject: [PATCH 14/41] Allow to specify '-suffix-files-seg' ('_seg' or '_label-SC_mask') --- manual_correction.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/manual_correction.py b/manual_correction.py index e968797..647e670 100644 --- a/manual_correction.py +++ b/manual_correction.py @@ -325,9 +325,10 @@ def main(): # Get list of segmentations files for all subjects in -path-in (if -add-seg-only) if args.add_seg_only: - path_list = glob.glob(args.path_in + "/**/*_seg.nii.gz", recursive=True) # TODO: add other extension + path_list = glob.glob(args.path_in + "/**/*" + args.suffix_files_seg + ".nii.gz", recursive=True) # Get only filenames without suffix _seg to match files in -config .yml list - file_list = [utils.remove_suffix(os.path.split(path)[-1], '_seg') for path in path_list] + # TODO: check if the line below is robust enough + file_list = [utils.remove_suffix(os.path.split(path)[-1], args.suffix_files_seg) for path in path_list] # TODO: address "none" issue if no file present under a key # Perform manual corrections From 3d1d81c980742c977183071c63d055a973f787ca Mon Sep 17 00:00:00 2001 From: valosekj Date: Thu, 19 Jan 2023 13:46:26 -0500 Subject: [PATCH 15/41] Add TODOs --- manual_correction.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/manual_correction.py b/manual_correction.py index 647e670..3a2dd6c 100644 --- a/manual_correction.py +++ b/manual_correction.py @@ -290,6 +290,7 @@ def main(): args = parser.parse_args() # Logging level + # TODO: how is this actually used? if args.verbose: coloredlogs.install(fmt='%(message)s', level='DEBUG') else: @@ -333,7 +334,8 @@ 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 corrrected files in -config. + # 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: From f89a9aceca97c7dc6aedf35e3aa950eb8da7ad91 Mon Sep 17 00:00:00 2001 From: valosekj Date: Thu, 19 Jan 2023 13:47:51 -0500 Subject: [PATCH 16/41] Allow to specify '-suffix-files-seg', '-suffix-files-gmseg', and '-suffix-files-label' to build the 'suffix_dict'. --- package_for_correction.py | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/package_for_correction.py b/package_for_correction.py index bc2d561..652f0e3 100644 --- a/package_for_correction.py +++ b/package_for_correction.py @@ -69,6 +69,24 @@ def get_parser(): help="Zip file that contains the packaged data, without the extension. Default: data_to_correct", default='data_to_correct' ) + parser.add_argument( + '-suffix-files-seg', + help="FILES-SEG suffix. Available options: '_seg' (default), '_label-SC_mask'.", + choices=['_seg', '_label-SC_mask'], + default='_seg' + ) + parser.add_argument( + '-suffix-files-gmseg', + help="FILES-GMSEG suffix. Available options: '_gmseg' (default), '_label-GM_mask'.", + choices=['_gmseg', '_label-GM_mask'], + default='_gmseg' + ) + parser.add_argument( + '-suffix-files-label', + help="FILES-LABEL suffix. Available options: '_labels' (default), '_labels-disc'.", + choices=['_labels', '_labels-disc'], + default='_labels' + ) parser.add_argument( '-v', '--verbose', help="Full verbose (for debugging)", @@ -104,7 +122,14 @@ def main(): dict_yml = utils.curate_dict_yml(dict_yml) # Check for missing files before starting the whole process - utils.check_files_exist(dict_yml, args.path_in) + utils.check_files_exist(dict_yml, utils.get_full_path(args.path_in)) + + suffix_dict = { + 'FILES_SEG': args.suffix_files_seg, # e.g., _seg or _label-SC_mask + 'FILES_GMSEG': args.suffix_files_gmseg, # e.g., _gmseg or _label-GM_mask + 'FILES_LABEL': args.suffix_files_label, # e.g., _labels or _labels-disc + 'FILES_PMJ': '_pmj' + } # Create temp folder path_tmp = tempfile.mkdtemp() @@ -113,12 +138,8 @@ def main(): # Note: in case the file is listed twice, we just overwrite it in the destination dir. for task, files in dict_yml.items(): for file in files: - if task == 'FILES_SEG': - suffix_label = '_seg' - elif task == 'FILES_LABEL': - suffix_label = None - elif task == 'FILES_PMJ': - suffix_label = None + if task in suffix_dict.keys(): + suffix_label = suffix_dict[task] else: sys.exit('Task not recognized from yml file: {}'.format(task)) # Copy image From 9060aaa4f418936442c6e752848421b8074ae13d Mon Sep 17 00:00:00 2001 From: valosekj Date: Thu, 19 Jan 2023 13:48:21 -0500 Subject: [PATCH 17/41] Construct paths using fetch_subject_and_session --- package_for_correction.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/package_for_correction.py b/package_for_correction.py index 652f0e3..8ef23c3 100644 --- a/package_for_correction.py +++ b/package_for_correction.py @@ -142,14 +142,20 @@ def main(): suffix_label = suffix_dict[task] else: sys.exit('Task not recognized from yml file: {}'.format(task)) + subject, ses, filename, contrast = utils.fetch_subject_and_session(file) + # Construct absolute path to the input file + # For example: '/Users/user/dataset/data_processed/sub-001/anat/sub-001_T2w.nii.gz' + fname = os.path.join(utils.get_full_path(args.path_in), subject, ses, contrast, filename) + # Construct absolute path to the temp folder + path_out = os.path.join(path_tmp, subject, ses, contrast) # Copy image - copy_file(os.path.join(args.path_in, utils.get_subject(file), utils.get_contrast(file), file), - os.path.join(path_tmp, utils.get_subject(file), utils.get_contrast(file))) + copy_file(fname, path_out) # Copy label if exists if suffix_label is not None: - copy_file(os.path.join(args.path_in, utils.get_subject(file), utils.get_contrast(file), - utils.add_suffix(file, suffix_label)), - os.path.join(path_tmp, utils.get_subject(file), utils.get_contrast(file))) + # 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' + fname_seg = utils.add_suffix(fname, suffix_dict[task]) + copy_file(fname_seg, path_out) # Package to zip file print("Creating archive...") @@ -159,7 +165,7 @@ def main(): if os.path.isdir(new_path_tmp): shutil.rmtree(new_path_tmp) shutil.move(path_tmp, new_path_tmp) - fname_archive = shutil.make_archive(args.o, 'zip', root_dir_tmp, base_dir_name) + fname_archive = shutil.make_archive(utils.get_full_path(args.o), 'zip', root_dir_tmp, base_dir_name) print("-> {}".format(fname_archive)) From b987153c2924760c45d994572568fc5c5736c6ed Mon Sep 17 00:00:00 2001 From: valosekj Date: Thu, 19 Jan 2023 13:49:29 -0500 Subject: [PATCH 18/41] Extend info message printed to CLI during file copying --- package_for_correction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package_for_correction.py b/package_for_correction.py index 8ef23c3..d4d2abe 100644 --- a/package_for_correction.py +++ b/package_for_correction.py @@ -101,7 +101,7 @@ def copy_file(fname_in, path_out): os.makedirs(path_out, exist_ok=True) # copy file fname_out = shutil.copy(fname_in, path_out) - print("-> {}".format(fname_out)) + print(f'Copying: {fname_in} to {fname_out}') def main(): From 7a3c18a140cfd865d28ecd4496ee626d7eec6825 Mon Sep 17 00:00:00 2001 From: valosekj Date: Thu, 19 Jan 2023 13:50:04 -0500 Subject: [PATCH 19/41] Use fetch_subject_and_session inside check_files_exist function --- utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/utils.py b/utils.py index d56fb63..90b50a5 100644 --- a/utils.py +++ b/utils.py @@ -203,7 +203,8 @@ def check_files_exist(dict_files, path_data): for task, files in dict_files.items(): if files is not None: for file in files: - fname = os.path.join(path_data, get_subject(file), get_contrast(file), file) + subject, ses, filename, contrast = fetch_subject_and_session(file) + fname = os.path.join(path_data, subject, ses, contrast, filename) if not os.path.exists(fname): missing_files.append(fname) if missing_files: From 43975fa7592df70dd14fa92836a630a333ae52ef Mon Sep 17 00:00:00 2001 From: valosekj Date: Thu, 19 Jan 2023 13:50:20 -0500 Subject: [PATCH 20/41] Description update --- utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utils.py b/utils.py index 90b50a5..823150a 100644 --- a/utils.py +++ b/utils.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 # -# Collection of useful functions from spine_generic, used for manual segmentation +# Collection of useful functions used by other scripts # # Authors: Jan Valosek, Sandrine Bédard, Julien Cohen-Adad # @@ -216,7 +216,7 @@ def check_output_folder(path_bids, folder_derivatives): """ Make sure path exists, has writing permissions, and create derivatives folder if it does not exist. :param path_bids: - :return: path_bids_derivatives + :return: folder_derivatives: """ if path_bids is None: logging.error("-path-out should be provided.") From 2cf35b28b4c745cf1457c5332a964febd1ad01e5 Mon Sep 17 00:00:00 2001 From: valosekj Date: Thu, 19 Jan 2023 13:53:19 -0500 Subject: [PATCH 21/41] Add copy_files_to_derivatives.py script. The script allows copying of manually corrected labels (segmentations, vertebral labeling, etc.) from the preprocessed dataset to the git-annex BIDS dataset's derivatives folder --- copy_files_to_derivatives.py | 86 ++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 copy_files_to_derivatives.py diff --git a/copy_files_to_derivatives.py b/copy_files_to_derivatives.py new file mode 100644 index 0000000..2f95249 --- /dev/null +++ b/copy_files_to_derivatives.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python +# +# Copy manually corrected labels (segmentations, vertebral labeling, etc.) from the preprocessed dataset to the +# git-annex BIDS dataset's derivatives folder +# +# Authors: Jan Valosek +# + +import argparse +import glob +import os +import shutil +import utils + + +def get_parser(): + """ + parser function + """ + + parser = argparse.ArgumentParser( + description='Copy manually corrected files (segmentations, vertebral labeling, etc.) from the source ' + 'preprocessed dataset to the git-annex BIDS derivatives folder', + formatter_class=utils.SmartFormatter, + prog=os.path.basename(__file__).strip('.py') + ) + parser.add_argument( + '-path-in', + metavar="", + required=True, + type=str, + help='Path to the folder with manually corrected files (usually derivatives). The script assumes that labels ' + 'folder is located in the provided folder.' + ) + parser.add_argument( + '-path-out', + metavar="", + required=True, + type=str, + help='Path to the BIDS dataset where manually corrected files will be copied. Include also derivatives folder ' + 'in the path. Files will be copied to the derivatives/label folder.' + ) + + return parser + + +def main(): + + # Parse the command line arguments + parser = get_parser() + args = parser.parse_args() + + # Check if path_in exists + if os.path.isdir(args.path_in): + path_in = os.path.join(os.path.abspath(args.path_in), 'labels') + else: + raise NotADirectoryError(f'{args.path_in} does not exist.') + + # Check if path_out exists + if os.path.isdir(args.path_out): + path_out = os.path.join(os.path.abspath(args.path_out), 'labels') + else: + raise NotADirectoryError(f'{args.path_out} does not exist.') + + # Loop across files in input dataset + for path_file_in in sorted(glob.glob(path_in + '/**/*.nii.gz', recursive=True)): + sub, ses, filename, contrast = utils.fetch_subject_and_session(path_file_in) + # Construct path for the output file + path_file_out = os.path.join(path_out, sub, ses, contrast, filename) + # Check if subject's folder exists in the output dataset, if not, create it + path_subject_folder_out = os.path.join(path_out, sub, ses, contrast) + if not os.path.isdir(path_subject_folder_out): + os.makedirs(path_subject_folder_out) + print(f'Creating directory: {path_subject_folder_out}') + # Copy nii and json files to the output dataset + # TODO - consider rsync instead of shutil.copy + shutil.copy(path_file_in, path_file_out) + print(f'Copying: {path_file_in} to {path_file_out}') + path_file_json_in = path_file_in.replace('nii.gz', 'json') + path_file_json_out = path_file_out.replace('nii.gz', 'json') + shutil.copy(path_file_json_in, path_file_json_out) + print(f'Copying: {path_file_json_in} to {path_file_json_out}') + + +if __name__ == '__main__': + main() From 57b5e1197dcffb14eb7decb454b58c85cf7049b5 Mon Sep 17 00:00:00 2001 From: valosekj Date: Thu, 19 Jan 2023 13:53:29 -0500 Subject: [PATCH 22/41] Add .gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..df656a8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +.idea +*/__pycache__/ +venv/ From a0e1b93df016ff0c46f1fbcaed0f08d54088018a Mon Sep 17 00:00:00 2001 From: valosekj Date: Thu, 19 Jan 2023 14:22:03 -0500 Subject: [PATCH 23/41] Add requirements.txt --- requirements.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..61ac723 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +coloredlogs==15.0.1 +humanfriendly==10.0 +PyYAML==6.0 From a200aaa9e0ebaf3423aabad31b4c507d832ca962 Mon Sep 17 00:00:00 2001 From: valosekj Date: Thu, 19 Jan 2023 15:17:52 -0500 Subject: [PATCH 24/41] Move QC generation to separate function (generate_qc) and do it also for SC seg. --- manual_correction.py | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/manual_correction.py b/manual_correction.py index 3a2dd6c..2acef1a 100644 --- a/manual_correction.py +++ b/manual_correction.py @@ -283,6 +283,24 @@ def ask_if_modify(fname_label): return do_labeling, overwrite +def generate_qc(fname, fname_label, task, fname_qc, subject, config_file): + """ + Generate QC report. + :param fname: + :param fname_seg: + :param fname_label: + :param fname_pmj: + :param qc_folder: + :return: + """ + os.system('sct_qc -i {} -s {} -p {} -qc {} -qc-subject {}'.format( + fname, fname_label, get_function_for_qc(task), fname_qc, subject)) + # Archive QC folder + shutil.copy(utils.get_full_path(config_file), fname_qc) + shutil.make_archive(fname_qc, 'zip', fname_qc) + print("Archive created:\n--> {}".format(fname_qc + '.zip')) + + def main(): # Parse the command line arguments @@ -379,15 +397,12 @@ def main(): sys.exit('Task not recognized from yml file: {}'.format(task)) # create json sidecar with the name of the expert rater create_json(fname_label, name_rater) + # Generate QC report + generate_qc(fname, fname_label, task, fname_qc, subject, args.config) - # generate QC report (only for vertebral labeling or for qc only) - if args.qc_only or task != 'FILES_SEG': - os.system('sct_qc -i {} -s {} -p {} -qc {} -qc-subject {}'.format( - fname, fname_label, get_function_for_qc(task), fname_qc, subject)) - # Archive QC folder - shutil.copy(utils.get_full_path(args.config), fname_qc) - shutil.make_archive(fname_qc, 'zip', fname_qc) - print("Archive created:\n--> {}".format(fname_qc+'.zip')) + # Generate QC report only + if args.qc_only: + generate_qc(fname, fname_label, task, fname_qc, subject, args.config) if __name__ == '__main__': From 72e7e1bb34a79f163de6d5ff707e98a378a395ec Mon Sep 17 00:00:00 2001 From: valosekj Date: Thu, 19 Jan 2023 15:18:16 -0500 Subject: [PATCH 25/41] README.md update --- README.md | 45 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 68b0f28..9b8d9c5 100644 --- a/README.md +++ b/README.md @@ -1 +1,44 @@ -# manual-correction \ No newline at end of file +# Manual correction + +This repository contains scripts for the manual correction of spinal cord labels. Currently supported labels are: +- spinal cord segmentation +- gray matter segmentation +- disc labels +- ponto-medullary junction (PMJ) label + +## Installation + +```console +git clone https://github.com/spinalcordtoolbox/manual-correction.git +cd manual-correction +python3 -m venv venv +source venv/bin/activate +pip install -r requirements.txt +``` + +## Usage + +> **Note** All scripts currently assume BIDS-compliant data. For more information about the BIDS standard, please visit http://bids.neuroimaging.io. + +### `package_for_correction.py` + +The `package_for_correction.py` script is used to create a zip file containing the processed images and labels +(segmentations, disc labels, etc.). The zip file is then sent to the user for manual correction. + +This is useful when you need to correct the labels of a large dataset processed on a remote server. In this case, you do +not need to copy the whole dataset. Instead, only the images and labels that need to be corrected are zipped. The yaml +list of images to correct can be obtained from the [SCT QC html report](https://spinalcordtoolbox.com/overview/concepts/inspecting-results-qc-fsleyes.html#how-do-i-use-the-qc-report). + +### `manual_correction.py` + +The `manual_correction.py` script is used to correct the labels (segmentations, disc labels, etc.) of a dataset. +The script takes as input a processed dataset and outputs the corrected labels to `derivatives/labels` folder. + +For the correction of spinal cord and gray matter segmentation, you can choose a viewer ([FSLeyes](https://open.win.ox.ac.uk/pages/fsl/fsleyes/fsleyes/userdoc/#), [ITK-SNAP](http://www.itksnap.org/pmwiki/pmwiki.php), [3D Slicer](https://www.slicer.org)). + +For the correction of vertebral labeling and ponto-medullary junction (PMJ), [sct_label_utils](https://github.com/spinalcordtoolbox/spinalcordtoolbox/blob/master/spinalcordtoolbox/scripts/sct_label_utils.py) is used. + +### `copy_files_to_derivatives.py` + +The `copy_files_to_derivatives.py` script is used to copy manually corrected labels (segmentations, disc labels, etc.) +from your local `derivatives/labels` folder to the already existing git-annex BIDS dataset's `derivatives/labels` folder. \ No newline at end of file From 8bc0b05d4a171cbd329e1fd3ca95ef0fbc4c1bc0 Mon Sep 17 00:00:00 2001 From: valosekj Date: Mon, 23 Jan 2023 17:29:44 -0500 Subject: [PATCH 26/41] Do not check for missing files for 'add-seg-only' flag --- manual_correction.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manual_correction.py b/manual_correction.py index 2acef1a..359ffa1 100644 --- a/manual_correction.py +++ b/manual_correction.py @@ -321,7 +321,8 @@ def main(): dict_yml = utils.curate_dict_yml(dict_yml) # Check for missing files before starting the whole process - utils.check_files_exist(dict_yml, utils.get_full_path(args.path_in)) + if not args.add_seg_only: + utils.check_files_exist(dict_yml, utils.get_full_path(args.path_in)) suffix_dict = { 'FILES_SEG': args.suffix_files_seg, # e.g., _seg or _label-SC_mask From 8cfc81f65eae5f7cbd4405cb7c138275ef9f34c9 Mon Sep 17 00:00:00 2001 From: valosekj Date: Mon, 23 Jan 2023 17:31:04 -0500 Subject: [PATCH 27/41] Remove TODO - code is robust enough --- manual_correction.py | 1 - 1 file changed, 1 deletion(-) diff --git a/manual_correction.py b/manual_correction.py index 359ffa1..72698db 100644 --- a/manual_correction.py +++ b/manual_correction.py @@ -347,7 +347,6 @@ def main(): if args.add_seg_only: path_list = glob.glob(args.path_in + "/**/*" + args.suffix_files_seg + ".nii.gz", recursive=True) # Get only filenames without suffix _seg to match files in -config .yml list - # TODO: check if the line below is robust enough file_list = [utils.remove_suffix(os.path.split(path)[-1], args.suffix_files_seg) for path in path_list] # TODO: address "none" issue if no file present under a key From 805f55612a1c4ac83480e2534639b2d36bd30121 Mon Sep 17 00:00:00 2001 From: valosekj Date: Mon, 23 Jan 2023 17:31:37 -0500 Subject: [PATCH 28/41] Allow to remove the file suffix (e.g., '_RPI_r') for 'add-seg-only' flag --- manual_correction.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manual_correction.py b/manual_correction.py index 72698db..57e29de 100644 --- a/manual_correction.py +++ b/manual_correction.py @@ -357,6 +357,8 @@ def main(): 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-in + 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 From 6aae8e96ca47048d2d60f88f65615c80a1d76ccb Mon Sep 17 00:00:00 2001 From: valosekj Date: Wed, 25 Jan 2023 16:13:13 -0500 Subject: [PATCH 29/41] Allow lesion correction using FILES_LESION --- manual_correction.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/manual_correction.py b/manual_correction.py index 57e29de..d3a45fc 100644 --- a/manual_correction.py +++ b/manual_correction.py @@ -105,6 +105,12 @@ def get_parser(): choices=['_gmseg', '_label-GM_mask'], default='_gmseg' ) + parser.add_argument( + '-suffix-files-lesion', + help="FILES-LESION suffix. Available options: '_lesion' (default).", + choices=['_lesion'], + default='_lesion' + ) parser.add_argument( '-suffix-files-label', help="FILES-LABEL suffix. Available options: '_labels' (default), '_labels-disc'.", @@ -327,6 +333,7 @@ def main(): suffix_dict = { 'FILES_SEG': args.suffix_files_seg, # e.g., _seg or _label-SC_mask 'FILES_GMSEG': args.suffix_files_gmseg, # e.g., _gmseg or _label-GM_mask + 'FILES_LESION': args.suffix_files_lesion, # e.g., _lesion 'FILES_LABEL': args.suffix_files_label, # e.g., _labels or _labels-disc 'FILES_PMJ': '_pmj' } @@ -391,6 +398,8 @@ def main(): if task in ['FILES_SEG', 'FILES_GMSEG']: if not args.add_seg_only: correct_segmentation(fname, fname_label, args.viewer) + elif task == 'FILES_LESION': + correct_segmentation(fname, fname_label, args.viewer) elif task == 'FILES_LABEL': correct_vertebral_labeling(fname, fname_label, args.label_list) elif task == 'FILES_PMJ': @@ -404,7 +413,9 @@ def main(): # Generate QC report only if args.qc_only: - generate_qc(fname, fname_label, task, fname_qc, subject, args.config) + # Note: QC for lesion segmentation is not implemented yet + if task != "FILES_LESION": + generate_qc(fname, fname_label, task, fname_qc, subject, args.config) if __name__ == '__main__': From b29c882ee79fe6c35b6ca6df8ea006243d4b33f5 Mon Sep 17 00:00:00 2001 From: valosekj Date: Wed, 25 Jan 2023 16:13:45 -0500 Subject: [PATCH 30/41] Rename overwrite to copy (for clarity) --- manual_correction.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/manual_correction.py b/manual_correction.py index d3a45fc..3d3946a 100644 --- a/manual_correction.py +++ b/manual_correction.py @@ -276,17 +276,18 @@ def ask_if_modify(fname_label): "Would you like to modify it? [y/n] ".format(fname_label)) if answer == "y": do_labeling = True - overwrite = False elif answer == "n": do_labeling = False - overwrite = False else: print("Please answer with 'y' or 'n'") + # We don't want to copy because we want to modify the existing file + copy = False + # If the file under derivatives does not exist, copy it from processed data else: do_labeling = True - overwrite = True + copy = True - return do_labeling, overwrite + return do_labeling, copy def generate_qc(fname, fname_label, task, fname_qc, subject, config_file): @@ -388,10 +389,10 @@ def main(): os.makedirs(os.path.join(path_out_deriv, subject, ses, contrast), exist_ok=True) if not args.qc_only: # Check if file under derivatives already exists. If so, asks user if they want to modify it. - do_labeling, overwrite = ask_if_modify(fname_label) + do_labeling, copy = ask_if_modify(fname_label) # Perform labeling (i.e., segmentation correction, labeling correction etc.) for the specific task if do_labeling: - if overwrite: + if copy: # Copy file to derivatives folder shutil.copyfile(fname_seg, fname_label) print(f'Copying: {fname_seg} to {fname_label}') From 7a9dae87bbbdce97149c7b0aa78ffdfe9250db50 Mon Sep 17 00:00:00 2001 From: valosekj Date: Wed, 25 Jan 2023 16:13:55 -0500 Subject: [PATCH 31/41] Clarify comments --- manual_correction.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manual_correction.py b/manual_correction.py index 3d3946a..bb795bf 100644 --- a/manual_correction.py +++ b/manual_correction.py @@ -266,9 +266,10 @@ def create_json(fname_nifti, name_rater): def ask_if_modify(fname_label): """ Check if file under derivatives already exists. If so, asks user if they want to modify it. - :param fname_label: + :param fname_label: file under derivatives :return: """ + # Check if file under derivatives already exists if os.path.isfile(fname_label): answer = None while answer not in ("y", "n"): From 8f1ffb862ead7df3e56fc0f49c1c35a168d8aa3e Mon Sep 17 00:00:00 2001 From: Naga Karthik Date: Thu, 26 Jan 2023 22:32:20 -0500 Subject: [PATCH 32/41] add args to specify color for fsleyes viewer --- manual_correction.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/manual_correction.py b/manual_correction.py index bb795bf..e4cb30b 100644 --- a/manual_correction.py +++ b/manual_correction.py @@ -129,6 +129,13 @@ def get_parser(): choices=['fsleyes', 'itksnap', 'slicer'], default='itksnap' ) + parser.add_argument( + '-fsl-color', + help="Color to be used for loading the label file on FSLeyes (default: red). `fsleyes -h` gives all the available color options. " + "If using a combination of colors, specify them with '-'. E.g. red-yellow ", + type=str, + default='red' + ) parser.add_argument( '-qc-only', help="Only output QC report based on the manually-corrected files already present in the derivatives folder. " @@ -169,7 +176,7 @@ def get_function_for_qc(task): raise ValueError("This task is not recognized: {}".format(task)) -def correct_segmentation(fname, fname_seg_out, viewer): +def correct_segmentation(fname, fname_seg_out, viewer, viewer_color): """ Open viewer (ITK-SNAP, FSLeyes, or 3D Slicer) with fname and fname_seg_out. :param fname: @@ -194,7 +201,7 @@ def correct_segmentation(fname, fname_seg_out, viewer): if shutil.which('fsleyes') is not None: # Check if command 'fsleyes' exists print("In FSLeyes, click on 'Edit mode', correct the segmentation, and then save it with the same name " "(overwrite).") - os.system('fsleyes {} {} -cm red'.format(fname, fname_seg_out)) + os.system('fsleyes {} {} -cm {}'.format(fname, fname_seg_out, viewer_color)) else: viewer_not_found(viewer) # launch 3D Slicer @@ -399,9 +406,9 @@ def main(): print(f'Copying: {fname_seg} to {fname_label}') if task in ['FILES_SEG', 'FILES_GMSEG']: if not args.add_seg_only: - correct_segmentation(fname, fname_label, args.viewer) + correct_segmentation(fname, fname_label, args.viewer, args.fsl_color) elif task == 'FILES_LESION': - correct_segmentation(fname, fname_label, args.viewer) + correct_segmentation(fname, fname_label, args.viewer, args.fsl_color) elif task == 'FILES_LABEL': correct_vertebral_labeling(fname, fname_label, args.label_list) elif task == 'FILES_PMJ': From f704feca59e689d6de214ce0c88cf9dc5c51bafc Mon Sep 17 00:00:00 2001 From: Naga Karthik Date: Thu, 26 Jan 2023 22:34:00 -0500 Subject: [PATCH 33/41] minor fix to not generate QC for FILES_LESION --- manual_correction.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/manual_correction.py b/manual_correction.py index e4cb30b..b791435 100644 --- a/manual_correction.py +++ b/manual_correction.py @@ -415,10 +415,16 @@ def main(): correct_pmj_label(fname, fname_label) else: sys.exit('Task not recognized from yml file: {}'.format(task)) - # create json sidecar with the name of the expert rater - create_json(fname_label, name_rater) - # Generate QC report - generate_qc(fname, fname_label, task, fname_qc, subject, args.config) + + if task == 'FILES_LESION': + # create json sidecar with the name of the expert rater + create_json(fname_label, name_rater) + # NOTE: QC for lesion segmentation does not exist or not implemented yet + else: + # create json sidecar with the name of the expert rater + create_json(fname_label, name_rater) + # Generate QC report + generate_qc(fname, fname_label, task, fname_qc, subject, args.config) # Generate QC report only if args.qc_only: From 65acb5ff07a7cce1e2b9781b8df1d86f940dbc9e Mon Sep 17 00:00:00 2001 From: valosekj Date: Fri, 27 Jan 2023 07:05:54 -0500 Subject: [PATCH 34/41] Add Naga Karthik as a script co-author. --- manual_correction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manual_correction.py b/manual_correction.py index b791435..70e5cbb 100644 --- a/manual_correction.py +++ b/manual_correction.py @@ -5,7 +5,7 @@ # # For usage, type: python manual_correction.py -h # -# Authors: Jan Valosek, Sandrine Bédard, Julien Cohen-Adad +# Authors: Jan Valosek, Sandrine Bédard, Naga Karthik, Julien Cohen-Adad # import argparse From c7f1f7ec0476f7a67ad35184bee2121a123d96d5 Mon Sep 17 00:00:00 2001 From: valosekj Date: Fri, 27 Jan 2023 09:45:59 -0500 Subject: [PATCH 35/41] Improve help formatting. Add docstring. --- manual_correction.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/manual_correction.py b/manual_correction.py index 70e5cbb..0b447af 100644 --- a/manual_correction.py +++ b/manual_correction.py @@ -131,8 +131,8 @@ def get_parser(): ) parser.add_argument( '-fsl-color', - help="Color to be used for loading the label file on FSLeyes (default: red). `fsleyes -h` gives all the available color options. " - "If using a combination of colors, specify them with '-'. E.g. red-yellow ", + help="Color to be used for loading the label file on FSLeyes (default: red). `fsleyes -h` gives all the " + "available color options. If using a combination of colors, specify them with '-', e.g. 'red-yellow'.", type=str, default='red' ) @@ -182,6 +182,7 @@ def correct_segmentation(fname, fname_seg_out, viewer, viewer_color): :param fname: :param fname_seg_out: :param viewer: + :param viewer_color: color to be used for the label. Only valid for on FSLeyes (default: red). :return: """ # launch ITK-SNAP From bb3dee46ee5d0419fa4e71b46a2355b9eb67248e Mon Sep 17 00:00:00 2001 From: valosekj Date: Fri, 27 Jan 2023 14:17:13 -0500 Subject: [PATCH 36/41] Fix package_for_correction.py description --- package_for_correction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package_for_correction.py b/package_for_correction.py index d4d2abe..35e8875 100644 --- a/package_for_correction.py +++ b/package_for_correction.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -# Script to package data for manual correction from SpineGeneric adapted for canproco project. +# Script to package data for manual correction. # # For usage, type: python package_for_correction.py -h # From b08a72f4a3e880cc5d177514dc67c7c05b496985 Mon Sep 17 00:00:00 2001 From: valosekj Date: Fri, 27 Jan 2023 14:24:37 -0500 Subject: [PATCH 37/41] Add -suffix-files-pmj input flag --- manual_correction.py | 8 +++++++- package_for_correction.py | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/manual_correction.py b/manual_correction.py index 0b447af..3f91396 100644 --- a/manual_correction.py +++ b/manual_correction.py @@ -117,6 +117,12 @@ def get_parser(): choices=['_labels', '_labels-disc'], default='_labels' ) + parser.add_argument( + '-suffix-files-pmj', + help="FILES-PMJ suffix. Available options: '_pmj' (default), '_label-pmj'.", + choices=['_pmj', '_label-pmj'], + default='_pmj' + ) parser.add_argument( '-label-list', help="Provide a comma-separated list containing individual values and/or intervals. Example: '1:4,6,8' or 1:20 " @@ -345,7 +351,7 @@ def main(): 'FILES_GMSEG': args.suffix_files_gmseg, # e.g., _gmseg or _label-GM_mask 'FILES_LESION': args.suffix_files_lesion, # e.g., _lesion 'FILES_LABEL': args.suffix_files_label, # e.g., _labels or _labels-disc - 'FILES_PMJ': '_pmj' + 'FILES_PMJ': args.suffix_files_pmj # e.g., _pmj or _label-pmj } path_out = utils.get_full_path(args.path_out) diff --git a/package_for_correction.py b/package_for_correction.py index 35e8875..383651b 100644 --- a/package_for_correction.py +++ b/package_for_correction.py @@ -87,6 +87,12 @@ def get_parser(): choices=['_labels', '_labels-disc'], default='_labels' ) + parser.add_argument( + '-suffix-files-pmj', + help="FILES-PMJ suffix. Available options: '_pmj' (default), '_label-pmj'.", + choices=['_pmj', '_label-pmj'], + default='_pmj' + ) parser.add_argument( '-v', '--verbose', help="Full verbose (for debugging)", @@ -128,7 +134,7 @@ def main(): 'FILES_SEG': args.suffix_files_seg, # e.g., _seg or _label-SC_mask 'FILES_GMSEG': args.suffix_files_gmseg, # e.g., _gmseg or _label-GM_mask 'FILES_LABEL': args.suffix_files_label, # e.g., _labels or _labels-disc - 'FILES_PMJ': '_pmj' + 'FILES_PMJ': args.suffix_files_pmj # e.g., _pmj or _label-pmj } # Create temp folder From fe3b20370aca866f18ad666d83396106e8a52650 Mon Sep 17 00:00:00 2001 From: valosekj Date: Fri, 27 Jan 2023 14:27:15 -0500 Subject: [PATCH 38/41] Add -suffix-files-lesion input flag --- package_for_correction.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/package_for_correction.py b/package_for_correction.py index 383651b..ca8fc36 100644 --- a/package_for_correction.py +++ b/package_for_correction.py @@ -81,6 +81,12 @@ def get_parser(): choices=['_gmseg', '_label-GM_mask'], default='_gmseg' ) + parser.add_argument( + '-suffix-files-lesion', + help="FILES-LESION suffix. Available options: '_lesion' (default).", + choices=['_lesion'], + default='_lesion' + ) parser.add_argument( '-suffix-files-label', help="FILES-LABEL suffix. Available options: '_labels' (default), '_labels-disc'.", @@ -133,6 +139,7 @@ def main(): suffix_dict = { 'FILES_SEG': args.suffix_files_seg, # e.g., _seg or _label-SC_mask 'FILES_GMSEG': args.suffix_files_gmseg, # e.g., _gmseg or _label-GM_mask + 'FILES_LESION': args.suffix_files_lesion, # e.g., _lesion 'FILES_LABEL': args.suffix_files_label, # e.g., _labels or _labels-disc 'FILES_PMJ': args.suffix_files_pmj # e.g., _pmj or _label-pmj } From 0a7c527a39112dd0b1e7d0bda8d1bf1b054886a6 Mon Sep 17 00:00:00 2001 From: valosekj Date: Fri, 27 Jan 2023 18:29:36 -0500 Subject: [PATCH 39/41] Clarify -path-derivatives argument description --- manual_correction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manual_correction.py b/manual_correction.py index 3f91396..d7f4175 100644 --- a/manual_correction.py +++ b/manual_correction.py @@ -79,7 +79,7 @@ def get_parser(): metavar="", help= "R|Path to the 'derivatives' BIDS-complaint folder where the corrected labels will be saved. " - "Example: derivatives/labels" + "Default: derivatives/labels" "Note: if the provided folder (e.g., 'derivatives/labels') does not already exist, it will be created." "Note: if segmentation or labels files already exist and you would like to correct them, provide path to them " "within this flag.", From 2f52f0c4cb1f6f66a8a9f5f2d6961853d5d5607a Mon Sep 17 00:00:00 2001 From: valosekj Date: Sat, 28 Jan 2023 18:30:07 -0500 Subject: [PATCH 40/41] Describe 'FILES_LESION' in the script description and help --- manual_correction.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/manual_correction.py b/manual_correction.py index d7f4175..596d128 100644 --- a/manual_correction.py +++ b/manual_correction.py @@ -25,8 +25,8 @@ def get_parser(): parser function """ parser = argparse.ArgumentParser( - description='Manual correction of spinal cord segmentation, gray matter segmentation, vertebral labeling, and ' - 'pontomedullary junction labeling.' + description='Manual correction of spinal cord segmentation, gray matter segmentation, multiple sclerosis ' + 'lesion segmentation, vertebral labeling, and pontomedullary junction labeling. ' 'Manually corrected files are saved under derivatives/ folder (according to BIDS standard).', formatter_class=utils.SmartFormatter, prog=os.path.basename(__file__).strip('.py') @@ -37,10 +37,12 @@ def get_parser(): required=True, help= "R|Config yaml file listing images that require manual corrections for segmentation and vertebral " - "labeling. 'FILES_SEG' lists images associated with spinal cord segmentation " - ",'FILES_GMSEG' lists images associated with gray matter segmentation " - ",'FILES_LABEL' lists images associated with vertebral labeling " - "and 'FILES_PMJ' lists images associated with pontomedullary junction labeling" + "labeling. " + "'FILES_SEG' lists images associated with spinal cord segmentation, " + "'FILES_GMSEG' lists images associated with gray matter segmentation, " + "'FILES_LESION' lists images associated with multiple sclerosis lesion segmentation, " + "'FILES_LABEL' lists images associated with vertebral labeling, " + "and 'FILES_PMJ' lists images associated with pontomedullary junction labeling. " "You can validate your .yml file at this website: http://www.yamllint.com/." "Below is an example .yml file:\n" + dedent( @@ -51,6 +53,9 @@ def get_parser(): FILES_GMSEG: - sub-001_T1w.nii.gz - sub-002_T2w.nii.gz + FILES_LESION: + - sub-001_T1w.nii.gz + - sub-002_T2w.nii.gz FILES_LABEL: - sub-001_T1w.nii.gz - sub-002_T1w.nii.gz From f46935564edee0cccca92fc0c9df42e65ad88695 Mon Sep 17 00:00:00 2001 From: valosekj Date: Tue, 31 Jan 2023 13:25:39 -0500 Subject: [PATCH 41/41] Rename '-label-list' to '-label-disc-list' --- manual_correction.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/manual_correction.py b/manual_correction.py index 596d128..2ca6005 100644 --- a/manual_correction.py +++ b/manual_correction.py @@ -129,9 +129,9 @@ def get_parser(): default='_pmj' ) parser.add_argument( - '-label-list', - help="Provide a comma-separated list containing individual values and/or intervals. Example: '1:4,6,8' or 1:20 " - "(default)", + '-label-disc-list', + help="Comma-separated list containing individual values and/or intervals for disc labeling. Example: '1:4,6,8' " + "or 1:20 (default)", default='1:20' ) parser.add_argument( @@ -422,7 +422,7 @@ def main(): elif task == 'FILES_LESION': correct_segmentation(fname, fname_label, args.viewer, args.fsl_color) elif task == 'FILES_LABEL': - correct_vertebral_labeling(fname, fname_label, args.label_list) + correct_vertebral_labeling(fname, fname_label, args.label_disc_list) elif task == 'FILES_PMJ': correct_pmj_label(fname, fname_label) else: