PyQt + VTK 三维软件 1. 自定义交互样式

键鼠交互的功能和逻辑

左键旋转,右键平移,滚轮缩放,中键选点,按键上色。自定义交互样式类,重点是重载几个事件监听调用函数,VTK 初始的那个交互样式说实话不是特别喜欢。也许我的习惯别人也不是很喜欢,打个模板大家自取,有更好的设计欢迎交换。

class CustomInteractorStyle(vtk.vtkInteractorStyleTrackballCamera):
    """
    创建一个自定义的交互器样式
    """
    def __init__(self):
        """
            重载以下几个键鼠监听事件:
        """
        # 鼠标左键的按和放
        self.AddObserver("LeftButtonPressEvent", self.left_button_down)
        self.AddObserver("LeftButtonReleaseEvent", self.left_button_up)

        # 鼠标右键的按和放
        self.AddObserver("RightButtonPressEvent", self.right_button_down)
        self.AddObserver("RightButtonReleaseEvent", self.right_button_up)

        # 鼠标中键
        self.AddObserver('MiddleButtonPressEvent', self.middle_button_press_event)
        
        # 鼠标的移动
        self.AddObserver("MouseMoveEvent", self.mouse_move)

        # 鼠标滚轮
        self.AddObserver("MouseWheelForwardEvent", self.mouse_wheel_forward)
        self.AddObserver("MouseWheelBackwardEvent", self.mouse_wheel_backward)

        # 键盘的按键
        self.AddObserver("KeyPressEvent", self.key_press)

        self.event_position = (0, 0)  # 用于存储鼠标事件发生时鼠标的位置
        self.mouse = 0  # 用 -1 表示左键,1 表示右键,因为用鼠标移动事件来表示拖曳行为,但不同键按下时的拖曳行为对应的效果不同

监听鼠标移动:左键拖曳旋转,右键拖曳平移

实现的逻辑如下:

  1. 记录下当前鼠标位置
  2. 判断如果左键是按下的状态,就按照移动量旋转目标;如果右键是按下的状态,就平移相机;如果没有按下任何键,就不动作。
  3. 用当前鼠标位置更新上一时刻鼠标位置

那么平移和旋转分别如何实现呢?鼠标在二维的屏幕上发生了平移,因此输入的移动量是 dx 和 dy。【旋转】比较简单,-dx(注意是用了负号) 和 -dy 分别对应 VTK 相机对象的 Azimuth() 和 Elevation() 方法就可以设置相机的方位角和俯仰角。需要注意的是,使用 Azimuth() 或 Yaw() 之后,在使用 Elevation() 之前需要重新计算 ViewUp。原因是视向量平行于视平面法向,因此在南极和北极存在奇异点,因此强制正交改变一下相机的坐标系(有一些优化不太好的三维软件就有这个毛病,鼠标控制旋转的时候老是达不到想要的效果,很烦人)。除此以外我在调试的时候还遇到一个问题,就是 VTK + PYQT 使用的时候,如果三维模型稍微大一些,在旋转的时候会有一部分被隐藏掉,其实是因为 VTK 的渲染优化吧,就是只显示比当前模型稍大一些的一个矩形框,框外是不渲染的,所以旋转的时候就需要用 ResetCameraClippingRange() 重置一下这个截断矩形框。

def rotate_camera(self, delta_x, delta_y):
    speed = 0.5
    camera = self.GetInteractor().GetRenderWindow().GetRenderers().GetFirstRenderer().GetActiveCamera()
    camera.Azimuth(-delta_x * speed)
    camera.OrthogonalizeViewUp()
    camera.Elevation(-delta_y * speed)
    # 防止在旋转的时候截断显示
    self.GetInteractor().GetRenderWindow().GetRenderers().GetFirstRenderer().ResetCameraClippingRange()
    self.GetInteractor().Render()

