def spherically_project_surface(insurf, outsurf): """ (string) -> None takes path to insurf, spherically projects it, outputs it to outsurf """ surf = fs.read_geometry(insurf, read_metadata=True) projected = sphericalProject(surf[0], surf[1]) fs.write_geometry(outsurf, projected[0], projected[1], volume_info=surf[2])
def save_surface_files(note, error, registrations, subject, no_surf_export, no_reg_export, surface_format, surface_path, angle_tag, eccen_tag, label_tag, radius_tag, registration_name): ''' save_surface_files is the calculator that saves the registration data out as surface files, which are put back in the registration as the value 'surface_files'. ''' if no_surf_export: return {'surface_files': ()} surface_format = surface_format.lower() # make an exporter for properties: if surface_format in ['curv', 'morph', 'auto', 'automatic']: def export(flnm, p): fsio.write_morph_data(flnm, p) return flnm elif surface_format in ['mgh', 'mgz']: def export(flnm, p): flnm = flnm + '.' + surface_format dt = np.int32 if np.issubdtype(p.dtype, np.dtype(int).type) else np.float32 img = fsmgh.MGHImage(np.asarray([[p]], dtype=dt), np.eye(4)) img.to_filename(flnm) return flnm elif surface_format in ['nifti', 'nii', 'niigz', 'nii.gz']: surface_format = 'nii' if surface_format == 'nii' else 'nii.gz' def export(flnm, p): flnm = flnm + '.' + surface_format dt = np.int32 if np.issubdtype(p.dtype, np.dtype(int).type) else np.float32 img = nib.Nifti1Image(np.asarray([[p]], dtype=dt), np.eye(4)) img.to_filename(flnm) return flnm else: error('Could not understand surface file-format %s' % surface_format) path = surface_path if surface_path else os.path.join(subject.path, 'surf') files = [] note('Exporting files...') for h in six.iterkeys(registrations): hemi = subject.hemis[h] reg = registrations[h] note('Extracting %s predicted mesh...' % h.upper()) pmesh = reg['predicted_mesh'] for (pname, tag) in zip( ['polar_angle', 'eccentricity', 'visual_area', 'radius'], [angle_tag, eccen_tag, label_tag, radius_tag]): flnm = export(os.path.join(path, h + '.' + tag), pmesh.prop(pname)) files.append(flnm) # last do the registration itself if registration_name and not no_reg_export: flnm = os.path.join(path, h + '.' + registration_name + '.sphere.reg') fsio.write_geometry(flnm, pmesh.coordinates.T, pmesh.tess.faces.T) return {'surface_files': tuple(files)}
def export_fssurf(tria, outfile): """ Save Freesurfer Surface Geometry file (wrap Nibabel) """ # open file try: from nibabel.freesurfer.io import write_geometry write_geometry(outfile, tria.v, tria.t, volume_info=tria.fsinfo) except IOError: print("[File " + outfile + " not writable]") return
def save_freesurfer_geometry(filename, obj, volume_info=None, create_stamp=None): ''' save_mgh(filename, obj) saves the given object to the given filename in the mgh format and returns the filename. All options that can be given to the to_mgh function can also be passed to this function; they are used to modify the object prior to exporting it. ''' obj = geo.to_mesh(obj) fsio.write_geometry(filename, obj.coordinates.T, obj.tess.faces.T, volume_info=volume_info, create_stamp=create_stamp) return filename
def resafe_surface(insurf, outsurf, pretess): """ takes path to insurf and rewrites it to outsurf thereby fixing vertex locs flag error (scannerRAS instead of surfaceRAS after marching cube) :param str insurf: path and name of input surface :param str outsurf: path and name of output surface :param str pretess: path and name of file the input surface was created on (e.g. filled-pretess127.mgz) """ surf = read_geometry(insurf, read_metadata=True) if not surf[2]['filename']: # Set information with file used for surface construction (volume info and name) surf[2]['filename'] = pretess surf[2]['volume'] = nibload(pretess).header.get_data_shape() fs.write_geometry(outsurf, surf[0], surf[1], volume_info=surf[2])
def makelayers(subjectid,layerdepths,layerprefix,fstruncate='pt'): ''' def makelayers(subjectid,layerdepths,layerprefix,fstruncate) <subjectid> is like 'C0041' <layerdepths> is a vector of fractional distances (each having at most two decimal places). e.g. linspace(.1,.9,6). NOTE: layerdepth=0 is pial and layerdepth=1 is white! <layerprefix> is a string that will be added to filenames (e.g. 'A'). this causes files like 'lh.layerA1' to be made. <fstruncate> is the name of the truncation surface in fsaverage (default. 'pt', which refers to 'lh.pt' and 'rh.pt') Create layer surfaces. Subdivide layer and other surfaces to form dense surfaces. Calculate transfer functions to go from/to fsaverage standard surfaces and the single-subject dense surfaces. Truncate the dense surfaces based on the fsaverage <fstruncate> surface, and write out new surfaces. Calculate thickness, curvature, and sulc values for the single-subject dense surfaces. Calculate SAPV and AEL for each surface (dense trunc only). Turn on matlabpool before calling for a big speed-up. Example files that are created: lh.layerA1 lh.layerA1DENSE lh.layerA1DENSETRUNCpt tfunDENSE.pkl lh.DENSETRUNCpt.pkl lh.curvatureDENSE.mgz lh.curvatureDENSETRUNCpt.mgz lh.sapv_sphere_DENSETRUNCpt.mgz history: - 2017/08/02 - added support for smoothwm - 2016/11/04 - added transfer functions for the fsaverage dense surfaces - 2016/04/29 - add saving of sulc, sapv, and ael start using unix_wrapper #######======== by RZ =============================================== history: 20180717 RZ started to use pathos.multiprocessing to perform parallel computing 20180714 RZ edited based on cvnmakelayers.m ''' from RZutilpy.cvnpy import cvnpath,transfertodense from RZutilpy.math import mod2 from RZutilpy.rzio import savepkl,loadpkl from math import floor import nibabel.freesurfer.io as fsio import numpy as np from pathos.multiprocessing import Pool # calc dir0 = (Path(cvnpath('anatomicals')) / subjectid).str fsdir = (Path(cvnpath('freesurfer')) / subjectid).str fsdirAVG = (Path(cvnpath('freesurfer')) / 'fsaverage').str # define hemis = ['lh', 'rh'] ########## create layer surfaces def expand(ii): p = mod2(ii,len(layerdepths)) # depth indx q = floor(ii / len(layerdepths)) # hemi index #use 1-depth so that depth 0 = pial and depth 1 = white # This way if you use linspace(.1,.9,6), A1 will be equivalent to canonical # layer I (molecular layer) and layer A6 will be equivalent to canonical # layer VI (innermost...) d = 1 - layerdepths[p - 1] unix_wrapper('mris_expand -thickness {wm} {depth:.2f} {outputsurf}'.format(\ wm=(Path(fsdir)/'surf'/f'{hemis[q]}.white').str, \ depth=d,\ outputsurf=(Path(fsdir)/'surf'/f'{hemis[q]}.layer{layerprefix}{p}').str)) if __name__ == '__main__': with Pool() as p: p.imap_unordered(expand, range(1, 2*len(layerdepths) + 1)) ######## subdivide layer and other surfaces (creating dense surfaces) # calc a list of surfaces surfs = ['inflated', 'sphere', 'sphere.reg', 'white', 'pial', 'smoothwm'] for p in range(1, len(layerdepths)+1): surfs.append(f'layer{layerprefix}{p}') # e.g. 'layerA1' # subdivide the surfaces def subdivide(ii): p = mod2(ii,len(surfs)) # surf index q = floor(ii/len(surfs)) # hemi index unix_wrapper('mris_mesh_subdivide --surf {inputsurf} --out {outputsurf} --method linear --iter 1'.format(\ inputsurf=(Path(fsdir)/hemis[q]/surfs{p}).str,\ outputsurf=(Path(fsdir)/hemis[q]/(surfs{p}+'DENSE')).str)) if __name__ == '__main__': with Pool() as p: p.imap_unordered(subdivide, range(1, 2*len(surfs) + 1)) ########## calculate some transfer functions for the dense surfaces [VERSION 1 (to standard fsaverage)] # calculate transfer functions [tfunFSSSlh,tfunFSSSrh,tfunSSFSlh,tfunSSFSrh] = \ calctransferfunctions((Path(fsdirAVG)/'surf'/'lh.sphere.reg').str, \ (Path(fsdirAVG)/'surf'/'rh.sphere.reg').str, \ (Path(fsdir)/'surf'/'lh.sphere.regDENSE').str, \ (Path(fsdir)/'surf'/'rh.sphere.regDENSE').str) # save savepkl((Path(dir0) / 'tfunDENSE.pkl').str,\ {'tfunFSSSlh':tfunFSSSlh,'tfunFSSSrh':tfunFSSSrh,\ 'tfunSSFSlh':tfunSSFSlh,'tfunSSFSrh':tfunSSFSrh}) ######## calculate some transfer functions for the dense surfaces [VERSION 2 (to dense fsaverage)] # calc [tfunFSSSlh,tfunFSSSrh,tfunSSFSlh,tfunSSFSrh] = \ calctransferfunctions((Path(fsdirAVG)/'surf'/'lh.sphere.regDENSE').str, \ (Path(fsdirAVG)/'surf'/'rh.sphere.regDENSE').str, \ (Path(fsdir)/'surf'/'lh.sphere.regDENSE').str, \ (Path(fsdir)/'surf'/'rh.sphere.regDENSE').str) # save savepkl((Path(dir0)/'tfunDENSEDENSE.pkl').str,\ {'tfunFSSSlh':tfunFSSSlh,'tfunFSSSrh':tfunFSSSrh,\ 'tfunSSFSlh':tfunSSFSlh,'tfunSSFSrh':tfunSSFSrh}) ########## revive the first version!! transfunc = loadpkl((Path(dir0)/'tfunDENSE.pkl').str) ########## truncate the dense surfaces based on the lh.<fstruncate> and rh.<fstruncate> fsaverage surfaces # calc number of vertices fsnumlh = fsio.read_geometry((Path(fsdirAVG)/'surf/'/'lh.white').str)[0].shape[0] fsnumrh = fsio.read_geometry((Path(fsdirAVG)/'surf/'/'rh.white').str)[0].shape[0] # do it for hemi in hemis: # calculate a vector of vertex values indicating which is included [fsaverage] ##!!! check here, should figure out read_patch_asc## surf = read_patch_asc((Path(fsdirAVG) / 'surf'/ f'{hemi}.{fstruncate}.patch.3d.asc').str) ##!!! ## vals = np.zeros(fsnumlh + fsnumrh) ## !!! ## if hemi == 'lh': vals[surf.vertices+1] = 1 elif hemi == 'rh': vals[fsnumlh+(surf.vertices+1)] = 1 ## !!! ## # transfer these values to the dense individual-subject surface and do a find. # this tells us indices of vertices in the dense surface that are valid validix = np.where(transfunc['tfunFSSSlh'][vals]) if hemi == 'lh' \ else np.where(transfunc['tfunFSSSrh'][vals]) # write out reduced surfaces for sf in surfs: # read in the original dense surface [verticesA,facesA,volinfo] = fsio.read_geometry((Path(fsdir)/'surf'/f'{hemi}.{sf}DENSE').str, read_metadata=True) # logical indicating which faces survive okfaces = np.all(np.isin(facesA,validix), axis=1) # FACES x 1 # calculate the new faces temp = facesA[okfaces,:] ##!!!## facesA = calcposition(validix, temp.flatten()).reshape(*temp.shape).copy() ##!!!## # calculate the new vertices verticesA = verticesA[validix,:] # write out the truncated surface fsio.write_geometry((Path(fsdir) / 'surf'/ f'{hemi}.{sf}DENSETRUNC{fstruncate}').str, verticesA, facesA, volinfo) # save the DENSE->DENSETRUNC indices (truncsize x 1) savepkl((Path(fsdir)/'surf'/f'{hemi}.DENSETRUNC{fstruncate}.pkl').str, {'validix':validix}) # save the orig->DENSE indices (densesize x 1) numverts_orig = fsio.read_geometry((Path(fsdir)/'surf'/f'{hemi}.inflated').str)[0].shape[0] # note the 0 inde problem here, here might not be correct validix = transfertodense(subjectid, np.arange(numverts_orig), hemi,'nearest','inflated') savepkl((fsdi/'surf'/f'{hemi}.DENSE.pkl').str,{'validix':validix})
def deform_surface(input_surf, input_orig, input_deform, input_target, hemi, path_output, input_mask=None, interp_method="nearest", smooth_iter=0, flip_faces=False, cleanup=True): """ This function deforms a surface mesh in freesurfer convention using a coordinate map containing voxel coordinates. The computation takes quite a while because in the case of removed vertices, i.e. if a mask is given as input, the remaining faces are reindexed. Inputs: *input_surf: surface mesh to be transformed. *input_orig: freesurfer orig.mgz. *input_deform: deformation (coordinate mapping). *input_target: target volume. *hemi: hemisphere. *path_output: path where to save output. *input_mask: mask volume. *interp_method: interpolation method (nearest or trilinear). *smooth_iter: number of smoothing iterations applied to final image (if set > 0). *flip_faces: reverse normal direction of mesh. *cleanup: remove intermediate files. created by Daniel Haenelt Date created: 06-02-2019 Last modified: 20-06-2020 """ import os import numpy as np import nibabel as nb import shutil as sh from nibabel.freesurfer.io import write_geometry, read_geometry from nibabel.affines import apply_affine from nipype.interfaces.freesurfer import SampleToSurface from nipype.interfaces.freesurfer import SmoothTessellation from lib.io.get_filename import get_filename from lib.io.mgh2nii import mgh2nii from lib.surface.vox2ras import vox2ras # set freesurfer path environment os.environ["SUBJECTS_DIR"] = path_output # freesurfer subject tmp = np.random.randint(0, 10, 5) tmp_string = ''.join(str(i) for i in tmp) sub = "tmp_" + tmp_string # make output folder if not os.path.exists(path_output): os.makedirs(path_output) # mimic freesurfer folder structure (with some additional folder for intermediate files) path_sub = os.path.join(path_output, sub) path_mri = os.path.join(path_sub, "mri") path_surf = os.path.join(path_sub, "surf") os.makedirs(path_sub) os.makedirs(path_mri) os.makedirs(path_surf) # get file extension of orig _, name_orig, ext_orig = get_filename(input_orig) # name of surface file name_surf = os.path.basename(input_surf) # copy orig, cmap and input surface to mimicked freesurfer folders sh.copyfile(input_surf, os.path.join(path_surf, hemi + ".source")) if ext_orig != ".mgz": mgh2nii(input_orig, path_mri, "mgz") os.rename(os.path.join(path_mri, name_orig + ".mgz"), os.path.join(path_mri, "orig.mgz")) else: sh.copyfile(input_orig, os.path.join(path_mri, "orig.mgz")) # read surface geometry vtx, fac = read_geometry(input_surf) # get affine vox2ras-tkr transformation to target volume vox2ras_tkr, _ = vox2ras(input_target) # divide coordinate mapping into its x, y and z components cmap_img = nb.load(input_deform) cmap_img.header["dim"][0] = 3 cmap_img.header["dim"][4] = 1 # apply vox2ras transformation to coordinate mappings cmap_array = cmap_img.get_fdata() cmap_array = apply_affine(vox2ras_tkr, cmap_array) components = ["x", "y", "z"] vtx_new = np.zeros([len(vtx), 3]) for i in range(len(components)): temp_array = cmap_array[:, :, :, i] temp_img = nb.Nifti1Image(temp_array, cmap_img.affine, cmap_img.header) nb.save(temp_img, os.path.join(path_mri, components[i] + "_deform.nii")) # mri_vol2surf sampler = SampleToSurface() sampler.inputs.subject_id = sub sampler.inputs.reg_header = True sampler.inputs.hemi = hemi sampler.inputs.source_file = os.path.join( path_mri, components[i] + "_deform.nii") sampler.inputs.surface = "source" sampler.inputs.sampling_method = "point" sampler.inputs.sampling_range = 0 sampler.inputs.sampling_units = "mm" sampler.inputs.interp_method = interp_method sampler.inputs.out_type = "mgh" sampler.inputs.out_file = os.path.join( path_surf, hemi + "." + components[i] + "_sampled.mgh") sampler.run() data_img = nb.load( os.path.join(path_surf, hemi + "." + components[i] + "_sampled.mgh")) vtx_new[:, i] = np.squeeze(data_img.get_fdata()) if input_mask: # mri_vol2surf (background) sampler = SampleToSurface() sampler.inputs.subject_id = sub sampler.inputs.reg_header = True sampler.inputs.hemi = hemi sampler.inputs.source_file = input_mask sampler.inputs.surface = "source" sampler.inputs.sampling_method = "point" sampler.inputs.sampling_range = 0 sampler.inputs.sampling_units = "mm" sampler.inputs.interp_method = "nearest" sampler.inputs.out_type = "mgh" sampler.inputs.out_file = os.path.join(path_surf, hemi + ".background.mgh") sampler.run() # get new indices background_list = nb.load( os.path.join(path_surf, hemi + ".background.mgh")).get_fdata() background_list = np.squeeze(background_list).astype(int) # only keep vertex indices within the slab ind_keep = np.arange(0, len(vtx[:, 0])) ind_keep[background_list == 0] = -1 ind_keep = ind_keep[ind_keep != -1] # get new vertices vtx_new = vtx_new[ind_keep, :] # get new faces fac_keep = np.zeros(len(fac[:, 0])) fac_keep += np.in1d(fac[:, 0], ind_keep) fac_keep += np.in1d(fac[:, 1], ind_keep) fac_keep += np.in1d(fac[:, 2], ind_keep) fac_temp = fac[fac_keep == 3, :] fac_new = fac[fac_keep == 3, :] # sort new faces c_step = 0 n_step = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100] for i in range(len(ind_keep)): temp = np.where(ind_keep[i] == fac_temp) fac_new[temp] = i # print status counter = np.floor(i / len(ind_keep) * 100).astype(int) if counter == n_step[c_step]: print("sort faces: " + str(counter) + " %") c_step += 1 # remove singularities (vertices without faces) fac_counter = 0 fac_old = fac_new.copy() n_singularity = np.zeros(len(vtx_new)) c_step = 0 for i in range(len(vtx_new)): row, col = np.where(fac_old == i) n_singularity[i] = len(row) if not n_singularity[i]: fac_temp = fac_new.copy() fac_temp[fac_temp >= fac_counter] = -1 fac_temp[fac_temp != -1] = 0 fac_new += fac_temp fac_counter -= 1 # update face counter fac_counter += 1 # print status counter = np.floor(i / len(vtx_new) * 100).astype(int) if counter == n_step[c_step]: print("clean vertices: " + str(counter) + " %") c_step += 1 # vertices and indices without singularities vtx_new = vtx_new[n_singularity != 0] ind_keep = ind_keep[n_singularity != 0] # save index mapping between original and transformed surface np.savetxt(os.path.join(path_output, name_surf + "_ind.txt"), ind_keep, fmt='%d') else: fac_new = fac # flip faces if flip_faces: fac_new = np.flip(fac_new, axis=1) # write new surface write_geometry(os.path.join(path_output, name_surf + "_def"), vtx_new, fac_new) # smooth surface if smooth_iter: smooth = SmoothTessellation() smooth.inputs.in_file = os.path.join(path_output, name_surf + "_def") smooth.inputs.out_file = os.path.join(path_output, name_surf + "_def_smooth") smooth.inputs.smoothing_iterations = smooth_iter smooth.inputs.disable_estimates = True smooth.run() # delete intermediate files if cleanup: sh.rmtree(path_sub, ignore_errors=True)
def run_anchor(vtx, fac, adjm, anchor, n_neighbor=20, smooth_iter=0): """ Run anchor This function loads a list of control points and shifts a mesh with its local neighborhood to match these control points. Control points are assumed to be in ras coordinates. Optionally, the output mesh can be smoothed. Parameters ---------- vtx : ndarray Array of vertex points. fac : ndarray Array of corresponding faces. adjm : obj Adjacency matrix. anchor : list List of control points. n_neighbor : int, optional Neighborhood size. The default is 20. smooth_iter : int, optional Number of smoothing iterations of final mesh. The default is 0. Returns ------- vtx : ndarray Shifted array of vertex points. ind_control : ndarray Indices of closest vertices to control points. Notes ------- created by Daniel Haenelt Date created: 12-05-2020 Last modified: 05-10-2020 """ print("start mesh initialization (anchoring)") # check vein array if anchor is None: sys.exit("error: no control points found for anchoring!") # number of anchor points n_anchor = len(anchor) # loop through control points ind_control = np.zeros(n_anchor).astype(int) for i in range(n_anchor): # print current status print("apply control point: " + str(i + 1) + "/" + str(n_anchor)) # get nearest vertex vtx_dist = norm(vtx - anchor[i, :], axis=1) ind_min = np.where(vtx_dist == np.min(vtx_dist))[0][0] ind_control[i] = ind_min # get neighborhood nn_ind = nn_2d(ind_min, adjm, n_neighbor) # get shift vtx_shift = vtx[ind_min, :] - anchor[i, :] # update mesh vtx = update_mesh(vtx, vtx_shift, ind_min, nn_ind, 1) # smooth output if smooth_iter: tmp = np.random.randint(0, 10, 5) tmp_string = ''.join(str(i) for i in tmp) surf_temp = os.path.join(os.getcwd(), "surf_" + tmp_string) write_geometry(surf_temp, vtx, fac) smooth_surface(surf_temp, surf_temp, smooth_iter) vtx, _ = read_geometry(surf_temp) os.remove(surf_temp) return vtx, ind_control
os.chdir(join(pathLIB, "registration")) os.system("matlab" + \ " -nodisplay -nodesktop -r " + \ "\"run_rBBR(\'{0}\', \'{1}\', \'{2}\'); exit;\"". \ format(input_cfg, pathSPM, pathMOURIK)) os.chdir(cwd) # vox2ras data = loadmat(out_surf_mat_vox) # apply ras2vox transformation to output for i in range(2): data["wSurface"][0][i] = data["wSurface"][0][i][:, 0:3] data["pSurface"][0][i] = data["pSurface"][0][i][:, 0:3] data["wSurface"][0][i] = apply_affine(vox2ras_tkr, data["wSurface"][0][i]) data["pSurface"][0][i] = apply_affine(vox2ras_tkr, data["pSurface"][0][i]) # save matfile savemat(out_surf_mat_ras, data) # save surfaces in freesurfer format write_geometry(join(subjects_dir, "lh.white"), data["wSurface"][0][0], data["faceData"][0][0]) write_geometry(join(subjects_dir, "rh.white"), data["wSurface"][0][1], data["faceData"][0][1]) write_geometry(join(subjects_dir, "lh.pial"), data["pSurface"][0][0], data["faceData"][0][0]) write_geometry(join(subjects_dir, "rh.pial"), data["pSurface"][0][1], data["faceData"][0][1])
def calculate_equivolumetric_surfaces(file_white, file_pial, n_surfs, factor, niter, hemi, path_output): """ The script calculates intracortical surfaces based on equi-volumetric layering. It is an adaption of Konrad Wagstyl's function in surface_tools. Here, the io_mesh is not used anymore and the call to a freesurfer function is omitted. Instead, vertex-wise area is calculated in a separate function and we use the nibabel to read the surface geometry. First, vertex-wise area is calculated from both input geometries. Smoothing to the areas is optional and done if factor is set to a non-zero value. Then, based on vertex-wise area, equi-volumetric surfaces are computed. Inputs: *file_white: input of GM/WM surface. *file_pial: input of GM/CSF surface. *n_surfs: number of output surfaces (returns input surfaces as 0 and 1). *factor: amount of smoothing. *niter: number of smoothing iterations. *hemi: declare hemisphere for output file. *path_output: path where output is saved. created by Daniel Haenelt Date created: 01-11-2018 Last modified: 10-06-2020 """ import os import numpy as np from nibabel.freesurfer.io import read_geometry, write_geometry from cortex.polyutils import Surface from lib.segmentation.calculate_area import calculate_area def beta(alpha, aw, ap): """Compute euclidean distance fraction, beta, that will yield the desired volume fraction, alpha, given vertex areas in the white matter surface, aw, and on the pial surface, ap. A surface with `alpha` fraction of the cortical volume below it and `1 - alpha` fraction above it can then be constructed from pial, px, and white matter, pw, surface coordinates as `beta * px + (1 - beta) * pw`. """ if alpha == 0: return np.zeros_like(aw) elif alpha == 1: return np.ones_like(aw) else: return 1 - (1 / (ap - aw) * (-aw + np.sqrt((1 - alpha) * ap**2 + alpha * aw**2))) # make output folder if not os.path.exists(path_output): os.makedirs(path_output) # load geometry and area data wm_vtx, wm_fac = read_geometry(file_white) pial_vtx, pial_fac = read_geometry(file_pial) wm_vertexareas = calculate_area(file_white) pial_vertexareas = calculate_area(file_pial) # smoothing area files (optional) if factor != 0: wm_vertexareas = Surface(wm_vtx, wm_fac).smooth(wm_vertexareas, factor=factor, iterations=niter) pial_vertexareas = Surface(pial_vtx, pial_fac).smooth(pial_vertexareas, factor=factor, iterations=niter) # number of equally space intracortical surfaces vectors = wm_vtx - pial_vtx tmp_vtx = pial_vtx.copy() tmp_fac = pial_fac.copy() mask = vectors.sum( axis=1) != 0 # create mask where vertex coordinates match for depth in range(n_surfs): print("creating surface " + str(depth + 1)) betas = beta( float(depth) / (n_surfs - 1), wm_vertexareas[mask], pial_vertexareas[mask]) betas = np.nan_to_num(betas) tmp_vtx[mask] = pial_vtx[mask] + vectors[mask] * np.array([betas]).T write_geometry( os.path.join(path_output, hemi + "." + "layer" + str(depth)), tmp_vtx, tmp_fac)
def plot_normal(vtx, fac, adjm, file_out, step_size=100, shape="line"): """ Plot normal This function generates lines to visualize outward directed surface normals of an input surface mesh. Parameters ---------- vtx : ndarray Array of vertex points. fac : ndarray Corresponding face array. adjm : obj Adjacency matrix. file_out : str Filename of output surface mesh. step_size : int, optional Subset of vertices. The default is 100. shape : str, optional line, triangle, prism. The default is "line". Returns ------- None. Notes ------- created by Daniel Haenelt Date created: 13-12-2019 Last modified: 05-10-2020 """ # get surface normals normal = get_normal(vtx, fac) # array containing a list of considered vertices t = np.arange(0, len(vtx), step_size) # initialise faces for specific shape if shape == "prism": fac_new = [[0, 1, 2], [3, 4, 5], [0, 1, 4], [0, 3, 4], [1, 2, 5], [1, 4, 5], [0, 2, 5], [0, 3, 5]] fac_iter = 6 elif shape == "triangle": fac_new = [[0, 1, 2]] fac_iter = 3 elif shape == "line": fac_new = [[0, 1, 0]] fac_iter = 2 vtx_res = [] fac_res = [] for i in range(len(t)): # get index from nearest neighbour of a given vertex nn = nn_2d(t[i], adjm, 0) nn = nn[:2] # get all vertex points for specific shape if shape == "prism": A = list(vtx[t[i]]) B = list(vtx[nn[0]]) C = list(vtx[nn[1]]) D = list(vtx[t[i]] - normal[t[i]]) E = list(vtx[nn[0]] - normal[nn[0]]) F = list(vtx[nn[1]] - normal[nn[1]]) vtx_new = [A, B, C, D, E, F] elif shape == "triangle": A = list(vtx[t[i]]) B = list(vtx[nn[0]]) C = list(vtx[t[i]] - normal[t[i]]) vtx_new = [A, B, C] elif shape == "line": A = list(vtx[t[i]]) B = list(vtx[t[i]] - normal[t[i]]) vtx_new = [A, B] # update faces if i > 0: for j in range(len(fac_new)): fac_new[j] = [x + fac_iter for x in fac_new[j]] # update resulting vertex and face list vtx_res.extend(vtx_new) fac_res.extend(fac_new) # vertices and faces as array vtx_res = np.array(vtx_res) fac_res = np.array(fac_res) # write output geometry write_geometry(file_out, vtx_res, fac_res)
def register_retinotopy_command(args): ''' register_retinotopy_command(args) can be given a list of arguments, such as sys.argv[1:]; these arguments may include any options and must include at least one subject id. All subjects whose ids are given are registered to a retinotopy model, and the resulting registration, as well as the predictions made by the model in the registration, are exported. ''' # Parse the arguments (args, opts) = _retinotopy_parser(args) # First, help? if opts['help']: print register_retinotopy_help return 1 # and if we are verbose, lets setup a note function verbose = opts['verbose'] def note(s): if verbose: print s return verbose # Add the subjects directory, if there is one if 'subjects_dir' in opts and opts['subjects_dir'] is not None: add_subject_path(opts['subjects_dir']) # Parse the simple numbers for o in ['weight_cutoff', 'edge_strength', 'angle_strength', 'func_strength', 'max_step_size', 'max_out_eccen']: opts[o] = float(opts[o]) opts['max_steps'] = int(opts['max_steps']) # These are for now not supported: #TODO if opts['angle_math'] or opts['angle_radians'] or opts['eccen_radians']: print 'Mathematical angles and angles not in degrees are not yet supported.' return 1 # The remainder of the args can wait for now; walk through the subjects: tag_key = {'eccen': 'eccentricity', 'angle': 'polar_angle', 'label': 'V123_label'} for subnm in args: sub = freesurfer_subject(subnm) note('Processing subject: %s' % sub.id) # we need to register this subject... res = {} ow = not opts['no_overwrite'] for h in ['LH','RH']: note(' Processing hemisphere: %s' % h) hemi = sub.__getattr__(h) # See if we are loading custom values... (ang,ecc,wgt) = (None,None,None) suffix = '_' + h.lower() + '_file' if opts['angle' + suffix] is not None: ang = _guess_surf_file(opts['angle' + suffix]) if opts['eccen' + suffix] is not None: ecc = _guess_surf_file(opts['eccen' + suffix]) if opts['weight' + suffix] is not None: wgt = _guess_surf_file(opts['weight' + suffix]) # Do the registration note(' - Running Registration...') res[h] = register_retinotopy(hemi, V123_model(), polar_angle=ang, eccentricity=ecc, weight=wgt, weight_cutoff=opts['weight_cutoff'], partial_voluming_correction=opts['part_vol_correct'], edge_scale=opts['edge_strength'], angle_scale=opts['angle_strength'], functional_scale=opts['func_strength'], prior=opts['prior'], max_predicted_eccen=opts['max_out_eccen'], max_steps=opts['max_steps'], max_step_size=opts['max_step_size']) # Perform the hemi-specific outputs now: if not opts['no_reg_export']: regnm = '.'.join([h.lower(), opts['registration_name'], 'sphere', 'reg']) flnm = (os.path.join(sub.directory, 'surf', regnm) if h == 'LH' else os.path.join(sub.directory, 'xhemi', 'surf', regnm)) if ow or not os.path.exist(flnm): note(' - Exporting registration file: %s' % flnm) fsio.write_geometry(flnm, res[h].coordinates.T, res[h].faces.T, 'Created by neuropythy (github.com/noahbenson/neuropythy)') else: note(' - Skipping registration file: %s (file exists)' % flnm) if not opts['no_surf_export']: for dim in ['angle', 'eccen', 'label']: flnm = os.path.join(sub.directory, 'surf', '.'.join([h.lower(), opts[dim + '_tag'], 'mgz'])) if ow or not os.path.exist(flnm): note(' - Exporting prediction file: %s' % flnm) img = fsmgh.MGHImage( np.asarray([[res[h].prop(tag_key[dim])]], dtype=(np.int32 if dim == 'label' else np.float32)), np.eye(4)) img.to_filename(flnm) else: note(' - Skipping prediction file: %s (file exists)' % flnm) # Do the volume exports here if not opts['no_vol_export']: note(' Processing volume data...') note(' - Calculating cortex-to-ribbon mapping...') surf2rib = cortex_to_ribbon_map(sub, hemi=None) for dim in ['angle', 'eccen', 'label']: flnm = os.path.join(sub.directory, 'mri', opts[dim + '_tag'] + '.mgz') if ow or not os.path.exist(flnm): note(' - Generating volume file: %s' % flnm) vol = cortex_to_ribbon(sub, (res['LH'].prop(tag_key[dim]), res['RH'].prop(tag_key[dim])), map=surf2rib, dtype=(np.int32 if dim == 'label' else np.float32)) note(' - Exporting volume file: %s' % flnm) vol.to_filename(flnm) else: note(' - Skipping volume file: %s (file exists)' % flnm) # That is it for this subject! note(' Subject %s finished!' % sub.id) # And if we made it here, all was successful. return 0
def write(self, surface, surface_path): write_geometry(filepath=surface_path, coords=surface.vertices, faces=surface.triangles, volume_info=surface.get_main_metadata())
def remove_vertex_outliers(input_surf, input_ind, n=5, overwrite=True): """ This function removes outlier vertices from a deformed surface mesh. Due to interpolation of the deformation fields, edge effects can move some vertices to the edge of the image volume. These are removed by comparing each vertex to the geometric center of the whole mesh and setting a global threshold. The threshold is defined as multiple (n) of the standard deviation of the distance distribution. This is a very crude method to delete outlier vertices. Inputs: *input_surf: path to the input surface mesh. *input_ind: path to the corresponding index list (with .txt extension) *n: distance threshold parameter. *overwrite: overwrite input surface. created by Daniel Haenelt Date created: 08-12-2019 Last modified: 09-12-2019 """ import os import numpy as np from nibabel.freesurfer.io import read_geometry, write_geometry # load geometry vtx, fac = read_geometry(input_surf) # load index file ind = np.loadtxt(input_ind) # get geometry center x_mean = np.sum(vtx[:,0]) / len(vtx[:,0]) y_mean = np.sum(vtx[:,1]) / len(vtx[:,1]) z_mean = np.sum(vtx[:,2]) / len(vtx[:,2]) # euclidean distance to geometric center vtx_dist = np.zeros_like(vtx) vtx_dist[:,0] = ( vtx[:,0] - x_mean ) ** 2 vtx_dist[:,1] = ( vtx[:,1] - y_mean ) ** 2 vtx_dist[:,2] = ( vtx[:,2] - z_mean ) ** 2 vtx_dist = np.sqrt( np.sum(vtx_dist, 1) ) # mean and std distance vtx_dist_mean = np.mean(vtx_dist) vtx_dist_std = np.std(vtx_dist) # distance threshold vtx_dist_threshold = vtx_dist_mean + n * vtx_dist_std # sort faces fac_old = fac.copy() fac_outlier = np.zeros_like(fac) n_outlier = np.zeros(len(vtx)) c_step = 0 n_step = [10,20,30,40,50,60,70,80,90,100] for i in range(len(vtx)): if vtx_dist[i] > vtx_dist_threshold: row, col = np.where(fac_old == i) fac_outlier[row, col] = 1 # remember which faces to remove n_outlier[i] = 1 # remember which vertices to remove fac_temp = fac.copy() # update face numbering fac_temp[fac_old >= i] = -1 fac_temp[fac_temp != -1] = 0 fac += fac_temp # print status counter = np.floor(i / len(vtx) * 100).astype(int) if counter == n_step[c_step]: print("remove outliers: "+str(counter)+" %") c_step += 1 # remove outlier faces fac_outlier = np.sum(fac_outlier,1) fac = fac[fac_outlier == 0] # remove outliers in vertex and ind vtx = vtx[n_outlier == 0] ind = ind[n_outlier == 0] # write output if overwrite: write_geometry(input_surf,vtx,fac) np.savetxt(input_ind, ind, fmt='%d') else: path_output = os.path.dirname(input_surf) name_output = os.path.basename(input_surf) write_geometry(os.path.join(path_output,name_output+"_out"),vtx,fac) path_output = os.path.dirname(input_ind) name_output = os.path.splitext(os.path.basename(input_ind))[0] np.savetxt(os.path.join(path_output,name_output+"_out.txt"), ind, fmt='%d')
def write_fs(self, surface, surface_path): write_geometry(filepath=surface_path, coords=surface.vertices, faces=surface.triangles, volume_info=surface.image_metadata)
def match_vertex_number(input_white_surf, input_pial_surf, input_white_ind, input_pial_ind, path_output): """ This function takes a white and a pial surface as input and matches the vertex numbers of both surfaces. Removed vertices (not found in the corresponding other surface) are removed and faces are updated. The index files are updated as well. Inputs: *input_white_surf: filename of white surface. *input_pial_surf: filename of pial surface. *input_white_ind: corresponding index file of white surface. *input_pial_ind: corresponding index file of pial surface. *path_output: path where output is written. created by Daniel Haenelt Date created: 10-12-2019 Last modified: 09-01-2020 """ import os import numpy as np from nibabel.freesurfer.io import read_geometry, write_geometry # make output folder if not os.path.exists(path_output): os.makedirs(path_output) # load geometry vtx_white, fac_white = read_geometry(input_white_surf) vtx_pial, _ = read_geometry(input_pial_surf) # load index file white_ind = np.loadtxt(input_white_ind).astype(int) pial_ind = np.loadtxt(input_pial_ind).astype(int) # vertex indices in orig space which are in neither of both deformed surfaces ind_remove = list(set(pial_ind) - set(white_ind)) ind_remove.extend(set(white_ind) - set(pial_ind)) ind_remove = np.sort(ind_remove) # sort vertices of white surface i = 0 c_white = np.zeros_like(white_ind) while i < np.size(white_ind): if np.any(ind_remove == white_ind[i]): c_white[i] = 1 i += 1 # sort vertices of pial surface i = 0 c_pial = np.zeros_like(pial_ind) while i < np.size(pial_ind): if np.any(ind_remove == pial_ind[i]): c_pial[i] = 1 i += 1 # sort faces fac_old = fac_white.copy() fac_new = fac_white.copy() fac_outlier = np.zeros_like(fac_white) c_step = 0 n_step = [10,20,30,40,50,60,70,80,90,100] for i in range(len(ind_remove)): check_remove = np.where(ind_remove[i] == white_ind)[0] if len(check_remove): row, col = np.where(fac_old == check_remove) fac_outlier[row,col] = 1 # remember which faces to remove fac_temp = fac_new.copy() # update face numbering fac_temp[fac_old > check_remove] = -1 fac_temp[fac_temp != -1] = 0 fac_new += fac_temp i += 1 # print status counter = np.floor(i / len(ind_remove) * 100).astype(int) if counter == n_step[c_step]: print("sort faces: "+str(counter)+" %") c_step += 1 # remove outlier faces fac_outlier = np.sum(fac_outlier,1) fac_new = fac_new[fac_outlier == 0] # remove outliers in vertices vtx_white = vtx_white[c_white == 0] vtx_pial = vtx_pial[c_pial == 0] # remove outliers in ind white_ind = white_ind[c_white == 0] pial_ind = pial_ind[c_pial == 0] # write output name_output = os.path.basename(input_white_surf) write_geometry(os.path.join(path_output, name_output+"_match"),vtx_white,fac_new) name_output = os.path.basename(input_pial_surf) write_geometry(os.path.join(path_output, name_output+"_match"),vtx_pial,fac_new) name_output = os.path.basename(input_white_ind) np.savetxt(os.path.join(path_output, name_output+"_match.txt"), white_ind, fmt='%d') name_output = os.path.basename(input_pial_ind) np.savetxt(os.path.join(path_output, name_output+"_match.txt"), pial_ind, fmt='%d')
def create_subcorts(self): # Important filepaths fsDir = self.subdir dataDir = self.scDir + os.sep + 'data' tmpDir = os.path.join(dataDir, 'tmp') os.makedirs(tmpDir) # Dictionary of labels and corresponding values subcort_dict = { 'L_Thalamus': { 'id': '10', 'color': '#b5ffbe' }, 'L_Caudate': { 'id': '11', 'color': '#d7cfff' }, 'L_Putamen': { 'id': '12', 'color': '#fa05b9' }, 'L_Pallidum': { 'id': '13', 'color': '#a2a3a2' }, 'L_Hippocampus': { 'id': '17', 'color': '#f7ffcc' }, 'L_Amygdala': { 'id': '18', 'color': '#ffbfc1' }, 'R_Thalamus': { 'id': '49', 'color': '#b5ffbe' }, 'R_Caudate': { 'id': '50', 'color': '#d7cfff' }, 'R_Putamen': { 'id': '51', 'color': '#fa05b9' }, 'R_Pallidum': { 'id': '52', 'color': '#a2a3a2' }, 'R_Hippocampus': { 'id': '53', 'color': '#f7ffcc' }, 'R_Amygdala': { 'id': '54', 'color': '#ffbfc1' } } # Create the meshes using freesurfer commands for subc in subcort_dict.keys(): # Set names of files that will be used in this loop pretessF = tmpDir + os.sep + subc + '_pretess.mgz' tessF = tmpDir + os.sep + subc + '_tess.lab' smoothF = tmpDir + os.sep + subc + '_tess.smooth' subcortF = dataDir + os.sep + subc.lower().replace('_', 'h.') # Pretess pretess_cmd_tmp = [ 'mri_pretess', os.path.join(fsDir, 'mri', 'aseg.mgz'), subcort_dict[subc]['id'], os.path.join(fsDir, 'mri', 'norm.mgz'), pretessF ] pretess_cmd = ' '.join(pretess_cmd_tmp) os.system(pretess_cmd) # Tessellate tess_cmd_tmp = [ 'mri_tessellate', pretessF, subcort_dict[subc]['id'], tessF ] tess_cmd = ' '.join(tess_cmd_tmp) os.system(tess_cmd) # Smooth smooth_cmd_tmp = ['mris_smooth', '-nw', tessF, smoothF] smooth_cmd = ' '.join(smooth_cmd_tmp) os.system(smooth_cmd) # Now reformat it so it's not in the weird QUAD format # By default the above freesurfer commands output the mesh in a funky, hard to read file format coords, faces = read_geometry(smoothF) write_geometry(subcortF, coords, faces) shutil.rmtree(tmpDir)
def plot_white2pial(input_white, input_pial, adjm, step_size=100, shape="line"): """ Plot white to pial This function generates lines between corresponding vertices at the white and pial surface to visualize the shift between matched vertices caused by realigning surfaces independently. You can either construct prisms, triangles or lines. Parameters ---------- input_white : str Filename of white surface. input_pial : str Filename of pial surface. adjm : obj Adjacency matrix. step_size : int, optional Subset of vertices. The default is 100. shape : str, optional line, triangle, prism. The default is "line". Returns ------- None. Notes ------- created by Daniel Haenelt Date created: 07-11-2019 Last modified: 05-10-2020 """ # read geometry vtx_white, fac_white, header_white = read_geometry(input_white, read_metadata=True) vtx_pial, fac_pial = read_geometry(input_pial) # array containing a list of considered vertices t = np.arange(0, len(vtx_white), step_size) # initialise faces for specific shape if shape == "prism": fac_new = [[0, 1, 2], [3, 4, 5], [0, 1, 4], [0, 3, 4], [1, 2, 5], [1, 4, 5], [0, 2, 5], [0, 3, 5]] fac_iter = 6 elif shape == "triangle": fac_new = [[0, 1, 2]] fac_iter = 3 elif shape == "line": fac_new = [[0, 1, 0]] fac_iter = 2 vtx_res = [] fac_res = [] for i in range(len(t)): # get index from nearest neighbour of a given vertex nn = nn_2d(t[i], adjm, 0) nn = nn[:2] # get all vertex points for specific shape if shape == "prism": A = list(vtx_white[t[i]]) B = list(vtx_white[nn[0]]) C = list(vtx_white[nn[1]]) D = list(vtx_pial[t[i]]) E = list(vtx_pial[nn[0]]) F = list(vtx_pial[nn[1]]) vtx_new = [A, B, C, D, E, F] elif shape == "triangle": A = list(vtx_white[t[i]]) B = list(vtx_white[nn[0]]) C = list(vtx_pial[t[i]]) vtx_new = [A, B, C] elif shape == "line": A = list(vtx_white[t[i]]) B = list(vtx_pial[t[i]]) vtx_new = [A, B] # update faces if i > 0: for j in range(len(fac_new)): fac_new[j] = [x + fac_iter for x in fac_new[j]] # update resulting vertex and face list vtx_res.extend(vtx_new) fac_res.extend(fac_new) # vertices and faces as array vtx_res = np.array(vtx_res) fac_res = np.array(fac_res) # write output geometry write_geometry(input_white + "_plot_white2pial", vtx_res, fac_res, volume_info=header_white)
from lib_gbb.normal.get_normal import get_normal file_surf = "/home/daniel/source/BlenderCBS-master/Haenelt/data/lh.refined_enhanced_inflated" file_data = "/home/daniel/source/BlenderCBS-master/Haenelt/data/lh.spmT_left_right_GE_EPI2_upsampled_avg_layer4_16.mgh" file_out = "/home/daniel/source/BlenderCBS-master/Haenelt/data/lh.refined_enhanced_inflated_relief" scale_factor = 2 """ do not edit below """ def sigmoid(x): return 1 / (1 + np.exp(-x)) # load geometry vtx, fac = read_geometry(file_surf) # load contrast data = np.zeros_like(vtx) data[:, 0] = nb.load(file_data).get_fdata()[:, 0, 0] data[:, 1] = nb.load(file_data).get_fdata()[:, 0, 0] data[:, 2] = nb.load(file_data).get_fdata()[:, 0, 0] # get normals normal = get_normal(vtx, fac) # make relief vtx += scale_factor * sigmoid(data) * normal # write output write_geometry(file_out, vtx, fac)
def process_bem(fs_subject_path, bem_path_): """ Main function for BEM extraction. """ subject_ = bem_path_.name.split("_")[0].replace("AVG", "ANTS") epi_img_ = nib.load(str(bem_path_)) for file_name_, discard_inds in zip( ["outer_skin.surf", "outer_skull.surf", "inner_skull.surf"], [[1, 2, 3, 4], [1, 2, 3], [1, 2]]): epi_img_data_ = deepcopy(epi_img_.get_fdata()) correct_line_artefact(epi_img_data_) cond = np.stack([(epi_img_data_ == i_) for i_ in discard_inds]).sum(0) epi_img_data_[np.where(cond)] = 1 epi_img_data_[np.where(np.logical_not(cond))] = 0 vertices, simplices = measure.marching_cubes_lewiner( epi_img_data_, spacing=(1, 1, 1), allow_degenerate=False)[:2] path_white = fs_subject_path / subject_ / "surf" / "lh.white" try: volume_info_ = read_geometry(path_white, read_metadata=True)[2] except: print("Skipping subject {}...".format(subject_)) continue vertices = vertices @ epi_img_.affine[:3, : 3] + epi_img_.affine[:3, 3] - volume_info_[ "cras"] mesh_ = trimesh.Trimesh(vertices=vertices, faces=simplices) trimesh.repair.fix_normals(mesh_, multibody=False) smooth_mesh = trimesh.smoothing.filter_laplacian( deepcopy(mesh_), lamb=0.8, iterations=15, volume_constraint=True) bem_output_path_ = fs_subject_path / subject_ / "bem" bem_output_path_.mkdir(parents=True, exist_ok=True) vertices, faces_ = smooth_mesh.vertices, smooth_mesh.faces # Defect corrections for the large meshes vertices, faces_ = fix_all_defects(vertices, faces_) # Writing a freesufer mesh file file_name_large = file_name_.split(".")[0] + "_large.surf" write_geometry(str(bem_output_path_ / file_name_large), vertices, faces_) # Writing an obj mesh file with (bem_output_path_ / file_name_large).with_suffix(".obj").open('w') as file_obj: file_obj.write( trimesh.exchange.obj.export_obj( trimesh.Trimesh(vertices, faces_))) # Decimating BEM surfaces vertices, faces_ = decimate_surface(vertices, faces_, n_triangles=5120, method='sphere') # Defect correction for decimated meshes... vertices, faces_ = fix_all_defects(vertices, faces_) # Writing an obj mesh file with (bem_output_path_ / file_name_).with_suffix(".obj").open('w') as file_obj: file_obj.write( trimesh.exchange.obj.export_obj( trimesh.Trimesh(vertices, faces_))) # Writing a freesufer mesh file print("Writing {}...".format(str(bem_output_path_ / file_name_))) write_geometry(str(bem_output_path_ / file_name_), vertices, faces_, volume_info=volume_info_)
def apply_deformation(vtx, fac, arr_deform, vox2ras_tkr, ras2vox_tkr, path_output="", name_output="", write_output=True): """ Apply deformation This function applies a coordinate shift to an array of vertices. The coordinate shift is taken from a deformation field where each voxel corresponds to a shift along one direction in voxel space. Vertices outside the deformation field are removed from the mesh. Parameters ---------- vtx : ndarray Array of vertices. fac : ndarray Array of faces. arr_deform : ndarray 4D volume array containing shifts in voxel space. vox2ras_tkr : ndarray Transformation from voxel to ras space. ras2vox_tkr : ndarray Transformation from ras to voxel space. path_output : str, optional Path where output is written. The default is "". name_output : str, optional Name of output file. The default is "". write_output : bool, optional Write output file. The default is True. Returns ------- vtx : ndarray Deformed vertices. fac : ndarray Corresponding faces. ind_keep : ndarray Input vertex indices to keep after cleaning. Notes ------- created by Daniel Haenelt Date created: 28-12-2019 Last modified: 23-10-2020 """ # get array dimensions xdim = np.shape(arr_deform)[0] ydim = np.shape(arr_deform)[1] zdim = np.shape(arr_deform)[2] # get vertices to voxel space vtx = apply_affine(ras2vox_tkr, vtx) # only keep vertex indices within the slab vtx[vtx[:, 0] < 0, 0] = np.nan vtx[vtx[:, 1] < 0, 1] = np.nan vtx[vtx[:, 2] < 0, 2] = np.nan vtx[vtx[:, 0] > xdim - 1, 0] = np.nan vtx[vtx[:, 1] > ydim - 1, 1] = np.nan vtx[vtx[:, 2] > zdim - 1, 2] = np.nan # remove vertices outside the slab if np.any(np.isnan(vtx)): ind_keep = np.arange(len(vtx)) ind_keep = ind_keep[~np.isnan(np.sum(vtx, axis=1))] vtx, fac, ind_keep = remove_vertex(vtx, fac, ind_keep) # sample shifts from deformation map vtx_shift = np.zeros((len(vtx), 3)) for i in range(3): vtx_shift[:, i] = linear_interpolation3d(vtx[:, 0], vtx[:, 1], vtx[:, 2], arr_deform[:, :, :, i]) vtx_shift[np.isnan(vtx_shift[:, i]), i] = 0 # shift coordinates to new location vtx += vtx_shift # get new coordinates in ras space vtx = apply_affine(vox2ras_tkr, vtx) if write_output: file_out = os.path.join(path_output, name_output) write_geometry(file_out, vtx, fac) np.savetxt(file_out + "_ind.txt", ind_keep, fmt="%d") return vtx, fac, ind_keep
def fix_intersecting_surfaces(inner_surface_path, outer_surface_path, move_margin=1.5, out_path=None): inner_mesh = trimesh.Trimesh( *read_geometry(inner_surface_path, read_metadata=False)) outer_mesh = trimesh.Trimesh( *read_geometry(outer_surface_path, read_metadata=False)) edges = np.array(trimesh.graph.vertex_adjacency_graph(outer_mesh).edges) ray_interceptor_inner = trimesh.ray.ray_pyembree.RayMeshIntersector( inner_mesh) outer_surface_path = Path(outer_surface_path) stem = outer_surface_path.stem name = outer_surface_path.name if out_path is None: out_path = outer_surface_path.parent / (stem + suffix + name[len(stem):]) out_vertices = deepcopy(outer_mesh.vertices) # Instead of using the vertex normal, we take the median of the # normal of the neighbor vertex in order for this direction # to be more robust to sharp angular changes. x = np.concatenate([edges[:, [1, 0]], edges]) normal = [ np.median(outer_mesh.vertex_normals[x[x[:, 0] == i][:, 1]], 0) for i in np.arange(outer_mesh.vertices.shape[0]) ] # Intersecting surface correction (from outer to inner) intersections, inds = ray_interceptor_inner.intersects_location( ray_origins=outer_mesh.vertices, ray_directions=normal, multiple_hits=True)[:2] delta1 = np.zeros(out_vertices.shape[0]) print("inner: ", inner_surface_path) print("outer: ", outer_surface_path) if len(inds): dist = np.sqrt( ((outer_mesh.vertices[inds, :] - intersections)**2).sum(axis=1)) # <5 to avoid picking very long distance points inds = inds[dist < 5] dist = dist[dist < 5] if len(inds): msg = "The inner surface intersect the outer surface. Pushing back the outer " + \ "surface {} mm out of the inner surface. Saving the outer ".format(move_margin) + \ "surface as {}.".format(out_path) print(msg) # <5 to avoid picking very long distance points delta1[inds] = dist + move_margin # Intersecting surface correction (from inner to outer) delta2 = np.zeros(out_vertices.shape[0]) closest, distance, triangle_id = trimesh.proximity.closest_point( outer_mesh, inner_mesh.vertices) normal = (inner_mesh.vertices - closest) / distance[:, None] face_normals = outer_mesh.face_normals[triangle_id] angles = trimesh.geometry.vector_angle( np.stack([normal, face_normals], axis=1)) new_deltas_df = pd.DataFrame( dict(vertex_id=outer_mesh.faces[triangle_id[angles < 1.0]].ravel(), delta=np.tile( distance[angles < 1.0] + move_margin, [3, 1]).T.ravel())).groupby("vertex_id").max().reset_index() delta2[new_deltas_df.vertex_id] = new_deltas_df.delta deltas = np.stack([delta1, delta2]).max(axis=0) if np.all(deltas == 0): return out_vertices += deltas[:, None] * outer_mesh.vertex_normals volume_info = read_geometry(outer_surface_path, read_metadata=True)[2] write_geometry(out_path, out_vertices, outer_mesh.faces, volume_info=volume_info) return deltas
def run_gbb(vtx, fac, vtx_n, ind_control, arr_ref, arr_gradient, arr_vein, arr_ignore, t2s, vox2ras_tkr, ras2vox_tkr, line_dir, line_length, r_size, l_rate, max_iterations, cost_threshold, cost_step=1000, cost_sample=10, path_output = "", show_cost=True, show_slope=False, write_temp=10000, Q0=0, M=0.5, h=1, s=1): """ Run GBB This function applies the core function of the gbb method. Parameters ---------- vtx : ndarray Array of vertex points. fac : ndarray Array of corresponding faces. vtx_n : ndarray Array of vertex normals. ind_control : ndarray Array of vertex indices matching control points. arr_ref : ndarray Array of reference volume. arr_gradient : ndarray Array of gradient image. arr_vein : ndarray Array of vein mask. arr_ignore : ndarray Array of ignore mask. t2s : bool Image contrast. vox2ras_tkr : ndarray Transformation matrix from voxel space to ras. ras2vox_tkr : ndarray Transformation matrix from ras to voxel space. line_dir : int Line axis in ras convention (0,1,2,3). line_length : float Line length in one direction in mm. r_size : list Neighborhood radius in mm. l_rate : list Learning rate. max_iterations : list Maximum iterations. cost_threshold : list Cost function threshold. cost_step : int, optional Step size between cost array points. The default is 1000. cost_sample : int, optional Sample size for linear fit. The default is 10. path_output : str, optional Path where intermediate folder is created. The default is "". show_cost : bool, optional Show temporary cost function. The default is True. show_slope : bool, optional Show temporary slope function. The default is False. write_temp : int, optional Step size to write intermediate surfaces (if set > 0). The default is 10000. Q0 : float, optional Const function offset parameter. The default is 0. M : float, optional Cost function slope parameter. The default is 0.5. h : float, optional Cost function weight parameter. The default is 1. s : float, optional Cost function distance parameter. The default is 1. Returns ------- vtx : ndarray Shifted array of vertex points. gbb : dict Collection of performance parameters. Notes ------- created by Daniel Haenelt Date created: 06-02-2020 Last modified: 21-10-2020 """ # make intermediate folder if write_temp > 0 and path_output: tmp = np.random.randint(0, 10, 5) tmp_string = ''.join(str(i) for i in tmp) path_temp = os.path.join(path_output,"tmp_"+tmp_string) if not os.path.exists(path_temp): os.makedirs(path_temp) # get vertices to ignore if arr_ignore is not None: _, ind_ignore = get_ignore(vtx, arr_ignore, ras2vox_tkr, write_output=False, path_output=False) else: ind_ignore = [] print("start mesh initialization (gbb)") # run surface reginement i = 0 j = 0 p = 0 q = 0 counter = 0 step = 0 cost_array = [] m_array = [] n_array = [] n_iter_step = [] n_coords = len(vtx) while True: # get current vertex point n_vertex = np.random.randint(n_coords) if n_vertex in ind_ignore: counter += 1 continue # get shift vtx_shift = get_shift(vtx, fac, vtx_n, n_vertex, arr_gradient, vox2ras_tkr, ras2vox_tkr, arr_vein, line_dir, line_length, t2s, False) # update mesh if len(vtx_shift): nn_ind, _ = nn_3d(vtx[n_vertex], vtx, r_size[step]) if np.any(np.in1d(nn_ind,ind_control)): counter += 1 continue vtx = update_mesh(vtx,vtx_shift,n_vertex,nn_ind,l_rate[step]) else: counter += 1 continue # get cost function if p >= cost_step: J = cost_BBR(vtx, vtx_n, arr_ref, ras2vox_tkr, Q0=Q0, M=M, h=h, s=s, t2s=t2s) cost_array.append(J) q += 1 p = 0 if len(cost_array) >= cost_sample: # check exit criterion m_fit, n_fit, exit_crit = check_exit(np.arange(q-cost_sample,q), cost_array[-cost_sample:], cost_threshold=cost_threshold[step]) # save computed slope and y-axis intercept of liner fit m_array.append(m_fit) n_array.append(n_fit) # shot plots if show_cost: set_title = "Exit criterion: "+str(exit_crit)+", Step: "+str(step) plot_cost(q, cost_array, m_fit, n_fit, set_title, save_plot=False, path_output=False, name_output=False) if show_slope: set_title = "Exit criterion: "+str(exit_crit)+", Step: "+str(step) plot_slope(q, m_array, set_title, save_plot=False, path_output=False, name_output=False) else: exit_crit = False # check exit if exit_crit and step < len(max_iterations) - 1 or j == max_iterations[step]: n_iter_step.append(j) step += 1 j = 0 print("start registration step "+str(step)+" at iteration "+str(i)) elif exit_crit and step == len(max_iterations) - 1: n_iter_step.append(j) gbb_converged = True print("registration converged!") break elif step == len(max_iterations) - 1 and j == max_iterations[-1]: n_iter_step.append(j) gbb_converged = False print("registration did not converge!") break # write intermediate surfaces if write_temp and path_output and not np.mod(i,write_temp): write_geometry(os.path.join(path_temp,"temp_"+str(i)), vtx, fac) i += 1 j += 1 p += 1 # print some information print("final number of iterations: "+str(i)) print("final number of skipped iterations: "+str(counter)) # collect some descriptive variables gbb = dict() gbb["convergence"] = gbb_converged gbb["cost_array"] = cost_array gbb["m_array"] = m_array gbb["n_array"] = n_array gbb["n_iter"] = i gbb["n_skip"] = counter gbb["n_iter_step"] = n_iter_step return vtx, gbb
def make_mesh(boundary_in, ref_in, file_out, nlayer, flip_faces=False, niter_smooth=2, niter_upsample=0, niter_inflate=15): """ This function generates a surface mesh from a levelset image. The surface mesh is smoothed and a curvature file is generated. Vertices are in the vertex ras coordinate system. Optionally, the mesh can be upsampled and an inflated version of the mesh can be written out. The hemisphere has to be indicated as prefix in the output file. If nlayer is set to -1, a 3D levelset image can be used as boundary input file. Inputs: *boundary_in: filename of 4D levelset image. *ref_in: filename of reference volume for getting the coordinate transformation. *file_out: filename of output surface. *nlayer: layer from the 4D boundary input at which the mesh is generated. *flip_faces: reverse normal direction of mesh. *niter_smooth: number of smoothing iterations. *niter_upsample: number of upsampling iterations (is performed if set > 0). *niter_inflate: number of inflating iterations (is performed if set > 0). created by Daniel Haenelt Date created: 18-12-2019 Last modified: 24-07-2020 """ import os import numpy as np import nibabel as nb from nibabel.affines import apply_affine from nibabel.freesurfer.io import write_geometry from nighres.surface import levelset_to_mesh from cortex.polyutils import Surface from lib.surface.vox2ras import vox2ras from lib.surface.smooth_surface import smooth_surface from lib.surface.upsample_surf_mesh import upsample_surf_mesh from lib.surface.get_curvature import get_curvature from lib.surface import inflate_surf_mesh # make output folder if not os.path.exists(os.path.dirname(file_out)): os.makedirs(os.path.dirname(file_out)) # get levelset boundary from single layer boundary = nb.load(boundary_in) boundary.header["dim"][0] = 1 boundary_array = boundary.get_fdata() if nlayer != -1: boundary_array = boundary_array[:, :, :, nlayer] boundary = nb.Nifti1Image(boundary_array, boundary.affine, boundary.header) # make mesh surf = levelset_to_mesh(boundary, connectivity="18/6", level=0.0, inclusive=True) # get vertices and faces vtx = surf["result"]["points"] fac = surf["result"]["faces"] # get vox2ras transformation vox2ras_tkr, _ = vox2ras(ref_in) # apply vox2ras to vertices vtx = apply_affine(vox2ras_tkr, vtx) # flip faces if flip_faces: fac = np.flip(fac, axis=1) # write mesh write_geometry(file_out, vtx, fac) # smooth surface smooth_surface(file_out, file_out, niter_smooth) # upsample mesh (optionally) if niter_upsample != 0: upsample_surf_mesh(file_out, file_out, niter_upsample, "linear") # print number of vertices and average edge length print("number of vertices: " + str(len(vtx[:, 0]))) print("average edge length: " + str(Surface(vtx, fac).avg_edge_length)) # get curvature (looks for hemisphere prefix) get_curvature(file_out, os.path.dirname(file_out)) # inflate surface (optionally) if niter_inflate != 0: inflate_surf_mesh(file_out, file_out + "_inflated", niter_inflate)
def make_sphere(file_in, file_out, n_inflate=100, radius=None): """ The scripts takes a generated FreeSurfer mesh and transformes it into a sphere with defined radius. Inputs: *file_in: filename of input surface. *file_out: filename of output surface. *n_inflated: number of inflating iterations (if > 0). *radius: radius of final sphere in mm (if not None). created by Daniel Haenelt Date created: 26-08-2020 Last modified: 26-08-2020 """ import os import sys import subprocess import numpy as np from shutil import copyfile from nibabel.freesurfer.io import read_geometry, write_geometry from lib.io.get_filename import get_filename from lib.surface.inflate_surf_mesh import inflate_surf_mesh def cart2pol(x, y, z): r = np.sqrt(x**2 + y**2 + z**2) phi = np.arctan2(y, x) theta = np.arccos(z / r) return r, phi, theta def pol2cart(r, phi, theta): x = r * np.sin(theta) * np.cos(phi) y = r * np.sin(theta) * np.sin(phi) z = r * np.cos(theta) return x, y, z # make output folder path_output, _, _ = get_filename(file_out) if not os.path.exists(path_output): os.makedirs(path_output) # temporary file tmp = np.random.randint(0, 10, 5) tmp_string = ''.join(str(i) for i in tmp) file_tmp = os.path.join(path_output, tmp_string) # inflate surface mesh if n_inflate: inflate_surf_mesh(file_in, file_tmp, n_inflate) else: copyfile(file_in, file_tmp) # inflate surface try: subprocess.run(['mris_sphere', '-q', file_tmp, file_out], check=True) except subprocess.CalledProcessError: sys.exit("Sphere computation failed!") # change radius if radius: vtx, fac = read_geometry(file_out) r, phi, theta = cart2pol(vtx[:, 0], vtx[:, 1], vtx[:, 2]) r[:] = radius vtx[:, 0], vtx[:, 1], vtx[:, 2] = pol2cart(r, phi, theta) write_geometry(file_out, vtx, fac) # remove temporary file os.remove(file_tmp)
def run_devein(vtx, fac, vtx_norm, arr_vein, arr_ignore, adjm, ras2vox_tkr, n_neighbor=20, line_dir=2, smooth_iter=30, max_iterations=1000): """ Run devein This function finds vertex points which are located within marked veins and shift these and their neighborhood until the mesh is free of trapped vertices (or the maximum number of iterations is reached). Shifts are computed by averaging vertex coordinates within a neighborhood around a single vertex along one direction or along all axes. All shifts are applied in inward direction. Optionally, the output mesh can be smoothed. Parameters ---------- vtx : ndarray Array of vertex points. fac : ndarray Array of corresponding faces. vtx_norm : ndarray Array of corresponding vertex normals. arr_vein : ndarray Vein mask. arr_ignore : ndarray Binary mask where deveining is omitted. adjm : obj Adjacecy matrix. ras2vox_tkr : ndarray Transformation matrix from ras to voxel space. n_neighbor : int, optional Neighborhood size. The default is 20. line_dir : int, optional Shift direction in ras conventions (0,1,2,3). The default is 2. smooth_iter : int, optional Number of smoothing iterations of final mesh. The default is 30. max_iterations : int, optional Maximum number of deveining iterations. The default is 1000. Returns ------- vtx : ndarray Shifted array of vertex points. counter : int Number of deveining iterations. Notes ------- created by Daniel Haenelt Date created: 06-02-2020 Last modified: 08-10-2020 """ # fix parameters line_threshold = 0.05 # if direction is along one axis, omit line if length is below threshold # check line direction if line_dir > 3 or line_dir < 0: sys.exit("error: choose a valid line direction!") # check vein array if arr_vein is None: sys.exit("error: no vein mask found for deveining!") # load arrays arr_vein = np.round(arr_vein).astype(int) # get image dimensions xdim = np.shape(arr_vein)[0] ydim = np.shape(arr_vein)[1] zdim = np.shape(arr_vein)[2] if arr_ignore is not None: arr_ignore = np.round(arr_ignore).astype(int) arr_vein[arr_ignore == 1] = 0 # centroid vtx_c = np.mean(vtx, axis=0) # get nearest voxel coordinates vtx_vox = apply_affine(ras2vox_tkr, vtx) vtx_vox = np.round(vtx_vox).astype(int) # mask outlier points (ignored in deveining) vtx_mask = np.ones(len(vtx_vox)) vtx_mask[vtx_vox[:, 0] > xdim - 1] = 0 vtx_mask[vtx_vox[:, 1] > ydim - 1] = 0 vtx_mask[vtx_vox[:, 2] > zdim - 1] = 0 vtx_mask[vtx_vox[:, 0] < 0] = 0 vtx_mask[vtx_vox[:, 1] < 0] = 0 vtx_mask[vtx_vox[:, 2] < 0] = 0 # get vertices trapped in veins vein_mask = arr_vein[vtx_vox[:, 0], vtx_vox[:, 1], vtx_vox[:, 2]] * vtx_mask n_veins = len(vein_mask[vein_mask == 1]) print("start mesh initialization (deveining)") # loop through vertices counter = 0 while n_veins > 0 and counter < max_iterations: # print current status print("i: " + str(counter) + ", # of trapped vertices: " + str(len(vein_mask[vein_mask == 1]))) # select random vein vertex vein_ind = np.where(vein_mask == 1)[0] curr_ind = vein_ind[np.random.randint(len(vein_ind))] nn_ind = nn_2d(curr_ind, adjm, n_neighbor) # get shift as weighted average if line_dir == 3: vtx_shift = vtx[nn_ind, :].copy() vtx_shift = vtx[curr_ind, :] - vtx_shift else: vtx_shift = np.zeros((len(nn_ind), 3)) vtx_shift[:, line_dir] = vtx[nn_ind, line_dir] vtx_shift[:, line_dir] = vtx[curr_ind, line_dir] - vtx_shift[:, line_dir] vtx_shift = np.mean(vtx_shift, axis=0) # check inward shift by comparing distance to centroid vtx_dist_noshift = norm(vtx[curr_ind, :] - vtx_c) vtx_dist_shift = norm(vtx[curr_ind, :] - vtx_shift - vtx_c) # do only inward shifts if vtx_dist_shift - vtx_dist_noshift > 0: vtx_shift = -1 * vtx_shift # update mesh if valid inward shift if line_dir != 3 and np.abs(vtx_norm[curr_ind, line_dir]) < line_threshold: vtx_temp_vox = apply_affine(ras2vox_tkr, vtx[curr_ind]) vtx_temp_vox = np.round(vtx_temp_vox).astype(int) arr_vein[vtx_temp_vox[0], vtx_temp_vox[1], vtx_temp_vox[2]] = 0 else: vtx = update_mesh(vtx, vtx_shift, curr_ind, nn_ind, 1) vtx_vox = apply_affine(ras2vox_tkr, vtx) vtx_vox = np.round(vtx_vox).astype(int) # get all vertices within vein vein_mask = np.zeros(len(vtx)) vein_mask = arr_vein[vtx_vox[:, 0], vtx_vox[:, 1], vtx_vox[:, 2]] * vtx_mask n_veins = len(vein_mask[vein_mask == 1]) counter += 1 if counter < max_iterations: print("deveining converged!") # smooth output if smooth_iter: tmp = np.random.randint(0, 10, 5) tmp_string = ''.join(str(i) for i in tmp) surf_temp = os.path.join(os.getcwd(), "surf_" + tmp_string) write_geometry(surf_temp, vtx, fac) smooth_surface(surf_temp, surf_temp, smooth_iter) vtx, _ = read_geometry(surf_temp) os.remove(surf_temp) return vtx, counter
def register_retinotopy_command(args): ''' register_retinotopy_command(args) can be given a list of arguments, such as sys.argv[1:]; these arguments may include any options and must include at least one subject id. All subjects whose ids are given are registered to a retinotopy model, and the resulting registration, as well as the predictions made by the model in the registration, are exported. ''' # Parse the arguments (args, opts) = _retinotopy_parser(args) # First, help? if opts['help']: print register_retinotopy_help return 1 # and if we are verbose, lets setup a note function verbose = opts['verbose'] def note(s): if verbose: print s return verbose # Add the subjects directory, if there is one if 'subjects_dir' in opts and opts['subjects_dir'] is not None: add_subject_path(opts['subjects_dir']) # Parse the simple numbers for o in [ 'weight_cutoff', 'edge_strength', 'angle_strength', 'func_strength', 'max_step_size', 'max_out_eccen' ]: opts[o] = float(opts[o]) opts['max_steps'] = int(opts['max_steps']) # These are for now not supported: #TODO if opts['angle_math'] or opts['angle_radians'] or opts['eccen_radians']: print 'Mathematical angles and angles not in degrees are not yet supported.' return 1 # The remainder of the args can wait for now; walk through the subjects: tag_key = { 'eccen': 'eccentricity', 'angle': 'polar_angle', 'label': 'visual_area' } for subnm in args: sub = freesurfer_subject(subnm) note('Processing subject: %s' % sub.id) # we need to register this subject... res = {} ow = not opts['no_overwrite'] for h in ['LH', 'RH']: note(' Processing hemisphere: %s' % h) hemi = sub.__getattr__(h) # See if we are loading custom values... (ang, ecc, wgt) = (None, None, None) suffix = '_' + h.lower() + '_file' if opts['angle' + suffix] is not None: ang = _guess_surf_file(opts['angle' + suffix]) if opts['eccen' + suffix] is not None: ecc = _guess_surf_file(opts['eccen' + suffix]) if opts['weight' + suffix] is not None: wgt = _guess_surf_file(opts['weight' + suffix]) # Do the registration note(' - Running Registration...') res[h] = register_retinotopy( hemi, retinotopy_model(), polar_angle=ang, eccentricity=ecc, weight=wgt, weight_cutoff=opts['weight_cutoff'], partial_voluming_correction=opts['part_vol_correct'], edge_scale=opts['edge_strength'], angle_scale=opts['angle_strength'], functional_scale=opts['func_strength'], prior=opts['prior'], max_predicted_eccen=opts['max_out_eccen'], max_steps=opts['max_steps'], max_step_size=opts['max_step_size']) # Perform the hemi-specific outputs now: if not opts['no_reg_export']: regnm = '.'.join( [h.lower(), opts['registration_name'], 'sphere', 'reg']) flnm = (os.path.join(sub.directory, 'surf', regnm) if h == 'LH' else os.path.join(sub.directory, 'xhemi', 'surf', regnm)) if ow or not os.path.exist(flnm): note(' - Exporting registration file: %s' % flnm) fsio.write_geometry( flnm, res[h].coordinates.T, res[h].faces.T, 'Created by neuropythy (github.com/noahbenson/neuropythy)' ) else: note(' - Skipping registration file: %s (file exists)' % flnm) if not opts['no_surf_export']: for dim in ['angle', 'eccen', 'label']: flnm = os.path.join( sub.directory, 'surf', '.'.join([h.lower(), opts[dim + '_tag'], 'mgz'])) if ow or not os.path.exist(flnm): note(' - Exporting prediction file: %s' % flnm) img = fsmgh.MGHImage( np.asarray([[res[h].prop(tag_key[dim])]], dtype=(np.int32 if dim == 'label' else np.float32)), np.eye(4)) img.to_filename(flnm) else: note(' - Skipping prediction file: %s (file exists)' % flnm) # Do the volume exports here if not opts['no_vol_export']: note(' Processing volume data...') note(' - Calculating cortex-to-ribbon mapping...') surf2rib = cortex_to_ribbon_map(sub, hemi=None) for dim in ['angle', 'eccen', 'label']: flnm = os.path.join(sub.directory, 'mri', opts[dim + '_tag'] + '.mgz') if ow or not os.path.exist(flnm): note(' - Generating volume file: %s' % flnm) vol = cortex_to_ribbon( sub, (res['LH'].prop(tag_key[dim]), res['RH'].prop( tag_key[dim])), map=surf2rib, method=('max' if dim == 'label' else 'weighted'), dtype=(np.int32 if dim == 'label' else np.float32)) note(' - Exporting volume file: %s' % flnm) vol.to_filename(flnm) else: note(' - Skipping volume file: %s (file exists)' % flnm) # That is it for this subject! note(' Subject %s finished!' % sub.id) # And if we made it here, all was successful. return 0