Drawing user interface¶

This is a Python port for the drawing user interface web page

In [1]:
import ipywidgets as widgets
from IPython.display import display
from ipyniivue import DragMode, NiiVue, ShowRender, SliceType

# create instance

nv = NiiVue(
    back_color=(0, 0, 0, 1),
    text_height=0.03,
    show_3d_crosshair=True,
    click_to_segment_is_2d=True,
)

nv.opts.is_colorbar = False
nv.opts.legend_text_color = (0, 0, 0, 1)
nv.opts.font_color = (0, 0, 0, 1)
nv.opts.crosshair_color = (0, 0, 1, 1)

nv.set_radiological_convention(False)
nv.set_slice_type(SliceType.MULTIPLANAR)
nv.set_slice_mm(True)
nv.opts.multiplanar_show_render = ShowRender.NEVER
nv.opts.is_filled_pen = True
nv.draw_opacity = 0.5

volume_list1 = [{"path": "../images/FLAIR.nii.gz"}]
nv.load_volumes(volume_list1)

## Create menus with check marks and descriptions

# --- File Menu ---
file_buttons = []
file_options = [
    ("SaveDraw", "Save Drawing (^S)", False),
    ("CloseDraw", "Close Drawing", False),
    ("SaveBitmap", "Screen Shot", False),
    ("ShowHeader", "Show Header", False),
]

for id_, label, _state in file_options:
    button = widgets.Button(
        description=label,
        tooltip=id_,
        disabled=False,
    )
    file_buttons.append(button)

file_menu = widgets.VBox(file_buttons)
file_accordion = widgets.Accordion(children=[file_menu])
file_accordion.set_title(0, "File")

# --- Edit Menu ---
edit_buttons = []
edit_options = [
    ("Undo", "Undo Draw (^Z)", False),
]

for id_, label, _state in edit_options:
    button = widgets.Button(
        description=label,
        tooltip=id_,
        disabled=False,
    )
    edit_buttons.append(button)

edit_menu = widgets.VBox(edit_buttons)
edit_accordion = widgets.Accordion(children=[edit_menu])
edit_accordion.set_title(0, "Edit")

# --- View Menu ---

# Slice modes as RadioButtons
slice_modes = [
    ("Axial", "Axial"),
    ("Sagittal", "Sagittal"),
    ("Coronal", "Coronal"),
    ("Render", "Render"),
    ("Multiplanar", "A+C+S"),
    ("MultiplanarRender", "A+C+S+R"),
]

slice_radio = widgets.RadioButtons(
    options=[(label, id_) for id_, label in slice_modes],
    value="Multiplanar",  # default value
    description="Slice Mode:",
    disabled=False,
)

# Toggles as Checkboxes
view_toggles = [
    ("Colorbar", "Colorbar", nv.opts.is_colorbar),
    ("Radiological", "Radiological", nv.opts.is_radiological_convention),
    ("Crosshair", "Render Crosshair", nv.opts.show_3d_crosshair),
    ("ClipPlane", "Render Clip Plane", False),
    ("WorldSpace", "World Space", nv.opts.is_slice_mm),
    ("Interpolate", "Smooth Interpolation", not nv.opts.is_nearest_interpolation),
]

view_checkboxes = []
for _, label, state in view_toggles:
    cb = widgets.Checkbox(
        value=state,
        description=label,
        disabled=False,
    )
    view_checkboxes.append(cb)

# Buttons for movement and Remove Haze
move_buttons = [
    ("Left", "Left"),
    ("Right", "Right"),
    ("Anterior", "Anterior"),
    ("Posterior", "Posterior"),
    ("Inferior", "Inferior"),
    ("Superior", "Superior"),
    ("RemoveHaze", "Remove Haze"),
]

move_button_widgets = []
for id_, label in move_buttons:
    button = widgets.Button(
        description=label,
        tooltip=id_,
        disabled=False,
    )
    move_button_widgets.append(button)

move_buttons_grid = widgets.GridBox(
    move_button_widgets, layout=widgets.Layout(grid_template_columns="repeat(2, auto)")
)

view_menu = widgets.VBox(
    [
        slice_radio,
        widgets.VBox(view_checkboxes),
        move_buttons_grid,
    ]
)

