codecamp

卷1:第24章 VTK

原文链接:http://www.aosabook.org/en/vtk.html

作者:Berk Geveci 与 Will Schroeder

可视化工具箱(Visualization Toolkit, VTK)是一种广泛使用的数据处理与可视化软件系统。它应用于科学计算、医学影像分析、计算几何、渲染、图像处理以及信息学等领域。本章,我们展示一个VTK的简要概览,包括一些使之成为一个成功系统的基本设计模式。

要真正理解一个软件系统,关键之处不仅要理解它能够解决什么问题,而且还要了解它出现时的特定文化环境。在VTK的案例中,系统表面看起来要设计成用于科学数据的三维可视化系统。但VTK出现时的文化语境为奋斗者们添上了一个意义深远的背后故事,这有助于解释软件为什么是这样设计和部署的。

在VTK创生和开始编写之时,它的初始作者(Will Schroeder、Ken Martin、Bill Lorensen)还是GE研发部门的科研人员。我们向一个名为LYMB的先驱性系统投入了大量精力,该系统是一种以C实现的、类似Smalltalk的开发环境。在那个时代,它是一个伟大的系统,我们作为科研人员一再地被阻止在两大障碍上:1)IP问题(此处意指知识产权(Intellectual Property, IP)——译注。)和2)非标准的、有所有权的软件。IP问题之所以是个问题,是因为一旦GE公司的律师介入,那么尝试将软件向公司外部公布软件就几乎不可能了。第二,即使我们在GE公司内部部署软件,许多我们的用户也会受制于学习一个有所有权的、非标准系统,因为为了掌握它而作出的努力不能在他离开公司后转移到新的雇主那里;并且,这种软件没有标准工具集所提供的广泛支持。于是,VTK的原始动机就是开发一个开放标准,或曰“协作平台”,通过它,我们能够很容易地将技术传授给我们的用户。因此,为VTK选择一个开源许可证或许是我们所做出的最重要的设计决策。

最终选择了非互利的、自由的许可证(比如:选BSD(即BSD许可证(Berkeley Software Distribution License,BSD)。——译注。)而不选GPL(即GPL许可证(GNU General Public License)。——译注。))在事后证明是一个值得效仿的决策,因为它最终使基于商业的服务和咨询成为可能,而这正成就了Kitware。在我们做出这个决定的时候,我们最感兴趣的是降低与学术界、研究机构以及商务实体之间合作的壁垒。我们从那时也发现,许多组织都避免使用互利性许可证,由于它们可能造成的严重问题。事实上,我们可能会争论互利性许可证在延缓开源软件的接收上有很大作用,但这另当别论。这里的要点是:与任何软件系统相关的重要设计决策之一就是著作权许可证的选择。重新审视项目的目标,然后再恰当地解决IP问题是很重要的。

24.1 VTK是什么?

VTK最初是以一个科学数据可视化系统出现的。可视化领域之外的许多人都天真地把它当成一种特殊的几何渲染:查看虚拟物体并与之交互。尽管这些确实是可视化的一部分,但是通常的数据可视化还包括把数据转换成感知性输入的整个过程,典型的数据是图像,此外还包括触觉、听觉等其他形式。数据形式不仅由几何拓扑结构组成——比如像网格或者复杂空间分解等抽象形式,还有核心结构的属性,诸如标量(如:温度或压强),矢量(如:速度),张量(如:应力与张力),以及渲染属性,诸如表面法线和纹理坐标等。

注意,通常情况下,表示时空信息的数据被看做是科学可视化的一部分。然而,还有更抽象数据形式,比如市场统计资料、网页、文档以及其它信息,它们只能通过诸如非结构文档、表格、图和树等抽象(即:非时空)关系来表示。这些抽象数据一般通过信息可视化的方法来处理。在社区的帮助下,VTK现在能够完成科学可视化和信息可视化方面的工作。

