VTK (http://www.vtk.org/) 是一套三維的數據可視化工具,它由C++編寫,包涵了近千個類幫助我們處理和顯示數據。它在Python下有標準的綁定,不過其API和C++相同,不能體現出Python作為動態語言的優勢。因此enthought.com開發了一套TVTK庫對標準的VTK庫進行包裝,提供了Python風格的API、支持Trait屬性和numpy的多維數組。本文將以TVTK為標準對VTK的一些功能進行介紹,如果讀者已經對VTK很了解,想知道TVTK和VTK的區別的話,可以直接跳到第二節。
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)。
原始數據被轉換為圖像要經過兩條流水線:
映射器(Mapper)則是可視化流水線的終點,圖形流水線的起點,它將各種派生類能將眾多的數據映射為圖形數據以供圖形流水線加工。
讓我們對照一下前面的的圓錐的例子:ConeSource的對象通過程序內部計算輸出一組描述圓錐的數據(PolyData):然后,PolyData通過PolyDataMapper映射器將數據映射為圖形數據。在這個例子中,可視化流水線由ConSource和PolyDataMapper組成。
圖形數據依次通過Actor、Renderer最終在RenderWindow中顯示出來,這一部分就是圖形流水線。
什么是PolyData
PolyData是一個描述一組三維空間中的點、線、面的數據結構。點、線、面通過以下幾個屬性描述:
- points : 類型為Points,保存三維空間中的點的坐標的數組,這些數據不是用來顯示的。
- verts : 類型為CellArray,它描述需要顯示的頂點,其值為 points 某個坐標點的下標,即通過 verts 屬性描述 points 中的哪些點是最終需要顯示的。
- line : 類型為CellArray,它描述需要顯示的邊線,其值為邊線的兩個端點在 points 中的下標。
- polys : 類型為CellArray,它描述需要顯示的面,其值為構成面的各個點在 points 中的下標。
為了方便我們操作和觀察流水線,交互式地修改各個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創建的窗口還為我們提供了如下幾個元素:
流水線瀏覽器中顯示的各個對象的類型都繼承于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
下面介紹一些照相機的一些常用屬性:
這些屬性雖然可以完全控制照相機的位置和方向,但是實際操作起來并不方便。當將照相機的焦點已經固定好在某個位置上的話,可以通過調用: ..TODO * azimuth : 以焦點為圓心,沿著緯度線旋轉指定角度,即水平旋轉,改變其經度
在以焦點為原點的球體坐標系中對照相機進行操作。這兩個函數保持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場景中顯示的物體通常被稱作prop,有幾種prop類型,其中包括:Prop3D和Actor。3D場景中所有prop的都從Prop3D繼承。
下面是使用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進行包裝,它具有如下的一些優點:
下面是用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對象都內部包裝有一個VTK對象,對tvtk對象的方法的調用將轉給相應的VTK對象的方法執行,如果返回值是VTK對象的話,將被包裝成tvtk對象返回。如果方法的參數是tvtk對象的話,其中的VTK對象將傳遞給VTK的方法。
通過調用tvtk.to_tvtk(p),可以得到p中所包裝的VTK對象
所有的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
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可以由不同數量的定點組成,因此內部采用上面所述的形式保存。
我們通過from enthought.tvtk.api import tvtk載入tvtk,然后像使用一個模塊一樣使用它。事實上,我們載入的tvtk并不是一個模塊,而是某個類的一個實例。之所以會如此設計,是因為VTK庫有近千個類,而TVTK對所有這些類都進行了包裝,如果一次性載入這么多類,會極大地影響庫的載入速度。
我們載入的tvtk雖然是某個類的實例,但是用起來就和模塊一樣:
所有tvtk相關的代碼全部都保存在tvtk_classes.zip文件中。而tvtk對象的類在此壓縮文件里的tvtk_helper.py中定義。對于TVTK中的每個類,tvtk對象都有一個同名的屬性和類相對應。