返回介绍

TVTK-三维可视化数据

发布于 2025-02-25 22:46:21 字数 16911 浏览 0 评论 0 收藏 0

VTK ( http://www.vtk.org/ ) 是一套三维的数据可视化工具,它由 C++编写,包涵了近千个类帮助我们处理和显示数据。它在 Python 下有标准的绑定,不过其 API 和 C++相同,不能体现出 Python 作为动态语言的优势。因此 enthought.com 开发了一套 TVTK 库对标准的 VTK 库进行包装,提供了 Python 风格的 API、支持 Trait 属性和 numpy 的多维数组。本文将以 TVTK 为标准对 VTK 的一些功能进行介绍,如果读者已经对 VTK 很了解,想知道 TVTK 和 VTK 的区别的话,可以直接跳到第二节。

TVTK 使用简介

a

显示圆锥

作为第一例子,让我们来看一个显示圆锥的小程序:

# -*- coding: utf-8 -*-
from enthought.tvtk.api import tvtk

# 创建一个圆锥数据源,并且同时设置其高度,底面半径和底面圆的分辨率(用 36 边形近似)
cs = tvtk.ConeSource(height=3.0, radius=1.0, resolution=36)
# 使用 PolyDataMapper 将数据转换为图形数据
m = tvtk.PolyDataMapper(input = cs.output)
# 创建一个 Actor
a = tvtk.Actor(mapper=m)
# 创建一个 Renderer,将 Actor 添加进去
ren = tvtk.Renderer(background=(0.1, 0.2, 0.4))
ren.add_actor(a)
# 创建一个 RenderWindow(窗口),将 Renderer 添加进去
rw = tvtk.RenderWindow(size=(300,300))
rw.add_renderer(ren)
# 创建一个 RenderWindowInteractor(窗口的交互工具)
rwi = tvtk.RenderWindowInteractor(render_window=rw)
# 开启交互
rwi.initialize()
rwi.start()

此程序的运行画面如下:

使用 TVTK 绘制简单的圆锥

首先从 tvtk.api 中载入 tvtk,tvtk 像是一个工厂,能够帮助我们创建 vtk 中的各种对象:

>>> from enthought.tvtk.api import tvtk

下面创建了一个 ConeSource(圆锥数据源)对象,并用变量 cs 保存它。原始的 VTK 对象的属性,在 tvtk 中都以 trait 属性的形式进行包装,因此我们可以在创建对象的同时,传递关键字参数直接配置各个 trait 属性的值,在这个例子中,同时设置了圆锥的高度,底面半径和底面圆的分辨率(用 36 边形近似) 等属性,最后调用 print_traits 显示所创建的圆锥数据的所有 trait 属性,为了节省篇幅,这里只挑选了其中的几个属性:

>>> cs = tvtk.ConeSource(height=3.0, radius=1.0, resolution=36)
>>> cs.print_traits()
...
angle:               18.43494882292201
...
center:              array([ 0.,  0.,  0.])
class_name:            'vtkConeSource'
...
direction:             array([ 1.,  0.,  0.])
...
height:              3.0
...
radius:              1.0
...
resolution:            36
...

在 VTK 中将原始数据转换为我们看到的屏幕上的一幅图像,要经过许多步骤的处理,这些步骤由众多的 VTK 的对象共同协调完成,就好象生产线上加工零件一样,每位工人都负责一部分的工作,整条生产线就能将原材料制作成产品。因此在 VTK 中,这种对象之间协调完成工作的过程被称作流水线(Pipeline)。

原始数据被转换为图像要经过两条流水线:

  • 可视化流水线(Visualization Pipeline):它的工作是将原始数据加工成图形数据。通常我们需要可视化的数据本身并不是图形数据,例如某个零件内部各个部分的温度,或者是流体在各个坐标点上的速度等等。
  • 图形流水线(Graphics Pipeline):它的工作是将将图形数据加工为我们所看到的图像。可视化流水线所产生的图形数据通常是三维空间的数据,如何在二维的屏幕上显示出来就需要图形流水线的加工了。

映射器(Mapper) 则是可视化流水线的终点,图形流水线的起点,它将各种派生类能将众多的数据映射为图形数据以供图形流水线加工。

让我们对照一下前面的的圆锥的例子:ConeSource 的对象通过程序内部计算输出一组描述圆锥的数据(PolyData):然后,PolyData 通过 PolyDataMapper 映射器将数据映射为图形数据。在这个例子中,可视化流水线由 ConSource 和 PolyDataMapper 组成。

图形数据依次通过 Actor、Renderer 最终在 RenderWindow 中显示出来,这一部分就是图形流水线。

  • Actor : 表示润色场景中的一个实体。它包括一个图形数据(mapper),并且具有描述实体的位置、方向、大小的属性。
  • Renderer : 表示润色的场景。它包括多个需要润色的 Actor。在圆锥的例子中,它只包括一个表示圆锥的 Actor。
  • RenderWindow : 表示润色用的图形窗口,它包括一个或者多个 Render。在圆锥的例子中,它只包括一个 Renderer。
  • RenderWindowInteractor : 给图形窗口提供一些用户交互功能,例如平移、旋转、放大缩小。这些交互式操作并不改变 Actor 或者图形数据的属性,只是调整场景中的照相机(Camera) 的一些设置而已。

什么是 PolyData

PolyData 是一个描述一组三维空间中的点、线、面的数据结构。点、线、面通过以下几个属性描述:

  • points : 类型为 Points,保存三维空间中的点的坐标的数组,这些数据不是用来显示的。
  • verts : 类型为 CellArray,它描述需要显示的顶点,其值为 points 某个坐标点的下标,即通过 verts 属性描述 points 中的哪些点是最终需要显示的。
  • line : 类型为 CellArray,它描述需要显示的边线,其值为边线的两个端点在 points 中的下标。
  • polys : 类型为 CellArray,它描述需要显示的面,其值为构成面的各个点在 points 中的下标。

用 ivtk 观察流水线

为了方便我们操作和观察流水线,交互式地修改各个 tvtk 对象的属性,TVTK 库为我们提供了一个叫做 ivtk 的对象。下面是使用 ivtk 显示圆锥的程序:

# -*- coding: utf-8 -*-
from enthought.tvtk.api import tvtk

# 载入 ivtk 所需要的对象
from enthought.tvtk.tools import ivtk
from enthought.pyface.api import GUI

cs = tvtk.ConeSource(height=3.0, radius=1.0, resolution=36)
m = tvtk.PolyDataMapper(input = cs.output)
a = tvtk.Actor(mapper=m)

# 创建一个 GUI 对象,和一个带 Crust(Python shell) 的 ivtk 窗口
gui = GUI()
window = ivtk.IVTKWithCrustAndBrowser(size=(800,600))
window.open()
window.scene.add_actor( a ) # 将圆锥的 actor 添加进窗口的场景中
gui.start_event_loop()
#window.scene.reset_zoom()

此程序的运行画面如下:

带流水线浏览器和 Python Shell 的界面

除了显示圆锥的场景之外,ivtk 创建的窗口还为我们提供了如下几个元素:

  • 场景工具条 : 位于润色场景的上方。主要提供了各种视角、全屏显示、保存图像等几个功能。
  • 流水线浏览器 : 场景的左边是一个用树状结构表示的流水线。从叶子节点(ConeSource) 开始逐步向上层直到根节点 RenderWindow,是完整的显示圆锥的流水线。
  • Python Shell : 下方提供了一个 Python Shell,便于我们直接输入命令操作各个对象。例如图中我们打印出 ConeSource 的输出 PolyData 的 points 属性的值。即构成圆锥图形的各个点的三维坐标。

流水线浏览器中显示的各个对象的类型都继承于 HasTraits 类,因此它们都可以提供一个用户界面交互式地修改它们的 trait 属性。下图是双击 ConeSource 之后出现的修改 ConeSource 属性的界面。

Note

在我的电脑上,双击 ConeSource 之后出现一个很小的窗口,需要手工调整大小。

编辑 ConeSource 对象的属性的对话框

我们看到可以通过此界面直接修改 height、Radius、resolution 等属性,并且修改之后场景中的圆锥按照最新的属性值立即更新显示。

从文件读取数据

大多数情况下我们不会只用 VTK 显示圆锥这样的简单物体,VTK 的建模功能并不强大,因此它支持许多种格式的文件,能将其它软件产生的数据通过各种 Reader 类读入 VTK,放到流水线上处理。下面的例子从文件 42400-IDGH.stl 中载入模型数据,并且润色显示。

# -*- coding: utf-8 -*-
from enthought.tvtk.api import tvtk
from enthought.tvtk.tools import ivtk
from enthought.pyface.api import GUI

part =tvtk.STLReader(file_name = "42400-IDGH.stl")
part_mapper = tvtk.PolyDataMapper( input = part.output )
part_actor = tvtk.Actor( mapper = part_mapper )

gui = GUI()
window = ivtk.IVTKWithBrowser(size=(800,600))
window.open()
window.scene.add_actor( part_actor ) 
gui.start_event_loop()

此程序的运行画面如下:

显示文件中的 3D 模型

对比显示圆锥的程序,除了 PolyDataMapper 的输入从 ConeSource 改为 STLReader 之外,其他的部分没有任何区别。STLReader 对象知道如何读取 STL 文件中的数据,并且转换为 PolyData,以供 PolyDataMapper 使用。另外请注意我们这次用 ivtk.IVTKWithBrowser 产生一个不带 Python Shell 的 ivtk 窗口。

STL 是什么文件

STL 的全称为 stereo-lithography,由 3D Systems 公司开发,它使用三角形面片来表示三维实体模型,现已成为 CAD/CAM 系统接口文件格式的工业标准之一,绝大多数造型系统能支持并生成此种格式文件。例子中的 42400-IDGH.stl 文件来自于 VTK 的示例数据。笔者对模具设计没有研究,只是照葫芦画瓢,把 VTK 的例子转换为 TVTK 而已。

过滤数据

前面的例子中包括一个数据源和 mapper 对象,但是流水线中没有过滤器对数据进行过滤,下面我们看看如何对数据进行过滤以减少多边形面的数量。对上节的程序进行修改,在 STLReader 和 PolyDataMapper 之间插入一个 ShrinkPolyData 对象:

part =tvtk.STLReader(file_name = "42400-IDGH.stl")
shrink = tvtk.ShrinkPolyData(input = part.output, shrink_factor = 0.5 )
part_mapper = tvtk.PolyDataMapper( input = shrink.output )

ShrinkPolyData 过滤器的输入和输出都是 PolyData,它可以减少输入 PolyData 对象中单元(点线面) 的数目,但是会造成不单元之间不连续。

使用 ShrinkPolyData 过滤器过滤后的模型

控制照相机

如果你使用 ivtk 显示 3D 数据的话,在左边的流水线浏览器中可以找到 OpenGLCamera,双击它弹出如下窗口:

编辑照相机属性的对话框

这个窗口显示的是 3D 场景的照相机的所有配置。如果你需要用程序控制照相机的话,可以用:

>>> camera = window.scene.renderer.active_camera

获得场景中的当前照相机对象,然后就可以获得或者修改照相机的各项配置:

>>> camera.clipping_rage
array([ 20.46912341,  51.21854284])
>>> camera.view_up = 0,1,0

下面介绍一些照相机的一些常用属性:

  • clipping_plane : 它有两个元素,分别表示照相机到近、远两个裁剪平面的距离。在这两个平面之外的对象将不会被显示出来。
  • position : 照相机在三维空间中的坐标
  • focal_point : 照相机所聚焦的焦点坐标
  • view_up : 照相机的上方向矢量
  • parallel_projection : 如果为 True 的话表示采用平行透视,即在 3D 场景中平行的线投影到 2D 平面上将仍然是平行的

这些属性虽然可以完全控制照相机的位置和方向,但是实际操作起来并不方便。当将照相机的焦点已经固定好在某个位置上的话,可以通过调用: ..TODO *azimuth : 以焦点为圆心,沿着纬度线旋转指定角度,即水平旋转,改变其经度

  • elevation : 沿着经度线方向旋转指定角度,即垂直旋转,改变其纬度

在以焦点为原点的球体坐标系中对照相机进行操作。这两个函数保持 view_up 属性不变。

控制照明

照明比照相机容易配置得多,假设你运行了 ivtk 的圆锥的例子的话,直接在窗口下方的命令行中输入:

>>> camera = window.scene.renderer.active_camera
>>> light = tvtk.Light(color=(1,0,0))
>>> light.position=camera.position
>>> light.focal_point=camera.focal_point
>>> window.scene.renderer.add_light(light)

..TODO 即可在照相机所在处添加一个红色的光源,它的照射方向为朝向 focal_point 点。如果你设置 light 的 positional 属性为 True 的话,那么它就变成一个探照灯光源,这时照射方向有效。并且可以通过 cone_angle 属性设置探照灯的光锥角度。光锥为 180 度的话,就是无方向光源。

控制 3D Props

在 3D 场景中显示的物体通常被称作 prop,有几种 prop 类型,其中包括:Prop3D 和 Actor。3D 场景中所有 prop 的都从 Prop3D 继承。

TVTK 的改进

下面是使用 Python 的标准 VTK 库显示一个圆锥的例子:

import vtk

# Source object .
cone = vtk.vtkConeSource( )
cone.SetHeight( 3.0 )
cone.SetRadius( 1.0 )
cone.SetResolution(10)
# The mapper .
coneMapper = vtk.vtkPolyDataMapper( )
coneMapper.SetInput( cone.GetOutput( ) )
# The actor.
coneActor = vtk.vtkActor( )
coneActor.SetMapper ( coneMapper )
# Set it to render in wireframe
coneActor.GetProperty( ).SetRepresentationToWireframe( )

# Renderer and render window .
ren1 = vtk.vtkRenderer( )
ren1.AddActor( coneActor )
ren1.SetBackground( 0.1 , 0.2 , 0.4 )
renWin = vtk.vtkRenderWindow( )
renWin.AddRenderer( ren1 )
renWin.SetSize(300 , 300)

# On screen interaction .
iren = vtk.vtkRenderWindowInteractor( )
iren.SetRenderWindow( renWin )
iren.Initialize( )
iren.Start( )

我们可以出这个例子和 C++的程序的区别仅仅是没有声明变量的类型,其它的用法完全是按照 C++的 VTK API 调用的。官方所提供的 VTK-Python 包和 C++语言的接口相似,许多地方没有能够体现出 Python 作为动态语言的优势,可以说标准的 VTK-Python 库不够 Python 风格。为了弥补标准库的这些不足之处,Enthought.com 开发了 TVTK 库进一步对 VTK 进行包装,它具有如下的一些优点:

  • 支持 Trait 属性
  • 支持元素的 Pickle 操作
  • API 更接近 Python 风格
  • 能自动处理 numpy 的数组或者 Python 的列表
  • 高级的脚本式的 mlab API,和流水线浏览器 ivtk
  • tvtk 的场景和流水线浏览器支持 Envisage 插件

TVTK 的基本用法

下面是用 TVTK 实现上面的显示圆锥的例子:

from enthought.tvtk.api import tvtk

cone = tvtk.ConeSource( height=3.0, radius=1.0, resolution=10 )
cone_mapper = tvtk.PolyDataMapper( input = cone.output )
cone_actor = tvtk.Actor( mapper=cone_mapper )
cone_actor.property.representation = "w"

ren1 = tvtk.Renderer()
ren1.add_actor( cone_actor )
ren1.background = 0.1, 0.2, 0.4
ren_win = tvtk.RenderWindow()
ren_win.add_renderer( ren1 )
ren_win.size = 300, 300

iren = tvtk.RenderWindowInteractor( render_window = ren_win )
iren.initialize()
iren.start()

可以看到这个程序比标准 VTK 版本要简短得多,从中我们可以看到 TVTK 的一些重要的更改:

  • tvtk 用 from enthought.tvtk.api import tvtk 语句载入
  • tvtk 的类名为 VTK 的类名除去前缀"vtk"。有些类名在"vtk"之后是数字:vtk3DSImporter,由于 Python 的标示符首字符不能为数字,因此 tvtk 对此进行特殊处理:如果首字符为数字,则用其英文单词代替: ThreeDSImporter。
  • tvtk 对象的方法名按照 Enthought 的一贯用法,采用下划线连接单词:例如如果 VTK 中的 AddItem,将对应 tvtk 中的 add_item。
  • 许多 VTK 的方法在 tvtk 中用 trait 属性替代,例如前面的例子中我们用 m.input = cs.output 表示 VTK 中的 m.SetInput(cs.GetOutput()),p.representation = 'w'表示 VTK 中的 p.SetRepresentationToWireframe()。由于在 VTK 中许多需要用 Set, Get 的属性在 TVTK 中都是以 Trait 属性表现的,Trait 属性的初始化可以在产生对象的同时进行配置。
  • trait 属性可以在创建类的对象的同时通过关键字参数进行设置

在内部实现中,所有的 tvtk 对象都内部包装有一个 VTK 对象,对 tvtk 对象的方法的调用将转给相应的 VTK 对象的方法执行,如果返回值是 VTK 对象的话,将被包装成 tvtk 对象返回。如果方法的参数是 tvtk 对象的话,其中的 VTK 对象将传递给 VTK 的方法。

通过调用 tvtk.to_tvtk(p),可以得到 p 中所包装的 VTK 对象

Trait 属性

所有的 tvtk 类都继承于 traits.HasStrictTraits,HasStrictTraits 规定了它的子类的对象在创建之后不能对不存在的属性进行赋值。

VTK 中所有和基本状态有关的的方法在 tvtk 中都以 trait 属性表示。trait 属性为我们带来如下的便利:

  • 通过调用 set 方法可以一次设置多个 trait 属性:
    >>> p = tvtk.Property()
    >>> p.set(opacity=0.5, color=(1,0,0), representation="w")
    
  • 通过调用 edit_traits 或者 configure_traits 方法直接出界面编辑属性。对 trait 属性的更改将自动作用到内部的 VTK 对象之上,反过来,内部的 VTK 对象的状态改变也将自动更新 trait 属性。下面是一个例子:
    >>> p.edit_traits()
    

每个 TVTK 对象都可以有自己的编辑属性的对话框界面

  • 我们可以通过 tvtk.to_tvtk(p) 函数得到任何 tvtk 对象所包装的 TVK 对象:
    >>> print p.representation
    wireframe
    >>> p_vtk = tvtk.to_vtk(p)
    >>> p_vtk.SetRepresentationToSurface()
    >>> print p.representation
    surface
    

序列化(Pickling)

tvtk 对象支持简单的序列化处理。单个 tvtk 对象的状态可以被序列化:

>>> import cPickle
>>> p = tvtk.Property()
>>> p.representation="w"
>>> s = cPickle.dumps(p)
>>> del p
>>> q = cPickle.loads(s)
>>> q.representation
'wireframe'

但是序列化仅仅能保存对象的状态,对象之间的引用无法被保存。因此 VTK 的整个流水线无法用序列化保存。

通常 pickle.load 将创建新的对象,如果我们希望更新某个已经存在的对象的状态的话,可以如下调用:

>>> p = tvtk.Property()
>>> p.interpolation = "flat"
>>> d = p.__getstate__()
>>> del p
>>> q = tvtk.Property()
>>> q.interpolation
'gouraud'
>>> q.__setstate__(d)
>>> q.interpolation
'flat'

集合迭代

从 tvtk.Collection 继承的对象可以像标准的 Python 序列对象一样使用:

>>> ac = tvtk.ActorCollection()
>>> len(ac)
0
>>> ac.append(tvtk.Actor())
>>> ac.append(tvtk.Actor())
>>> len(ac)
2
>>> for a in ac:
...   print a
...
vtkOpenGLActor (06A99EB8)
......
vtkOpenGLActor (069C4270)
......
>>> del ac[0]
>>> len(ac)
1

我们看到 ActorCollection 可以像 Python 的列表对象一样支持 len, append 和 for 循环。对比一下 VTK 的相应的版本,就能体会出 tvtk 的好处了:

>>> ac = vtk.vtkActorCollection()
>>> ac.GetNumberOfItems()
0
>>> ac.AddItem(vtk.vtkActor())
>>> ac.AddItem(vtk.vtkActor())
>>> ac.GetNumberOfItems()
2
>>> ac.InitTraversal()
>>> for i in range(ac.GetNumberOfItems()):
...   print ac.GetNextItem()
...
vtkOpenGLActor (05E0A750)
......
vtkOpenGLActor (05E0A8C0)
......
>>> ac.RemoveItem(0)
>>> ac.GetNumberOfItems()
1

数组操作

所有继承于 DataArray 类的对象和 Python 的序列一样,支持迭代接口,以及 __getitem__, __setitem__, __repr__, append, extend 等等。此外,它还可以直接用 numpy 的数组或者 python 的列表直接进行赋值(使用 from_array 方法),或者将 DataArray 中保存的数据转换为 numpy 的数组。Points 和 IdList 等类也同样支持这些特性:

>>> pts = tvtk.Points()
>>> p_array = np.eye(3)
>>> p_array
array([[ 1.,  0.,  0.],
 [ 0.,  1.,  0.],
 [ 0.,  0.,  1.]])
>>> pts.from_array(p_array)
>>> pts.print_traits()
_in_set:         0
_vtk_obj:        <vtkCommonPython.vtkPoints vtkobject at 069A0FB0>
actual_memory_size:    1L
bounds:          (0.0, 1.0, 0.0, 1.0, 0.0, 1.0)
class_name:        'vtkPoints'
data:          [(1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)]
data_type:         'double'
...
number_of_points:    3
reference_count:     1
>>> pts.to_array()
array([[ 1.,  0.,  0.],
 [ 0.,  1.,  0.],
 [ 0.,  0.,  1.]])

此外 tvtk 的方法或者属性如果接受 DataArray, Points, IdList 或者 CellArray 的对象的话,那么它也同时支持数组和列表:

>>> points = np.array([[0,0,0],[1,0,0],[0,1,0],[0,0,1]], 'f')
>>> triangles = np.array([[0,1,3],[0,3,2],[1,2,3],[0,2,1]])
>>> values = np.array([1.1, 1.2, 2.1, 2.2])
>>> mesh = tvtk.PolyData(points=points, polys=triangles)
>>> mesh.point_data.scalars = values
>>> mesh.points
[(0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)]
>>> mesh.polys
<tvtk_classes.cell_array.CellArray object at 0x142D4F60>
>>> mesh.polys.to_array()
array([3, 0, 1, 3, 3, 0, 3, 2, 3, 1, 2, 3, 3, 0, 2, 1])
>>> mesh.point_data.scalars
[1.1000000000000001, 1.2, 2.1000000000000001, 2.2000000000000002]

注意 CellArray 类(mesh 的 polys 属性) 的处理有所不同,我们给它传入的是一个二维数组,在内存中保存的却是一维的: array([3, 0, 1, 3, 3, 0, 3, 2, 3, 1, 2, 3, 3, 0, 2, 1])。它的格式是[Cell 的数据个数, Cell 数据..., Cell 的数据个数, Cell 数据...],如下图所示。

CellArray 用来描述多边形(Cell) 和顶点之间的关系,由于每个 Cell 可以由不同数量的定点组成,因此内部采用上面所述的形式保存。

TVTK 是什么

我们通过 from enthought.tvtk.api import tvtk 载入 tvtk,然后像使用一个模块一样使用它。事实上,我们载入的 tvtk 并不是一个模块,而是某个类的一个实例。之所以会如此设计,是因为 VTK 库有近千个类,而 TVTK 对所有这些类都进行了包装,如果一次性载入这么多类,会极大地影响库的载入速度。

我们载入的 tvtk 虽然是某个类的实例,但是用起来就和模块一样:

  • 通过 tvtk 对象可以使用所有的 TVTK 类
  • 它不需要载入近千个 TVTK 类就能支持类名的自动完成
  • 只有在真正使用某个 TVTK 类时,它才会被载入

所有 tvtk 相关的代码全部都保存在 tvtk_classes.zip 文件中。而 tvtk 对象的类在此压缩文件里的 tvtk_helper.py 中定义。对于 TVTK 中的每个类,tvtk 对象都有一个同名的属性和类相对应。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文