Mosaics with 4D volumes¶
Mosaics display multiple image views simultaneously, often called “lightbox” mode. NiiVue lets you define both the orientation and position for any number of image snapshots. In this example, a slider is used to step through each 3D volume in a 4D time series, with slices chosen to be evenly spaced across the volume. Because the number of volumes and spatial extent are unknown until loading completes, these parameters are computed within the on_image_loaded() event, triggered once the image becomes available. This approach differs from typical sequential Python workflows, as load_volumes() is asynchronous—allowing images to load without freezing the user interface.
In [1]:
import ipywidgets as widgets
from ipyniivue import NiiVue
nv = NiiVue()
nv.load_volumes([{"path": "../images/mpld_asl.nii.gz"}])
def world_coord(i, j, k, affine):
"""Return world (x,y,z) for voxel index (i,j,k) using 4x4 affine list."""
x = affine[0][0] * i + affine[0][1] * j + affine[0][2] * k + affine[0][3]
y = affine[1][0] * i + affine[1][1] * j + affine[1][2] * k + affine[1][3]
z = affine[2][0] * i + affine[2][1] * j + affine[2][2] * k + affine[2][3]
return (x, y, z)
def axis_range(dims, affine, mid_mode='voxel'):
"""
Return 1D lists with the canonical world coordinate for each slice along X, Y and Z.
Parameters
----------
dims : sequence
NIfTI dims where dims[1]=nx, dims[2]=ny, dims[3]=nz
affine : 4x4 nested sequence
Voxel-to-world affine matrix (used by world_coord)
mid_mode : {'voxel', 'between'}
'voxel' -> center voxel convention: (dim-1)/2.0 (default)
'between'-> geometric middle between indices: dim/2.0
Returns
-------
x_positions, y_positions, z_positions : tuple of lists
- x_positions: list of length nx of world X (float) for each i (0..nx-1)
- y_positions: list of length ny of world Y (float) for each j (0..ny-1)
- z_positions: list of length nz of world Z (float) for each k (0..nz-1)
"""
nx = int(dims[1])
ny = int(dims[2])
nz = int(dims[3])
if mid_mode == 'between':
mid_x = nx / 2.0
mid_y = ny / 2.0
mid_z = nz / 2.0
else:
# default: 'voxel' center convention
mid_x = (nx - 1) / 2.0
mid_y = (ny - 1) / 2.0
mid_z = (nz - 1) / 2.0
# X axis canonical (world X) for each i
x_positions = []
for i in range(nx):
wx, wy, wz = world_coord(i, mid_y, mid_z, affine)
x_positions.append(wx)
# Y axis canonical (world Y) for each j
y_positions = []
for j in range(ny):
wx, wy, wz = world_coord(mid_x, j, mid_z, affine)
y_positions.append(wy)
# Z axis canonical (world Z) for each k
z_positions = []
for k in range(nz):
wx, wy, wz = world_coord(mid_x, mid_y, k, affine)
z_positions.append(wz)
return x_positions, y_positions, z_positions
@nv.on_image_loaded
def on_image_loaded(volume):
"""
Event handler called when an image is loaded.
Parameters
----------
volume : ipyniivue.Volume
The loaded image volume.
"""
vol_slider4d.max = volume.n_frame_4d - 1
x_pos, y_pos, z_pos = axis_range(volume.hdr.dims, volume.hdr.affine)
# attach to the volume instance
volume.x_pos = x_pos
volume.y_pos = y_pos
volume.z_pos = z_pos
update_mosaic()
# Start by showing every 5th slice, with 10 columns
#nv2.opts.slice_mosaic_string = full_mosaic(coords, ncols=init_cols, step=init_step)
axis_selector = widgets.Dropdown(
options=["axial", "sagittal", "coronal"], description="View"
)
vol_slider4d = widgets.IntSlider(min=0, max=0, description="Volume")
column_slider = widgets.IntSlider(min=0, max=20, value=10, description="Columns")
step_slider = widgets.IntSlider(min=1, max=5, value=1, description="Step size")
size_slider = widgets.IntSlider(min=100, max=1000, value=300, description="Height")
def update_mosaic(*args):
"""Update the mosaic string using the slice_pos arrays and a step size."""
prefix = axis_selector.value[0].upper()
# pick axis and its slice positions array
if prefix == 'A':
slice_pos = nv.volumes[0].z_pos
elif prefix == 'C':
slice_pos = nv.volumes[0].y_pos
else:
slice_pos = nv.volumes[0].x_pos
cols = column_slider.value
stepSize = step_slider.value
# defensive checks
if stepSize <= 0:
# invalid step -> nothing to show
nv.opts.slice_mosaic_string = prefix
return
total = len(slice_pos)
N = total // stepSize # number of items to show (integer division)
if N <= 0:
nv.opts.slice_mosaic_string = prefix
return
# Build positions by taking every `stepSize`-th entry from slice_pos
positions = []
for n in range(N):
idx = n * stepSize
# safety: ensure index in bounds (should be by construction)
if idx < total:
positions.append(slice_pos[idx])
else:
break
# format with fixed decimals (3 here)
def fmt(v):
return f"{v:.3f}"
# build mosaic string: prefix + " " + numbers, inserting ";" after every `cols` items
parts = [prefix]
for i, pos in enumerate(positions):
parts.append(fmt(pos))
if (i + 1) % cols == 0 and (i + 1) != len(positions):
parts.append(";")
mosaic_string = " ".join(parts)
# apply
nv.volumes[0].frame_4d = vol_slider4d.value
nv.opts.slice_mosaic_string = mosaic_string
def update_height(*args):
"""Update widget height."""
nv.height = size_slider.value
vol_slider4d.observe(update_mosaic, "value")
column_slider.observe(update_mosaic, "value")
step_slider.observe(update_mosaic, "value")
axis_selector.observe(update_mosaic, "value")
size_slider.observe(update_height, "value")
display(vol_slider4d)
display(column_slider)
display(step_slider)
display(size_slider)
display(axis_selector)
display(nv)
In [ ]: