Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding the python script for manual labeling #2

Open
wants to merge 24 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions get_BIDS_sub.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
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",
add_help=None,
lrouhier marked this conversation as resolved.
Show resolved Hide resolved
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",
)
mandatoryArguments.add_argument(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

constraint, ope, value are great.
Possible avenue of improvement: add possibility to have multiple constraints (separated by commas?).
Could also use a config file to specify those.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason for requiring them as mandatory? I would recommend to change for optional.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really, in my mind there was another solution to get all file but I just realized that's will not work with the 'manual labeling.py' script. (ls > file.txt would get you all the subject but not image file) will fix

'-constraint',
required=True,
help="constraint field. You can use a specific fields from the header or 'orientation'",
)

mandatoryArguments.add_argument(
'-value',
required=True,
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 = parser.add_argument_group("\nOPTIONAL ARGUMENTS")
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 = parser.add_argument_group("\nOPTIONAL ARGUMENTS")
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]]
print (best_plane)
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)
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'+ field + operation + value
path_images = (glob.glob(path_data+'/sub-*/anat/*.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)


else:
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)

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()





Binary file added label_disc_capture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
108 changes: 108 additions & 0 deletions manual_labeling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
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 = argument.o
lrouhier marked this conversation as resolved.
Show resolved Hide resolved
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()
16 changes: 16 additions & 0 deletions sop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
this script is made for manual labeling of BIDS folder
run the script 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 th Bids root folder (duke) with a '/' at the end
Copy link
Member

@charleygros charleygros May 27, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor comment: You can check / adjust for missing / I think

- -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: in the output folder given by the 5th argument, you will find in BIDS convention sub-xxx/anat/sub-xxx_labels-disc-manual.nii.gz and sub-xxx/anat/sub-xxx_lables-disc-manual.json" if this is empty this will save under BIDS_PATH/derivatives/labels/sub-xx/anat/xxx
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend to remove "by the 5th argument".


to end : performa 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:
possible file suffix to file are _acq-sag_T2w, _T2w, _T1w, _acq-sagcerv_T2w
suffix to label is labels-disc-manual
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be relevant to add a parameter that controls this suffix?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's actually a remain of a first version where I asked for file suffix. I'll remove it from the SOP. Now there are no reason that I can think of. we can add a case for the constraint on it if you think this could be useful.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was simply thinking: if one day we want to label pmj instead of the disc --> pmj-manual instead of labels-disc-manual.
But we can leave it as it is for now :-)

Example image for manual labeling:
![example](label_disc_capture.png)