Drawing¶

This is a simple demo for drawing voxels using NiiVue. This jupyter script emulates the drawing web page.

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

nv = NiiVue(back_color=(1, 1, 1, 1))

nv.set_radiological_convention(False)
nv.opts.multiplanar_show_render = ShowRender.ALWAYS
nv.set_slice_type(SliceType.MULTIPLANAR)

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

## Set up Callbacks

drawing_loaded = False

location_label = widgets.Label(value="")

@nv.on_location_change
def handle_location_change(location):
    """Update the location label when the crosshair location changes."""
    location_label.value = f"Location: {location['string']}"

@nv.on_image_loaded
def handle_image_loaded(volume):
    """Reset drawing settings when a new image is loaded."""
    global drawing_loaded
    if not drawing_loaded:
        drawing_loaded = True
        nv.load_drawing("../images/lesion.nii.gz")
    nv.set_drawing_enabled(False)
    draw_pen.value = -1  # Set the drawPen dropdown to 'Off'

## Create GUI Controls

# Dropdown for Draw Pen
draw_pen = widgets.Dropdown(
    options=[
        ("Off", -1),
        ("Erase", 0),
        ("Red", 1),
        ("Green", 2),
        ("Blue", 3),
        ("Filled Erase", 8),
        ("Filled Red", 9),
        ("Filled Green", 10),
        ("Filled Blue", 11),
        ("Erase Selected Cluster", 12),
    ],
    value=-1,
    description="Draw color:",
)

# Movement Buttons
left_button = widgets.Button(description="Left")
right_button = widgets.Button(description="Right")
posterior_button = widgets.Button(description="Posterior")
anterior_button = widgets.Button(description="Anterior")
inferior_button = widgets.Button(description="Inferior")
superior_button = widgets.Button(description="Superior")

# Other Buttons
save_button = widgets.Button(description="Save Drawing")
undo_button = widgets.Button(description="Undo")
growcut_button = widgets.Button(description="Grow Cut")

# Draw Opacity Slider
draw_opacity = widgets.IntSlider(
    value=80,
    min=0,
    max=100,
    step=1,
    description="Drawing Opacity",
    readout=False,
    style={"description_width": "initial"},
)

# Checkboxes
fill_pen_overwrites_checkbox = widgets.Checkbox(
    value=True,
    description="Fill pen overwrites",
)
radiological_checkbox = widgets.Checkbox(
    value=False,
    description="Radiological",
)
world_space_checkbox = widgets.Checkbox(
    value=False,
    description="World space",
)
linear_interpolation_checkbox = widgets.Checkbox(
    value=True,
    description="Linear Interpolation",
)
highdpi_checkbox = widgets.Checkbox(
    value=True,
    description="HighDPI",
)

# Textarea for Custom Colormap
text_value = """{
    "R": [0, 255, 22, 127],
    "G": [0, 20, 192, 187],
    "B": [0, 152, 80, 255],
    "labels": ["clear", "pink", "lime", "sky"]
}"""
num_lines = text_value.count("\n") + 1  # Adding 1 to include the last line
script_text = widgets.Textarea(
    value=text_value,
    description="Colormap",
    layout=widgets.Layout(width="60%"),
    rows=num_lines,  # Set rows to the number of lines in your text
)

# Apply Button for Custom Colormap
custom_button = widgets.Button(description="Apply")

## Define Event Handlers

def on_draw_opacity_change(change):
    """Handle changes in drawing opacity."""
    nv.draw_opacity = change["new"] / 100

draw_opacity.observe(on_draw_opacity_change, names="value")

def on_draw_pen_change(change):
    """Handle changes in draw pen selection."""
    mode = int(change["new"])
    nv.set_drawing_enabled(mode >= 0)
    if 0 <= mode <= 11:
        nv.set_pen_value(mode & 7, mode > 7)
    if mode == 12:
        # Erase selected cluster
        nv.set_pen_value(-0.0, False)

draw_pen.observe(on_draw_pen_change, names="value")

def on_left_click(b):
    """Move left."""
    nv.move_crosshair_in_vox(-1, 0, 0)

def on_right_click(b):
    """Move right."""
    nv.move_crosshair_in_vox(1, 0, 0)

def on_posterior_click(b):
    """Posterior."""
    nv.move_crosshair_in_vox(0, -1, 0)

def on_anterior_click(b):
    """Anterior."""
    nv.move_crosshair_in_vox(0, 1, 0)

def on_inferior_click(b):
    """Inferior."""
    nv.move_crosshair_in_vox(0, 0, -1)

def on_superior_click(b):
    """Superior."""
    nv.move_crosshair_in_vox(0, 0, 1)

left_button.on_click(on_left_click)
right_button.on_click(on_right_click)
posterior_button.on_click(on_posterior_click)
anterior_button.on_click(on_anterior_click)
inferior_button.on_click(on_inferior_click)
superior_button.on_click(on_superior_click)

def on_undo_click(b):
    """Undo drawing action."""
    nv.draw_undo()

undo_button.on_click(on_undo_click)

def on_growcut_click(b):
    """Draw grow cut."""
    nv.draw_grow_cut()

growcut_button.on_click(on_growcut_click)

def on_save_click(b):
    """Save drawing."""
    nv.save_image("test.nii", is_save_drawing=True)

save_button.on_click(on_save_click)

def on_fill_pen_overwrites_change(change):
    """Draw fill overwrites option."""
    nv.draw_fill_overwrites = change["new"]

fill_pen_overwrites_checkbox.observe(on_fill_pen_overwrites_change, names="value")

def on_radiological_change(change):
    """Set radiological convention."""
    nv.set_radiological_convention(change["new"])

radiological_checkbox.observe(on_radiological_change, names="value")

def on_world_space_change(change):
    """Set slice mm."""
    nv.set_slice_mm(change["new"])

world_space_checkbox.observe(on_world_space_change, names="value")

def on_linear_interpolation_change(change):
    """Set interpolation."""
    nv.set_interpolation(not change["new"])

linear_interpolation_checkbox.observe(on_linear_interpolation_change, names="value")

def on_highdpi_change(change):
    """Set high resolution capable."""
    nv.set_high_resolution_capable(change["new"])

highdpi_checkbox.observe(on_highdpi_change, names="value")

def on_custom_button_click(b):
    """Set draw colormap."""
    try:
        val = script_text.value
        cmap = ColorMap(**json.loads(val))
        nv.set_draw_colormap(cmap)
    except Exception as e:
        print(f"Error applying custom colormap: {e}")

custom_button.on_click(on_custom_button_click)

## Setup controls arrangement

controls_row1 = widgets.HBox([draw_pen])

movement_buttons = widgets.HBox(
    [
        left_button,
        right_button,
        posterior_button,
        anterior_button,
        inferior_button,
        superior_button,
    ]
)

other_buttons = widgets.HBox([save_button, undo_button, growcut_button])

checkboxes1 = widgets.HBox(
    [
        fill_pen_overwrites_checkbox,
        radiological_checkbox,
        world_space_checkbox,
    ]
)

checkboxes2 = widgets.HBox(
    [
        linear_interpolation_checkbox,
        highdpi_checkbox,
    ]
)

controls = widgets.VBox(
    [
        controls_row1,
        movement_buttons,
        other_buttons,
        draw_opacity,
        checkboxes1,
        checkboxes2,
        script_text,
        custom_button,
        location_label,
    ]
)

## Display all

display(controls, nv)
In [ ]: