Tractograms in the viewer#

A tractogram is drawn as lines in the 2D views and the 3D view. Its options set how its streamlines are coloured (by direction, in one colour, or one colour per tractogram), what share of them is drawn, how many points each one keeps, and, in the 2D views, the slab: only the parts of streamlines within a few millimetres of the slice are drawn, so a slice shows what passes through it.

This example builds a synthetic head and two bundles of streamlines, one running left to right and one running up and down, and saves screenshots of the axial and coronal views with the slab, and of the 3D view with a subset of the streamlines.

import numpy as np

from larmor.types import Tractogram, Volume
from larmor.viewer import (
    CameraPreset,
    Category,
    Colouring,
    OptionName,
    Viewer,
    ViewKind,
)

A synthetic head#

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

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)

Two bundles#

A Tractogram holds the points of all its streamlines one after the other, and the offsets where each one starts. The first bundle arches from left to right over the middle of the brain, like the corpus callosum; the second runs from the top of the brain down through the right hemisphere, like a corticospinal tract.

rng = np.random.default_rng(0)
n, length = 2000, 60
t = np.linspace(0, 1, length, dtype=np.float32)


def bundle(curve: np.ndarray, spread: float) -> np.ndarray:
    """n copies of a curve, each shifted at random by about spread mm."""
    shifts = rng.normal(scale=spread, size=(n, 1, 3)).astype(np.float32)
    return curve[None] + shifts


arch = np.stack([120 * t - 60, np.zeros_like(t), 20 + 25 * np.sin(np.pi * t)], -1)
column = np.stack([25 + 5 * t, -10 + 5 * t, 70 - 120 * t], -1)
streamlines = np.concatenate([bundle(arch, 4.0), bundle(column, 3.0)])
points = streamlines.reshape(-1, 3)
offsets = np.arange(0, len(points) + 1, length, dtype=np.int64)
tracts = Tractogram(points, offsets)

The viewer and its items#

Tractograms are added like volumes; load reads a TCK or TRK file instead. The cursor, which sets the slices, goes through both bundles.

viewer = Viewer()
# The screenshots hide the crosshair and the text, so they show only the data.
viewer.toggle_crosshair()
viewer.toggle_overlay()
viewer.add(head, Category.VOLUMES, name="head")
item = viewer.add(tracts, Category.TRACTOGRAMS, name="bundles")
viewer.move_cursor((25.0, 0.0, 20.0))

The slab#

Each streamline starts coloured by its direction: red left to right, green front to back, blue up and down. In the 2D views the slab is on, 5 mm thick by default: the axial view shows where the column crosses the slice, the coronal view shows the arch along it. Here it is 10 mm, to show a little more of each bundle.

viewer.set(item.id, OptionName.SLAB_THICKNESS, 10.0)
viewer.screenshot(ViewKind.AXIAL, "viewer_tracts_axial.png", size=(600, 600))
viewer.screenshot(ViewKind.CORONAL, "viewer_tracts_coronal.png", size=(600, 600))
Axial view of synthetic bundles over a head: only the streamlines in a slab around the slice are drawn Coronal view of the same bundles in a slab

A subset in one colour#

With millions of streamlines, a subset is often enough to see the shape of the tractogram, and much faster to draw. The subset is a percentage of the streamlines, taken at random, the same ones each time. Here a quarter of them, in orange, seen from the front in the 3D view, without the head’s volume rendering (its 3D opacity at 0), which would hide the lines inside it.

A subset of the same bundles in the 3D view, in one orange colour
4000 streamlines, 240000 points