作为一种可视化系统,VTK的角色是以这些形式获取数据,并最终将它们转换成利于人类感官理解的形式。因此,VTK的核心需求之一就是创建数据流管线的能力,这种管线能够读入、处理、表示并最终渲染数据。这样,工具箱就必须构建成一个灵活的系统,它的设计在许多层面上反映了这一点。例如,我们有目的地将VTK设计成这样一种工具箱,它具有许多可互换的组件,这些组件可以组合起来用于处理多种数据。

24.2 架构特性

在深入介绍VTK特殊的架构特性之前,先介绍顶层的概念,它们系统的开发和使用都产生了深远的影响。其中之一就是VTK的混合包装设施。该设施从VTK的C++实现自动生成Python,Java,和Tcl等的语言绑定(还可绑定更多的语言,并且有些已经实现了——译注)。最具实力的开发者将使用C++进行工作。使用者和应用程序开发者也可以使用C++,但是通常情况下,上文提到的解释性语言更加适合这两个群体。混合的编译性/解释性环境将这两个领域的优势结合在了一起:计算密集型算法的高性能和样机或开发的灵活性。事实上,这种多语言计算的方法在许多科学计算社区中得到广泛应用,并且许多团队将VTK作为他们自己软件的一个范本。

就软件过程而言,VTK采用CMake来控制构建过程;CDash/CTest用于测试;然后CPack用于跨平台部署。VTK确实可以在几乎任何计算机上进行编译,包括因其简陋的开发环境而声名狼藉的超级计算机。此外,开发工具外围还包括网页、wiki、邮件列表(用户区和开发者区),文档生成设施(即:Doxygen)和bug追踪系统(Mantis)。

24.2.1 核心特性

由于VTK是面向对象系统,在其内部,对类的访问和数据成员的实例化都被小心地管理起来。通常情况下,所有的数据成员的访问权限均为protected或private。通过SetGet方法来访问这些数据成员,这两种方法具有各种类型的形参,例如:布尔型数据、模态数据、字符串、以及向量。这些方法中的多数的创建是通过向类的头文件中插入宏来实现的。例如:

vtkSetMacro(Tolerance, double);
vtkGetMacro(Tolerance, double);

可以展开为如下形式:

virtual void SetTolerance(double);
virtual double GetTolerance();

使用这些宏的原因已经超出了仅仅使代码清晰。VTK中有重要的数据成员控制调试、更新对象的修改时间(MTime)、并恰当地管理引用计数。这些宏正确地操作这些数据,因而强烈推荐使用它们。例如,当一个对象的修改时间没有得到恰当的管理时,VTK中就会出现一个尤其严重的bug。在这种情况下,代码就不会按其应该运行的方式运行,或者还会执行多次。

VTK的优势之一就是其相对简单的用于表示和管理数据的方法。典型的情况下,各种特殊数据(例如:vtkFloatArray)的数组用于表示信息的连续片段。例如:一个装载有三个三维坐标点的表可以用具有9个元素的vtkFloatArray来表示。这些数组有一种元组的记法,故有一个三维坐标点即一个3元组,而一个对称的3×3张量矩阵可以由一个6元组表示()。专门采用这种设计是因为在科学计算中,与操作数组的系统(例如:Fortran)接口是很常见的,并且这样还能使对大块连续数据的内存分配与回收变得更加高效。再者,连续数据的通信、串行、以及IO操作通常更有效率。这些(可以加载各种类型数据的)核心数据数组表示了VTK中的大部分数据,且具有多种方便的方法,以进行信息的插入和访问,包括用于快速访问的方法、以及在添加更多数据时所需要的自动分配内存的方法。数据数组是抽象类vtkDataArray的子类,该抽象类的意义在于:通用的虚方法可用于简化编码。但是,为了实现更高的性能,静态的、模版化的函数被引入,这样就可以根据不同的参数类型进行切换,并实现随后对连续数据数组的直接访问。

即使由于性能方面的原因,模板被广泛地使用,C++模板通常在公有类的API中也是不可见的。这点在STL中也是如此:我们采用了PIMPL设计模式来隐藏模版实现的复杂细节。这种模式为我们提供了很大帮助,尤其是在以前文所述将代码包装为解释性代码的时候。避免公有API中模板的复杂性意思是:在应用程序开发者看来,VTK实现大部分是无需考虑数据类型的选择的。当然,在其外壳之下,代码的执行是由数据类型来驱动的,而该数据类型则一般是运行时访问数据时确定的。

一些用户很想知道为什么VTK使用引用计数来管理内存而不是垃圾回收这一对用户来说更为友好的方式。基本的答案是当数据被删除的时候,VTK需要对其完全控制,因为要处理的数据量可能十分巨大。例如,一组1000×1000×1000字节的体数据的数据量是1G字节。把这么大的数据留在内存中等待垃圾回收器来决定是否应该释放它们,确实不是一个好主意。在VTK中,大部分类(vtkObject的子类)具有内建的引用计数能力。每个对象都包含有一个引用计数,它在该对象实例化时被初始化为1。每次使用该对象都会进行注册,然后引用计数就加1。类似地,当使用该对象进行了反注册(或者等效地认为该对象被删除),那么引用计数就会减1。最终的对象引用计数减至0,此时该对象自毁。下面列举一个典型的例子:

vtkCamera *camera = vtkCamera::New();     // reference count is 1
camera->Register(this);                   // reference count is 2
camera->Unregister(this);                 // reference count is 1
renderer->SetActiveCamera(camera);        // reference count is 2
renderer->Delete();                       // ref count is 1 when renderer is deleted
camera->Delete();                         // camera self destructs

这里还有另外一个关于为什么引用计数对于VTK很重要的原因——它提供了有效复制数据的能力。例如:想象有一个数据对象D1,它由许多数据数组组成:点、多边形、颜色、标量、以及纹理坐标等。现在假设处理该数据来生成一个新的数据对象D2,此对象与第一个对象相同,还外加了向量数据(用于定位点)。一种浪费资源的方式是完全复制(深拷贝)D1来创建D2,然后向其中加入新的向量数据数组。另有一种方法,我们创建一个空的D2,然后将D1中的数组传给D2(浅拷贝),使用引用计数来追踪数据所有权,最终向添加新的向量数组。后者方法避免了复制数据,这正如前文所述,对一个优秀可视化系统是必不可少的。我们在本章的稍后内容中可以看到,数据处理的管线例行公事式地实现了这种运行机制,即:将数据从算法的数据复制至输出,此时引用计数对于VTK是必不可少的。

当然,引用计数也有一些臭名昭著的问题。偶尔会存在引用周期,这时循环中的对象以一种相互支持的配置来引用彼此。这种情况下,就需要明智的介入,或者在VTK中,一种在vtkGarbageCollector中实现的特殊设施就可以用来管理牵涉与上述循环中的对象。当这样的类被鉴别到的时候(这被期望发生在开发过程中),该类就会将其自身注册至垃圾回收器,并管理其自己的RegisterUnregister方法的开销。然后紧接着的对象销毁(或者反注册)方法对局部的引用计数网络进行拓扑分析,搜索已经分离了的相互引用的对象群。这些都将被垃圾回收器予以删除。

VTK中的多数实例化过程是通过一种以静态类成员实现的对象工厂运行。典型的语义表达如下:

vtkLight *a = vtkLight::New();

这里要认识到的重要之处是:这里实际被实例化的可能不是vtkLight,可能是vtkLight的子类(例如:vtkOpenGLLight)。采用对象工厂的动机多种多样,最为重要的是应用的可移植性和设备不相关性。例如,前文中我们在一个渲染场景中创建了一个光源。在一个运行于特定平台上的特定的应用程序中,vtkLight::New可能会生成一个OpenGL光源,然而在不同的平台上,存在着图形系统中其他渲染库或方法来创建光源的可能性。到底实例化什么样的派生类是一种运行时系统信息的功能。在早期的VTK中,可以有包括gl、PHIGS、Starbase、XGL、以及OpenGL等多种选择。然而这些图形库中的多数现在已经消失了,出现了包括DirectX和基于GPU方法在内的新方法。随着时间的推移,一个利用VTK写成的应用程序没必要进行修改,因为开发者已经派生出了特定的对应于新设备的vtkLight的子类和其他渲染类来支持不断发展的技术。另外一个对象工厂的重要用处是使性能增强变动的运行时替换成为可能。例如,一个vtkImageFFT可能取代一个访问特种用途硬件或数值计算库的类。

