def calculate_area(filename_surf, filename_area=""): """ The function calculates vertex-wise surface area. The code is taken from the octave code surf2area.m from Anderson Winkler found in his github repository (https://github.com/ andersonwinkler/areal). Consider a triangular face ABC with corner points a = [x_A, y_A, z_A]' b = [x_B, y_B, z_B]' c = [x_C, y_C, z_C]' The area for this triangle is given by the normed cross product A = |u x v|/2 with u = a - c and v = b - c. This is a face-wise surface area representation. To convert this to a vertex-wise representation, we assign each vertex one third of the sum of the areas of all faces that meet at that vertex. Cf. Anderson Winkler et al. Measuring and comparing brain cortical surface area and other areal quantities, Neuroimage 61(4), p. 1428-1443 (2012). Inputs: *file_surf: input file geometry on which surface area is calculated. *file_area: file name of the surface area file. Outputs: *dpv: vertex-wise surface area. created by Daniel Haenelt Date created: 01-11-2018 Last modified: 17-12-2018 """ import numpy as np from numpy.linalg import norm from nibabel.freesurfer.io import write_morph_data, read_geometry # Read the surface file vtx, fac = read_geometry(filename_surf) nV = len(vtx) nF = len(fac) # compute area per face (DPF) facvtx = np.concatenate([vtx[fac[:, 0]], vtx[fac[:, 1]], vtx[fac[:, 2]]], axis=1) facvtx0 = facvtx[:, 0:6] - np.concatenate( [facvtx[:, 6:9], facvtx[:, 6:9]], axis=1) # place 3rd vtx at origin cp = np.cross(facvtx0[:, 0:3], facvtx0[:, 3:6], axisa=1, axisb=1) # cross product dpf = norm(cp, axis=1) / 2 # half of the norm print("Total area (facewise): " + str(np.sum(dpf))) # compute area per vertex (DPV) dpv = np.zeros(nV) # for speed, divide the dpf by 3 dpf = dpf / 3 # redistribute for f in range(nF): dpv[fac[f, :]] = dpv[fac[f, :]] + dpf[f] print("Total area (vertexwise): " + str(np.sum(dpv))) # save dpv if filename_area: write_morph_data(filename_area, dpv) # return vertex-wise surface area return dpv
def morph2dense(source_sphere,target_sphere,input_morph,path_output): """ This function maps a morphological file from a source surface to a target target surface. Inputs: *source_sphere: source surface. *target_sphere: target surface. *input_morph: morphological input file. *path_output: path where output is saved. created by Daniel Haenelt Date created: 13-07-2019 Last modified: 13-07-2019 """ import os from nibabel.freesurfer.io import read_morph_data, write_morph_data, read_geometry from scipy.interpolate import griddata # make output folder if not os.path.exists(path_output): os.mkdir(path_output) # transform morphological data to dense surfaces pts_sphere_dense, _ = read_geometry(target_sphere) pts_sphere, _ = read_geometry(source_sphere) # get morphological data morph = read_morph_data(input_morph) # do the transformation method = "nearest" morph_dense = griddata(pts_sphere, morph, pts_sphere_dense, method) # write dense morphological data write_morph_data(os.path.join(path_output,os.path.basename(input_morph)), morph_dense)
def save_freesurfer_morph(filename, obj, face_count=0): ''' save_freesurfer_morph(filename, obj) saves the given object using nibabel.freesurfer.io's write_morph_data function, and returns the given filename. ''' fsio.write_morph_data(filename, obj, fnum=face_count) return filename
def create_R2(in_vol, in_rh_surf, in_lh_surf): img = nib.load(in_vol) data = img.get_fdata() # set all visual areas 1 through 12 to 1.0 for i in range(data.shape[0]): for j in range(data.shape[1]): for k in range(data.shape[2]): if data[i,j,k] > 0.0: data[i,j,k] = 1.0 out_vol = nib.Nifti1Image(data, img.affine) nib.save(out_vol, os.path.join(os.getcwd(),'prf','r2.nii.gz')) data = fsio.read_morph_data(in_rh_surf) for i in range(data.shape[0]): if data[i] > 0.0: data[i] = 1.0 fsio.write_morph_data(os.path.join(os.getcwd(),'prf','prf_surfaces','rh.r2'),data) data = fsio.read_morph_data(in_lh_surf) for i in range(data.shape[0]): if data[i] > 0.0: data[i] = 1.0 fsio.write_morph_data(os.path.join(os.getcwd(),'prf','prf_surfaces','lh.r2'),data)
def get_b0_orientation(surf_in, vol_in, write_output=False, path_output="", name_output=""): """ This function computes the angle between surface normals and B0-direction per vertex. Inputs: *surf_in: input of surface mesh. *vol_in: input of corresponding nifti volume. *write output: write out to disk (boolean). *path_output: path where to save output. *name_output: basename of output file. Outputs: *theta: angle in radians. created by Daniel Haenelt Date created: 31-07-2020 Last modified: 31-07-2020 """ import os import numpy as np import nibabel as nb from nibabel.affines import apply_affine from nibabel.freesurfer.io import read_geometry, write_morph_data from lib.io.get_filename import get_filename from lib.surface.vox2ras import vox2ras from lib_gbb.normal import get_normal # make subfolders if write_output and not os.path.exists(path_output): os.makedirs(path_output) # get hemi from surface filename _, hemi, _ = get_filename(surf_in) # load surface vtx, fac = read_geometry(surf_in) # get transformation matrix _, r2v = vox2ras(vol_in) # ras-tkr -> voxel v2s = nb.load(vol_in).affine # voxel -> scanner-ras M = v2s.dot(r2v) # apply affine transformation vtx = apply_affine(M, vtx) # get surface normals n = get_normal(vtx, fac) # get angle between b0 and surface normals in radians theta = np.arccos(np.dot(n, [0, 0, 1])) # write output if write_output: write_morph_data(os.path.join(path_output, hemi + "." + name_output), theta) return theta
def project_results_annot(annot, results_file, file_like): results = pd.read_csv(results_file, sep = ',') print(results) annot_info = read_annot(annot) values = annot_info[0] output = values.astype(np.float32)*0 labels = np.array(annot_info[2]) for index, row in results.iterrows(): w = np.where(labels == row.label.encode('utf-8'))[0] if (len(w) > 0): output[values == w] = row.value print(row.label, w, row.value) write_morph_data(file_like = file_like, values = output)
def write_surface(image, filename): extension = filename.split('.')[-1] if extension == 'mha': sitk.WriteImage(image, filename) elif extension == 'annot': print(type(image[1])) io.write_annot(filename, image[0], image[1], image[2]) elif extension == 'label': raise ValueError('Reader for extensions \'label\' not yet implemented') elif extension in ['inflated', 'pial', 'white']: io.read_geometry(filename, image[0], image[1]) else: return io.write_morph_data(filename, image)
def export(flnm, p): fsio.write_morph_data(flnm, p) return flnm
def compare_editors(subjects_dir, outdir, subject, editor1, editor2, hemi, surfname): subj_dir1 = os.path.join(subjects_dir, '{}-{}'.format(subject, editor1)) subj_dir2 = os.path.join(subjects_dir, '{}-{}'.format(subject, editor2)) # Construct surface paths surf1_fname = os.path.join(subj_dir1, 'surf', '{}.{}'.format(hemi, surfname)) surf2_fname = os.path.join(subj_dir2, 'surf', '{}.{}'.format(hemi, surfname)) # Init Hausdorff distance return values d12, d21, dsym = np.nan, np.nan, np.nan # Init continuation flag keep_going = True coords1 = np.zeros([ 1, ]) coords2 = np.zeros([ 1, ]) if not os.path.isfile(surf1_fname): print('* Subject 1 surface file {} does not exist - exiting'.format( surf1_fname)) keep_going = False if not os.path.isfile(surf2_fname): print('* Subject 2 surface file {} does not exist - exiting'.format( surf2_fname)) keep_going = False # Load surfaces try: coords1, faces1 = read_geometry(surf1_fname) except IOError: print('* Problem loading surface from {}'.format(surf1_fname)) keep_going = False try: coords2, faces2 = read_geometry(surf2_fname) except IOError: print('* Problem loading surface from {}'.format(surf2_fname)) keep_going = False if keep_going: print('{}-{}-{}-{} mesh has {} points'.format(subject, editor1, hemi, surfname, coords1.shape[0])) print('{}-{}-{}-{} mesh has {} points'.format(subject, editor2, hemi, surfname, coords2.shape[0])) # Fast pairwise Euclidean distances between nodes of surface 1 and 2 # If coords1 is N x 3 and coords2 is M x 3, distmin is N x M print('Computing pairwise distances ({} to {})'.format( editor1, editor2)) t0 = dt.now() _, dmin12 = pairwise_distances_argmin_min(coords1, coords2) delta = dt.now() - t0 print('Done in {:0.3f} seconds'.format(delta.total_seconds())) # Calculate forward Hausdorff distance from pairwise distance results d12 = np.max(dmin12) # Fast Hausdorff distances between nodes of surface 1 and 2 print('Computing Fast Hausdorff Distances') t0 = dt.now() d21, _, _ = directed_hausdorff(coords2, coords1) delta = dt.now() - t0 print('Done in {:0.3f} seconds'.format(delta.total_seconds())) # Symmetric Hausdorff distance (max(d12, d21)) dsym = max(d12, d21) print('Forward Hausdorff Distance : {:0.3f} mm'.format(d12)) print('Reverse Hausdorff Distance : {:0.3f} mm'.format(d21)) print('Symmetric Hausdorff Distance : {:0.3f} mm'.format(dsym)) # Save closest distances as a morphometry/curv file dist_fname = os.path.join( outdir, '{}-{}-{}-{}-{}.dist'.format(subject, editor1, editor2, hemi, surfname)) print('Saving intersurface distances to {}'.format(dist_fname)) write_morph_data(dist_fname, dmin12) # Copy subject 1 surface to output directory for use with distance annotation in Freeview surf1_bname = os.path.basename(surf1_fname) surf1_outname = '{}-{}-{}'.format(subject, editor1, surf1_bname) print('Copying {} to {}'.format(surf1_bname, surf1_outname)) shutil.copy(surf1_fname, os.path.join(outdir, surf1_outname)) return subject, editor1, editor2, hemi, surfname, d12, d21, dsym
sulc = transfertodense(subjectid, a3, hemi,'nearest') # save(sprintf('%s/%smidgrayDENSE.pkl',dir0,hemi),'thickness','curvature') # write mgz writemgz(subjectid,'thicknessDENSE',thickness, hemi) writemgz(subjectid,'curvatureDENSE',curvature, hemi) writemgz(subjectid,'sulcDENSE', sulc, hemi) # write mgz for truncated writemgz(subjectid,f'thicknessDENSETRUNC{fstruncate}',thickness[a2['validix']],hemi) writemgz(subjectid,f'curvatureDENSETRUNC{fstruncate}',curvature[a2['validix']],hemi) writemgz(subjectid,f'sulcDENSETRUNC{fstruncate}', sulc[a2['validix']], hemi) # write curv _,facesA = fsio.read_geometry((Path(fsdir)/'surf'/f'{hemi}.inflatedDENSE').str) fsio.write_morph_data((Path(fsdir)/'surf'/f'{hemi}DENSE.curv').str,curvature,facesA.shape[0]) # write curv for truncated _,facesA = fsio.read_geometry((Path(fsdir)/'surf'/f'{hemi}.inflatedDENSETRUNC{fstruncate}').str) fsio.write_morph_data((Path(fsdir)/'surf'/f'{hemi}DENSETRUNC{fstruncate}.curv').str, curvature[a2['validix']], facesA.shape[0]) ######## compute and save some useful quantities for dense trunc surfaces # for each hemisphere for hemi in hemis: # for each of the surfaces for sf in surfs: # read in the surface
def calculate_distortion(file_patch, file_white, path_output, hemi): """ This script computes two metrics (areal distortion and line distortion) to estimate the amount of distortion in the flattening process. The form of the metric is similar to http://brainvis.wustl.edu/wiki/index.php/Caret:Operations/Morphing For vertex-wise areal distortion (VAD), first all faces which lie within the patch are searched. Then, triangle areas before and after flattening are computed for each face. The amount of distortion is defined as ratio between both areas. A vertex-wise representation is computed by considereing each vertex point within the patch and taking the sum of areal distortions of all neighbouring faces. For vertex-wise line distortion (VLD), for each node in the patch, we take the vertex points before and after flattening and look for all neighbouring nodes, i.e., we are looking for all faces which contain the node. We take then the sum of all euclidean distances to all neighbouring nodes and before and after flattening separately and compute the ratio between summed distances. Note that only points within the patch are taken into account which have full faces within the point cloud of the patch corresponding to the original white surface. I.e., a few border points are excluded from the analysis and set to zero in the morphological output file. The number of excluded points is returned for each metric. Inputs: *file_patch: filename of flattened patch. *file_white: filename of white surface. *path_output: path where output is written. *hemi: hemisphere. Outputs: *VAD_params: Descriptive parameters of areal distortion. *VLD_paramd: Descriptive parameters of line distortion. created by Daniel Haenelt Date created: 01-11-2018 Last modified: 15-01-2020 """ import os import numpy as np from numpy.linalg import norm from scipy.stats import sem from nibabel.freesurfer.io import read_geometry, write_morph_data from lib.io.read_patch import read_patch # make output folder if not os.path.exists(path_output): os.makedirs(path_output) # load data vtx_white, fac_white = read_geometry(file_white) _, _, _, ind_patch = read_patch(file_patch) vtx_patch = np.zeros((len(ind_patch),3)) vtx_patch[:,0], vtx_patch[:,1], vtx_patch[:,2], _ = read_patch(file_patch) # look for faces which exist in the patch fac_patch = [] for i in range(len(fac_white)): if np.any(fac_white[i,0]==ind_patch) and np.any(fac_white[i,1]==ind_patch) and np.any(fac_white[i,2]==ind_patch): fac_patch.append(fac_white[i,:]) fac_patch = np.array(fac_patch) """ Areal distortion """ # calculate face-wise areal distortion (before flattening) facvtx_white = np.concatenate([vtx_white[fac_patch[:,0]], vtx_white[fac_patch[:,1]], vtx_white[fac_patch[:,2]]], axis=1) facvtx0_white = facvtx_white[:,0:6] - np.concatenate([facvtx_white[:,6:9], facvtx_white[:,6:9]], axis=1) # place 3rd vtx at origin cp = np.cross(facvtx0_white[:,0:3],facvtx0_white[:,3:6], axisa=1, axisb=1) # cross product A_white = norm(cp, axis=1) / 2 # half of the norm # calculate face-wise areal distortion (after flattening) vtx_patch_all = np.zeros_like(vtx_white).astype(float) for i in range(len(ind_patch)): vtx_patch_all[ind_patch[i],:] = vtx_patch[i,:] facvtx_patch = np.concatenate([vtx_patch_all[fac_patch[:,0]], vtx_patch_all[fac_patch[:,1]], vtx_patch_all[fac_patch[:,2]]], axis=1) facvtx0_patch = facvtx_patch[:,0:6] - np.concatenate([facvtx_patch[:,6:9], facvtx_patch[:,6:9]], axis=1) # place 3rd vtx at origin cp = np.cross(facvtx0_patch[:,0:3],facvtx0_patch[:,3:6], axisa=1, axisb=1) # cross product A_patch = norm(cp, axis=1) / 2 # half of the norm # calculate face-wise distortion A_dist = A_patch/A_white # convert to vertex-wise representation VAD = np.zeros(len(vtx_white)).astype(float) VAD_miss = 0 for i in range(len(ind_patch)): temp = np.where(fac_patch==ind_patch[i]) if np.any(temp[0]): VAD[ind_patch[i]] = np.sum(A_dist[temp[0]])/len(temp[0]) else: VAD_miss+=1 VAD_params = [np.mean(VAD[ind_patch]), np.std(VAD[ind_patch]), sem(VAD[ind_patch]), np.min(VAD[ind_patch]), np.max(VAD[ind_patch]), VAD_miss] # save morphological data write_morph_data(os.path.join(path_output, os.path.basename(file_patch)+".areal_distortion"), VAD) """ Linear distortion """ VLD = np.zeros(len(vtx_white)).astype(float) VLD_miss = 0 for i in range(len(ind_patch)): node_patch = vtx_patch_all[ind_patch[i]] node_white = vtx_white[ind_patch[i]] ind_temp = fac_patch[np.where(fac_patch==ind_patch[i])[0],:] ind_temp = np.reshape(ind_temp,len(ind_temp)*3) ind_temp = np.unique(ind_temp) ind_temp = np.delete(ind_temp,np.where(ind_temp==ind_patch[i])) if np.any(ind_temp): VLD_patch = 0 VLD_white = 0 for j in range(len(ind_temp)): VLD_patch += norm(node_patch-vtx_patch_all[ind_temp[j]]) VLD_white += norm(node_white-vtx_white[ind_temp[j]]) VLD[ind_patch[i]] = VLD_patch/VLD_white else: VLD_miss+=1 VLD_params = [np.mean(VLD[ind_patch]), np.std(VLD[ind_patch]), sem(VLD[ind_patch]), np.min(VLD[ind_patch]), np.max(VLD[ind_patch]), VLD_miss] # save morphological data write_morph_data(os.path.join(path_output, os.path.basename(file_patch)+".line_distortion"), VLD) return VAD_params, VLD_params