diff --git a/get_BIDS_sub.py b/get_BIDS_sub.py new file mode 100755 index 0000000..d17043d --- /dev/null +++ b/get_BIDS_sub.py @@ -0,0 +1,124 @@ +import sys +import os +import argparse +import json +import subprocess +import nibabel as nib +import glob +import numpy + +def get_parser(): + # Mandatory arguments + parser = argparse.ArgumentParser( + description=" ", + epilog="EXAMPLES:\n", + prog=os.path.basename(__file__).strip('.py')) + + mandatoryArguments = parser.add_argument_group("\nMANDATORY ARGUMENTS") + mandatoryArguments.add_argument( + '-path', + required=True, + help="path to BIDS Data", + ) + optional = parser.add_argument_group("\nOPTIONAL ARGUMENTS") + optional.add_argument( + '-constraint', + help="constraint field. You can use a specific fields from the header or 'orientation'", + ) + + optional.add_argument( + '-value', + help="value of constraint. Number or other. For orientation use capital letters (e.g., RPI) default operation is '==' if you wish to use something else please check the -ope option.", + ) + optional.add_argument( + '-ope', + default='==', + help=" operation type. You can use '<' or '>'. Don't forget to use quote (Unix will throw EOL error otherwise)") + + optional.add_argument( + '-ofile', + help="name of output file (txt file). If the file already exist, the found subject will be added at the end of the file") + + return parser + + +def get_view(im): + nifti = nib.load(im) + axis = nib.aff2axcodes(nifti.affine) + best_res_axis = numpy.where(nifti.header['pixdim'][1:4] == nifti.header['pixdim'][1:4].min())[0] + if len(best_res_axis)==3: + return 'valid' + elif len(best_res_axis)==2: + plane_dic={'SA':'sagittal','SP':'sagittal','IA':'sagittal','IP':'sagittal','AS':'sagittal','PS':'sagittal','AI':'sagittal','PI':'sagittal','SR':'coronal','SL':'coronal','IR':'coronal','IL':'coronal','RS':'coronal','LS':'coronal','RI':'coronal','LI':'coronal','AR':'axial','AL':'axial','PR':'axial','PL':'axial','RP':'axial','LP':'axial','RA':'axial','LA':'axial'} + best_plane = axis[best_res_axis[0]] + axis[best_res_axis[1]] + if nifti.header['pixdim'][1:4].min()>2: + return 'not_valid' + else: + return plane_dic[best_plane] + + + +def main(args=None): + if args is None: + args = None if sys.argv[1:] else ['--help'] + parser = get_parser() + arguments = parser.parse_args(args=args) + if arguments.constraint is not None and arguments.value is None: + parser.error("-constraint requires the -value option") + field = arguments.constraint + path_data = arguments.path + value = arguments.value + operation = arguments.ope + if arguments.ofile is not None: + out = arguments.ofile + else: + out = 'list-generated' + for param in [field, operation, value]: + if param: + out += param + path_images = (glob.glob(path_data+'/sub-*/anat/sub-*.nii.gz')) #grab all subject images path + + to_keep = [] + for im in path_images: + + if field == 'orientation': + #Orientation gets a special case because it is not in the header per se + if nib.aff2axcodes(nifti.affine) == (str(value[0]), str(value[1]), str(value[2])): + spli = im.rsplit('/',3) #we get the last 3 element + subj = spli[-3]+'/'+spli[-2]+'/'+spli[-1] + to_keep.append(subj) + + elif field == 'view': + #sagittal or axial or coronal. + if get_view(im) == value or get_view(im) == 'valid': + spli = im.rsplit('/',3) #we get the last 3 element + subj = spli[-3]+'/'+spli[-2]+'/'+spli[-1] + to_keep.append(subj) + + + elif field is not None: + nifti = nib.load(im) + if eval(str(nifti.header[field]) + operation + str(value)): #eval() allows to ask the user for '<' or '>' + spli = im.rsplit('/',3) #we get the last 3 element + subj = spli[-3]+'/'+spli[-2]+'/'+spli[-1] + to_keep.append(subj) + else: + spli = im.rsplit('/',3) #we get the last 3 element + subj = spli[-3]+'/'+spli[-2]+'/'+spli[-1] + to_keep.append(subj) + + if len(to_keep)>0: + f = open(out + '.txt', 'a') # 'a' option allows you to append file to a list. + l1 = map(lambda x: x + '\n', to_keep) + f.writelines(l1) + f.close() + else: + print('No file matching your criteria found') + + +if __name__ == "__main__": + main() + + + + diff --git a/label_disc_capture.png b/label_disc_capture.png new file mode 100755 index 0000000..8a8b3d5 Binary files /dev/null and b/label_disc_capture.png differ diff --git a/manual_labeling.py b/manual_labeling.py new file mode 100755 index 0000000..60d8a03 --- /dev/null +++ b/manual_labeling.py @@ -0,0 +1,109 @@ +import sys +import os +import argparse +import json +import subprocess + + +def get_parser(): + # Mandatory arguments + parser = argparse.ArgumentParser( + description=" ", + epilog="EXAMPLES:\n", + add_help=None, + prog=os.path.basename(__file__).strip('.py')) + + mandatoryArguments = parser.add_argument_group("\nMANDATORY ARGUMENTS") + mandatoryArguments.add_argument( + '-file', + required=True, + help="Json file containing list of path to image file", + ) + mandatoryArguments.add_argument( + '-path', + required=True, + help="path to bids folder",) + mandatoryArguments.add_argument( + '-author', + required=True, + help="Author name for json file", + ) + optional = parser.add_argument_group("\nOPTIONAL ARGUMENTS") + optional.add_argument( + '-correct', + default=0, + help=" if this is activated the -ilabel option will be used and therefore existing file will be open") + optional.add_argument( + '-o', + help="output path. if empty it will save in BIDS_path/label/derivatives/sub/anat ") + + return parser + + +def main(args=None): + if args is None: + args = None if sys.argv[1:] else ['--help'] + parser = get_parser() + arguments = parser.parse_args(args=args) + file_path = arguments.file + author_name = arguments.author + correct = arguments.correct + json_content = {"author": author_name, "label": "labels-disc-manual"} + list_of_subj = [line.rstrip('\n') for line in open(file_path)] + derivatives_base = arguments.path # file path is BIDS: last 3 elements are /sub-xx/anat/FILENAM + derivatives_path = derivatives_base + 'derivatives/labels' + if arguments.o is not None: + out_path = arguments.o + i=0 + else: + out_path = derivatives_path + i=0 + + try: + for rel_path in list_of_subj: + im_path = arguments.path+rel_path + label_base = im_path.rsplit('/', 1)[-1][:-7] # we remove the last 7 caracters that are .nii.gz + subj = im_path.rsplit('/', 3)[-3] + label_filename = label_base + '_labels-disc-manual.nii.gz' + json_filename = label_base + '_labels-disc-manual.json' + + if os.path.exists( out_path+'/'+ subj + '/anat'): + pass + else: + os.makedirs(out_path +'/'+ subj + '/anat') + path_json = out_path +'/'+ subj +'/anat/' + json_filename + path_label = derivatives_path + subj + '/anat/' + label_filename # retrieving label filename + path_out = out_path +'/'+ subj + '/anat/' + label_filename + + if correct: + if os.path.exists(path_label): + command = """sct_label_utils -i """ + im_path + """ -create-viewer 3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20 -ilabel """ + path_label + """ -o """ + path_out + else: + command = """sct_label_utils -i """ + im_path + """ -create-viewer 3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20 -o """ + path_out + + subprocess.run(command, shell=True) + i=i+1 + + with open(path_json, 'w') as f: + json.dump(json_content, f) + else: + if os.path.exists(path_label): + pass + else: + command = """sct_label_utils -i """ + im_path + """ -create-viewer 3,4,5,6,7,8,9,10,11,12,13,14,15 -o """ + path_out + subprocess.run(command, shell=True) + i=i+1 + with open(path_json, 'w') as f: + json.dump(json_content, f) + + except KeyboardInterrupt: + print('saving list') + following = list_of_subj[i:] + f = open(file_path,'w') # 'a' option allows you to append file to a list. + l1 = map(lambda x: x + '\n', following) + f.writelines(l1) + f.close() + + +if __name__ == "__main__": + main() diff --git a/sop.md b/sop.md new file mode 100644 index 0000000..130ab59 --- /dev/null +++ b/sop.md @@ -0,0 +1,21 @@ +This script is made for manual labeling of BIDS folder + +Run the following with python: + +python manual-labeling/manual_labeling.py -file list_todo_update.txt -author lucas -path Path_to_duke/ \[-correct 1 -o label_tmp\] + +- -file: txt file that contains the list of all images inside bids root folder that you want to process separated by '\n'.The format in sub-xx/anat/sub-xxx_xxx.nii.gz. Can be obtained with get_bids_sub.py. +- -path : Path the Bids root folder (duke) with a '/' at the end +- -author: Author name that will appear on the .json file +- -correct: Boolean. Default is 0. If correct is 1, the script will look for existing label and open them with the -ilabel option from sct_label_utils so you can verify/correct existing label. +- -o: desired output folder. It will be created if missing. After the task, you will find the file there in BIDS convention sub-xxx/anat/sub-xxx_labels-disc-manual.nii.gz and sub-xxx/anat/sub-xxx_labels-disc-manual.json" if the argument is not use or empty files will be saved in BIDS_PATH/derivatives/labels/sub-xx/anat/xxx + +To end the script: +Perform a keyboard interrupt from the terminal (ctrl+c). This will update the list by deleting the viewed subjects. don't forget to commit and push it on github. + +Specific: +suffix to label is labels-disc-manual +The json file contain the name of the label and the name of the author given by the -author args. + +Example image for manual labeling: +![example](label_disc_capture.png)