24.2.2 数据表示

VTK的一个优点就是其表示数据复杂形式的能力。这些数据形式包括从简单表格到有限元网格之类的复杂结构。所有这些数据形式都是vtkDataObject的子类,如图24.1所示(注意这是数据对象类的继承图的一部分)。

图24.2:数据集类

24.2.3 管线架构

VTK由若干主干子系统组成。与可视化包关联最紧密的子系统或许应该是数据流/管线架构了。从概念上讲,管线架构由三类基本对象组成:表示数据的对象(上文中的vtkDataObject),将数据从一种形式处理、变换、滤波或者映射成另外一种形式的对象(vtkAlgorithm),以及执行管线的对象(vtkExecutive)——此管线控制着一个由交错数据(?)和过程对象(即:管线)组成的连通图。图24.3展示了一个典型的管线。

尽管这是交互的一种原始形式,它也是许多使用VTK的应用程序的基本要素。例如:上述的简短代码可以很容易地转换、用于显示并管理图形界面中的进度条。这一命令/观察者子系统也是VTK中三维挂件的核心,这些挂件是用于数据的请求、操纵以及编辑的复杂的交互性对象,下文将予以描述。

提到上面的例子,很重要的一点是,VTK中的事件都是预定义的,但是这里也为自定义事件开了后门。vtkCommand类定义了一组枚举型事件(例如:上面例子中的vtkCommand::ProgressEvent)以及一个用户事件。UserEvent只是一个整形数值,一般用作一组应用程序中自定义事件的起始抵消值。于是,vtkCommand::UserEvent+100可能是指一个VTK预定义的事件之外的某个事件。

从用户的角度来看,一个VTK挂件可以看作是场景中的一个演员,只是用户可以通过操纵句柄或者其它几何特性(句柄操纵与几何特性操纵均是基于前文所述之抓取功能——原文:picking functionality,即24.2.4一节中最后一段所述——的)来与之交互。与挂件的交互是很直观的:用户抓住球面句柄并将其移动,或者抓住一条直线并将其移动。然而,在场景的背后,事件被发送出去(例如:InteractionEvent),而一个编写合理的应用程序就能够观察到这些事件,并采取恰当的行动。例如,它们通常由下面所给出的vtkCommand::InteractorEvent所触发:

vtkLW2Callback *myCallback = vtkLW2Callback::New();
  myCallback->PolyData = seeds;    // streamlines seed points, updated on interaction
  myCallback->Actor = streamline;  // streamline actor, made visible on interaction

vtkLineWidget2 *lineWidget = vtkLineWidget::New();
  lineWidget->SetInteractor(iren);
  lineWidget->SetRepresentation(rep);
  lineWidget->AddObserver(vtkCommand::InteractionEvent, myCallback);

实际上,VTK挂件由两个对象构建而成:一个是vtkInteractorObserver的子类,另一个是vtkProp的子类。vtkInteractorObserver只是观察渲染窗中的用户交互(例如:鼠标事件和键盘事件)并处理之。这些操纵通常由突出显示句柄,改变鼠标指针的外观,以及变换数据等所组成,它们都会修改vtkProp的几何特征。当然,这些挂件的特殊细节要求编写子类来控制其行为的细微差别,目前系统中拥有50多个不同的挂件。

24.2.4 库的总结

VTK是一个大型软件工具箱。目前,系统由大约1500万行代码(包括注释,但是不包括自动生成的包裹层软件),约1000个C++类组成。为了管理系统的复杂度并减少构建和链接的时间,系统被分割放置在十几个子路径中。表24.1列出了这些子路径,并简要总结了这些库所提供的功能。

