I have a bunch of random points in a VTK renderer using QVTKRenderWindowInteractor
. I can update the scene by generating more random points by clicking on the QT push button as seen in the screenshot. Please find the MWE python code at the bottom.
At this point, I want to be able to click on one of these points and get the coordinates and/or the ID of it. I looked at the vtkPointPicker and vtkCellPicker examples. But I wasn't able to figure it out on my own.
I am new to VTK. Here's my code so far. Any pointers will be appreciated.
import sys
import numpy as np
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QPushButton
)
# noinspection PyUnresolvedReferences
import vtkmodules.vtkInteractionStyle
# noinspection PyUnresolvedReferences
import vtkmodules.vtkRenderingOpenGL2
from vtkmodules.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor
from vtkmodules.vtkCommonCore import vtkPoints
from vtkmodules.vtkCommonColor import vtkNamedColors
from vtkmodules.vtkInteractionWidgets import vtkCameraOrientationWidget
from vtkmodules.vtkCommonDataModel import (
vtkCellArray,
vtkPolyData,
)
from vtkmodules.vtkRenderingCore import (
vtkActor,
vtkPolyDataMapper,
vtkRenderer,
)
import vtkmodules.util.numpy_support as vtk_np
class Ui_MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setupUi()
def setupUi(self):
self.setWindowTitle('Visualization App')
self.resize(800, 600)
self.centralwidget = QWidget(self)
self.verticalLayout = QVBoxLayout()
self.colors = vtkNamedColors()
# Interactor widget
self.canvas = QVTKRenderWindowInteractor(self.centralwidget)
self.verticalLayout.addWidget(self.canvas)
# button
self.pushButton = QPushButton(self.centralwidget)
self.pushButton.setText('Update')
self.verticalLayout.addWidget(self.pushButton)
self.pushButton.clicked.connect(self.update_plot)
self.centralwidget.setLayout(self.verticalLayout)
self.setCentralWidget(self.centralwidget)
self.setup_canvas()
# enable user interface interactor
self.show()
self.ren_win.Render()
self.interactor.Initialize()
self.interactor.Start()
def setup_canvas(self):
# Renderer, render window and the interactor
self.ren = vtkRenderer()
self.ren_win = self.canvas.GetRenderWindow()
self.interactor = self.ren_win.GetInteractor()
self.ren.SetBackground(.2, .3, .4)
self.ren_win.AddRenderer(self.ren)
# Target
self.target_actor = self.init_canvas_actor(np.random.normal(size=(np.random.randint(100), 3), scale=0.2))
self.ren.AddActor(self.target_actor)
# Camera orientation widget
# Important: The interactor must be set prior to enabling the widget
self.interactor.SetRenderWindow(self.ren_win)
self.cam_orient_manipulator = vtkCameraOrientationWidget()
self.cam_orient_manipulator.SetParentRenderer(self.ren)
self.cam_orient_manipulator.On()
# Camera position
self.ren.GetActiveCamera().Azimuth(0)
self.ren.GetActiveCamera().Elevation(-80)
self.ren.ResetCamera()
def init_canvas_actor(self, nparray: np.ndarray):
self.nparray = nparray
nCoords = nparray.shape[0]
self.points = vtkPoints()
self.cells = vtkCellArray()
self.pd = vtkPolyData()
self.points.SetData(vtk_np.numpy_to_vtk(nparray))
cells_npy = np.vstack([np.ones(nCoords, dtype=np.int64), np.arange(nCoords, dtype=np.int64)]).T.flatten()
self.cells.SetCells(nCoords, vtk_np.numpy_to_vtkIdTypeArray(cells_npy))
self.pd.SetPoints(self.points)
self.pd.SetVerts(self.cells)
mapper = vtkPolyDataMapper()
mapper.SetInputDataObject(self.pd)
actor = vtkActor()
actor.SetMapper(mapper)
actor.GetProperty().SetRepresentationToPoints()
actor.GetProperty().SetColor(0.0, 1.0, 0.0)
actor.GetProperty().SetPointSize(4)
return actor
def update_plot(self):
nCoords = np.random.randint(100)
pc = np.random.normal(size=(nCoords, 3), scale=0.2)
points: vtkPoints = self.pd.GetPoints()
points.SetData(vtk_np.numpy_to_vtk(pc))
cells_npy = np.vstack([np.ones(nCoords, dtype=np.int64), np.arange(nCoords, dtype=np.int64)]).T.flatten()
self.cells.SetCells(nCoords, vtk_np.numpy_to_vtkIdTypeArray(cells_npy))
self.pd.SetPoints(points)
points.Modified()
self.cells.Modified()
self.pd.Modified()
self.show()
self.ren_win.Render()
if __name__ == '__main__':
app = QApplication(sys.argv)
main = Ui_MainWindow()
# main.show()
sys.exit(app.exec_())
I am using Python: v3.10.12, Qt: v5.15.2, PyQt: v5.15.10, VTK: v9.3.0.
I found an elegant way to get the coordinates of the clicked point. It uses vtkPointPicker
to get the point ID and then from that ID one can get the index and subsequently the coordinates of the said point.
In the MWE below, I have put together everything that is needed for the said objective. The code is organized in a structured way. It shows the index and the coordinates of the point when left-mouse-button is clicked and clears the text when right-mouse-button is clicked.
import sys
import numpy as np
from PyQt5.QtCore import QRect
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QGridLayout, QPushButton
)
# noinspection PyUnresolvedReferences
import vtkmodules.vtkInteractionStyle
# noinspection PyUnresolvedReferences
import vtkmodules.vtkRenderingOpenGL2
from vtkmodules.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor
from vtkmodules.vtkCommonCore import vtkPoints, vtkIntArray, vtkFloatArray
from vtkmodules.vtkInteractionWidgets import vtkCameraOrientationWidget
from vtkmodules.vtkCommonDataModel import vtkPolyData
from vtkmodules.vtkFiltersSources import vtkSphereSource
from vtkmodules.vtkFiltersCore import vtkGlyph3D
from vtkmodules.vtkRenderingLOD import vtkLODActor
from vtkmodules.vtkRenderingCore import (
vtkTextActor,
vtkCamera,
vtkPolyDataMapper,
vtkRenderer,
vtkAssembly,
vtkColorTransferFunction,
vtkPointPicker
)
class MainWindow(QMainWindow):
"""This class implements the main window of the application
"""
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.setWindowTitle('Visualization App')
self.setGeometry(QRect(0, 0, 800, 600))
self.central_widget = VisualizerWidget(self)
def startup(self):
self.setCentralWidget(self.central_widget)
self.show()
class VisualizerWidget(QWidget):
"""This class implements the central widget of the application
"""
def __init__(self, parent=None):
super(VisualizerWidget, self).__init__(parent)
self.init_ui()
def init_ui(self):
self.canvas = CanvasViewer(self)
self.update_button = QPushButton('Update', self)
self.update_button.clicked.connect(self.canvas.update_canvas)
self.central_layout = QGridLayout(self)
self.central_layout.setContentsMargins(4, 4, 4, 10)
self.central_layout.addWidget(self.canvas.iren)
self.central_layout.addWidget(self.update_button)
self.setLayout(self.central_layout)
self.canvas.startup()
class CanvasViewer(QWidget):
"""This class implements the canvas elements
"""
def __init__(self, parent: None):
super(CanvasViewer, self).__init__(parent)
self.init_ui()
def init_ui(self):
self.iren = QVTKRenderWindowInteractor(self)
self.ren = vtkRenderer()
self.ren_win = self.iren.GetRenderWindow()
self.interactor = self.ren_win.GetInteractor()
self.ren.SetBackground(.2, .3, .4)
self.ren_win.AddRenderer(self.ren)
self.interactor.GetInteractorStyle().SetCurrentStyleToTrackballCamera()
self.interactor.Enable()
self.interactor.Initialize()
# Camera orientation widget
# Important: The interactor must be set prior to enabling the widget
self.interactor.SetRenderWindow(self.ren_win)
self.cam_orient_manipulator = vtkCameraOrientationWidget()
self.cam_orient_manipulator.SetParentRenderer(self.ren)
self.cam_orient_manipulator.On()
# Text actor for printing the coordinates of the clicked point
self.actor_title = vtkTextActor()
self.actor_title.SetInput('')
self.actor_title.GetTextProperty().SetFontFamilyToArial()
self.actor_title.GetTextProperty().BoldOn()
self.actor_title.GetTextProperty().SetFontSize(12)
self.actor_title.GetTextProperty().SetColor(1, 0.9, 0.8)
self.actor_title.SetDisplayPosition(10, 10)
self.ren.AddActor(self.actor_title)
# Build an LUT for colors
self.lut_color = vtkColorTransferFunction()
self.lut_color.AddRGBPoint(0, 1.0, 0.0, 0.0)
self.lut_color.AddRGBPoint(1, 0.0, 1.0, 0.0)
self.lut_color.AddRGBPoint(2, 0.0, 0.0, 1.0)
self.lut_color.AddRGBPoint(3, 0.1, 0.1, 0.1)
self.interactor.AddObserver('LeftButtonPressEvent', self.on_pick_left)
self.interactor.AddObserver('RightButtonPressEvent', self.on_pick_right)
self.current_points_to_plot = np.empty((0, 3))
self.set_camera_position()
def set_camera_position(self):
"""This function sets up the camera position
"""
camera = vtkCamera()
camera.SetPosition((0, 0, 25))
camera.SetFocalPoint((0, 0, 0))
camera.SetViewUp((0, 1, 0))
camera.SetDistance(25)
camera.SetClippingRange((15, 40))
self.ren.SetActiveCamera(camera)
self.ren_win.Render()
def startup(self):
self.ren.ResetCamera()
self.ren_win.Render()
self.interactor.Start()
def update_canvas(self):
"""This function updates the canvas when the push button is clicked
"""
points = 10 * np.random.normal(size=(np.random.randint(100), 3), scale=0.2)
self.current_points_to_plot = points
self.clear_point_actors()
# Finally render the canvas with current points
self.set_points(self.current_points_to_plot)
def set_points(self, coords):
"""This function sets the new set of coordinates on the canvas
"""
n_tgt = len(coords)
radii, colors, indices = CanvasViewer.sphere_prop_to_vtkarray(n_tgt, 1, 0)
polydata = vtkPolyData()
polydata.GetPointData().AddArray(radii)
polydata.GetPointData().SetActiveScalars(radii.GetName())
polydata.GetPointData().AddArray(colors)
polydata.GetPointData().AddArray(indices)
points = vtkPoints()
points.SetNumberOfPoints(n_tgt)
for i, (x, y, z) in enumerate(coords):
points.SetPoint(i, x, y, z)
polydata.SetPoints(points)
# Finally update the renderer
self.current_point_actors = self.build_scene(polydata)
self.ren.AddActor(self.current_point_actors)
self.iren.Render()
def build_scene(self, polydata):
"""build a vtkPolyData object for a given frame of the trajectory
"""
# The rest is for building the point-spheres
sphere = vtkSphereSource()
sphere.SetCenter(0, 0, 0)
sphere.SetRadius(0.2)
sphere.SetPhiResolution(100)
sphere.SetThetaResolution(100)
self.glyph = vtkGlyph3D()
self.glyph.GeneratePointIdsOn()
self.glyph.SetInputData(polydata)
self.glyph.SetScaleModeToScaleByScalar()
self.glyph.SetSourceConnection(sphere.GetOutputPort())
self.glyph.Update()
sphere_mapper = vtkPolyDataMapper()
sphere_mapper.SetLookupTable(self.lut_color)
sphere_mapper.SetInputConnection(self.glyph.GetOutputPort())
sphere_mapper.SetScalarModeToUsePointFieldData()
sphere_mapper.SelectColorArray('color')
ball_actor = vtkLODActor()
ball_actor.SetMapper(sphere_mapper)
ball_actor.GetProperty().SetAmbient(0.2)
ball_actor.GetProperty().SetDiffuse(0.5)
ball_actor.GetProperty().SetSpecular(0.3)
self._picking_domain = ball_actor
assembly = vtkAssembly()
assembly.AddPart(ball_actor)
return assembly
def clear_point_actors(self):
if not hasattr(self, 'current_point_actors'):
pass
else:
self.current_point_actors.VisibilityOff()
self.current_point_actors.ReleaseGraphicsResources(self.ren_win)
self.ren.RemoveActor(self.current_point_actors)
def on_pick_left(self, obj, event=None):
"""Event handler when a point is mouse-picked with the left mouse button
"""
if not hasattr(self, '_picking_domain'):
return
# Get the picked position and retrieve the index of the target that was picked from it
pos = obj.GetEventPosition()
picker = vtkPointPicker()
picker.SetTolerance(0.005)
picker.AddPickList(self._picking_domain)
picker.PickFromListOn()
picker.Pick(pos[0], pos[1], 0, self.ren)
pid = picker.GetPointId()
if pid > 0:
idx = int(self.glyph.GetOutput().GetPointData().GetArray('index').GetTuple1(pid))
text = f'Index: {idx} {self.current_points_to_plot[idx]}'
print(text)
self.actor_title.SetInput(text)
def on_pick_right(self, obj, event=None):
"""Clears the the text field when right mouse button is clicked
"""
self.actor_title.SetInput(f'')
@staticmethod
def sphere_prop_to_vtkarray(n_sphere, radius, color):
radii = vtkFloatArray()
radii.SetName('radius')
for _ in range(n_sphere):
radii.InsertNextTuple1(radius)
colors = vtkFloatArray()
colors.SetName('color')
for _ in range(n_sphere):
colors.InsertNextTuple1(color)
indices = vtkIntArray()
indices.SetName('index')
for idx in range(n_sphere):
indices.InsertNextTuple1(idx)
return radii, colors, indices
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
window.startup()
sys.exit(app.exec_())
Here is the screenshot of the result:
However, I am not entirely sure if this is the best way to achieve my goal. Any comments are appreciated.