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 [ ]: