Working with Surface images

Here we explain how surface images are represented within Nilearn and how you can plot, save and load them.

What is a surface image?

Within the context of neuroimaging, a surface image is an alternative way of representing MRI data as opposed to a volumetric image.

While volumetric images are 3D grids of voxels, surface images consist of points (vertices) in 3D space connected to represent the surface of the brain.

Practically, this means that the main difference between the two is the basic unit that holds the data. For volumetric images, that basic unit is a voxel, while for surface images it is a vertex.

The goal of this tutorial is to show you how to work with surface images in Nilearn. For more existential questions like why surface images are useful, how they are created etc., Andy Jahn’s blog is a good starting point.

Surface images have two main components:
  1. The mesh, which is the geometry of the surface.

  2. The data, which is the information stored at each vertex of the mesh.

Mesh

A mesh can be defined by two arrays:
  1. The coordinates of the vertices.

  2. Which vertices need to be connected to form faces.

Note

This representation of a mesh is known as Face-Vertex representation.

For brain surfaces we typically have two meshes: one for the left hemisphere and one for the right hemisphere. Nilearn represents this as a PolyMesh object with two parts: left and right.

So you can define your own mesh, say, for the left part a tetrahedron and for the right part a pyramid, using numpy arrays and create a PolyMesh object as follows:

import numpy as np

from nilearn.surface import InMemoryMesh, PolyMesh

# for the tetrahedron
left_coords = np.asarray(
    [
        [0, 0, 0],  # vertex 0
        [1, 0, 0],  # vertex 1
        [0, 1, 0],  # vertex 2
        [0, 0, 1],  # vertex 3
    ]
)
left_faces = np.asarray(
    [
        [1, 0, 2],  # face created by connecting vertices 1, 0, 2
        [0, 1, 3],  # face created by connecting vertices 0, 1, 3
        [0, 3, 2],  # face created by connecting vertices 0, 3, 2
        [1, 2, 3],  # face created by connecting vertices 1, 2, 3
    ]
)
# for the pyramid
right_coords = (
    np.asarray([[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], [0, 0, 1]]) + 2
)
right_faces = np.asarray(
    [
        [0, 1, 4],
        [0, 3, 1],
        [1, 3, 2],
        [1, 2, 4],
        [2, 3, 4],
        [0, 4, 3],
    ]
)
# put the two parts together
mesh = PolyMesh(
    left=InMemoryMesh(left_coords, left_faces),
    right=InMemoryMesh(right_coords, right_faces),
)

Data

The data is the information stored at each vertex of the mesh. This can be anything from the thickness of the cortex to the activation level at that vertex.

For this example, let’s create some random data for the vertices of the mesh:

rng = np.random.default_rng(0)
left_data = rng.random(mesh.parts["left"].n_vertices)
right_data = rng.random(mesh.parts["right"].n_vertices)
# put them together in a dictionary
data = {"left": left_data, "right": right_data}

Creating a surface image

Now we can create a surface image by combining the mesh and the data using the SurfaceImage class:

from nilearn.surface import SurfaceImage

surface_image = SurfaceImage(mesh=mesh, data=data)

Plotting the surface image

The surface image can be plotted using the different functions from the nilearn.plotting module. Here we will show how to use the view_surf function:

from nilearn import plotting

Plot the left part

plotting.view_surf(
    surf_mesh=surface_image.mesh, surf_map=surface_image, hemi="left"
)


Plot the right part

plotting.view_surf(
    surf_mesh=surface_image.mesh, surf_map=surface_image, hemi="right"
)

plotting.show()

Data format

Brain-related surface data are typically stored in the GIFTI format (.gii files) which can be saved to and loaded from via Nilearn.

Save the surface image

You can save the mesh and the data separately as GIFTI files:

from pathlib import Path

output_dir = Path.cwd() / "results" / "plot_surface_101"
output_dir.mkdir(exist_ok=True, parents=True)
print(f"Output will be saved to: {output_dir}")
surface_image.mesh.to_filename(output_dir / "surface_image_mesh.gii")
surface_image.data.to_filename(output_dir / "surface_image_data.gii")
Output will be saved to: /home/runner/work/nilearn/nilearn/examples/00_tutorials/results/plot_surface_101

You will see that this creates four files in total – two for the mesh and two for the data. The files ending with _hemi-L.gii correspond to the left part and those ending with _hemi-R.gii correspond to the right part.

Load the surface image

You can load the saved files back into Nilearn using the SurfaceImage object:

mesh = {
    "left": output_dir / "surface_image_mesh_hemi-L.gii",
    "right": output_dir / "surface_image_mesh_hemi-R.gii",
}
data = {
    "left": output_dir / "surface_image_data_hemi-L.gii",
    "right": output_dir / "surface_image_data_hemi-R.gii",
}

surface_image_loaded = SurfaceImage(mesh=mesh, data=data)

You can now plot the loaded surface image:

And that’s it! Now you know how to create, plot, save and load surface images with Nilearn.

Further reading

Most things that can be done with volumetric images can also be done with surface images. See following examples for more details:

Total running time of the script: (0 minutes 0.971 seconds)

Estimated memory usage: 147 MB

Gallery generated by Sphinx-Gallery