| Common | VTK核心类 |
| Filtering | 用于管理管线数据流的类 |
| Rendering | 渲染,抓取,查看图像,以及交互 |
| VolumeRendering | 体绘制技术 |
| Graphics | 三维几何处理 |
| GenericFiltering | 非线性三维几何处理 |
| Imaging | 图像处理管线 |
| Hybrid | 同时要求使用图形学和图像处理功能的类 |
| Widgets | 复杂的交互 |
| IO | VTK的输入和输出 |
| Infovis | 信息可视化 |
| Parallel | 并行处理(控制器和通信器) |
| Wrapping | 对Tcl,Python以及Java的包裹的支持 |
| Examples | 内容广泛、文档良好的示例 |

表24.1:VTK的子路径

24.3 回顾与展望

VTK一直是一个非常成功的系统。虽然第一行代码于1993年写出,但是目前,VTK仍然在不断成长壮大、其开发速度也在不断加快2。本节,我们将谈谈一些经验和将来的挑战。

24.3.1 成长管理

VTK发展历程中,最令人惊叹的方面之一就是项目的寿命。开发的速度归因于若干主要原因:

  • 新算法和功能被持续不断地加入。例如,信息学子系统(Titan,最初由Sandia国立实验室和Kitware软件共同开发)是最近加入的一个重要的部分。额外的绘图和渲染类也同时加入进来,还有新的科学数据类型功能。另外一个加入的重要部分是三维交互挂件。最后,基于GPU的渲染以及数据处理的持续演进正在催生新的VTK功能。
  • VTK不断增多的曝光和使用是一个自我保持的过程,该过程向社区加入了更多的使用者和开发者。例如,ParaView是最受欢迎的基于VTK的科学可视化应用程序,并且受到了高性能计算社区的高度重视。3D Slicer是主要的生物医学计算平台,它大部分也建立于VTK之上,并且每年受到数百万美元的资助。
  • VTK的开发过程持续演进。近年来,CMake、CDash、CTest、以及CPack等软件过程工具已经集成到了VTK的构建环境中。最近,VTK的代码库已经迁移至Git和一个更为复杂的工作流。这些改进确保VTK保持科学计算社区内软件开发的领先地位。

虽然成长是令人兴奋的,确证软件系统的建立,预测VTK的未来,但妥善的管理却是极其困难的。因此,近期VTK将更多地专注于管理社区以及软件的成长。为此,已经采取了若干措施。

首先,创立了正式的管理架构。创建了架构审查委员会(Architectural Review Board),来指导社区和技术的发展,专注于高层次的、战略性的议题。VTK社区也正在组建一个由意见领袖组成的公认的团队,来指导某些VTK子系统的技术开发。

其次,制定了关于更进一步使工具箱模块化的计划,尤其是应对由git引入的工作流功能,还认识到使用者和开发者一般都想在工作中使用工具箱中小的子系统,并且不想构建并链接整个包。此外,为了支持不断成长的社区,对新的功能和子系统的支持是很重要的,即使它们并不一定是工具箱的核心部分。通过创建松散的、模块化的一群模块,在维持核心的稳定性的同时,适应外围的大量代码贡献是可能的。

24.3.2 技术整合

除了软件过程之外,在开发管线当中还有许多技术创新。

  • 共同处理是这样一种功能,可视化引擎被集成于仿真代码之中,而且周期性地提取生成用于可视化的数据。这一技术极大地降低了完整解决方案数据的大的输出数据量。
  • VTK中的数据处理管线还是太复杂。正在寻求简化和重构这些子系统的方法。
  • 直接与数据交互的能力正在使用者中间流行。尽管VTK拥有一大票挂件,但是更多的交互技术正在不断涌现,包括基于触摸屏的方法和三维方法。交互技术将会继续快速开发。
  • 计算化学对于材料设计人员和工程师的重要性正在不断提升。对化学数据的可视化与交互的功能正在加入VTK。
  • VTK的渲染系统素来因其过于复杂而饱受诟病,这使它难以派生出新的类或者支持新的渲染技术。此外,VTK不直接支持场景图概念,这同样也是许多使用者要求过的功能。
  • 最后是数据的新形式不断出现。例如,在医疗领域,变分辨率的层次化体数据(如:具有局部放大的共焦显微镜影像)。