【平移】原理也很简单,但是实现稍微麻烦一点,因为涉及到窗口的平面坐标系转换到三维世界坐标系。当然 VTK 也是提供了 ComputeWorldToDisplay() 和 ComputeDisplayToWorld() 方法,然后我做的是把当前相机的 FocalPoint 和 Position 一起移动,这样视角会发生轻微的旋转,因为 FocalPoint 并不是无限远的点;而且也不是只改 Position,我个人觉得这样稍微自然一些。

def mouse_move(self, obj, event):
    new_position = self.GetInteractor().GetEventPosition()
    if self.mouse == -1:
        # 左键旋转
        delta_x = new_position[0] - self.event_position[0]
        delta_y = new_position[1] - self.event_position[1]
        self.event_position = self.GetInteractor().GetEventPosition()
        self.rotate_camera(delta_x, delta_y)
    elif self.mouse == 1:
        # 右键平移
        delta_x = new_position[0] - self.event_position[0]
        delta_y = new_position[1] - self.event_position[1]
        self.move_camera(self.event_position[0], self.event_position[1], delta_x, delta_y)
    self.event_position = new_position

监听鼠标滚轮:滚动缩放,中键选点

【缩放】就比较简单了,VTK 相机对象的 Zoom() 方法,传递一个比例系数就可以。

def zoom_camera(self, factor):
    camera = self.GetInteractor().GetRenderWindow().GetRenderers().GetFirstRenderer().GetActiveCamera()
    camera.Zoom(factor)
    self.GetInteractor().Render()

【选点】选点的功能可以借助 VTK 的 vtkCellPicker 或 vtkPointPicker,查阅了一下资料,这两个选点器大致相同,唯一的区别是 vtkCellPicker 会返回可见面上对象的信息而 vtkPointPicker 不一定。这是因为 vtkCellPicker 会向三维场景发射一条射线,并返回射线第一次击中的对象信息,如果没有击中任何对象就返回 -1,而 vtkPointPicker 会返回在指定容差范围内投影到射线上距离最近的点的 id,这就说明 vtkPointPicker 返回的不一定是可见面上的点。两个选点器用法差不多,按需求选。同时,为了可视化选点,我还在选点器击中的位置那里画了一个红色小球,当然这样的话,后期如果要清除这些红色小球要从渲染器中遍历对象挨个剔除。

def middle_button_press_event(self, obj, event):

    # 获取中键按下时鼠标的位置
    pos = self.GetInteractor().GetEventPosition()

    # 创建选点器
    picker = vtk.vtkCellPicker()

    # 从渲染器中选点
    render = self.GetInteractor().GetRenderWindow().GetRenderers().GetFirstRenderer()
    picker.Pick(pos[0], pos[1], 0, render)
    
    if picker.GetPointId() != -1:
        
        # 击中对象在世界坐标系中的位置
        world_position = picker.GetPickPosition()
        print(f'选点位置: ({world_position[0]:.6g}, {world_position[1]:.6g}, {world_position[2]:.6g})')
        print("选点的 id", picker.GetPointId())

        # 为了高亮显示,所以在击中对象处创建一个红色小球用于标识
        sphere_source = vtk.vtkSphereSource()
        sphere_source.SetCenter(world_position[0], world_position[1], world_position[2])
        sphere_source.SetRadius(0.3)

        # 创建一个映射器和渲染对象
        mapper = vtk.vtkPolyDataMapper()
        mapper.SetInputConnection(sphere_source.GetOutputPort())

        actor = vtk.vtkActor()
        actor.SetMapper(mapper)
        actor.GetProperty().SetColor(1, 0, 0)  # 设置为红色

        # 将渲染对象添加到渲染器中
        render.AddActor(actor)

    self.OnMiddleButtonDown()

监听键盘按键:按 C 一键换色

【键盘】键盘交互肯定不能少的,但按键很多,根据自己需求挨个定义就行,比如我这里定义了一个按下 C 键用随机的颜色涂满所有的 actor。 

