def register(param, file_src, file_dest, file_mat, file_out, im_mask=None): """ Register two images by estimating slice-wise Tx and Ty transformations, which are regularized along Z. This function uses ANTs' isct_antsSliceRegularizedRegistration. :param param: :param file_src: :param file_dest: :param file_mat: :param file_out: :param im_mask: Image of mask, could be 2D or 3D :return: """ # TODO: deal with mask # initialization failed_transfo = 0 # by default, failed matrix is 0 (i.e., no failure) do_registration = True # get metric radius (if MeanSquares, CC) or nb bins (if MI) if param.metric == 'MI': metric_radius = '16' else: metric_radius = '4' file_out_concat = file_out kw = dict() im_data = Image( file_src ) # TODO: pass argument to use antsReg instead of opening Image each time # register file_src to file_dest if param.todo == 'estimate' or param.todo == 'estimate_and_apply': # If orientation is sagittal, use antsRegistration in 2D mode # Note: the parameter --restrict-deformation is irrelevant with affine transfo if param.sampling == 'None': # 'None' sampling means 'fully dense' sampling # see https://github.com/ANTsX/ANTs/wiki/antsRegistration-reproducibility-issues sampling = param.sampling else: # param.sampling should be a float in [0,1], and means the # samplingPercentage that chooses a subset of points to # estimate from. We always use 'Regular' (evenly-spaced) # mode, though antsRegistration offers 'Random' as well. # Be aware: even 'Regular' is not fully deterministic: # > Regular includes a random perturbation on the grid sampling # - https://github.com/ANTsX/ANTs/issues/976#issuecomment-602313884 sampling = 'Regular,' + param.sampling if im_data.orientation[2] in 'LR': cmd = [ 'isct_antsRegistration', '-d', '2', '--transform', 'Affine[%s]' % param.gradStep, '--metric', param.metric + '[' + file_dest + ',' + file_src + ',1,' + metric_radius + ',' + sampling + ']', '--convergence', param.iter, '--shrink-factors', '1', '--smoothing-sigmas', param.smooth, '--verbose', '1', '--output', '[' + file_mat + ',' + file_out_concat + ']' ] cmd += get_interpolation('isct_antsRegistration', param.interp) if im_mask is not None: # if user specified a mask, make sure there are non-null voxels in the image before running the registration if np.count_nonzero(im_mask.data): cmd += ['--masks', im_mask.absolutepath] else: # Mask only contains zeros. Copying the image instead of estimating registration. copy(file_src, file_out_concat, verbose=0) do_registration = False # TODO: create affine mat file with identity, in case used by -g 2 # 3D mode else: cmd = [ 'isct_antsSliceRegularizedRegistration', '--polydegree', param.poly, '--transform', 'Translation[%s]' % param.gradStep, '--metric', param.metric + '[' + file_dest + ',' + file_src + ',1,' + metric_radius + ',' + sampling + ']', '--iterations', param.iter, '--shrinkFactors', '1', '--smoothingSigmas', param.smooth, '--verbose', '1', '--output', '[' + file_mat + ',' + file_out_concat + ']' ] cmd += get_interpolation('isct_antsSliceRegularizedRegistration', param.interp) if im_mask is not None: cmd += ['--mask', im_mask.absolutepath] # run command if do_registration: kw.update(dict(is_sct_binary=True)) # reducing the number of CPU used for moco (see issue #201 and #2642) env = { **os.environ, **{ "ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS": "1" } } status, output = run_proc(cmd, verbose=1 if param.verbose == 2 else 0, env=env, **kw) elif param.todo == 'apply': sct_apply_transfo.main(argv=[ '-i', file_src, '-d', file_dest, '-w', file_mat + param.suffix_mat, '-o', file_out_concat, '-x', param.interp, '-v', '0' ]) # check if output file exists # Note (from JCA): In the past, i've tried to catch non-zero output from ANTs function (via the 'status' variable), # but in some OSs, the function can fail while outputing zero. So as a pragmatic approach, I decided to go with # the "output file checking" approach, which is 100% sensitive. if not os.path.isfile(file_out_concat): # printv(output, verbose, 'error') printv( 'WARNING in ' + os.path.basename(__file__) + ': No output. Maybe related to improper calculation of ' 'mutual information. Either the mask you provided is ' 'too small, or the subject moved a lot. If you see too ' 'many messages like this try with a bigger mask. ' 'Using previous transformation for this volume (if it' 'exists).', param.verbose, 'warning') failed_transfo = 1 # If sagittal, copy header (because ANTs screws it) and add singleton in 3rd dimension (for z-concatenation) if im_data.orientation[2] in 'LR' and do_registration: im_out = Image(file_out_concat) im_out.header = im_data.header im_out.data = np.expand_dims(im_out.data, 2) im_out.save(file_out, verbose=0) # return status of failure return failed_transfo
def apply(self): # Initialization fname_src = self.input_filename # source image (moving) list_warp = self.list_warp # list of warping fields fname_out = self.output_filename # output fname_dest = self.fname_dest # destination image (fix) verbose = self.verbose remove_temp_files = self.remove_temp_files crop_reference = self.crop # if = 1, put 0 everywhere around warping field, if = 2, real crop islabel = False if self.interp == 'label': islabel = True self.interp = 'nn' interp = get_interpolation('isct_antsApplyTransforms', self.interp) # Parse list of warping fields printv('\nParse list of warping fields...', verbose) use_inverse = [] fname_warp_list_invert = [] # list_warp = list_warp.replace(' ', '') # remove spaces # list_warp = list_warp.split(",") # parse with comma for idx_warp, path_warp in enumerate(self.list_warp): # Check if this transformation should be inverted if path_warp in self.list_warpinv: use_inverse.append('-i') # list_warp[idx_warp] = path_warp[1:] # remove '-' fname_warp_list_invert += [[ use_inverse[idx_warp], list_warp[idx_warp] ]] else: use_inverse.append('') fname_warp_list_invert += [[path_warp]] path_warp = list_warp[idx_warp] if path_warp.endswith((".nii", ".nii.gz")) \ and Image(list_warp[idx_warp]).header.get_intent()[0] != 'vector': raise ValueError( "Displacement field in {} is invalid: should be encoded" " in a 5D file with vector intent code" " (see https://nifti.nimh.nih.gov/pub/dist/src/niftilib/nifti1.h" .format(path_warp)) # need to check if last warping field is an affine transfo isLastAffine = False path_fname, file_fname, ext_fname = extract_fname( fname_warp_list_invert[-1][-1]) if ext_fname in ['.txt', '.mat']: isLastAffine = True # check if destination file is 3d # check_dim(fname_dest, dim_lst=[3]) # PR 2598: we decided to skip this line. # N.B. Here we take the inverse of the warp list, because sct_WarpImageMultiTransform concatenates in the reverse order fname_warp_list_invert.reverse() fname_warp_list_invert = functools.reduce(lambda x, y: x + y, fname_warp_list_invert) # Extract path, file and extension path_src, file_src, ext_src = extract_fname(fname_src) path_dest, file_dest, ext_dest = extract_fname(fname_dest) # Get output folder and file name if fname_out == '': path_out = '' # output in user's current directory file_out = file_src + '_reg' ext_out = ext_src fname_out = os.path.join(path_out, file_out + ext_out) # Get dimensions of data printv('\nGet dimensions of data...', verbose) img_src = Image(fname_src) nx, ny, nz, nt, px, py, pz, pt = img_src.dim # nx, ny, nz, nt, px, py, pz, pt = get_dimension(fname_src) printv( ' ' + str(nx) + ' x ' + str(ny) + ' x ' + str(nz) + ' x ' + str(nt), verbose) # if 3d if nt == 1: # Apply transformation printv('\nApply transformation...', verbose) if nz in [0, 1]: dim = '2' else: dim = '3' # if labels, dilate before resampling if islabel: printv("\nDilate labels before warping...") path_tmp = tmp_create(basename="apply_transfo") fname_dilated_labels = os.path.join(path_tmp, "dilated_data.nii") # dilate points dilate(Image(fname_src), 4, 'ball').save(fname_dilated_labels) fname_src = fname_dilated_labels printv( "\nApply transformation and resample to destination space...", verbose) run_proc([ 'isct_antsApplyTransforms', '-d', dim, '-i', fname_src, '-o', fname_out, '-t' ] + fname_warp_list_invert + ['-r', fname_dest] + interp, is_sct_binary=True) # if 4d, loop across the T dimension else: if islabel: raise NotImplementedError dim = '4' path_tmp = tmp_create(basename="apply_transfo") # convert to nifti into temp folder printv('\nCopying input data to tmp folder and convert to nii...', verbose) img_src.save(os.path.join(path_tmp, "data.nii")) copy(fname_dest, os.path.join(path_tmp, file_dest + ext_dest)) fname_warp_list_tmp = [] for fname_warp in list_warp: path_warp, file_warp, ext_warp = extract_fname(fname_warp) copy(fname_warp, os.path.join(path_tmp, file_warp + ext_warp)) fname_warp_list_tmp.append(file_warp + ext_warp) fname_warp_list_invert_tmp = fname_warp_list_tmp[::-1] curdir = os.getcwd() os.chdir(path_tmp) # split along T dimension printv('\nSplit along T dimension...', verbose) im_dat = Image('data.nii') im_header = im_dat.hdr data_split_list = sct_image.split_data(im_dat, 3) for im in data_split_list: im.save() # apply transfo printv('\nApply transformation to each 3D volume...', verbose) for it in range(nt): file_data_split = 'data_T' + str(it).zfill(4) + '.nii' file_data_split_reg = 'data_reg_T' + str(it).zfill(4) + '.nii' status, output = run_proc([ 'isct_antsApplyTransforms', '-d', '3', '-i', file_data_split, '-o', file_data_split_reg, '-t', ] + fname_warp_list_invert_tmp + [ '-r', file_dest + ext_dest, ] + interp, verbose, is_sct_binary=True) # Merge files back printv('\nMerge file back...', verbose) import glob path_out, name_out, ext_out = extract_fname(fname_out) # im_list = [Image(file_name) for file_name in glob.glob('data_reg_T*.nii')] # concat_data use to take a list of image in input, now takes a list of file names to open the files one by one (see issue #715) fname_list = glob.glob('data_reg_T*.nii') fname_list.sort() im_list = [Image(fname) for fname in fname_list] im_out = sct_image.concat_data(im_list, 3, im_header['pixdim']) im_out.save(name_out + ext_out) os.chdir(curdir) generate_output_file(os.path.join(path_tmp, name_out + ext_out), fname_out) # Delete temporary folder if specified if remove_temp_files: printv('\nRemove temporary files...', verbose) rmtree(path_tmp, verbose=verbose) # Copy affine matrix from destination space to make sure qform/sform are the same printv( "Copy affine matrix from destination space to make sure qform/sform are the same.", verbose) im_src_reg = Image(fname_out) im_src_reg.copy_qform_from_ref(Image(fname_dest)) im_src_reg.save( verbose=0 ) # set verbose=0 to avoid warning message about rewriting file if islabel: printv( "\nTake the center of mass of each registered dilated labels..." ) labeled_img = cubic_to_point(im_src_reg) labeled_img.save(path=fname_out) if remove_temp_files: printv('\nRemove temporary files...', verbose) rmtree(path_tmp, verbose=verbose) # Crop the resulting image using dimensions from the warping field warping_field = fname_warp_list_invert[-1] # If the last transformation is not an affine transfo, we need to compute the matrix space of the concatenated # warping field if not isLastAffine and crop_reference in [1, 2]: printv('Last transformation is not affine.') if crop_reference in [1, 2]: # Extract only the first ndim of the warping field img_warp = Image(warping_field) if dim == '2': img_warp_ndim = Image(img_src.data[:, :], hdr=img_warp.hdr) elif dim in ['3', '4']: img_warp_ndim = Image(img_src.data[:, :, :], hdr=img_warp.hdr) # Set zero to everything outside the warping field cropper = ImageCropper(Image(fname_out)) cropper.get_bbox_from_ref(img_warp_ndim) if crop_reference == 1: printv( 'Cropping strategy is: keep same matrix size, put 0 everywhere around warping field' ) img_out = cropper.crop(background=0) elif crop_reference == 2: printv( 'Cropping strategy is: crop around warping field (the size of warping field will ' 'change)') img_out = cropper.crop() img_out.save(fname_out) display_viewer_syntax([fname_dest, fname_out], verbose=verbose)