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 all 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
124 changes: 124 additions & 0 deletions get_BIDS_sub.py
Original file line number Diff line number Diff line change
@@ -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()




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.
109 changes: 109 additions & 0 deletions manual_labeling.py
Original file line number Diff line number Diff line change
@@ -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()
21 changes: 21 additions & 0 deletions sop.md
Original file line number Diff line number Diff line change
@@ -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
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 :-)

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)