コード例 #1
0
ファイル: surface.py プロジェクト: sipv/tvb-recon
class SurfaceService(object):
    logger = get_logger(__name__)

    def __init__(self):
        self.annotation_service = AnnotationService()

    # #TODO: transfer the following two to some general services?:
    # def mask2index(self,mask):
    #     """
    #     Convert a mask to an array of indices
    #     :param mask:
    #     :return:
    #     """
    #     return numpy.where(mask)
    #
    # def index2mask(self,index):
    #     """
    #     Convert an array of indices to the corresponding mask
    #     :param index:
    #     :return:
    #     """
    #     index=numpy.unique(index)
    #     mask=numpy.zeros((len(index,))).astype('bool')
    #     mask[index]=True
    #     return mask

    def tri_area(self, tri):
        i, j, k = numpy.transpose(tri, (1, 0, 2))
        ij = j - i
        ik = k - i
        return numpy.sqrt(numpy.sum(numpy.cross(ij, ik)**2, axis=1)) / 2.0

    def convert_fs_to_brain_visa(self, in_surf_path):
        surface = IOUtils.read_surface(in_surf_path, False)
        IOUtils.write_surface(in_surf_path + '.tri', surface)

    def convert_bem_to_tri(self, surfaces_directory_path):
        surfs_glob = '%s/*_surface-low' % (surfaces_directory_path)
        for surf_name in glob.glob(surfs_glob):
            self.convert_fs_to_brain_visa(surf_name)

    def merge_surfaces(self, surfaces):
        """
        Merge several surfaces, and their region mappings.
        :return: the merge result surface and region mapping.
        """
        n_surfaces = len(surfaces)
        out_surface = Surface([], [])
        # TODO: how to deal with the metadata of merged surfaces, so that freesurfer.io can handle them, e.g., write them
        # i.e., we need to have a final unique version of the metadata, not a list of them, as I am doing here in the
        # commented code
        # out_surface_attributes=dict()
        # for attribute in ["vertices_coord_system", "generic_metadata", "vertices_metadata", "triangles_metadata"]:
        #     out_surface_attributes[attribute]=[]
        for i_srf in range(n_surfaces):
            out_surface.add_vertices_and_triangles(surfaces[i_srf].vertices,
                                                   surfaces[i_srf].triangles,
                                                   surfaces[i_srf].area_mask)
            if len(surfaces[i_srf].center_ras) == 0:
                pass
            elif len(out_surface.center_ras) == 0:
                out_surface.center_ras = surfaces[i_srf].center_ras
            elif numpy.any(
                    out_surface.center_ras != surfaces[i_srf].center_ras):
                raise ValueError(
                    "At least two surfaces have different -non empty- centers in RAS coordinates!"
                )
            # #TODO: think about how to better merge these fields
            # for attribute in ["vertices_coord_system", "generic_metadata", "vertices_metadata", "triangles_metadata"]:
            #     out_surface_attributes[attribute].append(getattr(surfaces[i_srf],attribute))
            #     setattr(out_surface,attribute,out_surface_attributes[attribute])
        return out_surface

    def compute_gdist_mat(self, surf_name='pial', max_distance=40.0):
        max_distance = float(max_distance)  # in case passed from sys.argv
        for h in 'rl':
            subjects_dir = os.environ['SUBJECTS_DIR']
            subject = os.environ['SUBJECT']
            surf_path = '%s/%s/surf/%sh.%s' % (subjects_dir, subject, h,
                                               surf_name)
            surface = IOUtils.read_surface(surf_path, False)
            mat_path = '%s/%s/surf/%sh.%s.gdist.mat' % (subjects_dir, subject,
                                                        h, surf_name)
            mat = gdist.local_gdist_matrix(surface.vertices,
                                           surface.triangles.astype('<i4'),
                                           max_distance=max_distance)
            scipy.io.savemat(mat_path, {'gdist': mat})

    # TODO: maybe create a new "connectome" service and transfer this function there
    # TODO: add more normalizations modes
    def compute_geodesic_dist_affinity(self, dist, norm=None):
        """
        This function calculates geodesic distances among nodes of a mesh,
        starting from the array of the distances between directly connected nodes.
        Optionally, normalization with the maximum geodesic distances is performed.
        Infinite distances (corresponding to disconnected components of the mesh, are not allowed)
        :param dist: a dense array of distances between directly connected nodes of a mesh/network
        :param norm: a flag to be currently used for optional normalization with the maximum geodesic distance

        :return:
        """
        # TODO: make sure that this returns a symmetric matrix!
        geodist = shortest_path(dist,
                                method='auto',
                                directed=False,
                                return_predecessors=False,
                                unweighted=False,
                                overwrite=False).astype('single')
        # Find the maximum non infinite geodesic distance:
        max_gdist = numpy.max(geodist, axis=None)
        assert numpy.isfinite(max_gdist)
        if norm is not None:
            geodist /= max_gdist
        # Convert them to normalized distances and return them
        return geodist

    def extract_subsurf(self, surface, verts_mask, output='surface'):
        """
        Extracts a sub-surface that contains only the masked vertices and the corresponding faces.
        An important step is to replace old vertices indexes of faces to the new ones.
        :param: surface: input surface object
        :param: verts_mask: mask of the sub-surface to be extracted
        :return: output surface object
        """

        verts_out = surface.vertices[verts_mask]
        triangles_mask = numpy.c_[verts_mask[surface.triangles[:, 0]],
                                  verts_mask[surface.triangles[:, 1]],
                                  verts_mask[surface.triangles[:,
                                                               2]]].all(axis=1)
        triangles_out = numpy.array(surface.triangles[triangles_mask, :])
        verts_out_inds, = numpy.where(verts_mask)
        for triang_idx in range(triangles_out.shape[0]):
            for vertex_idx in range(3):
                triangles_out[triang_idx, vertex_idx], = \
                    numpy.where(
                        triangles_out[triang_idx, vertex_idx] == verts_out_inds)
        if output == 'surface':
            out_surface = Surface(
                verts_out,
                triangles_out,
                area_mask=surface.area_mask[verts_out_inds],
                center_ras=surface.center_ras,
                vertices_coord_system=surface.vertices_coord_system,
                generic_metadata=surface.generic_metadata,
                vertices_metadata=surface.vertices_metadata,
                triangles_metadata=surface.triangles_metadata)
            return out_surface
        else:
            return (verts_out, triangles_out,
                    surface.area_mask[verts_out_inds])

    def compute_surface_area(self, surface, area_mask=None):
        """
            This function computes the surface area, after optionally applying a mask to choose a sub-surface
            :param: surface: input surface object
            :param area_mask: optional boolean mask (number of vertices x ) to overwrite the surface.area_mask
            :return: (sub)surface area, float
            """
        if area_mask is None:
            area_mask = surface.area_mask
        # Apply the mask in order to extract the sub-surface (vertices and
        # relevant triangles)
        (vertices,
         triangles) = self.extract_subsurf(surface,
                                           area_mask,
                                           output='verts_triangls')[:2]
        return numpy.sum(self.tri_area(vertices[triangles]))

    def vertex_connectivity(self,
                            surface,
                            mode="sparse",
                            metric=None,
                            symmetric=False,
                            verts_mask=None):
        """
        It computes a sparse matrix of the connectivity among the vertices of a surface.
        :param surface: input surface object
        :param mode: "sparse" by default or "2D"
        :param metric: None by default, could be "euclidean"
        :param symmetric: True for symmetric matrix output
        :param verts_mask: a mask to apply the method to a a sub-surface of the original surface
        :return: the computed matrix.
        """
        if verts_mask is not None:
            (vertices,
             triangles) = self.extract_subsurf(surface,
                                               verts_mask,
                                               output='verts_triangls')[:2]
        else:
            vertices = surface.vertices
            triangles = surface.triangles
        # Get all pairs of vertex indexes (i.e., edges) that appear in each
        # face (triangle)
        edges = numpy.r_[triangles[:, [0, 1]], triangles[:, [1, 2]],
                         triangles[:, [2, 0]]]
        # Remove repetitions
        edges = numpy.vstack(set(map(tuple, edges)))
        # Mark all existing pairs to 1
        n_v = vertices.shape[0]
        n_e = edges.shape[0]
        # For symmetric output...
        if symmetric:
            #...create for the moment the "double" edges
            edges2 = numpy.r_[edges, edges[:, [1, 0]]]
        if metric is None:
            # For symmetric output...
            if symmetric:
                # ...remove repetitions of edges2
                edges = numpy.vstack(set(map(tuple, edges2)))
                n_e = edges.shape[0]
            con = csr_matrix((numpy.ones((n_e, )), (edges[:, 0], edges[:, 1])),
                             shape=(n_v, n_v))
            if mode != "sparse":
                # Create non-sparse matrix
                con = con.todense()
        else:
            d = paired_distances(vertices[edges[:, 0]], vertices[edges[:, 1]],
                                 metric)
            # For symmetric output...
            if symmetric:
                # double also d...
                d = numpy.r_[d, d]
                edges = edges2
            if mode == "sparse":
                # Create sparse matrix
                con = csr_matrix((d, (edges[:, 0], edges[:, 1])),
                                 shape=(n_v, n_v))
        return con

    # TODO: use surface instead of verts and faces?? Denis: not sure about
    # this!..
    def connected_surface_components(self,
                                     surface=None,
                                     connectivity=None,
                                     verts_mask=None):
        """
        This function returns all the different disconnected components of a surface, their number and their areas,
        after applying an optional boolean mask to exclude some subsurface from the whole computation.
        There should be at least one component returned, if the whole surface is connected.
        :param surface: input surface object
        :param connectivity: optionally an array or sparse matrix of structural connectivity constraints,
                            where True or 1 or entry>0 stands for the existing direct connections
                            among neighboring vertices (i.e., vertices of a common triangular face)
        :param verts_mask: optional boolean mask (number of vertices x ) for vertices to include to the input surface
        :return:
        """
        if (surface is None) and connectivity is None:
            print(
                "Error: neither a surface, nor a connectivity matrix in the input!"
            )
            return 0
        elif connectivity is None:
            n_verts = surface.vertices.shape[0]
            # Create the connectivity matrix, if not in the input:
            connectivity = self.vertex_connectivity(surface,
                                                    verts_mask=verts_mask)
            if verts_mask is None:
                verts_mask = numpy.ones((n_verts, ), dtype=bool)
        else:
            n_verts = connectivity.shape[0]
            if verts_mask is None:
                verts_mask = numpy.ones((n_verts, ), dtype=bool)
            else:
                connectivity = connectivity[verts_mask, :][:, verts_mask]
        # Find all connected components of this surface
        (n_components, components_masked) = \
            connected_components(connectivity, directed=False,
                                 connection='weak', return_labels=True)
        comp_area = []
        if surface is not None:
            # For each component...
            for ic in range(n_components):
                i_comp_verts = components_masked == ic
                # ...compute the surface area, after applying any specified mask
                comp_area.append(
                    self.compute_surface_area(surface,
                                              mask=numpy.logical_and(
                                                  i_comp_verts,
                                                  surface.area_mask)))
        # Prepare final components' labels output:
        components = -numpy.ones((n_verts, )).astype('i')
        components[verts_mask] = components_masked
        return n_components, components, comp_area

    def aseg_surf_conc_annot(self,
                             surf_path,
                             out_surf_path,
                             annot_path,
                             label_indices,
                             lut_path=None):
        """
        Concatenate surfaces of one specific label of interest each, to create a single annotated surface.
        """

        lut_path = lut_path or default_lut_path()

        label_names, color_table = self.annotation_service.lut_to_annot_names_ctab(
            lut_path=lut_path, labels=label_indices)
        label_indices = numpy.array(label_indices.split()).astype('i')

        #                  verts tri area_mask cras
        surfaces = []
        out_annotation = Annotation([], [], [])
        label_number = -1

        for label_index in label_indices:
            this_surf_path = surf_path + "-%06d" % int(label_index)

            if os.path.exists(this_surf_path):
                ind_l, = numpy.where(label_indices == label_index)
                out_annotation.add_region_names_and_colors(
                    numpy.array(label_names)[ind_l], color_table[ind_l, :])
                label_number += 1
                surfaces.append(IOUtils.read_surface(this_surf_path, False))
                out_annotation.add_region_mapping(label_number * numpy.ones(
                    (surfaces[-1].n_vertices, ), dtype='int64'))
        out_surface = self.merge_surfaces(surfaces)
        #out_annotation.regions_color_table = numpy.squeeze(numpy.array(out_annotation.regions_color_table).astype('i'))

        IOUtils.write_surface(out_surf_path, out_surface)
        IOUtils.write_annotation(annot_path, out_annotation)

    def __prepare_grid(self, vertex_neighbourhood):
        # Prepare grid if needed for possible use:
        if vertex_neighbourhood > 0:
            grid = numpy.meshgrid(
                list(range(-vertex_neighbourhood, vertex_neighbourhood + 1,
                           1)),
                list(range(-vertex_neighbourhood, vertex_neighbourhood + 1,
                           1)),
                list(range(-vertex_neighbourhood, vertex_neighbourhood + 1,
                           1)),
                indexing='ij')
            grid = numpy.c_[numpy.array(grid[0]).flatten(),
                            numpy.array(grid[1]).flatten(),
                            numpy.array(grid[2]).flatten()]
            n_grid = grid.shape[0]

            return grid, n_grid

    def sample_vol_on_surf(self,
                           surf_path,
                           vol_path,
                           annot_path,
                           out_surf_path,
                           cras_path,
                           add_string='',
                           vertex_neighbourhood=1,
                           add_lbl=[],
                           lut_path=None):
        """
        Sample a volume of a specific label on a surface, by keeping only those surface vertices, the nearest voxel of
        which is of the given label (+ of possibly additional target labels, such as white matter).
        Allow optionally for vertices within a given voxel distance vn from the target voxels.
        """

        lut_path = lut_path or default_lut_path()

        # Read the inputs
        surface = IOUtils.read_surface(surf_path, False)

        annotation = IOUtils.read_annotation(annot_path)
        labels = self.annotation_service.annot_names_to_labels(
            annotation.region_names, add_string=add_string, lut_path=lut_path)
        region_mapping_indexes = numpy.unique(annotation.region_mapping)

        volume_parser = VolumeIO()
        volume = volume_parser.read(vol_path)
        ras2vox_affine_matrix = numpy.linalg.inv(volume.affine_matrix)

        cras = numpy.loadtxt(cras_path)

        grid, n_grid = self.__prepare_grid(vertex_neighbourhood)

        # Initialize the output mask:
        verts_out_mask = numpy.repeat([False], surface.vertices.shape[0])
        for label_index in range(len(region_mapping_indexes)):

            self.logger.info("%s",
                             add_string + annotation.region_names[label_index])

            # Get the indexes of the vertices corresponding to this label:
            verts_indices_of_label, = numpy.where(
                annotation.region_mapping[:] ==
                region_mapping_indexes[label_index])
            verts_indices_of_label_size = verts_indices_of_label.size
            if verts_indices_of_label_size == 0:
                continue

            # Add any additional labels
            all_labels = [labels[label_index]] + add_lbl

            # get the vertices for current label and add cras to take them to
            # scanner ras
            verts_of_label = surface.vertices[verts_indices_of_label, :]
            verts_of_label += numpy.repeat(numpy.expand_dims(cras, 1).T,
                                           verts_indices_of_label_size,
                                           axis=0)

            # Compute the nearest voxel coordinates using the affine transform
            ijk = numpy.round(
                ras2vox_affine_matrix.dot(numpy.c_[verts_of_label, numpy.ones(verts_indices_of_label_size)].T)[:3].T) \
                .astype('i')

            # Get the labels of these voxels:
            surf_vxls = volume.data[ijk[:, 0], ijk[:, 1], ijk[:, 2]]

            # Vertex mask to keep: those that correspond to voxels of one of
            # the target labels
            # surf_vxls==lbl if only one target label
            verts_keep, = numpy.where(numpy.in1d(surf_vxls, all_labels))
            verts_out_mask[verts_indices_of_label[verts_keep]] = True

            if vertex_neighbourhood > 0:
                # These are now the remaining indexes to be checked for
                # neighboring voxels
                verts_indices_of_label = numpy.delete(verts_indices_of_label,
                                                      verts_keep)
                ijk = numpy.delete(ijk, verts_keep, axis=0)

                for vertex_index in range(verts_indices_of_label.size):
                    # Generate the specific grid centered at the voxel ijk
                    ijk_grid = grid + \
                        numpy.tile(ijk[vertex_index, :], (n_grid, 1))

                    # Remove voxels outside the volume
                    indexes_within_limits = numpy.all(
                        [(ijk_grid[:, 0] >= 0),
                         (ijk_grid[:, 0] < volume.dimensions[0]),
                         (ijk_grid[:, 1] >= 0),
                         (ijk_grid[:, 1] < volume.dimensions[1]),
                         (ijk_grid[:, 2] >= 0),
                         (ijk_grid[:, 2] < volume.dimensions[2])],
                        axis=0)
                    ijk_grid = ijk_grid[indexes_within_limits, :]
                    surf_vxls = volume.data[ijk_grid[:, 0], ijk_grid[:, 1],
                                            ijk_grid[:, 2]]

                    # If any of the neighbors is of the target labels include
                    # the current vertex
                    # surf_vxls==lbl if only one target label
                    if numpy.any(numpy.in1d(surf_vxls, all_labels)):
                        verts_out_mask[
                            verts_indices_of_label[vertex_index]] = True

        # Vertex indexes and vertices to keep:
        verts_out_indices, = numpy.where(verts_out_mask)
        verts_out = surface.vertices[verts_out_indices]

        # TODO maybe: make sure that all voxels of this label correspond to at least one vertex.
        # Create a similar mask for faces by picking only triangles of which
        # all 3 vertices are included
        face_out_mask = numpy.c_[verts_out_mask[surface.triangles[:, 0]],
                                 verts_out_mask[surface.triangles[:, 1]],
                                 verts_out_mask[surface.triangles[:, 2]]].all(
                                     axis=1)
        faces_out = surface.triangles[face_out_mask]

        # The old vertices' indexes of faces have to be transformed to the new
        # vrtx_out_inds:
        for iF in range(faces_out.shape[0]):
            for vertex_index in range(3):
                faces_out[iF, vertex_index], = numpy.where(
                    faces_out[iF, vertex_index] == verts_out_indices)

        surface.vertices = verts_out
        surface.triangles = faces_out

        # Write the output surfaces and annotations to files. Also write files
        # with the indexes of vertices to keep.
        IOUtils.write_surface(out_surf_path, surface)

        annotation.set_region_mapping(
            annotation.get_region_mapping_by_indices([verts_out_indices]))
        IOUtils.write_annotation(out_surf_path + ".annot", annotation)

        numpy.save(out_surf_path + "-idx.npy", verts_out_indices)
        numpy.savetxt(out_surf_path + "-idx.txt", verts_out_indices, fmt='%d')

    # TODO: maybe create a new "connectome" service and transfer this function
    # there
    def compute_consim_affinity(self, verts, vox, voxxzy, con, cras=None):
        """
        This function creates a connectome affinity matrix among vertices,
        starting from an affinity matrix among voxels,
        by assignment from the nearest neighboring voxel to the respective vertex.
        :param verts: vertices' coordinates array (number of vertices x 3)
        :param vox: labels of connectome nodes-voxels (integers>=1)
        :param voxxzy: coordinates of the connectome nodes-voxels in ras space
        :param con: connectivity affinity matrix
        :param cras: center ras point to be optionally added to the vertices coordinates
                    (being probably in freesurfer tk-ras or surface ras coordinates) to align with the volume voxels
        :return: the affinity matrix among vertices
        """
        # Add the cras to take them to scanner ras coordinates, if necessary:
        if cras is not None:
            verts += numpy.repeat(numpy.expand_dims(cras, 1).T,
                                  verts.shape[0],
                                  axis=0)
        # TODO?: to use aparc+aseg to correspond vertices only to voxels of the same label
        # There would have to be a vertex->voxel of aparc+aseg of the same label -> voxel of tdi_lbl_in_T1 mapping
        # Maybe redundant  because we might be ending to the same voxel of tdi_lbl anyway...
        # Something to test/discuss...
        # Find for each vertex the closest voxel node in terms of euclidean
        # distance:
        v2n = numpy.argmin(cdist(verts, voxxzy, 'euclidean'), axis=1)
        # Assign to each vertex the integer identity of the nearest voxel node.
        v2n = vox[v2n]
        print("...surface component's vertices correspond to " +
              str(numpy.size(numpy.unique(v2n))) + " distinct voxel nodes")
        affinity = con[v2n - 1, :][:, v2n - 1]
        return affinity