def key_press(self, obj, event):
    key = self.GetInteractor().GetKeySym()
    if key == 'c' or key == 'C':
        # 产生随机颜色
        r = vtk.vtkMath.Random()
        g = vtk.vtkMath.Random()
        b = vtk.vtkMath.Random()
        actors = self.GetInteractor().GetRenderWindow().GetRenderers().GetFirstRenderer().GetActors()
        for actor in actors:
            actor.GetProperty().SetColor(r, g, b)
        # 更新渲染
        self.GetInteractor().Render()

完整代码

可以把完整代码保存成一个 .py 文件然后通过下面这种方式导入,在设置 VTK 交互器的时候使用 SetInteractorStyle() 即可。

from VTKQTInteractorStyle import CustomInteractorStyle

完整的类定义,以及一个简单的用法示例如下:

import vtk


class CustomInteractorStyle(vtk.vtkInteractorStyleTrackballCamera):
    """
    创建一个自定义的交互器样式
    """
    def __init__(self):
        """
            重载以下几个键鼠监听事件:
        """
        # 鼠标左键的按和放
        self.AddObserver("LeftButtonPressEvent", self.left_button_down)
        self.AddObserver("LeftButtonReleaseEvent", self.left_button_up)

        # 鼠标右键的按和放
        self.AddObserver("RightButtonPressEvent", self.right_button_down)
        self.AddObserver("RightButtonReleaseEvent", self.right_button_up)

        # 鼠标中键
        self.AddObserver('MiddleButtonPressEvent', self.middle_button_press_event)
        
        # 鼠标的移动
        self.AddObserver("MouseMoveEvent", self.mouse_move)

        # 鼠标滚轮
        self.AddObserver("MouseWheelForwardEvent", self.mouse_wheel_forward)
        self.AddObserver("MouseWheelBackwardEvent", self.mouse_wheel_backward)

        # 键盘的按键
        self.AddObserver("KeyPressEvent", self.key_press)

        self.event_position = (0, 0)  # 用于存储鼠标事件发生时鼠标的位置
        self.mouse = 0  # 用 -1 表示左键,1 表示右键,因为用鼠标移动事件来表示拖曳行为,但不同键按下时的拖曳行为对应的效果不同

    def left_button_down(self, obj, event):
        # 按下左键
        self.mouse = -1
        self.event_position = self.GetInteractor().GetEventPosition()

    def left_button_up(self, obj, event):
        # 松开左键
        self.mouse = 0
        self.event_position = (0, 0)

    def right_button_down(self, obj, event):
        # 按下右键
        self.mouse = 1
        self.event_position = self.GetInteractor().GetEventPosition()

    def right_button_up(self, obj, event):
        # 松开右键
        self.mouse = 0
        self.event_position = (0, 0)

    def mouse_move(self, obj, event):
        new_position = self.GetInteractor().GetEventPosition()
        if self.mouse == -1:
            # 左键旋转
            delta_x = new_position[0] - self.event_position[0]
            delta_y = new_position[1] - self.event_position[1]
            self.event_position = self.GetInteractor().GetEventPosition()
            self.rotate_camera(delta_x, delta_y)
        elif self.mouse == 1:
            # 右键平移
            delta_x = new_position[0] - self.event_position[0]
            delta_y = new_position[1] - self.event_position[1]
            self.move_camera(self.event_position[0], self.event_position[1], delta_x, delta_y)
        self.event_position = new_position

    def rotate_camera(self, delta_x, delta_y):
        speed = 0.5
        camera = self.GetInteractor().GetRenderWindow().GetRenderers().GetFirstRenderer().GetActiveCamera()
        camera.Azimuth(-delta_x * speed)
        camera.OrthogonalizeViewUp()
        camera.Elevation(-delta_y * speed)
        # 防止在旋转的时候截断显示
        self.GetInteractor().GetRenderWindow().GetRenderers().GetFirstRenderer().ResetCameraClippingRange()
        self.GetInteractor().Render()

    def move_camera(self, x, y, delta_x, delta_y):

        render = self.GetInteractor().GetRenderWindow().GetRenderers().GetFirstRenderer()
        camera = render.GetActiveCamera()

        view_focus_3d = camera.GetFocalPoint()
        view_focus_2d = [0, 0, 0]
        self.ComputeWorldToDisplay(render, view_focus_3d[0], view_focus_3d[1], view_focus_3d[2], view_focus_2d)
        new_mouse_point = [0, 0, 0, 1]
        self.ComputeDisplayToWorld(render, x, y, view_focus_2d[2], new_mouse_point)
        old_mouse_point = [0, 0, 0, 1]
        self.ComputeDisplayToWorld(render, x - delta_x, y - delta_y, view_focus_2d[2], old_mouse_point)

        motion_vector = [0, 0, 0]
        motion_vector[0] = old_mouse_point[0] - new_mouse_point[0]
        motion_vector[1] = old_mouse_point[1] - new_mouse_point[1]
        motion_vector[2] = old_mouse_point[2] - new_mouse_point[2]

        view_focus = camera.GetFocalPoint()
        view_point = camera.GetPosition()
        camera.SetFocalPoint(motion_vector[0] + view_focus[0], motion_vector[1] + view_focus[1],
                             motion_vector[2] + view_focus[2])
        camera.SetPosition(motion_vector[0] + view_point[0], motion_vector[1] + view_point[1],
                           motion_vector[2] + view_point[2])
        self.GetInteractor().Render()

    def mouse_wheel_forward(self, obj, event):
        self.zoom_camera(1.1)

    def mouse_wheel_backward(self, obj, event):
        self.zoom_camera(0.9)

    def zoom_camera(self, factor):
        camera = self.GetInteractor().GetRenderWindow().GetRenderers().GetFirstRenderer().GetActiveCamera()
        camera.Zoom(factor)
        self.GetInteractor().Render()

    def key_press(self, obj, event):
        key = self.GetInteractor().GetKeySym()
        if key == 'c' or key == 'C':
            # 产生随机颜色
            r = vtk.vtkMath.Random()
            g = vtk.vtkMath.Random()
            b = vtk.vtkMath.Random()
            actors = self.GetInteractor().GetRenderWindow().GetRenderers().GetFirstRenderer().GetActors()
            for actor in actors:
                actor.GetProperty().SetColor(r, g, b)
            # 更新渲染
            self.GetInteractor().Render()

    def middle_button_press_event(self, obj, event):

        # 获取中键按下时鼠标的位置
        pos = self.GetInteractor().GetEventPosition()

        # 创建选点器
        picker = vtk.vtkCellPicker()

        # 从渲染器中选点
        render = self.GetInteractor().GetRenderWindow().GetRenderers().GetFirstRenderer()
        picker.Pick(pos[0], pos[1], 0, render)

        if picker.GetPointId() != -1:

            # 击中对象在世界坐标系中的位置
            world_position = picker.GetPickPosition()
            print(f'选点位置: ({world_position[0]:.6g}, {world_position[1]:.6g}, {world_position[2]:.6g})')
            print("选点的 id", picker.GetPointId())

            # 为了高亮显示,所以在击中对象处创建一个红色小球用于标识
            sphere_source = vtk.vtkSphereSource()
            sphere_source.SetCenter(world_position[0], world_position[1], world_position[2])
            sphere_source.SetRadius(0.3)

            # 创建一个映射器和渲染对象
            mapper = vtk.vtkPolyDataMapper()
            mapper.SetInputConnection(sphere_source.GetOutputPort())

            actor = vtk.vtkActor()
            actor.SetMapper(mapper)
            actor.GetProperty().SetColor(1, 0, 0)  # 设置为红色

            # 将渲染对象添加到渲染器中
            render.AddActor(actor)

        self.OnMiddleButtonDown()


if __name__ == '__main__':

    # 创建一个渲染器
    renderer = vtk.vtkRenderer()

    # 创建一个 STL 文件读取器
    reader = vtk.vtkSTLReader()
    file_path = "Arduino.STL"
    reader.SetFileName(file_path)
    reader.Update()

    # 获取读取的数据
    polyData = reader.GetOutput()

    # 创建一个 Mapper 和 Actor 来显示模型
    mapper = vtk.vtkPolyDataMapper()
    mapper.SetInputData(polyData)

    actor = vtk.vtkActor()
    actor.SetMapper(mapper)
    renderer.AddActor(actor)

    # 创建一个渲染窗口
    renderWindow = vtk.vtkRenderWindow()
    renderWindow.AddRenderer(renderer)

    # 创建一个交互器
    interactor = vtk.vtkRenderWindowInteractor()
    interactor.SetRenderWindow(renderWindow)

    # 设置自定义的交互器样式
    style = CustomInteractorStyle()
    style.SetDefaultRenderer(renderer)
    interactor.SetInteractorStyle(style)

    # 设置相机的裁剪范围
    renderer.ResetCamera()
    renderer.ResetCameraClippingRange()

    # 开始渲染和交互
    renderWindow.Render()
    interactor.Start()

  • 38
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
PyQt是一个用于创建图形用户界面的Python库,而VTK(Visualization Toolkit)是一个用于可视化和处理三维数据的开源库。结合PyQtVTK可以实现在PyQt界面中显示VTK三维图像的功能。 下面是一种常见的实现方式: 1. 首先,确保已经安装了PyQtVTK库。 2. 创建一个PyQt的窗口类,继承自QWidget或QMainWindow。 3. 在窗口类中创建一个QVTKRenderWindowInteractor对象,用于在PyQt界面中显示VTK图像。 4. 创建一个VTK的渲染器和渲染窗口对象,并将渲染窗口对象与QVTKRenderWindowInteractor对象关联。 5. 加载或生成需要显示的三维数据,并创建一个VTK的数据源对象。 6. 创建一个VTK的Mapper对象,将数据源对象与Mapper对象关联。 7. 创建一个VTK的Actor对象,将Mapper对象与Actor对象关联。 8. 将Actor对象添加到渲染器中。 9. 最后,通过调用QVTKRenderWindowInteractor对象的Start()方法来启动渲染循环,显示VTK图像在PyQt界面中。 下面是一个简单的示例代码: ```python import sys from PyQt5.QtWidgets import QApplication, QMainWindow from PyQt5.QtGui import QPalette, QColor from PyQt5.QtCore import Qt import vtk from vtk.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor class MainWindow(QMainWindow): def __init__(self): super().__init__() # 创建QVTKRenderWindowInteractor对象 self.vtkWidget = QVTKRenderWindowInteractor(self) # 创建VTK渲染器和渲染窗口对象 self.ren = vtk.vtkRenderer() self.renWin = self.vtkWidget.GetRenderWindow() self.renWin.AddRenderer(self.ren) # 加载或生成需要显示的三维数据 # ... # 创建VTK数据源对象 # ... # 创建VTK Mapper对象 # ... # 创建VTK Actor对象 # ... # 将Actor对象添加到渲染器中 # ... # 设置窗口背景颜色 self.setAutoFillBackground(True) pal = self.palette() pal.setColor(QPalette.Background, QColor(0, 0, 0)) self.setPalette(pal) # 设置窗口布局 layout = QVBoxLayout() layout.addWidget(self.vtkWidget) centralWidget = QWidget() centralWidget.setLayout(layout) self.setCentralWidget(centralWidget) if __name__ == "__main__": app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec_()) ``` 这是一个简单的示例,具体的实现方式可能会根据具体需求而有所不同。你可以根据自己的需求进行修改和扩展。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值