Voxel Mosaics 2¶

Choose axial (A), coronal (C) or sagittal (S) slices. Modify with cross slices (X) and renderings (R).

This notebook mirrors the NiiVue web page recipe.

In [1]:
import ipywidgets as widgets
from IPython.display import display
from ipyniivue import NiiVue

nv = NiiVue(height=600, back_color=(1, 1, 1, 1), is_colorbar=True, drag_mode="CONTRAST")

nv.load_volumes(
    [
        {
            "path": "../images/fslmean.nii.gz",
            "colormap": "gray",
            "colorbar_visible": False,
        },
        {
            "path": "../images/fslt.nii.gz",
            "colormap": "warm",
            "colormap_negative": "winter",
            "cal_min": 2,
            "cal_max": 6,
        },
    ]
)

initial_mosaic = (
    "A 0 L+ 50 L- 60 C -10 0 S 40; A X 0 S X 0 C X 0 R A X 0 R S X 0 R C X 0"
)
nv.set_slice_mosaic_string(initial_mosaic)

## Create interactive controls

# mosaic string input
mosaic_text = widgets.Text(
    value=initial_mosaic,
    placeholder="Enter mosaic string",
    description="Mosaic string:",
    style={"description_width": "initial"},
    layout=widgets.Layout(width="70%"),
)

# help button
help_button = widgets.Button(
    description="Help",
    button_style="info",
    tooltip="Click for information about mosaic strings",
    layout=widgets.Layout(width="80px"),
)

# radiological convention checkbox
radio_check = widgets.Checkbox(value=False, description="Radiological", indent=False)

# world space checkbox
mm_check = widgets.Checkbox(value=False, description="World space", indent=False)

# ruler checkbox
ruler_check = widgets.Checkbox(value=False, description="Ruler", indent=False)

# nose left checkbox
sag_check = widgets.Checkbox(value=False, description="Nose left", indent=False)

# colorbar checkbox
colorbar_check = widgets.Checkbox(value=True, description="Colorbar", indent=False)

# negative colors checkbox
negative_check = widgets.Checkbox(
    value=True, description="Negative colors", indent=False
)

# orient cube checkbox
cube_check = widgets.Checkbox(value=False, description="Cube", indent=False)

# high DPI checkbox
dpi_check = widgets.Checkbox(value=True, description="HighDPI", indent=False)

# gamma slider
gamma_slider = widgets.FloatSlider(
    value=1.0, min=0.1, max=2.0, step=0.1, description="Gamma:", continuous_update=True, readout=False
)

# drag mode dropdown
drag_mode = widgets.Dropdown(
    options=[
        ("none", 0),
        ("contrast", 1),
        ("measurement", 2),
    ],
    value=1,
    description="Drag mode:",
    style={"description_width": "initial"},
)

back_color_picker = widgets.ColorPicker(
    concise=False,
    description='Back color',
    value='white',
    disabled=False,
    continuous_update= True
)

N = 11   # replace with your actual upper index
options = list(range(N + 1))

stat_range = widgets.SelectionRangeSlider(
    options=options,
    index=(2, 6),
    description="Threshold:",
    disabled=False,
    readout=False
)

# output for messages
output = widgets.Output()

## Setup Event Handlers

def on_mosaic_change(change):
    """Handle mosaic string changes."""
    nv.set_slice_mosaic_string(change["new"])


def on_help_click(b):
    """Display help information."""
    with output:
        output.clear_output()
        print("Mosaic String Help:")
        print("=" * 50)
        print("Choose axial (A), coronal (C) or sagittal (S) slices.")
        print("Modify with cross slices (X) and renderings (R).")
        print("\nExample patterns:")
        print("  A 0 20 40        - Axial slices at positions 0, 20, 40")
        print("  C -10 0 10       - Coronal slices at positions -10, 0, 10")
        print("  S 0              - Sagittal slice at position 0")
        print("  A X 0            - Axial slice with cross at position 0")
        print("  R                - 3D rendering")
        print("  L+ 50            - Left hemisphere at position 50")
        print("  L- 60            - Right hemisphere at position 60")


def on_radio_change(change):
    """Handle radiological convention toggle."""
    nv.set_radiological_convention(change["new"])


def on_mm_change(change):
    """Handle world space toggle."""
    nv.set_slice_mm(change["new"])


def on_ruler_change(change):
    """Handle ruler toggle."""
    nv.opts.is_ruler = change["new"]


def on_sag_change(change):
    """Handle sagittal nose left toggle."""
    nv.opts.sagittal_nose_left = change["new"]


def on_colorbar_change(change):
    """Handle colorbar toggle."""
    nv.opts.is_colorbar = change["new"]


def on_negative_change(change):
    """Handle negative colormap toggle."""
    if change["new"]:
        nv.set_colormap_negative(nv.volumes[1].id, "winter")
    else:
        nv.set_colormap_negative(nv.volumes[1].id, "")


def on_cube_change(change):
    """Handle orient cube toggle."""
    nv.opts.is_orient_cube = change["new"]


def on_dpi_change(change):
    """Handle high DPI toggle."""
    nv.set_high_resolution_capable(change["new"])


def on_gamma_change(change):
    """Handle gamma adjustment."""
    nv.set_gamma(change["new"])


def on_drag_mode_change(change):
    """Handle drag mode change."""
    nv.opts.drag_mode = change["new"]


def on_color_change(change):
    """Convert hex color (e.g. '#1f1f23') to normalized RGBA tuple (0..1)."""
    hexstr = change["new"].lstrip("#")        # remove leading '#'
    if len(hexstr) != 6:
        print("Invalid hex color:", change["new"])
        return
    
    r = int(hexstr[0:2], 16) / 255.0
    g = int(hexstr[2:4], 16) / 255.0
    b = int(hexstr[4:6], 16) / 255.0
    a = 1.0
    nv.opts.back_color = (r, g, b, a)
    #nv.draw_scene()

def on_stat_change(change):
    """Set threshold for statistical overlay."""
    low, high = change["new"]    # extract the two values
    nv.volumes[1].cal_min = low
    nv.volumes[1].cal_max = high 

# attach event handlers
mosaic_text.observe(on_mosaic_change, names="value")
help_button.on_click(on_help_click)
radio_check.observe(on_radio_change, names="value")
mm_check.observe(on_mm_change, names="value")
ruler_check.observe(on_ruler_change, names="value")
sag_check.observe(on_sag_change, names="value")
colorbar_check.observe(on_colorbar_change, names="value")
negative_check.observe(on_negative_change, names="value")
cube_check.observe(on_cube_change, names="value")
dpi_check.observe(on_dpi_change, names="value")
gamma_slider.observe(on_gamma_change, names="value")
drag_mode.observe(on_drag_mode_change, names="value")
back_color_picker.observe(on_color_change, names="value")
stat_range.observe(on_stat_change, names="value")

## Display controls and image

# organize controls
mosaic_row = widgets.HBox([mosaic_text, help_button])

checkbox_row1 = widgets.HBox([radio_check, mm_check, ruler_check, sag_check])

checkbox_row2 = widgets.HBox([colorbar_check, negative_check, cube_check, dpi_check])

slider_row = widgets.HBox([gamma_slider, drag_mode, back_color_picker, stat_range])

# create main layout
controls = widgets.VBox([mosaic_row, checkbox_row1, checkbox_row2, slider_row, output])

# display everything
display(widgets.VBox([controls, nv]))
In [ ]: