diff --git a/src/aslprep-pre.sh b/src/aslprep-pre.sh index 87cbaed..afc4216 100755 --- a/src/aslprep-pre.sh +++ b/src/aslprep-pre.sh @@ -8,25 +8,20 @@ export m0scan=NO_M0SCAN export aslscan=NO_ASLSCAN export aslsource=NO_ASLSOURCE export examcard=NO_EXAMCARD +export t1scan=NO_T1SCAN # Parse options while [[ $# -gt 0 ]]; do key="${1}" case $key in - --indir) - export indir="${2}"; shift; shift ;; - --outdir) - export bidsdir="${2}"; shift; shift ;; - --m0scan) - export m0scan="${2}"; shift; shift ;; - --aslscan) - export aslscan="${2}"; shift; shift ;; - --sourcescan) - export sourcescan="${2}"; shift; shift ;; - --examcard) - export examcard="${2}"; shift; shift ;; - --fs_license) - export fs_license="${2}"; shift; shift ;; + --indir) export indir="${2}"; shift; shift ;; + --outdir) export bidsdir="${2}"; shift; shift ;; + --m0scan) export m0scan="${2}"; shift; shift ;; + --aslscan) export aslscan="${2}"; shift; shift ;; + --sourcescan) export sourcescan="${2}"; shift; shift ;; + --examcard) export examcard="${2}"; shift; shift ;; + --fs_license) export fs_license="${2}"; shift; shift ;; + --t1scan) export t1scan="${2}"; shift; shift ;; *) echo Unknown input "${1}"; shift ;; esac @@ -35,10 +30,10 @@ done # Format BIDS directory and convert to nii # Save Series Description to json -organize_data.py -i ${indir} -a ${aslscan} -m ${m0scan} -s ${sourcescan} +organize_data.py -i ${indir} -a ${aslscan} -m ${m0scan} -s ${sourcescan} -t ${t1scan} #Get necessary data form examcard and write to json sidecar examcard2json.py -i ${indir} -b ${bidsdir} -e ${examcard} #Create ASL context tsv file -create_context_tsv.py -b ${bidsdir} +create_tsv.py -b ${bidsdir} diff --git a/src/bash_commands.sh b/src/bash_commands.sh new file mode 100644 index 0000000..9525dd6 --- /dev/null +++ b/src/bash_commands.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +export indir=$1 +export source=$2 +export m0=$3 +export t1w=$4 +export source_base=$5 +export m0_base=$6 +export t1w_base=$7 + +# move scans to BIDS directories +mkdir -p $indir/BIDS/sub-01/ses-01/anat/ +mkdir -p $indir/BIDS/sub-01/ses-01/perf/ + +cp $source $indir/BIDS/sub-01/ses-01/perf/$source_base +cp $m0 $indir/BIDS/sub-01/ses-01/perf/m0$m0_base +cp $t1w $indir/BIDS/sub-01/ses-01/anat/$t1w_base + +# run dcm2niix on source and m0 scans +/data/mcr/centos7/dcm2niix/v1.0.20240202/console/dcm2niix -z y -f %b $indir/BIDS/sub-01/ses-01/anat +/data/mcr/centos7/dcm2niix/v1.0.20240202/console/dcm2niix -z y -f %b $indir/BIDS/sub-01/ses-01/perf diff --git a/src/create_context_tsv.py b/src/create_context_tsv.py deleted file mode 100755 index f522048..0000000 --- a/src/create_context_tsv.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python - -''' -Inputs: - -b: BIDS file structure containing nifti files and json sidecars -Outputs: - updated json sidecar for ASL images in Exam Card - Name: sub-01_ses-01_asl.json OR sub-01_ses-01_m0scan.json - - -''' -from __future__ import print_function -import os -import csv -import sys, getopt -import glob -import nibabel as nib - -def main(argv): - bids = '' - try: - opts, args = getopt.getopt(argv, "hb:",["bids="]) - except getopt.GetoptError: - print('create_tsv.py -b ') - sys.exit(2) - for opt, arg in opts: - if opt == '-h': - print('create_tsv.py -b ') - sys.exit() - elif opt in ("-b", "--bids"): - bids = arg - - print('BIDS Folder: ', bids) - - asl_file = glob.glob(bids+'/sub-*/ses-*/perf/*_asl.nii.gz') - - if asl_file: - asl_img = nib.load(asl_file[0]) - if len(asl_img.shape) < 4: - print('ASL data not 4-dimensional. Please repeat with correct file.') - sys.exit() - else: - abs_path = os.path.abspath(asl_file[0]) - file_struct = abs_path.split('/') - tsv_loc = '/'.join(file_struct[:-1]) + '' - name_struct = file_struct[-1].split('_') - tsv_name = '_'.join(name_struct[:-1]) + '_aslcontext.tsv' - - with open(tsv_loc + '/' + tsv_name,'wt') as tsv_file: - csv_writer=csv.writer(tsv_file,delimiter='\t') - csv_writer.writerow(['volume_type']) - for x in range(asl_img.shape[3]): - if (x % 2) == 0: - csv_writer.writerow(['control']) - else: - csv_writer.writerow(['label']) - - else: - print('Files not found or data is not in BIDS format. Please repeat with correct file/structure.') - sys.exit() - -if __name__ == '__main__': - main(sys.argv[1:]) \ No newline at end of file diff --git a/src/create_tsv.py b/src/create_tsv.py new file mode 100644 index 0000000..82d0068 --- /dev/null +++ b/src/create_tsv.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python + +''' +Inputs: + -b: BIDS file structure containing nifti files and json sidecars +Outputs: + updated json sidecar for ASL images in Exam Card + Name: sub-01_ses-01_asl.json OR sub-01_ses-01_m0scan.json +''' + +from __future__ import print_function +import os +import csv +import sys, getopt +import glob +import nibabel as nib + +def main(argv): + bids = '' + try: + opts, args = getopt.getopt(argv, "hb:",["bids="]) + except getopt.GetoptError: + print('create_tsv.py -b ') + sys.exit(2) + + for opt, arg in opts: + if opt == '-h': + print('create_tsv.py -b ') + sys.exit() + elif opt in ("-b", "--bids"): + bids = arg + + print('BIDS Folder: ', bids) + + asl_file = glob.glob(bids+'/sub-*/ses-*/perf/*_asl.nii.gz') + + print(asl_file) + print(asl_file[0]) + + if asl_file: + asl_img = nib.load(asl_file[0]) + if len(asl_img.shape) < 4: + print('ASL data not 4-dimensional. Please repeat with correct file.') + sys.exit() + else: + abs_path = os.path.abspath(asl_file[0]) + file_struct = abs_path.split('/') + tsv_loc = '/'.join(file_struct[:-1]) + '' + name_struct = file_struct[-1].split('_') + tsv_name = '_'.join(name_struct[:-1]) + '_aslcontext.tsv' + + with open(tsv_loc + '/' + tsv_name,'wt') as tsv_file: + csv_writer=csv.writer(tsv_file,delimiter='\t') + csv_writer.writerow(['volume_type']) + for x in range(asl_img.shape[3]): + if (x % 2) == 0: + csv_writer.writerow(['control']) + else: + csv_writer.writerow(['label']) + else: + print('Files not found or data is not in BIDS format. Please repeat with correct file/structure.') + sys.exit() + +if __name__ == '__main__': + main(sys.argv[1:]) diff --git a/src/examcard2json.py b/src/examcard2json.py index 1eb87cf..e5b9272 100755 --- a/src/examcard2json.py +++ b/src/examcard2json.py @@ -2,56 +2,56 @@ ''' Inputs: - -i: Filename of text file containing Exam Card info - -s: List of asl scan names to search in exam card - -b: BIDS file structure containing nifti files and json sidecars + -i: Filename of text file containing Exam Card info + -s: List of asl scan names to search in exam card + -b: BIDS file structure containing nifti files and json sidecars Outputs: - updated json sidecar for ASL images in Exam Card - Name: sub-01_ses-01_asl.json OR sub-01_ses-01_m0scan.json + updated json sidecar for ASL images in Exam Card + Name: sub-01_ses-01_asl.json OR sub-01_ses-01_m0scan.json Required inputs for json sidecar ASL general metadata fields: - MagneticFieldStrength - MRAcquisitionType (2D or 3D) - EchoTime - If MRAcquisitionType definied as 2D: - SliceTiming - If LookLocker is True: - RepetitionTimePreparation - FlipAngle - ArterialSpinLabelingType (CASL, PCASL, PASL) - PostLabelingDelay (in seconds) (0 for m0scans) - BackgroundSuppression - M0Type - TotalAcquiredPairs + MagneticFieldStrength + MRAcquisitionType (2D or 3D) + EchoTime + If MRAcquisitionType definied as 2D: + SliceTiming + If LookLocker is True: + RepetitionTimePreparation + FlipAngle + ArterialSpinLabelingType (CASL, PCASL, PASL) + PostLabelingDelay (in seconds) (0 for m0scans) + BackgroundSuppression + M0Type + TotalAcquiredPairs (P)CASL specific metadata fields: - LabelingDuration (0 for m0scans) + LabelingDuration (0 for m0scans) PASL specific metadata fields: - BolusCutOffFlag (boolean) - If BolusCutOffFlag is True: - BolusCutOffDelayTime - BolusCutOffTechnique + BolusCutOffFlag (boolean) + If BolusCutOffFlag is True: + BolusCutOffDelayTime + BolusCutOffTechnique m0scan metadata fields: - EchoTime - RepetitionTimePreparation - If LookLocker is True: - FlipAngle - IntendedFor (string with associated ASL image filename) + EchoTime + RepetitionTimePreparation + If LookLocker is True: + FlipAngle + IntendedFor (string with associated ASL image filename) Units of time should always be seconds. ''' from __future__ import print_function +import argparse import json import re -import sys +import sys, getopt import glob import numpy as np -import argparse class NumpyEncoder(json.JSONEncoder): def default(self, obj): @@ -60,313 +60,324 @@ def default(self, obj): return json.JSONEncoder.default(self, obj) def search_string_in_file(file_name, string_to_search, starting_line): - """ - Search for given string in file starting at provided line number - and return the first line containing that string, - along with line numbers. - :param file_name: name of text file to scrub - :param string_to_search: string of text to search for in file - :param starting_line: line at which search starts - """ - line_number = 0 - list_of_results = [] - # Open file in read only mode - with open(file_name, 'r') as read_obj: - # Read all lines one by one - for line in read_obj: - line_number += 1 - if line_number < starting_line: - continue - else: - line = line.rstrip() - if re.search(r"{}".format(string_to_search),line): - # If yes add the line number & line as a tuple in the list - list_of_results.append((line_number,line.rstrip())) - #Return list of tuples containing line numbers and lines where string is found - return list_of_results + """ + Search for given string in file starting at provided line number + and return the first line containing that string, + along with line numbers. + :param file_name: name of text file to scrub + :param string_to_search: string of text to search for in file + :param starting_line: line at which search starts + """ + line_number = 0 + list_of_results = [] + # Open file in read only mode + with open(file_name, 'r') as read_obj: + # Read all lines one by one + for line in read_obj: + line_number += 1 + if line_number < starting_line: + continue + else: + line = line.rstrip() + if re.search(r"{}".format(string_to_search),line): + # If yes add the line number & line as a tuple in the list + list_of_results.append((line_number,line.rstrip())) + #Return list of tuples containing line numbers and lines where string is found + return list_of_results def modify_json(json_file, s_dict): - """ - Add contents of s_dict to .json file - :param json_file: name of json file - :param s_dict: dictionary of info to add to .json file - """ - if json_file: - with open(json_file, 'r') as f: - json_contents = json.load(f) - json_contents.update(s_dict) - f.close - with open(json_file, 'w') as f: - json.dump(json_contents,f,indent = 4,cls=NumpyEncoder) - f.close - print('Added exam card information to',json_file) - else: - print('Files not found or data is not in BIDS format. Please repeat with correct file/structure.') - sys.exit() + """ + Add contents of s_dict to .json file + :param json_file: name of json file + :param s_dict: dictionary of info to add to .json file + """ + if json_file: + with open(json_file, 'r') as f: + json_contents = json.load(f) + json_contents.update(s_dict) + f.close + with open(json_file, 'w') as f: + json.dump(json_contents,f,indent = 4,cls=NumpyEncoder) + f.close + print('Added exam card information to',json_file) + else: + print('Files not found or data is not in BIDS format. Please repeat with correct file/structure.') + sys.exit() def main(argv): - - # FIXME take filenames as input instead of using glob to search for files - parser = argparse.ArgumentParser() - parser.add_argument('indir') - parser.add_argument('bidsdir') - parser.add_argument('examcard') + parser.add_argument('-i','--indir') + parser.add_argument('-b','--bids') + parser.add_argument('-e','--examcard') args = parser.parse_args() indir = args.indir - bidsdir = args.bidsdir - examcard = args.examcard - - #Initialize dictionaries - scan_dict = {} - - #Get scans from SeriesDescription.json - with open(indir + '/SeriesDescription.json','r') as infile: - scannames = json.load(infile) - - for scan in scannames.values(): - - scan_dict[scan] = {} - - print('\nStarting scan:', scan) - find_scans = search_string_in_file(examcard,scan,0) - # Find start of scan section - string_to_search = 'Protocol Name: ' + scan - if find_scans: - for line in find_scans: - for num in line: - if re.search(r"\b{}\b".format(string_to_search),str(num)): - start_line = line[0] - search_tmp = search_string_in_file(examcard,'Arterial Spin labeling',start_line) - tmp = search_tmp[0][1].split(':') - asl_type = tmp[-1].strip() - - # set repetiion time prep until better method found - scan_dict[scan]["RepetitionTimePreparation"] = 0 - print('\tRepetition Time Preparation:',str(0), 'sec') - - # If ASL type is 'NO', then scan is m0 - if asl_type == 'NO': - print('\tASL type: M0 scan') - # Get file name for non-m0 scan to set as 'IntendedFor' - for s in scannames: - if s.find('m0') == -1 & s.find('M0') == -1: - for nii_file in glob.glob(bidsdir + '/sub-*/ses-*/perf/*.nii.gz'): - if nii_file.find('m0') == -1 & nii_file.find('M0') == -1: - asl_nii = nii_file.split('/') - IntendedFor = '/'.join(asl_nii[-3:]) - print('\tM0 intended for: ',IntendedFor) - scan_dict[scan]["IntendedFor"] = IntendedFor - - # Parse examcard for AcquisitionVoxelSize - acq_vox_size = [] - search_tmp = search_string_in_file(examcard,'ACQ voxel size',start_line) - tmp = search_tmp[0][1].split(':') - acq_vox_size.append(float(tmp[-1].strip())) - tmp = search_tmp[1][1].split(':') - acq_vox_size.append(float(tmp[-1].strip())) - - search_tmp = search_string_in_file(examcard,'Slice thickness',start_line) - tmp = search_tmp[0][1].split(':') - acq_vox_size.append(float(tmp[-1].strip())) - - print('\tAcquisitionVoxelSize:', acq_vox_size, 'mm') - scan_dict[scan]["AcquisitionVoxelSize"] = acq_vox_size - - - # Add exam card info to m0 json - json_file = glob.glob(bidsdir+'/sub-*/ses-*/perf/*m0scan.json') - modify_json(json_file[0],scan_dict[scan]) - - else: - print('\tASL type:',asl_type) - scan_dict[scan]["ArterialSpinLabelingType"] = asl_type.upper() - # Set M0 type - if any('m0' or 'M0' in s for s in scannames): - M0_type = 'Separate' - else: - M0_type = 'Absent' - - scan_dict[scan]["M0Type"] = M0_type - - print('\tM0 type:',M0_type) - - # Parse exam card for background suppression - search_tmp = search_string_in_file(examcard,'back. supp.',start_line) - tmp = search_tmp[0][1].split(':') - back_supp = tmp[-1].strip() - if back_supp == 'NO': - back_supp = False - else: - back_supp = True - print('\tBackground Suppression:', back_supp) - scan_dict[scan]["BackgroundSuppression"] = back_supp - - # Parse exam card for label delay - search_tmp = search_string_in_file(examcard,'label delay',start_line) - tmp = search_tmp[0][1].split(':') - label_delay = float(tmp[-1].strip())/1000 - print('\tLabel delay:',label_delay, 'sec') - scan_dict[scan]["PostLabelingDelay"] = label_delay - - # Parse exam card for TR and nSlices to generate slice timing - search_tmp = search_string_in_file(examcard,'Slices',start_line) - tmp = search_tmp[0][1].split(':') - n_slices = int(tmp[-1].strip()) - - search_tmp = search_string_in_file(examcard,'TR ',start_line) - tmp = search_tmp[0][1].split(':') - if tmp[-1].strip() == 'USER_DEF': - search_tmp = search_string_in_file(examcard,'(ms)',search_tmp[0][0]) - tmp = search_tmp[0][1].split(':') - tr = float(tmp[-1].strip())/1000 - else: - tr = float(tmp[-1].strip())/1000 - - # calculate slice timing - ta = tr/n_slices - slice_timing = np.linspace(0,tr-ta,n_slices) #ascending - - # If slice order is descending, flip the slice timing list - search_tmp = search_string_in_file(examcard,'Slice scan order',start_line) - tmp = search_tmp[0][1].split(':') - if tmp[-1].strip() == 'DESCEND': - slice_timing = slice_timing.reverse() - - print('\tSlice timing:',slice_timing, 'sec') - scan_dict[scan]["SliceTiming"] = slice_timing.tolist() - - if asl_type == 'pCASL' or 'CASL' or 'PCASL': - # Parse exam card for background suppression - search_tmp = search_string_in_file(examcard,'label duration',start_line) - if not search_tmp: - search_tmp = search_string_in_file(examcard,'EX_FLL_casl_dur',start_line) - tmp = search_tmp[0][1].split(':') - label_duration = float(tmp[-1].strip())/1000 - print('\tLabeling duration:',label_duration, 'sec') - scan_dict[scan]["LabelingDuration"] = label_duration - - # Parse examcard for Background Suppression Pulses - search_tmp = search_string_in_file(examcard,'back. supp. pulses',start_line) - tmp = search_tmp[0][1].split(':') - back_supp_pulses_str = tmp[-1].strip() - - back_supp_pulses = list(back_supp_pulses_str.split(" ")) - - back_supp_list = [float(x)/1000 for x in back_supp_pulses] - back_supp_num_pulses = len(back_supp_list) - - print('\tBackground Suppression Pulses:', back_supp_list, 'sec') - print('\tNumber of Background Suppression Pulses:', back_supp_num_pulses) - scan_dict[scan]["BackgroundSuppressionPulseTime"] = back_supp_list - scan_dict[scan]["BackgroundSuppressionNumberPulses"] = back_supp_num_pulses - - # Parse examcard for label distance - search_tmp = search_string_in_file(examcard,'label distance',start_line) - tmp = search_tmp[0][1].split(':') - label_distance = tmp[-1].strip() - print('\tLabel Distance:', label_distance, 'mm') - scan_dict[scan]["LabelDistance"] = label_distance - - # Parse examcard for AcquisitionVoxelSize - acq_vox_size = [] - search_tmp = search_string_in_file(examcard,'ACQ voxel size',start_line) - tmp = search_tmp[0][1].split(':') - acq_vox_size.append(float(tmp[-1].strip())) - tmp = search_tmp[1][1].split(':') - acq_vox_size.append(float(tmp[-1].strip())) - - search_tmp = search_string_in_file(examcard,'Slice thickness',start_line) - tmp = search_tmp[0][1].split(':') - acq_vox_size.append(float(tmp[-1].strip())) - - print('\tAcquisitionVoxelSize:', acq_vox_size, 'mm') - scan_dict[scan]["AcquisitionVoxelSize"] = acq_vox_size - - # Parse examcard for Vascular crushing - search_tmp = search_string_in_file(examcard,'vascular crushing',start_line) - tmp = search_tmp[0][1].split(':') - vasc_crush = tmp[-1].strip() - if vasc_crush == 'NO': - vasc_crush = False - else: - vasc_crush = True - print('\tVascular crushing:', vasc_crush) - scan_dict[scan]["VascularCrushing"] = vasc_crush - - - if asl_type == 'pASL': - # Parse exam card for background suppression - #search_tmp = search_string_in_file(examcard,'BolusCutOffFlag',start_line) - #tmp = search_tmp[0][1].split(':') - #bolus = tmp[-1].strip() - #print('\tBolus Cut Off Flag:',bolus) - scan_dict[scan]["BolusCutOffFlag"] = False - - # Parse exam card for background suppression - search_tmp = search_string_in_file(examcard,'label duration',start_line) - if not search_tmp: - search_tmp = search_string_in_file(examcard,'EX_FLL_casl_dur',start_line) - tmp = search_tmp[0][1].split(':') - label_duration = float(tmp[-1].strip())/1000 - print('\tLabeling duration:',label_duration, 'sec') - scan_dict[scan]["LabelingDuration"] = label_duration - - # Parse examcard for Background Suppression Pulses - search_tmp = search_string_in_file(examcard,'back. supp. pulses',start_line) - tmp = search_tmp[0][1].split(':') - back_supp_pulses_str = tmp[-1].strip() - - back_supp_pulses = list(back_supp_pulses_str.split(" ")) - - back_supp_list = [float(x)/1000 for x in back_supp_pulses] - back_supp_num_pulses = len(back_supp_list) - - print('\tBackground Suppression Pulses:', back_supp_list, 'sec') - print('\tNumber of Background Suppression Pulses:', back_supp_num_pulses) - scan_dict[scan]["BackgroundSuppressionPulseTime"] = back_supp_list - scan_dict[scan]["BackgroundSuppressionNumberPulses"] = back_supp_num_pulses - - # Parse examcard for label distance - search_tmp = search_string_in_file(examcard,'label distance',start_line) - tmp = search_tmp[0][1].split(':') - label_distance = tmp[-1].strip() - print('\tLabel Distance:', label_distance, 'mm') - scan_dict[scan]["LabelDistance"] = label_distance - - # Parse examcard for AcquisitionVoxelSize - acq_vox_size = [] - search_tmp = search_string_in_file(examcard,'ACQ voxel size',start_line) - tmp = search_tmp[0][1].split(':') - acq_vox_size.append(float(tmp[-1].strip())) - tmp = search_tmp[1][1].split(':') - acq_vox_size.append(float(tmp[-1].strip())) - - search_tmp = search_string_in_file(examcard,'Slice thickness',start_line) - tmp = search_tmp[0][1].split(':') - acq_vox_size.append(float(tmp[-1].strip())) - - print('\tAcquisitionVoxelSize:', acq_vox_size, 'mm') - scan_dict[scan]["AcquisitionVoxelSize"] = acq_vox_size - - # Parse examcard for Vascular crushing - search_tmp = search_string_in_file(examcard,'vascular crushing',start_line) - tmp = search_tmp[0][1].split(':') - vasc_crush = tmp[-1].strip() - if vasc_crush == 'NO': - vasc_crush = False - else: - vasc_crush = True - print('\tVascular crushing:', vasc_crush) - scan_dict[scan]["VascularCrushing"] = vasc_crush - - # Add exam card info to asl json - json_file = glob.glob(bidsdir+'/sub-*/ses-*/perf/*asl.json') - modify_json(json_file[0],scan_dict[scan]) - else: - print(scan,' not found. Please repeat with correct scan name.') - sys.exit() - + bids = args.bids + inputfile = args.examcard + + #Initialize dictionaries and vars + scan_dict = {} + slice_list = [] + COUNT = 0 + + #Get scans from SeriesDescription.json + with open(indir + '/SeriesDescription.json','r') as infile: + scannames = json.load(infile) + + for scan in scannames.values(): + scan_dict[scan] = {} + + print('\nStarting scan:', scan) + find_scans = search_string_in_file(inputfile,scan,0) + # Find start of scan section + start_line = 0 + string_to_search = 'Protocol Name: ' + scan + if find_scans and COUNT < len(find_scans): + for line in find_scans: +# for num in line: +# if re.search(r"\b{}\b".format(string_to_search),str(num)): + if re.search(r"\b()\b".format(string_to_search),str(line)): + start_line = line[0] + search_tmp = search_string_in_file(inputfile,'Slice scan order',start_line) + tmp = search_tmp[0][1].split(':') + sso_type = tmp[-1].strip() + print('Slice Scan Order:',sso_type) + + #Load slice dictionary + slice_list.append((scan,sso_type)) + COUNT+=1 + + # set repetiion time prep until better method found + scan_dict[scan]["RepetitionTimePreparation"] = 0 + print('\tRepetition Time Preparation:',str(0), 'sec') + + # Probably need a better way to do this. Compare length of list to length of set list, + # which removed duplicates. If there are not duplicates, but same scans, it's not really + # getting handled (A, A; A, B). Also, not really handling two different that aren't the + # same (A, Y; B, Z). + if len(slice_list) != len(set(slice_list)): + print('duplicates') + else: + # Need to check two mentioned above here and a way to exit out entirely if we need it + print('not duplicates') + + # If ASL type is 'NO', then scan is m0 + if asl_type == 'NO': + print('\tASL type: M0 scan') + # Get file name for non-m0 scan to set as 'IntendedFor' + for s in scannames: + if s.find('m0') == -1 & s.find('M0') == -1: + for nii_file in glob.glob(bids + '/sub-*/ses-*/perf/*.nii.gz'): + if nii_file.find('m0') == -1 & nii_file.find('M0') == -1: + asl_nii = nii_file.split('/') + IntendedFor = '/'.join(asl_nii[-3:]) + print('\tM0 intended for: ',IntendedFor) + scan_dict[scan]["IntendedFor"] = IntendedFor + + # Parse examcard for AcquisitionVoxelSize +# acq_vox_size = [] +# search_tmp = search_string_in_file(inputfile,'ACQ voxel size',start_line) +# tmp = search_tmp[0][1].split(':') +# acq_vox_size.append(float(tmp[-1].strip())) +# tmp = search_tmp[1][1].split(':') +# acq_vox_size.append(float(tmp[-1].strip())) +# search_tmp = search_string_in_file(inputfile,'Slice thickness',start_line) +# tmp = search_tmp[0][1].split(':') +# acq_vox_size.append(float(tmp[-1].strip())) + +# print('\tAcquisitionVoxelSize:', acq_vox_size, 'mm') +# scan_dict[scan]["AcquisitionVoxelSize"] = acq_vox_size + + # Add exam card info to m0 json + json_file = glob.glob(bids+'/sub-*/ses-*/perf/*m0scan.json') + modify_json(json_file[0],scan_dict[scan]) + + else: + print('\tASL type:',asl_type) + scan_dict[scan]["ArterialSpinLabelingType"] = asl_type.upper() + # Set M0 type + if any('m0' or 'M0' in s for s in scannames): + M0_type = 'Separate' + else: + M0_type = 'Absent' + + scan_dict[scan]["M0Type"] = M0_type + + print('\tM0 type:',M0_type) + + # Parse exam card for background suppression + search_tmp = search_string_in_file(inputfile,'back. supp.',start_line) + tmp = search_tmp[0][1].split(':') + back_supp = tmp[-1].strip() + if back_supp == 'NO': + back_supp = False + else: + back_supp = True + print('\tBackground Suppression:', back_supp) + scan_dict[scan]["BackgroundSuppression"] = back_supp + + # Parse exam card for label delay + search_tmp = search_string_in_file(inputfile,'label delay',start_line) + tmp = search_tmp[0][1].split(':') + label_delay = float(tmp[-1].strip())/1000 + print('\tLabel delay:',label_delay, 'sec') + scan_dict[scan]["PostLabelingDelay"] = label_delay + + # Parse exam card for TR and nSlices to generate slice timing +# search_tmp = search_string_in_file(inputfile,'Slices',start_line) +# tmp = search_tmp[0][1].split(':') +# n_slices = int(tmp[-1].strip()) + +# search_tmp = search_string_in_file(inputfile,'TR ',start_line) +# tmp = search_tmp[0][1].split(':') +# if tmp[-1].strip() == 'USER_DEF': +# search_tmp = search_string_in_file(inputfile,'(ms)',search_tmp[0][0]) +# tmp = search_tmp[0][1].split(':') +# tr = float(tmp[-1].strip())/1000 +# else: +# tr = float(tmp[-1].strip())/1000 + + # calculate slice timing +# ta = tr/n_slices +# slice_timing = np.linspace(0,tr-ta,n_slices) #ascending + + # If slice order is descending, flip the slice timing list +# search_tmp = search_string_in_file(inputfile,'Slice scan order',start_line) +# tmp = search_tmp[0][1].split(':') +# if tmp[-1].strip() == 'DESCEND': +# slice_timing = slice_timing.reverse() + +# print('\tSlice timing:',slice_timing, 'sec') +# scan_dict[scan]["SliceTiming"] = slice_timing.tolist() + + if asl_type == 'pCASL' or 'CASL' or 'PCASL': + # Parse exam card for background suppression + search_tmp = search_string_in_file(inputfile,'label duration',start_line) + if not search_tmp: + search_tmp = search_string_in_file(inputfile,'EX_FLL_casl_dur',start_line) + tmp = search_tmp[0][1].split(':') + label_duration = float(tmp[-1].strip())/1000 + print('\tLabeling duration:',label_duration, 'sec') + scan_dict[scan]["LabelingDuration"] = label_duration + + # Parse examcard for Background Suppression Pulses + search_tmp = search_string_in_file(inputfile,'back. supp. pulses',start_line) + tmp = search_tmp[0][1].split(':') + back_supp_pulses_str = tmp[-1].strip() + + back_supp_pulses = list(back_supp_pulses_str.split(" ")) + + back_supp_list = [float(x)/1000 for x in back_supp_pulses] + back_supp_num_pulses = len(back_supp_list) + + print('\tBackground Suppression Pulses:', back_supp_list, 'sec') + print('\tNumber of Background Suppression Pulses:', back_supp_num_pulses) + scan_dict[scan]["BackgroundSuppressionPulseTime"] = back_supp_list + scan_dict[scan]["BackgroundSuppressionNumberPulses"] = back_supp_num_pulses + + # Parse examcard for label distance + search_tmp = search_string_in_file(inputfile,'label distance',start_line) + tmp = search_tmp[0][1].split(':') + label_distance = tmp[-1].strip() + print('\tLabel Distance:', label_distance, 'mm') + scan_dict[scan]["LabelDistance"] = label_distance + + # Parse examcard for AcquisitionVoxelSize +# acq_vox_size = [] +# search_tmp = search_string_in_file(inputfile,'ACQ voxel size',start_line) +# tmp = search_tmp[0][1].split(':') +# acq_vox_size.append(float(tmp[-1].strip())) +# tmp = search_tmp[1][1].split(':') +# acq_vox_size.append(float(tmp[-1].strip())) + +# search_tmp = search_string_in_file(inputfile,'Slice thickness',start_line) +# tmp = search_tmp[0][1].split(':') +# acq_vox_size.append(float(tmp[-1].strip())) + +# print('\tAcquisitionVoxelSize:', acq_vox_size, 'mm') +# scan_dict[scan]["AcquisitionVoxelSize"] = acq_vox_size + + # Parse examcard for Vascular crushing + search_tmp = search_string_in_file(inputfile,'vascular crushing',start_line) + tmp = search_tmp[0][1].split(':') + vasc_crush = tmp[-1].strip() + if vasc_crush == 'NO': + vasc_crush = False + else: + vasc_crush = True + print('\tVascular crushing:', vasc_crush) + scan_dict[scan]["VascularCrushing"] = vasc_crush + +# if asl_type == 'pASL': + # Parse exam card for background suppression + #search_tmp = search_string_in_file(inputfile,'BolusCutOffFlag',start_line) + #tmp = search_tmp[0][1].split(':') + #bolus = tmp[-1].strip() + #print('\tBolus Cut Off Flag:',bolus) +# scan_dict[scan]["BolusCutOffFlag"] = False + + # Parse exam card for background suppression +# search_tmp = search_string_in_file(inputfile,'label duration',start_line) +# if not search_tmp: +# search_tmp = search_string_in_file(inputfile,'EX_FLL_casl_dur',start_line) +# tmp = search_tmp[0][1].split(':') +# label_duration = float(tmp[-1].strip())/1000 +# print('\tLabeling duration:',label_duration, 'sec') +# scan_dict[scan]["LabelingDuration"] = label_duration + + # Parse examcard for Background Suppression Pulses +# search_tmp = search_string_in_file(inputfile,'back. supp. pulses',start_line) +# tmp = search_tmp[0][1].split(':') +# back_supp_pulses_str = tmp[-1].strip() + +# back_supp_pulses = list(back_supp_pulses_str.split(" ")) + +# back_supp_list = [float(x)/1000 for x in back_supp_pulses] +# back_supp_num_pulses = len(back_supp_list) + +# print('\tBackground Suppression Pulses:', back_supp_list, 'sec') +# print('\tNumber of Background Suppression Pulses:', back_supp_num_pulses) +# scan_dict[scan]["BackgroundSuppressionPulseTime"] = back_supp_list +# scan_dict[scan]["BackgroundSuppressionNumberPulses"] = back_supp_num_pulses + + # Parse examcard for label distance +# search_tmp = search_string_in_file(inputfile,'label distance',start_line) +# tmp = search_tmp[0][1].split(':') +# label_distance = tmp[-1].strip() +# print('\tLabel Distance:', label_distance, 'mm') +# scan_dict[scan]["LabelDistance"] = label_distance + + # Parse examcard for AcquisitionVoxelSize +# acq_vox_size = [] +# search_tmp = search_string_in_file(inputfile,'ACQ voxel size',start_line) +# tmp = search_tmp[0][1].split(':') +# acq_vox_size.append(float(tmp[-1].strip())) +# tmp = search_tmp[1][1].split(':') +# acq_vox_size.append(float(tmp[-1].strip())) + +# search_tmp = search_string_in_file(inputfile,'Slice thickness',start_line) +# tmp = search_tmp[0][1].split(':') +# acq_vox_size.append(float(tmp[-1].strip())) + +# print('\tAcquisitionVoxelSize:', acq_vox_size, 'mm') +# scan_dict[scan]["AcquisitionVoxelSize"] = acq_vox_size + + # Parse examcard for Vascular crushing +# search_tmp = search_string_in_file(inputfile,'vascular crushing',start_line) +# tmp = search_tmp[0][1].split(':') +# vasc_crush = tmp[-1].strip() +# if vasc_crush == 'NO': +# vasc_crush = False +# else: +# vasc_crush = True +# print('\tVascular crushing:', vasc_crush) +# scan_dict[scan]["VascularCrushing"] = vasc_crush + + # Add exam card info to asl json + json_file = glob.glob(bids+'/sub-*/ses-*/perf/*asl.json') + modify_json(json_file[0],scan_dict[scan]) + else: + print(scan,' not found. Please repeat with correct scan name.') + sys.exit() if __name__ == '__main__': - main(sys.argv[1:]) \ No newline at end of file + main(sys.argv[1:]) diff --git a/src/organize_data.py b/src/organize_data.py index a40c4e5..d960c8c 100755 --- a/src/organize_data.py +++ b/src/organize_data.py @@ -1,68 +1,91 @@ #!/usr/bin/env python +# Script for organizing ASL data to BIDS format +# Pull scan name from asl and m0 image, NEED TO DO: output to examcard2json +# Remove asl image (or just don't put it in the BIDS folder?) +# Convert dicom to nifti + import argparse +import glob import os -import sys +import sys, getopt from pydicom import dcmread import json def main(argv): - parser = argparse.ArgumentParser() - parser.add_argument('outdir') - parser.add_argument('t1_dcm') - parser.add_argument('asl_dcm') - parser.add_argument('m0_dcm') - parser.add_argument('source_dcm') + parser.add_argument('-i','--indir') + parser.add_argument('-a','--asl') + parser.add_argument('-m','--m0') + parser.add_argument('-s','--source') + parser.add_argument('-t','--t1w') args = parser.parse_args() - outdir = args.outdir - t1_dcm = args.t1_dcm - asl_dcm = args.asl_dcm - m0_dcm = args.m0_dcm - source_dcm = args.source_dcm + indir = args.indir + asl = args.asl + m0 = args.m0 + source = args.source + t1w = args.t1w + + # check if file paths are absolute + if os.path.isabs(asl) == False: + asl = indir + '/' + asl + m0 = indir + '/' + m0 + source = indir + '/' + source + + # get pydicom info for each scan + ds_asl = dcmread(asl) + ds_m0 = dcmread(m0) + ds_t1w = dcmread(t1w) + ds_source = dcmread(source) + + # pull scan name from dicom header + scanname = {} + scanname['asl'] = ds_asl.SeriesDescription + scanname['m0'] = ds_m0.SeriesDescription + + # write scanname dict to json + with open(indir + '/SeriesDescription.json','w') as outfile: + json.dump(scanname,outfile) + + subprocess.call(['./bash_commands.sh', str(indir), str(source), str(m0), str(t1w), str(source_base), str(m0_base), str(t1w_base)]) - # get pydicom info for each scan - ds_asl = dcmread(asl_dcm) - ds_m0 = dcmread(m0_dcm) - ds_t1w = dcmread(t1_dcm) - ds_source = dcmread(source_dcm) + # remove leftover dicoms + for file in glob.glob(indir + '/BIDS/sub-01/ses-01/*/*'): + if file.endswith('.dcm'): + os.system('rm ' + file) - # pull scan name from dicom header and write to json - scanname = {} - scanname['asl'] = ds_asl.SeriesDescription - scanname['m0'] = ds_m0.SeriesDescription - with open(outdir + '/SeriesDescription.json','w') as outfile: - json.dump(scanname,outfile) + anat_rename = 'sub-01_ses-01_T1w' + for file in glob.glob(indir + '/BIDS/sub-01/ses-01/anat/*'): + if file.endswith('.json'): + os.system('mv ' + file + ' ' + os.path.dirname(file) + '/' + anat_rename + '.json') + else: + os.system('mv ' + file + ' ' + os.path.dirname(file) + '/' + anat_rename + '.nii') - # Make BIDS directories - anatdir = 'f{outdir}/BIDS/sub-01/ses-01/anat' - os.makedirs(anatdir) - perfdir = 'f{outdir}/BIDS/sub-01/ses-01/perf' - os.makedirs(perfdir) + asl_rename = 'sub-01_ses-01_asl' + m0_rename = 'sub-01_ses-01_m0scan' + for file in glob.glob(indir + '/BIDS/sub-01/ses-01/perf/*'): + if 'M0' in file or 'm0' in file: + if file.endswith('.json'): + os.system('mv ' + file + ' ' + os.path.dirname(file) + '/' + m0_rename + '.json') + else: + os.system('mv ' + file + ' ' + os.path.dirname(file) + '/' + m0_rename + '.nii') + else: + if file.endswith('.json'): + os.system('mv ' + file + ' ' + os.path.dirname(file) + '/' + asl_rename + '.json') + else: + os.system('mv ' + file + ' ' + os.path.dirname(file) + '/' + asl_rename + '.nii') - # run dcm2niix on source and m0 scans, with BIDS-compliant filenames. - # "Source" is the ASL secondary recon from the scanner and is - # relabeled as 'asl' for BIDS - os.system(f'dcm2niix -s y -f sub-01_ses-01_T1w -o {anatdir} {t1_dcm}') - os.system(f'dcm2niix -s y -f sub-01_ses-01_m0scan -o {perfdir} {m0_dcm}') - os.system(f'dcm2niix -s y -f sub-01_ses-01_asl -o {perfdir} {source_dcm}') - - #ds_t1w.SeriesDescription = ds_t1w.SeriesDescription.replace(" ","").replace('/', "").replace(":", "").replace("_", "") - #ds_asl.SeriesDescription = ds_asl.SeriesDescription.replace(" ","").replace('/', "").replace(":", "").replace("_", "") - #ds_m0.SeriesDescription = ds_m0.SeriesDescription.replace(" ","").replace('/', "").replace(":", "").replace("_", "") + # create dataset_description.json + dataset_description = { + "BIDSVersion": "1.0.1", + "Name": "XNAT Project", + "DatasetDOI": "https://xnat2.vanderbilt.edu/xnat", + "Author": "No Author defined on XNAT" + } - # create dataset_description.json - # FIXME what can we do that's better than NA? - dataset_description = { - "BIDSVersion": "1.0.1", - "Name": "NA", - "DatasetDOI": "NA", - "Author": "NA" - } - - with open(indir + '/BIDS/dataset_description.json','w') as outfile: - json.dump(dataset_description,outfile) + with open(indir + '/BIDS/dataset_description.json','w') as outfile: + json.dump(dataset_description,outfile) if __name__ == '__main__': - main(sys.argv[1:]) + main(sys.argv[1:])