24.3.3 开放科学

最后,Kitware和更加广泛的VTK社区决定加入Open Science。从务实的角度讲,它一个这样的方式,我们将传播公开的数据、公开的发表、以及公开的源代码——这是确保我们正在创建可重现的科学系统所必需的特征。虽然VTK一直以来都以开源和公开数据的系统的形式传播,但是文档过程却一直缺乏。在拥有正式书籍[Kit10,SML06]的同时,还一直有各种非正式的方法来收集包括新的源码在内的技术发表物。我们正在通过开发像是VTK Journal3的新的发表机制来改善这种状况,该期刊可以发表由文档、源代码、数据、以及有效的测试图像组成的文章。它还实现了自动化的代码审查(利用VTK的高质量的软件测试过程)以及人对递交文章的审查。

24.3.4 经验教训

虽然VTK很成功,但是还有许多事情我们没有处理好:

  • 设计的模块性。我们在选择我们的类的模块性上做得不错。例如,我们不会做类似为每个像素都创建一个对象的这种傻事,而是创建了高层次的vtkImageClass,它内部处理像素数据组成的数组。然而,在某些情况下,我们不得不将之重构为小的片段,并继续这一过程。一个基本的例子就是数据处理管线。最初,数据管线是通过数据和算法对象的交互而隐式实现的。我们最终认识到我们得创建一种显式的管线执行对象来协调数据与算法之间的交互,并且用于实现不同的数据处理策略。
  • 遗漏的关键概念。我们曾经的最大遗憾就是没有广泛的利用C++的迭代器。在许多情况下,VTK中的数据的遍历与科学编程语言Fortran十分类似。迭代器所提供的额外的灵活性本来可能对系统有很大帮助。例如,在处理局部区域的数据,或者仅仅是那些满足某种迭代准则的数据时,这是极具优势的。
  • 设计上的问题。当然,有一长列非最优的设计决策。我们同数据处理管线斗争,已经经历了许多代,每次都设计得更好些。渲染系统也是很复杂的,并且难以从其中派生出新类。另外一个由VTK的最初概念所引起的挑战是:我们将其看作是用于观察数据的只读可视化系统。然而,目前的客户经常希望它能够编辑数据,这就需要完全不同的数据结构。

像VTK这样的开源系统的好处之一是许多这些错误能够并且将会随着时间而得以纠正。我们拥有一个积极的、有能力的开发社区,他们每天都在改进着这个系统,并且我们希望在可预见的将来,这一状态能够维持下去。


脚注
1. http://en.wikipedia.org/wiki/Opaque_pointer.
2. See the latest VTK code analysis at http://www.ohloh.net/p/vtk/analyses/latest.
3. http://www.midasjournal.org/?journal=35.

卷1:第22章 Violet
卷1:第25章 韦诺之战
温馨提示
下载编程狮App,免费阅读超1000+编程语言教程
取消
确定
目录

关闭

MIP.setData({ 'pageTheme' : getCookie('pageTheme') || {'day':true, 'night':false}, 'pageFontSize' : getCookie('pageFontSize') || 20 }); MIP.watch('pageTheme', function(newValue){ setCookie('pageTheme', JSON.stringify(newValue)) }); MIP.watch('pageFontSize', function(newValue){ setCookie('pageFontSize', newValue) }); function setCookie(name, value){ var days = 1; var exp = new Date(); exp.setTime(exp.getTime() + days*24*60*60*1000); document.cookie = name + '=' + value + ';expires=' + exp.toUTCString(); } function getCookie(name){ var reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)'); return document.cookie.match(reg) ? JSON.parse(document.cookie.match(reg)[2]) : null; }