from dataclasses import dataclass, field
from typing import Any
from datetime import datetime
from collections import OrderedDict
import logging
import math
import numpy as np
from vedo.colors import colorMap
import vtk
import vedo
from iblviewer.volume import Volume, VolumeModel, VolumeController
from iblviewer.collection import Collection
import iblviewer.objects as obj
import iblviewer.utils as utils
# A camera model does not necessarily reflect the view. It's a state that can be set
# to a view. It's also a state that can be retrieved from the view.
[docs]@dataclass
class CameraModel:
camera: vtk.vtkCamera = None
autofocus: bool = True
target: Any = None
distance_factor: int = 3
orthographic_scale_factor: int = 1
[docs]@dataclass
class UIModel:
DEFAULT = 'default'
CAMERA = 'camera'
SCENE = 'scene'
OBJECT = 'object'
DATA = 'data'
EXPORT = 'export'
visible: bool = True
embed: bool = False
embed_menu_x: int = 40
embed_menu_y: int = 50
embed_submenu_x: int = 200
embed_submenu_y: int = 50
embed_slider_width: int = 250
color: Any = '#000000'
background_color: Any = '#ffffff'
contexts = {DEFAULT: {}, CAMERA: {}, SCENE: {}, OBJECT: {}, DATA: {}, EXPORT: {}}
# Font license is under https://raw.github.com/int-brain-lab/iblviewer/main/assets/fonts
font_path = str(utils.FONTS_FOLDER)
font = 'SourceSansPro-Regular'
#font = 'https://raw.github.com/int-brain-lab/iblviewer/main/assets/fonts/SourceSansPro-Regular.ttf'
# VTK is a nightmare for having a basic UI working...
# and we need one when running in standalone or jupyter modes.
font_size = 15
toggle_config = {'c':['black', '#bbbbbb'],
'bc':['#dddddd', '#ffffff'],
'font':font, 'size':font_size,
'bold':False, 'italic':False}
button_config = {'c':['black', 'black'],
'bc':['#dddddd', '#dddddd'],
'font':font, 'size':font_size,
'bold':False, 'italic':False}
slider_config = {'font':font, 'c':'black'}
context: str = DEFAULT
context_changed: bool = False
last_context: str = None
main_context: str = None
dark_mode: bool = False
[docs] def set_dark_mode(self, on=True):
self.toggle_config['c'] = ['#eeeeee', '#eeeeee'] if on else ['#111111', '#111111']
self.toggle_config['bc'] = ['#444444', '#777777'] if on else ['#cfcfcf', '#dfdfdf']
self.button_config['c'] = ['#eeeeee', '#eeeeee'] if on else ['#111111', '#111111']
self.button_config['bc'] = ['#444444', '#777777'] if on else ['#cfcfcf', '#dfdfdf']
self.slider_config['c'] = 'white' if on else 'black'
self.color = '#eeeeee' if on else '#222222'
self.background_color = '#2d2d2d' if on else '#eeeeee'
self.dark_mode = on
[docs] def all_elements(self):
elements = dict()
for context_id in self.contexts:
elements.update(self.get_elements(context_id))
return elements
[docs] def get_elements(self, context=None):
if context is None:
context = self.context
return self.contexts.get(context)
[docs] def get_element(self, name, context=None):
return self.get_elements(context).get(name)
[docs] def is_camera_context(self):
return self.context == UIModel.CAMERA
[docs] def is_scene_context(self):
return self.context == UIModel.SCENE
[docs] def is_object_context(self):
return self.context == UIModel.OBJECT
[docs] def is_data_context(self):
return self.context == UIModel.DATA
[docs] def is_export_context(self):
return self.context == UIModel.EXPORT
[docs] def is_valid_context(self, context):
return context in self.contexts
[docs] def toggle_context(self, context=None):
"""
Toggles between current context and given one
:param context: A context string
"""
if context is None or self.main_context is not None:
if self.main_context == context:
self.context_changed = False
return
self.context = self.main_context
self.main_context = None
self.context_changed = True
else:
if self.context == context or not self.is_valid_context(context):
self.context_changed = False
return
self.main_context = self.context
self.context = context
self.context_changed = True
[docs] def set_context(self, context):
"""
Set the context
:param context: A context from CONTEXTS
:return: True if context was changed
"""
if self.is_valid_context(context) and context != self.context:
self.last_context = self.context
self.context = context
self.context_changed = True
return True
else:
#logging.error('Context ' + str(context) + ' is invalid. Ignoring it.')
self.context_changed = False
return False
[docs] def register(self, element, context=None):
"""
Register UI components (or anything else really) to the given context
"""
if context is None:
context = self.context
if not self.is_valid_context(context):
return
if isinstance(element, list):
for item in element:
self.contexts[context][item.name] = item
else:
self.contexts[context][element.name] = element
[docs]@dataclass
class ViewerModel:
POINT = 'point'
LINE = 'line'
SURFACE = 'surface'
VOLUME = 'volume'
UI = 'ui'
OBJECTS_TYPE = {POINT:0, LINE:1, SURFACE:2, VOLUME:3, UI:4}
# Global application camera presets
X_UP = np.array([1.0, 0.0, 0.0])
Y_UP = np.array([0.0, 1.0, 0.0])
Z_UP = np.array([0.0, 0.0, 1.0])
X_DOWN = np.array([-1.0, 0.0, 0.0])
Y_DOWN = np.array([0.0, -1.0, 0.0])
Z_DOWN = np.array([0.0, 0.0, -1.0])
title: str = 'VTK Viewer'
window_size: np.ndarray = np.array([1280, 720])
web_window_size: np.ndarray = np.array([960, 600])
web_ui: bool = False
offscreen: bool = False
ui: UIModel = field(default_factory=UIModel)
origin: np.ndarray = np.array([0.0, 0.0, 0.0])
cameras: Collection = field(default_factory=Collection)
# VolumeModel instances have their own SlicerModel instances (ie. box slicing)
volumes: Collection = field(default_factory=Collection)
# This set is used only by independent SlicerModel instances
slicers: Collection = field(default_factory=Collection)
points: Collection = field(default_factory=Collection)
lines: Collection = field(default_factory=Collection)
surfaces: Collection = field(default_factory=Collection)
probes: Collection = field(default_factory=Collection)
luts: Collection = field(default_factory=Collection)
# Optimization for faster interactivity with volumes
interactive_volume_subsampling: bool = False
runtime: datetime = datetime.now()
timer_id: Any = None
playback_speed: int = 20 #ms
animation_playing: bool = False
animation_function: Any = None
time: float = 0.0
selection_changed: bool = False
selection_point_changed: bool = False
selection: Any = None
selection_type: Any = None
selection_point: Any = None # Point relative to absolute origin in VTK
selection_relative_point: Any = None # Point relative to the origin you define
selection_controller: Any = None
selection_value: float = None
selection_related_value: float = None # Custom value that you assign to a volume region (optional)
selection_related_name: str = None # Related name to the custom value
selection_camera_position: Any = None
selection_marker_visible: bool = True
selection_text_visible: bool = True
selection_outline_visible: bool = True
selection_color_bar_visible: bool = True
probe_initial_point1: Any = None
probe_initial_point2: Any = None
outline_opacity: float = 1.0
outline_color: list = field(default_factory=lambda:[0.5, 0.5, 0.5])
outline_width: int = 3
pickings: list = field(default_factory=list)
first_picked_position: np.ndarray = None
last_picked_position: np.ndarray = None
video_start_angle: float = 0.0
video_end_angle: float = 360.0
video_duration: float = 8.0
global_slicer: vtk.vtkPlaneCollection = None
[docs] def initialize(self):
"""
Initialize the model
"""
# We always want to have at least a camera
self.cameras.store(CameraModel())
[docs] def get_actor_type(self, actor):
"""
Get the type of the given actor
Vedo does a similar thing for some of its events:
"isPoints": isinstance(actor, vedo.Points),
"isMesh": isinstance(actor, vedo.Mesh),
"isAssembly": isinstance(actor, vedo.Assembly),
"isVolume": isinstance(actor, vedo.Volume),
"isPicture": isinstance(actor, vedo.Picture),
"isActor2D": isinstance(actor, vtk.vtkActor2D),
:param actor: vtkActor
:return: String
"""
qualified_type = str(type(actor)).lower()
obj_type = None
if isinstance(actor, vedo.Mesh) and ViewerModel.LINE in qualified_type:
obj_type = ViewerModel.LINE
elif isinstance(actor, vedo.Mesh):
obj_type = ViewerModel.SURFACE
elif isinstance(actor, vedo.Points) or isinstance(actor, vedo.Spheres) or isinstance(actor, obj.Points):
obj_type = ViewerModel.POINT
elif isinstance(actor, Volume) or isinstance(actor, vedo.Volume):
obj_type = ViewerModel.VOLUME
return obj_type
[docs] def got_selection(self):
"""
Whether a selection is set
:return: Boolean
"""
return self.selection is not None
[docs] def got_new_selection(self):
"""
Whether a the selection target changed
:return: Boolean
"""
return self.selection_changed and self.got_selection()
[docs] def is_selection_point(self):
"""
Whether the type of selection is a point cloud
:return: Boolean
"""
return self.selection_type == ViewerModel.POINT
[docs] def is_selection_line(self):
"""
Whether the type of selection is a set of lines
:return: Boolean
"""
return self.selection_type == ViewerModel.LINE
[docs] def is_selection_surface(self):
"""
Whether the type of selection is a surface mesh
:return: Boolean
"""
return self.selection_type == ViewerModel.SURFACE
[docs] def is_selection_volume(self):
"""
Whether the type of selection is a volume
:return: Boolean
"""
return self.selection_type == ViewerModel.VOLUME
[docs] def define_view_axis(self, normal):
if abs(normal[0]) < 0.05:
return ViewerModel.X_UP
elif abs(normal[1]) < 0.05:
return ViewerModel.Y_UP
elif abs(normal[2]) < 0.05:
return ViewerModel.Z_UP
[docs]class Viewer():
"""
This class is the container and context for having
an interactive VTK viewer with a controllable scene
and objects.
"""
def __init__(self):
"""
Constructor
"""
#logging.basicConfig(level=logging.DEBUG)
self.plot = None
self.plot_window_id = 0
self.num_windows = 1
self.model = ViewerModel()
self.model.initialize()
#self.views = Collection()
self.objects = OrderedDict()
self.controllers_map = OrderedDict()
self.selection_marker = None
self.selection_info = None
self.color_bar = None
self.last_mouse_press_position = None
self.last_mouse_release_position = None
self.left_mouse_down = False
self.last_mouse_time = datetime.now()
self.last_time_series_value = None
# UI related
self.outline_actor = None
self.axes_assembly = None
self.widgets_reflection = dict()
self.widgets = dict()
self.box_widget = None
self.line_widget = None
self.depth_picking = OrderedDict()
self.depth_peeling_enabled = True
vedo.settings.useDepthPeeling = True
vedo.settings.useFXAA = True
vedo.settings.multiSamples = 0
vedo.settings.alphaBitPlane = 1
vedo.settings.immediateRendering = False
# This line is necessary as for some reason,
# vedo's allowInteraction kills the app when we listen to TimerEvents!
vedo.settings.allowInteraction = False
vedo.settings.fonts_path = self.model.ui.font_path
vedo.settings.defaultFont = self.model.ui.font
font_params = {'islocal':True}
if vedo.settings.font_parameters is None:
vedo.settings.font_parameters = {}
vedo.settings.font_parameters[self.model.ui.font] = font_params
vedo.settings.enableDefaultKeyboardCallbacks = False
[docs] def initialize(self, offscreen=False, jupyter=False, embed_ui=False, embed_font_size=15,
plot=None, plot_window_id=0, num_windows=1, dark_mode=False, silent=False):
"""
Initialize the controller, main entry point to the viewer
:param context: Context of the visualization
:param embed_ui: Whether the UI is embed within the VTK window
:param embed_font_size: Embed font size. Defaults to 16 points. You might need larger values
in case you have a small screen with high dpi (but VTK methods fail to detect that).
:param jupyter: Whether we're running from a jupyter notebook or not
:param offscreen: Whether the rendering is done offscreen (headless) or not
:param plot: A vedo Plotter instance. You can either create it by yourself before hand, in case
you want to have multiple windows with other stats or let the controller create a new one
:param plot_window_id: Sub-window id where the 3D visualization will be displayed
:param num_windows: Number of subwindows, in case you want to display your own stuff later
:param render: Whether rendering occurs at the end of the initialization or not. Defaults to False
:param dark_mode: Whether the viewer is in dark mode
:param auto_select_first_object: Auto select the first object displayed
:param silent: Whether printing to console is disabled or not
"""
self.silent = silent
if not silent:
print('IBL Viewer...')
self.model.ui.font_size = embed_font_size
self.model.web_ui = jupyter
if self.model.web_ui:
vedo.embedWindow('ipyvtk')
self.plot = plot
window_size = self.model.web_window_size if self.model.web_ui else self.model.window_size
if plot is None:
self.plot = vedo.Plotter(N=num_windows, size=window_size,
title=self.model.title,
bg=self.model.ui.background_color,
offscreen=offscreen or jupyter)
self.plot_window_id = plot_window_id
self.plot.window.SetSize(*window_size)
self.set_renderer()
self.set_dark_mode(dark_mode, False)
'''
if jupyter:
self.plot.offscreen = offscreen if offscreen is not None else offscreen is None
self.plot.window.SetOffScreenRendering(self.plot.offscreen)
'''
try:
# In cases where we are headless, this will fail because there is no interactor
# but in cases where a headless window is embed into an app like Qt, this will work
# So everything's fine like this
self.initialize_window_interactions()
except Exception:
pass
self.model.ui.embed = embed_ui
if self.model.ui.embed:
self.model.ui.visible = True
self.model.ui.set_context(UIModel.DEFAULT)
self.initialize_vtk_ui(self.model.ui.embed_menu_x, self.model.ui.embed_menu_y)
self.update_ui()
# Generic UI part: prepares the cursor and draws a box outline on the selected object
self.initialize_selection_ui()
self.initialized()
#logging.info('Initialization complete.')
[docs] def initialize_window_interactions(self):
"""
Initialize window interactions on the VTK window
"""
# LeftButtonReleaseEvent does not work. You have to use EndInteractionEvent instead (go figure...)
# see https://stackoverflow.com/questions/52209534
self.add_callback('LeftButtonPressEvent', self.handle_left_mouse_press)
self.add_callback('EndInteractionEvent', self.handle_left_mouse_release)
self.plot.interactor.AddObserver('TimerEvent', self.handle_timer)
self.plot.interactor.AddObserver('KeyPressEvent', self.handle_key_press)
[docs] def initialize_selection_ui(self):
"""
Initialize selection marker and text
"""
#if self.model.ui.embed:
self.selection_info = self.add_text('selection_info', '', [0.02, 0.95],
color=self.model.ui.color)
self.plot.add(self.selection_info, render=False)
self.set_selection_marker()
[docs] def handle_key_press(self, iren, event):
"""
Handle key press events
:param iren: vtk iren
:param event: vtk event
"""
key = iren.GetKeySym().lower()
if 'esc' in key:
iren.ExitCallback()
elif 'space' in key:
self.clear_line_widget()
self.clear_box_widget()
[docs] def exit_interactive_mode(self):
"""
Exit/leave interactive mode
"""
self.plot.window.ExitCallback()
[docs] def toggle_dark_mode(self):
"""
Toggle dark/light mode
"""
self.set_dark_mode(not self.model.ui.dark_mode)
[docs] def set_dark_mode(self, on=True, update_ui=True):
"""
Set dark mode on or off
"""
self.model.ui.set_dark_mode(on)
if self.plot is not None:
self.plot.background(self.model.ui.background_color)
if self.color_bar is not None:
self.update_element_color(self.color_bar)
if not update_ui:
return
if self.model.ui.embed:
all_elements = self.model.ui.all_elements()
for key in all_elements:
element = all_elements[key]
self.update_element_color(element)
[docs] def set_renderer(self):
"""
Set VTK renderer, attempts to use OSPRay, if available
OSPRay is not supported (2021) by default and there is no
pip wheel for it with vtk, or paraview or any vtk-based tool.
So you can only rely on OSPRay if you compile it alongside VTK.
"""
renderer = self.plot.renderer
try:
ospray_pass= vtk.vtkOSPRayPass()
renderer.SetPass(ospray_pass)
node = vtk.vtkOSPRayRendererNode()
node.SetSamplesPerPixel(4,renderer)
node.SetAmbientSamples(4,renderer)
node.SetMaxFrames(4, renderer)
except (AttributeError, ImportError, NameError):
pass
# For some reason, depth peeling is sometimes not activated
# so it's further made active in volume.SlicerView.initialize_mapper()
# If you don't know about depth peeling: https://vtk.org/Wiki/VTK/Depth_Peeling
for renderer in self.plot.renderers:
renderer.UseDepthPeelingForVolumesOn()
renderer.SetOcclusionRatio(0.001)
renderer.SetMaximumNumberOfPeels(100)
[docs] def register_object(self, vtk_object, name=None, selectable=True):
"""
Register an object as selectable by the user in the UI
:param vtk_object: VTK object
:param name: Name or given id. IF None, the name of the
object is used
"""
if name is None:
name = vtk_object.name
existing_obj = self.objects.get(name)
if existing_obj != vtk_object and name in self.objects:
# Then we have two same names for two different objects
# let's change that
name = self.get_unique_object_name(name)
self.objects[name] = vtk_object
if selectable:
vtk_object.SetPickable(True)
# We overwrite the vtk object's name with the new one
vtk_object.name = name
self.update_selection_slider()
self.objects_changed()
[docs] def get_unique_object_name(self, name, spacer='_'):
"""
Get a unique key/name for selectable objects with similar names
:param name: Name (for instance 'Points')
:param spacer: Spacer char
:return: New name, for instance 'Points_4'
"""
return utils.get_unique_name(self.objects, name, spacer)
[docs] def unregister_object(self, name):
"""
Unregister an object from the selectable objects list
:param name: Object name or given id or int or the object itself
"""
if isinstance(name, int):
keys = list(self.objects.keys())
try:
name = keys[name]
except Exception:
pass
elif not isinstance(name, str):
for key in self.objects:
if name == self.objects[key]:
name = key
break
del self.objects[name]
self.update_selection_slider()
[docs] def update_selection_slider(self, max_value=None):
"""
Update the selection slider max value
:param max_value: Max value. If None, the maximum value
is the length of the self.objects
"""
if max_value is None:
max_value = len(self.objects)-1
slider = self.widgets.get('selection')
if slider is not None:
slider.GetRepresentation().SetMinimumValue(0)
slider.GetRepresentation().SetMaximumValue(max_value)
[docs] def register_controller(self, controller, vtk_objects):
"""
Register/map VTK objects to a view
:param controller: A controller instance (like VolumeController)
:param vtk_objects: All objects directly related to this view
"""
self.register_object(controller.actor)
if not isinstance(vtk_objects, list):
vtk_objects = [vtk_objects]
for obj in vtk_objects:
self.controllers_map[obj] = controller
[docs] def get_view_objects(self):
"""
Get all view objects registered in this model
:return: List
"""
return list(self.controllers_map.keys())
[docs] def get_view_objects_names(self):
"""
Get view objects names
:return: Dict
"""
names = {}
for obj in self.controllers_map:
names[obj.name] = obj
return names
[docs] def get_window(self):
"""
Get the plot window object. This is useful for displaying this window
in Jupyter notebooks for instance
:return: iren window object
"""
return self.plot.window
[docs] def render(self, save_to_file=None, width=None, height=None, scale=1):
"""
Render the current state of the viewer, optionally to a file.
Supported formats are jpg, png, pdf, svg, eps
:param save_to_file: File path
:param width: Width of the rendered image
:param height: Height of the rendered image
:param scale: Rendering scale factor. Defaults to 1
"""
self.plot.render()
if save_to_file is not None:
custom_size = isinstance(width, int) and isinstance(height, int)
if custom_size:
current_width, current_height = self.plot.window.GetSize()
# Set the desired size
self.plot.window.SetSize(width, height)
self.plot.show()
if scale > 1:
# Apparently it's better to enable the setting below when we use
# scaling for larger size rendering.
# There's a typo in the variable name and even the variable name
# is inappropriate. This isn't a screenshot but a render. (@marcomusy)
# So I put that in a try catch just in case it changes in the future...
try:
vedo.settings.screeshotLargeImage = True
except Exception:
pass
vedo.screenshot(save_to_file, scale)
if custom_size:
# Now reset the size as it was
self.plot.window.SetSize(current_width, current_height)
self.plot.show()
[docs] def show(self, interactive=True, actors=None, at=0, **kwargs):
"""
Render the plot and let the user interact with it
:param interactive: Whether we render and make the window interactive
:param actors: List of actors to show. Use this parameter only if you know what you're doing.
:param at: Which VTK window to use. Defaults to 0
"""
if actors is not None:
actors_to_show = actors
else:
actors_to_show = self.plot.actors
if self.model.selection is None:
self.select(-1)
#self.view_selected()
if not interactive:
self.plot.render()
if self.model.web_ui:
logging.info(f'\nVisualizer started in Web UI mode: ' + str(utils.time_diff(self.model.runtime)) + 's\n')
return self.plot.show(actors_to_show, at=at, resetcam=False, interactive=interactive, **kwargs)
#return ViewInteractiveWidget(self.plot.window)
else:
logging.info('\nVisualizer started: ' + str(utils.time_diff(self.model.runtime)) + 's\n')
#self.plot.window.SetWindowName()
return self.plot.show(actors_to_show, at=at, resetcam=False, interactive=interactive, **kwargs)
[docs] def close(self):
"""
Close the current plot
"""
self.plot.close()
[docs] def add_callback(self, event_name, func, priority=0.0):
"""
Add an event listener (aka callback method)
:param event_name: A VTK event name
:param func: Listener function
:param priority: Priority in event queue
:return: Callback id
"""
return utils.add_callback(self.plot, event_name, func, priority)
[docs] def remove_callback(self, callback_id):
"""
Add an event listener (aka callback method)
:param callback_id_or_event_name: A VTK event name
"""
self.plot.interactor.RemoveObserver(callback_id)
[docs] def handle_timer(self, iren, event, **kwargs):
"""
Handle vtk timer event
:param iren: Render window
:param event: TimerEvent
"""
if self.model.animation_playing:
valid = self.next_time_series()
if not valid:
self.plot.interactor.DestroyTimer(self.model.timer_id)
[docs] def handle_left_mouse_press(self, event):
"""
Handle left mouse down event. The event is triggered for any "pickable" object
:param mesh: Selected object
"""
if event is None:
return
self.last_mouse_press_position = np.array(event.picked2d)
self.left_mouse_down = True
# 1. Make sure the click occured on a vtk actor
actor = event.actor
return actor is not None
[docs] def clear_depth_picking(self, return_element=None):
"""
Clear depth picking, making all objects put in depth queue pickable again
:param return_element: An element to be returned by its index of insertion
:return: object and picked 3d coordinates (only if return_element is valid)
"""
picked3d = None
iter_id = 0
for obj in self.depth_picking:
if return_element == iter_id:
picked3d = self.depth_picking[obj]
obj.SetPickable(True)
iter_id += 1
self.depth_picking = OrderedDict()
if return_element:
return obj, picked3d
[docs] def get_actor_type(self, actor):
"""
Get what type is the given actor
:param actor: vtkActor
:return: String
"""
return self.get_actor_type(actor)
[docs] def handle_left_mouse_release(self, event):
"""
Handle left mouse up event.
:param event: vtkEvent
:return: Int value. 0 for invalid click, 1 for actor click,
2 for double click on actor
"""
state = 0
# 1. Make sure it's left mouse context
if not self.left_mouse_down:
return state
# Handle mouse click release with a workaround for bad
# VTK event mgmt (LeftButtonReleaseEvent does not work)
self.left_mouse_down = False
if event is None:
return
drag_delta = np.linalg.norm(self.last_mouse_press_position - event.picked2d)
# 2. If delta is larger than a few pixels, it means there was a click and drag
# which is not considered as a single click
actor = event.actor
if drag_delta > 4:
self.clear_depth_picking()
self.handle_drag_release(state, actor, event)
return state
if event.isActor2D:
self.handle_actor2d_click(state, actor, event)
return
picked3d = event.picked3d
if self.last_mouse_release_position is not None:
release_delta = np.linalg.norm(self.last_mouse_release_position - event.picked2d)
if release_delta > 2:
self.clear_depth_picking()
self.last_mouse_release_position = np.array(event.picked2d)
# 3. Make sure the click occured on a vtk actor
if actor is None:
self.handle_void_click(state, event)
return state
# 4. If we click on the same spot, maybe we want to select another object behind that one
camera_position = self.plot.camera.GetPosition()
actor_type = self.model.get_actor_type(actor)
if actor_type == ViewerModel.VOLUME:
controller = self.controllers_map.get(actor)
if controller is not None:
# Volume picking often results in a wrong coordinate so we compute it properly here
picked3d, value = controller.pick(camera_position, np.array(event.picked2d))
if actor in self.depth_picking:
# First disable temporarily the selected actor
actor.SetPickable(False)
# This gets needlessly complicated but there's no other way with VTK...
# So, the standard vtk.vtkPropPicker() that is hardware accelerated works
# for points, lines and partly for volumes (which is why we have the pick()
# method in VolumeController to readjust that).
# Now, vtkPropPicker fails to work on meshes that do not have ForceOpaqueOn()
# and obviously we want to keep transparency enabled so if we want to pick
# a transparent mesh, we have to use vtkCellPicker and then compare results...
cell_picker = vtk.vtkCellPicker()
picker = self.plot.picker # this is a vtk.vtkPropPicker()
x, y = event.picked2d
picker.Pick(x, y, 0.0, self.plot.renderer)
cell_picker.Pick(x, y, 0.0, self.plot.renderer)
valid_result = True
if picker.GetProp3D() is None and cell_picker.GetProp3D() is None:
valid_result = False
elif picker.GetProp3D() is not None and picker.GetProp3D() != actor:
actor = picker.GetProp3D()
elif cell_picker.GetProp3D() is not None:
actor = cell_picker.GetProp3D()
picker = cell_picker
else:
valid_result = False
if not valid_result:
result = self.clear_depth_picking(0)
if result is None:
actor.SetPickable(True)
else:
actor, picked3d = result
last = picked3d
picked3d = np.array(picker.GetPickPosition())
if np.linalg.norm(last - picked3d) > 0:
self.clear_depth_picking()
try:
actor.picked3d = picked3d
except AttributeError:
pass
event['picked3d'] = picked3d
delta3d = np.array([0,0,0])
try:
if actor.picked3d is not None:
delta3d = picked3d - actor.picked3d
actor.picked3d = picked3d
except AttributeError:
return
event['delta3d'] = delta3d
event['speed3d'] = np.sqrt(np.dot(delta3d,delta3d))
event['isPoints'] = isinstance(actor, vedo.Points)
event['isMesh'] = isinstance(actor, vedo.Mesh)
event['isAssembly'] = isinstance(actor, vedo.Assembly)
event['isVolume'] = isinstance(actor, vedo.Volume)
event['isPicture'] = isinstance(actor, vedo.Picture)
event['isActor2D'] = isinstance(actor, vtk.vtkActor2D)
self.depth_picking[actor] = event.picked3d
state = 1
# 5. Handle double click case
if utils.time_diff(self.last_mouse_time) < 0.5:
state = 2
self.last_mouse_time = datetime.now()
# We have a valid click event
self.handle_object_click(actor, state, event)
return state
[docs] def handle_actor2d_click(self, state, actor, event):
"""
Handle click on a 2D actor
"""
pass
[docs] def handle_void_click(self, state=None, event=None):
"""
Handle click and drag
"""
# Selection is None here
self.clear_box_widget()
self.clear_line_widget()
self._select()
self.update_selection_info()
self.set_outline_visibility(False)
self.set_color_bar_visibility(False)
# Event-like function that you may customize
self.selection_changed()
#self.update_ui()
[docs] def handle_drag_release(self, state, actor, event):
"""
Handle click and drag
"""
pass
[docs] def handle_object_click(self, actor, state=1, event=None):
"""
Handle valid object selection
:param actor: Selected actor
:param state: Number of mouse clicks
:param event: Event given by vedo
"""
# We ignore double clicks by default
if state != 1:
return
camera = self.plot.renderer.GetActiveCamera()
camera_position = np.array(camera.GetPosition())
# If a view container is found by reverse mapping, we get it here
view = self.controllers_map.get(actor)
self._select(actor, view, event, camera_position)
if self.model.selection_changed:
self.set_outline_visibility()
self.clear_line_widget()
self.clear_box_widget()
self.selection_changed()
elif self.model.selection_point_changed:
self.sub_selection_changed()
self.update_ui()
[docs] def objects_changed(self):
"""
Function called when a new object is registered
"""
pass
[docs] def initialized(self):
"""
Function called the viewer is initialized
"""
pass
[docs] def selection_changed(self):
"""
Function called when another object is selected.
"""
pass
[docs] def sub_selection_changed(self):
"""
Function called when for the same object, a new data point is selected
"""
pass
[docs] def get_object_names(self):
return list(self.objects.keys())
[docs] def get_selectable_key_by_id(self, value=0):
"""
Get an object by the id of the key (not by the key/name itself)
This is useful for sliders
:return: String
"""
object_ids = self.get_object_names()
key = None
try:
key = object_ids[int(value)]
except IndexError:
pass
return key
def _select(self, actor=None, controller=None, event=None,
camera_position=None, position=None, value=None):
"""
Set the current selected object in the model
:param actor: a vtkActor
:param controller: Controller of the given actor (optional)
:param event: a vedo event from which we use picked3d and picked2d (we could directly use vtk)
:param camera_position: position of the camera (optional) at selection time
:param position: The final position computed on the volume or mesh or point or line.
If not given, this will be automatically calculated
.param value: The value corresponding to the point on the object. If not given, this will
be automatically retrieved
"""
mdl = self.model
mdl.selection_changed = actor != mdl.selection
if actor is None:
mdl.selection_point_changed = False
mdl.selection = None
mdl.selection_controller = None
mdl.selection_type = None
mdl.selection_point = None
mdl.selection_relative_point = None
mdl.selection_value = None
mdl.selection_camera_position = None
return
if mdl.selection_changed:
# We assume that, even in the case where the point is the same
# as before, the selection point changed because it's a different object
mdl.selection_point_changed = True
mdl.selection_value = None
actor_type = mdl.get_actor_type(actor)
if event is None:
mdl.selection = actor
mdl.selection_controller = controller
mdl.selection_type = actor_type
return
if position is None or value is None:
# We're interested in the vedo class type like <class 'vedo.shapes.Line'>
if actor_type == ViewerModel.LINE:
mesh_id = actor.closestPoint(actor.picked3d, returnCellId=True)
value = actor.polydata().GetCellData().GetScalars().GetValue(mesh_id)
position = actor.picked3d
elif actor_type == ViewerModel.SURFACE:
point_id = actor.closestPoint(event.picked3d, returnPointId=True)
# Scalar values in volume are integers in this case
arr = actor.getPointArray()
if arr is not None:
value = arr[point_id]
position = event.picked3d# actor.points(point_id)
elif actor_type == ViewerModel.POINT:
# If on a sphere glyph, we will not get the center of it here
point_id = actor.closestPoint(actor.picked3d, returnPointId=True)
poly = actor.polydata()
scalars = poly.GetPointData().GetScalars()
if scalars is not None and 0 < point_id < scalars.GetNumberOfValues():
value = scalars.GetValue(point_id)
'''
[Optional] TODO: WIP below to have the position in the center of the glyph when using
non screen-space points.
# So we do another query to have the center position which
# correctly represents the given data
if actor.point_locator is None:
actor.point_locator = vtk.vtkPointLocator()
actor.point_locator.SetDataSet(poly)
actor.point_locator.BuildLocator()
pid = point_id
point_id = actor.point_locator.FindClosestPoint(actor.picked3d)
'''
position = poly.GetPoints().GetPoint(point_id)
elif actor_type == ViewerModel.VOLUME:
position, value = controller.pick(camera_position, np.array(event.picked2d))
if position is None or value is None:
return
if controller.slicers_selectable:
override_selection = False
if camera_position is None:
camera_position = mdl.cameras.current.GetPosition()
if 'volume' in str(type(controller)).lower():
actor = controller.actor
if mdl.selection_point is None and position is not None:
mdl.selection_point_changed = True
else:
p1 = np.array(mdl.selection_point)
mdl.selection_point_changed = np.linalg.norm(p1 - position) > 0.1
mdl.selection = actor
mdl.selection_controller = controller
mdl.selection_type = actor_type
mdl.selection_point = np.array(position)
mdl.selection_relative_point = np.array(position) - mdl.origin
mdl.selection_value = value
mdl.selection_camera_position = camera_position
[docs] def select(self, target=None, allow_none=False):
"""
Select an object, either by its name (or subname) or by
reference to the vtk object.
:param target: Either the vtk actor or its name
or its index (an int from object.keys())
:param allow_none: Whether an invalid target is accepted
as clicking on nothing (to cancel the current selection)
"""
if isinstance(target, str):
key = target
target = self.objects.get(target)
if not allow_none and target is None:
keys = list(self.objects.keys())
if not self.silent:
print('Could not select object with given key',
key, 'Please use a name from this list', keys)
elif isinstance(target, int) or isinstance(target, float):
target = int(target)
key = self.get_selectable_key_by_id(target)
try:
target = self.objects.get(key, None)
except Exception:
pass
if target is None:
if allow_none:
self.handle_void_click()
else:
return
else:
# Case where an object was removed and the user wants to add it back
if target not in self.plot.actors:
self.plot.add(target)
self.handle_object_click(target)
if self.model.cameras.current.autofocus:
self.view_selected()
self.update_ui()
[docs] def get_selection_info(self):
"""
Get information about the current selection
:return: Preformatted multiline text and a dictionary of extra data
"""
if self.model.selection is None:
text = ''
else:
text = f'{self.model.selection.name}'
data_type = self.model.selection_type.title()
if self.model.selection_point is not None:
# We display a point relative to the origin set by the user
relative_point = self.model.selection_relative_point
text += f'\n\nX: {relative_point[0]:0.2f}'
text += f'\nY: {relative_point[1]:0.2f}'
text += f'\nZ: {relative_point[2]:0.2f}'
if self.model.selection_value is not None:
text += f'\n\n{data_type} value: {self.model.selection_value}'
return text, {}
[docs] def update_selection_info(self):
"""
Update text and point information for the current selection
"""
text, data = self.get_selection_info()
if self.selection_marker is not None:
if self.model.selection is None:
self.selection_marker.SetVisibility(False)
else:
cond = self.model.selection_point is not None and self.model.selection_marker_visible
self.selection_marker.SetVisibility(cond)
self.selection_marker.pos(self.model.selection_point)
self.selection_marker.color(self.model.ui.color)
if self.selection_info is not None:
self.selection_info.GetMapper().SetInput(text)
self.selection_info.SetVisibility(self.model.selection_text_visible)
[docs] def draw_axes(self):
"""
Draw axes around selection
"""
if self.model.selection is None:
return
if self.axes_assembly is not None:
self.plot.remove(self.axes_assembly, render=False)
xr = [0, 0, -180]
yr = [-180, 0, -180]
zr = [-180, -45, 0]
font = self.model.ui.font
self.axes_assembly = vedo.addons.Axes(self.model.selection, hTitleRotation=xr,
xLabelRotation=xr, yLabelRotation=yr, zLabelRotation=zr,
xTitleRotation=xr, yTitleRotation=yr, zTitleRotation=zr,
labelFont=font, titleFont=font, hTitleFont=font,
xyGrid=False, yzGrid=False, zxGrid=False) #buildRulerAxes(actor)
self.plot.add(self.axes_assembly)
def set_outline_visibility(self, on=None):
"""
Show/hide the outline
:param on: Visibility boolean. If None, the value from the model will be used
"""
if on is None:
on = self.model.selection_outline_visible
if self.outline_actor is not None:
self.outline_actor.SetVisibility(on)
[docs] def draw_outline(self, auto_hide_if_no_selection=True):
"""
Draw the bounding box of the current selection.
:param auto_hide_if_no_selection: Whether we hide the outline when there is no selection
"""
if self.model.selection is None:
if auto_hide_if_no_selection:
self.set_outline_visibility(False)
return
if 'volume' in utils.get_type(self.model.selection):
outline = vtk.vtkVolumeOutlineSource()
outline.GenerateOutlineOn()
outline.SetColor(self.model.outline_color)
outline.SetVolumeMapper(self.model.selection.mapper())
outline.Update()
else:
outline = vtk.vtkOutlineFilter()
#outline.GenerateOutlineOn()
outline.SetInputData(self.model.selection.polydata())
outline.Update()
if self.outline_actor is not None:
# Update existing outline
pdm = self.outline_actor.mapper()
pdm.SetInputConnection(outline.GetOutputPort())
pdm.Update()
self.set_outline_visibility()
else:
# Create a new outline object
pdm = vtk.vtkPolyDataMapper()
pdm.SetInputConnection(outline.GetOutputPort())
pdm.Update()
outline_actor = vedo.Mesh(pdm.GetInput())
if 'volume' in utils.get_type(self.model.selection):
# vtkVolumeOutlineSource doesn't account for the position of the object
# so we have to move the outline here
outline_actor.pos(self.model.selection.pos())
outline_actor.SetPickable(False)
outline_actor.GetProperty().SetOpacity(self.model.outline_opacity)
outline_actor.GetProperty().SetColor(self.model.outline_color)
outline_actor.GetProperty().SetLineWidth(self.model.outline_width)
self.outline_actor = outline_actor
self.plot.add(outline_actor)
[docs] def add_segments(self, points, end_points=None, line_width=2, values=None, color_map='Accent',
name='Segments', use_origin=True, add_to_scene=True,
relative_end_points=False, spherical_angles=None, radians=True):
"""
Add a set of segments (lines made of two points). The difference with add_lines()
is that you have more options like giving spherical angles and setting relative end points.
:param points: 3D numpy array of points of length n
:param end_points: 3D numpy array of points of length n
:param line_width: Line width, defaults to 2px
:param values: 1D list of length n, for one scalar value per line
:param color_map: A color map, it can be a color map built by IBLViewer or
a color map name (see vedo documentation), or a list of values, etc.
:param name: Name to give to the object
:param use_origin: Whether the current origin (not necessarily absolute 0) is used as offset
:param add_to_scene: Whether the new lines are added to scene/plot and rendered
:param relative_end_points: Whether the given end point is relative to the start point. False by default,
except is spherical coordinates are given
:param spherical_angles: 3D numpy array of spherical angle data of length n
In case end_points is None, this replaces end_points by finding the relative
coordinate to each start point with the given radius/depth, theta and phi
:param radians: Whether the given spherical angle data is in radians or in degrees
:return: Lines
"""
# The base assumption is that points is a 2D array, each row having a start and end point
# but you may also pass a 1D list of positions for points and another 1D list of
# size N positions for end_points. Alternatively, you may pass spherical angles for end points.
if end_points is None and spherical_angles is not None:
relative_end_points = True
spherical_angles = np.array(spherical_angles)
if radians:
end_points = spherical_angles.apply(vedo.spher2cart)
else:
end_points = spherical_angles.apply(utils.spherical_degree_angles_to_xyz)
if relative_end_points:
end_points += points
points = np.c_[points, end_points].reshape(-1, 2, 3)
elif end_points is not None and len(points) != len(end_points):
n = min(len(points), len(end_points))
logging.error(f'[add_segments() error] Mismatch between start and end points length. Only {n} segments shown.')
points = np.c_[points[n], end_points[n]].reshape(-1, 2, 3)
return self.add_lines(points, line_width, values, color_map, name, use_origin, add_to_scene)
[docs] def add_lines(self, points, line_width=2, values=None, color_map='Accent',
name='Lines', use_origin=True, add_to_scene=True):
"""
Create a set of lines with given point sets
:param points: List or 2D array of 3D coordinates
:param line_width: Line width, defaults to 2px
:param values: 1D list of length n, for one scalar value per line
:param color_map: A color map, it can be a color map built by IBLViewer or
a color map name (see vedo documentation), or a list of values, etc.
:param name: Name to give to the object
:param use_origin: Whether the current origin (not necessarily absolute 0) is used as offset
:param add_to_scene: Whether the new lines are added to scene/plot and rendered
:return: objects.Lines
"""
if not isinstance(points, np.ndarray):
points = np.array(points, dtype=object)
if use_origin:
points += self.model.origin
if values is None:
values = np.arange(len(points))
lines = obj.Lines(points, lw=line_width)
lines.lighting(0)
lines.cmap(color_map, values, on='cells', arrayName='data')
# This method renames the object with a suffix if a conflicting name is found
self.register_object(lines, name)
if add_to_scene:
self.plot.add(lines)
return lines
[docs] def add_spheres(self, positions, radius=10, values=None, color_map='Accent', name='Points',
use_origin=True, add_to_scene=True, **kwargs):
"""
Add new spheres. This is a shortcut for add_points(screen_space=False)
:param positions: 3D array of coordinates
:param radius: List same length as positions of radii. The default size is 5um, or 5 pixels
in case as_spheres is False.
:param values: 1D array of values, one per neuron or a time series of such 1D arrays (numpy format)
:param color_map: A color map, it can be a color map built by IBLViewer or
a color map name (see vedo documentation), or a list of values, etc.
:param name: All point neurons are grouped into one object, you can give it a custom name
:param use_origin: Whether the current origin (not necessarily absolute 0) is used as offset
:param add_to_scene: Whether the new lines are added to scene/plot and rendered
:return: objects.Points
"""
# If you use obj.Spheres, you have to do it this way
#spheres = obj.Spheres(positions, radius, color_map, **kwargs)
#spheres.cmap(color_map, values)
return self.add_points(positions, radius, values, color_map, name, False,
use_origin, add_to_scene, **kwargs)
[docs] def add_points(self, positions, radius=10, values=None, color_map='Accent', name='Points',
screen_space=True, use_origin=True, add_to_scene=True, **kwargs):
"""
Add new points, either as screen-space dots or as 3D spheres (see screen_space param)
:param positions: 3D array of coordinates
:param radius: List same length as positions of radii. The default size is 5um, or 5 pixels
in case as_spheres is False.
:param values: 1D array of values, one per neuron or a time series of such 1D arrays (numpy format)
:param color_map: A color map, it can be a color map built by IBLViewer or
a color map name (see vedo documentation), or a list of values, etc.
:param name: All point neurons are grouped into one object, you can give it a custom name
:param screen_space: Type of point, Defaults to True. If True then the points always occupy
the same amount of pixels defined by the radius, this is the fastest method to display
large amount of points. If False, then points are 3D spheres and you see them larger
when you zoom closer to them with the camera.
:param use_origin: Whether the current origin (not necessarily absolute 0) is used as offset
:param add_to_scene: Whether the new lines are added to scene/plot and rendered
:return: objects.Points
"""
if use_origin:
positions = np.array(positions) + self.model.origin
if values is None:
values = np.arange(len(positions))
# You cannot easily set a time series to vedo.Spheres()
# so this one of the reasons why objects.Points exists
points = obj.Points(positions, radius, values, color_map, screen_space, **kwargs)
points.lighting('off')
# This method renames the object with a suffix if a conflicting name is found
self.register_object(points, name)
if add_to_scene:
self.plot.add(points)
return points
[docs] def add_mesh(self, file_path, mesh_name=None, use_origin=True, add_to_scene=True):
"""
Add a surface mesh to the scene
:param file_path: Mesh file path (any kind of file supported by vedo)
:param mesh_name: Name of the mesh. If None, the file name will be used.
:param use_origin: Whether the origin is used in positioning the mesh
:param add_to_scene: Whether the new lines are added to scene/plot and rendered
:return: Mesh
"""
return self.add_surface_mesh(file_path, mesh_name, use_origin, add_to_scene)
[docs] def add_surface_mesh(self, file_path, mesh_name=None, use_origin=True, add_to_scene=True):
"""
Add a surface mesh to the scene
:param file_path: Mesh file path (any kind of file supported by vedo)
:param mesh_name: Name of the mesh. If None, the file name will be used.
:param use_origin: Whether the origin is used in positioning the mesh
:param add_to_scene: Whether the new lines are added to scene/plot and rendered
:return: Mesh
"""
mesh = vedo.load(file_path)
if use_origin:
mesh.pos(self.model.origin)
name = mesh_name if mesh_name is not None else utils.split_path(file_path)[1]
# This method renames the object with a suffix if a conflicting name is found
self.register_object(mesh, name)
# There is a bug in VTK 9 that prevents clicking on transparent objects
# as reported on vedo's tracker https://github.com/marcomusy/vedo/issues/291
# The "Force opaque fix" below should be gone with the next VTK update hopefully.
# In the meantime, we use this.
# TODO: remove this when this bug is fixed in VTK
#mesh.ForceOpaqueOn()
if add_to_scene:
self.plot.add(mesh)
return mesh
[docs] def add_volume(self, data=None, resolution=None, file_path=None, color_map='viridis',
alpha_map=None, select=False, add_to_scene=True, transpose=None):
"""
Add a volume to the viewer with box clipping and slicing enabled by default
:param data: Volume image data or a file_path
:param resolution: Resoluton of the volume
:param file_path: File path of the volume. If you don't provide an image volume data,
then the file_path will be used to load the volume data
:param color_map: Color map for the volume
:param alpha_map: Alpha map for the volume. If None, it will assume that 0 values
are transparent and maximum values are opaque
:param select: Whether the volume is selected
:param add_to_scene: Whether the volume is added to scene
:param transpose: Transposition parameter. If None. nothing happens. If True,
then the default IBL transposition is applied. You can provide your own, that is,
a list of 3 elements to reorder the volume as desired.
:return: VolumeController
"""
if isinstance(data, str) and file_path is None:
file_path = data
data = None
model = VolumeModel(resolution=resolution, file_path=file_path, data=data)
if data is None:
model.load_volume(file_path)
if transpose is not None:
model.transpose(transpose)
model.compute_size()
if alpha_map is None:
alpha_map = np.linspace(0.0, 1.0, 10)
#alpha_map = np.flip(alpha_map)
controller = VolumeController(self.plot, model, add_to_scene=add_to_scene)
controller.set_color_map(color_map, alpha_map)
# register_object is automatically called on view.actor within register_controller
self.register_controller(controller, controller.get_related_actors())
if select:
self.select(controller.actor)
self.view_selected()
return controller
[docs] def remove_object(self, target=None, unregister=False):
"""
Remove an object from the plot
:param target: Target object
:param unregister: Whether the object is also unregistered, which
means that it's not in the list of objects anymore and cannot
be recovered using select(object_name)
"""
deselect = False
if target is None:
target = self.model.selection
deselect = True
if target is None:
return
if deselect:
self.handle_void_click()
self.plot.remove(target)
if unregister:
self.unregister_object(target)
self.update_ui()
[docs] def camera_context(self):
"""
Set camera context
"""
self.set_menu_context('camera', UIModel.CAMERA)
[docs] def scene_context(self):
"""
Set scene context
"""
self.set_menu_context('scene', UIModel.SCENE)
[docs] def data_context(self):
"""
Set data context
"""
self.set_menu_context('data', UIModel.DATA)
[docs] def object_context(self):
"""
Set object context
"""
self.set_menu_context('object', UIModel.OBJECT)
[docs] def time_series_context(self):
"""
Set time series context
"""
self.set_menu_context('time_series', UIModel.TIME_SERIES)
[docs] def export_context(self):
"""
Set export context
"""
self.set_menu_context('export', UIModel.EXPORT)
[docs] def toggle_marker_type(self):
"""
Toggle marker type (between cross and sphere)
"""
if isinstance(self.selection_marker, obj.Cross3D):
self.set_selection_marker('sphere')
else:
self.set_selection_marker('cross')
[docs] def set_selection_marker(self, marker_type='cross', size=500, cross_thickness=3):
"""
Set the selection marker type. Accepted values currently are 'cross' or 'sphere'
:param marker_type: Marker type
"""
if self.selection_marker is not None:
#position = self.selection_marker.pos()
self.plot.remove(self.selection_marker, render=False)
if marker_type.lower() == 'cross':
self.selection_marker = obj.Cross3D(size=size, thickness=cross_thickness, color=self.model.ui.color)
else:
self.selection_marker = vedo.Sphere(r=size / 2, c=self.model.ui.color)
self.selection_marker.pickable(False)
if self.model.selection_point is not None:
self.selection_marker.pos(self.model.selection_point)
self.plot.add(self.selection_marker, render=False)
self.selection_marker.lighting('off')
#self.selection_marker.GetProperty().SetOpacity(0.5)
[docs] def initialize_vtk_ui(self, x=40, y=50, sx=None, sy=None, sw=None):
"""
Initialize a per-context VTK UI.
:param x: Base screen-space x coordinate
:param y: Base screen-space y coordinate (starts from the bottom in VTK)
"""
if sx is None:
sx = self.model.ui.embed_submenu_x
if sy is None:
sy = self.model.ui.embed_submenu_y
if sw is None:
sw = self.model.ui.embed_slider_width
self.add_button('camera', self.camera_context, [x, y+200], 'Camera', toggle=True)
self.add_button('scene', self.scene_context, [x, y+150], 'Scene', toggle=True)
self.add_button('object', self.object_context, [x, y+100], 'Object', toggle=True)
self.add_button('data', self.data_context, [x, y+50], 'Data', toggle=True)
self.add_button('export', self.export_context, [x, y], 'Export', toggle=True)
self.add_camera_embed_ui(sx, sy, sw)
self.add_scene_embed_ui(sx, sy, sw)
self.add_object_embed_ui(sx, sy, sw)
self.add_data_embed_ui(sx, sy, sw)
self.add_export_embed_ui(sx, sy, sw)
[docs] def add_camera_embed_ui(self, x, y, sw, nc=150):
"""
Add camera-context UI
:param x: Base X position
:param y: Base Y position (0 is at the bottom of the screen)
:param sw: Slider width
"""
self.model.ui.toggle_context(UIModel.CAMERA)
self.add_button('anterior', self.set_anterior_view, [x, y+120], 'Anterior')
self.add_button('posterior', self.set_posterior_view, [x+nc, y+120], 'Posterior')
self.add_button('dorsal', self.set_dorsal_view, [x, y+80], 'Dorsal')
self.add_button('ventral', self.set_ventral_view, [x+nc, y+80], 'Ventral')
self.add_button('left', self.set_left_view, [x, y+40], 'Left')
self.add_button('right', self.set_right_view, [x + nc, y + 40], 'Right')
self.add_button('ortho', self.toggle_orthographic_view, [x, y], 'Orthographic', toggle=True)
# or better yet, group these buttons into a labelled collection? If this is any useful later on?
#self.model.ui.register([b1, b2, b3, b4, b5, b6, b7], UIModel.CAMERA)
self.model.ui.toggle_context(UIModel.CAMERA)
[docs] def add_scene_embed_ui(self, x, y, sw):
"""
Add scene-context UI
:param x: Base X position
:param y: Base Y position (0 is at the bottom of the screen)
:param sw: Slider width
"""
self.model.ui.toggle_context(UIModel.SCENE)
s_kw = self.model.ui.slider_config
#self.add_button('move_pivot', self.move_camera_pivot, [x, y + 90], 'Move camera pivot')
self.add_button('previous_selection', self.select_previous_object, [x, y-5], '<')
self.add_slider('selection', self.select_object_by_id, 0, 0, 0, [x+55, y, sw], precision=0, **s_kw)
self.add_button('next_selection', self.select_next_object, [x+sw+80, y-5], '>')
self.add_button('autofocus', self.toggle_autofocus, [x, y+40],
['Autofocus: Off', 'Autofocus: On'], toggle=True, state=1)
self.add_button('view_selected', self.view_selected, [x, y+80], 'Focus on selection')
self.add_button('dark_mode', self.toggle_dark_mode, [x+400, y+80],
['Dark UI', 'Light UI'], toggle=True, state=0 if self.model.ui.dark_mode else 1)
self.add_button('marker', self.toggle_marker, [x+400, y+40],
['Marker: Off', 'Marker: On'], toggle=True, state=1)
#self.add_button('clickable', self.toggle_pickable, [x, y], ['Clickable: On', 'Clickable: Off'], toggle=True)
self.add_button('toggle_info', self.toggle_info, [x+200, y+40],
['Info overlay: Off', 'Info overlay: On'], toggle=True, state=1)
self.add_button('toggle_outline', self.toggle_outline, [x+200, y+80],
['Bounding box: Off', 'Bounding box: On'], toggle=True, state=1)
#self.add_button('view_slices', self.toggle_view_slices, [x, y], ["Hide slices", "Show slices"], True)
#self.axes_button = self.add_button(self.toggle_axes_visibility, pos=(50, 0.78), ["Show axes", "Hide axes"], True)
self.model.ui.toggle_context(UIModel.SCENE)
[docs] def add_object_embed_ui(self, x, y, sw):
"""
Add object-context UI
:param x: Base X position
:param y: Base Y position (0 is at the bottom of the screen)
:param sw: Slider width
"""
s_kw = self.model.ui.slider_config
# TODO: add option to use same slicer for all objects
self.model.ui.toggle_context(UIModel.OBJECT)
#self.add_button('global_slicer', self.toggle_global_slicer, [x, y+145], ['Global slicer: OFF', 'Global slicer: ON'], toggle=True)
self.add_button('box_widget', self.add_box_widget, [x, y+40], 'Cutter/Slicer')
self.add_button('remove_object', self.remove_object, [x+130, y+40])
self.add_button('isosurface', self.isosurface, [x+280, y+40])
#self.add_slider('isosurface', self.update_isosurface, 0, 1200, 200, [x+2*sw+80, y-5, sw], precision=2, **s_kw)
self.add_button('hollow_volume', self.toggle_hollow_mode, [x+380, y+40], 'Hollow regions', toggle=True)
self.add_slider('opacity', self.update_opacity, 0.0, 1.0, 0.9, [x, y-5, sw], precision=2, **s_kw)
self.add_slider('slices_opacity', self.update_slices_opacity, 0, 1.0, 0.75, [x+sw+40, y-5, sw], precision=2, **s_kw)
#self.add_button('slices_visibility', self.toggle_slices_visibility, [x, yb + 130], ["Hide slices", "Show slices"])
'''
# Code for adding six sliders for box slicing.
self.add_slider('-x', self.update_nx_slicer, -1, 0, 0, [x+sw+40, y+70, sw], oid=0, **s_kw)
self.add_slider('+x', self.update_px_slicer, 0, 1, 0, [x, y+70, sw], oid=1, **s_kw)
self.add_slider('-y', self.update_ny_slicer, -1, 0, 0, [x+sw+40, y+35, sw], oid=2, **s_kw)
self.add_slider('+y', self.update_py_slicer, 0, 1, 0, [x, y+35, sw], oid=3, **s_kw)
self.add_slider('-z', self.update_nz_slicer, -1, 0, 0, [x+sw+40, y, sw], oid=4, **s_kw)
self.add_slider('+z', self.update_pz_slicer, 0, 1, 0, [x, y, sw], oid=5, **s_kw)
'''
self.model.ui.toggle_context(UIModel.OBJECT)
[docs] def add_data_embed_ui(self, x, y, sw):
"""
Add data-context UI
:param x: Base X position
:param y: Base Y position (0 is at the bottom of the screen)
:param sw: Slider width
"""
self.model.ui.toggle_context(UIModel.DATA)
s_kw = self.model.ui.slider_config
self.add_slider('data_time_series', self.update_time_series, 0, 1, 0, [x, y-5, sw], **s_kw)
self.add_button('play_time_series', self.play_time_series, [x, y+40],
['Play time series', 'Pause time series'], toggle=True)
self.add_button('new_probe', self.add_probe, [x+180, y+40])
self.add_button('move_probe', self.edit_probe, [x+180, y+40])
self.model.ui.toggle_context(UIModel.DATA)
[docs] def add_export_embed_ui(self, x, y, sw):
"""
Add export-context UI
:param x: Base X position
:param y: Base Y position (0 is at the bottom of the screen)
:param sw: Slider width
"""
s_kw = self.model.ui.slider_config
self.model.ui.toggle_context(UIModel.EXPORT)
self.add_button('export_image', self.export_image, [x, y], 'Export image')
self.add_button('export_turntable_video', self.export_turntable_video, [x + 150, y], 'Export 360 video')
self.add_slider('video_duration', self.update_video_duration, 0, 60,
self.model.video_duration, [x + 150, y+120, sw], 'Video duration (s)', **s_kw)
self.add_slider('start_angle', self.update_video_start_angle, 0, 360, 0, [x + 150, y+80, sw], **s_kw)
self.add_slider('end_angle', self.update_video_end_angle, 0, 360, 360, [x + 150, y+40, sw], **s_kw)
self.model.ui.toggle_context(UIModel.EXPORT)
[docs] def update_scene_ui(self, context_elements):
"""
Update the scene-related UI
:param context_elements: Current UI context elements
"""
if not self.model.ui.is_scene_context():
return
slider = self.model.ui.get_element('selection')
value = 0
got_value = False
for key in self.objects:
if self.objects[key] == self.model.selection:
got_value = True
break
value += 1
if got_value:
slider.update(widget=slider, value=value)
[docs] def is_volume(self, target=None):
"""
Get whether the target object is a volume
:param target: vtkActor. If None, the current selected object is used.
:return: Boolean
"""
if target is None:
target = self.model.selection
if target is None:
return
return isinstance(target, vedo.Volume)
[docs] def get_selection_opacity(self):
"""
Get the opacity of the current selected object
:return: Float or None
"""
opacity_value = None
try:
opacity_value = self.model.selection.GetProperty().GetOpacity()
except Exception:
if isinstance(self.model.selection_controller, VolumeController):
opacity_value = self.model.selection_controller.get_opacity()
return opacity_value
[docs] def get_selection_slices_opacity(self):
"""
Get the opacity of the current volume slices
:return: Float or None
"""
slices_opacity_value = None
if isinstance(self.model.selection_controller, VolumeController):
slices_opacity_value = self.model.selection_controller.get_slices_opacity()
return slices_opacity_value
[docs] def update_object_ui(self, context_elements):
"""
Update the object mode UI
:param context_elements: Current UI context elements
"""
if not self.model.ui.is_object_context():
return
volume_mode = isinstance(self.model.selection_controller, VolumeController)
self.set_element_visibility(self.model.ui.get_element('hollow_volume'), volume_mode)
self.set_element_visibility(self.model.ui.get_element('view_region'), volume_mode)
slider = self.widgets.get('opacity')#self.model.ui.get_element('opacity')
opacity_value = self.get_selection_opacity()
if opacity_value is not None:
slider.update(widget=slider, value=opacity_value)
slider = self.widgets.get('slices_opacity')#model.ui.get_element('slices_opacity')
slices_opacity_value = self.get_selection_slices_opacity()
if slices_opacity_value is not None:
slider.update(widget=slider, value=slices_opacity_value)
'''
# Below code not necessary anymore, we keep it here as a reference
# if you want to update sliders for slicing instead of using vtk widgets
dimensions = utils.get_actor_dimensions(self.model.selection)
for element_name in context_elements:
element = context_elements[element_name]
axis = None
slicers = None
try:
axis = element.oid
slicers = self.model.selection_controller.slicers
except Exception:
continue
model = slicers.get(axis).model
mod = axis % 2
orientation = -1 if mod == 0 else 1
if orientation == 1:
min_v = 0
max_v = dimensions[model.axis] * orientation
else:
min_v = dimensions[model.axis] * orientation
max_v = 0
rep = element.GetRepresentation()
rep.SetMinimumValue(min_v)
rep.SetMaximumValue(max_v)
if model.value is not None:
element.update(widget=element, value=model.value)
'''
[docs] def update_data_ui(self, context_elements):
"""
Update data UI
:param context_elements: Current UI context elements
"""
if not self.model.ui.is_data_context():
return
volume_mode = isinstance(self.model.selection_controller, VolumeController)
self.set_element_visibility(self.model.ui.get_element('new_probe'), volume_mode)
probe_mode = self.is_probe(self.model.selection)
self.set_element_visibility(self.model.ui.get_element('move_probe'), probe_mode)
slider = self.widgets.get('data_time_series')
current_value, max_value = self.get_time_series_info()
if slider is not None:
if current_value is not None:
slider.update(widget=slider, value=current_value)
slider.GetRepresentation().SetMinimumValue(0)
slider.GetRepresentation().SetMaximumValue(max_value)
self.draw_color_bar()
[docs] def get_time_series_info(self, target=None):
"""
Get the number of time series and id of the current one for a target.
If None, then the current selection is used
:param target: a vtk object
:return: Int, Int
"""
if target is None:
target = self.model.selection
if target is None:
return None, None
# Only volume object has a controller in iblviewer
controller = self.controllers_map.get(target) #self.model.selection_controller
max_value = 0
current_value = None
if isinstance(controller, VolumeController):
max_value = len(controller.model.luts)-1
current_value = controller.model.luts.current_key_id
else:
try:
point_data = target.polydata().GetPointData()
#name = target.polydata().GetPointData().GetArrayName()
if isinstance(target, obj.Points):
# We use this method to ignore unwanted arrays that are automatically
# created by VTK
max_value = target.get_number_of_arrays() - 1
else:
max_value = point_data.GetNumberOfArrays() - 1
polydata = target.polydata()
current_name = polydata.GetPointData().GetScalars().GetName()
# Forced to make a loop here in order to get the current id...
for array_id in range(max_value):
if point_data.GetArray(array_id).GetName() == current_name:
current_value = array_id
break
#current_value = point_data.GetAbstractArray(current_name)
except Exception:
return None, None
return current_value, max_value
[docs] def update_ui(self):
"""
Update the UI
"""
self.update_selection_info()
if self.model.got_selection():
self.draw_outline()
if self.model.selection_changed or self.model.selection_point_changed:
self.draw_color_bar()
self.set_color_bar_visibility()
if self.model.ui.embed:
context_elements = self.model.ui.get_elements()
if self.model.ui.context_changed:
default_elements = self.model.ui.get_elements(UIModel.DEFAULT)
context_elements.update(default_elements) # concatenates dictionaries
self.set_ui_visibility(False, context_elements)
self.update_scene_ui(context_elements)
self.update_object_ui(context_elements)
self.update_data_ui(context_elements)
[docs] def set_color_bar_visibility(self, on=None):
"""
Show or hide the color bar
:param on: Visibility boolean
"""
if self.color_bar is not None:
if on is None:
on = self.model.selection_color_bar_visible
self.color_bar.SetVisibility(on)
[docs] def draw_color_bar(self):
"""
Draw a color bar for the current selection
"""
ui = self.model.ui
selection = self.model.selection
controller = self.model.selection_controller
if selection is None:
return
lut = None
if self.is_probe(selection):
controller = selection.target_controller
if controller is not None and hasattr(controller.model, 'luts'):
if controller.model.luts.current is not None:
lut = controller.model.luts.current.scalar_lut
if lut is None and isinstance(controller, VolumeController):
lut = vedo.utils.ctf2lut(controller.actor)
elif lut is None:
# Default way to get a LUT for the scalar/color bar
lut = selection.mapper().GetLookupTable()
if lut is None:
return
#if self.color_bar is not None:
#self.set_color_bar_visibility(True)
#self.plot.remove(self.color_bar, render=False)
if self.color_bar is not None:
utils.update_scalar_bar(self.color_bar, lut)
else:
self.color_bar = utils.add_scalar_bar(lut, pos=(0.85,0.04),
font_color=ui.color, titleFontSize=ui.font_size)
self.plot.add([self.color_bar])
[docs] def clear_color_bar(self):
"""
Clear the color bar
"""
self.plot.remove(self.color_bar)
[docs] def set_element_visibility(self, element, visible=True):
"""
Set a UI element's visibility
:param element: a vtk2DActor or something like that (changes for sliders, texts, etc.)
:param visible: Visible or not
"""
t = utils.get_type(element)
if 'slider' in t:
element.GetRepresentation().SetVisibility(visible)
element.GetRepresentation().SetPickable(visible)
if visible:
element.EnabledOn()
else:
element.EnabledOff()
#element.GetRepresentation().PickingManagedOn()
if 'text' in t:
element.SetVisibility(visible)
elif 'button' in t:
element.actor.SetVisibility(visible)
element.actor.SetPickable(visible)
[docs] def update_element_color(self, element):
"""
Set a UI element's color
:param element: a vtk2DActor or something like that (changes for sliders, texts, etc.)
"""
t = utils.get_type(element)
if 'slider' in t:
config = self.model.ui.slider_config
color = vedo.getColor(config['c'])
rep = element.GetRepresentation()
rep.GetLabelProperty().SetColor(color)
rep.GetTubeProperty().SetColor(color)
#rep.GetTubeProperty().SetOpacity(0.6)
rep.GetSliderProperty().SetColor(color)
rep.GetSelectedProperty().SetColor(np.sqrt(np.array(color)))
rep.GetCapProperty().SetColor(color)
rep.GetTitleProperty().SetColor(color)
element.label.property.SetColor(color)
if 'text' in t:
config = self.model.ui.slider_config
color = vedo.getColor(config['c'])
element.property.SetColor(color)
elif 'button' in t:
if element.toggle:
config = self.model.ui.toggle_config
else:
config = self.model.ui.button_config
colors = config['c']
background_colors = config['bc']
status = element.statusIdx
element.colors = colors
element.bcolors = background_colors
element.textproperty.SetColor(vedo.getColor(element.colors[status]))
bcc = np.array(vedo.getColor(element.bcolors[status]))
element.textproperty.SetBackgroundColor(bcc)
if element.showframe:
#element.textproperty.FrameOn()
element.textproperty.SetFrameColor(np.sqrt(bcc))
elif 'scalarbar' in t:
color = vedo.getColor(self.model.ui.color)
element.GetTitleTextProperty().SetColor(color)
element.GetLabelTextProperty().SetColor(color)
[docs] def set_ui_visibility(self, visible, exceptions=None, ui_button_visible=True):
"""
Set the UI visibility
:param visible: Whether the UI is visible or not
:param exceptions: Dictionary of objects that have to be the inverse of
the given state
:param ui_button_visible: Whether the UI button is still visible or not,
so that the user can restore the UI by clicking on it.
This is not necessary in case of Jupyter NB
"""
self.model.ui.visible = visible
all_elements = self.model.ui.all_elements()
for key in all_elements:
self.set_element_visibility(all_elements[key], visible)
if exceptions is None:
return
for element_id in exceptions:
element = exceptions[element_id]
self.set_element_visibility(element, not visible)
[docs] def add_text(self, name, text, pos, color=None, justify='top-left', context=None, **kwargs):
"""
Add a 2D text on scene
:param name: Name of the object
:param text: Text string
:param pos: Position on screen
:param color: Color of the font
:param justify: Type of justification
:param context: Context to register this object to
:return: Text2D
"""
if color is None:
color = self.model.ui.color
text = vedo.Text2D(text, c=color, pos=pos, font=self.model.ui.font, justify=justify, **kwargs)
text.property.SetFontSize(self.model.ui.font_size)
# Assume absolute coordinates in pixels
if pos[0] > 1.0 or pos[1] > 1.0:
text.GetPositionCoordinate().SetCoordinateSystemToDisplay()
text.SetPosition(pos)
text.name = name
self.model.ui.register(text, context)
self.set_element_visibility(text, False)
return text
[docs] def get_slider_bounds(self, x, y, length, horizontal=True):
"""
Get slider coordinates, releative to lower left corner of the window
:param x: X position
:param y: Y position
:param length: Length of the slider
:param horizontal: Whether the length is horizontal or vertical
:return: np 2d array with min and max coordinates
"""
if horizontal:
return np.array([[x, y], [x + length, y]])
else:
return np.array([[x, y], [x, y + length]])
[docs] def add_slider(self, name, event_handler, min_value, max_value, value, pos,
title=None, oid=None, precision=0, context=None, **kwargs):
"""
Add a slider to the UI
:param event_handler: Event handler function called when the slider is updated
:param min_value: Min slider value
:param max_value: Max slider value
:param value: Default value
:param size: Font size
:param pos: Position in the UI as a tuple or list, see get_slider_bounds for details
:param kwargs: Dictionary of kwargs, see vedo.addons.addSlider2D() for details
:return: 2D Slider
"""
position = self.get_slider_bounds(*pos)
if kwargs is None:
kwargs = {}
kwargs['title'] = False
kwargs['showValue'] = False
# Decorator for the event handler
def update_slider(widget=None, event=None, value=None, *args, **kwargs):
if precision == 0 and value is not None:
value = int(value)
if widget is not None:
if value is not None:
widget.GetRepresentation().SetValue(value)
else:
value = widget.GetRepresentation().GetValue()
if precision == 0 and value is not None:
value = int(value)
widget.text = f'{slider.label_prefix}: {value:.{precision}f}'
widget.label.GetMapper().SetInput(widget.text)
if widget.last_value == value:
return
widget.last_value = value
if event is not None:
event_handler(widget, event, *args, **kwargs)
slider = self.plot.addSlider2D(update_slider, min_value, max_value, value, position, **kwargs)
if precision == 0:
slider.SetAnimationModeToJump()
rep = slider.GetRepresentation()
'''
rep.SetLabelHeight(0.003 * size)
rep.GetLabelProperty().SetFontSize(size)
rep.SetTitleHeight(0.003 * size)
rep.GetTitleProperty().SetFontSize(size)
'''
label_position = [position[0, 0], position[0, 1] + 0.11]
# Assume absolute coordinates in pixels
if pos[0] > 1.0 or pos[1] > 1.0:
rep.GetPoint1Coordinate().SetCoordinateSystemToDisplay()
rep.GetPoint2Coordinate().SetCoordinateSystemToDisplay()
rep.GetPoint1Coordinate().SetValue(position[0, 0], position[0, 1])
rep.GetPoint2Coordinate().SetValue(position[1, 0], position[1, 1])
label_position = [position[0, 0], position[0, 1] + 27]
rep.SetSliderLength(0.012)
rep.SetSliderWidth(0.01)
rep.SetTubeWidth(0.0025)
if oid is not None:
slider.oid = oid
slider.update = update_slider
slider.name = name
prefix = title.title() if isinstance(title, str) and title != '' else name.title()
prefix = prefix.replace('_', ' ')
slider.label_prefix = prefix
slider.text = f'{slider.label_prefix}: {rep.GetValue():.{precision}f}'
slider.label = self.add_text('selection_name', slider.text, label_position)
slider.label.name = name + '_label'
slider.last_value = None
self.plot.add(slider.label)
self.set_element_visibility(slider, False)
self.set_element_visibility(slider.label, False)
self.widgets_reflection[slider.GetRepresentation()] = slider
self.widgets[name] = slider
self.model.ui.register([slider, slider.label], context)
return slider
[docs] def slider_value(self, element_id, value=None, widget=None):
"""
Abstract method to get or set a value on a VTK slider element
:param element_id: Element id (found in self.model.ui.get_elements().keys())
:param value: Value of Z slice, defaults to 0.0
:param widget: Widget instance (given by the event caller)
:return: Float value
"""
if widget is not None:
value = widget.GetRepresentation().GetValue()
elif value is not None:
widget = self.model.ui.context_element(element_id)
if widget is not None:
widget.GetRepresentation().SetValue(value)
return value
[docs] def clip_to_bounds(self, bounds_obj, target=None):
"""
Clip a target object with the bounding box of another object
:param bounds_obj: a vtkActor
:param target: Target object. If None, the current selection is used
"""
if target is None:
target = self.model.selection
if target is None:
return
bounds = utils.get_actor_bounds(bounds_obj)
if isinstance(target, Volume):
controller = self.controllers_map.get(target)
if controller is not None:
controller.clip_to_bounds(bounds)
else:
utils.set_clipping_planes(target, bounds)
[docs] def isosurface(self, value=None, volume_controller=None, split_meshes=False, remove_existing=True):
"""
Generate mesh(es) for a given value in a volume
:param value: Value/label for which we want to generate an isosurface mesh
:param volume_controller: Volume controller. If None, the current one is used.
If the current one is None, then nothing happens.
:param split_meshes: Whether manifold meshes are split if there are multiple ones
:param remove_existing: Whether existing meshes are removed from the plot
:return: List of meshes computed
"""
if volume_controller is None:
volume_controller = self.model.selection_controller
if volume_controller is None:
return
if value is None:
value = self.model.selection_value
if value is None:
return
if remove_existing:
current_meshes = volume_controller.model.isosurfaces.current
self.plot.remove(current_meshes)
if value is None:
value = self.model.selection_value
if value is None:
return
meshes = volume_controller.isosurface(value, split_meshes=split_meshes)
if meshes is None:
return
for mesh in meshes:
self.register_object(mesh)
self.plot.add(mesh)
return meshes
[docs] def set_probe_position(self, pt1=None, pt2=None):
"""
Set the position of the current probe
"""
if self.line_widget is not None and pt1 is not None:
self.line_widget.SetPoint1(pt1)
if self.line_widget is not None and pt2 is not None:
self.line_widget.SetPoint2(pt2)
[docs] def add_probe(self):
"""
Add a new probe widget
"""
self.clear_line_widget()
event_handler = None
controller = self.model.selection_controller
if isinstance(controller, VolumeController):
event_handler = self.update_current_probe
self.line_widget = utils.probe(self.plot, self.model.selection,
self.line_widget, event_handler,
self.model.probe_initial_point1,
self.model.probe_initial_point2)
pt1 = self.line_widget.GetPoint1()
pt2 = self.line_widget.GetPoint2()
probe = controller.add_probe(pt1, pt2)
self.register_object(probe)
self.model.probes.store(probe, probe.name, replace_existing=False, set_current=True)
'''
# If later interested in this, a generic approach to surface mesh data probing
// Get the ID of the point that is closest to the query position
vtkIdType id = locator->FindClosestPoint(pt);
// Retrieve the first attribute value from this point
double value = polyData->GetPointData()->GetScalars()->GetTuple(id, 0);
'''
[docs] def is_probe(self, probe=None):
"""
Check if an object is (the result of) a probe
:param probe: Probe object
:return: Boolean
"""
if probe is None:
probe = self.model.selection
if probe is None:
return False
target = hasattr(probe, 'target_controller') and hasattr(probe, 'target')
points = hasattr(probe, 'origin') and hasattr(probe, 'destination')
return target and points
[docs] def update_current_probe(self, widget=None, event=None, point1=None, point2=None):
"""
Update the current probe
:param widget: vtkLineWidget
:param event: vtkEvent
:param point1: Point 1, this will override the widget's point 1
:param point2: Point 2, this will override the widget's point 2
"""
if widget is None:
widget = self.line_widget
if widget is None and point1 is None and point2 is None:
return
if widget is not None:
point1 = widget.GetPoint1()
point2 = widget.GetPoint2()
obj = self.model.probes.current
volume_controller = obj.target_controller
# Probe method is within VolumeController
volume_controller.update_probe(point1, point2, obj)
[docs] def edit_probe(self, probe=None):
"""
Edit/move an existing probe
"""
if probe is None:
probe = self.model.selection
if probe is None:
return
if self.is_probe(probe):
# The event handler in this case will refer to the active probe in
# self.model.probes.current so we have to set it
self.model.probes.set_current(probe) # or probe.name works too
event_handler = self.update_current_probe
self.line_widget = utils.probe(self.plot, probe, self.line_widget, event_handler,
probe.origin, probe.destination)
#self.line_widget.RemoveObservers('InteractionEvent')
#self.plot.widgets.remove(self.line_widget)
[docs] def update_px_slicer(self, widget=None, event=None, value=0.0):
"""
Event handler for Slicer on +X axis
:param widget: Widget instance (given by the event caller)
:param event: Event (given by the event caller)
:param value: Value of X slice, defaults to 0.0
"""
value = self.slider_value('+x', value, widget)
try:
self.model.selection_controller.update_slicer(1, value)
except AttributeError as e:
pass
[docs] def update_nx_slicer(self, widget=None, event=None, value=0.0):
"""
Event handler for Slicer on -X axis
:param widget: Widget instance (given by the event caller)
:param event: Event (given by the event caller)
:param value: Value of X slice, defaults to 0.0
"""
value = self.slider_value('-x', value, widget)
try:
self.model.selection_controller.update_slicer(0, value)
except AttributeError:
pass
[docs] def update_py_slicer(self, widget=None, event=None, value=0.0):
"""
Event handler for Slicer on +Y axis
:param widget: Widget instance (given by the event caller)
:param event: Event (given by the event caller)
:param value: Value of Y slice, defaults to 0.0
"""
value = self.slider_value('+y', value, widget)
try:
self.model.selection_controller.update_slicer(3, value)
except AttributeError:
pass
[docs] def update_ny_slicer(self, widget=None, event=None, value=0.0):
"""
Event handler for Slicer on -Y axis
:param widget: Widget instance (given by the event caller)
:param event: Event (given by the event caller)
:param value: Value of Y slice, defaults to 0.0
"""
value = self.slider_value('-y', value, widget)
try:
self.model.selection_controller.update_slicer(2, value)
except AttributeError:
pass
[docs] def update_pz_slicer(self, widget=None, event=None, value=0.0):
"""
Event handler for Slicer on +Z axis
:param widget: Widget instance (given by the event caller)
:param event: Event (given by the event caller)
:param value: Value of Z slice, defaults to 0.0
"""
value = self.slider_value('+z', value, widget)
try:
self.model.selection_controller.update_slicer(5, value)
except AttributeError:
pass
[docs] def update_nz_slicer(self, widget=None, event=None, value=0.0):
"""
Event handler for Slicer on -Z axis
:param widget: Widget instance (given by the event caller)
:param event: Event (given by the event caller)
:param value: Value of Z slice, defaults to 0.0
"""
value = self.slider_value('-z', value, widget)
try:
self.model.selection_controller.update_slicer(4, value)
except AttributeError:
pass
[docs] def toggle_slices(self, event=None):
"""
Toggle slicers visibility
:param widget: Widget instance (given by the event caller)
:param event: Event (given by the event caller)
"""
self.model.slices_visible = not self.model.slices_visible
try:
self.model.selection_controller.toggle_slices_visibility()
except Exception:
pass
[docs] def toggle_global_slicer(self, event=None):
"""
Toggle global slicer mode
:param widget: Widget instance (given by the event caller)
:param event: Event (given by the event caller)
"""
raise NotImplementedError
'''
self.model.global_slicer = current_slicer
try:
for key in self.objects:
obj = self.objects[key]
obj.mapper().SetClippingPlanes(self.model.global_slicer)
except Exception:
pass
'''
[docs] def set_camera(position, quat, camera_distance=10000):
"""
TODO: make it possible to store and assign a camera transformation matrix
"""
raise NotImplementedError
camera = vtk.vtkCamera()
# Define the quaternion in vtk, note the swapped order
# w,x,y,z instead of x,y,z,w
quat_vtk = vtk.vtkQuaterniond(quat[3], quat[0], quat[1], quat[2])
M = np.zeros((3, 3), dtype=np.float32)
quat_vtk.ToMatrix3x3(M)
up = [0, 1, 0]
pos = [0, 0, camera_distance]
camera.SetViewUp(*np.dot(M, up))
camera.SetPosition(*np.dot(M, pos))
p = camera.GetPosition()
p_new = np.array(p) + position
camera.SetPosition(*p_new)
camera.SetFocalPoint(*position)
return camera
[docs] def update_camera(self, normal=None, view_up=None, scale_factor=1.5, min_distance=10000):
"""
Update the camera frustrum
:param normal: View normal
:param view_up: Up axis normal
:param scale_factor: Scale factor to make the camera closer to the target
Smaller values than 1 make the target closer to the camera.
"""
camera_model = self.model.cameras.current
if camera_model is None or camera_model.target is None:
return
if view_up is not None:
camera_model.view_up = view_up
self.plot.camera.SetViewUp(*camera_model.view_up)
focal_point = None
try:
focal_point = utils.get_actor_center(camera_model.target)
except Exception:
try:
if len(camera_model.target) >= 3:
focal_point = camera_model.target[:3]
except TypeError:
pass
if focal_point is None:
focal_point = np.zeros(3)
try:
target_dimensions = utils.get_actor_dimensions(camera_model.target)
except Exception:
target_dimensions = np.ones(3).astype(float) * min_distance
max_dim = max(target_dimensions)
if normal is None:
normal = self.plot.camera.GetViewPlaneNormal()
normal = np.array(normal) * -1.0
if not self.plot.camera.GetParallelProjection():
distance = max_dim * camera_model.distance_factor * 1 / scale_factor
else:
distance = max_dim * 1 / scale_factor
# Update orthographic scale too so that it's synced with perspective
self.plot.camera.SetParallelScale(distance / scale_factor)
#self.plot.camera.SetDistance(0)
self.plot.camera.SetFocalPoint(focal_point)
camera_position = focal_point - distance * np.array(normal)
self.plot.camera.SetPosition(camera_position)
self.plot.camera.SetClippingRange([0.1, abs(distance)*4])
[docs] def toggle_orthographic_view(self):
"""
Toggle orthographic/perspective views
"""
is_ortho = self.plot.camera.GetParallelProjection()
vedo.settings.useParallelProjection = not is_ortho
self.plot.camera.SetParallelProjection(not is_ortho)
self.update_camera()
[docs] def set_left_view(self):
"""
Set left sagittal view
"""
self.update_camera([1.0, 0.0, 0.0], self.model.Z_UP)
[docs] def set_right_view(self):
"""
Set right sagittal view
"""
self.update_camera([-1.0, 0.0, 0.0], self.model.Z_UP)
[docs] def set_anterior_view(self):
"""
Set anterior coronal view
"""
self.update_camera([0.0, 1.0, 0.0], self.model.Z_UP)
[docs] def set_posterior_view(self):
"""
Set posterior coronal view
"""
self.update_camera([0.0, -1.0, 0.0], self.model.Z_UP)
[docs] def set_dorsal_view(self):
"""
Set dorsal axial view
"""
self.update_camera([0.0, 0.0, -1.0], self.model.Y_UP)
[docs] def set_ventral_view(self):
"""
Set ventral axial view
"""
self.update_camera([0.0, 0.0, 1.0], self.model.Y_UP)
[docs] def toggle_autofocus(self):
"""
Reset the camera target to the selected object
"""
self.model.cameras.current.autofocus = not self.model.cameras.current.autofocus
[docs] def view_selected(self):
"""
Reset the camera target to the selected object
"""
camera_model = self.model.cameras.current
if camera_model is None or self.model.selection is None:
return
camera_model.target = self.model.selection
self.update_camera()
[docs] def move_camera_pivot(self, point=None):
"""
Focus the camera on the current selection point
:param point: Point to focus on
"""
camera_model = self.model.cameras.current
if camera_model is None:
return
camera_model.target = self.model.selection
self.update_camera()
[docs] def toggle_ui(self):
"""
Toggle user interface
"""
self.set_ui_visibility(not self.model.ui.visible)
[docs] def toggle_hollow_mode(self):
"""
Toggle hollow volume visualization
"""
try:
self.model.selection_controller.toggle_hollow()
except Exception:
pass
[docs] def toggle_info(self):
"""
Toggle visibility of information about current selection
"""
common_value = not self.model.selection_outline_visible
self.model.selection_outline_visible = common_value
self.model.selection_marker_visible = common_value
self.model.selection_color_bar_visible = common_value
self.outline_actor.SetVisibility(self.model.selection_outline_visible)
self.selection_marker.SetVisibility(self.model.selection_marker_visible)
self.color_bar.SetVisibility(self.model.selection_color_bar_visible)
[docs] def toggle_outline(self, event=None):
"""
Toggle outline visibility
:param event: Event (given by the event caller)
"""
self.set_outline_visibility(not self.model.selection_outline_visible)
[docs] def set_outline_visibility(self, on=None):
"""
Set outline visibility
:param on: Visibility boolean
"""
if on is None:
on = self.model.selection_outline_visible
self.model.selection_outline_visible = on
if self.outline_actor is not None:
self.outline_actor.SetVisibility(on)
[docs] def toggle_marker(self):
"""
Toggle visibility of the marker
"""
self.set_marker_visibility(not self.model.selection_marker_visible)
[docs] def set_marker_visibility(self, on=True):
"""
Set marker visibility
:param on: Visibility boolean
"""
self.model.selection_marker_visible = on
if self.selection_marker is not None:
self.selection_marker.SetVisibility(on)
[docs] def toggle_info_text(self):
"""
Toggle visibility of the marker
"""
self.set_info_text_visibility(not self.model.selection_text_visible)
[docs] def set_info_text_visibility(self, on=True):
"""
Set info text visibility
:param on: Visibility boolean
"""
self.model.selection_text_visible = on
if self.selection_info is not None:
self.selection_info.SetVisibility(on)
[docs] def toggle_color_bar(self):
"""
Togggle the color bar visibility
"""
self.model.selection_color_bar_visible = not self.model.selection_color_bar_visible
if self.model.selection_color_bar_visible:
self.draw_color_bar()
self.set_color_bar_visibility()
[docs] def set_info_visibility(self, visible=True, actors=None, update_color_bar=True):
"""
Set the visibility of info data
:param visible: Whether info data is visible or not
:param actors: Any desired actors to be made visible/invisible
:param update_color_bar: Whether this method also affects the color bar
"""
self.model.selection_info_visible = visible
if update_color_bar:
self.model.selection_color_bar_visible = visible
if self.model.selection_color_bar_visible:
self.draw_color_bar()
self.set_color_bar_visibility()
if actors is None:
actors = []
if self.selection_info is not None:
actors += [self.selection_info]
actors += [self.selection_marker]
for actor in actors:
if actor is not None:
actor.SetVisibility(visible)
[docs] def select_object_by_name(self, name):
raise NotImplementedError
[docs] def select_previous_object(self):
"""
Select the next object in the list of selectable objects
"""
widget = self.widgets.get('selection')
repr = widget.GetRepresentation()
value = repr.GetValue()
min_value = repr.GetMinimumValue()
previous_value = max(value - 1, min_value)
self.select(previous_value)
[docs] def select_object_by_id(self, widget=None, event=None, value=1.0):
"""
Select object by id (order in which objects were registered in this app)
:param widget: Widget instance (given by the event caller)
:param event: Event (given by the event caller)
:param value: Object id
"""
if widget is not None and event is not None:
value = widget.GetRepresentation().GetValue()
self.select(value)
[docs] def select_next_object(self):
"""
Select the next object in the list of selectable objects
"""
widget = self.widgets.get('selection')
repr = widget.GetRepresentation()
value = repr.GetValue()
max_value = repr.GetMaximumValue()
next_value = min(value + 1, max_value)
self.select(next_value)
[docs] def toggle_pickable(self, widget=None, event=None, value=1.0):
"""
Toggle the clickable state of an object
:param widget: Widget instance (given by the event caller)
:param event: Event (given by the event caller)
:param value: 0 for disabled, 1 for enabled
"""
if self.model.selection is None:
return
try:
pickable = self.model.selection.pickable()
self.model.selection.pickable(not pickable)
actors = self.model.selection_controller.get_related_actors()
for actor in actors:
actor.pickable(not pickable)
except Exception as e:
if not self.silent:
print(e)
[docs] def update_opacity(self, widget=None, event=None, value=1.0):
"""
Update the alpha unit of the current volume, making it more or less transparent
:param widget: Widget instance (given by the event caller)
:param event: Event (given by the event caller)
:param value: alpha unit value. If none given by the event, the value defaults to 1.0
"""
if widget is not None and event is not None:
value = widget.GetRepresentation().GetValue()
if self.model.selection is None:
return
self.model.selection.SetVisibility(value > 0)
if isinstance(self.model.selection_controller, VolumeController):
self.model.selection_controller.set_opacity(value)
else:
self.model.selection.alpha(value)
[docs] def update_slices_opacity(self, widget=None, event=None, value=1.0):
"""
Update the opacity of the current volume slices
:param widget: Widget instance (given by the event caller)
:param event: Event (given by the event caller)
:param value: alpha unit value. If none given by the event, the value defaults to 1.0
"""
if widget is not None and event is not None:
value = widget.GetRepresentation().GetValue()
try:
self.model.selection_controller.set_slices_opacity(value)
except Exception:
pass
[docs] def play_time_series(self, widget=None, event=None):
"""
Play/pause time series
:param widget: Widget instance (given by the event caller)
:param event: Event (given by the event caller)
"""
self.model.animation_playing = not self.model.animation_playing
if self.model.timer_id is not None:
self.plot.interactor.DestroyTimer(self.model.timer_id)
if self.model.animation_playing:
self.model.timer_id = self.plot.interactor.CreateRepeatingTimer(self.model.playback_speed)
[docs] def previous_time_series(self, offset=1, loop=True):
"""
Previous time series
:param offset: Offset integer. If negative, then it's like using next_time_series()
:param loop: Whether next() goes to 0 when it reached the end of the time series or not
:return: Returns whether the next time series is valid (within range of the time series)
"""
if self.model.selection is None:
return
# TODO: handle the case where the last value could be invalid if selection changes
self.set_time_series(self.last_time_series_value - offset)
[docs] def next_time_series(self, offset=1, loop=True):
"""
Next time series
:param offset: Offset integer, can be negative to go backwards
:param loop: Whether next() goes to 0 when it reached the end of the time series or not
:return: Returns whether the next time series is valid (within range of the time series)
"""
if self.model.selection is None:
return
self.set_time_series(self.last_time_series_value + offset)
# TODO: below code would require that the slider min-max is set on object selection
return
slider = self.widgets.get('time_series')
value = int(slider.GetRepresentation().GetValue())
min_value = int(slider.GetRepresentation().GetMinimumValue())
max_value = int(slider.GetRepresentation().GetMaximumValue())
new_value = value + offset
if new_value > max_value and loop:
# Reached the end, playing from 0
new_value = min_value
elif new_value < min_value and loop:
# Playing backwards
new_value = max_value
slider.GetRepresentation().SetValue(new_value)
self.update_time_series(value=new_value)
return loop or (min_value <= new_value <= max_value)
[docs] def update_time_series(self, widget=None, event=None, value=None):
"""
Update the time series
:param widget: The slider widget (optional)
:param event: The slider event (optional)
:param value: The value to set. If None, then the slider value
is used, if it's given as widget param
"""
if widget is not None and event is not None:
value = int(widget.GetRepresentation().GetValue())
self.set_time_series(value)
"""
TODO: See if it's worth implementing transfer functions tweening in order to let
the user see a gradual change between steps in a time series instead of a jump. Probably
quite far down in the list of features.
if value is None:
lut = self.model.lut
index, next_lut = self.model.get_lut_and_id(self.model.lut_id+1)
tweened_tf = LUTModel()
tweened_rgb = utils.blend_maps(lut.color_map, next_lut.color_map, self.model.time)
tweened_alpha = utils.blend_maps(lut.alpha_map, next_lut.alpha_map, self.model.time)
tweened_tf.set_color_and_alpha(tweened_rgb, tweened_alpha)
"""
[docs] def set_time_series(self, value, force_update=False):
"""
Set the time series for the current selected object
:param value: The given time step
:param force_update: Whether an update is forced even if the
given value is the same as the existing time step
"""
if value is None:
return
if self.last_time_series_value == value and not force_update:
return
self.last_time_series_value = value
actor = self.model.selection
controller = self.model.selection_controller
# TODO: handle case where value == string
if isinstance(actor, vedo.Lines):
cell_data = actor.polydata().GetCellData()
if hasattr(cell_data, 'GetArrayNames'):
names = cell_data.GetArrayNames()
#name = actor.scalars_prefix + str(value)
#point_data.SetActiveScalars(names[value])
# Necessary for utils.Points in sphere mode (non screen-space)
actor.mapper().SelectColorArray(names[value])
elif isinstance(actor, vedo.Points):
name = actor.scalars_prefix + str(value)
actor.polydata().GetPointData().SetActiveScalars(name)
# Necessary for utils.Points in sphere mode (non screen-space)
actor.mapper().SelectColorArray(name)
elif isinstance(actor, vedo.Mesh):
name = actor.scalars_prefix + str(value)
actor.polydata().GetPointData().SetActiveScalars(value)
elif isinstance(actor, vedo.Volume):
controller.model.luts.set_current(value)
self.handle_lut_update()
self.draw_color_bar()
#elif isinstance(actor, vedo.Assembly):
#elif isinstance(actor, vedo.Picture),
#elif isinstance(actor, vtk.vtkActor2D)
[docs] def assign_scalars(self, scalar_map=None, scalar_range=None, color_map='viridis', make_active=True):
"""
Assign a new data set and a color map to a target
min_value=None, max_value=None
"""
selected_view = self.model.selection_controller
if selected_view is None:
return
if isinstance(selected_view, VolumeController):
volume_model = selected_view.model
lut_model = volume_model.build_lut(scalar_map, scalar_range, color_map, make_active=make_active)
selected_view.set_color_map()
if make_active:
self.handle_lut_update()
self.update_ui()
return lut_model
[docs] def handle_lut_update(self):
"""
Update the view with the given or current transfer function
:param lut_model: A LUTModel (whose table property is a vtkLookupTable)
to set on the current volume. If None, the current one will be used.
"""
view = self.model.selection_controller
if view is None or not isinstance(view, VolumeController):
return
if view.model.luts.current is None:
return
view.set_color_map()
[docs] def animation_callback(self, progress):
"""
You may override this function according to your needs
:param progress: ratio of total animation (from 0.0 to 1.0)
"""
pass
[docs] def update_video_duration(self, widget=None, event=None, value=None):
"""
Update the video duration
:param widget: The slider widget (optional)
:param event: The slider event (optional)
:param value: The value to set. If None, then the slider value
is used, if it's given as widget param
"""
if widget is not None and event is not None:
value = widget.GetRepresentation().GetValue()
if value is None:
return
self.model.video_duration = int(value)
[docs] def update_video_start_angle(self, widget=None, event=None, value=None):
"""
Update the video start angle
:param widget: The slider widget (optional)
:param event: The slider event (optional)
:param value: The value to set. If None, then the slider value
is used, if it's given as widget param
"""
if widget is not None and event is not None:
value = widget.GetRepresentation().GetValue()
if value is None:
return
self.model.video_start_angle = int(value)
[docs] def update_video_end_angle(self, widget=None, event=None, value=None):
"""
Update the video end angle
:param widget: The slider widget (optional)
:param event: The slider event (optional)
:param value: The value to set. If None, then the slider value
is used, if it's given as widget param
"""
if widget is not None and event is not None:
value = widget.GetRepresentation().GetValue()
if value is None:
return
self.model.video_end_angle = int(value)
[docs] def toggle_volumetric_lod(self):
"""
Toggle volumetric LOD (subsampling)
"""
self.volumetric_lod(not self.model.interactive_volume_subsampling)
[docs] def volumetric_lod(self, on=False):
"""
Enable or disable volumetric LOD subsampling.
Turn this on when you want faster interactive visualizations
:param on: Whether volumetric subsampling is activated
"""
if self.model.interactive_volume_subsampling == on:
return
self.model.interactive_volume_subsampling = on
for target in self.controllers_map:
controller = self.controllers_map.get(target)
if isinstance(controller, VolumeController):
controller.set_interactive_subsampling(on)
[docs] def export_image(self, file_name='iblviewer.png', width=None, height=None, scale=2):
"""
Export the current image to a file
:param file_name: File name with extension. PNG by default
:param width: Width in pixels. If None, the width of the window is used.
:param height: Height in pixels. If None, the height of the window is used.
:param scale: Scale factor to make the image larger. Defaults to 2.
"""
ui_visibility = self.model.ui.visible
self.set_ui_visibility(False, ui_button_visible=False)
self.render(file_name, width, height, scale)
self.set_ui_visibility(ui_visibility)
[docs] def export_turntable_video(self, file_name='iblviewer.mp4', start_angle=0,
end_angle=360, duration=None, fps=25):
"""
Export a sagittal turntable video of the viewer.
:param file_name: File name
:param start_angle: Start angle
:param end_angle: End angle. If it's the same value as start angle, then 360 is added
:param duration: Duration of the video
:param fps: Frames per second, defaults to 25
"""
if self.model.ui.embed:
start_angle = self.widgets.get('start_angle').GetRepresentation().GetValue()
end_angle = self.widgets.get('end_angle').GetRepresentation().GetValue()
duration = self.widgets.get('video_duration').GetRepresentation().GetValue()
#if start_angle is None or end_angle is None or duration is None or fps is None:
#return
if start_angle == end_angle:
end_angle += 360
video = vedo.Video(file_name, duration=duration, backend='ffmpeg', fps=fps)
# Disable volumetric LOD for video-making
self.volumetric_lod(False)
ui_visibility = self.model.ui.visible
self.set_ui_visibility(False, ui_button_visible=False)
start = 0
end = int(duration * fps)
angle_step = (end_angle - start_angle) / end
#axes = [0, 1, 2]
for step in range(start, end):
normal = np.zeros(3) * 1.0
t = step * angle_step / 180 * math.pi
normal[0] = math.cos(t)
normal[1] = math.sin(t)
self.update_camera(normal, self.model.Z_DOWN)
self.animation_callback(step / (end - start))
self.render()
video.addFrame()
# Disable volumetric LOD for video-making
self.volumetric_lod(self.model.interactive_volume_subsampling)
self.set_ui_visibility(ui_visibility)
video.close()
'''
TODO: record a video from offscreen buffer
def record_video(self):
video = Video(video_file_path, fps=fps, duration=duration)
self.show(interactive=False, resetcam=False)
video.addFrame()
video.close()
'''