def qc(t1files, wmfiles, asegfiles, whitefiles, pialfiles, annotfiles, actor_ang=[0., 0., 0.], output_directory=None, fsconfig="/i2bm/local/freesurfer/SetUpFreeSurfer.sh"): """ Compute some quality check plots on the converted FrreSurfer outputs. The subjecy id in the input files must appear in the -3 position: xxx/subject_id/convert/t1.nii.gz Steps: * t1-images overlays * 3d surface segmentation snaps * t1-surfaces overlays actor_ang: float (optional, default 0) the actor rotation in the z direction. <unit> <input name="t1files" type="List_File" description="The t1 subject files."/> <input name="wmfiles" type="List_File" description="The white matter subject files."/> <input name="asegfiles" type="List_File" description="The subcortical segmentation subject files."/> <input name="output_directory" type="Directory" description="The conversion destination folder."/> <input name="whitefiles" type="List_File" description="The subject cortex surfaces."/> <input name="pialfiles" type="List_File" description="The subject pial surfaces."/> <input name="annotfiles" type="List_File" description="The pial/white surface annotations."/> <input name="actor_ang" type="List_Float" description="The actor x, y, z position (in degrees)."/> <input name="fsconfig" type="File" description="The freesurfer configuration batch."/> <output name="qcfiles" type="List_File" description="The quality check snaps."/> </unit> """ import clindmri.plot.pvtk as pvtk from clindmri.plot.slicer import plot_image # Create a t1 subject map t1map = {} for fname in t1files: subject_id = fname.split("/")[-3] if subject_id in t1map: raise Exception("Can't map two t1 for subject '{0}'" ".".format(subject_id)) t1map[subject_id] = fname # Create the output list that will contain all the qc files qcfiles = [] # Construct the t1-surfaces overlays and the 3d surface segmentation snaps ren = pvtk.ren() for name, files in [("white", whitefiles), ("pial", pialfiles)]: for fname in files: # Get the t1 reference image subject_id = fname.split("/")[-3] t1file = t1map[subject_id] t1_image = nibabel.load(t1file) # Get the qc output directory qcdir = os.path.join(os.path.dirname(fname), "qc") qcname = os.path.basename(fname) if not os.path.isdir(qcdir): os.makedirs(qcdir) # Get the triangular mesh basename = os.path.basename(fname).replace( name, "aparc.annot").replace(".native", "") annotfile = os.path.join(os.path.dirname(fname), basename) if annotfile not in annotfiles: raise ValueError( "Annotation file '{0}' can't be found.".format(annotfile)) surface = TriSurface.load(fname, annotfile=annotfile) # Construct the surfaces binarized volume binarizedfile = os.path.join(qcdir, qcname + ".nii.gz") overlay = numpy.zeros(t1_image.shape, dtype=numpy.uint) indices = numpy.round(surface.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) snap_file = os.path.join(qcdir, qcname + ".png") plot_image(t1file, overlay_file=binarizedfile, snap_file=snap_file, name=qcname, overlay_cmap="cold_hot") qcfiles.append(snap_file) # Create a vtk surface actor of the cortex surface with the # associated labels ctab = [item["color"] for _, item in surface.metadata.items()] actor = pvtk.surface( surface.vertices, surface.triangles, surface.labels, ctab) actor.RotateX(actor_ang[0]) actor.RotateY(actor_ang[1]) actor.RotateZ(actor_ang[2]) # Create a 3d surface segmentation snap pvtk.add(ren, actor) snaps = pvtk.record(ren, qcdir, qcname, n_frames=36, az_ang=10, animate=True, delay=50) qcfiles.append(snaps[0]) snaps = pvtk.record(ren, qcdir, qcname + ".3d", n_frames=1) qcfiles.append(snaps[0]) pvtk.clear(ren) # Get the FreeSurfer lookup table fs_lut_names, fs_lut_colors = parse_fs_lut(os.path.join( os.path.dirname(fsconfig), "FreeSurferColorLUT.txt")) cmap = [] nb_values = numpy.asarray(fs_lut_colors.keys()).max() cmap = numpy.zeros((nb_values, 4), dtype=numpy.single) for key, color in fs_lut_colors.items(): if key > 0: cmap[key - 1, :3] = color cmap[:, 3] = 200. cmap /= 255. # Compute t1-images overlays for name, files in [("aseg", asegfiles), ("wm", wmfiles)]: for fname in files: # Get the t1 reference image subject_id = fname.split("/")[-3] t1file = t1map[subject_id] t1_image = nibabel.load(t1file) # Get the qc output directory qcdir = os.path.join(os.path.dirname(fname), "qc") if not os.path.isdir(qcdir): os.makedirs(qcdir) # Troncate the color map based on the label max array = nibabel.load(fname).get_data() order = sorted(set(array.flatten())) ccmap = cmap[order[1]: order[-1] + 1] # Overlay the current image with the t1 image qcname = "t1-{0}".format(name) snap_file = os.path.join(qcdir, qcname + ".png") plot_image(t1file, overlay_file=fname, snap_file=snap_file, name=qcname, overlay_cmap=ccmap, cut_coords=(0, 0, 0)) qcfiles.append(snap_file) return qcfiles
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. """ import clindmri.plot.pvtk as pvtk from clindmri.plot.slicer import animate_image # 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 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
mesharray = numpy.zeros(t1shape, dtype=numpy.uint) for basename in surf: # > construc path fpath = os.path.join(mripath, basename) if not os.path.isfile(fpath): raise ValueError("'{0}' is not a valid file name.".format(fpath)) surf[basename] = fpath # > load mesh name = basename.split(".")[1] annot_basename = basename.replace( name, "aparc.annot").replace(".native", "") annotfile = os.path.join(mripath, annot_basename) if not os.path.isfile(fpath): raise ValueError("'{0}' is not a valid file name.".format(fpath)) surface = TriSurface.load(fpath, annotfile=annotfile) # > binarize mesh indices = numpy.round(surface.vertices).astype(int).T indices[0, numpy.where(indices[0] >= t1shape[0])] = 0 indices[1, numpy.where(indices[1] >= t1shape[1])] = 0 indices[2, numpy.where(indices[2] >= t1shape[2])] = 0 mesharray[indices.tolist()] = colors[basename] # Save the mesh volume meshim = nibabel.Nifti1Image(mesharray, t1affine) nibabel.save(meshim, meshfile) # Need to reorient the image to MNI standard space reomeshfile = os.path.join(qcdir, "reo." + os.path.basename(meshfile)) fslreorient2std(meshfile, reomeshfile, shfile=fslconfig)
def qc(t1files, wmfiles, asegfiles, whitefiles, pialfiles, annotfiles, actor_ang=[0., 0., 0.], output_directory=None, fsconfig="/i2bm/local/freesurfer/SetUpFreeSurfer.sh"): """ Compute some quality check plots on the converted FrreSurfer outputs. The subjecy id in the input files must appear in the -3 position: xxx/subject_id/convert/t1.nii.gz Steps: * t1-images overlays * 3d surface segmentation snaps * t1-surfaces overlays actor_ang: float (optional, default 0) the actor rotation in the z direction. <unit> <input name="t1files" type="List_File" description="The t1 subject files."/> <input name="wmfiles" type="List_File" description="The white matter subject files."/> <input name="asegfiles" type="List_File" description="The subcortical segmentation subject files."/> <input name="output_directory" type="Directory" description="The conversion destination folder."/> <input name="whitefiles" type="List_File" description="The subject cortex surfaces."/> <input name="pialfiles" type="List_File" description="The subject pial surfaces."/> <input name="annotfiles" type="List_File" description="The pial/white surface annotations."/> <input name="actor_ang" type="List_Float" description="The actor x, y, z position (in degrees)."/> <input name="fsconfig" type="File" description="The freesurfer configuration batch."/> <output name="qcfiles" type="List_File" description="The quality check snaps."/> </unit> """ import clindmri.plot.pvtk as pvtk from clindmri.plot.slicer import plot_image # Create a t1 subject map t1map = {} for fname in t1files: subject_id = fname.split("/")[-3] if subject_id in t1map: raise Exception("Can't map two t1 for subject '{0}'" ".".format(subject_id)) t1map[subject_id] = fname # Create the output list that will contain all the qc files qcfiles = [] # Construct the t1-surfaces overlays and the 3d surface segmentation snaps ren = pvtk.ren() for name, files in [("white", whitefiles), ("pial", pialfiles)]: for fname in files: # Get the t1 reference image subject_id = fname.split("/")[-3] t1file = t1map[subject_id] t1_image = nibabel.load(t1file) # Get the qc output directory qcdir = os.path.join(os.path.dirname(fname), "qc") qcname = os.path.basename(fname) if not os.path.isdir(qcdir): os.makedirs(qcdir) # Get the triangular mesh basename = os.path.basename(fname).replace(name, "aparc.annot").replace( ".native", "") annotfile = os.path.join(os.path.dirname(fname), basename) if annotfile not in annotfiles: raise ValueError( "Annotation file '{0}' can't be found.".format(annotfile)) surface = TriSurface.load(fname, annotfile=annotfile) # Construct the surfaces binarized volume binarizedfile = os.path.join(qcdir, qcname + ".nii.gz") overlay = numpy.zeros(t1_image.shape, dtype=numpy.uint) indices = numpy.round(surface.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) snap_file = os.path.join(qcdir, qcname + ".png") plot_image(t1file, overlay_file=binarizedfile, snap_file=snap_file, name=qcname, overlay_cmap="cold_hot") qcfiles.append(snap_file) # Create a vtk surface actor of the cortex surface with the # associated labels ctab = [item["color"] for _, item in surface.metadata.items()] actor = pvtk.surface(surface.vertices, surface.triangles, surface.labels, ctab) actor.RotateX(actor_ang[0]) actor.RotateY(actor_ang[1]) actor.RotateZ(actor_ang[2]) # Create a 3d surface segmentation snap pvtk.add(ren, actor) snaps = pvtk.record(ren, qcdir, qcname, n_frames=36, az_ang=10, animate=True, delay=50) qcfiles.append(snaps[0]) snaps = pvtk.record(ren, qcdir, qcname + ".3d", n_frames=1) qcfiles.append(snaps[0]) pvtk.clear(ren) # Get the FreeSurfer lookup table fs_lut_names, fs_lut_colors = parse_fs_lut( os.path.join(os.path.dirname(fsconfig), "FreeSurferColorLUT.txt")) cmap = [] nb_values = numpy.asarray(fs_lut_colors.keys()).max() cmap = numpy.zeros((nb_values, 4), dtype=numpy.single) for key, color in fs_lut_colors.items(): if key > 0: cmap[key - 1, :3] = color cmap[:, 3] = 200. cmap /= 255. # Compute t1-images overlays for name, files in [("aseg", asegfiles), ("wm", wmfiles)]: for fname in files: # Get the t1 reference image subject_id = fname.split("/")[-3] t1file = t1map[subject_id] t1_image = nibabel.load(t1file) # Get the qc output directory qcdir = os.path.join(os.path.dirname(fname), "qc") if not os.path.isdir(qcdir): os.makedirs(qcdir) # Troncate the color map based on the label max array = nibabel.load(fname).get_data() order = sorted(set(array.flatten())) ccmap = cmap[order[1]:order[-1] + 1] # Overlay the current image with the t1 image qcname = "t1-{0}".format(name) snap_file = os.path.join(qcdir, qcname + ".png") plot_image(t1file, overlay_file=fname, snap_file=snap_file, name=qcname, overlay_cmap=ccmap, cut_coords=(0, 0, 0)) qcfiles.append(snap_file) return qcfiles
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. """ import clindmri.plot.pvtk as pvtk from clindmri.plot.slicer import animate_image # 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 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
if trf_file is None: trf_file = os.path.join(connectdir, "dmri_to_t1.trf") reg_file = os.path.join(connectdir, "nodif_to_t1.nii.gz") flirt(nodif_file, t1_file, omat=trf_file, out=reg_file, usesqform=False, cost="normmi", dof=6) """ Launch the tractography on the requested point of the cortical surface on the selected hemisphere """ # Load the white mesh in the diffusion space surface = TriSurface.load(whitefile) voxel_diff_to_t1 = flirt2aff(trf_file, nodif_file, t1_file) voxel_t1_to_diff = numpy.linalg.inv(voxel_diff_to_t1) white_diff_vertices = apply_affine_on_mesh(surface.vertices, voxel_t1_to_diff) # Select the vertices of interest if vertices_indices is None: vertices_indices = range(len(surface.vertices)) # Go through all the hemisphere vertices textures = {} for index in vertices_indices: # Select the seeding vertex point = white_diff_vertices[index]
mesharray = numpy.zeros(t1shape, dtype=numpy.uint) for basename in surf: # > construc path fpath = os.path.join(mripath, basename) if not os.path.isfile(fpath): raise ValueError("'{0}' is not a valid file name.".format(fpath)) surf[basename] = fpath # > load mesh name = basename.split(".")[1] annot_basename = basename.replace(name, "aparc.annot").replace(".native", "") annotfile = os.path.join(mripath, annot_basename) if not os.path.isfile(fpath): raise ValueError("'{0}' is not a valid file name.".format(fpath)) surface = TriSurface.load(fpath, annotfile=annotfile) # > binarize mesh indices = numpy.round(surface.vertices).astype(int).T indices[0, numpy.where(indices[0] >= t1shape[0])] = 0 indices[1, numpy.where(indices[1] >= t1shape[1])] = 0 indices[2, numpy.where(indices[2] >= t1shape[2])] = 0 mesharray[indices.tolist()] = colors[basename] # Save the mesh volume meshim = nibabel.Nifti1Image(mesharray, t1affine) nibabel.save(meshim, meshfile) # Need to reorient the image to MNI standard space reomeshfile = os.path.join(qcdir, "reo." + os.path.basename(meshfile)) fslreorient2std(meshfile, reomeshfile, shfile=fslconfig)
If no '.trf' file is provided, register the nodif image on the t1 image to get it. """ if trf_file is None: trf_file = os.path.join(connectdir, "dmri_to_t1.trf") reg_file = os.path.join(connectdir, "nodif_to_t1.nii.gz") flirt(nodif_file, t1_file, omat=trf_file, out=reg_file, usesqform=False, cost="normmi", dof=6) """ Launch the tractography on the requested point of the cortical surface on the selected hemisphere """ # Load the white mesh in the diffusion space surface = TriSurface.load(whitefile) voxel_diff_to_t1 = flirt2aff(trf_file, nodif_file, t1_file) voxel_t1_to_diff = numpy.linalg.inv(voxel_diff_to_t1) white_diff_vertices = apply_affine_on_mesh(surface.vertices, voxel_t1_to_diff) # Select the vertices of interest if vertices_indices is None: vertices_indices = range(len(surface.vertices)) # Go through all the hemisphere vertices textures = {} for index in vertices_indices: # Select the seeding vertex point = white_diff_vertices[index]