The 3D view#

The 3D view shows the volume’s three slices where the crosshair is, a volume rendering of the whole image, and a surface for each mask. Its camera turns with a drag, zooms with the wheel, and has standard positions: from above, from the front and from the right.

This example builds a synthetic head with a mask of a blob inside it, and saves screenshots of the 3D view: from the default position, with the volume rendering dimmed so the mask’s surface shows, and from the front without the volume rendering, so only the slices and the surface are left.

import numpy as np

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

A synthetic head and a mask#

A 2 mm image of an ellipsoid with a bright scalp, dark fluid under it and a brain whose intensity rises and falls with the distance from its centre; and a mask of a ball 20 mm across in the right hemisphere.

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)
t1 = np.select(
    [r < 0.82, r < 0.9, r < 1.0],
    [500 + 250 * np.cos(9 * r), np.full(shape, 80.0), np.full(shape, 900.0)],
    0.0,
).astype(np.float32)
head = Volume(t1, affine)

world = np.stack([i, j, k], axis=-1) @ affine[:3, :3].T + affine[:3, 3]
inside = np.linalg.norm(world - [30.0, 10.0, 10.0], axis=-1) < 20.0
ball = Volume(inside.astype(np.uint8), affine)

The viewer and its items#

The first volume is rendered in 3D, and the mask gets a surface, computed in the background in a window and at once off screen.

viewer = Viewer()
head_item = viewer.add(head, Category.VOLUMES, name="head")
ball_item = viewer.add(ball, Category.OVERLAYS, name="ball")
viewer.move_cursor((30.0, 10.0, 10.0))  # the slices through the ball
viewer.screenshot(ViewKind.THREE_D, "viewer_3d_default.png", size=(600, 600))
3D view of a synthetic head: three slices and a volume rendering, from the default position

Seeing inside#

The volume rendering is blended: each voxel adds a little colour and hides a little of what is behind it. A lower 3D opacity lets the mask’s surface show through; turning the camera puts the right hemisphere in front.

viewer.set(head_item.id, OptionName.OPACITY_3D, 0.3)
viewer.rotate_camera(60.0, 0.0)
viewer.screenshot(ViewKind.THREE_D, "viewer_3d_inside.png", size=(600, 600))
The head with its volume rendering dimmed, so the surface of the blob inside shows

Slices and a surface#

Without the volume rendering, the 3D view shows the slices and the surface alone. “Surface” (in the 3D section) turns a mask’s surface off and on, not its slices.

viewer.set(head_item.id, OptionName.OPACITY_3D, 0.0)
viewer.set_camera(ViewKind.THREE_D, CameraPreset.CORONAL)
viewer.screenshot(ViewKind.THREE_D, "viewer_3d_slices.png", size=(600, 600))
print(f"{ball_item.name}: {int(inside.sum())} voxels, shown as a surface")
The three slices and the blob's surface alone, seen from the front
ball: 4139 voxels, shown as a surface