Surfaces in the viewer#

A surface, such as the white matter surface FreeSurfer makes, is drawn in the 3D view as a shaded mesh and in the 2D views as the line where each slice cuts it. Its values per vertex, such as its curvature, can colour it.

This example builds a synthetic head and a folded surface inside it, with a curvature-like value per vertex, and saves screenshots of the 3D view coloured by that value and of the axial view with the cut line over the head.

import numpy as np

from larmor.types import Surface, Volume
from larmor.viewer import Category, OptionName, Viewer, ViewKind

A synthetic head#

A 2 mm image of an ellipsoid centred at the world origin (world coordinates are in mm, RAS+), as in the other examples.

shape = (90, 108, 90)
affine = np.diag([2.0, 2.0, 2.0, 1.0])
affine[:3, 3] = -np.array(shape)  # half the extent in mm: the centre at 0
i, j, k = np.indices(shape, dtype=np.float32)
x, y, z = (2 * i / shape[0] - 1, 2 * j / shape[1] - 1, 2 * k / shape[2] - 1)
r = np.sqrt((x / 0.8) ** 2 + (y / 0.9) ** 2 + (z / 0.85) ** 2)
head = Volume(np.where(r < 0.9, 600 - 300 * r, 0).astype(np.float32), affine)

A folded surface#

One hemisphere-like shape: an ellipsoid, long front to back and flattened on its inner (right) side, meshed by latitude and longitude, its radius rippled into folds. A surface holds its vertices in world coordinates (mm) and its faces as vertex indices, counter-clockwise seen from outside. The ripple, negated, plays the part of FreeSurfer’s curvature: positive in the folds’ troughs (sulci), negative on their crests (gyri). It is stored as the values “curv”, the name FreeSurfer’s curvature files get.

rows, columns = 120, 240
theta = np.linspace(0, np.pi, rows)[:, None]  # from the top down
phi = np.linspace(0, 2 * np.pi, columns, endpoint=False)[None, :]  # around
ripple = np.sin(17 * phi + 3 * np.cos(4 * theta)) * np.sin(
    13 * theta + 2 * np.sin(3 * phi)
)
radius = 1 + 0.04 * ripple
sx = np.sin(theta) * np.cos(phi)
sy = np.sin(theta) * np.sin(phi)
sz = np.cos(theta) * np.ones_like(phi)
sx = np.where(sx > 0, 0.35 * sx, sx)  # the flat inner side faces the midline
points = np.stack(
    [-30 + 35 * radius * sx, 70 * radius * sy, 5 + 55 * radius * sz], axis=-1
).reshape(-1, 3)
faces = []
for row in range(rows - 1):
    for column in range(columns):
        a = row * columns + column
        b = row * columns + (column + 1) % columns
        faces += [[a, a + columns, b], [b, a + columns, b + columns]]
surface = Surface(points, np.array(faces), vertex_data={"curv": -ripple.ravel() / 2})

The viewer and its items#

Surfaces are added like the other items; load reads a FreeSurfer surface (such as lh.white, with its lh.curv beside it) or a GIfTI surf.gii file. A name with “white” in it gives the surface yellow, as FreeSurfer’s viewer does, and a surface with “curv” values starts coloured by them.

viewer = Viewer()
# The screenshots hide the crosshair and the text, so they show only the data.
viewer.toggle_crosshair()
viewer.toggle_overlay()
head_item = viewer.add(head, Category.VOLUMES, name="head")
item = viewer.add(surface, Category.SURFACES, name="lh.white")

Coloured by curvature#

In grey from -0.3 to 0.3, the troughs dark and the crests light. The head would hide the surface in the 3D view, so it is hidden for this screenshot.

viewer.set(head_item.id, OptionName.VISIBLE, False)
viewer.screenshot(ViewKind.THREE_D, "viewer_surfaces_curvature.png", size=(600, 600))
A synthetic folded surface in the 3D view, coloured light and dark by a curvature-like value

The cut line#

On the 2D views, the line where the slice cuts the surface, here in the surface’s own colour after switching the values off. Cut lines are how a surface is checked against its T1; the Cut lines option turns them off.

viewer.set(head_item.id, OptionName.VISIBLE, True)
viewer.set(item.id, OptionName.COLOUR_BY, None)
viewer.screenshot(ViewKind.AXIAL, "viewer_surfaces_cut_line.png", size=(600, 600))
print(f"{surface.num_vertices} vertices, {surface.num_faces} faces")
Axial slice of a synthetic head with the line where the slice cuts the surface
28800 vertices, 57120 faces