Как получить координаты и/или идентификатор точки при щелчке левой кнопкой мыши в VTK PolyData в Python

У меня есть несколько случайных точек в рендерере 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.

Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
0
0
55
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я нашел элегантный способ получить координаты точки щелчка. Он использует 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_())

Вот скриншот результата:

Однако я не совсем уверен, что это лучший способ достичь моей цели. Любые комментарии приветствуются.

Другие вопросы по теме