У меня есть несколько случайных точек в рендерере VTK, использующих QVTKRenderWindowInteractor
. Я могу обновить сцену, создав больше случайных точек, нажав кнопку QT, как показано на скриншоте. Пожалуйста, найдите код Python MWE внизу.
На этом этапе я хочу иметь возможность щелкнуть одну из этих точек и получить ее координаты и/или идентификатор. Я рассмотрел примеры vtkPointPicker и vtkCellPicker. Но я не смог разобраться в этом самостоятельно.
Я новичок в ВТК. Вот мой код на данный момент. Любые указатели будут оценены.
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_())
Я использую Python: v3.10.12, Qt: v5.15.2, PyQt: v5.15.10, VTK: v9.3.0.
Я нашел элегантный способ получить координаты точки щелчка. Он использует vtkPointPicker
для получения идентификатора точки, а затем из этого идентификатора можно получить индекс, а затем и координаты указанной точки.
В приведенном ниже MWE я собрал все, что необходимо для достижения указанной цели. Код организован структурированно. Он показывает индекс и координаты точки при нажатии левой кнопки мыши и очищает текст при нажатии правой кнопки мыши.
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_())
Вот скриншот результата:
Однако я не совсем уверен, что это лучший способ достичь моей цели. Любые комментарии приветствуются.