def preprocessing(df, folder_out, contrast_centerline): sct.printv("Preprocessing...") for idx, row in df.iterrows(): sct.printv("\t" + row.subject) img = row['img'] labels = row['label'] img_head, img_tail = os.path.split(img) img_basename = img_tail.split('.nii')[0] folder_out_cur = os.path.join(folder_out, row.subject) if not os.path.isdir(folder_out_cur): os.makedirs(folder_out_cur) ctr = os.path.join(folder_out_cur, img_basename + '_centerline_optic.nii.gz') if not os.path.isfile(ctr): run_optic(img, contrast_centerline, folder_out_cur) flat_in = os.path.join(img_head, img_basename + '_flatten.nii.gz') flat = os.path.join(folder_out_cur, img_basename + '_flatten.nii.gz') if not os.path.isfile(flat) and os.path.isfile(ctr): run_flat(img, ctr, folder_out_cur) sct.mv(flat_in, flat) oneslice = os.path.join(folder_out_cur, img_basename + '_oneslice.nii') oneslice_gt = os.path.join(folder_out_cur, img_basename + '_oneslice_gt.nii') if os.path.isfile(flat) and not os.path.isfile(oneslice): run_crop(flat, oneslice) if os.path.isfile(labels) and not os.path.isfile(oneslice_gt): run_crop(labels, oneslice_gt) if os.path.isfile(oneslice) and os.path.isfile(oneslice_gt): df.loc[idx, 'train'] = os.path.abspath(oneslice) df.loc[idx, 'gt'] = os.path.abspath(oneslice_gt) return df
def fmri_moco(param): file_data = 'fmri' ext_data = '.nii' mat_final = 'mat_final/' ext_mat = 'Warp.nii.gz' # warping field # Get dimensions of data sct.printv('\nGet dimensions of data...', param.verbose) im_data = Image(file_data + '.nii') nx, ny, nz, nt, px, py, pz, pt = im_data.dim sct.printv(' ' + str(nx) + ' x ' + str(ny) + ' x ' + str(nz) + ' x ' + str(nt), param.verbose) # Get orientation sct.printv('\nData orientation: ' + im_data.orientation, param.verbose) if im_data.orientation[2] in 'LR': param.is_sagittal = True sct.printv(' Treated as sagittal') elif im_data.orientation[2] in 'IS': param.is_sagittal = False sct.printv(' Treated as axial') else: param.is_sagittal = False sct.printv('WARNING: Orientation seems to be neither axial nor sagittal.') # Adjust group size in case of sagittal scan if param.is_sagittal and param.group_size != 1: sct.printv('For sagittal data group_size should be one for more robustness. Forcing group_size=1.', 1, 'warning') param.group_size = 1 # Split into T dimension sct.printv('\nSplit along T dimension...', param.verbose) im_data = Image(file_data + ext_data) im_data_split_list = split_data(im_data, 3) for im in im_data_split_list: im.save() # assign an index to each volume index_fmri = list(range(0, nt)) # Number of groups nb_groups = int(math.floor(nt / param.group_size)) # Generate groups indexes group_indexes = [] for iGroup in range(nb_groups): group_indexes.append(index_fmri[(iGroup * param.group_size):((iGroup + 1) * param.group_size)]) # add the remaining images to the last fMRI group nb_remaining = nt%param.group_size # number of remaining images if nb_remaining > 0: nb_groups += 1 group_indexes.append(index_fmri[len(index_fmri) - nb_remaining:len(index_fmri)]) # groups for iGroup in tqdm(range(nb_groups), unit='iter', unit_scale=False, desc="Merge within groups", ascii=True, ncols=80): # get index index_fmri_i = group_indexes[iGroup] nt_i = len(index_fmri_i) # Merge Images file_data_merge_i = file_data + '_' + str(iGroup) # cmd = fsloutput + 'fslmerge -t ' + file_data_merge_i # for it in range(nt_i): # cmd = cmd + ' ' + file_data + '_T' + str(index_fmri_i[it]).zfill(4) im_fmri_list = [] for it in range(nt_i): im_fmri_list.append(im_data_split_list[index_fmri_i[it]]) im_fmri_concat = concat_data(im_fmri_list, 3, squeeze_data=True).save(file_data_merge_i + ext_data) file_data_mean = file_data + '_mean_' + str(iGroup) if param.group_size == 1: # copy to new file name instead of averaging (faster) # note: this is a bandage. Ideally we should skip this entire for loop if g=1 sct.copy(file_data_merge_i + '.nii', file_data_mean + '.nii') else: # Average Images sct.run(['sct_maths', '-i', file_data_merge_i + '.nii', '-o', file_data_mean + '.nii', '-mean', 't'], verbose=0) # if not average_data_across_dimension(file_data_merge_i+'.nii', file_data_mean+'.nii', 3): # sct.printv('ERROR in average_data_across_dimension', 1, 'error') # cmd = fsloutput + 'fslmaths ' + file_data_merge_i + ' -Tmean ' + file_data_mean # sct.run(cmd, param.verbose) # Merge groups means. The output 4D volume will be used for motion correction. sct.printv('\nMerging volumes...', param.verbose) file_data_groups_means_merge = 'fmri_averaged_groups' im_mean_list = [] for iGroup in range(nb_groups): im_mean_list.append(Image(file_data + '_mean_' + str(iGroup) + ext_data)) im_mean_concat = concat_data(im_mean_list, 3).save(file_data_groups_means_merge + ext_data) # Estimate moco sct.printv('\n-------------------------------------------------------------------------------', param.verbose) sct.printv(' Estimating motion...', param.verbose) sct.printv('-------------------------------------------------------------------------------', param.verbose) param_moco = param param_moco.file_data = 'fmri_averaged_groups' param_moco.file_target = file_data + '_mean_' + param.num_target param_moco.path_out = '' param_moco.todo = 'estimate_and_apply' param_moco.mat_moco = 'mat_groups' file_mat = moco.moco(param_moco) # TODO: if g=1, no need to run the block below (already applied) if param.group_size == 1: # if flag g=1, it means that all images have already been corrected, so we just need to rename the file sct.mv('fmri_averaged_groups_moco.nii', 'fmri_moco.nii') else: # create final mat folder sct.create_folder(mat_final) # Copy registration matrices sct.printv('\nCopy transformations...', param.verbose) for iGroup in range(nb_groups): for data in range(len(group_indexes[iGroup])): # we cannot use enumerate because group_indexes has 2 dim. # fetch all file_mat_z for given t-group list_file_mat_z = file_mat[:, iGroup] # loop across file_mat_z and copy to mat_final folder for file_mat_z in list_file_mat_z: # we want to copy 'mat_groups/mat.ZXXXXTYYYYWarp.nii.gz' --> 'mat_final/mat.ZXXXXTYYYZWarp.nii.gz' # Notice the Y->Z in the under the T index: the idea here is to use the single matrix from each group, # and apply it to all images belonging to the same group. sct.copy(file_mat_z + ext_mat, mat_final + file_mat_z[11:20] + 'T' + str(group_indexes[iGroup][data]).zfill(4) + ext_mat) # Apply moco on all fmri data sct.printv('\n-------------------------------------------------------------------------------', param.verbose) sct.printv(' Apply moco', param.verbose) sct.printv('-------------------------------------------------------------------------------', param.verbose) param_moco.file_data = 'fmri' param_moco.file_target = file_data + '_mean_' + str(0) param_moco.path_out = '' param_moco.mat_moco = mat_final param_moco.todo = 'apply' moco.moco(param_moco) # copy geometric information from header # NB: this is required because WarpImageMultiTransform in 2D mode wrongly sets pixdim(3) to "1". im_fmri = Image('fmri.nii') im_fmri_moco = Image('fmri_moco.nii') im_fmri_moco.header = im_fmri.header im_fmri_moco.save() # Average volumes sct.printv('\nAveraging data...', param.verbose) sct_maths.main(args=['-i', 'fmri_moco.nii', '-o', 'fmri_moco_mean.nii', '-mean', 't', '-v', '0'])
def fmri_moco(param): file_data = "fmri.nii" mat_final = 'mat_final/' ext_mat = 'Warp.nii.gz' # warping field # Get dimensions of data sct.printv('\nGet dimensions of data...', param.verbose) im_data = Image(param.fname_data) nx, ny, nz, nt, px, py, pz, pt = im_data.dim sct.printv( ' ' + str(nx) + ' x ' + str(ny) + ' x ' + str(nz) + ' x ' + str(nt), param.verbose) # Get orientation sct.printv('\nData orientation: ' + im_data.orientation, param.verbose) if im_data.orientation[2] in 'LR': param.is_sagittal = True sct.printv(' Treated as sagittal') elif im_data.orientation[2] in 'IS': param.is_sagittal = False sct.printv(' Treated as axial') else: param.is_sagittal = False sct.printv( 'WARNING: Orientation seems to be neither axial nor sagittal.') # Adjust group size in case of sagittal scan if param.is_sagittal and param.group_size != 1: sct.printv( 'For sagittal data group_size should be one for more robustness. Forcing group_size=1.', 1, 'warning') param.group_size = 1 # Split into T dimension sct.printv('\nSplit along T dimension...', param.verbose) im_data_split_list = split_data(im_data, 3) for im in im_data_split_list: x_dirname, x_basename, x_ext = sct.extract_fname(im.absolutepath) # Make further steps slurp the data to avoid too many open files (#2149) im.absolutepath = os.path.join(x_dirname, x_basename + ".nii.gz") im.save() # assign an index to each volume index_fmri = list(range(0, nt)) # Number of groups nb_groups = int(math.floor(nt / param.group_size)) # Generate groups indexes group_indexes = [] for iGroup in range(nb_groups): group_indexes.append(index_fmri[(iGroup * param.group_size):((iGroup + 1) * param.group_size)]) # add the remaining images to the last fMRI group nb_remaining = nt % param.group_size # number of remaining images if nb_remaining > 0: nb_groups += 1 group_indexes.append(index_fmri[len(index_fmri) - nb_remaining:len(index_fmri)]) # groups for iGroup in tqdm(range(nb_groups), unit='iter', unit_scale=False, desc="Merge within groups", ascii=True, ncols=80): # get index index_fmri_i = group_indexes[iGroup] nt_i = len(index_fmri_i) # Merge Images file_data_merge_i = sct.add_suffix(file_data, '_' + str(iGroup)) # cmd = fsloutput + 'fslmerge -t ' + file_data_merge_i # for it in range(nt_i): # cmd = cmd + ' ' + file_data + '_T' + str(index_fmri_i[it]).zfill(4) im_fmri_list = [] for it in range(nt_i): im_fmri_list.append(im_data_split_list[index_fmri_i[it]]) im_fmri_concat = concat_data(im_fmri_list, 3, squeeze_data=True).save(file_data_merge_i) file_data_mean = sct.add_suffix(file_data, '_mean_' + str(iGroup)) if file_data_mean.endswith(".nii"): file_data_mean += ".gz" # #2149 if param.group_size == 1: # copy to new file name instead of averaging (faster) # note: this is a bandage. Ideally we should skip this entire for loop if g=1 convert(file_data_merge_i, file_data_mean) else: # Average Images sct.run([ 'sct_maths', '-i', file_data_merge_i, '-o', file_data_mean, '-mean', 't' ], verbose=0) # if not average_data_across_dimension(file_data_merge_i+'.nii', file_data_mean+'.nii', 3): # sct.printv('ERROR in average_data_across_dimension', 1, 'error') # cmd = fsloutput + 'fslmaths ' + file_data_merge_i + ' -Tmean ' + file_data_mean # sct.run(cmd, param.verbose) # Merge groups means. The output 4D volume will be used for motion correction. sct.printv('\nMerging volumes...', param.verbose) file_data_groups_means_merge = 'fmri_averaged_groups.nii' im_mean_list = [] for iGroup in range(nb_groups): file_data_mean = sct.add_suffix(file_data, '_mean_' + str(iGroup)) if file_data_mean.endswith(".nii"): file_data_mean += ".gz" # #2149 im_mean_list.append(Image(file_data_mean)) im_mean_concat = concat_data(im_mean_list, 3).save(file_data_groups_means_merge) # Estimate moco sct.printv( '\n-------------------------------------------------------------------------------', param.verbose) sct.printv(' Estimating motion...', param.verbose) sct.printv( '-------------------------------------------------------------------------------', param.verbose) param_moco = param param_moco.file_data = 'fmri_averaged_groups.nii' param_moco.file_target = sct.add_suffix(file_data, '_mean_' + param.num_target) if param_moco.file_target.endswith(".nii"): param_moco.file_target += ".gz" # #2149 param_moco.path_out = '' param_moco.todo = 'estimate_and_apply' param_moco.mat_moco = 'mat_groups' file_mat = moco.moco(param_moco) # TODO: if g=1, no need to run the block below (already applied) if param.group_size == 1: # if flag g=1, it means that all images have already been corrected, so we just need to rename the file sct.mv('fmri_averaged_groups_moco.nii', 'fmri_moco.nii') else: # create final mat folder sct.create_folder(mat_final) # Copy registration matrices sct.printv('\nCopy transformations...', param.verbose) for iGroup in range(nb_groups): for data in range( len(group_indexes[iGroup]) ): # we cannot use enumerate because group_indexes has 2 dim. # fetch all file_mat_z for given t-group list_file_mat_z = file_mat[:, iGroup] # loop across file_mat_z and copy to mat_final folder for file_mat_z in list_file_mat_z: # we want to copy 'mat_groups/mat.ZXXXXTYYYYWarp.nii.gz' --> 'mat_final/mat.ZXXXXTYYYZWarp.nii.gz' # Notice the Y->Z in the under the T index: the idea here is to use the single matrix from each group, # and apply it to all images belonging to the same group. sct.copy( file_mat_z + ext_mat, mat_final + file_mat_z[11:20] + 'T' + str(group_indexes[iGroup][data]).zfill(4) + ext_mat) # Apply moco on all fmri data sct.printv( '\n-------------------------------------------------------------------------------', param.verbose) sct.printv(' Apply moco', param.verbose) sct.printv( '-------------------------------------------------------------------------------', param.verbose) param_moco.file_data = 'fmri.nii' param_moco.file_target = sct.add_suffix(file_data, '_mean_' + str(0)) if param_moco.file_target.endswith(".nii"): param_moco.file_target += ".gz" param_moco.path_out = '' param_moco.mat_moco = mat_final param_moco.todo = 'apply' file_mat = moco.moco(param_moco) # copy geometric information from header # NB: this is required because WarpImageMultiTransform in 2D mode wrongly sets pixdim(3) to "1". im_fmri = Image('fmri.nii') im_fmri_moco = Image('fmri_moco.nii') im_fmri_moco.header = im_fmri.header im_fmri_moco.save() # Extract and output the motion parameters if param.output_motion_param: from sct_image import multicomponent_split import csv #files_warp = [] files_warp_X, files_warp_Y = [], [] moco_param = [] for fname_warp in file_mat[0]: # Cropping the image to keep only one voxel in the XY plane im_warp = Image(fname_warp + ext_mat) im_warp.data = np.expand_dims(np.expand_dims( im_warp.data[0, 0, :, :, :], axis=0), axis=0) # These three lines allow to generate one file instead of two, containing X, Y and Z moco parameters #fname_warp_crop = fname_warp + '_crop_' + ext_mat #files_warp.append(fname_warp_crop) #im_warp.save(fname_warp_crop) # Separating the three components and saving X and Y only (Z is equal to 0 by default). im_warp_XYZ = multicomponent_split(im_warp) fname_warp_crop_X = fname_warp + '_crop_X_' + ext_mat im_warp_XYZ[0].save(fname_warp_crop_X) files_warp_X.append(fname_warp_crop_X) fname_warp_crop_Y = fname_warp + '_crop_Y_' + ext_mat im_warp_XYZ[1].save(fname_warp_crop_Y) files_warp_Y.append(fname_warp_crop_Y) # Calculating the slice-wise average moco estimate to provide a QC file moco_param.append([ np.mean(np.ravel(im_warp_XYZ[0].data)), np.mean(np.ravel(im_warp_XYZ[1].data)) ]) # These two lines allow to generate one file instead of two, containing X, Y and Z moco parameters #im_warp_concat = concat_data(files_warp, dim=3) #im_warp_concat.save('fmri_moco_params.nii') # Concatenating the moco parameters into a time series for X and Y components. im_warp_concat = concat_data(files_warp_X, dim=3) im_warp_concat.save('fmri_moco_params_X.nii') im_warp_concat = concat_data(files_warp_Y, dim=3) im_warp_concat.save('fmri_moco_params_Y.nii') # Writing a TSV file with the slicewise average estimate of the moco parameters, as it is a useful QC file. with open('fmri_moco_params.tsv', 'wt') as out_file: tsv_writer = csv.writer(out_file, delimiter='\t') tsv_writer.writerow(['X', 'Y']) for mocop in moco_param: tsv_writer.writerow([mocop[0], mocop[1]]) # Average volumes sct.printv('\nAveraging data...', param.verbose) sct_maths.main(args=[ '-i', 'fmri_moco.nii', '-o', 'fmri_moco_mean.nii', '-mean', 't', '-v', '0' ])
def preprocessing(df, folder_out, contrast_centerline): sct.printv("Preprocessing...") qc_fold = os.path.join(folder_out, 'qc') if not os.path.isdir(qc_fold): os.makedirs(qc_fold) for idx, row in df.iterrows(): if row.contrast.startswith(contrast_centerline): sct.printv("\t" + row.subject) img = row['img'] labels = row['labels'] img_head, img_tail = os.path.split(img) img_basename = img_tail.split('.nii')[0] folder_out_cur = os.path.join(folder_out, row.subject) if not os.path.isdir(folder_out_cur): os.makedirs(folder_out_cur) ctr = os.path.join(folder_out_cur, img_basename + '_centerline_optic.nii.gz') if not os.path.isfile(ctr): run_optic(img, contrast_centerline, folder_out_cur) flat_in = os.path.join(img_head, img_basename + '_flatten.nii.gz') flat = os.path.join(folder_out_cur, img_basename + '_flatten.nii.gz') if not os.path.isfile(flat) and os.path.isfile(ctr): run_flat(img, ctr, folder_out_cur) sct.mv(flat_in, flat) oneslice = os.path.join(folder_out_cur, img_basename + '_oneslice.nii') oneslice_gt = os.path.join(folder_out_cur, img_basename + '_oneslice_gt.nii') if os.path.isfile(flat) and not os.path.isfile(oneslice): run_crop(flat, oneslice, 7.0) if os.path.isfile(labels) and not os.path.isfile(oneslice_gt): run_crop(labels, oneslice_gt, 1.0) if os.path.isfile(oneslice) and os.path.isfile(oneslice_gt): df.loc[idx, 'train'] = os.path.abspath(oneslice) df.loc[idx, 'gt'] = os.path.abspath(oneslice_gt) qc_file = os.path.join( qc_fold, '_'.join([row.subject, row.contrast]) + '.png') if not os.path.isfile(qc_file): create_qc(oneslice, oneslice_gt, qc_file) return df # def train_model(df, model_name): sct.printv("Training...") train_txt = 'train_lst.txt' train_gt_txt = 'train_gt_lst.txt' if os.path.isfile(train_txt) or os.path.isfile(train_gt_txt): sct.rm(train_txt) sct.rm(train_gt_txt) stg_train = '\n'.join([ os.path.abspath(f).split('.nii')[0] for f in df['train'].values if str(f) != 'nan' ]) stg_gt_train = '\n'.join([ os.path.abspath(f).split('.nii')[0] for f in df['gt'].values if str(f) != 'nan' ]) with open(train_txt, 'w') as text_file: text_file.write(stg_train) text_file.close() with open(train_gt_txt, 'w') as text_file: text_file.write(stg_gt_train) text_file.close() model_path = os.getcwd() + '/trained_model_t1.yml' if os.path.isfile(model_path): sct.rm(model_path) cmd_train = 'isct_train_svm -hogsg -incr=20 ' + model_name + ' ' + train_txt + ' ' + train_gt_txt + ' --list True' sct.run(cmd_train, verbose=0, raise_exception=False)
def propseg(img_input, options_dict): """ :param img_input: source image, to be segmented :param options_dict: arguments as dictionary :return: segmented Image """ arguments = options_dict fname_input_data = img_input.absolutepath fname_data = os.path.abspath(fname_input_data) contrast_type = arguments["-c"] contrast_type_conversion = { 't1': 't1', 't2': 't2', 't2s': 't2', 'dwi': 't1' } contrast_type_propseg = contrast_type_conversion[contrast_type] # Starting building the command cmd = ['isct_propseg', '-t', contrast_type_propseg] if "-ofolder" in arguments: folder_output = arguments["-ofolder"] else: folder_output = './' cmd += ['-o', folder_output] if not os.path.isdir(folder_output) and os.path.exists(folder_output): sct.log.error("output directory %s is not a valid directory" % folder_output) if not os.path.exists(folder_output): os.makedirs(folder_output) if "-down" in arguments: cmd += ["-down", str(arguments["-down"])] if "-up" in arguments: cmd += ["-up", str(arguments["-up"])] remove_temp_files = 1 if "-r" in arguments: remove_temp_files = int(arguments["-r"]) verbose = 0 if "-v" in arguments: if arguments["-v"] is "1": verbose = 2 cmd += ["-verbose"] # Output options if "-mesh" in arguments: cmd += ["-mesh"] if "-centerline-binary" in arguments: cmd += ["-centerline-binary"] if "-CSF" in arguments: cmd += ["-CSF"] if "-centerline-coord" in arguments: cmd += ["-centerline-coord"] if "-cross" in arguments: cmd += ["-cross"] if "-init-tube" in arguments: cmd += ["-init-tube"] if "-low-resolution-mesh" in arguments: cmd += ["-low-resolution-mesh"] if "-detect-nii" in arguments: cmd += ["-detect-nii"] if "-detect-png" in arguments: cmd += ["-detect-png"] # Helping options use_viewer = None use_optic = True # enabled by default init_option = None rescale_header = arguments["-rescale"] if "-init" in arguments: init_option = float(arguments["-init"]) if init_option < 0: sct.log.error('Command-line usage error: ' + str(init_option) + " is not a valid value for '-init'") sys.exit(1) if "-init-centerline" in arguments: if str(arguments["-init-centerline"]) == "viewer": use_viewer = "centerline" elif str(arguments["-init-centerline"]) == "hough": use_optic = False else: if rescale_header is not 1: fname_labels_viewer = func_rescale_header(str( arguments["-init-centerline"]), rescale_header, verbose=verbose) else: fname_labels_viewer = str(arguments["-init-centerline"]) cmd += ["-init-centerline", fname_labels_viewer] use_optic = False if "-init-mask" in arguments: if str(arguments["-init-mask"]) == "viewer": use_viewer = "mask" else: if rescale_header is not 1: fname_labels_viewer = func_rescale_header( str(arguments["-init-mask"]), rescale_header) else: fname_labels_viewer = str(arguments["-init-mask"]) cmd += ["-init-mask", fname_labels_viewer] use_optic = False if "-mask-correction" in arguments: cmd += ["-mask-correction", str(arguments["-mask-correction"])] if "-radius" in arguments: cmd += ["-radius", str(arguments["-radius"])] if "-detect-n" in arguments: cmd += ["-detect-n", str(arguments["-detect-n"])] if "-detect-gap" in arguments: cmd += ["-detect-gap", str(arguments["-detect-gap"])] if "-init-validation" in arguments: cmd += ["-init-validation"] if "-nbiter" in arguments: cmd += ["-nbiter", str(arguments["-nbiter"])] if "-max-area" in arguments: cmd += ["-max-area", str(arguments["-max-area"])] if "-max-deformation" in arguments: cmd += ["-max-deformation", str(arguments["-max-deformation"])] if "-min-contrast" in arguments: cmd += ["-min-contrast", str(arguments["-min-contrast"])] if "-d" in arguments: cmd += ["-d", str(arguments["-d"])] if "-distance-search" in arguments: cmd += ["-dsearch", str(arguments["-distance-search"])] if "-alpha" in arguments: cmd += ["-alpha", str(arguments["-alpha"])] # check if input image is in 3D. Otherwise itk image reader will cut the 4D image in 3D volumes and only take the first one. image_input = Image(fname_data) nx, ny, nz, nt, px, py, pz, pt = image_input.dim if nt > 1: sct.log.error( 'ERROR: your input image needs to be 3D in order to be segmented.') path_data, file_data, ext_data = sct.extract_fname(fname_data) # rescale header (see issue #1406) if rescale_header is not 1: fname_data_propseg = func_rescale_header(fname_data, rescale_header) else: fname_data_propseg = fname_data # add to command cmd += ['-i', fname_data_propseg] # if centerline or mask is asked using viewer if use_viewer: from spinalcordtoolbox.gui.base import AnatomicalParams from spinalcordtoolbox.gui.centerline import launch_centerline_dialog params = AnatomicalParams() if use_viewer == 'mask': params.num_points = 3 params.interval_in_mm = 15 # superior-inferior interval between two consecutive labels params.starting_slice = 'midfovminusinterval' if use_viewer == 'centerline': # setting maximum number of points to a reasonable value params.num_points = 20 params.interval_in_mm = 30 params.starting_slice = 'top' im_data = Image(fname_data_propseg) im_mask_viewer = msct_image.zeros_like(im_data) # im_mask_viewer.absolutepath = sct.add_suffix(fname_data_propseg, '_labels_viewer') controller = launch_centerline_dialog(im_data, im_mask_viewer, params) fname_labels_viewer = sct.add_suffix(fname_data_propseg, '_labels_viewer') if not controller.saved: sct.log.error( 'The viewer has been closed before entering all manual points. Please try again.' ) sys.exit(1) # save labels controller.as_niftii(fname_labels_viewer) # add mask filename to parameters string if use_viewer == "centerline": cmd += ["-init-centerline", fname_labels_viewer] elif use_viewer == "mask": cmd += ["-init-mask", fname_labels_viewer] # If using OptiC elif use_optic: path_script = os.path.dirname(__file__) path_sct = os.path.dirname(path_script) path_classifier = os.path.join(path_sct, 'data/optic_models', '{}_model'.format(contrast_type)) init_option_optic, fname_centerline = optic.detect_centerline( fname_data_propseg, contrast_type, path_classifier, folder_output, remove_temp_files, init_option, verbose=verbose) if init_option is not None: # TODO: what's this??? cmd += ["-init", str(init_option_optic)] cmd += ["-init-centerline", fname_centerline] # enabling centerline extraction by default (needed by check_and_correct_segmentation() ) cmd += ['-centerline-binary'] # run propseg status, output = sct.run(cmd, verbose, raise_exception=False) # check status is not 0 if not status == 0: sct.log.error( 'Automatic cord detection failed. Please initialize using -init-centerline or ' '-init-mask (see help).') sys.exit(1) # build output filename fname_seg = os.path.join( folder_output, os.path.basename(sct.add_suffix(fname_data, "_seg"))) fname_centerline = os.path.join( folder_output, os.path.basename(sct.add_suffix(fname_data, "_centerline"))) # in case header was rescaled, we need to update the output file names by removing the "_rescaled" if rescale_header is not 1: sct.mv( os.path.join( folder_output, sct.add_suffix(os.path.basename(fname_data_propseg), "_seg")), fname_seg) sct.mv( os.path.join( folder_output, sct.add_suffix(os.path.basename(fname_data_propseg), "_centerline")), fname_centerline) # if user was used, copy the labelled points to the output folder (they will then be scaled back) if use_viewer: fname_labels_viewer_new = os.path.join( folder_output, os.path.basename(sct.add_suffix(fname_data, "_labels_viewer"))) sct.copy(fname_labels_viewer, fname_labels_viewer_new) # update variable (used later) fname_labels_viewer = fname_labels_viewer_new # check consistency of segmentation if arguments["-correct-seg"] == "1": check_and_correct_segmentation(fname_seg, fname_centerline, folder_output=folder_output, threshold_distance=3.0, remove_temp_files=remove_temp_files, verbose=verbose) # copy header from input to segmentation to make sure qform is the same sct.printv( "Copy header input --> output(s) to make sure qform is the same.", verbose) list_fname = [fname_seg, fname_centerline] if use_viewer: list_fname.append(fname_labels_viewer) for fname in list_fname: im = Image(fname) im.header = image_input.header im.save(dtype='int8' ) # they are all binary masks hence fine to save as int8 return Image(fname_seg)
def fmri_moco(param): file_data = "fmri.nii" mat_final = 'mat_final/' ext_mat = 'Warp.nii.gz' # warping field # Get dimensions of data sct.printv('\nGet dimensions of data...', param.verbose) im_data = Image(param.fname_data) nx, ny, nz, nt, px, py, pz, pt = im_data.dim sct.printv(' ' + str(nx) + ' x ' + str(ny) + ' x ' + str(nz) + ' x ' + str(nt), param.verbose) # Get orientation sct.printv('\nData orientation: ' + im_data.orientation, param.verbose) if im_data.orientation[2] in 'LR': param.is_sagittal = True sct.printv(' Treated as sagittal') elif im_data.orientation[2] in 'IS': param.is_sagittal = False sct.printv(' Treated as axial') else: param.is_sagittal = False sct.printv('WARNING: Orientation seems to be neither axial nor sagittal.') # Adjust group size in case of sagittal scan if param.is_sagittal and param.group_size != 1: sct.printv('For sagittal data group_size should be one for more robustness. Forcing group_size=1.', 1, 'warning') param.group_size = 1 # Split into T dimension sct.printv('\nSplit along T dimension...', param.verbose) im_data_split_list = split_data(im_data, 3) for im in im_data_split_list: x_dirname, x_basename, x_ext = sct.extract_fname(im.absolutepath) # Make further steps slurp the data to avoid too many open files (#2149) im.absolutepath = os.path.join(x_dirname, x_basename + ".nii.gz") im.save() # assign an index to each volume index_fmri = list(range(0, nt)) # Number of groups nb_groups = int(math.floor(nt / param.group_size)) # Generate groups indexes group_indexes = [] for iGroup in range(nb_groups): group_indexes.append(index_fmri[(iGroup * param.group_size):((iGroup + 1) * param.group_size)]) # add the remaining images to the last fMRI group nb_remaining = nt%param.group_size # number of remaining images if nb_remaining > 0: nb_groups += 1 group_indexes.append(index_fmri[len(index_fmri) - nb_remaining:len(index_fmri)]) # groups for iGroup in tqdm(range(nb_groups), unit='iter', unit_scale=False, desc="Merge within groups", ascii=True, ncols=80): # get index index_fmri_i = group_indexes[iGroup] nt_i = len(index_fmri_i) # Merge Images file_data_merge_i = sct.add_suffix(file_data, '_' + str(iGroup)) # cmd = fsloutput + 'fslmerge -t ' + file_data_merge_i # for it in range(nt_i): # cmd = cmd + ' ' + file_data + '_T' + str(index_fmri_i[it]).zfill(4) im_fmri_list = [] for it in range(nt_i): im_fmri_list.append(im_data_split_list[index_fmri_i[it]]) im_fmri_concat = concat_data(im_fmri_list, 3, squeeze_data=True).save(file_data_merge_i) file_data_mean = sct.add_suffix(file_data, '_mean_' + str(iGroup)) if file_data_mean.endswith(".nii"): file_data_mean += ".gz" # #2149 if param.group_size == 1: # copy to new file name instead of averaging (faster) # note: this is a bandage. Ideally we should skip this entire for loop if g=1 convert(file_data_merge_i, file_data_mean) else: # Average Images sct.run(['sct_maths', '-i', file_data_merge_i, '-o', file_data_mean, '-mean', 't'], verbose=0) # if not average_data_across_dimension(file_data_merge_i+'.nii', file_data_mean+'.nii', 3): # sct.printv('ERROR in average_data_across_dimension', 1, 'error') # cmd = fsloutput + 'fslmaths ' + file_data_merge_i + ' -Tmean ' + file_data_mean # sct.run(cmd, param.verbose) # Merge groups means. The output 4D volume will be used for motion correction. sct.printv('\nMerging volumes...', param.verbose) file_data_groups_means_merge = 'fmri_averaged_groups.nii' im_mean_list = [] for iGroup in range(nb_groups): file_data_mean = sct.add_suffix(file_data, '_mean_' + str(iGroup)) if file_data_mean.endswith(".nii"): file_data_mean += ".gz" # #2149 im_mean_list.append(Image(file_data_mean)) im_mean_concat = concat_data(im_mean_list, 3).save(file_data_groups_means_merge) # Estimate moco sct.printv('\n-------------------------------------------------------------------------------', param.verbose) sct.printv(' Estimating motion...', param.verbose) sct.printv('-------------------------------------------------------------------------------', param.verbose) param_moco = param param_moco.file_data = 'fmri_averaged_groups.nii' param_moco.file_target = sct.add_suffix(file_data, '_mean_' + param.num_target) if param_moco.file_target.endswith(".nii"): param_moco.file_target += ".gz" # #2149 param_moco.path_out = '' param_moco.todo = 'estimate_and_apply' param_moco.mat_moco = 'mat_groups' file_mat = moco.moco(param_moco) # TODO: if g=1, no need to run the block below (already applied) if param.group_size == 1: # if flag g=1, it means that all images have already been corrected, so we just need to rename the file sct.mv('fmri_averaged_groups_moco.nii', 'fmri_moco.nii') else: # create final mat folder sct.create_folder(mat_final) # Copy registration matrices sct.printv('\nCopy transformations...', param.verbose) for iGroup in range(nb_groups): for data in range(len(group_indexes[iGroup])): # we cannot use enumerate because group_indexes has 2 dim. # fetch all file_mat_z for given t-group list_file_mat_z = file_mat[:, iGroup] # loop across file_mat_z and copy to mat_final folder for file_mat_z in list_file_mat_z: # we want to copy 'mat_groups/mat.ZXXXXTYYYYWarp.nii.gz' --> 'mat_final/mat.ZXXXXTYYYZWarp.nii.gz' # Notice the Y->Z in the under the T index: the idea here is to use the single matrix from each group, # and apply it to all images belonging to the same group. sct.copy(file_mat_z + ext_mat, mat_final + file_mat_z[11:20] + 'T' + str(group_indexes[iGroup][data]).zfill(4) + ext_mat) # Apply moco on all fmri data sct.printv('\n-------------------------------------------------------------------------------', param.verbose) sct.printv(' Apply moco', param.verbose) sct.printv('-------------------------------------------------------------------------------', param.verbose) param_moco.file_data = 'fmri.nii' param_moco.file_target = sct.add_suffix(file_data, '_mean_' + str(0)) if param_moco.file_target.endswith(".nii"): param_moco.file_target += ".gz" param_moco.path_out = '' param_moco.mat_moco = mat_final param_moco.todo = 'apply' moco.moco(param_moco) # copy geometric information from header # NB: this is required because WarpImageMultiTransform in 2D mode wrongly sets pixdim(3) to "1". im_fmri = Image('fmri.nii') im_fmri_moco = Image('fmri_moco.nii') im_fmri_moco.header = im_fmri.header im_fmri_moco.save() # Average volumes sct.printv('\nAveraging data...', param.verbose) sct_maths.main(args=['-i', 'fmri_moco.nii', '-o', 'fmri_moco_mean.nii', '-mean', 't', '-v', '0'])
def straighten(self): """ Straighten spinal cord. Steps: (everything is done in physical space) 1. open input image and centreline image 2. extract bspline fitting of the centreline, and its derivatives 3. compute length of centerline 4. compute and generate straight space 5. compute transformations for each voxel of one space: (done using matrices --> improves speed by a factor x300) a. determine which plane of spinal cord centreline it is included b. compute the position of the voxel in the plane (X and Y distance from centreline, along the plane) c. find the correspondant centreline point in the other space d. find the correspondance of the voxel in the corresponding plane 6. generate warping fields for each transformations 7. write warping fields and apply them step 5.b: how to find the corresponding plane? The centerline plane corresponding to a voxel correspond to the nearest point of the centerline. However, we need to compute the distance between the voxel position and the plane to be sure it is part of the plane and not too distant. If it is more far than a threshold, warping value should be 0. step 5.d: how to make the correspondance between centerline point in both images? Both centerline have the same lenght. Therefore, we can map centerline point via their position along the curve. If we use the same number of points uniformely along the spinal cord (1000 for example), the correspondance is straight-forward. :return: """ # Initialization fname_anat = self.input_filename fname_centerline = self.centerline_filename fname_output = self.output_filename remove_temp_files = self.remove_temp_files verbose = self.verbose interpolation_warp = self.interpolation_warp # TODO: remove this # start timer start_time = time.time() # Extract path/file/extension path_anat, file_anat, ext_anat = sct.extract_fname(fname_anat) path_tmp = sct.tmp_create(basename="straighten_spinalcord", verbose=verbose) # Copying input data to tmp folder sct.printv('\nCopy files to tmp folder...', verbose) Image(fname_anat).save(os.path.join(path_tmp, "data.nii")) Image(fname_centerline).save( os.path.join(path_tmp, "centerline.nii.gz")) if self.use_straight_reference: Image(self.centerline_reference_filename).save( os.path.join(path_tmp, "centerline_ref.nii.gz")) if self.discs_input_filename != '': Image(self.discs_input_filename).save( os.path.join(path_tmp, "labels_input.nii.gz")) if self.discs_ref_filename != '': Image(self.discs_ref_filename).save( os.path.join(path_tmp, "labels_ref.nii.gz")) # go to tmp folder curdir = os.getcwd() os.chdir(path_tmp) # Change orientation of the input centerline into RPI image_centerline = Image("centerline.nii.gz").change_orientation( "RPI").save("centerline_rpi.nii.gz", mutable=True) # Get dimension nx, ny, nz, nt, px, py, pz, pt = image_centerline.dim if self.speed_factor != 1.0: intermediate_resampling = True px_r, py_r, pz_r = px * self.speed_factor, py * self.speed_factor, pz * self.speed_factor else: intermediate_resampling = False if intermediate_resampling: sct.mv('centerline_rpi.nii.gz', 'centerline_rpi_native.nii.gz') pz_native = pz # TODO: remove system call sct.run([ 'sct_resample', '-i', 'centerline_rpi_native.nii.gz', '-mm', str(px_r) + 'x' + str(py_r) + 'x' + str(pz_r), '-o', 'centerline_rpi.nii.gz' ]) image_centerline = Image('centerline_rpi.nii.gz') nx, ny, nz, nt, px, py, pz, pt = image_centerline.dim if np.min(image_centerline.data) < 0 or np.max( image_centerline.data) > 1: image_centerline.data[image_centerline.data < 0] = 0 image_centerline.data[image_centerline.data > 1] = 1 image_centerline.save() # 2. extract bspline fitting of the centerline, and its derivatives img_ctl = Image('centerline_rpi.nii.gz') centerline = _get_centerline(img_ctl, self.param_centerline, verbose) number_of_points = centerline.number_of_points # ========================================================================================== logger.info('Create the straight space and the safe zone') # 3. compute length of centerline # compute the length of the spinal cord based on fitted centerline and size of centerline in z direction # Computation of the safe zone. # The safe zone is defined as the length of the spinal cord for which an axial segmentation will be complete # The safe length (to remove) is computed using the safe radius (given as parameter) and the angle of the # last centerline point with the inferior-superior direction. Formula: Ls = Rs * sin(angle) # Calculate Ls for both edges and remove appropriate number of centerline points radius_safe = 0.0 # mm # inferior edge u = centerline.derivatives[0] v = np.array([0, 0, -1]) angle_inferior = np.arctan2(np.linalg.norm(np.cross(u, v)), np.dot(u, v)) length_safe_inferior = radius_safe * np.sin(angle_inferior) # superior edge u = centerline.derivatives[-1] v = np.array([0, 0, 1]) angle_superior = np.arctan2(np.linalg.norm(np.cross(u, v)), np.dot(u, v)) length_safe_superior = radius_safe * np.sin(angle_superior) # remove points inferior_bound = bisect.bisect(centerline.progressive_length, length_safe_inferior) - 1 superior_bound = centerline.number_of_points - bisect.bisect( centerline.progressive_length_inverse, length_safe_superior) z_centerline = centerline.points[:, 2] length_centerline = centerline.length size_z_centerline = z_centerline[-1] - z_centerline[0] # compute the size factor between initial centerline and straight bended centerline factor_curved_straight = length_centerline / size_z_centerline middle_slice = (z_centerline[0] + z_centerline[-1]) / 2.0 bound_curved = [ z_centerline[inferior_bound], z_centerline[superior_bound] ] bound_straight = [(z_centerline[inferior_bound] - middle_slice) * factor_curved_straight + middle_slice, (z_centerline[superior_bound] - middle_slice) * factor_curved_straight + middle_slice] logger.info('Length of spinal cord: {}'.format(length_centerline)) logger.info( 'Size of spinal cord in z direction: {}'.format(size_z_centerline)) logger.info('Ratio length/size: {}'.format(factor_curved_straight)) logger.info( 'Safe zone boundaries (curved space): {}'.format(bound_curved)) logger.info( 'Safe zone boundaries (straight space): {}'.format(bound_straight)) # 4. compute and generate straight space # points along curved centerline are already regularly spaced. # calculate position of points along straight centerline # Create straight NIFTI volumes. # ========================================================================================== # TODO: maybe this if case is not needed? if self.use_straight_reference: image_centerline_pad = Image('centerline_rpi.nii.gz') nx, ny, nz, nt, px, py, pz, pt = image_centerline_pad.dim fname_ref = 'centerline_ref_rpi.nii.gz' image_centerline_straight = Image('centerline_ref.nii.gz') \ .change_orientation("RPI") \ .save(fname_ref, mutable=True) centerline_straight = _get_centerline(image_centerline_straight, algo_fitting, self.degree, verbose) nx_s, ny_s, nz_s, nt_s, px_s, py_s, pz_s, pt_s = image_centerline_straight.dim # Prepare warping fields headers hdr_warp = image_centerline_pad.hdr.copy() hdr_warp.set_data_dtype('float32') hdr_warp_s = image_centerline_straight.hdr.copy() hdr_warp_s.set_data_dtype('float32') if self.discs_input_filename != "" and self.discs_ref_filename != "": discs_input_image = Image('labels_input.nii.gz') coord = discs_input_image.getNonZeroCoordinates( sorting='z', reverse_coord=True) coord_physical = [] for c in coord: c_p = discs_input_image.transfo_pix2phys([[c.x, c.y, c.z] ]).tolist()[0] c_p.append(c.value) coord_physical.append(c_p) centerline.compute_vertebral_distribution(coord_physical) centerline.save_centerline( image=discs_input_image, fname_output='discs_input_image.nii.gz') discs_ref_image = Image('labels_ref.nii.gz') coord = discs_ref_image.getNonZeroCoordinates( sorting='z', reverse_coord=True) coord_physical = [] for c in coord: c_p = discs_ref_image.transfo_pix2phys([[c.x, c.y, c.z]]).tolist()[0] c_p.append(c.value) coord_physical.append(c_p) centerline_straight.compute_vertebral_distribution( coord_physical) centerline_straight.save_centerline( image=discs_ref_image, fname_output='discs_ref_image.nii.gz') else: logger.info( 'Pad input volume to account for spinal cord length...') start_point, end_point = bound_straight[0], bound_straight[1] offset_z = 0 # if the destination image is resampled, we still create the straight reference space with the native # resolution. # TODO: Maybe this if case is not needed? if intermediate_resampling: padding_z = int( np.ceil(1.5 * ((length_centerline - size_z_centerline) / 2.0) / pz_native)) sct.run([ 'sct_image', '-i', 'centerline_rpi_native.nii.gz', '-o', 'tmp.centerline_pad_native.nii.gz', '-pad', '0,0,' + str(padding_z) ]) image_centerline_pad = Image('centerline_rpi_native.nii.gz') nx, ny, nz, nt, px, py, pz, pt = image_centerline_pad.dim start_point_coord_native = image_centerline_pad.transfo_phys2pix( [[0, 0, start_point]])[0] end_point_coord_native = image_centerline_pad.transfo_phys2pix( [[0, 0, end_point]])[0] straight_size_x = int(self.xy_size / px) straight_size_y = int(self.xy_size / py) warp_space_x = [ int(np.round(nx / 2)) - straight_size_x, int(np.round(nx / 2)) + straight_size_x ] warp_space_y = [ int(np.round(ny / 2)) - straight_size_y, int(np.round(ny / 2)) + straight_size_y ] if warp_space_x[0] < 0: warp_space_x[1] += warp_space_x[0] - 2 warp_space_x[0] = 0 if warp_space_y[0] < 0: warp_space_y[1] += warp_space_y[0] - 2 warp_space_y[0] = 0 spec = dict(( (0, warp_space_x), (1, warp_space_y), (2, (0, end_point_coord_native[2] - start_point_coord_native[2])), )) msct_image.spatial_crop( Image("tmp.centerline_pad_native.nii.gz"), spec).save("tmp.centerline_pad_crop_native.nii.gz") fname_ref = 'tmp.centerline_pad_crop_native.nii.gz' offset_z = 4 else: fname_ref = 'tmp.centerline_pad_crop.nii.gz' nx, ny, nz, nt, px, py, pz, pt = image_centerline.dim padding_z = int( np.ceil(1.5 * ((length_centerline - size_z_centerline) / 2.0) / pz)) + offset_z image_centerline_pad = pad_image(image_centerline, pad_z_i=padding_z, pad_z_f=padding_z) nx, ny, nz = image_centerline_pad.data.shape hdr_warp = image_centerline_pad.hdr.copy() hdr_warp.set_data_dtype('float32') start_point_coord = image_centerline_pad.transfo_phys2pix( [[0, 0, start_point]])[0] end_point_coord = image_centerline_pad.transfo_phys2pix( [[0, 0, end_point]])[0] straight_size_x = int(self.xy_size / px) straight_size_y = int(self.xy_size / py) warp_space_x = [ int(np.round(nx / 2)) - straight_size_x, int(np.round(nx / 2)) + straight_size_x ] warp_space_y = [ int(np.round(ny / 2)) - straight_size_y, int(np.round(ny / 2)) + straight_size_y ] if warp_space_x[0] < 0: warp_space_x[1] += warp_space_x[0] - 2 warp_space_x[0] = 0 if warp_space_x[1] >= nx: warp_space_x[1] = nx - 1 if warp_space_y[0] < 0: warp_space_y[1] += warp_space_y[0] - 2 warp_space_y[0] = 0 if warp_space_y[1] >= ny: warp_space_y[1] = ny - 1 spec = dict(( (0, warp_space_x), (1, warp_space_y), (2, (0, end_point_coord[2] - start_point_coord[2] + offset_z)), )) image_centerline_straight = msct_image.spatial_crop( image_centerline_pad, spec) nx_s, ny_s, nz_s, nt_s, px_s, py_s, pz_s, pt_s = image_centerline_straight.dim hdr_warp_s = image_centerline_straight.hdr.copy() hdr_warp_s.set_data_dtype('float32') if self.template_orientation == 1: raise NotImplementedError() start_point_coord = image_centerline_pad.transfo_phys2pix( [[0, 0, start_point]])[0] end_point_coord = image_centerline_pad.transfo_phys2pix( [[0, 0, end_point]])[0] number_of_voxel = nx * ny * nz logger.debug('Number of voxels: {}'.format(number_of_voxel)) time_centerlines = time.time() coord_straight = np.empty((number_of_points, 3)) coord_straight[..., 0] = int(np.round(nx_s / 2)) coord_straight[..., 1] = int(np.round(ny_s / 2)) coord_straight[..., 2] = np.linspace( 0, end_point_coord[2] - start_point_coord[2], number_of_points) coord_phys_straight = image_centerline_straight.transfo_pix2phys( coord_straight) derivs_straight = np.empty((number_of_points, 3)) derivs_straight[..., 0] = derivs_straight[..., 1] = 0 derivs_straight[..., 2] = 1 dx_straight, dy_straight, dz_straight = derivs_straight.T centerline_straight = Centerline(coord_phys_straight[:, 0], coord_phys_straight[:, 1], coord_phys_straight[:, 2], dx_straight, dy_straight, dz_straight) time_centerlines = time.time() - time_centerlines logger.info('Time to generate centerline: {} ms'.format( np.round(time_centerlines * 1000.0))) if verbose == 2: # TODO: use OO import matplotlib.pyplot as plt from datetime import datetime curved_points = centerline.progressive_length straight_points = centerline_straight.progressive_length range_points = np.linspace(0, 1, number_of_points) dist_curved = np.zeros(number_of_points) dist_straight = np.zeros(number_of_points) for i in range(1, number_of_points): dist_curved[i] = dist_curved[ i - 1] + curved_points[i - 1] / centerline.length dist_straight[i] = dist_straight[i - 1] + straight_points[ i - 1] / centerline_straight.length plt.plot(range_points, dist_curved) plt.plot(range_points, dist_straight) plt.grid(True) plt.savefig('fig_straighten_' + datetime.now().strftime("%y%m%d%H%M%S%f") + '.png') plt.close() # alignment_mode = 'length' alignment_mode = 'levels' lookup_curved2straight = list(range(centerline.number_of_points)) if self.discs_input_filename != "": # create look-up table curved to straight for index in range(centerline.number_of_points): disc_label = centerline.l_points[index] if alignment_mode == 'length': relative_position = centerline.dist_points[index] else: relative_position = centerline.dist_points_rel[index] idx_closest = centerline_straight.get_closest_to_absolute_position( disc_label, relative_position, backup_index=index, backup_centerline=centerline_straight, mode=alignment_mode) if idx_closest is not None: lookup_curved2straight[index] = idx_closest else: lookup_curved2straight[index] = 0 for p in range(0, len(lookup_curved2straight) // 2): if lookup_curved2straight[p] == lookup_curved2straight[p + 1]: lookup_curved2straight[p] = 0 else: break for p in range( len(lookup_curved2straight) - 1, len(lookup_curved2straight) // 2, -1): if lookup_curved2straight[p] == lookup_curved2straight[p - 1]: lookup_curved2straight[p] = 0 else: break lookup_curved2straight = np.array(lookup_curved2straight) lookup_straight2curved = list( range(centerline_straight.number_of_points)) if self.discs_input_filename != "": for index in range(centerline_straight.number_of_points): disc_label = centerline_straight.l_points[index] if alignment_mode == 'length': relative_position = centerline_straight.dist_points[index] else: relative_position = centerline_straight.dist_points_rel[ index] idx_closest = centerline.get_closest_to_absolute_position( disc_label, relative_position, backup_index=index, backup_centerline=centerline_straight, mode=alignment_mode) if idx_closest is not None: lookup_straight2curved[index] = idx_closest for p in range(0, len(lookup_straight2curved) // 2): if lookup_straight2curved[p] == lookup_straight2curved[p + 1]: lookup_straight2curved[p] = 0 else: break for p in range( len(lookup_straight2curved) - 1, len(lookup_straight2curved) // 2, -1): if lookup_straight2curved[p] == lookup_straight2curved[p - 1]: lookup_straight2curved[p] = 0 else: break lookup_straight2curved = np.array(lookup_straight2curved) # Create volumes containing curved and straight warping fields data_warp_curved2straight = np.zeros((nx_s, ny_s, nz_s, 1, 3)) data_warp_straight2curved = np.zeros((nx, ny, nz, 1, 3)) # 5. compute transformations # Curved and straight images and the same dimensions, so we compute both warping fields at the same time. # b. determine which plane of spinal cord centreline it is included # sct.printv(nx * ny * nz, nx_s * ny_s * nz_s) if self.curved2straight: for u in tqdm(range(nz_s)): x_s, y_s, z_s = np.mgrid[0:nx_s, 0:ny_s, u:u + 1] indexes_straight = np.array( list(zip(x_s.ravel(), y_s.ravel(), z_s.ravel()))) physical_coordinates_straight = image_centerline_straight.transfo_pix2phys( indexes_straight) nearest_indexes_straight = centerline_straight.find_nearest_indexes( physical_coordinates_straight) distances_straight = centerline_straight.get_distances_from_planes( physical_coordinates_straight, nearest_indexes_straight) lookup = lookup_straight2curved[nearest_indexes_straight] indexes_out_distance_straight = np.logical_or( np.logical_or( distances_straight > self.threshold_distance, distances_straight < -self.threshold_distance), lookup == 0) projected_points_straight = centerline_straight.get_projected_coordinates_on_planes( physical_coordinates_straight, nearest_indexes_straight) coord_in_planes_straight = centerline_straight.get_in_plans_coordinates( projected_points_straight, nearest_indexes_straight) coord_straight2curved = centerline.get_inverse_plans_coordinates( coord_in_planes_straight, lookup) displacements_straight = coord_straight2curved - physical_coordinates_straight # Invert Z coordinate as ITK & ANTs physical coordinate system is LPS- (RAI+) # while ours is LPI- # Refs: https://sourceforge.net/p/advants/discussion/840261/thread/2a1e9307/#fb5a # https://www.slicer.org/wiki/Coordinate_systems displacements_straight[:, 2] = -displacements_straight[:, 2] displacements_straight[indexes_out_distance_straight] = [ 100000.0, 100000.0, 100000.0 ] data_warp_curved2straight[indexes_straight[:, 0], indexes_straight[:, 1], indexes_straight[:, 2], 0, :]\ = -displacements_straight if self.straight2curved: for u in tqdm(range(nz)): x, y, z = np.mgrid[0:nx, 0:ny, u:u + 1] indexes = np.array(list(zip(x.ravel(), y.ravel(), z.ravel()))) physical_coordinates = image_centerline_pad.transfo_pix2phys( indexes) nearest_indexes_curved = centerline.find_nearest_indexes( physical_coordinates) distances_curved = centerline.get_distances_from_planes( physical_coordinates, nearest_indexes_curved) lookup = lookup_curved2straight[nearest_indexes_curved] indexes_out_distance_curved = np.logical_or( np.logical_or(distances_curved > self.threshold_distance, distances_curved < -self.threshold_distance), lookup == 0) projected_points_curved = centerline.get_projected_coordinates_on_planes( physical_coordinates, nearest_indexes_curved) coord_in_planes_curved = centerline.get_in_plans_coordinates( projected_points_curved, nearest_indexes_curved) coord_curved2straight = centerline_straight.points[lookup] coord_curved2straight[:, 0:2] += coord_in_planes_curved[:, 0:2] coord_curved2straight[:, 2] += distances_curved displacements_curved = coord_curved2straight - physical_coordinates displacements_curved[:, 2] = -displacements_curved[:, 2] displacements_curved[indexes_out_distance_curved] = [ 100000.0, 100000.0, 100000.0 ] data_warp_straight2curved[indexes[:, 0], indexes[:, 1], indexes[:, 2], 0, :] = -displacements_curved # Creation of the safe zone based on pre-calculated safe boundaries coord_bound_curved_inf, coord_bound_curved_sup = image_centerline_pad.transfo_phys2pix( [[0, 0, bound_curved[0]]]), image_centerline_pad.transfo_phys2pix( [[0, 0, bound_curved[1]]]) coord_bound_straight_inf, coord_bound_straight_sup = image_centerline_straight.transfo_phys2pix( [[0, 0, bound_straight[0]]]), image_centerline_straight.transfo_phys2pix( [[0, 0, bound_straight[1]]]) if radius_safe > 0: data_warp_curved2straight[:, :, 0:coord_bound_straight_inf[0][2], 0, :] = 100000.0 data_warp_curved2straight[:, :, coord_bound_straight_sup[0][2]:, 0, :] = 100000.0 data_warp_straight2curved[:, :, 0:coord_bound_curved_inf[0][2], 0, :] = 100000.0 data_warp_straight2curved[:, :, coord_bound_curved_sup[0][2]:, 0, :] = 100000.0 # Generate warp files as a warping fields hdr_warp_s.set_intent('vector', (), '') hdr_warp_s.set_data_dtype('float32') hdr_warp.set_intent('vector', (), '') hdr_warp.set_data_dtype('float32') if self.curved2straight: img = Nifti1Image(data_warp_curved2straight, None, hdr_warp_s) save(img, 'tmp.curve2straight.nii.gz') logger.info('Warping field generated: tmp.curve2straight.nii.gz') if self.straight2curved: img = Nifti1Image(data_warp_straight2curved, None, hdr_warp) save(img, 'tmp.straight2curve.nii.gz') logger.info('Warping field generated: tmp.straight2curve.nii.gz') image_centerline_straight.save(fname_ref) if self.curved2straight: logger.info('Apply transformation to input image...') sct.run([ 'isct_antsApplyTransforms', '-d', '3', '-r', fname_ref, '-i', 'data.nii', '-o', 'tmp.anat_rigid_warp.nii.gz', '-t', 'tmp.curve2straight.nii.gz', '-n', 'BSpline[3]' ], is_sct_binary=True, verbose=verbose) if self.accuracy_results: time_accuracy_results = time.time() # compute the error between the straightened centerline/segmentation and the central vertical line. # Ideally, the error should be zero. # Apply deformation to input image logger.info('Apply transformation to centerline image...') sct.run([ 'isct_antsApplyTransforms', '-d', '3', '-r', fname_ref, '-i', 'centerline.nii.gz', '-o', 'tmp.centerline_straight.nii.gz', '-t', 'tmp.curve2straight.nii.gz', '-n', 'NearestNeighbor' ], is_sct_binary=True, verbose=verbose) file_centerline_straight = Image('tmp.centerline_straight.nii.gz', verbose=verbose) nx, ny, nz, nt, px, py, pz, pt = file_centerline_straight.dim coordinates_centerline = file_centerline_straight.getNonZeroCoordinates( sorting='z') mean_coord = [] for z in range(coordinates_centerline[0].z, coordinates_centerline[-1].z): temp_mean = [ coord.value for coord in coordinates_centerline if coord.z == z ] if temp_mean: mean_value = np.mean(temp_mean) mean_coord.append( np.mean([[ coord.x * coord.value / mean_value, coord.y * coord.value / mean_value ] for coord in coordinates_centerline if coord.z == z], axis=0)) # compute error between the straightened centerline and the straight line. x0 = file_centerline_straight.data.shape[0] / 2.0 y0 = file_centerline_straight.data.shape[1] / 2.0 count_mean = 0 if number_of_points >= 10: mean_c = mean_coord[ 2: -2] # we don't include the four extrema because there are usually messy. else: mean_c = mean_coord for coord_z in mean_c: if not np.isnan(np.sum(coord_z)): dist = ((x0 - coord_z[0]) * px)**2 + ( (y0 - coord_z[1]) * py)**2 self.mse_straightening += dist dist = np.sqrt(dist) if dist > self.max_distance_straightening: self.max_distance_straightening = dist count_mean += 1 self.mse_straightening = np.sqrt(self.mse_straightening / float(count_mean)) self.elapsed_time_accuracy = time.time() - time_accuracy_results os.chdir(curdir) # Generate output file (in current folder) # TODO: do not uncompress the warping field, it is too time consuming! logger.info('Generate output files...') if self.curved2straight: sct.generate_output_file( os.path.join(path_tmp, "tmp.curve2straight.nii.gz"), os.path.join(self.path_output, "warp_curve2straight.nii.gz"), verbose) if self.straight2curved: sct.generate_output_file( os.path.join(path_tmp, "tmp.straight2curve.nii.gz"), os.path.join(self.path_output, "warp_straight2curve.nii.gz"), verbose) # create ref_straight.nii.gz file that can be used by other SCT functions that need a straight reference space if self.curved2straight: sct.copy(os.path.join(path_tmp, "tmp.anat_rigid_warp.nii.gz"), os.path.join(self.path_output, "straight_ref.nii.gz")) # move straightened input file if fname_output == '': fname_straight = sct.generate_output_file( os.path.join(path_tmp, "tmp.anat_rigid_warp.nii.gz"), os.path.join(self.path_output, file_anat + "_straight" + ext_anat), verbose) else: fname_straight = sct.generate_output_file( os.path.join(path_tmp, "tmp.anat_rigid_warp.nii.gz"), os.path.join(self.path_output, fname_output), verbose) # straightened anatomic # Remove temporary files if remove_temp_files: logger.info('Remove temporary files...') sct.rmtree(path_tmp) if self.accuracy_results: logger.info('Maximum x-y error: {} mm'.format( self.max_distance_straightening)) logger.info('Accuracy of straightening (MSE): {} mm'.format( self.mse_straightening)) # display elapsed time self.elapsed_time = int(np.round(time.time() - start_time)) return fname_straight
def straighten(self): """ Straighten spinal cord. Steps: (everything is done in physical space) 1. open input image and centreline image 2. extract bspline fitting of the centreline, and its derivatives 3. compute length of centerline 4. compute and generate straight space 5. compute transformations for each voxel of one space: (done using matrices --> improves speed by a factor x300) a. determine which plane of spinal cord centreline it is included b. compute the position of the voxel in the plane (X and Y distance from centreline, along the plane) c. find the correspondant centreline point in the other space d. find the correspondance of the voxel in the corresponding plane 6. generate warping fields for each transformations 7. write warping fields and apply them step 5.b: how to find the corresponding plane? The centerline plane corresponding to a voxel correspond to the nearest point of the centerline. However, we need to compute the distance between the voxel position and the plane to be sure it is part of the plane and not too distant. If it is more far than a threshold, warping value should be 0. step 5.d: how to make the correspondance between centerline point in both images? Both centerline have the same lenght. Therefore, we can map centerline point via their position along the curve. If we use the same number of points uniformely along the spinal cord (1000 for example), the correspondance is straight-forward. :return: """ # Initialization fname_anat = self.input_filename fname_centerline = self.centerline_filename fname_output = self.output_filename remove_temp_files = self.remove_temp_files verbose = self.verbose interpolation_warp = self.interpolation_warp algo_fitting = self.algo_fitting # start timer start_time = time.time() # Extract path/file/extension path_anat, file_anat, ext_anat = sct.extract_fname(fname_anat) path_tmp = sct.tmp_create(basename="straighten_spinalcord", verbose=verbose) # Copying input data to tmp folder sct.printv('\nCopy files to tmp folder...', verbose) Image(fname_anat).save(os.path.join(path_tmp, "data.nii")) Image(fname_centerline).save(os.path.join(path_tmp, "centerline.nii.gz")) if self.use_straight_reference: Image(self.centerline_reference_filename).save(os.path.join(path_tmp, "centerline_ref.nii.gz")) if self.discs_input_filename != '': Image(self.discs_input_filename).save(os.path.join(path_tmp, "labels_input.nii.gz")) if self.discs_ref_filename != '': Image(self.discs_ref_filename).save(os.path.join(path_tmp, "labels_ref.nii.gz")) # go to tmp folder curdir = os.getcwd() os.chdir(path_tmp) # Change orientation of the input centerline into RPI image_centerline = Image("centerline.nii.gz").change_orientation("RPI").save("centerline_rpi.nii.gz", mutable=True) # Get dimension nx, ny, nz, nt, px, py, pz, pt = image_centerline.dim if self.speed_factor != 1.0: intermediate_resampling = True px_r, py_r, pz_r = px * self.speed_factor, py * self.speed_factor, pz * self.speed_factor else: intermediate_resampling = False if intermediate_resampling: sct.mv('centerline_rpi.nii.gz', 'centerline_rpi_native.nii.gz') pz_native = pz # TODO: remove system call sct.run(['sct_resample', '-i', 'centerline_rpi_native.nii.gz', '-mm', str(px_r) + 'x' + str(py_r) + 'x' + str(pz_r), '-o', 'centerline_rpi.nii.gz']) image_centerline = Image('centerline_rpi.nii.gz') nx, ny, nz, nt, px, py, pz, pt = image_centerline.dim if np.min(image_centerline.data) < 0 or np.max(image_centerline.data) > 1: image_centerline.data[image_centerline.data < 0] = 0 image_centerline.data[image_centerline.data > 1] = 1 image_centerline.save() # 2. extract bspline fitting of the centerline, and its derivatives img_ctl = Image('centerline_rpi.nii.gz') centerline = _get_centerline(img_ctl, algo_fitting, self.degree, verbose) number_of_points = centerline.number_of_points # ========================================================================================== logger.info('Create the straight space and the safe zone') # 3. compute length of centerline # compute the length of the spinal cord based on fitted centerline and size of centerline in z direction # Computation of the safe zone. # The safe zone is defined as the length of the spinal cord for which an axial segmentation will be complete # The safe length (to remove) is computed using the safe radius (given as parameter) and the angle of the # last centerline point with the inferior-superior direction. Formula: Ls = Rs * sin(angle) # Calculate Ls for both edges and remove appropriate number of centerline points radius_safe = 0.0 # mm # inferior edge u = centerline.derivatives[0] v = np.array([0, 0, -1]) angle_inferior = np.arctan2(np.linalg.norm(np.cross(u, v)), np.dot(u, v)) length_safe_inferior = radius_safe * np.sin(angle_inferior) # superior edge u = centerline.derivatives[-1] v = np.array([0, 0, 1]) angle_superior = np.arctan2(np.linalg.norm(np.cross(u, v)), np.dot(u, v)) length_safe_superior = radius_safe * np.sin(angle_superior) # remove points inferior_bound = bisect.bisect(centerline.progressive_length, length_safe_inferior) - 1 superior_bound = centerline.number_of_points - bisect.bisect(centerline.progressive_length_inverse, length_safe_superior) z_centerline = centerline.points[:, 2] length_centerline = centerline.length size_z_centerline = z_centerline[-1] - z_centerline[0] # compute the size factor between initial centerline and straight bended centerline factor_curved_straight = length_centerline / size_z_centerline middle_slice = (z_centerline[0] + z_centerline[-1]) / 2.0 bound_curved = [z_centerline[inferior_bound], z_centerline[superior_bound]] bound_straight = [(z_centerline[inferior_bound] - middle_slice) * factor_curved_straight + middle_slice, (z_centerline[superior_bound] - middle_slice) * factor_curved_straight + middle_slice] logger.info('Length of spinal cord: {}'.format(length_centerline)) logger.info('Size of spinal cord in z direction: {}'.format(size_z_centerline)) logger.info('Ratio length/size: {}'.format(factor_curved_straight)) logger.info('Safe zone boundaries (curved space): {}'.format(bound_curved)) logger.info('Safe zone boundaries (straight space): {}'.format(bound_straight)) # 4. compute and generate straight space # points along curved centerline are already regularly spaced. # calculate position of points along straight centerline # Create straight NIFTI volumes. # ========================================================================================== # TODO: maybe this if case is not needed? if self.use_straight_reference: image_centerline_pad = Image('centerline_rpi.nii.gz') nx, ny, nz, nt, px, py, pz, pt = image_centerline_pad.dim fname_ref = 'centerline_ref_rpi.nii.gz' image_centerline_straight = Image('centerline_ref.nii.gz') \ .change_orientation("RPI") \ .save(fname_ref, mutable=True) centerline_straight = _get_centerline(image_centerline_straight, algo_fitting, self.degree, verbose) nx_s, ny_s, nz_s, nt_s, px_s, py_s, pz_s, pt_s = image_centerline_straight.dim # Prepare warping fields headers hdr_warp = image_centerline_pad.hdr.copy() hdr_warp.set_data_dtype('float32') hdr_warp_s = image_centerline_straight.hdr.copy() hdr_warp_s.set_data_dtype('float32') if self.discs_input_filename != "" and self.discs_ref_filename != "": discs_input_image = Image('labels_input.nii.gz') coord = discs_input_image.getNonZeroCoordinates(sorting='z', reverse_coord=True) coord_physical = [] for c in coord: c_p = discs_input_image.transfo_pix2phys([[c.x, c.y, c.z]]).tolist()[0] c_p.append(c.value) coord_physical.append(c_p) centerline.compute_vertebral_distribution(coord_physical) centerline.save_centerline(image=discs_input_image, fname_output='discs_input_image.nii.gz') discs_ref_image = Image('labels_ref.nii.gz') coord = discs_ref_image.getNonZeroCoordinates(sorting='z', reverse_coord=True) coord_physical = [] for c in coord: c_p = discs_ref_image.transfo_pix2phys([[c.x, c.y, c.z]]).tolist()[0] c_p.append(c.value) coord_physical.append(c_p) centerline_straight.compute_vertebral_distribution(coord_physical) centerline_straight.save_centerline(image=discs_ref_image, fname_output='discs_ref_image.nii.gz') else: logger.info('Pad input volume to account for spinal cord length...') start_point, end_point = bound_straight[0], bound_straight[1] offset_z = 0 # if the destination image is resampled, we still create the straight reference space with the native # resolution. # TODO: Maybe this if case is not needed? if intermediate_resampling: padding_z = int(np.ceil(1.5 * ((length_centerline - size_z_centerline) / 2.0) / pz_native)) sct.run( ['sct_image', '-i', 'centerline_rpi_native.nii.gz', '-o', 'tmp.centerline_pad_native.nii.gz', '-pad', '0,0,' + str(padding_z)]) image_centerline_pad = Image('centerline_rpi_native.nii.gz') nx, ny, nz, nt, px, py, pz, pt = image_centerline_pad.dim start_point_coord_native = image_centerline_pad.transfo_phys2pix([[0, 0, start_point]])[0] end_point_coord_native = image_centerline_pad.transfo_phys2pix([[0, 0, end_point]])[0] straight_size_x = int(self.xy_size / px) straight_size_y = int(self.xy_size / py) warp_space_x = [int(np.round(nx / 2)) - straight_size_x, int(np.round(nx / 2)) + straight_size_x] warp_space_y = [int(np.round(ny / 2)) - straight_size_y, int(np.round(ny / 2)) + straight_size_y] if warp_space_x[0] < 0: warp_space_x[1] += warp_space_x[0] - 2 warp_space_x[0] = 0 if warp_space_y[0] < 0: warp_space_y[1] += warp_space_y[0] - 2 warp_space_y[0] = 0 spec = dict(( (0, warp_space_x), (1, warp_space_y), (2, (0, end_point_coord_native[2] - start_point_coord_native[2])), )) msct_image.spatial_crop(Image("tmp.centerline_pad_native.nii.gz"), spec).save( "tmp.centerline_pad_crop_native.nii.gz") fname_ref = 'tmp.centerline_pad_crop_native.nii.gz' offset_z = 4 else: fname_ref = 'tmp.centerline_pad_crop.nii.gz' nx, ny, nz, nt, px, py, pz, pt = image_centerline.dim padding_z = int(np.ceil(1.5 * ((length_centerline - size_z_centerline) / 2.0) / pz)) + offset_z image_centerline_pad = pad_image(image_centerline, pad_z_i=padding_z, pad_z_f=padding_z) nx, ny, nz = image_centerline_pad.data.shape hdr_warp = image_centerline_pad.hdr.copy() hdr_warp.set_data_dtype('float32') start_point_coord = image_centerline_pad.transfo_phys2pix([[0, 0, start_point]])[0] end_point_coord = image_centerline_pad.transfo_phys2pix([[0, 0, end_point]])[0] straight_size_x = int(self.xy_size / px) straight_size_y = int(self.xy_size / py) warp_space_x = [int(np.round(nx / 2)) - straight_size_x, int(np.round(nx / 2)) + straight_size_x] warp_space_y = [int(np.round(ny / 2)) - straight_size_y, int(np.round(ny / 2)) + straight_size_y] if warp_space_x[0] < 0: warp_space_x[1] += warp_space_x[0] - 2 warp_space_x[0] = 0 if warp_space_x[1] >= nx: warp_space_x[1] = nx - 1 if warp_space_y[0] < 0: warp_space_y[1] += warp_space_y[0] - 2 warp_space_y[0] = 0 if warp_space_y[1] >= ny: warp_space_y[1] = ny - 1 spec = dict(( (0, warp_space_x), (1, warp_space_y), (2, (0, end_point_coord[2] - start_point_coord[2] + offset_z)), )) image_centerline_straight = msct_image.spatial_crop(image_centerline_pad, spec) nx_s, ny_s, nz_s, nt_s, px_s, py_s, pz_s, pt_s = image_centerline_straight.dim hdr_warp_s = image_centerline_straight.hdr.copy() hdr_warp_s.set_data_dtype('float32') if self.template_orientation == 1: raise NotImplementedError() start_point_coord = image_centerline_pad.transfo_phys2pix([[0, 0, start_point]])[0] end_point_coord = image_centerline_pad.transfo_phys2pix([[0, 0, end_point]])[0] number_of_voxel = nx * ny * nz logger.debug('Number of voxels: {}'.format(number_of_voxel)) time_centerlines = time.time() coord_straight = np.empty((number_of_points, 3)) coord_straight[..., 0] = int(np.round(nx_s / 2)) coord_straight[..., 1] = int(np.round(ny_s / 2)) coord_straight[..., 2] = np.linspace(0, end_point_coord[2] - start_point_coord[2], number_of_points) coord_phys_straight = image_centerline_straight.transfo_pix2phys(coord_straight) derivs_straight = np.empty((number_of_points, 3)) derivs_straight[..., 0] = derivs_straight[..., 1] = 0 derivs_straight[..., 2] = 1 dx_straight, dy_straight, dz_straight = derivs_straight.T centerline_straight = Centerline(coord_phys_straight[:, 0], coord_phys_straight[:, 1], coord_phys_straight[:, 2], dx_straight, dy_straight, dz_straight) time_centerlines = time.time() - time_centerlines logger.info('Time to generate centerline: {} ms'.format(np.round(time_centerlines * 1000.0))) if verbose == 2: # TODO: use OO import matplotlib.pyplot as plt from datetime import datetime curved_points = centerline.progressive_length straight_points = centerline_straight.progressive_length range_points = np.linspace(0, 1, number_of_points) dist_curved = np.zeros(number_of_points) dist_straight = np.zeros(number_of_points) for i in range(1, number_of_points): dist_curved[i] = dist_curved[i - 1] + curved_points[i - 1] / centerline.length dist_straight[i] = dist_straight[i - 1] + straight_points[i - 1] / centerline_straight.length plt.plot(range_points, dist_curved) plt.plot(range_points, dist_straight) plt.grid(True) plt.savefig('fig_straighten_' + datetime.now().strftime("%y%m%d%H%M%S%f") + '.png') plt.close() # alignment_mode = 'length' alignment_mode = 'levels' lookup_curved2straight = list(range(centerline.number_of_points)) if self.discs_input_filename != "": # create look-up table curved to straight for index in range(centerline.number_of_points): disc_label = centerline.l_points[index] if alignment_mode == 'length': relative_position = centerline.dist_points[index] else: relative_position = centerline.dist_points_rel[index] idx_closest = centerline_straight.get_closest_to_absolute_position(disc_label, relative_position, backup_index=index, backup_centerline=centerline_straight, mode=alignment_mode) if idx_closest is not None: lookup_curved2straight[index] = idx_closest else: lookup_curved2straight[index] = 0 for p in range(0, len(lookup_curved2straight) // 2): if lookup_curved2straight[p] == lookup_curved2straight[p + 1]: lookup_curved2straight[p] = 0 else: break for p in range(len(lookup_curved2straight) - 1, len(lookup_curved2straight) // 2, -1): if lookup_curved2straight[p] == lookup_curved2straight[p - 1]: lookup_curved2straight[p] = 0 else: break lookup_curved2straight = np.array(lookup_curved2straight) lookup_straight2curved = list(range(centerline_straight.number_of_points)) if self.discs_input_filename != "": for index in range(centerline_straight.number_of_points): disc_label = centerline_straight.l_points[index] if alignment_mode == 'length': relative_position = centerline_straight.dist_points[index] else: relative_position = centerline_straight.dist_points_rel[index] idx_closest = centerline.get_closest_to_absolute_position(disc_label, relative_position, backup_index=index, backup_centerline=centerline_straight, mode=alignment_mode) if idx_closest is not None: lookup_straight2curved[index] = idx_closest for p in range(0, len(lookup_straight2curved) // 2): if lookup_straight2curved[p] == lookup_straight2curved[p + 1]: lookup_straight2curved[p] = 0 else: break for p in range(len(lookup_straight2curved) - 1, len(lookup_straight2curved) // 2, -1): if lookup_straight2curved[p] == lookup_straight2curved[p - 1]: lookup_straight2curved[p] = 0 else: break lookup_straight2curved = np.array(lookup_straight2curved) # Create volumes containing curved and straight warping fields data_warp_curved2straight = np.zeros((nx_s, ny_s, nz_s, 1, 3)) data_warp_straight2curved = np.zeros((nx, ny, nz, 1, 3)) # 5. compute transformations # Curved and straight images and the same dimensions, so we compute both warping fields at the same time. # b. determine which plane of spinal cord centreline it is included # sct.printv(nx * ny * nz, nx_s * ny_s * nz_s) if self.curved2straight: for u in tqdm(range(nz_s)): x_s, y_s, z_s = np.mgrid[0:nx_s, 0:ny_s, u:u + 1] indexes_straight = np.array(list(zip(x_s.ravel(), y_s.ravel(), z_s.ravel()))) physical_coordinates_straight = image_centerline_straight.transfo_pix2phys(indexes_straight) nearest_indexes_straight = centerline_straight.find_nearest_indexes(physical_coordinates_straight) distances_straight = centerline_straight.get_distances_from_planes(physical_coordinates_straight, nearest_indexes_straight) lookup = lookup_straight2curved[nearest_indexes_straight] indexes_out_distance_straight = np.logical_or( np.logical_or(distances_straight > self.threshold_distance, distances_straight < -self.threshold_distance), lookup == 0) projected_points_straight = centerline_straight.get_projected_coordinates_on_planes( physical_coordinates_straight, nearest_indexes_straight) coord_in_planes_straight = centerline_straight.get_in_plans_coordinates(projected_points_straight, nearest_indexes_straight) coord_straight2curved = centerline.get_inverse_plans_coordinates(coord_in_planes_straight, lookup) displacements_straight = coord_straight2curved - physical_coordinates_straight # Invert Z coordinate as ITK & ANTs physical coordinate system is LPS- (RAI+) # while ours is LPI- # Refs: https://sourceforge.net/p/advants/discussion/840261/thread/2a1e9307/#fb5a # https://www.slicer.org/wiki/Coordinate_systems displacements_straight[:, 2] = -displacements_straight[:, 2] displacements_straight[indexes_out_distance_straight] = [100000.0, 100000.0, 100000.0] data_warp_curved2straight[indexes_straight[:, 0], indexes_straight[:, 1], indexes_straight[:, 2], 0, :]\ = -displacements_straight if self.straight2curved: for u in tqdm(range(nz)): x, y, z = np.mgrid[0:nx, 0:ny, u:u + 1] indexes = np.array(list(zip(x.ravel(), y.ravel(), z.ravel()))) physical_coordinates = image_centerline_pad.transfo_pix2phys(indexes) nearest_indexes_curved = centerline.find_nearest_indexes(physical_coordinates) distances_curved = centerline.get_distances_from_planes(physical_coordinates, nearest_indexes_curved) lookup = lookup_curved2straight[nearest_indexes_curved] indexes_out_distance_curved = np.logical_or( np.logical_or(distances_curved > self.threshold_distance, distances_curved < -self.threshold_distance), lookup == 0) projected_points_curved = centerline.get_projected_coordinates_on_planes(physical_coordinates, nearest_indexes_curved) coord_in_planes_curved = centerline.get_in_plans_coordinates(projected_points_curved, nearest_indexes_curved) coord_curved2straight = centerline_straight.points[lookup] coord_curved2straight[:, 0:2] += coord_in_planes_curved[:, 0:2] coord_curved2straight[:, 2] += distances_curved displacements_curved = coord_curved2straight - physical_coordinates displacements_curved[:, 2] = -displacements_curved[:, 2] displacements_curved[indexes_out_distance_curved] = [100000.0, 100000.0, 100000.0] data_warp_straight2curved[indexes[:, 0], indexes[:, 1], indexes[:, 2], 0, :] = -displacements_curved # Creation of the safe zone based on pre-calculated safe boundaries coord_bound_curved_inf, coord_bound_curved_sup = image_centerline_pad.transfo_phys2pix( [[0, 0, bound_curved[0]]]), image_centerline_pad.transfo_phys2pix([[0, 0, bound_curved[1]]]) coord_bound_straight_inf, coord_bound_straight_sup = image_centerline_straight.transfo_phys2pix( [[0, 0, bound_straight[0]]]), image_centerline_straight.transfo_phys2pix([[0, 0, bound_straight[1]]]) if radius_safe > 0: data_warp_curved2straight[:, :, 0:coord_bound_straight_inf[0][2], 0, :] = 100000.0 data_warp_curved2straight[:, :, coord_bound_straight_sup[0][2]:, 0, :] = 100000.0 data_warp_straight2curved[:, :, 0:coord_bound_curved_inf[0][2], 0, :] = 100000.0 data_warp_straight2curved[:, :, coord_bound_curved_sup[0][2]:, 0, :] = 100000.0 # Generate warp files as a warping fields hdr_warp_s.set_intent('vector', (), '') hdr_warp_s.set_data_dtype('float32') hdr_warp.set_intent('vector', (), '') hdr_warp.set_data_dtype('float32') if self.curved2straight: img = Nifti1Image(data_warp_curved2straight, None, hdr_warp_s) save(img, 'tmp.curve2straight.nii.gz') logger.info('Warping field generated: tmp.curve2straight.nii.gz') if self.straight2curved: img = Nifti1Image(data_warp_straight2curved, None, hdr_warp) save(img, 'tmp.straight2curve.nii.gz') logger.info('Warping field generated: tmp.straight2curve.nii.gz') image_centerline_straight.save(fname_ref) if self.curved2straight: logger.info('Apply transformation to input image...') sct.run(['isct_antsApplyTransforms', '-d', '3', '-r', fname_ref, '-i', 'data.nii', '-o', 'tmp.anat_rigid_warp.nii.gz', '-t', 'tmp.curve2straight.nii.gz', '-n', 'BSpline[3]'], is_sct_binary=True, verbose=verbose) if self.accuracy_results: time_accuracy_results = time.time() # compute the error between the straightened centerline/segmentation and the central vertical line. # Ideally, the error should be zero. # Apply deformation to input image logger.info('Apply transformation to centerline image...') sct.run(['isct_antsApplyTransforms', '-d', '3', '-r', fname_ref, '-i', 'centerline.nii.gz', '-o', 'tmp.centerline_straight.nii.gz', '-t', 'tmp.curve2straight.nii.gz', '-n', 'NearestNeighbor'], is_sct_binary=True, verbose=verbose) file_centerline_straight = Image('tmp.centerline_straight.nii.gz', verbose=verbose) nx, ny, nz, nt, px, py, pz, pt = file_centerline_straight.dim coordinates_centerline = file_centerline_straight.getNonZeroCoordinates(sorting='z') mean_coord = [] for z in range(coordinates_centerline[0].z, coordinates_centerline[-1].z): temp_mean = [coord.value for coord in coordinates_centerline if coord.z == z] if temp_mean: mean_value = np.mean(temp_mean) mean_coord.append( np.mean([[coord.x * coord.value / mean_value, coord.y * coord.value / mean_value] for coord in coordinates_centerline if coord.z == z], axis=0)) # compute error between the straightened centerline and the straight line. x0 = file_centerline_straight.data.shape[0] / 2.0 y0 = file_centerline_straight.data.shape[1] / 2.0 count_mean = 0 if number_of_points >= 10: mean_c = mean_coord[2:-2] # we don't include the four extrema because there are usually messy. else: mean_c = mean_coord for coord_z in mean_c: if not np.isnan(np.sum(coord_z)): dist = ((x0 - coord_z[0]) * px) ** 2 + ((y0 - coord_z[1]) * py) ** 2 self.mse_straightening += dist dist = np.sqrt(dist) if dist > self.max_distance_straightening: self.max_distance_straightening = dist count_mean += 1 self.mse_straightening = np.sqrt(self.mse_straightening / float(count_mean)) self.elapsed_time_accuracy = time.time() - time_accuracy_results os.chdir(curdir) # Generate output file (in current folder) # TODO: do not uncompress the warping field, it is too time consuming! logger.info('Generate output files...') if self.curved2straight: sct.generate_output_file(os.path.join(path_tmp, "tmp.curve2straight.nii.gz"), os.path.join(self.path_output, "warp_curve2straight.nii.gz"), verbose) if self.straight2curved: sct.generate_output_file(os.path.join(path_tmp, "tmp.straight2curve.nii.gz"), os.path.join(self.path_output, "warp_straight2curve.nii.gz"), verbose) # create ref_straight.nii.gz file that can be used by other SCT functions that need a straight reference space if self.curved2straight: sct.copy(os.path.join(path_tmp, "tmp.anat_rigid_warp.nii.gz"), os.path.join(self.path_output, "straight_ref.nii.gz")) # move straightened input file if fname_output == '': fname_straight = sct.generate_output_file(os.path.join(path_tmp, "tmp.anat_rigid_warp.nii.gz"), os.path.join(self.path_output, file_anat + "_straight" + ext_anat), verbose) else: fname_straight = sct.generate_output_file(os.path.join(path_tmp, "tmp.anat_rigid_warp.nii.gz"), os.path.join(self.path_output, fname_output), verbose) # straightened anatomic # Remove temporary files if remove_temp_files: logger.info('Remove temporary files...') sct.rmtree(path_tmp) if self.accuracy_results: logger.info('Maximum x-y error: {} mm'.format(self.max_distance_straightening)) logger.info('Accuracy of straightening (MSE): {} mm'.format(self.mse_straightening)) # display elapsed time self.elapsed_time = int(np.round(time.time() - start_time)) return fname_straight
def propseg(img_input, options_dict): """ :param img_input: source image, to be segmented :param options_dict: arguments as dictionary :return: segmented Image """ arguments = options_dict fname_input_data = img_input.absolutepath fname_data = os.path.abspath(fname_input_data) contrast_type = arguments["-c"] contrast_type_conversion = {'t1': 't1', 't2': 't2', 't2s': 't2', 'dwi': 't1'} contrast_type_propseg = contrast_type_conversion[contrast_type] # Starting building the command cmd = ['isct_propseg', '-t', contrast_type_propseg] if "-ofolder" in arguments: folder_output = arguments["-ofolder"] else: folder_output = './' cmd += ['-o', folder_output] if not os.path.isdir(folder_output) and os.path.exists(folder_output): logger.error("output directory %s is not a valid directory" % folder_output) if not os.path.exists(folder_output): os.makedirs(folder_output) if "-down" in arguments: cmd += ["-down", str(arguments["-down"])] if "-up" in arguments: cmd += ["-up", str(arguments["-up"])] remove_temp_files = 1 if "-r" in arguments: remove_temp_files = int(arguments["-r"]) verbose = int(arguments.get('-v')) sct.init_sct(log_level=verbose, update=True) # Update log level # Update for propseg binary if verbose > 0: cmd += ["-verbose"] # Output options if "-mesh" in arguments: cmd += ["-mesh"] if "-centerline-binary" in arguments: cmd += ["-centerline-binary"] if "-CSF" in arguments: cmd += ["-CSF"] if "-centerline-coord" in arguments: cmd += ["-centerline-coord"] if "-cross" in arguments: cmd += ["-cross"] if "-init-tube" in arguments: cmd += ["-init-tube"] if "-low-resolution-mesh" in arguments: cmd += ["-low-resolution-mesh"] if "-detect-nii" in arguments: cmd += ["-detect-nii"] if "-detect-png" in arguments: cmd += ["-detect-png"] # Helping options use_viewer = None use_optic = True # enabled by default init_option = None rescale_header = arguments["-rescale"] if "-init" in arguments: init_option = float(arguments["-init"]) if init_option < 0: sct.printv('Command-line usage error: ' + str(init_option) + " is not a valid value for '-init'", 1, 'error') sys.exit(1) if "-init-centerline" in arguments: if str(arguments["-init-centerline"]) == "viewer": use_viewer = "centerline" elif str(arguments["-init-centerline"]) == "hough": use_optic = False else: if rescale_header is not 1: fname_labels_viewer = func_rescale_header(str(arguments["-init-centerline"]), rescale_header, verbose=verbose) else: fname_labels_viewer = str(arguments["-init-centerline"]) cmd += ["-init-centerline", fname_labels_viewer] use_optic = False if "-init-mask" in arguments: if str(arguments["-init-mask"]) == "viewer": use_viewer = "mask" else: if rescale_header is not 1: fname_labels_viewer = func_rescale_header(str(arguments["-init-mask"]), rescale_header) else: fname_labels_viewer = str(arguments["-init-mask"]) cmd += ["-init-mask", fname_labels_viewer] use_optic = False if "-mask-correction" in arguments: cmd += ["-mask-correction", str(arguments["-mask-correction"])] if "-radius" in arguments: cmd += ["-radius", str(arguments["-radius"])] if "-detect-n" in arguments: cmd += ["-detect-n", str(arguments["-detect-n"])] if "-detect-gap" in arguments: cmd += ["-detect-gap", str(arguments["-detect-gap"])] if "-init-validation" in arguments: cmd += ["-init-validation"] if "-nbiter" in arguments: cmd += ["-nbiter", str(arguments["-nbiter"])] if "-max-area" in arguments: cmd += ["-max-area", str(arguments["-max-area"])] if "-max-deformation" in arguments: cmd += ["-max-deformation", str(arguments["-max-deformation"])] if "-min-contrast" in arguments: cmd += ["-min-contrast", str(arguments["-min-contrast"])] if "-d" in arguments: cmd += ["-d", str(arguments["-d"])] if "-distance-search" in arguments: cmd += ["-dsearch", str(arguments["-distance-search"])] if "-alpha" in arguments: cmd += ["-alpha", str(arguments["-alpha"])] # check if input image is in 3D. Otherwise itk image reader will cut the 4D image in 3D volumes and only take the first one. image_input = Image(fname_data) image_input_rpi = image_input.copy().change_orientation('RPI') nx, ny, nz, nt, px, py, pz, pt = image_input_rpi.dim if nt > 1: sct.printv('ERROR: your input image needs to be 3D in order to be segmented.', 1, 'error') path_data, file_data, ext_data = sct.extract_fname(fname_data) path_tmp = sct.tmp_create(basename="label_vertebrae", verbose=verbose) # rescale header (see issue #1406) if rescale_header is not 1: fname_data_propseg = func_rescale_header(fname_data, rescale_header) else: fname_data_propseg = fname_data # add to command cmd += ['-i', fname_data_propseg] # if centerline or mask is asked using viewer if use_viewer: from spinalcordtoolbox.gui.base import AnatomicalParams from spinalcordtoolbox.gui.centerline import launch_centerline_dialog params = AnatomicalParams() if use_viewer == 'mask': params.num_points = 3 params.interval_in_mm = 15 # superior-inferior interval between two consecutive labels params.starting_slice = 'midfovminusinterval' if use_viewer == 'centerline': # setting maximum number of points to a reasonable value params.num_points = 20 params.interval_in_mm = 30 params.starting_slice = 'top' im_data = Image(fname_data_propseg) im_mask_viewer = msct_image.zeros_like(im_data) # im_mask_viewer.absolutepath = sct.add_suffix(fname_data_propseg, '_labels_viewer') controller = launch_centerline_dialog(im_data, im_mask_viewer, params) fname_labels_viewer = sct.add_suffix(fname_data_propseg, '_labels_viewer') if not controller.saved: sct.printv('The viewer has been closed before entering all manual points. Please try again.', 1, 'error') sys.exit(1) # save labels controller.as_niftii(fname_labels_viewer) # add mask filename to parameters string if use_viewer == "centerline": cmd += ["-init-centerline", fname_labels_viewer] elif use_viewer == "mask": cmd += ["-init-mask", fname_labels_viewer] # If using OptiC elif use_optic: image_centerline = optic.detect_centerline(image_input, contrast_type, verbose) fname_centerline_optic = os.path.join(path_tmp, 'centerline_optic.nii.gz') image_centerline.save(fname_centerline_optic) cmd += ["-init-centerline", fname_centerline_optic] if init_option is not None: if init_option > 1: init_option /= (nz - 1) cmd += ['-init', str(init_option)] # enabling centerline extraction by default (needed by check_and_correct_segmentation() ) cmd += ['-centerline-binary'] # run propseg status, output = sct.run(cmd, verbose, raise_exception=False, is_sct_binary=True) # check status is not 0 if not status == 0: sct.printv('Automatic cord detection failed. Please initialize using -init-centerline or -init-mask (see help)', 1, 'error') sys.exit(1) # build output filename fname_seg = os.path.join(folder_output, os.path.basename(sct.add_suffix(fname_data, "_seg"))) fname_centerline = os.path.join(folder_output, os.path.basename(sct.add_suffix(fname_data, "_centerline"))) # in case header was rescaled, we need to update the output file names by removing the "_rescaled" if rescale_header is not 1: sct.mv(os.path.join(folder_output, sct.add_suffix(os.path.basename(fname_data_propseg), "_seg")), fname_seg) sct.mv(os.path.join(folder_output, sct.add_suffix(os.path.basename(fname_data_propseg), "_centerline")), fname_centerline) # if user was used, copy the labelled points to the output folder (they will then be scaled back) if use_viewer: fname_labels_viewer_new = os.path.join(folder_output, os.path.basename(sct.add_suffix(fname_data, "_labels_viewer"))) sct.copy(fname_labels_viewer, fname_labels_viewer_new) # update variable (used later) fname_labels_viewer = fname_labels_viewer_new # check consistency of segmentation if arguments["-correct-seg"] == "1": check_and_correct_segmentation(fname_seg, fname_centerline, folder_output=folder_output, threshold_distance=3.0, remove_temp_files=remove_temp_files, verbose=verbose) # copy header from input to segmentation to make sure qform is the same sct.printv("Copy header input --> output(s) to make sure qform is the same.", verbose) list_fname = [fname_seg, fname_centerline] if use_viewer: list_fname.append(fname_labels_viewer) for fname in list_fname: im = Image(fname) im.header = image_input.header im.save(dtype='int8') # they are all binary masks hence fine to save as int8 return Image(fname_seg)
def main(args=None): # initialization start_time = time.time() param = Param() # check user arguments if not args: args = sys.argv[1:] # Get parser info parser = get_parser() arguments = parser.parse(sys.argv[1:]) param.fname_data = arguments['-i'] if '-g' in arguments: param.group_size = arguments['-g'] if '-m' in arguments: param.fname_mask = arguments['-m'] if '-param' in arguments: param.update(arguments['-param']) if '-x' in arguments: param.interp = arguments['-x'] if '-ofolder' in arguments: path_out = arguments['-ofolder'] if '-r' in arguments: param.remove_temp_files = int(arguments['-r']) param.verbose = int(arguments.get('-v')) sct.init_sct(log_level=param.verbose, update=True) # Update log level sct.printv('\nInput parameters:', param.verbose) sct.printv(' input file ............' + param.fname_data, param.verbose) # Get full path param.fname_data = os.path.abspath(param.fname_data) if param.fname_mask != '': param.fname_mask = os.path.abspath(param.fname_mask) # Extract path, file and extension path_data, file_data, ext_data = sct.extract_fname(param.fname_data) path_tmp = sct.tmp_create(basename="fmri_moco", verbose=param.verbose) # Copying input data to tmp folder and convert to nii # TODO: no need to do that (takes time for nothing) sct.printv('\nCopying input data to tmp folder and convert to nii...', param.verbose) convert(param.fname_data, os.path.join(path_tmp, "fmri.nii"), squeeze_data=False) # go to tmp folder curdir = os.getcwd() os.chdir(path_tmp) # run moco fmri_moco(param) # come back os.chdir(curdir) # Generate output files fname_fmri_moco = os.path.join(path_out, file_data + param.suffix + ext_data) sct.create_folder(path_out) sct.printv('\nGenerate output files...', param.verbose) sct.generate_output_file( os.path.join(path_tmp, "fmri" + param.suffix + '.nii'), fname_fmri_moco, param.verbose) sct.generate_output_file( os.path.join(path_tmp, "fmri" + param.suffix + '_mean.nii'), os.path.join(path_out, file_data + param.suffix + '_mean' + ext_data), param.verbose) sct.generate_output_file( os.path.join(path_tmp, "fmri" + param.suffix + '_params_X.nii'), os.path.join(path_out, file_data + param.suffix + '_params_X' + ext_data), squeeze_data=False, verbose=param.verbose) sct.generate_output_file( os.path.join(path_tmp, "fmri" + param.suffix + '_params_Y.nii'), os.path.join(path_out, file_data + param.suffix + '_params_Y' + ext_data), squeeze_data=False, verbose=param.verbose) sct.mv(os.path.join(path_tmp, 'fmri_moco_params.tsv'), os.path.join(path_out + file_data + param.suffix + '_params.tsv')) # Delete temporary files if param.remove_temp_files == 1: sct.printv('\nDelete temporary files...', param.verbose) sct.rmtree(path_tmp, verbose=param.verbose) # display elapsed time elapsed_time = time.time() - start_time sct.printv( '\nFinished! Elapsed time: ' + str(int(np.round(elapsed_time))) + 's', param.verbose) sct.display_viewer_syntax([fname_fmri_moco, file_data], mode='ortho,ortho')