view_accordion = widgets.Accordion(children=[view_menu])
view_accordion.set_title(0, "View")

# --- Color Menu ---
color_maps = [
    ("gray", "Gray"),
    ("plasma", "Plasma"),
    ("viridis", "Viridis"),
    ("inferno", "Inferno"),
]

colormap_radio = widgets.RadioButtons(
    options=[(label, id_) for id_, label in color_maps],
    value=nv.volumes[0].colormap if nv.volumes and nv.volumes[0].colormap else "gray",
    description="Colormap:",
)

back_color_checkbox = widgets.Checkbox(
    value=nv.opts.back_color[0] > 0.5,
    description="Dark Background"
    if nv.opts.back_color[0] > 0.5
    else "Light Background",
    disabled=False,
)

color_menu = widgets.VBox(
    [
        colormap_radio,
        back_color_checkbox,
    ]
)

color_accordion = widgets.Accordion(children=[color_menu])
color_accordion.set_title(0, "Color")

# --- Draw Menu ---
draw_tools = [
    ("Off", "Off"),
    ("Red", "Red"),
    ("Green", "Green"),
    ("Blue", "Blue"),
    ("Yellow", "Yellow"),
    ("Cyan", "Cyan"),
    ("Purple", "Purple"),
    ("Erase", "Erase"),
    ("EraseCluster", "Erase Cluster"),
    ("GrowClusterDark", "Grow Cluster Dark"),
    ("GrowClusterBright", "Grow Cluster Bright"),
    ("ClickToSegmentAuto", "Click To Segment (Auto)"),
]

draw_radio = widgets.RadioButtons(
    options=[(label, id_) for id_, label in draw_tools],
    value="Off" if not nv.opts.drawing_enabled else "Red",
    description="Draw Tool:",
)

# Toggles
draw_toggles = [
    ("DrawFilled", "Fill Outline", nv.opts.is_filled_pen),
    ("DrawOverwrite", "Pen Overwrites Existing", nv.draw_fill_overwrites),
    ("Translucent", "Translucent", nv.draw_opacity < 1.0),
]

draw_checkboxes = []
for _, label, state in draw_toggles:
    cb = widgets.Checkbox(
        value=state,
        description=label,
        disabled=False,
    )
    draw_checkboxes.append(cb)

draw_menu = widgets.VBox(
    [
        draw_radio,
        widgets.VBox(draw_checkboxes),
    ]
)

draw_accordion = widgets.Accordion(children=[draw_menu])
draw_accordion.set_title(0, "Draw")


@nv.on_image_loaded
def handle_image_loaded(volume):
    """Close drawing on new image loaded."""
    nv.close_drawing()
    draw_radio.value = "Off"


# --- Drag Menu ---
drag_modes = [
    ("contrast", "Contrast"),
    ("measurement", "Measurement"),
    ("pan", "Pan/Zoom"),
    ("none", "None"),
]

drag_radio = widgets.RadioButtons(
    options=[(label, id_) for id_, label in drag_modes],
    value="contrast",
    description="Drag Mode:",
)

drag_menu = widgets.VBox([drag_radio])
drag_accordion = widgets.Accordion(children=[drag_menu])
drag_accordion.set_title(0, "Drag")

# --- Script Menu ---
scripts = [
    ("FLAIR", "FLAIR"),
    ("mni152", "mni152"),
    ("CT", "CT"),
    ("CT_CBF", "CT CBF"),
    ("pCASL", "pCASL"),
    ("mesh", "mesh"),
]

script_radio = widgets.RadioButtons(
    options=[(label, id_) for id_, label in scripts],
    value="FLAIR",
    description="Scripts:",
)

script_menu = widgets.VBox([script_radio])
script_accordion = widgets.Accordion(children=[script_menu])
script_accordion.set_title(0, "Script")

# All menus
menus_hbox = widgets.HBox(
    [
        file_accordion,
        edit_accordion,
        view_accordion,
        color_accordion,
        draw_accordion,
        drag_accordion,
        script_accordion,
    ]
)

## Set up event handlers

# --- File Menu Handlers ---
header_output = widgets.Output()