コード例 #2
0
ファイル: surface.py プロジェクト: sipv/tvb-recon
 def __init__(self):
     self.annotation_service = AnnotationService()
コード例 #3
0
def compute_region_details(atlas_suffix: AtlasSuffix, fs_color_lut: os.PathLike, t1: os.PathLike, lh_cort: os.PathLike,
                           rh_cort: os.PathLike, lh_cort_annot: os.PathLike, rh_cort_annot: os.PathLike,
                           lh_subcort: os.PathLike, rh_subcort: os.PathLike, lh_subcort_annot: os.PathLike,
                           rh_subcort_annot: os.PathLike):
    annot_cort_lh = IOUtils.read_annotation(lh_cort_annot)
    annot_cort_rh = IOUtils.read_annotation(rh_cort_annot)

    annot_subcort_lh = IOUtils.read_annotation(lh_subcort_annot)
    annot_subcort_rh = IOUtils.read_annotation(rh_subcort_annot)

    mapping = MappingService(atlas_suffix, annot_cort_lh, annot_cort_rh, annot_subcort_lh, annot_subcort_rh)
    mapping.generate_region_mapping_for_cort_annot(annot_cort_lh, annot_cort_rh)
    mapping.generate_region_mapping_for_subcort_annot(annot_subcort_lh, annot_subcort_rh)

    surface_service = SurfaceService()

    surf_cort_lh = IOUtils.read_surface(lh_cort, False)
    surf_cort_rh = IOUtils.read_surface(rh_cort, False)

    full_cort_surface = surface_service.merge_surfaces([surf_cort_lh, surf_cort_rh])

    surf_subcort_lh = IOUtils.read_surface(lh_subcort, False)
    surf_subcort_rh = IOUtils.read_surface(rh_subcort, False)

    full_subcort_surface = surface_service.merge_surfaces([surf_subcort_lh, surf_subcort_rh])

    genericIO.write_list_to_txt_file(mapping.cort_region_mapping, AsegFiles.RM_CORT_TXT.value.replace("%s", atlas_suffix))
    genericIO.write_list_to_txt_file(mapping.subcort_region_mapping,
                                     AsegFiles.RM_SUBCORT_TXT.value.replace("%s", atlas_suffix))

    vox2ras_file = "vox2ras.txt"
    subprocess.call(["mri_info", "--vox2ras", t1, "--o", vox2ras_file])

    surf_subcort_filename = "surface_subcort.zip"
    IOUtils.write_surface(surf_subcort_filename, full_subcort_surface)

    surf_cort_filename = "surface_cort.zip"
    IOUtils.write_surface(surf_cort_filename, full_cort_surface)

    os.remove(vox2ras_file)

    cort_subcort_full_surf = surface_service.merge_surfaces([full_cort_surface, full_subcort_surface])
    cort_subcort_full_region_mapping = mapping.cort_region_mapping + mapping.subcort_region_mapping

    dict_fs_custom = mapping.get_mapping_for_connectome_generation()
    genericIO.write_dict_to_txt_file(dict_fs_custom, AsegFiles.FS_CUSTOM_TXT.value.replace("%s", atlas_suffix))

    region_areas = surface_service.compute_areas_for_regions(mapping.get_all_regions(), cort_subcort_full_surf,
                                                             cort_subcort_full_region_mapping)
    genericIO.write_list_to_txt_file(region_areas, AsegFiles.AREAS_TXT.value.replace("%s", atlas_suffix))

    region_centers = surface_service.compute_centers_for_regions(mapping.get_all_regions(), cort_subcort_full_surf,
                                                                 cort_subcort_full_region_mapping)
    cort_subcort_lut = mapping.get_entire_lut()
    region_names = list(cort_subcort_lut.values())

    with open(AsegFiles.CENTERS_TXT.value.replace("%s", atlas_suffix), "w") as f:
        for idx, (val_x, val_y, val_z) in enumerate(region_centers):
            f.write("%s %.2f %.2f %.2f\n" % (region_names[idx], val_x, val_y, val_z))

    region_orientations = surface_service.compute_orientations_for_regions(mapping.get_all_regions(),
                                                                           cort_subcort_full_surf,
                                                                           cort_subcort_full_region_mapping)

    lh_region_centers = surface_service.compute_centers_for_regions(mapping.get_lh_regions(), surf_cort_lh,
                                                                    mapping.lh_region_mapping)
    lh_region_orientations = surface_service.compute_orientations_for_regions(mapping.get_lh_regions(), surf_cort_lh,
                                                                              mapping.lh_region_mapping)
    with open(AsegFiles.LH_DIPOLES_TXT.value.replace("%s", atlas_suffix), "w") as f:
        for idx, (val_x, val_y, val_z) in enumerate(lh_region_centers):
            f.write("%.2f %.2f %.2f %.2f %.2f %.2f\n" % (
                val_x, val_y, val_z, lh_region_orientations[idx][0], lh_region_orientations[idx][1],
                lh_region_orientations[idx][2]))

    rh_region_centers = surface_service.compute_centers_for_regions(mapping.get_rh_regions(), surf_cort_rh,
                                                                    mapping.rh_region_mapping)
    rh_region_orientations = surface_service.compute_orientations_for_regions(mapping.get_rh_regions(), surf_cort_rh,
                                                                              mapping.rh_region_mapping)
    with open(AsegFiles.RH_DIPOLES_TXT.value.replace("%s", atlas_suffix), "w") as f:
        for idx, (val_x, val_y, val_z) in enumerate(rh_region_centers):
            f.write("%.2f %.2f %.2f %.2f %.2f %.2f\n" % (
                val_x, val_y, val_z, rh_region_orientations[idx][0], rh_region_orientations[idx][1],
                rh_region_orientations[idx][2]))

    numpy.savetxt(AsegFiles.ORIENTATIONS_TXT.value.replace("%s", atlas_suffix), region_orientations, fmt='%.2f %.2f %.2f')

    annotation_service = AnnotationService()
    lut_dict, _, _ = annotation_service.read_lut(fs_color_lut, "name")
    rm_index_dict = mapping.get_mapping_for_aparc_aseg(lut_dict)
    genericIO.write_dict_to_txt_file(rm_index_dict, AsegFiles.RM_TO_APARC_ASEG_TXT.value.replace("%s", atlas_suffix))

    genericIO.write_list_to_txt_file(mapping.is_cortical_region_mapping(),
                                     AsegFiles.CORTICAL_TXT.value.replace("%s", atlas_suffix))
コード例 #4
0
ファイル: reconutils.py プロジェクト: sipv/tvb-recon
try:
    import gdist
except ImportError:
    warnings.warn(
        'Geodesic distance module unavailable; please pip install gdist.')


SUBJECTS_DIR, SUBJECT, FREESURFER_HOME = [os.environ[
    key] for key in 'SUBJECTS_DIR SUBJECT FREESURFER_HOME'.split()]

surfaceService = SurfaceService()
volumeService = VolumeService()
subparcelatioService = SubparcellationService()
sensorService = SensorService()
annotationService = AnnotationService()


def gen_head_model():
    sensorService.gen_head_model()

#-----------------------------Freesurfer surfaces------------------------------


def convert_fs_to_brain_visa(fs_surf):
    surfaceService.convert_fs_to_brain_visa(fs_surf)


def compute_gdist_mat(surf_name='pial', max_distance=40.0):
    surfaceService.compute_gdist_mat(surf_name, max_distance)
コード例 #5
0
ファイル: volume.py プロジェクト: maedoc/tvb-virtualizer
 def __init__(self):
     self.annotation_service = AnnotationService()
コード例 #6
0
ファイル: volume.py プロジェクト: maedoc/tvb-virtualizer
class VolumeService(object):
    logger = get_logger(__name__)

    def __init__(self):
        self.annotation_service = AnnotationService()

    def gen_label_volume_from_labels_inds(self, values: Union[numpy.ndarray, list],
                                          input_label_volume_file: os.PathLike, output_label_volume_file: os.PathLike) \
            -> nibabel.nifti1.Nifti1Image:
        """
        This function creates a new volume matrix from an input label_volume matrix
        by setting values[i] to all voxels where label_volume == i
        """

        label_nii = nibabel.load(input_label_volume_file)
        label_volume = label_nii.get_data()

        new_volume = numpy.zeros(label_volume.shape)
        new_volume[:, :] = numpy.nan

        for i, value in enumerate(values):
            region = i + 1
            mask = label_volume[:, :, :] == region
            new_volume[mask] = value

        # TODO: I don't know what this is... I have to ask Viktor...
        def add_min_max(volume):
            """Ugly hack to deal with the MRtrix bug (?) that causes MRview to crop min/max values"""
            volume[0, 0, 0] = numpy.nanmin(volume) - 1
            volume[0, 0, 1] = numpy.nanmax(volume) + 1

        # add_min_max(new_volume)
        new_nii = nibabel.Nifti1Image(new_volume, label_nii.affine)
        nibabel.save(new_nii, output_label_volume_file)

        return new_nii

    def gen_label_volume_from_coords(self, values: Union[numpy.ndarray, list],
                                     coords: Union[os.PathLike, numpy.ndarray],
                                     labels: Union[os.PathLike, numpy.ndarray, list], ref_volume_file: os.PathLike,
                                     out_volume_file: os.PathLike, skip_missing: bool=False, dist: int=0) \
            -> numpy.ndarray:
        """
        # Create and save a new nifti label volume of similar shape to a reference volume
        # by setting input values at input positions (optionally + & - (int) dist)
        # after applying to them the ref_aff transform

        :param values: vector of values to be included in the output volume, numpy.ndarray of shape (n_vals, )
        :param coords: array or list of 3D point coordinates, numpy.ndarray of shape (n_coords, 3), or file to read array
        :param labels: array or list of labels'names, or file to read them
        :param ref_volume_file: path to nifti volume
        :param out_volume_file: file path for the output nifti volume to be written
        :param skip_missing: flag
        :param dist: integer indicating the size of the neighborhood around the coords' positions to be labeled
        :return: output nifti volume
        """
        ref_volume = nibabel.load(ref_volume_file)

        if os.path.isfile(str(labels)):
            labels = list(numpy.genfromtxt(labels, dtype=str, usecols=(0,)))
        else:
            labels = list(labels)

        if os.path.isfile(str(coords)):
            coords = numpy.genfromtxt(coords, dtype=float, usecols=(1, 2, 3))

        positions = numpy.zeros((len(values), 3))
        for i, label in enumerate(labels):
            try:
                contact_ind = labels.index(label)
                coord = coords[contact_ind, :]
            except ValueError:
                coord = numpy.array([numpy.nan, numpy.nan, numpy.nan])

            positions[i, :] = coord

        missing_mask = numpy.isnan(positions[:, 0])
        if skip_missing:
            values = values[~missing_mask]
            positions = positions[~missing_mask, :]
        else:
            if numpy.any(missing_mask):
                raise ValueError("Missing contact position(s) for: %s." % ", ".join([
                    name for name, missing in zip(labels, missing_mask) if missing]))

            # numpy.array(names)[missing_mask]))

        new_volume = numpy.zeros(ref_volume.shape)
        # TODO: Find out the use of the following commented line:
        # new_volume[:, :] = numpy.nan

        kx, ky, kz = numpy.mgrid[-dist:dist + 1, -dist:dist + 1, -dist:dist + 1]

        for val, pos in zip(values, positions):
            ix, iy, iz = numpy.linalg.solve(ref_volume.affine, numpy.append(pos, 1.0))[0:3].astype(int)
            # new_volume[inds[0], inds[1], inds[2]] = val
            new_volume[ix + kx, iy + ky, iz + kz] = val

        # add_min_max(new_volume)

        new_nii = nibabel.Nifti1Image(new_volume, ref_volume.affine)
        nibabel.save(new_nii, out_volume_file)

        return nibabel.nifti1.Nifti1Image

    def vol_to_ext_surf_vol(self, in_vol_path: os.PathLike, labels: Optional[Union[numpy.ndarray, list]]=None,
                            ctx: Optional[os.PathLike]=None, out_vol_path: Optional[os.PathLike]=None,
                            labels_surf: Optional[Union[numpy.ndarray, list]]=None, labels_inner: str='0'):
        """
        Separate the voxels of the outer surface of a structure, from the inner ones. Default behavior: surface voxels
        retain their label, inner voxels get the label 0, and the input file is overwritten by the output.
        """

        labels = self.annotation_service.read_input_labels(
            labels=labels, ctx=ctx)
        number_of_labels = len(labels)
        # Set the labels for the surfaces
        if labels_surf is None:
            labels_surf = labels
        else:
            # Read the surface labels and make sure there is one for each label
            labels_surf = numpy.array(labels_surf.split()).astype('i')
            if len(labels_surf) == 1:
                labels_surf = numpy.repeat(
                    labels_inner, number_of_labels).tolist()
            elif len(labels_surf) != number_of_labels:
                self.logger.warning(
                    "Output labels for surface voxels are neither of length "
                    "1 nor of length equal to the one of target labels.")
                return
            else:
                labels_surf = labels_surf.tolist()
        # Read the inner, non-surface labels
        labels_inner = numpy.array(labels_inner.split()).astype('i')
        # ...and make sure there is one for each label
        if len(labels_inner) == 1:
            labels_inner = numpy.repeat(
                labels_inner, number_of_labels).tolist()
        elif len(labels_inner) != number_of_labels:
            self.logger.warning(
                "Output labels for inner voxels are neither of length 1 nor "
                "of length equal to the one of the target labels.")
            return
        else:
            labels_inner = labels_inner.tolist()

        # Read the input volume...
        volume = IOUtils.read_volume(in_vol_path)

        # Neigbors' grid sharing a face
        eye3 = numpy.identity(3)
        border_grid = numpy.c_[eye3, -eye3].T.astype('i')
        n_border = 6

        out_volume = Volume(numpy.array(volume.data),
                            volume.affine_matrix, volume.header)

        # Initialize output indexes
        out_ijk = []

        for label_index in range(number_of_labels):
            current_label = labels[label_index]
            # Get the indexes of all voxels of this label:
            label_volxels_i, label_voxels_j, label_voxels_k = numpy.where(
                volume.data == current_label)
            # and for each voxel
            for voxel_index in range(label_volxels_i.size):
                # indexes of this voxel:
                current_voxel_i, current_voxel_j, current_voxel_k = \
                    label_volxels_i[voxel_index], label_voxels_j[
                        voxel_index], label_voxels_k[voxel_index]
                # Create the neighbors' grid sharing a face
                ijk_grid = border_grid + \
                           numpy.tile(numpy.array(
                               [current_voxel_i, current_voxel_j, current_voxel_k]), (n_border, 1))
                # Remove voxels outside the image
                indices_inside_image = numpy.all([(ijk_grid[:, 0] >= 0), (ijk_grid[:, 0] < volume.dimensions[0]),
                                                  (ijk_grid[:, 1] >= 0), (ijk_grid[
                                                                          :, 1] < volume.dimensions[1]),
                                                  (ijk_grid[:, 2] >= 0), (ijk_grid[:, 2] < volume.dimensions[2])],
                                                 axis=0)
                ijk_grid = ijk_grid[indices_inside_image, :]
                try:
                    # If all face neighbors are of the same label...
                    if numpy.all(volume.data[ijk_grid[:, 0], ijk_grid[:, 1], ijk_grid[:, 2]] == numpy.tile(
                            volume.data[current_voxel_i,
                                        current_voxel_j, current_voxel_k],
                            (n_border, 1))):
                        # ...set this voxel to the corresponding inner target label
                        out_volume.data[current_voxel_i, current_voxel_j,
                                        current_voxel_k] = labels_inner[label_index]
                    else:
                        # ...set this voxel to the corresponding surface target label
                        out_volume.data[current_voxel_i, current_voxel_j,
                                        current_voxel_k] = labels_surf[label_index]
                        out_ijk.append(
                            [current_voxel_i, current_voxel_j, current_voxel_k])
                except ValueError:  # empty grid
                    self.logger.error("Error at voxel ( %s, %s, %s ) of label %s: It appears to have no common-face "
                                      "neighbors inside the image!", str(
                        current_voxel_i), str(current_voxel_j),
                                      str(current_voxel_k), str(current_label))
                    return

        if out_vol_path is None:
            out_vol_path = in_vol_path

        IOUtils.write_volume(out_vol_path, out_volume)

        # save the output indexes that survived masking
        out_ijk = numpy.vstack(out_ijk)
        filepath = os.path.splitext(out_vol_path)[0]
        numpy.save(filepath + "-idx.npy", out_ijk)
        numpy.savetxt(filepath + "-idx.txt", out_ijk, fmt='%d')

    def mask_to_vol(self, in_vol_path: os.PathLike, mask_vol_path: os.PathLike,
                    out_vol_path: Optional[os.PathLike]=None, labels: Optional[Union[numpy.ndarray, list]]=None,
                    ctx: Optional[str]=None, vol2mask_path: Optional[os.PathLike]=None, vn: int=1, th: float=0.999,
                    labels_mask: Optional[os.PathLike]=None, labels_nomask: str='0'):
        """
        Identify the voxels that are neighbors within a voxel distance vn, to a mask volume, with a mask threshold of th
        Default behavior: we assume a binarized mask and set th=0.999, no neighbors search, only looking at the exact
        voxel position, i.e., vn=0. Accepted voxels retain their label, whereas rejected ones get a label of 0
        """

        # Set the target labels:
        labels = self.annotation_service.read_input_labels(
            labels=labels, ctx=ctx)
        number_of_labels = len(labels)
        # Set the labels for the selected voxels
        if labels_mask is None:
            labels_mask = labels

        else:
            # Read the labels and make sure there is one for each label
            labels_mask = numpy.array(labels_mask.split()).astype('i')

            if len(labels_mask) == 1:
                labels_mask = numpy.repeat(
                    labels_mask, number_of_labels).tolist()

            elif len(labels_mask) != number_of_labels:
                self.logger.warning("Output labels for selected voxels are neither of length 1 nor of length equal to "
                                    "the one of target labels")
                return

            else:
                labels_mask = labels_mask.tolist()

        # Read the excluded labels and make sure there is one for each label
        labels_nomask = numpy.array(labels_nomask.split()).astype('i')
        if len(labels_nomask) == 1:
            labels_nomask = numpy.repeat(
                labels_nomask, number_of_labels).tolist()

        elif len(labels_nomask) != number_of_labels:
            self.logger.warning("Output labels for excluded voxels are neither of length 1 nor of length equal to the "
                                "one of the target labels")
            return

        else:
            labels_nomask = labels_nomask.tolist()

        volume = IOUtils.read_volume(in_vol_path)

        mask_vol = IOUtils.read_volume(mask_vol_path)

        # Compute the transform from vol ijk to mask ijk:
        ijk2ijk = numpy.identity(4)

        # If vol and mask are not in the same space:
        if os.path.exists(str(vol2mask_path)):
            # read the xyz2xyz transform and apply it to the inverse mask
            # affine transform to get an ijk2ijk transform.
            xyz2xyz = numpy.loadtxt(vol2mask_path)
            ijk2ijk = volume.affine_matrix.dot(
                numpy.dot(xyz2xyz, numpy.linalg.inv(mask_vol.affine_matrix)))

        # Construct a grid template of voxels +/- vn voxels around each ijk
        # voxel, sharing at least a corner
        grid = numpy.meshgrid(list(range(-vn, vn + 1, 1)), list(
            range(-vn, vn + 1, 1)), list(range(-vn, vn + 1, 1)), indexing='ij')
        grid = numpy.c_[numpy.array(grid[0]).flatten(), numpy.array(
            grid[1]).flatten(), numpy.array(grid[2]).flatten()]
        n_grid = grid.shape[0]

        out_volume = Volume(numpy.array(volume.data),
                            volume.affine_matrix, volume.header)

        # Initialize output indexes
        out_ijk = []

        # For each target label:
        for label_index in range(number_of_labels):
            current_label = labels[label_index]
            # Get the indexes of all voxels of this label:
            label_voxels_i, label_voxels_j, label_voxels_k = numpy.where(
                volume.data == current_label)

            for voxel_index in range(label_voxels_i.size):
                current_voxel_i, current_voxel_j, current_voxel_k = \
                    label_voxels_i[voxel_index], label_voxels_j[
                        voxel_index], label_voxels_k[voxel_index]
                # TODO if necessary: deal with voxels at the edge of the image, such as brain stem ones...
                #     if any([(i==0), (i==mask_shape[0]-1),(j==0), (j==mask_shape[0]-1),(k==0), (k==mask_shape[0]-1)]):
                #               mask_shape[i,j,k]=0
                #               continue

                # ...get the corresponding voxel in the mask volume:
                ijk = numpy.round(ijk2ijk.dot(numpy.array(
                    [current_voxel_i, current_voxel_j, current_voxel_k, 1]))[:3]).astype('i')

                # Make sure this point is within image limits
                for cc in range(3):
                    if ijk[cc] < 0:
                        ijk[cc] = 0

                    elif ijk[cc] >= mask_vol.dimensions[cc]:
                        ijk[cc] = mask_vol.dimensions[cc] - 1

                # If this is a voxel to keep, set it so...
                if mask_vol.data[ijk[0], ijk[1], ijk[2]] >= th:
                    out_volume.data[current_voxel_i, current_voxel_j,
                                    current_voxel_k] = labels_mask[label_index]
                    out_ijk.append(
                        [current_voxel_i, current_voxel_j, current_voxel_k])

                elif vn > 0:
                    # If not, and as long as vn>0 check whether any of its vn neighbors is a mask voxel.
                    # Generate the specific grid centered at the vertex ijk
                    ijk_grid = grid + numpy.tile(ijk, (n_grid, 1))

                    # Remove voxels outside the mask volume
                    indexes_within_limits = numpy.all([(ijk_grid[:, 0] >= 0), (ijk_grid[:, 0] < mask_vol.dimensions[0]),
                                                       (ijk_grid[:, 1] >= 0), (ijk_grid[
                                                                               :, 1] < mask_vol.dimensions[1]),
                                                       (ijk_grid[:, 2] >= 0),
                                                       (ijk_grid[:, 2] < mask_vol.dimensions[2])],
                                                      axis=0)
                    ijk_grid = ijk_grid[indexes_within_limits, :]

                    try:
                        # If none of these points is a mask point:
                        if (mask_vol.data[ijk_grid[:, 0], ijk_grid[
                                                          :, 1], ijk_grid[:, 2]] < th).all():
                            out_volume.data[
                                current_voxel_i, current_voxel_j, current_voxel_k] = labels_nomask[label_index]

                        else:  # if any of them is a mask point:
                            out_volume.data[
                                current_voxel_i, current_voxel_j, current_voxel_k] = labels_mask[label_index]
                            out_ijk.append(
                                [current_voxel_i, current_voxel_j, current_voxel_k])

                    except ValueError:  # empty grid
                        self.logger.error("Error at voxel ( %s, %s, %s ): It appears to have no common-face neighbors "
                                          "inside the image!", str(
                            current_voxel_i), str(current_voxel_j),
                                          str(current_voxel_k))
                        return

                else:
                    out_volume.data[current_voxel_i, current_voxel_j,
                                    current_voxel_k] = labels_nomask[label_index]

        if out_vol_path is None:
            out_vol_path = in_vol_path

        IOUtils.write_volume(out_vol_path, out_volume)

        # Save the output indexes that survived masking
        out_ijk = numpy.vstack(out_ijk)
        filepath = os.path.splitext(out_vol_path)[0]
        numpy.save(filepath + "-idx.npy", out_ijk)
        numpy.savetxt(filepath + "-idx.txt", out_ijk, fmt='%d')

    def vol_val_xyz(self, vol: numpy.ndarray, aff: numpy.ndarray, val: float) -> numpy.ndarray:
        vox_idx = numpy.argwhere(vol == val)
        xyz = aff.dot(numpy.c_[vox_idx, numpy.ones(vox_idx.shape[0])].T)[:3].T
        return xyz

    def compute_label_volume_centers(self, label_volume: numpy.ndarray, affine: numpy.ndarray):

        for val in numpy.unique(label_volume):
            xyz = self.vol_val_xyz(label_volume, affine, val)
            x, y, z = xyz.mean(axis=0)
            yield val, (x, y, z)

    def label_with_dilation(self, to_label_nii_fname: os.PathLike, dilated_nii_fname: os.PathLike,
                            out_nii_fname: os.PathLike):
        """
        Labels a volume using its labeled dilation. The dilated volume is labeled using scipy.ndimage.label function.
        :param to_label_nii_fname: usually a CT-mask.nii.gz
        :param dilated_nii_fname: dilated version of the to_label_nii_fname volume
        """

        # TODO could make dilation with ndimage also.
        mask = IOUtils.read_volume(to_label_nii_fname)
        dil_mask = IOUtils.read_volume(dilated_nii_fname)

        lab, n = scipy.ndimage.label(dil_mask.data)

        # TODO: this change is from tvb-make. Keep it or not? It returns a different result than the old version.
        lab_xyz = list(self.compute_label_volume_centers(lab, dil_mask.affine_matrix))
        lab_sort = numpy.r_[:n + 1]
        # sort labels along AP axis
        for i, (val, _) in enumerate(sorted(lab_xyz, key=lambda t: t[1][1])):
            lab_sort[val] = i
        lab = lab_sort[lab]

        mask.data *= lab
        self.logger.info(
            '%d objects found when labeling the dilated volume.', n)

        IOUtils.write_volume(out_nii_fname, mask)

    def _label_config(self, aparc: nibabel.nifti1.Nifti1Image) -> nibabel.nifti1.Nifti1Image:
        unique_data = numpy.unique(aparc.data)
        unique_data_map = numpy.r_[:unique_data.max() + 1]
        unique_data_map[unique_data] = numpy.r_[:unique_data.size]
        aparc.data = unique_data_map[aparc.data]
        return aparc

    def simple_label_config(self, in_aparc_path: os.PathLike, out_volume_path: os.PathLike):
        """
        Relabel volume to have contiguous values like Mrtrix' labelconfig.
        :param in_aparc_path: volume voxel value is the index of the region it belongs to.
        :return: writes the labeled volume to out_volume_path.
        """

        aparc = IOUtils.read_volume(in_aparc_path)
        aparc = self._label_config(aparc)
        IOUtils.write_volume(out_volume_path, aparc)

    def _label_volume(self, tdi_volume: nibabel.nifti1.Nifti1Image, lo: float=0.5) -> nibabel.nifti1.Nifti1Image:
        mask = tdi_volume.data > lo
        tdi_volume.data[~mask] = 0
        tdi_volume.data[mask] = numpy.r_[1:mask.sum() + 1]
        return tdi_volume

    def label_vol_from_tdi(self, tdi_volume_path: os.PathLike, out_volume_path: os.PathLike, lo: float=0.5):
        """
        Creates a mask of the voxels with tract ends > lo and any other voxels become 0.
        Labels each voxel different from 0 with integer labels starting from 1.
        :param tdi_volume_path: volume voxel value is the sum of tract ends. Voxel without tract ends has value 0.
        :param lo: tract ends threshold used for masking.
        :return: writes labeled volume to :ut_volume_path.
        """

        nii_volume = IOUtils.read_volume(tdi_volume_path)
        tdi_volume = self._label_volume(nii_volume, lo)
        IOUtils.write_volume(out_volume_path, tdi_volume)

    def remove_zero_connectivity_nodes(self, node_volume_path: os.PathLike, connectivity_matrix_path: os.PathLike,
                                       tract_length_path: Optional[str]=None):
        """
        It removes network nodes with zero connectivity from the volume and connectivity matrices.
        The zero connectivity nodes will be labeled with 0 in the volume and the remaining labels will be updated.
        The connectivity matrices will be symmetric.
        :param node_volume_path: tdi_lbl.nii volume path
        :param connectivity_matrix_path: .csv file, output of Mrtrix3 tck2connectome
        :param tract_length_path: optional .csv tract lengths matrix
        :return: overwrites the input volume and matrices with the processed ones. Also saves matrices as .npy.
        """

        node_volume = IOUtils.read_volume(node_volume_path)

        connectivity = numpy.array(numpy.genfromtxt(
            connectivity_matrix_path, dtype='int64'))
        connectivity = connectivity + connectivity.T
        connectivity_row_sum = numpy.sum(connectivity, axis=0)

        nodes_to_keep_indices = connectivity_row_sum > 0
        connectivity = connectivity[nodes_to_keep_indices, :][
                       :, nodes_to_keep_indices]

        numpy.save(os.path.splitext(connectivity_matrix_path)
                   [0] + NPY_EXTENSION, connectivity)
        numpy.savetxt(connectivity_matrix_path, connectivity, fmt='%1d')

        if os.path.exists(str(tract_length_path)):
            connectivity = numpy.array(numpy.genfromtxt(
                tract_length_path, dtype='int64'))
            connectivity = connectivity[nodes_to_keep_indices, :][
                           :, nodes_to_keep_indices]

            numpy.save(os.path.splitext(tract_length_path)
                       [0] + NPY_EXTENSION, connectivity)
            numpy.savetxt(tract_length_path, connectivity, fmt='%1d')

        else:
            self.logger.warning("Path %s is not valid.", tract_length_path)

        nodes_to_remove_indices, = numpy.where(~nodes_to_keep_indices)
        nodes_to_remove_indices += 1

        for node_index in nodes_to_remove_indices:
            node_volume.data[node_volume.data == node_index] = 0

        node_volume.data[node_volume.data > 0] = numpy.r_[
                                                 1:(connectivity.shape[0] + 1)]

        IOUtils.write_volume(node_volume_path, node_volume)

    def con_vox_in_ras(self, ref_vol_path: os.PathLike) -> (numpy.ndarray, numpy.ndarray):
        """
        This function reads a tdi_lbl volume and returns the voxels that correspond to connectome nodes,
        and their coordinates in ras space, simply by applying the affine transform of the volume
        :param ref_vol_path: the path to the tdi_lbl volume
        :return: vox and voxxyz,
                i.e., the labels (integers>=1) and the coordinates of the connnectome nodes-voxels, respectively
        """
        # Read the reference tdi_lbl volume:
        vollbl = IOUtils.read_volume(ref_vol_path)
        vox = vollbl.data.astype('i')
        # Get only the voxels that correspond to connectome nodes:
        voxijk, = numpy.where(vox.flatten() > 0)
        voxijk = numpy.unravel_index(voxijk, vollbl.dimensions)
        vox = vox[voxijk[0], voxijk[1], voxijk[2]]
        # ...and their coordinates in ras xyz space
        voxxzy = vollbl.affine_matrix.dot(numpy.c_[voxijk[0], voxijk[1], voxijk[
            2], numpy.ones(vox.shape[0])].T)[:3].T
        return vox, voxxzy

    def change_labels_of_aparc_aseg(self, atlas_suffix, volume, mapping_dict, conn_regs_nr):
        if atlas_suffix == AtlasSuffix.A2009S:
            volume.data[volume.data == 1000] = 11100
            volume.data[volume.data == 2000] = 12100
        not_matched = set()
        for i in range(volume.data.shape[0]):
            for j in range(volume.data.shape[1]):
                for k in range(volume.data.shape[2]):
                    val = volume.data[i][j][k]
                    if not val in mapping_dict:
                        not_matched.add(val)
                    volume.data[i][j][k] = mapping_dict.get(val, -1)

        print("Now values are in interval [%d - %d]" % (volume.data.min(), volume.data.max()))

        if not_matched:
            print("Not matched regions will be considered background: %s" % not_matched)
        assert (volume.data.min() >= -1 and volume.data.max() < conn_regs_nr)

        return volume

    def transform_coords(self, coords: Union[list, numpy.ndarray, str], src_img: os.PathLike, dest_img: os.PathLike,
                  transform_mat: os.PathLike, output_file: Optional[str]=None) \
            -> (numpy.array, Union[numpy.ndarray, type(None)]):

        if os.path.isfile(coords):
            command = "img2imgcoord %s -mm -src %s -dest %s -xfm %s"
            args = [coords, src_img, dest_img, transform_mat]
        else:
            coords_str = " ".join([str(x) for x in coords])
            command = "echo %s | img2imgcoord -mm -src %s -dest %s -xfm %s"
            args = [coords_str, src_img, dest_img, transform_mat]

        if os.path.isdir(os.path.dirname(output_file)):
            command += " > %s"
            args.append(output_file)

        output, std_out, time = \
            execute_command(command % tuple(args), cwd=os.path.dirname(transform_mat), shell=True)

        transformed_coords_str = output.strip().split('\n')[-1]

        return numpy.array([float(x) for x in transformed_coords_str.split(" ") if x]), output_file
コード例 #7
0
ファイル: volume.py プロジェクト: sipv/tvb-recon
class VolumeService(object):
    logger = get_logger(__name__)

    def __init__(self):
        self.annotation_service = AnnotationService()

    def vol_to_ext_surf_vol(self,
                            in_vol_path,
                            labels=None,
                            ctx=None,
                            out_vol_path=None,
                            labels_surf=None,
                            labels_inner='0'):
        """
        Separate the voxels of the outer surface of a structure, from the inner ones. Default behavior: surface voxels
        retain their label, inner voxels get the label 0, and the input file is overwritten by the output.
        """

        labels = self.annotation_service.read_input_labels(labels=labels,
                                                           ctx=ctx)
        number_of_labels = len(labels)
        # Set the labels for the surfaces
        if labels_surf is None:
            labels_surf = labels
        else:
            # Read the surface labels and make sure there is one for each label
            labels_surf = numpy.array(labels_surf.split()).astype('i')
            if len(labels_surf) == 1:
                labels_surf = numpy.repeat(labels_inner,
                                           number_of_labels).tolist()
            elif len(labels_surf) != number_of_labels:
                self.logger.warning(
                    "Output labels for surface voxels are neither of length "
                    "1 nor of length equal to the one of target labels.")
                return
            else:
                labels_surf = labels_surf.tolist()
        # Read the inner, non-surface labels
        labels_inner = numpy.array(labels_inner.split()).astype('i')
        # ...and make sure there is one for each label
        if len(labels_inner) == 1:
            labels_inner = numpy.repeat(labels_inner,
                                        number_of_labels).tolist()
        elif len(labels_inner) != number_of_labels:
            self.logger.warning(
                "Output labels for inner voxels are neither of length 1 nor "
                "of length equal to the one of the target labels.")
            return
        else:
            labels_inner = labels_inner.tolist()

        # Read the input volume...
        volume = IOUtils.read_volume(in_vol_path)

        # Neigbors' grid sharing a face
        eye3 = numpy.identity(3)
        border_grid = numpy.c_[eye3, -eye3].T.astype('i')
        n_border = 6

        out_volume = Volume(numpy.array(volume.data), volume.affine_matrix,
                            volume.header)

        # Initialize output indexes
        out_ijk = []

        for label_index in range(number_of_labels):
            current_label = labels[label_index]
            # Get the indexes of all voxels of this label:
            label_volxels_i, label_voxels_j, label_voxels_k = numpy.where(
                volume.data == current_label)
            # and for each voxel
            for voxel_index in range(label_volxels_i.size):
                # indexes of this voxel:
                current_voxel_i, current_voxel_j, current_voxel_k = \
                    label_volxels_i[voxel_index], label_voxels_j[
                        voxel_index], label_voxels_k[voxel_index]
                # Create the neighbors' grid sharing a face
                ijk_grid = border_grid + \
                    numpy.tile(numpy.array(
                        [current_voxel_i, current_voxel_j, current_voxel_k]), (n_border, 1))
                # Remove voxels outside the image
                indices_inside_image = numpy.all(
                    [(ijk_grid[:, 0] >= 0),
                     (ijk_grid[:, 0] < volume.dimensions[0]),
                     (ijk_grid[:, 1] >= 0),
                     (ijk_grid[:, 1] < volume.dimensions[1]),
                     (ijk_grid[:, 2] >= 0),
                     (ijk_grid[:, 2] < volume.dimensions[2])],
                    axis=0)
                ijk_grid = ijk_grid[indices_inside_image, :]
                try:
                    # If all face neighbors are of the same label...
                    if numpy.all(
                            volume.data[ijk_grid[:, 0], ijk_grid[:, 1],
                                        ijk_grid[:, 2]] == numpy.tile(
                                            volume.data[current_voxel_i,
                                                        current_voxel_j,
                                                        current_voxel_k], (
                                                            n_border, 1))):
                        # ...set this voxel to the corresponding inner target label
                        out_volume.data[
                            current_voxel_i, current_voxel_j,
                            current_voxel_k] = labels_inner[label_index]
                    else:
                        # ...set this voxel to the corresponding surface target label
                        out_volume.data[
                            current_voxel_i, current_voxel_j,
                            current_voxel_k] = labels_surf[label_index]
                        out_ijk.append([
                            current_voxel_i, current_voxel_j, current_voxel_k
                        ])
                except ValueError:  # empty grid
                    self.logger.error(
                        "Error at voxel ( %s, %s, %s ) of label %s: It appears to have no common-face "
                        "neighbors inside the image!", str(current_voxel_i),
                        str(current_voxel_j), str(current_voxel_k),
                        str(current_label))
                    return

        if out_vol_path is None:
            out_vol_path = in_vol_path

        IOUtils.write_volume(out_vol_path, out_volume)

        # save the output indexes that survived masking
        out_ijk = numpy.vstack(out_ijk)
        filepath = os.path.splitext(out_vol_path)[0]
        numpy.save(filepath + "-idx.npy", out_ijk)
        numpy.savetxt(filepath + "-idx.txt", out_ijk, fmt='%d')

    def mask_to_vol(self,
                    in_vol_path,
                    mask_vol_path,
                    out_vol_path=None,
                    labels=None,
                    ctx=None,
                    vol2mask_path=None,
                    vn=1,
                    th=0.999,
                    labels_mask=None,
                    labels_nomask='0'):
        """
        Identify the voxels that are neighbors within a voxel distance vn, to a mask volume, with a mask threshold of th
        Default behavior: we assume a binarized mask and set th=0.999, no neighbors search, only looking at the exact
        voxel position, i.e., vn=0. Accepted voxels retain their label, whereas rejected ones get a label of 0
        """

        # Set the target labels:
        labels = self.annotation_service.read_input_labels(labels=labels,
                                                           ctx=ctx)
        number_of_labels = len(labels)
        # Set the labels for the selected voxels
        if labels_mask is None:
            labels_mask = labels

        else:
            # Read the labels and make sure there is one for each label
            labels_mask = numpy.array(labels_mask.split()).astype('i')

            if len(labels_mask) == 1:
                labels_mask = numpy.repeat(labels_mask,
                                           number_of_labels).tolist()

            elif len(labels_mask) != number_of_labels:
                self.logger.warning(
                    "Output labels for selected voxels are neither of length 1 nor of length equal to "
                    "the one of target labels")
                return

            else:
                labels_mask = labels_mask.tolist()

        # Read the excluded labels and make sure there is one for each label
        labels_nomask = numpy.array(labels_nomask.split()).astype('i')
        if len(labels_nomask) == 1:
            labels_nomask = numpy.repeat(labels_nomask,
                                         number_of_labels).tolist()

        elif len(labels_nomask) != number_of_labels:
            self.logger.warning(
                "Output labels for excluded voxels are neither of length 1 nor of length equal to the "
                "one of the target labels")
            return

        else:
            labels_nomask = labels_nomask.tolist()

        volume = IOUtils.read_volume(in_vol_path)

        mask_vol = IOUtils.read_volume(mask_vol_path)

        # Compute the transform from vol ijk to mask ijk:
        ijk2ijk = numpy.identity(4)

        # If vol and mask are not in the same space:
        if os.path.exists(str(vol2mask_path)):
            # read the xyz2xyz transform and apply it to the inverse mask
            # affine transform to get an ijk2ijk transform.
            xyz2xyz = numpy.loadtxt(vol2mask_path)
            ijk2ijk = volume.affine_matrix.dot(
                numpy.dot(xyz2xyz, numpy.linalg.inv(mask_vol.affine_matrix)))

        # Construct a grid template of voxels +/- vn voxels around each ijk
        # voxel, sharing at least a corner
        grid = numpy.meshgrid(list(range(-vn, vn + 1, 1)),
                              list(range(-vn, vn + 1, 1)),
                              list(range(-vn, vn + 1, 1)),
                              indexing='ij')
        grid = numpy.c_[numpy.array(grid[0]).flatten(),
                        numpy.array(grid[1]).flatten(),
                        numpy.array(grid[2]).flatten()]
        n_grid = grid.shape[0]

        out_volume = Volume(numpy.array(volume.data), volume.affine_matrix,
                            volume.header)

        # Initialize output indexes
        out_ijk = []

        # For each target label:
        for label_index in range(number_of_labels):
            current_label = labels[label_index]
            # Get the indexes of all voxels of this label:
            label_voxels_i, label_voxels_j, label_voxels_k = numpy.where(
                volume.data == current_label)

            for voxel_index in range(label_voxels_i.size):
                current_voxel_i, current_voxel_j, current_voxel_k = \
                    label_voxels_i[voxel_index], label_voxels_j[
                        voxel_index], label_voxels_k[voxel_index]
                # TODO if necessary: deal with voxels at the edge of the image, such as brain stem ones...
                #     if any([(i==0), (i==mask_shape[0]-1),(j==0), (j==mask_shape[0]-1),(k==0), (k==mask_shape[0]-1)]):
                #               mask_shape[i,j,k]=0
                #               continue

                # ...get the corresponding voxel in the mask volume:
                ijk = numpy.round(
                    ijk2ijk.dot(
                        numpy.array([
                            current_voxel_i, current_voxel_j, current_voxel_k,
                            1
                        ]))[:3]).astype('i')

                # Make sure this point is within image limits
                for cc in range(3):
                    if ijk[cc] < 0:
                        ijk[cc] = 0

                    elif ijk[cc] >= mask_vol.dimensions[cc]:
                        ijk[cc] = mask_vol.dimensions[cc] - 1

                # If this is a voxel to keep, set it so...
                if mask_vol.data[ijk[0], ijk[1], ijk[2]] >= th:
                    out_volume.data[current_voxel_i, current_voxel_j,
                                    current_voxel_k] = labels_mask[label_index]
                    out_ijk.append(
                        [current_voxel_i, current_voxel_j, current_voxel_k])

                elif vn > 0:
                    # If not, and as long as vn>0 check whether any of its vn neighbors is a mask voxel.
                    # Generate the specific grid centered at the vertex ijk
                    ijk_grid = grid + numpy.tile(ijk, (n_grid, 1))

                    # Remove voxels outside the mask volume
                    indexes_within_limits = numpy.all(
                        [(ijk_grid[:, 0] >= 0),
                         (ijk_grid[:, 0] < mask_vol.dimensions[0]),
                         (ijk_grid[:, 1] >= 0),
                         (ijk_grid[:, 1] < mask_vol.dimensions[1]),
                         (ijk_grid[:, 2] >= 0),
                         (ijk_grid[:, 2] < mask_vol.dimensions[2])],
                        axis=0)
                    ijk_grid = ijk_grid[indexes_within_limits, :]

                    try:
                        # If none of these points is a mask point:
                        if (mask_vol.data[ijk_grid[:, 0], ijk_grid[:, 1],
                                          ijk_grid[:, 2]] < th).all():
                            out_volume.data[
                                current_voxel_i, current_voxel_j,
                                current_voxel_k] = labels_nomask[label_index]

                        else:  # if any of them is a mask point:
                            out_volume.data[
                                current_voxel_i, current_voxel_j,
                                current_voxel_k] = labels_mask[label_index]
                            out_ijk.append([
                                current_voxel_i, current_voxel_j,
                                current_voxel_k
                            ])

                    except ValueError:  # empty grid
                        self.logger.error(
                            "Error at voxel ( %s, %s, %s ): It appears to have no common-face neighbors "
                            "inside the image!", str(current_voxel_i),
                            str(current_voxel_j), str(current_voxel_k))
                        return

                else:
                    out_volume.data[
                        current_voxel_i, current_voxel_j,
                        current_voxel_k] = labels_nomask[label_index]

        if out_vol_path is None:
            out_vol_path = in_vol_path

        IOUtils.write_volume(out_vol_path, out_volume)

        # Save the output indexes that survived masking
        out_ijk = numpy.vstack(out_ijk)
        filepath = os.path.splitext(out_vol_path)[0]
        numpy.save(filepath + "-idx.npy", out_ijk)
        numpy.savetxt(filepath + "-idx.txt", out_ijk, fmt='%d')

    def label_with_dilation(self, to_label_nii_fname, dilated_nii_fname,
                            out_nii_fname):
        """
        Labels a volume using its labeled dilation. The dilated volume is labeled using scipy.ndimage.label function.
        :param to_label_nii_fname: usually a CT-mask.nii.gz
        :param dilated_nii_fname: dilated version of the to_label_nii_fname volume
        """

        # TODO could make dilation with ndimage also.
        mask = IOUtils.read_volume(to_label_nii_fname)
        dil_mask = IOUtils.read_volume(dilated_nii_fname)

        lab, n = scipy.ndimage.label(dil_mask.data)
        mask.data *= lab
        self.logger.info('%d objects found when labeling the dilated volume.',
                         n)

        IOUtils.write_volume(out_nii_fname, mask)

    def _label_config(self, aparc):
        unique_data = numpy.unique(aparc.data)
        unique_data_map = numpy.r_[:unique_data.max() + 1]
        unique_data_map[unique_data] = numpy.r_[:unique_data.size]
        aparc.data = unique_data_map[aparc.data]
        return aparc

    def simple_label_config(self, in_aparc_path, out_volume_path):
        """
        Relabel volume to have contiguous values like Mrtrix' labelconfig.
        :param in_aparc_path: volume voxel value is the index of the region it belongs to.
        :return: writes the labeled volume to out_volume_path.
        """

        aparc = IOUtils.read_volume(in_aparc_path)
        aparc = self._label_config(aparc)
        IOUtils.write_volume(out_volume_path, aparc)

    def _label_volume(self, tdi_volume, lo=0.5):
        mask = tdi_volume.data > lo
        tdi_volume.data[~mask] = 0
        tdi_volume.data[mask] = numpy.r_[1:mask.sum() + 1]
        return tdi_volume

    def label_vol_from_tdi(self, tdi_volume_path, out_volume_path, lo=0.5):
        """
        Creates a mask of the voxels with tract ends > lo and any other voxels become 0.
        Labels each voxel different from 0 with integer labels starting from 1.
        :param tdi_volume_path: volume voxel value is the sum of tract ends. Voxel without tract ends has value 0.
        :param lo: tract ends threshold used for masking.
        :return: writes labeled volume to :ut_volume_path.
        """

        nii_volume = IOUtils.read_volume(tdi_volume_path)
        tdi_volume = self._label_volume(nii_volume, lo)
        IOUtils.write_volume(out_volume_path, tdi_volume)

    def remove_zero_connectivity_nodes(self,
                                       node_volume_path,
                                       connectivity_matrix_path,
                                       tract_length_path=None):
        """
        It removes network nodes with zero connectivity from the volume and connectivity matrices.
        The zero connectivity nodes will be labeled with 0 in the volume and the remaining labels will be updated.
        The connectivity matrices will be symmetric.
        :param node_volume_path: tdi_lbl.nii volume path
        :param connectivity_matrix_path: .csv file, output of Mrtrix3 tck2connectome
        :param tract_length_path: optional .csv tract lengths matrix
        :return: overwrites the input volume and matrices with the processed ones. Also saves matrices as .npy.
        """

        node_volume = IOUtils.read_volume(node_volume_path)

        connectivity = numpy.array(
            numpy.genfromtxt(connectivity_matrix_path, dtype='int64'))
        connectivity = connectivity + connectivity.T
        connectivity_row_sum = numpy.sum(connectivity, axis=0)

        nodes_to_keep_indices = connectivity_row_sum > 0
        connectivity = connectivity[
            nodes_to_keep_indices, :][:, nodes_to_keep_indices]

        numpy.save(
            os.path.splitext(connectivity_matrix_path)[0] + NPY_EXTENSION,
            connectivity)
        numpy.savetxt(connectivity_matrix_path, connectivity, fmt='%1d')

        if os.path.exists(str(tract_length_path)):
            connectivity = numpy.array(
                numpy.genfromtxt(tract_length_path, dtype='int64'))
            connectivity = connectivity[
                nodes_to_keep_indices, :][:, nodes_to_keep_indices]

            numpy.save(
                os.path.splitext(tract_length_path)[0] + NPY_EXTENSION,
                connectivity)
            numpy.savetxt(tract_length_path, connectivity, fmt='%1d')

        else:
            self.logger.warning("Path %s is not valid.", tract_length_path)

        nodes_to_remove_indices, = numpy.where(~nodes_to_keep_indices)
        nodes_to_remove_indices += 1

        for node_index in nodes_to_remove_indices:
            node_volume.data[node_volume.data == node_index] = 0

        node_volume.data[node_volume.data > 0] = numpy.r_[1:(
            connectivity.shape[0] + 1)]

        IOUtils.write_volume(node_volume_path, node_volume)

    def con_vox_in_ras(self, ref_vol_path):
        """
        This function reads a tdi_lbl volume and returns the voxels that correspond to connectome nodes,
        and their coordinates in ras space, simply by applying the affine transform of the volume
        :param ref_vol_path: the path to the tdi_lbl volume
        :return: vox and voxxyz,
                i.e., the labels (integers>=1) and the coordinates of the connnectome nodes-voxels, respectively
        """
        # Read the reference tdi_lbl volume:
        vollbl = IOUtils.read_volume(ref_vol_path)
        vox = vollbl.data.astype('i')
        # Get only the voxels that correspond to connectome nodes:
        voxijk, = numpy.where(vox.flatten() > 0)
        voxijk = numpy.unravel_index(voxijk, vollbl.dimensions)
        vox = vox[voxijk[0], voxijk[1], voxijk[2]]
        # ...and their coordinates in ras xyz space
        voxxzy = vollbl.affine_matrix.dot(
            numpy.c_[voxijk[0], voxijk[1], voxijk[2],
                     numpy.ones(vox.shape[0])].T)[:3].T
        return vox, voxxzy
コード例 #8
0
ファイル: volume.py プロジェクト: umarbrowser/tvb-recon
class VolumeService(object):
    logger = get_logger(__name__)

    def __init__(self):
        self.annotation_service = AnnotationService()

    def gen_label_volume_from_labels_inds(self, values: Union[numpy.ndarray, list],
                                          input_label_volume_file: os.PathLike, output_label_volume_file: os.PathLike) \
            -> nibabel.nifti1.Nifti1Image:
        """
        This function creates a new volume matrix from an input label_volume matrix
        by setting values[i] to all voxels where label_volume == i
        """

        label_nii = nibabel.load(input_label_volume_file)
        label_volume = label_nii.get_data()

        new_volume = numpy.zeros(label_volume.shape)
        new_volume[:, :] = numpy.nan

        for i, value in enumerate(values):
            region = i + 1
            mask = label_volume[:, :, :] == region
            new_volume[mask] = value

        # TODO: I don't know what this is... I have to ask Viktor...
        def add_min_max(volume):
            """Ugly hack to deal with the MRtrix bug (?) that causes MRview to crop min/max values"""
            volume[0, 0, 0] = numpy.nanmin(volume) - 1
            volume[0, 0, 1] = numpy.nanmax(volume) + 1

        # add_min_max(new_volume)
        new_nii = nibabel.Nifti1Image(new_volume, label_nii.affine)
        nibabel.save(new_nii, output_label_volume_file)

        return new_nii

    def gen_label_volume_from_coords(self, values: Union[numpy.ndarray, list],
                                     coords: Union[os.PathLike, numpy.ndarray],
                                     labels: Union[os.PathLike, numpy.ndarray, list], ref_volume_file: os.PathLike,
                                     out_volume_file: os.PathLike, skip_missing: bool=False, dist: int=0) \
            -> numpy.ndarray:
        """
        # Create and save a new nifti label volume of similar shape to a reference volume
        # by setting input values at input positions (optionally + & - (int) dist)
        # after applying to them the ref_aff transform

        :param values: vector of values to be included in the output volume, numpy.ndarray of shape (n_vals, )
        :param coords: array or list of 3D point coordinates, numpy.ndarray of shape (n_coords, 3), or file to read array
        :param labels: array or list of labels'names, or file to read them
        :param ref_volume_file: path to nifti volume
        :param out_volume_file: file path for the output nifti volume to be written
        :param skip_missing: flag
        :param dist: integer indicating the size of the neighborhood around the coords' positions to be labeled
        :return: output nifti volume
        """
        ref_volume = nibabel.load(ref_volume_file)

        if os.path.isfile(str(labels)):
            labels = list(numpy.genfromtxt(labels, dtype=str, usecols=(0, )))
        else:
            labels = list(labels)

        if os.path.isfile(str(coords)):
            coords = numpy.genfromtxt(coords, dtype=float, usecols=(1, 2, 3))

        positions = numpy.zeros((len(values), 3))
        for i, label in enumerate(labels):
            try:
                contact_ind = labels.index(label)
                coord = coords[contact_ind, :]
            except ValueError:
                coord = numpy.array([numpy.nan, numpy.nan, numpy.nan])

            positions[i, :] = coord

        missing_mask = numpy.isnan(positions[:, 0])
        if skip_missing:
            values = values[~missing_mask]
            positions = positions[~missing_mask, :]
        else:
            if numpy.any(missing_mask):
                raise ValueError(
                    "Missing contact position(s) for: %s." % ", ".join([
                        name for name, missing in zip(labels, missing_mask)
                        if missing
                    ]))

            # numpy.array(names)[missing_mask]))

        new_volume = numpy.zeros(ref_volume.shape)
        # TODO: Find out the use of the following commented line:
        # new_volume[:, :] = numpy.nan

        kx, ky, kz = numpy.mgrid[-dist:dist + 1, -dist:dist + 1,
                                 -dist:dist + 1]

        for val, pos in zip(values, positions):
            ix, iy, iz = numpy.linalg.solve(ref_volume.affine,
                                            numpy.append(pos,
                                                         1.0))[0:3].astype(int)
            # new_volume[inds[0], inds[1], inds[2]] = val
            new_volume[ix + kx, iy + ky, iz + kz] = val

        # add_min_max(new_volume)

        new_nii = nibabel.Nifti1Image(new_volume, ref_volume.affine)
        nibabel.save(new_nii, out_volume_file)

        return nibabel.nifti1.Nifti1Image

    def vol_to_ext_surf_vol(self,
                            in_vol_path: os.PathLike,
                            labels: Optional[Union[numpy.ndarray,
                                                   list]] = None,
                            ctx: Optional[os.PathLike] = None,
                            out_vol_path: Optional[os.PathLike] = None,
                            labels_surf: Optional[Union[numpy.ndarray,
                                                        list]] = None,
                            labels_inner: str = '0'):
        """
        Separate the voxels of the outer surface of a structure, from the inner ones. Default behavior: surface voxels
        retain their label, inner voxels get the label 0, and the input file is overwritten by the output.
        """

        labels = self.annotation_service.read_input_labels(labels=labels,
                                                           ctx=ctx)
        number_of_labels = len(labels)
        # Set the labels for the surfaces
        if labels_surf is None:
            labels_surf = labels
        else:
            # Read the surface labels and make sure there is one for each label
            labels_surf = numpy.array(labels_surf.split()).astype('i')
            if len(labels_surf) == 1:
                labels_surf = numpy.repeat(labels_inner,
                                           number_of_labels).tolist()
            elif len(labels_surf) != number_of_labels:
                self.logger.warning(
                    "Output labels for surface voxels are neither of length "
                    "1 nor of length equal to the one of target labels.")
                return
            else:
                labels_surf = labels_surf.tolist()
        # Read the inner, non-surface labels
        labels_inner = numpy.array(labels_inner.split()).astype('i')
        # ...and make sure there is one for each label
        if len(labels_inner) == 1:
            labels_inner = numpy.repeat(labels_inner,
                                        number_of_labels).tolist()
        elif len(labels_inner) != number_of_labels:
            self.logger.warning(
                "Output labels for inner voxels are neither of length 1 nor "
                "of length equal to the one of the target labels.")
            return
        else:
            labels_inner = labels_inner.tolist()

        # Read the input volume...
        volume = IOUtils.read_volume(in_vol_path)

        # Neigbors' grid sharing a face
        eye3 = numpy.identity(3)
        border_grid = numpy.c_[eye3, -eye3].T.astype('i')
        n_border = 6

        out_volume = Volume(numpy.array(volume.data), volume.affine_matrix,
                            volume.header)

        # Initialize output indexes
        out_ijk = []

        for label_index in range(number_of_labels):
            current_label = labels[label_index]
            # Get the indexes of all voxels of this label:
            label_volxels_i, label_voxels_j, label_voxels_k = numpy.where(
                volume.data == current_label)
            # and for each voxel
            for voxel_index in range(label_volxels_i.size):
                # indexes of this voxel:
                current_voxel_i, current_voxel_j, current_voxel_k = \
                    label_volxels_i[voxel_index], label_voxels_j[
                        voxel_index], label_voxels_k[voxel_index]
                # Create the neighbors' grid sharing a face
                ijk_grid = border_grid + \
                           numpy.tile(numpy.array(
                               [current_voxel_i, current_voxel_j, current_voxel_k]), (n_border, 1))
                # Remove voxels outside the image
                indices_inside_image = numpy.all(
                    [(ijk_grid[:, 0] >= 0),
                     (ijk_grid[:, 0] < volume.dimensions[0]),
                     (ijk_grid[:, 1] >= 0),
                     (ijk_grid[:, 1] < volume.dimensions[1]),
                     (ijk_grid[:, 2] >= 0),
                     (ijk_grid[:, 2] < volume.dimensions[2])],
                    axis=0)
                ijk_grid = ijk_grid[indices_inside_image, :]
                try:
                    # If all face neighbors are of the same label...
                    if numpy.all(
                            volume.data[ijk_grid[:, 0], ijk_grid[:, 1],
                                        ijk_grid[:, 2]] == numpy.tile(
                                            volume.data[current_voxel_i,
                                                        current_voxel_j,
                                                        current_voxel_k], (
                                                            n_border, 1))):
                        # ...set this voxel to the corresponding inner target label
                        out_volume.data[
                            current_voxel_i, current_voxel_j,
                            current_voxel_k] = labels_inner[label_index]
                    else:
                        # ...set this voxel to the corresponding surface target label
                        out_volume.data[
                            current_voxel_i, current_voxel_j,
                            current_voxel_k] = labels_surf[label_index]
                        out_ijk.append([
                            current_voxel_i, current_voxel_j, current_voxel_k
                        ])
                except ValueError:  # empty grid
                    self.logger.error(
                        "Error at voxel ( %s, %s, %s ) of label %s: It appears to have no common-face "
                        "neighbors inside the image!", str(current_voxel_i),
                        str(current_voxel_j), str(current_voxel_k),
                        str(current_label))
                    return

        if out_vol_path is None:
            out_vol_path = in_vol_path

        IOUtils.write_volume(out_vol_path, out_volume)

        # save the output indexes that survived masking
        out_ijk = numpy.vstack(out_ijk)
        filepath = os.path.splitext(out_vol_path)[0]
        numpy.save(filepath + "-idx.npy", out_ijk)
        numpy.savetxt(filepath + "-idx.txt", out_ijk, fmt='%d')

    def mask_to_vol(self,
                    in_vol_path: os.PathLike,
                    mask_vol_path: os.PathLike,
                    out_vol_path: Optional[os.PathLike] = None,
                    labels: Optional[Union[numpy.ndarray, list]] = None,
                    ctx: Optional[str] = None,
                    vol2mask_path: Optional[os.PathLike] = None,
                    vn: int = 1,
                    th: float = 0.999,
                    labels_mask: Optional[os.PathLike] = None,
                    labels_nomask: str = '0'):
        """
        Identify the voxels that are neighbors within a voxel distance vn, to a mask volume, with a mask threshold of th
        Default behavior: we assume a binarized mask and set th=0.999, no neighbors search, only looking at the exact
        voxel position, i.e., vn=0. Accepted voxels retain their label, whereas rejected ones get a label of 0
        """

        # Set the target labels:
        labels = self.annotation_service.read_input_labels(labels=labels,
                                                           ctx=ctx)
        number_of_labels = len(labels)
        # Set the labels for the selected voxels
        if labels_mask is None:
            labels_mask = labels

        else:
            # Read the labels and make sure there is one for each label
            labels_mask = numpy.array(labels_mask.split()).astype('i')

            if len(labels_mask) == 1:
                labels_mask = numpy.repeat(labels_mask,
                                           number_of_labels).tolist()

            elif len(labels_mask) != number_of_labels:
                self.logger.warning(
                    "Output labels for selected voxels are neither of length 1 nor of length equal to "
                    "the one of target labels")
                return

            else:
                labels_mask = labels_mask.tolist()

        # Read the excluded labels and make sure there is one for each label
        labels_nomask = numpy.array(labels_nomask.split()).astype('i')
        if len(labels_nomask) == 1:
            labels_nomask = numpy.repeat(labels_nomask,
                                         number_of_labels).tolist()

        elif len(labels_nomask) != number_of_labels:
            self.logger.warning(
                "Output labels for excluded voxels are neither of length 1 nor of length equal to the "
                "one of the target labels")
            return

        else:
            labels_nomask = labels_nomask.tolist()

        volume = IOUtils.read_volume(in_vol_path)

        mask_vol = IOUtils.read_volume(mask_vol_path)

        # Compute the transform from vol ijk to mask ijk:
        ijk2ijk = numpy.identity(4)

        # If vol and mask are not in the same space:
        if os.path.exists(str(vol2mask_path)):
            # read the xyz2xyz transform and apply it to the inverse mask
            # affine transform to get an ijk2ijk transform.
            xyz2xyz = numpy.loadtxt(vol2mask_path)
            ijk2ijk = volume.affine_matrix.dot(
                numpy.dot(xyz2xyz, numpy.linalg.inv(mask_vol.affine_matrix)))

        # Construct a grid template of voxels +/- vn voxels around each ijk
        # voxel, sharing at least a corner
        grid = numpy.meshgrid(list(range(-vn, vn + 1, 1)),
                              list(range(-vn, vn + 1, 1)),
                              list(range(-vn, vn + 1, 1)),
                              indexing='ij')
        grid = numpy.c_[numpy.array(grid[0]).flatten(),
                        numpy.array(grid[1]).flatten(),
                        numpy.array(grid[2]).flatten()]
        n_grid = grid.shape[0]

        out_volume = Volume(numpy.array(volume.data), volume.affine_matrix,
                            volume.header)

        # Initialize output indexes
        out_ijk = []

        # For each target label:
        for label_index in range(number_of_labels):
            current_label = labels[label_index]
            # Get the indexes of all voxels of this label:
            label_voxels_i, label_voxels_j, label_voxels_k = numpy.where(
                volume.data == current_label)

            for voxel_index in range(label_voxels_i.size):
                current_voxel_i, current_voxel_j, current_voxel_k = \
                    label_voxels_i[voxel_index], label_voxels_j[
                        voxel_index], label_voxels_k[voxel_index]
                # TODO if necessary: deal with voxels at the edge of the image, such as brain stem ones...
                #     if any([(i==0), (i==mask_shape[0]-1),(j==0), (j==mask_shape[0]-1),(k==0), (k==mask_shape[0]-1)]):
                #               mask_shape[i,j,k]=0
                #               continue

                # ...get the corresponding voxel in the mask volume:
                ijk = numpy.round(
                    ijk2ijk.dot(
                        numpy.array([
                            current_voxel_i, current_voxel_j, current_voxel_k,
                            1
                        ]))[:3]).astype('i')

                # Make sure this point is within image limits
                for cc in range(3):
                    if ijk[cc] < 0:
                        ijk[cc] = 0

                    elif ijk[cc] >= mask_vol.dimensions[cc]:
                        ijk[cc] = mask_vol.dimensions[cc] - 1

                # If this is a voxel to keep, set it so...
                if mask_vol.data[ijk[0], ijk[1], ijk[2]] >= th:
                    out_volume.data[current_voxel_i, current_voxel_j,
                                    current_voxel_k] = labels_mask[label_index]
                    out_ijk.append(
                        [current_voxel_i, current_voxel_j, current_voxel_k])

                elif vn > 0:
                    # If not, and as long as vn>0 check whether any of its vn neighbors is a mask voxel.
                    # Generate the specific grid centered at the vertex ijk
                    ijk_grid = grid + numpy.tile(ijk, (n_grid, 1))

                    # Remove voxels outside the mask volume
                    indexes_within_limits = numpy.all(
                        [(ijk_grid[:, 0] >= 0),
                         (ijk_grid[:, 0] < mask_vol.dimensions[0]),
                         (ijk_grid[:, 1] >= 0),
                         (ijk_grid[:, 1] < mask_vol.dimensions[1]),
                         (ijk_grid[:, 2] >= 0),
                         (ijk_grid[:, 2] < mask_vol.dimensions[2])],
                        axis=0)
                    ijk_grid = ijk_grid[indexes_within_limits, :]

                    try:
                        # If none of these points is a mask point:
                        if (mask_vol.data[ijk_grid[:, 0], ijk_grid[:, 1],
                                          ijk_grid[:, 2]] < th).all():
                            out_volume.data[
                                current_voxel_i, current_voxel_j,
                                current_voxel_k] = labels_nomask[label_index]

                        else:  # if any of them is a mask point:
                            out_volume.data[
                                current_voxel_i, current_voxel_j,
                                current_voxel_k] = labels_mask[label_index]
                            out_ijk.append([
                                current_voxel_i, current_voxel_j,
                                current_voxel_k
                            ])

                    except ValueError:  # empty grid
                        self.logger.error(
                            "Error at voxel ( %s, %s, %s ): It appears to have no common-face neighbors "
                            "inside the image!", str(current_voxel_i),
                            str(current_voxel_j), str(current_voxel_k))
                        return

                else:
                    out_volume.data[
                        current_voxel_i, current_voxel_j,
                        current_voxel_k] = labels_nomask[label_index]

        if out_vol_path is None:
            out_vol_path = in_vol_path

        IOUtils.write_volume(out_vol_path, out_volume)

        # Save the output indexes that survived masking
        out_ijk = numpy.vstack(out_ijk)
        filepath = os.path.splitext(out_vol_path)[0]
        numpy.save(filepath + "-idx.npy", out_ijk)
        numpy.savetxt(filepath + "-idx.txt", out_ijk, fmt='%d')

    def vol_val_xyz(self, vol: numpy.ndarray, aff: numpy.ndarray,
                    val: float) -> numpy.ndarray:
        vox_idx = numpy.argwhere(vol == val)
        xyz = aff.dot(numpy.c_[vox_idx, numpy.ones(vox_idx.shape[0])].T)[:3].T
        return xyz

    def compute_label_volume_centers(self, label_volume: numpy.ndarray,
                                     affine: numpy.ndarray):

        for val in numpy.unique(label_volume):
            xyz = self.vol_val_xyz(label_volume, affine, val)
            x, y, z = xyz.mean(axis=0)
            yield val, (x, y, z)

    def label_with_dilation(self, to_label_nii_fname: os.PathLike,
                            dilated_nii_fname: os.PathLike,
                            out_nii_fname: os.PathLike):
        """
        Labels a volume using its labeled dilation. The dilated volume is labeled using scipy.ndimage.label function.
        :param to_label_nii_fname: usually a CT-mask.nii.gz
        :param dilated_nii_fname: dilated version of the to_label_nii_fname volume
        """

        # TODO could make dilation with ndimage also.
        mask = IOUtils.read_volume(to_label_nii_fname)
        dil_mask = IOUtils.read_volume(dilated_nii_fname)

        lab, n = scipy.ndimage.label(dil_mask.data)

        # TODO: this change is from tvb-make. Keep it or not? It returns a different result than the old version.
        lab_xyz = list(
            self.compute_label_volume_centers(lab, dil_mask.affine_matrix))
        lab_sort = numpy.r_[:n + 1]
        # sort labels along AP axis
        for i, (val, _) in enumerate(sorted(lab_xyz, key=lambda t: t[1][1])):
            lab_sort[val] = i
        lab = lab_sort[lab]

        mask.data *= lab
        self.logger.info('%d objects found when labeling the dilated volume.',
                         n)

        IOUtils.write_volume(out_nii_fname, mask)

    def _label_config(
            self,
            aparc: nibabel.nifti1.Nifti1Image) -> nibabel.nifti1.Nifti1Image:
        unique_data = numpy.unique(aparc.data)
        unique_data_map = numpy.r_[:unique_data.max() + 1]
        unique_data_map[unique_data] = numpy.r_[:unique_data.size]
        aparc.data = unique_data_map[aparc.data]
        return aparc

    def simple_label_config(self, in_aparc_path: os.PathLike,
                            out_volume_path: os.PathLike):
        """
        Relabel volume to have contiguous values like Mrtrix' labelconfig.
        :param in_aparc_path: volume voxel value is the index of the region it belongs to.
        :return: writes the labeled volume to out_volume_path.
        """

        aparc = IOUtils.read_volume(in_aparc_path)
        aparc = self._label_config(aparc)
        IOUtils.write_volume(out_volume_path, aparc)

    def _label_volume(self,
                      tdi_volume: nibabel.nifti1.Nifti1Image,
                      lo: float = 0.5) -> nibabel.nifti1.Nifti1Image:
        mask = tdi_volume.data > lo
        tdi_volume.data[~mask] = 0
        tdi_volume.data[mask] = numpy.r_[1:mask.sum() + 1]
        return tdi_volume

    def label_vol_from_tdi(self,
                           tdi_volume_path: os.PathLike,
                           out_volume_path: os.PathLike,
                           lo: float = 0.5):
        """
        Creates a mask of the voxels with tract ends > lo and any other voxels become 0.
        Labels each voxel different from 0 with integer labels starting from 1.
        :param tdi_volume_path: volume voxel value is the sum of tract ends. Voxel without tract ends has value 0.
        :param lo: tract ends threshold used for masking.
        :return: writes labeled volume to :ut_volume_path.
        """

        nii_volume = IOUtils.read_volume(tdi_volume_path)
        tdi_volume = self._label_volume(nii_volume, lo)
        IOUtils.write_volume(out_volume_path, tdi_volume)

    def remove_zero_connectivity_nodes(
            self,
            node_volume_path: os.PathLike,
            connectivity_matrix_path: os.PathLike,
            tract_length_path: Optional[str] = None):
        """
        It removes network nodes with zero connectivity from the volume and connectivity matrices.
        The zero connectivity nodes will be labeled with 0 in the volume and the remaining labels will be updated.
        The connectivity matrices will be symmetric.
        :param node_volume_path: tdi_lbl.nii volume path
        :param connectivity_matrix_path: .csv file, output of Mrtrix3 tck2connectome
        :param tract_length_path: optional .csv tract lengths matrix
        :return: overwrites the input volume and matrices with the processed ones. Also saves matrices as .npy.
        """

        node_volume = IOUtils.read_volume(node_volume_path)

        connectivity = numpy.array(
            numpy.genfromtxt(connectivity_matrix_path, dtype='int64'))
        connectivity = connectivity + connectivity.T
        connectivity_row_sum = numpy.sum(connectivity, axis=0)

        nodes_to_keep_indices = connectivity_row_sum > 0
        connectivity = connectivity[
            nodes_to_keep_indices, :][:, nodes_to_keep_indices]

        numpy.save(
            os.path.splitext(connectivity_matrix_path)[0] + NPY_EXTENSION,
            connectivity)
        numpy.savetxt(connectivity_matrix_path, connectivity, fmt='%1d')

        if os.path.exists(str(tract_length_path)):
            connectivity = numpy.array(
                numpy.genfromtxt(tract_length_path, dtype='int64'))
            connectivity = connectivity[
                nodes_to_keep_indices, :][:, nodes_to_keep_indices]

            numpy.save(
                os.path.splitext(tract_length_path)[0] + NPY_EXTENSION,
                connectivity)
            numpy.savetxt(tract_length_path, connectivity, fmt='%1d')

        else:
            self.logger.warning("Path %s is not valid.", tract_length_path)

        nodes_to_remove_indices, = numpy.where(~nodes_to_keep_indices)
        nodes_to_remove_indices += 1

        for node_index in nodes_to_remove_indices:
            node_volume.data[node_volume.data == node_index] = 0

        node_volume.data[node_volume.data > 0] = numpy.r_[1:(
            connectivity.shape[0] + 1)]

        IOUtils.write_volume(node_volume_path, node_volume)

    def con_vox_in_ras(
            self, ref_vol_path: os.PathLike) -> (numpy.ndarray, numpy.ndarray):
        """
        This function reads a tdi_lbl volume and returns the voxels that correspond to connectome nodes,
        and their coordinates in ras space, simply by applying the affine transform of the volume
        :param ref_vol_path: the path to the tdi_lbl volume
        :return: vox and voxxyz,
                i.e., the labels (integers>=1) and the coordinates of the connnectome nodes-voxels, respectively
        """
        # Read the reference tdi_lbl volume:
        vollbl = IOUtils.read_volume(ref_vol_path)
        vox = vollbl.data.astype('i')
        # Get only the voxels that correspond to connectome nodes:
        voxijk, = numpy.where(vox.flatten() > 0)
        voxijk = numpy.unravel_index(voxijk, vollbl.dimensions)
        vox = vox[voxijk[0], voxijk[1], voxijk[2]]
        # ...and their coordinates in ras xyz space
        voxxzy = vollbl.affine_matrix.dot(
            numpy.c_[voxijk[0], voxijk[1], voxijk[2],
                     numpy.ones(vox.shape[0])].T)[:3].T
        return vox, voxxzy

    def change_labels_of_aparc_aseg(self, atlas_suffix, volume, mapping_dict,
                                    conn_regs_nr):
        if atlas_suffix == AtlasSuffix.A2009S:
            volume.data[volume.data == 1000] = 11100
            volume.data[volume.data == 2000] = 12100
        not_matched = set()
        for i in range(volume.data.shape[0]):
            for j in range(volume.data.shape[1]):
                for k in range(volume.data.shape[2]):
                    val = volume.data[i][j][k]
                    if not val in mapping_dict:
                        not_matched.add(val)
                    volume.data[i][j][k] = mapping_dict.get(val, -1)

        print("Now values are in interval [%d - %d]" %
              (volume.data.min(), volume.data.max()))

        if not_matched:
            print("Not matched regions will be considered background: %s" %
                  not_matched)
        assert (volume.data.min() >= -1 and volume.data.max() < conn_regs_nr)

        return volume

    def transform_coords(self, coords: Union[list, numpy.ndarray, str], src_img: os.PathLike, dest_img: os.PathLike,
                  transform_mat: os.PathLike, output_file: Optional[str]=None) \
            -> (numpy.array, Union[numpy.ndarray, type(None)]):

        if os.path.isfile(coords):
            command = "img2imgcoord %s -mm -src %s -dest %s -xfm %s"
            args = [coords, src_img, dest_img, transform_mat]
        else:
            coords_str = " ".join([str(x) for x in coords])
            command = "echo %s | img2imgcoord -mm -src %s -dest %s -xfm %s"
            args = [coords_str, src_img, dest_img, transform_mat]

        if os.path.isdir(os.path.dirname(output_file)):
            command += " > %s"
            args.append(output_file)

        output, std_out, time = \
            execute_command(command % tuple(args), cwd=os.path.dirname(transform_mat), shell=True)

        transformed_coords_str = output.strip().split('\n')[-1]

        return numpy.array([
            float(x) for x in transformed_coords_str.split(" ") if x
        ]), output_file
コード例 #9
0
ファイル: surface.py プロジェクト: maedoc/tvb-virtualizer
class SurfaceService(object):
    logger = get_logger(__name__)

    def __init__(self):
        self.annotation_service = AnnotationService()

    # #TODO: transfer the following two to some general services?:
    # def mask2index(self,mask):
    #     """
    #     Convert a mask to an array of indices
    #     :param mask:
    #     :return:
    #     """
    #     return numpy.where(mask)
    #
    # def index2mask(self,index):
    #     """
    #     Convert an array of indices to the corresponding mask
    #     :param index:
    #     :return:
    #     """
    #     index=numpy.unique(index)
    #     mask=numpy.zeros((len(index,))).astype('bool')
    #     mask[index]=True
    #     return mask

    def tri_area(self, tri: numpy.ndarray) -> numpy.ndarray:
        i, j, k = numpy.transpose(tri, (1, 0, 2))
        ij = j - i
        ik = k - i
        return numpy.sqrt(numpy.sum(numpy.cross(ij, ik) ** 2, axis=1)) / 2.0

    def convert_fs_to_brain_visa(self, in_surf_path: str, out_surf_path: Optional[str]=None):
        surface = IOUtils.read_surface(in_surf_path, False)
        if out_surf_path is None:
            out_surf_path = in_surf_path + '.tri'
        IOUtils.write_surface(out_surf_path, surface)

    def convert_bem_to_tri(self, surfaces_directory_path: str):
        surfs_glob = '%s/*_surface-low' % (surfaces_directory_path)
        for surf_name in glob.glob(surfs_glob):
            self.convert_fs_to_brain_visa(surf_name)

    def merge_surfaces(self, surfaces: Surface) -> Surface:
        """
        Merge several surfaces, and their region mappings.
        :return: the merge result surface and region mapping.
        """
        n_surfaces = len(surfaces)
        out_surface = Surface([], [])
        # TODO: how to deal with the metadata of merged surfaces, so that freesurfer.io can handle them, e.g., write them
        # i.e., we need to have a final unique version of the metadata, not a list of them, as I am doing here in the
        # commented code
        # out_surface_attributes=dict()
        # for attribute in ["vertices_coord_system", "generic_metadata", "vertices_metadata", "triangles_metadata"]:
        #     out_surface_attributes[attribute]=[]
        for i_srf in range(n_surfaces):
            out_surface.add_vertices_and_triangles(surfaces[i_srf].vertices,
                                                   surfaces[i_srf].triangles,
                                                   surfaces[i_srf].area_mask)
            if out_surface.get_main_metadata() is None:
                out_surface.set_main_metadata(surfaces[i_srf].get_main_metadata())
            if len(surfaces[i_srf].center_ras) == 0:
                pass
            elif len(out_surface.center_ras) == 0:
                out_surface.center_ras = surfaces[i_srf].center_ras
            elif numpy.any(out_surface.center_ras != surfaces[i_srf].center_ras):
                self.logger.warn("At least two surfaces have different -non empty- centers in RAS coordinates!")
            # #TODO: think about how to better merge these fields
            # for attribute in ["vertices_coord_system", "generic_metadata", "vertices_metadata", "triangles_metadata"]:
            #     out_surface_attributes[attribute].append(getattr(surfaces[i_srf],attribute))
            #     setattr(out_surface,attribute,out_surface_attributes[attribute])
        return out_surface

    def compute_gdist_mat(self, surf_name: str='pial', max_distance: float=40.0) -> numpy.ndarray:
        max_distance = float(max_distance)  # in case passed from sys.argv
        for h in 'rl':
            subjects_dir = os.environ['SUBJECTS_DIR']
            subject = os.environ['SUBJECT']
            surf_path = '%s/%s/surf/%sh.%s' % (subjects_dir,
                                               subject, h, surf_name)
            surface = IOUtils.read_surface(surf_path, False)
            mat_path = '%s/%s/surf/%sh.%s.gdist.mat' % (
                subjects_dir, subject, h, surf_name)
            mat = gdist.local_gdist_matrix(
                surface.vertices, surface.triangles.astype('<i4'), max_distance=max_distance)
            scipy.io.savemat(mat_path, {'gdist': mat})

            return mat

    # TODO: maybe create a new "connectome" service and transfer this function there
    # TODO: add more normalizations modes
    def compute_geodesic_dist_affinity(self, dist: numpy.ndarray, norm: bool=False) -> numpy.ndarray:
        """
        This function calculates geodesic distances among nodes of a mesh,
        starting from the array of the distances between directly connected nodes.
        Optionally, normalization with the maximum geodesic distances is performed.
        Infinite distances (corresponding to disconnected components of the mesh, are not allowed)
        :param dist: a dense array of distances between directly connected nodes of a mesh/network
        :param norm: a flag to be currently used for optional normalization with the maximum geodesic distance

        :return:
        """
        # TODO: make sure that this returns a symmetric matrix!
        geodist = shortest_path(dist, method='auto', directed=False,
                                return_predecessors=False, unweighted=False, overwrite=False).astype('single')
        # Find the maximum non infinite geodesic distance:
        max_gdist = numpy.max(geodist, axis=None)
        assert numpy.isfinite(max_gdist)
        if norm:
            geodist /= max_gdist
        # Convert them to normalized distances and return them
        return geodist

    def extract_subsurf(self, surface: Surface, verts_mask: Union[numpy.ndarray, list], output: str='surface')\
            -> Surface:
        """
        Extracts a sub-surface that contains only the masked vertices and the corresponding faces.
        An important step is to replace old vertices indexes of faces to the new ones.
        :param: surface: input surface object
        :param: verts_mask: mask of the sub-surface to be extracted
        :return: output surface object
        """

        verts_out = surface.vertices[verts_mask]
        triangles_mask = numpy.c_[verts_mask[surface.triangles[:, 0]],
                                  verts_mask[surface.triangles[:, 1]],
                                  verts_mask[surface.triangles[:, 2]]].all(axis=1)
        triangles_out = numpy.array(surface.triangles[triangles_mask, :])
        verts_out_inds, = numpy.where(verts_mask)
        for triang_idx in range(triangles_out.shape[0]):
            for vertex_idx in range(3):
                triangles_out[triang_idx, vertex_idx], = \
                    numpy.where(
                        triangles_out[triang_idx, vertex_idx] == verts_out_inds)
        if output == 'surface':
            out_surface = Surface(verts_out, triangles_out, area_mask=surface.area_mask[verts_out_inds],
                                  center_ras=surface.center_ras, vertices_coord_system=surface.vertices_coord_system,
                                  generic_metadata=surface.generic_metadata,
                                  vertices_metadata=surface.vertices_metadata,
                                  triangles_metadata=surface.triangles_metadata)
            return out_surface
        else:
            return (verts_out, triangles_out,
                    surface.area_mask[verts_out_inds])

    def compute_surface_area(self, surface: Surface, area_mask: Optional[Union[numpy.ndarray, list]]=None):
        """
            This function computes the surface area, after optionally applying a mask to choose a sub-surface
            :param: surface: input surface object
            :param area_mask: optional boolean mask (number of vertices x ) to overwrite the surface.area_mask
            :return: (sub)surface area, float
            """
        if area_mask is None:
            area_mask = surface.area_mask
        # Apply the mask in order to extract the sub-surface (vertices and
        # relevant triangles)
        (vertices, triangles) = self.extract_subsurf(
            surface, area_mask, output='verts_triangls')[:2]
        return numpy.sum(self.tri_area(vertices[triangles]))

    def vertex_connectivity(self, surface: Surface, mode: str="sparse", metric: Optional[str]=None,
                            symmetric: bool=False, verts_mask: Union[numpy.ndarray, list]=None) \
            -> Union[numpy.ndarray, scipy.sparse.csr.csr_matrix]:
        """
        It computes a sparse matrix of the connectivity among the vertices of a surface.
        :param surface: input surface object
        :param mode: "sparse" by default or "2D"
        :param metric: None by default, could be "euclidean"
        :param symmetric: True for symmetric matrix output
        :param verts_mask: a mask to apply the method to a a sub-surface of the original surface
        :return: the computed matrix.
        """
        if verts_mask is not None:
            (vertices, triangles) = self.extract_subsurf(
                surface, verts_mask, output='verts_triangls')[:2]
        else:
            vertices = surface.vertices
            triangles = surface.triangles
        # Get all pairs of vertex indexes (i.e., edges) that appear in each
        # face (triangle)
        edges = numpy.r_[triangles[:, [0, 1]],
                         triangles[:, [1, 2]], triangles[:, [2, 0]]]
        # Remove repetitions
        edges = numpy.vstack(set(map(tuple, edges)))
        # Mark all existing pairs to 1
        n_v = vertices.shape[0]
        n_e = edges.shape[0]
        # For symmetric output...
        if symmetric:
            # ...create for the moment the "double" edges
            edges2 = numpy.r_[edges, edges[:, [1, 0]]]
        if metric is None:
            # For symmetric output...
            if symmetric:
                # ...remove repetitions of edges2
                edges = numpy.vstack(set(map(tuple, edges2)))
                n_e = edges.shape[0]
            con = csr_matrix(
                (numpy.ones((n_e,)), (edges[:, 0], edges[:, 1])), shape=(n_v, n_v))
            if mode != "sparse":
                # Create non-sparse matrix
                con = con.todense()
        else:
            d = paired_distances(vertices[edges[:, 0]], vertices[
                edges[:, 1]], metric)
            # For symmetric output...
            if symmetric:
                # double also d...
                d = numpy.r_[d, d]
                edges = edges2
            if mode == "sparse":
                # Create sparse matrix
                con = csr_matrix(
                    (d, (edges[:, 0], edges[:, 1])), shape=(n_v, n_v))
        return con

    # TODO: use surface instead of verts and faces?? Denis: not sure about
    # this!..
    def connected_surface_components(self, surface: Optional[Surface]=None, connectivity: Optional[numpy.ndarray]=None,
                                     verts_mask: Optional[Union[numpy.ndarray, list]]=None) \
            -> (int, numpy.ndarray,  numpy.ndarray):
        """
        This function returns all the different disconnected components of a surface, their number and their areas,
        after applying an optional boolean mask to exclude some subsurface from the whole computation.
        There should be at least one component returned, if the whole surface is connected.
        :param surface: input surface object
        :param connectivity: optionally an array or sparse matrix of structural connectivity constraints,
                            where True or 1 or entry>0 stands for the existing direct connections
                            among neighboring vertices (i.e., vertices of a common triangular face)
        :param verts_mask: optional boolean mask (number of vertices x ) for vertices to include to the input surface
        :return:
        """
        if (surface is None) and connectivity is None:
            print("Error: neither a surface, nor a connectivity matrix in the input!")
            return 0
        elif connectivity is None:
            n_verts = surface.vertices.shape[0]
            # Create the connectivity matrix, if not in the input:
            connectivity = self.vertex_connectivity(
                surface, verts_mask=verts_mask)
            if verts_mask is None:
                verts_mask = numpy.ones((n_verts,), dtype=bool)
        else:
            n_verts = connectivity.shape[0]
            if verts_mask is None:
                verts_mask = numpy.ones((n_verts,), dtype=bool)
            else:
                connectivity = connectivity[verts_mask, :][:, verts_mask]
        # Find all connected components of this surface
        (n_components, components_masked) = \
            connected_components(connectivity, directed=False,
                                 connection='weak', return_labels=True)
        comp_area = []
        if surface is not None:
            # For each component...
            for ic in range(n_components):
                i_comp_verts = components_masked == ic
                # ...compute the surface area, after applying any specified mask
                comp_area.append(self.compute_surface_area(surface,
                                                           mask=numpy.logical_and(i_comp_verts, surface.area_mask)))
        # Prepare final components' labels output:
        components = -numpy.ones((n_verts,)).astype('i')
        components[verts_mask] = components_masked
        return n_components, components, numpy.array(comp_area)

    def aseg_surf_conc_annot(self, surf_path: str, out_surf_path: str, annot_path: str,
                             label_indices: Union[numpy.ndarray, list], lut_path: Optional[str]=None) -> Surface:
        """
        Concatenate surfaces of one specific label of interest each, to create a single annotated surface.
        """

        lut_path = lut_path or default_lut_path()

        label_names, color_table = self.annotation_service.lut_to_annot_names_ctab(lut_path=lut_path,
                                                                                   labels=label_indices)
        label_indices = numpy.array(label_indices.split()).astype('i')

        #                  verts tri area_mask cras
        surfaces = []
        out_annotation = Annotation([], [], [])
        label_number = -1

        for label_index in label_indices:
            # TODO: This is hardcoded: /aseg-%06d here and also in pegasus dax generator
            this_surf_path = surf_path + "/aseg-%06d" % int(label_index)

            if os.path.exists(this_surf_path):
                ind_l, = numpy.where(label_indices == label_index)
                out_annotation.add_region_names_and_colors(
                    label_names[int(ind_l)],
                    color_table[ind_l, :])
                label_number += 1
                surfaces.append(IOUtils.read_surface(this_surf_path, False))
                out_annotation.add_region_mapping(
                    label_number * numpy.ones((surfaces[-1].n_vertices,), dtype='int64'))
        out_surface = self.merge_surfaces(surfaces)
        # out_annotation.regions_color_table = numpy.squeeze(numpy.array(out_annotation.regions_color_table).astype('i'))

        IOUtils.write_surface(out_surf_path, out_surface)
        IOUtils.write_annotation(annot_path, out_annotation)

        return out_surface

    def __prepare_grid(self, vertex_neighbourhood: int) -> (numpy.ndarray, int):
        # Prepare grid if needed for possible use:
        if vertex_neighbourhood > 0:
            grid = numpy.meshgrid(list(range(-vertex_neighbourhood, vertex_neighbourhood + 1, 1)),
                                  list(range(-vertex_neighbourhood,
                                             vertex_neighbourhood + 1, 1)),
                                  list(range(-vertex_neighbourhood, vertex_neighbourhood + 1, 1)), indexing='ij')
            grid = numpy.c_[
                numpy.array(grid[0]).flatten(), numpy.array(grid[1]).flatten(), numpy.array(grid[2]).flatten()]
            n_grid = grid.shape[0]

            return grid, n_grid

    def sample_vol_on_surf(self, surf_path: str, vol_path: str, annot_path: str, out_surf_path: str,
                           cras_path: str, add_string: str='', vertex_neighbourhood: int=1,
                           add_lbl: list=[], lut_path: Optional[str]=None) -> (Surface, Annotation):
        """
        Sample a volume of a specific label on a surface, by keeping only those surface vertices, the nearest voxel of
        which is of the given label (+ of possibly additional target labels, such as white matter).
        Allow optionally for vertices within a given voxel distance vn from the target voxels.
        """

        lut_path = lut_path or default_lut_path()

        # Read the inputs
        surface = IOUtils.read_surface(surf_path, False)

        annotation = IOUtils.read_annotation(annot_path)
        labels = self.annotation_service.annot_names_to_labels(annotation.region_names,
                                                               add_string=add_string, lut_path=lut_path)
        region_mapping_indexes = numpy.unique(annotation.region_mapping)

        volume_parser = VolumeIO()
        volume = volume_parser.read(vol_path)
        ras2vox_affine_matrix = numpy.linalg.inv(volume.affine_matrix)

        cras = numpy.loadtxt(cras_path)

        grid, n_grid = self.__prepare_grid(vertex_neighbourhood)

        # Initialize the output mask:
        verts_out_mask = numpy.repeat([False], surface.vertices.shape[0])
        for label_index in range(len(region_mapping_indexes)):

            self.logger.info("%s", add_string +
                             annotation.region_names[label_index])

            # Get the indexes of the vertices corresponding to this label:
            verts_indices_of_label, = numpy.where(
                annotation.region_mapping[:] == region_mapping_indexes[label_index])
            verts_indices_of_label_size = verts_indices_of_label.size
            if verts_indices_of_label_size == 0:
                continue

            # Add any additional labels
            all_labels = [labels[label_index]] + add_lbl

            # get the vertices for current label and add cras to take them to
            # scanner ras
            verts_of_label = surface.vertices[verts_indices_of_label, :]
            verts_of_label += numpy.repeat(numpy.expand_dims(
                cras, 1).T, verts_indices_of_label_size, axis=0)

            # Compute the nearest voxel coordinates using the affine transform
            ijk = numpy.round(
                ras2vox_affine_matrix.dot(numpy.c_[verts_of_label, numpy.ones(verts_indices_of_label_size)].T)[:3].T) \
                .astype('i')

            # Get the labels of these voxels:
            surf_vxls = volume.data[ijk[:, 0], ijk[:, 1], ijk[:, 2]]

            # Vertex mask to keep: those that correspond to voxels of one of
            # the target labels
            # surf_vxls==lbl if only one target label
            verts_keep, = numpy.where(numpy.in1d(surf_vxls, all_labels))
            verts_out_mask[verts_indices_of_label[verts_keep]] = True

            if vertex_neighbourhood > 0:
                # These are now the remaining indexes to be checked for
                # neighboring voxels
                verts_indices_of_label = numpy.delete(
                    verts_indices_of_label, verts_keep)
                ijk = numpy.delete(ijk, verts_keep, axis=0)

                for vertex_index in range(verts_indices_of_label.size):
                    # Generate the specific grid centered at the voxel ijk
                    ijk_grid = grid + \
                               numpy.tile(ijk[vertex_index, :], (n_grid, 1))

                    # Remove voxels outside the volume
                    indexes_within_limits = numpy.all([(ijk_grid[:, 0] >= 0), (ijk_grid[:, 0] < volume.dimensions[0]),
                                                       (ijk_grid[:, 1] >= 0), (ijk_grid[
                                                                               :, 1] < volume.dimensions[1]),
                                                       (ijk_grid[:, 2] >= 0), (ijk_grid[:, 2] < volume.dimensions[2])],
                                                      axis=0)
                    ijk_grid = ijk_grid[indexes_within_limits, :]
                    surf_vxls = volume.data[
                        ijk_grid[:, 0], ijk_grid[:, 1], ijk_grid[:, 2]]

                    # If any of the neighbors is of the target labels include
                    # the current vertex
                    # surf_vxls==lbl if only one target label
                    if numpy.any(numpy.in1d(surf_vxls, all_labels)):
                        verts_out_mask[
                            verts_indices_of_label[vertex_index]] = True

        # Vertex indexes and vertices to keep:
        verts_out_indices, = numpy.where(verts_out_mask)
        verts_out = surface.vertices[verts_out_indices]

        # TODO maybe: make sure that all voxels of this label correspond to at least one vertex.
        # Create a similar mask for faces by picking only triangles of which
        # all 3 vertices are included
        face_out_mask = numpy.c_[
            verts_out_mask[surface.triangles[:, 0]], verts_out_mask[surface.triangles[:, 1]], verts_out_mask[
                surface.triangles[:, 2]]].all(axis=1)
        faces_out = surface.triangles[face_out_mask]

        # The old vertices' indexes of faces have to be transformed to the new
        # vrtx_out_inds:
        for iF in range(faces_out.shape[0]):
            for vertex_index in range(3):
                faces_out[iF, vertex_index], = numpy.where(
                    faces_out[iF, vertex_index] == verts_out_indices)

        surface.vertices = verts_out
        surface.triangles = faces_out

        # Write the output surfaces and annotations to files. Also write files
        # with the indexes of vertices to keep.
        IOUtils.write_surface(out_surf_path, surface)

        annotation.set_region_mapping(
            annotation.get_region_mapping_by_indices([verts_out_indices]))
        IOUtils.write_annotation(out_surf_path + ".annot", annotation)

        numpy.save(out_surf_path + "-idx.npy", verts_out_indices)
        numpy.savetxt(out_surf_path + "-idx.txt", verts_out_indices, fmt='%d')

        return surface, annotation

    # TODO: maybe create a new "connectome" service and transfer this function
    # there
    def compute_consim_affinity(self, verts: numpy.ndarray, vox: Union[numpy.ndarray, list], voxxzy: numpy.ndarray,
                                con: numpy.ndarray, cras: Optional[Union[numpy.ndarray, list]]=None) -> numpy.ndarray:
        """
        This function creates a connectome affinity matrix among vertices,
        starting from an affinity matrix among voxels,
        by assignment from the nearest neighboring voxel to the respective vertex.
        :param verts: vertices' coordinates array (number of vertices x 3)
        :param vox: labels of connectome nodes-voxels (integers>=1)
        :param voxxzy: coordinates of the connectome nodes-voxels in ras space
        :param con: connectivity affinity matrix
        :param cras: center ras point to be optionally added to the vertices coordinates
                    (being probably in freesurfer tk-ras or surface ras coordinates) to align with the volume voxels
        :return: the affinity matrix among vertices
        """
        # Add the cras to take them to scanner ras coordinates, if necessary:
        if cras is not None:
            verts += numpy.repeat(numpy.expand_dims(cras,
                                                    1).T, verts.shape[0], axis=0)
        # TODO?: to use aparc+aseg to correspond vertices only to voxels of the same label
        # There would have to be a vertex->voxel of aparc+aseg of the same label -> voxel of tdi_lbl_in_T1 mapping
        # Maybe redundant  because we might be ending to the same voxel of tdi_lbl anyway...
        # Something to test/discuss...
        # Find for each vertex the closest voxel node in terms of euclidean
        # distance:
        v2n = numpy.argmin(cdist(verts, voxxzy, 'euclidean'), axis=1)
        # Assign to each vertex the integer identity of the nearest voxel node.
        v2n = vox[v2n]
        print("...surface component's vertices correspond to " +
              str(numpy.size(numpy.unique(v2n))) + " distinct voxel nodes")
        affinity = con[v2n - 1, :][:, v2n - 1]
        return affinity

    # TODO: keep the commented methods definition in py3
    def compute_areas_for_regions(self, regions: list, surface: Surface, region_mapping: list) -> numpy.array:
        """Compute the areas of given regions"""

        region_surface_area = numpy.zeros(len(regions))
        avt = numpy.array(surface.get_vertex_triangles())
        # NOTE: Slightly overestimates as it counts overlapping border triangles,
        #       but, not really a problem provided triangle-size << region-size.
        for i, k in enumerate(regions):
            regs = list(map(set, avt[numpy.array(region_mapping) == k]))
            if len(regs) == 0:
                continue
            region_triangles = set.union(*regs)
            if region_triangles:
                region_surface_area[i] = surface.get_triangle_areas()[list(region_triangles)].sum()

        return region_surface_area

    def compute_orientations_for_regions(self, regions, surface, region_mapping) -> numpy.ndarray:
        """Compute the orientation of given regions from vertex_normals and region mapping"""

        average_orientation = numpy.zeros((len(regions), 3))
        vertex_normals = surface.vertex_normals()
        # Average orientation of the region
        for i, k in enumerate(regions):
            orient = vertex_normals[numpy.array(region_mapping) == k, :]
            if orient.shape[0] > 0:
                avg_orient = numpy.mean(orient, axis=0)
                average_orientation[i, :] = avg_orient / numpy.sqrt(numpy.sum(avg_orient ** 2))

        return average_orientation

    def compute_centers_for_regions(self, regions, surface, region_mapping) -> numpy.ndarray:
        region_centers = numpy.zeros((len(regions), 3))
        for i, k in enumerate(regions):
            vert = surface.vertices[numpy.array(region_mapping) == k, :]
            if vert.shape[0] > 0:
                region_centers[i, :] = numpy.mean(vert, axis=0)

        return region_centers