def load_folds(folds_file, graph_file=None): """ Load morphologist folds and associated labels. Parameters ---------- folds_file: str( mandatory) the folds '.gii' file. graph_file: str (optional, default None) the path to a morphologist '.arg' graph file. Returns ------- folds: dict with TriSurface all the loaded folds. The fold names are stored in the metadata. """ # Load the labels if graph_file is not None: labels = parse_graph(graph_file) else: labels = {} # Load folds image = gio.read(folds_file) nb_of_surfs = len(image.darrays) if nb_of_surfs % 2 != 0: raise ValueError("Need an odd number of arrays (vertices, triangles).") folds = {} for vertindex in range(0, nb_of_surfs, 2): vectices = image.darrays[vertindex].data triangles = image.darrays[vertindex + 1].data labelindex = image.darrays[vertindex].get_metadata()["Timestep"] if labelindex != image.darrays[vertindex + 1].get_metadata()["Timestep"]: raise ValueError("Gifti arrays '{0}' and '{1}' do not share the " "same label.".format(vertindex, vertindex + 1)) labelindex = int(labelindex) if labelindex in labels: label = labels[labelindex] else: label = "NC" metadata = {"fold_name": label} surf = TriSurface(vectices, triangles, labels=None, metadata=metadata) folds[labelindex] = surf return folds
def surf_convert( fsdir, t1files, surffiles, sidpos=-3, rm_orig=False, fsconfig=DEFAULT_FREESURFER_PATH): """ Export FreeSurfer surfaces to the native space. Note that all the returned vetices are given in the index coordinate system. The subject id in the t1 and surf files must appear in the 'sidpos' position. For the default value '-3', the T1 path might look like 'xxx/subject_id/convert/t1.nii.gz' Parameters ---------- fsdir: str (mandatory) The FreeSurfer working directory with all the subjects. t1files: str (mandatory) The t1 nifti files. surffiles: The surfaces to be converted. sidpos: int (optional, default -3) The subject identifier position in the surface and T1 files. rm_orig: bool (optional) If True remove the input surfaces. fsconfig: str (optional) The FreeSurfer configuration batch. Returns ------- csurffiles: The converted surfaces in the native space indexed coordinates. """ # Check input parameters for path in t1files + surffiles: if not os.path.isfile(path): raise ValueError("'{0}' is not a valid file.".format(path)) if not os.path.isdir(fsdir): raise ValueError("'{0}' is not a valid directory.".format(fsdir)) # Create a t1 subject map t1map = {} for fname in t1files: subject_id = fname.split(os.path.sep)[sidpos] if subject_id in t1map: raise ValueError("Can't map two t1 for subject " "'{0}'.".format(subject_id)) t1map[subject_id] = fname # Convert all the surfaces csurffiles = [] for fname in surffiles: # Get the t1 reference image subject_id = fname.split(os.path.sep)[sidpos] t1file = t1map[subject_id] t1_image = nibabel.load(t1file) # Compute the conformed space to the native anatomical deformation asegfile = os.path.join(fsdir, subject_id, "mri", "aseg.mgz") physical_to_index = numpy.linalg.inv(t1_image.get_affine()) translation = tkregister_translation(asegfile, fsconfig) deformation = numpy.dot(physical_to_index, translation) # Load and warp the mesh # The mesh: a 2-uplet with vertex (x, y, z) coordinates and # mesh triangles mesh = freesurfer.read_geometry(fname) surf = TriSurface(vertices=apply_affine_on_mesh(mesh[0], deformation), triangles=mesh[1]) # Save the mesh in the native space outputfile = fname + ".native" surf.save(outputfile) csurffiles.append(outputfile) # Construct the surfaces binarized volume binarizedfile = os.path.join(outputfile + ".nii.gz") overlay = numpy.zeros(t1_image.shape, dtype=numpy.uint) indices = numpy.round(surf.vertices).astype(int).T indices[0, numpy.where(indices[0] >= t1_image.shape[0])] = 0 indices[1, numpy.where(indices[1] >= t1_image.shape[1])] = 0 indices[2, numpy.where(indices[2] >= t1_image.shape[2])] = 0 overlay[indices.tolist()] = 1 overlay_image = nibabel.Nifti1Image(overlay, t1_image.get_affine()) nibabel.save(overlay_image, binarizedfile) # Clean input surface if specified if rm_orig: os.remove(fname) return csurffiles
def surf_convert(fsdir, t1files, surffiles, sidpos=-3, rm_orig=False, fsconfig=DEFAULT_FREESURFER_PATH): """ Export FreeSurfer surfaces to the native space. Note that all the returned vetices are given in the index coordinate system. The subject id in the t1 and surf files must appear in the 'sidpos' position. For the default value '-3', the T1 path might look like 'xxx/subject_id/convert/t1.nii.gz' Parameters ---------- fsdir: str (mandatory) The FreeSurfer working directory with all the subjects. t1files: str (mandatory) The t1 nifti files. surffiles: The surfaces to be converted. sidpos: int (optional, default -3) The subject identifier position in the surface and T1 files. rm_orig: bool (optional) If True remove the input surfaces. fsconfig: str (optional) The FreeSurfer configuration batch. Returns ------- csurffiles: The converted surfaces in the native space indexed coordinates. """ # Check input parameters for path in t1files + surffiles: if not os.path.isfile(path): raise ValueError("'{0}' is not a valid file.".format(path)) if not os.path.isdir(fsdir): raise ValueError("'{0}' is not a valid directory.".format(fsdir)) # Create a t1 subject map t1map = {} for fname in t1files: subject_id = fname.split(os.path.sep)[sidpos] if subject_id in t1map: raise ValueError("Can't map two t1 for subject " "'{0}'.".format(subject_id)) t1map[subject_id] = fname # Convert all the surfaces csurffiles = [] for fname in surffiles: # Get the t1 reference image subject_id = fname.split(os.path.sep)[sidpos] t1file = t1map[subject_id] t1_image = nibabel.load(t1file) # Compute the conformed space to the native anatomical deformation asegfile = os.path.join(fsdir, subject_id, "mri", "aseg.mgz") physical_to_index = numpy.linalg.inv(t1_image.get_affine()) translation = tkregister_translation(asegfile, fsconfig) deformation = numpy.dot(physical_to_index, translation) # Load and warp the mesh # The mesh: a 2-uplet with vertex (x, y, z) coordinates and # mesh triangles mesh = freesurfer.read_geometry(fname) surf = TriSurface(vertices=apply_affine_on_mesh(mesh[0], deformation), triangles=mesh[1]) # Save the mesh in the native space outputfile = fname + ".native" surf.save(outputfile) csurffiles.append(outputfile) # Construct the surfaces binarized volume binarizedfile = os.path.join(outputfile + ".nii.gz") overlay = numpy.zeros(t1_image.shape, dtype=numpy.uint) indices = numpy.round(surf.vertices).astype(int).T indices[0, numpy.where(indices[0] >= t1_image.shape[0])] = 0 indices[1, numpy.where(indices[1] >= t1_image.shape[1])] = 0 indices[2, numpy.where(indices[2] >= t1_image.shape[2])] = 0 overlay[indices.tolist()] = 1 overlay_image = nibabel.Nifti1Image(overlay, t1_image.get_affine()) nibabel.save(overlay_image, binarizedfile) # Clean input surface if specified if rm_orig: os.remove(fname) return csurffiles
def qc_profile( nodif_file, proba_file, proba_texture, ico_order, fsdir, sid, outdir, fsconfig, actor_ang=(0., 0., 0.)): """ Connectivity profile QC. Generates views of: - the superposition of the nodif image with tractography result volume. - the connected points on the cortical surface Resample cortical meshes if needed. Results output are available as gif and png. Parameters ---------- nodif_file: str (mandatory) file for probtrackx2 containing the no diffusion volume and associated space information. proba_file: str (mandatory) the protrackx2 output seeding probabilistic path volume. proba_texture: dict (mandatory) the FreeSurfer mri_vol2surf '.mgz' 'lh' and 'rh' textrue that contains the cortiacal connection strength. ico_order: int (mandatory) icosahedron order in [0, 7] that will be used to generate the cortical surface texture at a specific tessalation (the corresponding cortical surface can be resampled using the 'clindmri.segmentation.freesurfer.resample_cortical_surface' function). fsdir: str (mandatory) FreeSurfer subjects directory 'SUBJECTS_DIR'. sid: str (mandatory) FreeSurfer subject identifier. outdir: str (mandatory) The QC output directory. fsconfig: str (mandatory) the FreeSurfer '.sh' config file. actor_ang: 3-uplet (optinal, default (0, 0, 0)) the actor x, y, z position (in degrees). Returns ------- snaps: list of str two gifs images, one showing the connection profile as a texture on the cortical surface, the other a volumic representation of the deterministic tractography. """ # Construct/check the subject directory subjectdir = os.path.join(fsdir, sid) if not os.path.isdir(subjectdir): raise ValueError( "'{0}' is not a FreeSurfer subject directory.".format(subjectdir)) # Check that the output QC directory exists if not os.path.isdir(outdir): os.makedirs(outdir) # Superpose the nodif and probabilistic tractography volumes proba_shape = nibabel.load(proba_file).shape snaps = [] snaps.append( animate_image(nodif_file, overlay_file=proba_file, clean=True, overlay_cmap="Spectral", cut_coords=proba_shape[2], outdir=outdir)) # Define a renderer ren = pvtk.ren() # For each hemisphere for hemi in ["lh", "rh"]: # Get the white mesh on the desired icosphere meshfile = os.path.join( subjectdir, "convert", "{0}.white.{1}.native".format( hemi, ico_order)) if not os.path.isfile(meshfile): raise ValueError( "'{0}' is not a valid white mesh. Generate it through the " "'clindmri.scripts.freesurfer_conversion' script.".format( meshfile)) # Check texture has the expected extension, size texture_file = proba_texture[hemi] if not texture_file.endswith(".mgz"): raise ValueError("'{0}' is not a '.mgz' file. Format not " "supported.".format(texture_file)) profile_array = nibabel.load(texture_file).get_data() profile_dim = profile_array.ndim profile_shape = profile_array.shape if profile_dim != 3: raise ValueError( "Expected profile texture array of dimension 3 not " "'{0}'".format(profile_dim)) if (profile_shape[1] != 1) or (profile_shape[2] != 1): raise ValueError( "Expected profile texture array of shape (*, 1, 1) not " "'{0}'.".format(profile_shape)) # Flatten the profile texture array texture = profile_array.ravel() # Load the white mesh surface = TriSurface.load(meshfile) # Define a textured surface actor actor = pvtk.surface(surface.vertices, surface.triangles, texture) actor.RotateX(actor_ang[0]) actor.RotateY(actor_ang[1]) actor.RotateZ(actor_ang[2]) pvtk.add(ren, actor) # Create a animaton with the generated surface qcname = "profile_as_texture" snaps.extend( pvtk.record(ren, outdir, qcname, n_frames=36, az_ang=10, animate=True, delay=10)) return snaps
def display_folds(folds_file, labels, weights, white_file=None, pits_file=None, dist_indices=None, interactive=True, snap=False, animate=False, outdir=None, name="folds", actor_ang=(0., 0., 0.)): """ Display the folds computed by morphologist. The scene supports one feature activated via the keystroke: * 'p': Pick the data at the current mouse point. This will pop-up a window with information on the current pick (ie. the fold name). Parameters ---------- folds_file: str( mandatory) the folds '.gii' file. labels: dict (mandatory) a mapping between a mesh id and its label. weights: dict (mandatory) a mapping between a mesh label and its wheight in [0, 1]. white_file: str (optional, default None) if specified the white surface will be displayed. pits_file: str (optional, default None) if specified the PITS locations (need the white mesh). dist_indices: array (N, 2) a list of two white matter mesh vertex indices from which we compute a geodesic path. interactive: bool (optional, default True) if True display the renderer. snap: bool (optional, default False) if True create a snap of the scene: need a valid outdir. animate: bool (optional, default False) if True create a gif 360 degrees animation of the scene: need a valid outdir. outdir: str (optional, default None) an existing directory. name: str (optional, default 'folds') the basename of the generated files. actor_ang: 3-uplet (optinal, default (0, 0, 0)) the actors x, y, z position (in degrees). """ # Load the folds file folds = load_folds(folds_file, graph_file=None) # Create an actor for each fold ren = pvtk.ren() ren.SetBackground(1, 1, 1) for labelindex, surf in folds.items(): if labelindex in labels: label = labels[labelindex] if label in weights: weight = weights[label] * 256. else: weight = 0 else: label = "NC" weight = 0 actor = pvtk.surface(surf.vertices, surf.triangles, surf.labels + weight) actor.label = label actor.RotateX(actor_ang[0]) actor.RotateY(actor_ang[1]) actor.RotateZ(actor_ang[2]) pvtk.add(ren, actor) # Add the white surface if specified if white_file is not None: image = gio.read(white_file) nb_of_surfs = len(image.darrays) if nb_of_surfs != 2: raise ValueError("'{0}' does not a contain a valid white " "mesh.".format(white_file)) vertices = image.darrays[0].data triangles = image.darrays[1].data wm_surf = TriSurface(vertices, triangles, labels=None) actor = pvtk.surface(wm_surf.vertices, wm_surf.triangles, wm_surf.labels, opacity=0.7, set_lut=False) actor.label = "white" actor.RotateX(actor_ang[0]) actor.RotateY(actor_ang[1]) actor.RotateZ(actor_ang[2]) pvtk.add(ren, actor) # Add the PITS if specified if pits_file is not None and white_file is not None: image = gio.read(pits_file) nb_of_surfs = len(image.darrays) if nb_of_surfs != 1: raise ValueError("'{0}' does not a contain a valid pits " "texture.".format(pits_file)) pits_texture = image.darrays[0].data pits_locations = wm_surf.vertices[numpy.where(pits_texture == 1)] actor = pvtk.dots(pits_locations, color=(1, 0, 0), psize=20, opacity=1) actor.label = "pits" actor.RotateX(actor_ang[0]) actor.RotateY(actor_ang[1]) actor.RotateZ(actor_ang[2]) pvtk.add(ren, actor) # Geodesic path if dist_indices is not None and white_file is not None: all_path = [] for ind1, ind2 in dist_indices: all_path.append( wm_surf.geodesic_distance(vertices[ind1], vertices[ind2])) actor = pvtk.tubes(all_path, (0, 1, 0), opacity=1, linewidth=1, tube_sides=8, lod=True, lod_points=10**4, lod_points_size=5) actor.label = "geodesic" actor.RotateX(actor_ang[0]) actor.RotateY(actor_ang[1]) actor.RotateZ(actor_ang[2]) pvtk.add(ren, actor) # Show the renderer if interactive: actor = pvtk.text("!!!!", font_size=15, position=(10, 10), is_visible=False) pvtk.add(ren, actor) obs = LabelsOnPick(actor, static_position=True, to_keep_actors=["white", "pits", "geodesic"]) pvtk.show(ren, title=name, observers=[obs]) # Create a snap if snap: if not os.path.isdir(outdir): raise ValueError("'{0}' is not a valid directory.".format(outdir)) pvtk.record(ren, outdir, name, n_frames=1) # Create an animation if animate: if not os.path.isdir(outdir): raise ValueError("'{0}' is not a valid directory.".format(outdir)) pvtk.record(ren, outdir, name, n_frames=36, az_ang=10, animate=True, delay=25)
def display_pits_parcellation(white_file, parcellation_file, labels=None, pits_file=None, parcellation_as_annotation=False, interactive=True, snap=False, animate=False, outdir=None, name="pits_parcellation", actor_ang=(0., 0., 0.)): """ Display the pits parcellation. The scene supports one feature activated via the keystroke: * 'p': Pick the data at the current mouse point. This will pop-up a window with information on the current pick (ie. the areal name). Parameters ---------- white_file: str the white surface that will be displayed. parcellation_file: str the parcellation texture file. labels: dict, default None a mapping between an areal number and its name. pits_file: str, default None if specified the PITS locations. parcellation_as_annotation: bool, default False if set expect a FreeSurfer annotation file as a parcellation input. interactive: bool, default True if True display the renderer. snap: bool, default False if True create a snap of the scene: need a valid outdir. animate: bool, default False if True create a gif 360 degrees animation of the scene: need a valid outdir. outdir: str, default None an existing directory. name: str, default 'pits_parcellation' the basename of the generated files. actor_ang: 3-uplet, default (0, 0, 0) the actors x, y, z position (in degrees). """ # Load the PITS if specified if pits_file is not None: image = gio.read(pits_file) nb_of_surfs = len(image.darrays) if nb_of_surfs != 1: raise ValueError("'{0}' does not a contain a valid pits " "texture.".format(pits_file)) pits_texture = image.darrays[0].data else: pits_texture = None # Create an actor for the white matter surface ren = pvtk.ren() ren.SetBackground(1, 1, 1) if white_file.endswith(".gii"): image = gio.read(white_file) nb_of_surfs = len(image.darrays) if nb_of_surfs != 2: raise ValueError("'{0}' does not a contain a valid white " "mesh.".format(white_file)) vertices = image.darrays[0].data triangles = image.darrays[1].data else: _surf = TriSurface.load(white_file) vertices = _surf.vertices triangles = _surf.triangles if parcellation_as_annotation: annotations = fio.read_annot(parcellation_file) texture, _, labels = annotations else: image_labels = gio.read(parcellation_file) texture = numpy.round(image_labels.darrays[0].data).astype(int) wm_surf = TriSurface(vertices, triangles, labels=texture.copy()) # Four colors theorem to generate the cmap import networkx as nx import json # > define distinct colors colors_rgb = [(230, 25, 75), (60, 180, 75), (255, 225, 25), (0, 130, 200), (245, 130, 48), (145, 30, 180), (70, 240, 240), (240, 50, 230), (210, 245, 60), (250, 190, 190), (0, 128, 128), (230, 190, 255), (170, 110, 40), (255, 250, 200), (128, 0, 0), (170, 255, 195), (128, 128, 0), (255, 215, 180), (0, 0, 128), (128, 128, 128), (255, 255, 255)] # > create the graph nodes graph = nx.Graph() unique_labels = numpy.unique(texture) graph.add_nodes_from(unique_labels, color=None) # > get the cluster centroids & neighboor vertices clusters_map = {} for label in unique_labels: indices = numpy.where(wm_surf.labels == label)[0] cluster_triangles = wm_surf.triangles[list( numpy.where(numpy.isin(wm_surf.triangles, indices))[0])] cluster_indices = cluster_triangles[numpy.where( numpy.isin(cluster_triangles, indices, invert=True))] neighboors_indices = list( set(cluster_indices.astype(int)) - set(indices.astype(int))) clusters_map[label] = { "vertices": indices.tolist(), "neighboors": neighboors_indices } # > compute the graph edges edges = [] nb_labels = len(unique_labels) for ind1 in range(nb_labels): for ind2 in range(ind1 + 1, nb_labels): label = unique_labels[ind1] other_label = unique_labels[ind2] if numpy.isin(clusters_map[other_label]["vertices"], clusters_map[label]["neighboors"]).any(): edges.append([label, other_label]) graph.add_edges_from(edges) # > graph coloring colors = nx.algorithms.coloring.greedy_coloring.greedy_color(graph) ctab = [] for label, color_id in colors.items(): if label < 0: continue ctab.append( list(colors_rgb[color_id % len(colors_rgb)]) + [255., label]) ctab.append([0., 0., 0., 255., unique_labels.max() + 1]) ctab = numpy.asarray(ctab) # > create the actor wm_surf.labels = wm_surf.labels.astype(float) if pits_texture is not None: wm_surf.labels[numpy.where(pits_texture == 1)] = (unique_labels.max() + 1) wm_surf.labels[numpy.where(wm_surf.labels == -1)] = unique_labels.max() + 1 actor = pvtk.surface(wm_surf.vertices, wm_surf.triangles, wm_surf.labels, ctab=ctab, opacity=1, set_lut=True) actor.label = "white" actor.RotateX(actor_ang[0]) actor.RotateY(actor_ang[1]) actor.RotateZ(actor_ang[2]) pvtk.add(ren, actor) # Show the renderer if interactive: pvtk.add(ren, actor) pvtk.show(ren, title=name) # Create a snap if snap: if not os.path.isdir(outdir): raise ValueError("'{0}' is not a valid directory.".format(outdir)) pvtk.record(ren, outdir, name, n_frames=1) # Create an animation if animate: if not os.path.isdir(outdir): raise ValueError("'{0}' is not a valid directory.".format(outdir)) pvtk.record(ren, outdir, name, n_frames=36, az_ang=10, animate=True, delay=25)