def on_file_button_click(button):
    """File menu."""
    id_ = button.tooltip
    if id_ == "SaveDraw":
        nv.save_image(file_name="draw.nii.gz", save_drawing=True)
    elif id_ == "CloseDraw":
        nv.close_drawing()
        # Set the draw tool back to 'Off'
        draw_radio.value = "Off"
    elif id_ == "SaveBitmap":
        nv.save_scene(file_name="ScreenShot.png")
    elif id_ == "ShowHeader":
        if nv.volumes:
            # Get the path of the loaded volume
            volume = nv.volumes[0]
            file_path = volume.path
            with header_output:
                header_output.clear_output()
                print("Volume loaded:", file_path)

        else:
            with header_output:
                header_output.clear_output()
                print("No volume loaded.")

for button in file_buttons:
    button.on_click(on_file_button_click)

# --- Edit Menu Handlers ---
def on_edit_button_click(button):
    """Edit button."""
    id_ = button.tooltip
    if id_ == "Undo":
        nv.draw_undo()

for button in edit_buttons:
    button.on_click(on_edit_button_click)

# --- View Menu Handlers ---
def on_slice_mode_change(change):
    """Slice mode changes."""
    id_ = change["new"]
    if id_ == "Axial":
        nv.set_slice_type(SliceType.AXIAL)
    elif id_ == "Coronal":
        nv.set_slice_type(SliceType.CORONAL)
    elif id_ == "Sagittal":
        nv.set_slice_type(SliceType.SAGITTAL)
    elif id_ == "Render":
        nv.set_slice_type(SliceType.RENDER)
    elif id_ == "Multiplanar":
        nv.opts.multiplanar_show_render = ShowRender.NEVER
        nv.set_slice_type(SliceType.MULTIPLANAR)
    elif id_ == "MultiplanarRender":
        nv.opts.multiplanar_show_render = ShowRender.ALWAYS
        nv.set_slice_type(SliceType.MULTIPLANAR)

slice_radio.observe(on_slice_mode_change, names="value")

def on_view_checkbox_change(change):
    """View checkboxes."""
    widget = change["owner"]
    id_ = next(id_ for id_, label, state in view_toggles if label == widget.description)
    if id_ == "Colorbar":
        nv.opts.is_colorbar = widget.value
    elif id_ == "Radiological":
        nv.set_radiological_convention(widget.value)
    elif id_ == "Crosshair":
        nv.opts.show_3d_crosshair = widget.value
    elif id_ == "ClipPlane":
        if widget.value:
            nv.set_clip_plane(0.3, 270, 0)
        else:
            nv.set_clip_plane(2, 270, 0)
    elif id_ == "WorldSpace":
        nv.set_slice_mm(widget.value)
    elif id_ == "Interpolate":
        nv.set_interpolation(widget.value)

for cb in view_checkboxes:
    cb.observe(on_view_checkbox_change, names="value")

def on_view_button_click(button):
    """View buttons."""
    id_ = button.tooltip
    if id_ == "RemoveHaze":
        nv.remove_haze()
    else:
        if id_ in ["Left", "Right", "Anterior", "Posterior", "Inferior", "Superior"]:
            offsets = {
                "Left": (-1, 0, 0),
                "Right": (1, 0, 0),
                "Posterior": (0, -1, 0),
                "Anterior": (0, 1, 0),
                "Inferior": (0, 0, -1),
                "Superior": (0, 0, 1),
            }
            dx, dy, dz = offsets[id_]
            nv.move_crosshair_in_vox(dx, dy, dz)

for button in move_button_widgets:
    button.on_click(on_view_button_click)

# --- Color Menu Handlers ---
def on_colormap_change(change):
    """Set colormap."""
    value = change["new"]
    if nv.volumes:
        nv.set_colormap(nv.volumes[0].id, value)

colormap_radio.observe(on_colormap_change, names="value")

def on_back_color_change(change):
    """Set background color."""
    if change["new"]:
        nv.opts.back_color = (0.8, 0.8, 0.8, 1)
        nv.opts.legend_text_color = (0, 0, 0, 1)
        nv.opts.font_color = (0, 0, 0, 1)
        nv.opts.crosshair_color = (0, 0, 1, 1)
        back_color_checkbox.description = "Dark Background"
    else:
        nv.opts.back_color = (0, 0, 0, 1)
        nv.opts.legend_text_color = (1, 1, 1, 1)
        nv.opts.font_color = (1, 1, 1, 1)
        nv.opts.crosshair_color = (0, 0, 1, 1)
        back_color_checkbox.description = "Light Background"

back_color_checkbox.observe(on_back_color_change, names="value")

# --- Draw Menu Handlers ---
def on_draw_tool_change(change):
    """Draw radio."""
    value = change["new"]
    # Deactivate clickToSegment unless it's the selected tool
    nv.opts.click_to_segment = False
    nv.opts.click_to_segment_auto_intensity = False
    is_drawing_enabled = value != "Off"
    if is_drawing_enabled:
        pen_values = {
            "Erase": 0,
            "Red": 1,
            "Green": 2,
            "Blue": 3,
            "Yellow": 4,
            "Cyan": 5,
            "Purple": 6,
            "EraseCluster": -0.0,
            "GrowClusterDark": float("-inf"),
            "GrowClusterBright": float("inf"),
            "ClickToSegmentAuto": 1,  # assuming red pen
        }
        pen_value = pen_values.get(value, 1)
        if value == "ClickToSegmentAuto":
            nv.opts.click_to_segment = True
            nv.opts.click_to_segment_auto_intensity = True
        nv.opts.pen_value = pen_value
    nv.set_drawing_enabled(is_drawing_enabled)

draw_radio.observe(on_draw_tool_change, names="value")

def on_draw_checkbox_change(change):
    """Draw checkboxes."""
    widget = change["owner"]
    label = widget.description
    if label == "Fill Outline":
        nv.opts.is_filled_pen = widget.value
    elif label == "Pen Overwrites Existing":
        nv.draw_fill_overwrites = widget.value
    elif label == "Translucent":
        nv.draw_opacity = 0.5 if widget.value else 1.0

for cb in draw_checkboxes:
    cb.observe(on_draw_checkbox_change, names="value")

# --- Drag Menu Handlers ---
def on_drag_mode_change(change):
    """Drag menu."""
    value = change["new"]
    drag_modes = {
        "contrast": DragMode.CONTRAST,
        "measurement": DragMode.MEASUREMENT,
        "pan": DragMode.PAN,
        "none": DragMode.NONE,
    }
    nv.opts.drag_mode = drag_modes[value]

drag_radio.observe(on_drag_mode_change, names="value")

# --- Script Menu Handlers ---
previous_script_value = [script_radio.value]
output = widgets.Output()

def on_script_change(change):
    """Script menu."""
    value = change["new"]

    # Check if any drawing is active
    with output:
        output.clear_output()
    if nv.opts.drawing_enabled:
        with output:
            print("Close open drawing before opening a new volume.")
        script_radio.value = previous_script_value[0]
        return

    # Update the previous_script_value
    previous_script_value[0] = value

    nv.meshes = []
    if value == "FLAIR":
        volume_list1 = [{"path": "../images/FLAIR.nii.gz"}]
        nv.load_volumes(volume_list1)
    elif value == "mni152":
        volume_list1 = [{"path": "../images/mni152.nii.gz"}]
        nv.load_volumes(volume_list1)
    elif value == "CT":
        volume_list1 = [{"path": "../images/shear.nii.gz"}]
        nv.load_volumes(volume_list1)
    elif value == "CT_CBF":
        volume_list1 = [{"path": "../images/ct_perfusion.nii.gz"}]
        nv.load_volumes(volume_list1)
    elif value == "pCASL":
        volume_list1 = [{"path": "../images/pcasl.nii.gz"}]
        nv.load_volumes(volume_list1)
    elif value == "mesh":
        # Clear existing meshes
        volume_list1 = [{"path": "../images/mni152.nii.gz"}]
        nv.load_volumes(volume_list1)
        # Load meshes
        nv.load_meshes(
            [
                {
                    "path": "../images/BrainMesh_ICBM152.lh.mz3",
                    "rgba255": [200, 162, 255, 255],
                },
                {"path": "../images/dpsv.trx", "rgba255": [255, 255, 255, 255]},
            ]
        )


script_radio.observe(on_script_change, names="value")

## Display results

display(widgets.VBox([output, nv, menus_hbox, header_output]))
In [ ]: