可视化技术可以分为两大类:科学可视化和信息可视化。这两者的主要区别在于它们所表示的数据类型。科学可视化是指显示具有固有时空属性的数据,如模拟、模型或图像。对于这些类型的数据,元素的相对位置是已知的,因为每个元素在三维空间中具有固有的结构和位置。
另一方面,信息可视化是所有其他形式的数据的可视化。元数据、关系数据库和一般表格数据属于这一类。在这些情况下,单个元素的位置不是固定的,必须执行嵌入来将相似或相关的元素放置得更近。想象一群人。组织人员的一种简单方法是将他们放在列表或表格中。如果附带的信息也可用,其他格式可能对查看者有帮助。人们可以在二维视图中呈现,根据年龄、性别、职业等相似性进行分组。如果“友谊”链接是已知的(例如来自社交网站),他们可以被分组到由这些链接形成的自然友谊组中。如果每个人的当前位置是已知的,他们可以被安排在地图上(“地理空间可视化”在207页的VTK在这方面的能力)。
适合信息可视化的其他数据示例包括数据库、电子表格、XML和任何类型的元数据,如患者的人口统计信息或模拟运行的参数。对于这种类型的可视化,使用诸如聚类和图形布局之类的算法来揭示突出的关系或确定复杂异构数据的高级图像。
信息可视化的基本数据结构是表、图和树。表是一个简单的二维数据数组,很像电子表格。在VTK中,表由作为表列的命名数组向量组成。每个列的作用很像其他数据对象(如vtkImageData)中的属性。表用于存储数据库查询的结果和分隔文本文件的内容。
图数据结构由称为顶点的实体集合以及称为边的顶点对之间的链接组成。图可以用来存储多种数据。例如,在社交网络中,每个顶点可能代表一个人,而每个边可能表示两个人之间的友谊。另一个例子是生物通路网络,其中顶点可能代表化合物,边缘可能表示化学反应可以从另一种化合物产生一种化合物。
树是一种不包含任何循环(形成完整循环的连接边序列)的图。VTK的树数据结构是一个有根的树,它可以被认为是一个层次结构,根顶点在顶部。树可以通过将实体分组到多个级别来组织大量数据。
表、图和树可以用几种方式表示复杂的数据。下面的部分提供了一些关于如何使用这些数据结构执行一些基本操作和可视化的细节。
8.1探索表格数据中的关系
表是一种非常常见的数据形式。纯文本分隔的文件、电子表格和关系数据库都使用表作为基本数据结构。表中的一行通常表示单个实体(人、事件、样本等),列表示这些实体上的属性。让我们以2008年夏季奥运会的奖牌榜为例。该表将显示奖牌获得者的姓名和国家,以及赛事信息。在表格形式中,很难回答诸如:谁获得了最多的奖牌?哪个国家在某一学科上表现突出?为了做到这一点,我们希望以图或树的形式从表中提取关系信息。
将表转换为图
下面的python示例演示了如何获取这些信息,并使用vtkTableToGraph将其转换为vtkGraph数据结构并在图形视图中显示它。注意,脚本试图找到VTK数据路径,这可以在本例和其他示例中通过在运行脚本时向命令行添加参数“-D <path>”来显式定义。下面的示例是将简单表转换为图的Python代码。
from vtk import *
import vtk.util.misc
import versionUtil
datapath = vtk.util.misc.vtkGetDataRoot()
reader = vtkDelimitedTextReader()
reader.SetFileName(datapath + '/Data/Infovis/medals.txt')
reader.SetFieldDelimiterCharacters('\t')
reader.SetHaveHeaders(True)
ttg = vtkTableToGraph()
ttg.SetInputConnection(reader.GetOutputPort())
ttg.AddLinkVertex('Name', 'Name', False)
ttg.AddLinkVertex('Country', 'Country', False)
ttg.AddLinkVertex('Discipline', 'Discipline', False)
ttg.AddLinkEdge('Name', 'Country')
ttg.AddLinkEdge('Name', 'Discipline')
category = vtkStringToCategory()
category.SetInputConnection(ttg.GetOutputPort())
category.SetInputArrayToProcess(0, 0, 0, 4, 'domain')
view = vtkGraphLayoutView()
view.AddRepresentationFromInputConnection(
category.GetOutputPort())
view.SetLayoutStrategyToSimple2D()
view.SetVertexLabelArrayName('label')
view.VertexLabelVisibilityOn()
view.SetVertexColorArrayName('category')
view.ColorVerticesOn()
view.SetVertexLabelFontSize(18)
theme = vtkViewTheme.CreateMellowTheme()
view.ApplyViewTheme(theme)
rw = versionUtil.SetupView(view)
versionUtil.ShowView(view)]
首先,我们创建一个vtkDelimitedTextReader来读取奖牌文本文件。该阅读器生成一个vtkTable作为输出,它只是一个命名数据数组列表。该文件中的字段由制表符分隔,因此我们适当地设置了字段分隔符。我们指定SetHaveHeaders(True),以便从文件的第一行开始分配列名。否则,列将具有通用名称“字段0”、“字段1”等,并且假定表中的数据从第一行开始。
您将注意到对versionUtil的调用。设置视图和版本util。用于处理本示例和其他示例末尾的代码。这反映了VTK 5.4后的代码更改,使vtkRenderView包含vtkRenderWindow。这消除了手动创建渲染窗口和调用SetupRenderWindow()的需要。此外,这些方法已经被添加到vtkRenderView中,而不是在渲染窗口上调用ResetCamera()和Render()等方法,并且应该在视图上调用。下面的清单显示了versionUtil.py的内容。下面的代码示例演示了在5.4版本和更高版本中设置和显示vtk视图。
from vtk import *
def VersionGreaterThan(major, minor):
v = vtkVersion()
if v.GetVTKMajorVersion() > major:
return True
if v.GetVTKMajorVersion() == major and v.GetVTKMinorVersion() > minor:
return True
return False
def SetupView(view):
if not VersionGreaterThan(5, 4):
win = vtkRenderWindow()
view.SetupRenderWindow(win)
return win
else:
return view.GetRenderWindow()
def ShowView(view):
if VersionGreaterThan(5, 4):
view.ResetCamera()
view.Render()
view.GetInteractor().Start()
else:
view.GetRenderer().ResetCamera()
view.GetRenderWindow().Render()
view.GetRenderWindow().GetInteractor().Start()
现在我们希望可视化这个表中的一些关系。该表包含以下几列:“Name”包含运动员的姓名,“Country”包含运动员的国家,“Discipline”包含运动员所属的学科。该表有以下几列:“Name”包含运动员的姓名,“Country”包含运动员的国家,“Discipline”包含运动员参加的项目的学科(如游泳、射箭等)。查看图中关系的一种方法是使用vtkTableToGraph从表中生成vtkGraph。vtkTableToGraph将vtkTable作为输入(以及稍后描述的第二个“顶点表”输入),并产生一个vtkGraph,其结构,顶点属性和边缘属性都取自表。
vtkTableToGraph算法需要知道应该使用哪些列来生成顶点,哪些列对应该成为边,将顶点连接在一起。因此,在我们的示例中,我们可能希望创建一个图形,其顶点表示名称、国家和学科。要做到这一点,我们调用AddLinkVertex()与列的名称来制作顶点。该列中所有不同的值都将成为图中的顶点。因此,AddLinkVertex('Country',…)将在输出图中为表“Country”列中的每个不同的国家名称创建一个顶点。AddLinkVertex()的第二个参数提供与这些顶点相关联的域。域对于实体解析非常重要。例如,如果“国家”和“名称”被赋予相同的域名,那么一个名为“乍得”的国家将与一个名为“乍得”的人合并。给每个列一个不同的域可以避免这些冲突。但是,在某些情况下,表的两列应该被赋予相同的域。例如,在通信表中,可能有名为“From”和“To”的列。这些应该被分配到相同的域(也许命名为“Person”),这样一个人的顶点将被合并到同一个顶点中,而不管这个人是发送者还是接收者。AddLinkVertex()的第三个参数在第168页的“隐藏顶点”中描述。
既然已经定义了顶点,我们就可以用AddLinkEdge()来定义图中的边集了。AddLinkEdge()接受两个表列名,每次A和B一起出现在这两列的同一行时,它都会在两个顶点A和B之间产生一条边。因此调用AddLinkEdge('Name', 'Country')将在每个运动员和他或她的国家之间的图表中添加一条边。类似地,AddLinkEdge('Name', 'Discipline')将在运动员和他或她参加的每个项目之间增加一个优势。奖牌表转换成图形的结果如图8-1所示。图中顶点的颜色表示它们所属的域。由于您可能只使用数字数组分配颜色,因此程序使用vtkStringToCategory将域数组(包含字符串)转换为每个不同顶点的数字标识符。
属性。vtkTableToGraph输出中的边具有表中定义的完整属性集。也就是说,每条边都与产生这条边的表行中的所有条目相关联。
然而,默认情况下,顶点只有三个属性与它们相关联。一个名为“domain”的属性将是一个包含每个顶点域名的字符串数组。" ids "和" label "属性都包含相同的数量,但以不同的格式表示。两者包含的数量是定义顶点的特定单元格的内容。“ids”属性将其存储在原始类型中,该类型可能因属于不同域的顶点而异。因此,“ids”数组存储在vtkVariantArray中,其中每个条目的数据类型是可变的。“label”属性是一个vtkStringArray,包含这些值的字符串等价。
vtkTableToGraph接受一个可选的第二个vtkTable输入,这样顶点可以有额外的属性分配给他们。过滤器的第一个输入定义了边的连通性(因此被称为“边表”),第二个输入定义了图顶点的附加属性(因此被称为“顶点表”)。
因此,要添加顶点属性,必须创建一个顶点表,其中包含标识符与边缘表列的标识符匹配的列。顶点表可以有任意数量的附加列,其中包含其他顶点属性。在我们的示例中,可能存在驻留在单独文件或关系数据库中的其他表,这些表定义运动员属性(例如年龄、性别)、国家属性(例如人口、土地面积)和学科属性(例如赛事数量)。要将这些列作为顶点属性添加到图中,需要构造一个包含所有这些列的表。例如,这可以通过正确形成的SQL查询或应用vtkMergeTables算法来完成。当这个扩展表被设置为vtkTableToGraph的第二个输入时,AddLinkVertex()的第二个参数中定义的域具有特殊的含义。域名必须与包含匹配标识符的顶点表列名匹配。因此,在我们的示例代码中,顶点表需要有名为“Name”、“Country”和“Discipline”的数组,其中包含与边缘表中的值匹配的值。
隐藏的顶点。AddLinkVertex()的第三个参数表示顶点是否应该隐藏。大多数情况下,显示顶点是值得的,因此默认值为False。但是,将其设置为True可以创建高级链接效果,例如将运动员与代表同一国家的其他运动员连接起来,但不显式显示国家。为此,您将添加“Name”作为非隐藏顶点类型,并添加“Country”作为隐藏顶点类型。然后使用AddLinkEdge()添加对(“Name”,“Country”)和(“Country”,“Name”)。在内部,vtkTableToGraph创建带有名称和国家顶点的图,然后删除国家顶点,为通过国家顶点的每条两条边路径创建一条新边。请注意,隐藏顶点的成本可能会非常高,因为生成的边的数量可能会快速增长,并且可能产生大的团块(即每个顶点对都由一条边连接的顶点集合)。出于这个原因,将来可能不推荐使用该选项。
将表转换为树
图表示任意复杂的结构,而树具有更受限制的结构,因此结构更简单。在VTK中,vtkTree包含一个根树,其中有一个层次结构的顶点,顶部有一个根顶点。向外的边沿着树流动,将“父”顶点连接到“子”顶点。vtkTrees的一个例子是检查任意XML文件的结构。只要使用vtkXMLTreeReader,就可以直接从任何XML文件生成树。该读取器解析嵌套的XML元素并创建vtkTree。还可以从表格数据创建分类树。下面的python代码演示了如何使用vtktabletotreeffilter和vtkGroupLeafVertices算法来做到这一点。
VERTICES = 4 # Constant for SetInputArrayToProcess
datapath = vtk.util.misc.vtkGetDataRoot()
reader = vtkDelimitedTextReader()
reader.SetFileName(datapath + '/Data/Infovis/medals.txt')
reader.SetFieldDelimiterCharacters('\t')
reader.SetHaveHeaders(True)
if versionUtil.VersionGreaterThan(5,4):
reader.OutputPedigreeIdsOn()
ttt = vtkTableToTreeFilter()
ttt.SetInputConnection(reader.GetOutputPort())
group_disc = vtkGroupLeafVertices()
group_disc.SetInputConnection(ttt.GetOutputPort())
group_disc.SetInputArrayToProcess(0, 0, 0, VERTICES, 'Discipline')
group_disc.SetInputArrayToProcess(1, 0, 0, VERTICES, 'Name')
group_country = vtkGroupLeafVertices()
group_country.SetInputConnection(group_disc.GetOutputPort())
group_country.SetInputArrayToProcess(0, 0, 0, VERTICES, 'Country')
group_country.SetInputArrayToProcess(1, 0, 0, VERTICES, 'Name')
category = vtkStringToCategory()
category.SetInputArrayToProcess(0, 0, 0, VERTICES, 'Name')
category.SetInputConnection(group_country.GetOutputPort())
view = vtkTreeRingView()
view.AddRepresentationFromInputConnection(
category.GetOutputPort())
view.RootAtCenterOn()
view.SetInteriorRadius(1)
view.SetAreaHoverArrayName('Name')
view.SetAreaLabelArrayName('Name')
view.AreaLabelVisibilityOn()
view.SetAreaColorArrayName('category')
if versionUtil.VersionGreaterThan(5,4):
view.ColorAreasOn()
else:
view.ColorVerticesOn()
view.SetAreaLabelFontSize(18)
该代码首先读取与前几节中描述的奥运奖牌数据相同的以制表符分隔的文件。然后,vtkTableToTreeFilter执行将表转换为树的第一个基本步骤。它生成一个新顶点作为树的根,然后为输入表中的每一行创建一个子顶点,将表中的属性与树中的子顶点相关联。这棵树并不有趣,因为它只包含一个没有任何有意义结构的关卡。在我们的例子中,vtkTableToTreeFilter后面跟着两个vtkGroupLeafVertices实例。这些过滤器中的每一个都为树添加一个新的层次,根据特定数组中的匹配值对现有的叶顶点(即没有子顶点)进行分组。vtkGroupLeafVertices的第一个实例将主输入数组设置为“纪律”属性。这将在根下添加一个新级别,该级别为纪律数组中的每个唯一值包含一个顶点。原始叶顶点根据每个学科顶点的属性值收集。
类似地,第二个vtkGroupLeafVertices添加了一个表示学科以下国家的级别。数据已经按项目进行了划分,所以每个国家可能会出现几次,在该国获得奖牌的每个项目下出现一次。这是将表转换为图或树的一个主要区别。图是一种更复杂的结构,它允许代表同一实体的顶点只出现一次。另一方面,树是更简单的结构,通常更容易理解,但由于它们的连接性限制,通常需要重复数据。将表转换为树并使用vtkTreeRingView可视化的结果如图8-2所示。树轮视图和图形布局视图在第170页的“图形可视化技术”中有进一步的描述。简单地反转两个vtkGroupLeafVertices算法将产生一个首先按国家,然后按学科组织的树。
这些过滤器可用于处理vtkTables或其他数据对象中的属性数据:
•vtkBoostSplitTableField。cxx—通过分隔符将字符串属性拆分为多个部分,并为每个部分创建重复行,仅通过拆分属性区分。
•vtkDataObjectToTable。cxx -转换vtkDataSet子类的点或单元格属性,或vtkGraph子类的顶点或边缘数据到vtkTable。
•vtkGenerateIndexArray。生成一个简单的从零开始的索引数组,并将其附加到一个数据对象。
•vtkMergeColumns。cxx -将多个列合并为单个列。
•vtkMergeTables。cxx -将多个表合并为一个包含所有列的联合表。
•vtkStringToCategory。cxx -通过为数组中的每个唯一字符串赋值一个整数,将字符串数组转换为整数数组。
•vtkStringToNumeric。cxx -自动检测和转换包含可转换为整数或浮点值的值的字符串数组。
•vtkThresholdTable。cxx -从vtkTable中过滤出属性值超出给定范围的行。其他图形变换算法包括:cxx -将具有匹配属性值的顶点组合成单个顶点。
•vtkPruneTreeFilter。cxx -从vtkTree中删除子树。
•vtkRemoveIsolatedVertices。从图中删除没有边连接的顶点。
8.2图形可视化技术
作为一种划分实体之间关系、实体聚类和更高层次抽象的方法,图形可视化正变得越来越重要。图通常在二维或三维中可视化,并试图通过顶点和边缘着色、标记、注释、聚类和图标注入额外的维度信息。由于显示图形的多样性,包括理论、启发式、经验和手动布局方法,图形可视化已经成为艺术和科学的混合体,需要既能处理灵活性和复杂性,又能保持相当数量的通用性的方法。这些方法通常分为顶点布局方法、边缘布局方法和显示顶点和多边形数据结构(如圆、环、块等)关系的方法。除了布局方法,显示图形属性的辅助功能也是需要的(例如,常见的着色主题、标签机制、注释和选择等)。为了适应这种灵活性,同时保持高水平的功能,VTK中的图形可视化工具利用“策略”方法来布局顶点和边缘,并使用“视图”对象来集中与图形可视化相关的通用辅助功能。
为了演示基本功能,我们提供一个简单的示例,使用布局和视图在VTK中创建图形显示,参见图8-3。本节的其余部分将讨论在执行图形可视化时提供的额外灵活性。下面的代码片段创建一个随机图形的简单图形视图。
from vtk import *
# create a random graph
source = vtkRandomGraphSource()
source.SetNumberOfVertices(100)
source.SetNumberOfEdges(110)
source.StartWithTreeOn()
#create a view to display the graph
view = vtkGraphLayoutView()
view.SetLayoutStrategyToSimple2D()
view.ColorVerticesOn()
view.SetVertexColorArrayName("vertex id")
view.SetRepresentationFromInputConnection(source.GetOutputPort())
# Apply a theme to the views
theme = vtkViewTheme.CreateMellowTheme()
view.ApplyViewTheme(theme)
theme.FastDelete()
#view.GetRenderWindow().SetSize(500,500)
view.ResetCamera()
view.Render()
view.GetInteractor().Start()
在信息可视化中必须进行的一个主要步骤是将数据嵌入到一些可绘制的空间中。
回想一下,vtkGraph中的顶点与更传统的vtkDataSet类中的vtkPoints没有关系。
点有X, Y和Z坐标,但顶点没有。
根据这些分配的方式,最终的图像将会有很大的不同,并且底层数据的不同特征将更容易辨别。
信息可视化的第二个自由度是连接顶点的边的精确路由。
顶点格式
要确定顶点的位置,可以在管道中放置一个vtkGraphLayout类。这个类负责为顶点分配坐标的总体任务,但它将坐标分配的细节留给了一个可切换的助手类。helper类是策略的一个示例。这种两部分结构最大限度地提高了灵活性,最大限度地降低了复杂性。因此,在一个典型的图形可视化中,一个图形被管道输送到vtkGraphLayout类中,之后每个顶点都被分配了一个vtkPoint。管道作者可以从许多策略中进行选择。策略可能从基于弹簧的布局到聚类方法各不相同。每个策略类都是vtkGraphLayoutStrategy的子类,所有这些都可以插入到vtkGraphLayout中。
•vtkAssignCoordinatesLayoutStrategy—允许从指定为x, y和z坐标的数组中分配坐标的策略。
•vtkCircularLayoutStrategy -分配点到圆周围的顶点与单位半径。
•vtkClustering2DLayoutStrategy -利用基于密度网格的力定向布局策略的策略。布局运行时间为O(V+E),具有极高的常数。
•vtkCommunity2DLayoutStrategy -类似于vtkClustering2DLayoutStrategy,但在其输入上寻找社区数组,并在社区内加强边缘,并在社区内削弱边缘。
•vtkConeLayoutStrategy -基于锥树方法在3D空间中定位树(森林)节点的策略,该方法由Robertson, Mackinlay和Card在ACM计算系统中的人为因素会议(CHI'91)的会议记录中首次描述。这个实现还结合了Carriere和Kazman以及Auber开发的布局的改进。
•vtkConstrained2DLayoutStrategy -类似于vtkClustering2DLayoutStrategy,但寻找一个约束数组,指示节点在布局优化期间对力计算的阻抗水平。
•vtkCosmicTreeLayoutStrategy -树布局策略让人想起天文系统
•vtkFast2DLayoutStrategy -一个简单的快速2D图形布局。
•vtkForceDirectedLayoutStrategy -使用力定向算法在2D或3D中布局图形。
•vtkPassThroughLayoutStrategy -一种布局策略,绝对简单地通过图形而不修改数据。
•vtkRandomLayoutStrategy -在有限的范围内随机放置2或3维的顶点。
•vtkSimple2DLayoutStrategy - Fruchterman & Reingold布局策略的实现(参见“通过力定向放置绘制图形”软件实践与经验21(11)1991))。
•vtkTreeLayoutStrategy -在标准或径向布局中为树的节点分配点。
•vtkTreeOrbitLayoutStrategy -分配点到树的节点到轨道布局。每个父节点都被它的子节点递归地围绕。
为了简化可视化不同图形的任务,顶点布局策略可以直接提供给视图类。这是通过调用vtkGraphLayoutView::SetLayoutStrategy(vtkGraphLayoutStrategy *s)来完成的,就像前面的例子一样。视图在第176页的“视图和表示”中有更深入的讨论。
边缘布局
为了确定如何路由边,我们再次使用策略抽象。每个边缘布局策略都是vtkEdgeLayoutStrategy的子类,然后插入到vtkEdgeLayout类中。当前可用的边布局策略的例子包括:
•vtkArcParallelEdgeStrategy -路由单个边作为线段,路由平行边作为弧。
•vtkGeoEdgeStrategy -布局图形边缘在一个地球仪作为弧。
•vtkPassThroughEdgeStrategy -一个布局策略,简单地通过图形而不修改数据。
边缘布局策略也可以使用vtkGraphLayoutView::SetEdgeLayoutStrategy(vtkEdgeLayoutStrategy *s)
直接提供给视图类。
一旦布局完成,图形仍然必须转换为几何图形:点、线、折线、多边形等,这些图形可以映射为角色,以便在渲染窗口中显示。
•vtkGraphToGlyphs—将vtkGraph转换为包含每个顶点的字形(圆形,菱形,十字等)的vtkPolyData。
•vtkGraphMapper -映射vtkGraph和派生类的图形原语。
•vtkGraphToPolyData -将图的边转换为polydata,假设顶点数据已经有关联的点数据并传递此数据。
•vtkEdgeCenters -在图边的中心生成点。
下面是绘制图形的代码示例,演示了上述类的使用。我们使用单一策略进行初始图形布局;然而,如前所述,策略类是可互换的,在本例中,可以通过将一种策略替换为另一种策略来应用新的布局策略。
from vtk import *
# create a random graph
source = vtkRandomGraphSource()
source.SetNumberOfVertices(100)
source.SetNumberOfEdges(110)
source.StartWithTreeOn()
source.Update()
# setup a strategy for laying out the graph
# NOTE: You can set additional options for each strategy, as desired
strategy = vtkFast2DLayoutStrategy()
#strategy = vtkSimple2DLayoutStrategy()
#strategy = vtkCosmicTreeLayoutStrategy()
#strategy = vtkForceDirectedLayoutStrategy()
#strategy = vtkTreeLayoutStrategy()
# set the strategy on the layout
layout = vtkGraphLayout()
layout.SetLayoutStrategy(strategy)
layout.SetInputConnection(source.GetOutputPort())
# create the renderer to help in sizing glyphs for the vertices
ren = vtkRenderer()
# Pipeline for displaying vertices - glyph -> mapper -> actor -> display
# mark each vertex with a circle glyph
vertex_glyphs = vtkGraphToGlyphs()
vertex_glyphs.SetInputConnection(layout.GetOutputPort())
vertex_glyphs.SetGlyphType(7)
vertex_glyphs.FilledOn()
vertex_glyphs.SetRenderer(ren)
# create a mapper for vertex display
vertex_mapper = vtkPolyDataMapper()
vertex_mapper.SetInputConnection(vertex_glyphs.GetOutputPort())
vertex_mapper.SetScalarRange(0,100)
vertex_mapper.SetScalarModeToUsePointFieldData()
vertex_mapper.SelectColorArray("vertex id")
# create the actor for displaying vertices
vertex_actor = vtkActor()
vertex_actor.SetMapper(vertex_mapper)
# Pipeline for displaying edges of the graph - layout -> lines -> mapper
-> actor -> display
# NOTE: If no edge layout is performed, all edges will be rendered as
# line segments between vertices in the graph.
edge_strategy = vtkArcParallelEdgeStrategy()
edge_layout = vtkEdgeLayout()
edge_layout.SetLayoutStrategy(edge_strategy)
edge_layout.SetInputConnection(layout.GetOutputPort())
edge_geom = vtkGraphToPolyData()
edge_geom.SetInputConnection(edge_layout.GetOutputPort())
# create a mapper for edge display
edge_mapper = vtkPolyDataMapper()
edge_mapper.SetInputConnection(edge_geom.GetOutputPort())
# create the actor for displaying the
edges
edge_actor = vtkActor()
edge_actor.SetMapper(edge_mapper)
edge_actor.GetProperty().SetColor(0.,0.
,0.)
edge_actor.GetProperty().SetOpacity(0.2
5)
尽管上述类提供了灵活性,但当图中的边数量很大时,视觉混乱经常是一个问题。特别是每对相关顶点之间的直线连接,显示的图形很快就变得无法解释。为了克服这个问题,VTK包含了额外的算法来减少视觉混乱。它们本质上是弯曲并把相关的边捆在一起。实现此功能的过滤器包括:
•vtkGraphHierarchicalBundle(以及相关的vtkGraphHierarchicalBundleEdges) -在弧形束中布局图边,遵循Danny Holten在“分层边缘束:分层数据中邻接关系的可视化”中开发的算法。可视化与计算机图形学学报,Vol. 12, No. 5, 2006。741 - 748页。
•vtkSplineGraphEdges -子样本图边缘,使光滑(样条)曲线。下面是一个使用样条执行边缘捆绑的例子。
下面的代码执行树形布局,然后使用vtkGraphHierarchicalBundle从树的顶部显示捆绑的边(图8-5)。
// Create a standard radial
// tree layout strategy
vtkTreeLayoutStrategy* treeStrategy =
vtkTreeLayoutStrategy::New();
treeStrategy->SetAngle(360.);
treeStrategy->SetRadial(true);
treeStrategy->SetLogSpacingValue(0.8);
treeStrategy->SetLeafSpacing(0.9);
// Layout the vertices of the tree
// using the strategy just created
vtkGraphLayout* treeLayout =
vtkGraphLayout::New();
treeLayout->SetInput(realTree);
treeLayout->SetLayoutStrategy(treeStrategy);
// Use the tree to control the layout of the graph edges
vtkGraphHierarchicalBundle* bundle =
vtkGraphHierarchicalBundle::New();
bundle->SetInput(0, graph);
bundle->SetInputConnection(1, treeLayout->GetOutputPort(0));
bundle->SetBundlingStrength(0.9);
bundle->SetDirectMapping(true);
// Smooth the edges using with splines
vtkSplineFilter* spline = vtkSplineFilter::New();
spline->SetInputConnection(0, bundle->GetOutputPort(0));
路段布局
对于具有嵌入层次信息的树或图形(即具有嵌入生成树的图形),可选择的可视化方法将顶点的显示转换为基于多边形的结构(例如块,环,圆等)。这些方法在从原始层次结构中推断出附加结构时已被证明是有用的。这些类型的图形可视化的例子包括树状图、径向空间填充树和冰柱表示(图8-6)。
对于这些技术,每个顶点通常在一个4元组数组中分配一组值,这些值表示矩形区域(或树轮的圆形扇形)的位置和大小。图8-5从代码中捆绑图边的结果。)。这些方法还利用插入vtkAreaLayout的布局策略。目前可用的策略包括:
•vtkBoxLayoutStrategy (treemap) -一个树状地图布局,通过递归划分子顶点的空间,将顶点放在方形盒子中。
•vtkSliceAndDiceLayoutStrategy (treemap) -在水平和垂直切片之间交替布局树状图。
•vtkSquarifyLayoutStrategy (treemap) -使用Bruls, D.M, C. Huizing, J.J. van Wijk提出的方形树图算法。Squarified treemap。见:W. de Leeuw, R. van Liere(编),数据可视化2000,欧洲图形学与IEEE TCVG可视化研讨会论文集,2000,Springer,维也纳,第33-42页。
•vtkStackedTreeLayoutStrategy(树木年轮和冰柱)-在堆叠的盒子或年轮中布局树木。
•vtkTreeMapToPolyData—将带有关联数据数组的树转换为表示树状图的多边形数据。
•vtkTreeRingToPolyData -转换树与相关的数据数组多边形数据表示冰柱或树轮布局。
这些类利用在布局类中创建的4元组数组,并将这些数据转换为数组中值表示的每个顶点的标准多边形原语。
8.3意见及陈述
视图在VTK结合呈现逻辑,交互,可视化参数,并选择到一个地方。数据集通过向视图添加所谓的“表示”来显示在视图中。表示准备要在视图中显示的输入数据集,并包含如何在视图中呈现该数据的选项,包括颜色、图标、布局算法和标签。所有视图都是vtkView的子类,所有视图表示都是vtkDataRepresentation的子类。前面的小节已经介绍了vtkGraphLayoutView和vtkTreeRingView等视图。
可以通过两种方式向视图添加数据对象。一种方法是创建适当的表示,通过调用SetInputConnection()或SetInput()设置表示上的输入,然后在视图上调用AddRepresentation()。另一种通常更方便的方法是让视图通过调用AddRepresentationFromInput()或AddRepresentationFromInputConnection()为您自动创建默认表示。这些方法接受数据对象或算法图8-6 Treemap, tree ring, and icicle视图显示类划分为库,以及连接每个类到其超类的链接输出端口,为视图创建合适的表示,将表示添加到视图中,并返回指向新表示的指针。
下面的Python代码生成了一个简单的vtkRenderView,其中有一个球体源,显示为vtkSurfaceRepresentation。
from vtk import *
import versionUtil
if versionUtil.VersionGreaterThan(5, 4):
rep = vtkRenderedSurfaceRepresentation()
else:
rep = vtkSurfaceRepresentation()
sphere = vtkSphereSource()
sphere.SetPhiResolution(100)
sphere.SetThetaResolution(100)
rep.SetInputConnection(sphere.GetOutputPort())
view = vtkRenderView()
view.AddRepresentation(rep)
rw = versionUtil.SetupView(view)
versionUtil.ShowView(view)
注意,示例代码反映了VTK 5.4之后从vtkSurfaceRepresentation到vtkRenderedSurfaceRepresentation的名称变化。
下面显示了一个使用vtkTreeRingView类进行图形可视化的示例。注意,这个视图接受两个输入,一个用于图,一个用于树。此代码生成图8-6中的中心图像。树环包含由库组织的VTK类,而内部边显示子类到超类的关系。
# Import a grapRush with an embedded
hierarchy (tree).
from vtk import *
import vtk.util.misc
import versionUtil
datapath =
vtk.util.misc.vtkGetDataRoot()
reader1 = vtkXMLTreeReader()
reader1.SetFileName(datapath + "/
Data/Infovis/XML/vtkclasses.xml")
reader1.SetEdgePedigreeIdArrayName("tree edge")
reader1.GenerateVertexPedigreeIdsOff();
reader1.SetVertexPedigreeIdArrayName("id");
reader2 = vtkXMLTreeReader()
reader2.SetFileName(datapath + "/Data/Infovis/XML/vtklibrary.xml")
reader2.SetEdgePedigreeIdArrayName("graph edge")
reader2.GenerateVertexPedigreeIdsOff();
reader2.SetVertexPedigreeIdArrayName("id");
# Setup the view parameters for displaying the graph
view = vtkTreeRingView()
view.SetTreeFromInputConnection(reader2.GetOutputPort())
view.SetGraphFromInputConnection(reader1.GetOutputPort())
view.SetAreaColorArrayName("VertexDegree")
view.SetEdgeColorToSplineFraction()
view.SetAreaHoverArrayName("id")
view.SetColorEdges(True)
view.SetAreaLabelArrayName("id")
view.SetAreaLabelVisibility(True)
view.SetShrinkPercentage(0.02)
view.SetBundlingStrength(.8)
# Apply a theme to the views
theme = vtkViewTheme.CreateMellowTheme()
view.ApplyViewTheme(theme)
rw = versionUtil.SetupView(view)
versionUtil.ShowView(view)
这些是目前在VTK中实现的渲染视图子类:
•vtkGraphLayoutView -可视化图形的视图窗口
•vtkHierarchicalGraphView -视图窗口可视化与关联的图形,或派生的,分层数据。
•vtkIcicleView -视图可视化树,或图形与关联或派生的分层数据,使用冰柱布局。
•vtkTreeMapView -视图可视化树,或图形与关联或派生的分层数据,使用树形图布局。
•vtkTreeRingView -视图可视化树,或图形与关联或派生的分层数据,使用径向空间填充布局。
视图项的选择处理
选择是VTK视图架构的重要组成部分。有关VTK中选择的一般描述,请参阅第255页的“交互、小部件和选择”。大多数视图都具有交互式创建选择的能力(例如,通过鼠标点击或橡皮带选择),并且还具有通过高亮显示或其他机制显示当前选择的能力。
当用户执行选择交互(例如,通过点击或拖动选择框)时,视图会自动生成相应类型的选择。只需在多个表示上设置一个通用的vtkSelectionLink对象,就可以在视图之间共享选择。(在5.4之后的VTK版本中,vtkSelectionLink已被更灵活的vtkAnnotationLink所取代。这与vtkSelectionLink的工作方式相同,但除了共享选择之外,还可以在视图之间共享数据上的注释。)
下面的示例生成带有链接选择的图形视图和表视图。表视图在Qt小部件中显示图形的顶点数据。类似地,vtkQtTreeView可以在Qt树小部件中显示vtkTree的内容。当在表视图中选择行时,图视图更新以反映这一点,反之亦然。
QApplication app(argc, argv);
// Create the graph source and table conversion
vtkRandomGraphSource* src = vtkRandomGraphSource::New();
vtkDataObjectToTable* o2t = vtkDataObjectToTable::New();
o2t->SetInputConnection(src->GetOutputPort());
o2t->SetFieldType(vtkDataObjectToTable::VERTEX_DATA);
// Create Qt table view and add a representation
vtkQtTableView* tv = vtkQtTableView::New();
vtkDataRepresentation* tr;
tr = tv->AddRepresentationFromInputConnection(o2t->GetOutputPort());
// Create graph layout view
vtkGraphLayoutView* gv = vtkGraphLayoutView::New();
gv->SetVertexLabelArrayName("vertex id");
gv->VertexLabelVisibilityOn();
gv->SetLayoutStrategyToSimple2D();
// Add representation to graph view
vtkDataRepresentation* gr;
gr = gv->AddRepresentationFromInputConnection(src->GetOutputPort());
gr->SetSelectionLink(tr->GetSelectionLink());
// Ensure both views update when selection changes
vtkViewUpdater* vu = vtkViewUpdater::New();
vu->AddView(gv);
vu->AddView(tv);
// Start application
#if VTK_5_4_OR_EARLIER
vtkRenderWindow* win = vtkRenderWindow::New();
gv->SetupRenderWindow(win);
#endif
tv->GetItemView()->show();
app.exec();
}
8.4图算法
图数据结构是任何信息学应用程序的重要组成部分。许多信息学问题由无数数据源(人、电话、电子邮件、出版物)和这些数据类型之间的关系组成。一般来说,图非常适合存储、操作和分析不同的实体以及这些实体之间的连接。在语义图中,实体由图顶点表示,实体之间的关系由图边表示。vtkGraph数据结构可以在顶点和边上存储任意属性,因此属性和类型(“语义”)很容易表达(参见Siek, Jeremy, Lie-Quan Lee和Andrew Lumsdaine. 2001)。Boost图形库:用户指南和参考手册。addison - wesley专业)。
图的有效处理和分析需要大量的图算法。VTK工具包利用第三方库来提供它们。这些库包括Boost Graph Library (BGL), Parallel Boost Graph Library (PBGL)和多线程Graph Library (MTGL)。
其中一个库是Boost Graph Library (BGL),它为许多常见的图算法实现提供了一个通用的c++模板接口。VTK提供了一个“无数据”适配器,实现了所需的BGL概念,并允许BGL算法直接处理vtkGraphs。
任何图形算法的使用都遵循VTK管道模型。清单<BFS>演示了图形算法的用法。代码包括算法的头文件,为算法创建VTK过滤器(在本例中为vtkBoostBreadthFirstSearch),并将算法放入管道中。更新管道后,该算法的结果可以作为图的节点和/或边缘上的属性使用。这个例子简单地标记每个顶点与起始顶点的距离(标记为“0”)。
#include "vtkBoostBreadthFirstSearch.h"
#include "vtkGraphLayoutView.h"
#include "vtkRandomGraphSource.h"
#include "vtkRenderWindow.h"
#include "vtkRenderWindowInteractor.h"
int main(int argc, char* argv[])
{
// Create a random graph
vtkRandomGraphSource* source = vtkRandomGraphSource::New();
// Create BGL algorithm and put it in the pipeline
vtkBoostBreadthFirstSearch* bfs = vtkBoostBreadthFirstSearch::New();
bfs->SetInputConnection(source->GetOutputPort());
// Create a view and add the BFS output
vtkGraphLayoutView* view = vtkGraphLayoutView::New();
view->AddRepresentationFromInputConnection(bfs->GetOutputPort());
// Color vertices based on BFS search
view->SetVertexColorArrayName("BFS");
view->ColorVerticesOn();
view->SetVertexLabelArrayName("BFS");
view->VertexLabelVisibilityOn();
// See the start of the Information Visualization chapter
// for information on how this has changed after VTK 5.4.
vtkRenderWindow* window = vtkRenderWindow::New();
view->SetupRenderWindow(window);
window->GetInteractor()->Start();
source->Delete();
bfs->Delete();
view->Delete();
window->Delete();
return 0;
}
作为管道组件,图算法也可以以独特的方式组合。例如,下面的python代码片段(摘自VTK\Examples\Infovis\ python \boost_mst.py)展示了两种协同工作的图算法。
# Create a random graph
randomGraph = vtkRandomGraphSource()
# Connect to the centrality filter.
centrality = vtkBoostBrandesCentrality ()
centrality.SetInputConnection(randomGraph.GetOutputPort())
# Find the minimal spanning tree
mstTreeSelection = vtkBoostKruskalMinimumSpanningTree()
mstTreeSelection.SetInputConnection(centrality.GetOutputPort())
mstTreeSelection.SetEdgeWeightArrayName("centrality")
mstTreeSelection.NegateEdgeWeightsOn()
# Create a graph layout view
view = vtkGraphLayoutView()
view.AddRepresentationFromInputConnection(centrality.GetOutputPort())
这里顶点和边的中心性都是由vtkBoostBrandesCentrality过滤器计算的,该算法的结果被馈送到vtkBoostKruskalMinimumSpanningTree过滤器,该过滤器计算图中最高中心性边的“最大”生成树(假设设置了“NegateEdgeWeights”参数)。
Boost图形库算法
•vtkBoostBreadthFirstSearch -执行广度优先搜索(BFS)的图从一些原点节点,并返回一个vtkGraph与新的属性。
•vtkBoostBreadthFirstSearchTree -执行一个图的BFS,并返回一个扎根于原始节点的树。
•vtkBoostBiconnectedComponents -计算vtkGraph的双连接组件。
•vtkBoostBrandesCentrality -使用Brandes算法计算图的中心性。•vtkBoostConnectedComponents -发现vtkGraph的连接组件。如果图是无向的,则计算自然连通分量,如果图是有向的,则计算强连通分量。
•vtkBoostKruskalMinimumSpanningTree -使用BoostKruskal最小生成树(MST)算法来计算加权图上的MST。
•vtkBoostPrimMinimumSpanningTree -使用BoostPrim MST算法来计算正加权vtkGraph上的MST。
本节中讨论的许多算法都在VTK\ examples \Infovis\ python下的python示例中进行了演示。
vtkBoostBreadthFirstSearch。这个过滤器实现了一个vtkgraphhalgalgorithm,该算法计算扎根于某个起始节点的vtkGraph的BFS。起始节点可以是一个选择,也可以通过它在图中的索引来指定。Boost BFS实现的时间复杂度为0 (E+V)。
SetOriginSelection(vtkSelection* s)通过包含图中节点的选择设置搜索的起始节点。
SetOriginSelectionConnection(vtkAlgorithmOutput *)使用另一个VTK过滤器的输出设置源节点。
SetOriginVertex(vtkIdType index)设置BFS原点顶点的索引(到顶点数组中)。图8-10基于图边中心性计算的最小生成树(紫色)8.4图算法183
SetOriginVertex(arrayName,value)设置BFS的起始顶点。该方法允许应用程序简单地指定数组名称和值,而不必知道顶点的特定索引。
SetOriginVertexString(arrayName,value)设置给定数组名称和字符串值的BFS 'origin'顶点。
SetOutputArrayName(string)设置输出图顶点属性数组的名称。默认为“BFS”。
SetOriginFromSelection(bool)如果为true/on,则使用输入端口1的vtkSelection作为起始顶点。选择应该是字段类型为POINTS的id选择。选择中的第一个ID将被用作“原点”顶点。默认为OFF。
SetOutputSelection(bool)如果为true/on,则根据输出选择类型创建一个包含顶点ID的输出选择。默认是使用到起始顶点的最大距离。默认为OFF。
SetOutputSelectionType(string)设置输出选择类型。默认是使用到起始顶点的最大距离“MAX_DIST_FROM_ROOT”。其他选项如“ROOT”,“2D_MAX”等也可以使用。这个VTK类使用Boost BFS通用算法从给定的源顶点执行vtkGraph的BFS。这个过滤器的结果是一个vtkTree,其根节点对应于搜索的开始节点。
SetOriginVertex(index)设置起始顶点数组的索引。settoriginvertex (arrayName,value)在指定顶点数组中给定一个值,设置起始顶点。
SetCreateGraphVertexIdArray(bool)将树顶点的顶点id存储在名为“GraphVertexId”的数组中。默认为OFF。
vtkBoostBiconnectedComponents。这个VTK类在vtkGraph中搜索双连接组件。图的双连通分量是图的最大区域,从该区域移除任何单个顶点都不会断开图。该算法返回一个顶点属性数组和一个边缘属性数组,其中包含它们的双连接组件id。
每条边都只属于一个双连接组件,默认情况下会在名为“双连接组件”的边缘数组中给出。同样,默认情况下,每个顶点的双连通组件id也在名为“双连通组件”的顶点数组中给定。
切割顶点(或连接点)是属于多个双连接组件的顶点,如果移除则会将图分开。这些是通过分配组件值- 1来表示的。要确定切割顶点所属的双连接组件,遍历其边列表并收集其关联边的不同组件id。
Boost双连接分量算法的时间复杂度为0 (V+E)
SetOutputArrayName(string)
设置顶点和边数组的输出数组名称。如果没有指定输出数组名称,则使用“bicconnected component”。
vtkBoostBrandesCentrality。这个类使用Boost brandes_betweeness_centrality算法来计算vtkGraph上的between centrality。这个过滤器将顶点数组和边缘数组添加到vtkGraph,名称为“centrality”,类型为vtkFloatArray。
Boost Brandes中间性中心性的时间复杂度对于未加权图为O(VE),对于加权图为O(VE+V(V+E) log V)。空间复杂度为0 (VE)。
vtkBoostConnectedComponents。发现vtkGraph的连接区域,在顶点数组“components”中为每个顶点分配一个组件ID。如果输入图是无向图,则输出包含图的自然连通分量。相反,如果输入图是一个有向图,这个过滤器将发现图的强连接分量(即,在每个集合内的任何一对顶点之间存在有向路径的顶点的最大集合)。对于无向图,该算法的时间复杂度为O(V+E alpha(E,V)),其中alpha是Ackermann函数的逆。对于大多数实际用途,时间复杂度仅略大于0 (V+E)。
有向图算法的时间复杂度为0 (V+E)。
vtkBoostKruskalMinimumSpanningTree。该过滤器使用Boost Kruskal MST通用算法为输入图中的每个边给定加权值,找到vtkGraph的最小生成树(MST)。如果需要,该算法还允许将边权取为负值以创建最大生成树。该过滤器产生一个vtkSelection,其中包含定义MST的图的边缘。
Boost Kruskal MST算法的时间复杂度为O(E log E)。
SetEdgeWeightArrayName(string)设置边权输入数组的名称,该数组必须是输入图的边数据的一部分,并且包含数值数据。如果边权数组不是vtkDoubleArray类型,它将被复制到一个临时的vtkDoubleArray中。
SetOutputSelectionType(string)设置输出选择类型。默认是使用最小生成树边集“MINIMUM_SPANNING_TREE_EDGES”。当前没有定义其他选项。
SetNegateEdgeWeights(bool)开关过滤器是否应该否定边的权重。通过消去边权,该算法将尝试创建“最大”生成树。“最大”生成树是指具有最高权边的生成树。默认为OFF。
vtkBoostPrimMinimumSpanningTree。该过滤器使用Boost Prim最小生成树通用算法在给定原点顶点的边加权vtkGraph上创建MST。这个过滤器与Kruskal MST的区别主要在以下几个方面:
•必须指定一个起始顶点。
•负边权方法不能用于获得“最大”生成树,如果存在任何负边权,将抛出异常。
•Prim算法的boost实现返回一个顶点前置映射,如果存在平行边,则会导致原始图中应该使用哪条边的模糊;由于这个原因,当前的VTK实现不会将边缘数据从图复制到新树。
•边权数组必须是vtkDataArray类型或vtkDataArray的子类型,在Kruskal变体上提供更多的通用性。Boost Prim MST算法的时间复杂度为0 (E log V)
SetEdgeWeightArrayName(string)设置边权输入数组的名称。边权数组必须是vtkDataArray。
SetOriginVertex(index)通过在图中的索引来设置“原点”顶点。
SetCreateGraphVertexIdArray(bool)如果启用,将树顶点的图形顶点id存储在名为“GraphVertexId”的数组中。默认为OFF。
SetNegateEdgeWeights(bool)如果开启,则边的权重被否定。请参阅描述中的注意事项,如果存在负边权值,此过滤器将抛出异常。默认为OFF。
创建图算法
n实践任何人都可以添加一个图算法到VTK。开发人员可以采用几种方法。对于python程序员来说,第一个也是最简单的方法是使用vtkprogramablefilter。图8-11中的python代码演示了使用可编程python过滤器来计算顶点度(取自VTK\Examples\Infovis\ python \vertex_degree_programmable.py)。def computeVertexDegree ():
input = vertexDegree.GetInput()
output = vertexDegree.GetOutput()
output.ShallowCopy(input)
# Create output array
vertexArray = vtkIntArray()
vertexArray.SetName("VertexDegree")
vertexArray.SetNumberOfTuples(output.GetNumberOfVertices())
# Loop through all the vertices setting the degree for the new
# attribute array
for i in range(output.GetNumberOfVertices()):
vertexArray.SetValue(i, output.GetDegree(i))
# Add the new attribute array to the output graph
output.GetEdgeData().AddArray(vertexArray)
vertexDegree = vtkProgrammableFilter()
vertexDegree.SetExecuteMethod(computeVertexDegree)
# VTK Pipeline
randomGraph = vtkRandomGraphSource()
vertexDegree.AddInputConnection(randomGraph.GetOutputPort())
view = vtkGraphLayoutView()
view.AddRepresentationFromInputConnection(vertexDegree.GetOutputPort()
)
view.SetVertexLabelArrayName("VertexDegree")
view.SetVertexLabelVisibility(True)
另一种直接的方法是创建一个常规的VTK c++过滤器作为图形算法。作为参考使用的最简单的过滤器是vtkVertexDegree (VTK\Infovis\vtkVertexDegree.cxx)。c++代码在vtkVertexDegree。cxx看起来与上面图X中的python示例非常相似,并且包含相同的功能。vtkGraph的文档和用于访问数据结构的各种方法的API也将有助于创建新的过滤器。
也许在VTK中创建新图形算法最有趣(和先进)的方法是向Boost graph Library贡献算法,然后将该算法“包装”到VTK类中。一个很好的参考是vtkBoostBreadthFirstSearch过滤器(VTK\Infovis\vtkBoostBreadthFirstSearch.cxx)和vtkBoostGraphAdapter.h文件。关于Boost Graph Library的详细文档可以在http://www.boost.org/doc/以及the Boost Graph Library:用户指南和参考手册中找到。虽然需要做更多的工作,但这种方法对VTK用户和Boost社区的成员都有好处
并行Boost图形库
作为一个分布式内存工具包,VTK目前围绕并行科学数据处理和可视化提供了无数的功能。并行增强图库(PBGL)是一个通用的c++库,用于高性能并行和分布式图算法。vtkGraph数据结构,以及一些分布式助手类,使PBGL功能能够以与BGL类相同的方式工作。将PBGL功能集成到VTK中目前还处于早期阶段。我们还想指出,PBGL是Boost 1.40版本中Boost库的一部分。VTK已经开始包括一些基于PBGL的过滤器,这些过滤器可以在Parallel子目录中找到。
多线程图形库
多线程图形库(MTGL)针对共享内存平台,如大规模多线程的Cray MTA/XMT,当与Qthreads库芯片多处理器(如Sun Niagara和多核工作站)一起使用时。MTGL是基于串行Boost的图8-11由每个顶点的计算度标记的图graph Library,因为数据分布在所讨论的平台上不是问题。共享内存编程是一个挑战,而MTGL中的算法对象可以封装这个挑战的大部分,但不是全部。与BGL中一样,访问者模式使用户能够在算法执行的关键点上应用方法。MTGL用户像BGL用户一样在他们的图数据结构上编写适配器,但是不假设Boost和STL可用。非结构化图上连接组件的MTGL代码,这是分布式内存架构的一个难题,在Cray MTA-2上几乎可以完美地扩展。
8.5数据库
作为其信息可视化功能的一部分,VTK 5.4提供了访问SQL数据库的类。Infovis数据集通常很适合关系数据库模型。它们通常包含几个较小的数据集,这些数据集通过适合表的公共属性链接在一起。此外,数据库允许应用程序通过针对感兴趣的子集发出查询来减轻管理大数据的任务。
低级数据库访问被分成两个抽象类。第一个是vtkSQLDatabase,它负责打开和关闭数据库,报告是否支持事务、准备好的语句和触发器等特性,以及使用模式对象创建表。第二个是vtkSQLQuery,它负责将SQL语句发送到数据库执行,检查错误,并提供对结果的访问。
连接特定数据库(MySQL, SQLite, Oracle等)的细节是在vtkSQLDatabase的具体子类中实现的。类似地,为特定数据库执行查询和检索结果的细节在vtkSQLQuery的具体子类中实现。这些具体的子类分别称为“数据库驱动程序”和“查询驱动程序”。
VTK 5.4包含以下数据库的驱动程序:
•SQLite 3.4.1
•MySQL 5.0(及更高版本)
•PostgreSQL 7.1(及更高版本)
•ODBC SQLite的副本包含在VTK源代码中。
为了与其他数据库实现交互,您必须从源代码构建VTK,并链接到供应商提供的客户端库。如果您自己编译数据库,这些库可能是自动构建的。如果您安装了预编译包,您可能需要单独下载它们(通常在“开发”包中)。
连接数据库
为了连接到数据库,必须首先获得适当的数据库驱动程序的实例。有两种方法可以做到。
1. 从URL自动创建驱动程序:vtkSQLDatabase *db =
2.vtkSQLDatabase:: createfrommurl (" sqlite://mydata.db ");
3. 直接实例化驱动:
4.vtksqliteddatabase *db = vtksqliteddatabase::New();
CreateFromURL()方法将尝试在驱动程序对象上设置所有提供的参数(用户名、服务器地址、服务器端口、数据库名称)。如果您直接实例化驱动程序,您必须自己设置所有这些参数。如果数据库需要密码,则必须在Open()调用中提供密码。您可以通过调用close()或删除数据库驱动程序来关闭连接。如果数据库连接无法打开,那么Open()将返回false。您可以使用GetLastErrorText()方法检索有关失败原因的信息。打开数据库的c++代码通常如下所示:
vtkSQLDatabase *db = vtkSQLDatabase::CreateFromURL("sqlite://
mydata.db");
bool status = db->Open("");
if (!status)
{
cout << "Couldn't open database. Error message: "
<< db->GetLastErrorText() << endl;
}
一旦建立了连接,就可以调用GetTables()来获取数据库中的表列表。使用其中一个表的名称调用GetRecord()函数,它将返回该表中列的列表。这些函数对于连接到数据库的应用程序非常有用,这些应用程序不需要预先了解数据库的模式。
建设和执行查询
要实际执行查询,必须使用其中一个查询驱动程序的实例。查询驱动程序永远不会直接实例化。要获得一个,在连接成功打开后,在数据库驱动程序上调用GetQueryInstance()。通过使用包含整个SQL语句的字符串调用SetQuery()来设置查询。没有必要用分号结束语句。必须在字符串中嵌入所有查询参数。查询将被发送到数据库,并在调用Execute()方法时执行。Execute()将根据查询是否成功运行或是否遇到错误返回true或false。与数据库驱动程序一样,您可以通过调用查询驱动程序上的GetLastErrorText()来检索任何错误消息。
vtkSQLQuery* query = db->GetQueryInstance();
const char *queryText = "SELECT name, age, weight "
"FROM people WHERE age <= 20";
query->SetQuery(queryText);
if (!query->Execute())
{
cout << "Query failed. Error message: "
<< query->GetLastErrorText() << endl;
}
您可以为多个查询重用单个查询驱动程序。没有“关闭”当前活动查询的概念:您可以简单地使用SetQuery()设置新的查询字符串并调用Execute()。如果驱动程序正在读取一组查询结果,它们将被自动清理
查询和线程
vtkSQLQuery和vtkSQLDatabase都不是线程安全的。这更多的是底层本地数据库api的限制,它们本身很少是线程安全的,而不是VTK本身的限制。如果应用程序需要从多个线程并发访问数据库,则必须为每个线程创建一个新的数据库连接(通过vtkSQLDatabase)。每个线程可以根据需要维护尽可能多的vtkSQLQuery对象。在某些安装中,数据库管理员可能会对同时打开的并发会话和查询的数量施加限制。请咨询您的系统管理员,看看这是否是个问题。
读取结果
一旦查询成功执行,Execute()返回一个true值,你的程序就可以读取查询结果了。这里可以检索两种数据。查询元数据包括列的数量、列的名称和列的数据类型。这些可以通过GetNumberOfColumns(), GetColumnName(int)和GetColumnType(int)来检索。VTK将尽最大努力从数据库后端的本地数据类型转换为c++、Python和Java支持的标准数据类型。每次读取一行结果数据。必须调用vtkSQLQuery::NextRow()才能前进到下一个可用行。如果有另一行可读,此方法将返回true,如果无法检索到更多数据,则返回false。一行中的数据值可以一次检索一个,也可以一次检索全部。要检索单个列的值,请使用要检索的列的索引调用vtkSQLQuery::DataValue(int)。整个过程如下所示。
while (query->NextRow())
{
for (int field = 0; field < query->GetNumberOfFields(); field++)
{
vtkVariant v = query->DataValue(field);
// Process it
}
}
要一次检索整行,调用vtkSQLQuery::NextRow(vtkVariantArray *)而不是NextRow()。该行的值将存储在您提供的数组中。
vtkVariantArray* va = vtkVariantArray::New();
while (query->NextRow(va))
{
// Process it
}
在处理查询结果时,我们通常希望将整个结果集存储在vtkTable中,以便通过管道传递。vtkRowQueryToTable过滤器就是这样做的。通过获取和设置查询驱动程序来使用它,然后将查询作为SetQuery()的参数传递给过滤器。当手动更新过滤器(通过调用Update())或通过来自管道下游的请求更新过滤器时,将执行查询。
vtkRowQueryToTable* reader = vtkRowQueryToTable::New();
reader->SetQuery(query);
reader->Update();
vtkTable* table = reader->GetOutput();
存取数据
因为vtkSQLQuery只要求查询字符串是有效的SQL,它可以用于更多的操作,而不仅仅是从表中读取。例如,CREATE、UPDATE、INSERT、DROP和TRUNCATE(以及其他)都可以使用。唯一的要求是,如果查询返回除状态码以外的任何输出,则必须采用行格式。这将排除诸如EXPLAIN之类返回未格式化文本的命令。您可以使用SQL INSERT命令将数据写回数据库。由于必须用单独的INSERT语句插入每一行,这对于大量数据可能很慢。下一个版本的VTK支持预置语句和绑定参数将有助于消除这一瓶颈。同时,如果您需要将大型数据集写回数据库,请考虑使用数据库的本机接口或批量加载器,而不是VTK访问类。下面的示例创建一个名为PEOPLE的表并填充它。为简洁起见,省略了错误检查
vtkStdString createQuery("CREATE TABLE IF NOT EXISTS people "
"(name TEXT, age INTEGER, weight FLOAT)");
query->SetQuery( createQuery.c_str() );
query->Execute();
for (int i = 0; i < 20; i++)
{
char insertQuery[200];
sprintf( insertQuery,
"INSERT INTO people (name, age, weight) "
"VALUES('John Doe %d', %d, %f)",
i, i, 10.1*i );
query->SetQuery(insertQuery);
query->Execute()
}
表模式
VTK提供了一个类,vtkSQLDatabaseSchema,用于表示关系数据库模式。如果您的程序需要创建数据库,而不是简单地访问现有数据库,这将非常有用。这个类能够在跨平台设置中表示表、表的列和表的索引。这个类的目标是简化处理多个数据库后端,而不是用任何给定的数据库类型表示每种可能的模式。例如,只支持有限的一组列类型;不支持PostgreSQL中可用的扩展类型。但是,它可以存储触发器(对于支持触发器的数据库)和前置语句,并指出每个触发器或前置语句是为哪个数据库后端编写的,以适应特定于后端的SQL语句。
提供了对模式的编程访问,以便可以根据应用程序中提供的一些选项动态生成表、列或索引。一个模式对象可以包含多个表。每个表都有一个由AddTable方法返回的唯一整数句柄。要向表中添加列、索引或触发器,必须将该整数作为第一个参数分别传递给AddColumnToTable()、AddIndexToTable()或AddTriggerToTable()。
AddColumnToTable()函数接受一个列类型(使用vtkSQLDatabaseSchema提供的枚举)、一个列名、一个整数字段宽度和一个指定列选项(如默认值)的文本字符串。字段宽度指定VARCHAR或其他可变大小列类型的存储大小,并指定MySQL中数值类型打印输出的默认列宽度。
AddIndexToTable()函数为表创建主键、唯一值索引或普通索引。因为索引可能有多个列,所以必须调用AddIndexToTable,然后将索引中每个列的整数索引传递给AddColumnToIndex。
最后,AddTriggerToTable()允许添加SQL语句,每次修改表的记录时执行这些语句。因为触发器的语法因后端而异,所以AddTriggerToTable()方法的最后一个参数允许您指定触发器用于哪个数据库后端。触发器只会被添加到相同后端的数据库中。因为可以为每个表向模式中添加多个触发器,所以可以为希望应用程序支持的每个后端创建相同触发器的不同版本。另外,一些后端如PostgreSQL要求你指定一个触发器作为命名函数。为了允许您在添加触发器之前定义函数,模式类提供了AddPreamble()。传递给AddPreamble()的语句在创建任何表之前执行。与AddTriggerToTable()一样,AddPreamble()的最后一个参数允许您指定语句应用于哪个后端。
一旦创建了vtkSQLDatabaseSchema对象并使用上面的函数填充它,就可以调用vtkSQLDatabase::EffectSchema()将模式转换为一组表。下面是如何使用Python中的vtkSQLDatabaseSchema类的示例。
from vtk import *
schema = vtkSQLDatabaseSchema()
schema.SetName('TestSchema')
url = 'psql://vtk@localhost/vtk_test'
## Values of enums
VTK_SQL_SCHEMA_SERIAL = 0
VTK_SQL_SCHEMA_BIGINT = 3
VTK_SQL_SCHEMA_PRIMARY_KEY = 2
btab = schema.AddTable('btable')
col0 = schema.AddColumnToTable(btab,
VTK_SQL_SCHEMA_SERIAL, 'tablekey', 0, '')
col1 = schema.AddColumnToTable(btab,
VTK_SQL_SCHEMA_BIGINT, 'somevalue', 12, 'DEFAULT 0')
idx0 = schema.AddIndexToTable(btab,
VTK_SQL_SCHEMA_PRIMARY_KEY, '')
i0c0 = schema.AddColumnToIndex(btab, idx0, col0)
# Create a dummy database instance
# so we can call CreateFromURL,
# then replace instance with real thing
db = vtkSQLiteDatabase()
db = db.CreateFromURL(url);
# Try opening the database without a password
if not db.Open(''):
# Ask the user for a password and try it.
from getpass import getpass
db.Open(getpass('Password for %s' % url))
if db.IsOpen():
# If we were able to open the database, effect the schema
db.EffectSchema(schema, True);
在c++(但不是包装语言)中,有一个名为AddTableMultipleArguments的方便例程可以帮助声明静态模式。它使用cstdarg包,因此您可以传递任意数量的参数,指定许多表列、索引和触发器。该函数的第一个参数是要创建的表的名称。它后面跟着任意数量的令牌,每个令牌在它之后和下一个令牌之前可能需要额外的参数。最后一个参数必须是特殊的vtkSQLDatabaseSchema::END_TABLE_TOKEN。存在用于添加列、索引和触发器的令牌。清单2显示了AddTableMultipleArguments的使用,清单1中的示例被转换成c++来说明AddTableMultipleArguments。
vtkSQLDatabaseSchema* schema = vtkSQLDatabaseSchema::New();
schema->SetName("TestSchema");
tblHandle = schema->AddTableMultipleArguments("btable",
vtkSQLDatabaseSchema::COLUMN_TOKEN,
vtkSQLDatabaseSchema::SERIAL, "tablekey", 0, "",
vtkSQLDatabaseSchema::COLUMN_TOKEN,
vtkSQLDatabaseSchema::BIGINT, "somevalue", 12, "DEFAULT 0",
vtkSQLDatabaseSchema::INDEX_TOKEN,
vtkSQLDatabaseSchema::PRIMARY_KEY, "",
vtkSQLDatabaseSchema::INDEX_COLUMN_TOKEN, "tablekey",
vtkSQLDatabaseSchema::END_INDEX_TOKEN,
vtkSQLDatabaseSchema::END_TABLE_TOKEN
);
vtkSQLDatabase* db = vtkSQLDatabase::CreateFromURL(url);
db->EffectSchema(schema);
8.6统计
统计特征是有用的,因为它们不仅可以提供关于数据趋势的信息,而且可以提供关于这些趋势的重要性的信息。此外,由于数据集的大小和复杂性都在不断增加,因此拥有表征高维数据的工具非常重要,这样就可以使用传统技术发现和可视化低维特征。许多统计工具已经在VTK中实现,用于检查单个字段的行为,字段对之间的关系以及任意数量字段之间的关系。每个工具对存储在一个或多个vtkTable对象中的数据起作用;第一个表作为观测值,其他表作为模型数据。第一个表的每一行都是一个观测值,而其他表的形式取决于统计分析的类型。第一个表的每一列都是一个变量。
单变量统计算法只使用来自单个列的信息,类似地,双变量算法只使用来自两个列的信息。因为一个输入表可能有比算法可以使用的更多的列,VTK必须为用户提供一种方式来表示感兴趣的列。因为它可能更有效地执行同一类型的多个分析在不同的列集一次而不是一个接一个,VTK为用户提供了一种方式,使多个分析请求在一个单一的过滤器。
以图8-12为例。它有5个变量的6个观测值。如果A,B和C之间以及B,C和D之间存在线性相关性,则必须发出两个请求R1和R2:第一个请求R1将具有感兴趣的列{A,B,C},而R2将具有感兴趣的列{B,C,D}。在一次传递中计算R1和R2的线性相关性比单独计算它们更有效,因为两个请求都需要协方差cov(B,B)、cov(C,C)和cov(B,C),但只需要计算一次。
阶段
每个统计算法在一个公共阶段序列中执行其计算,而不考虑正在执行的特定分析。可以根据需要配置VTK统计算法来执行这三种操作的各个子集。这些阶段可以描述为:
•学习:从输入数据集计算“原始”统计模型。通过“原始”,我们的意思是期望模型的最小表示,它只包含主要统计数据。例如,在描述性统计的情况下:样本大小,最小值,最大值,平均值,以及居中的M2, M3和M4聚合(参见P. pbay,鲁棒公式,协方差和任意阶统计矩的一次并行计算,Sandia报告SAND2008-6212, 2008年9月,http://infoserve.sandia.gov/sand_doc/2008/086212.pdf)。对于请求R1={B}的表1,这些值是6,1,11,4.83…, 68.83……, 159.4……,和1759.8194…,分别。
•推导:从原始模型中计算出“完整”的统计模型。所谓“完整”,我们指的是期望模型的完整表示,它包含原始统计和派生统计。例如,在描述性统计的情况下,从原始模型计算出以下派生统计量:无偏方差估计量、标准差和两个偏度和峰度估计量(g和g)。对于请求R1={B}的表1,这些附加值是13.76…, 3.7103, 0.520253, 0.936456, -1.4524, -1.73616。
•评估:给定一个统计模型——来自相同或另一个数据集——标记给定数据集的每个数据。例如,在描述性统计的情况下,每个数据都标有其
相对于模型均值和标准偏差的相对偏差(这相当于一维马氏距离)。表1显示了e列中R1 = {B}的这个距离。一个使用VTK的统计工具和Qt应用程序客户端描述的例子-为infovis>定制的鸟瞰应用程序。如图8-13所示;具体地说,描述性、相关性和顺序统计类与各种表视图和图一起使用。除了可以在任何类型(名义、基数或序数)变量上执行的偶然性统计外,所有当前实现的算法都需要基数或序数变量作为输入。以下统计算法目前在VTK中可用。
单变量算法这些算法接受单列(或一组单列),并对该列中的数据分布进行分析。描述性统计。
•学习:计算最小,最大,平均,和居中的M2, M3和M4聚集体;
•推导:计算无偏方差估计量,标准差,偏度(g1和g1估计量),峰度(g2和g2估计量);
•评估:用相对偏差(一维马氏距离)标记。订单统计。
•学习:计算直方图;
•推导:计算任意分位数,例如箱形图、十分位数、百分位数等的5点统计(四分位数);
•评估:用分位数指数进行评分。图8-13 VTK统计算法在OverView客户端的应用示例二元统计:接受一对列进行操作,并进行比较分析。相关的统计数据。
•学习:计算最小值,最大值,均值,和居中的M2聚集;
•推导:计算无偏方差和协方差估计量,Pearson相关系数和线性回归(双向);
•评估:用二维马氏距离的平方标记。应急数据。
•学习:计算列联表;
•推导:计算联合概率、条件概率和边际概率,以及信息熵;
•评估:用联合和条件PDF值标记,以及点对点的相互信息。多变量统计:这些过滤器都接受多个请求Ri,每个请求Ri都是一组ni变量,应该根据这些变量计算同时的统计信息。Multi-correlative统计数据。
•学习:计算均值和成对中心的M2聚集;
•推导:计算对称ni x ni协方差矩阵的上三角部分及其(下)Cholesky分解;
•评估:用多维马氏距离的平方标记。主成分分析(PCA)统计。
•学习:与多相关滤波器相同;
•推导:多相关滤波器提供的所有内容,加上协方差矩阵的ni特征值和特征向量;
•评估:对主成分(特征向量)执行基的变化,可选地投射到第一个mi成分,其中mi <= ni要么是一些用户指定的值,要么是由最大特征值的分数决定,其总和高于用户指定的阈值。这将导致为每个请求Ri增加mi列的数据。
k-Means统计(kMeans在VTK 5.4之后添加)。
•学习:使用初始集群中心计算数据的新集群中心。当用户使用附加输入表提供初始集群中心时,将计算多组新集群中心。输出元数据是一个多块数据集,其中至少包含一个vtkTable,列指定每次运行的以下内容:运行ID,集群数量,收敛所需的迭代次数,与集群相关的RMS误差,集群中元素的num- 196信息可视化精度,以及新的集群坐标;
•推导:计算在学习阶段计算的聚类集之间的全局和局部排名。全局排名是由所有新聚类中心之间的误差决定的,而局部排名是在具有相同数量聚类的聚类集之间计算的。总误差也会被报告;
•评估:用最近的集群id和每组集群中心的相关距离进行标记。使用统计算法使用VTK的统计类相当容易。例如,清单1演示了如何在不进行后续数据评估的情况下,对类型为vtkTable的输入集inData的两对列计算偶然性统计信息。这里假设输入数据表至少有3列。
// Assume the input dataset is passed to us
// Also is assume that it has a least 3 columns
vtkTable* inData = static_cast<vtkTable*>(arg);
// Create contingency statistics class
vtkContingencyStatistics* cs = vtkContingencyStatistics::New();
// Set input data port
cs->SetInput(0, inData);
// Select pairs of columns (0,1) and (0,2) in inData
cs->AddColumnPair(inData->GetColumnName[0], inData-
>GetColumnName[1]);
cs->AddColumnPair(inData->GetColumnName[0], inData-
>GetColumnName[2]);
// Calculate statistics with Learn and Derive phases only
#if VTK_5_4_OR_EARLIER
cs->SetLearn(true);
cs->SetDerive(true);
cs->SetAssess(false);
#else
cs->SetLearnOption(true);
cs->SetDeriveOption(true);
cs->SetAssessOption(false);
#endif
cs->Update();
前面代码部分对每对感兴趣的列的请求都是通过调用AddColumnPair()来指定的,所有二元算法都是这样做的。单变量算法会多次调用AddColumn()来明确地指定一组请求。然而,多变量过滤器的使用模式略有不同。为了给多变量统计算法的请求排队,应该调用SetColumnStatus()来打开感兴趣的列(并关闭以前选择的不再感兴趣的列)。一旦指定了所需的列集,就应该调用RequestSelectedColumns()。考虑表1中的例子,其中提到了个请求:{A,B,C}和{B,C,D}。清单2中的代码片段显示了如何为vtkPCAStatistics对象排队。
vtkPCAStatistics* pps = vtkPCAStatistics::New();
// Turn on columns of interest
ps->SetColumnStatus("A", 1);
ps->SetColumnStatus("B", 1);
ps->SetColumnStatus("C", 1);
ps->RequestSelectedColumns();
// Columns A, B, and C are still selected, so first we turn off
// column A so it will not appear in the next request.
ps->SetColumnStatus("A", 0);
ps->SetColumnStatus("D", 1);
ps->RequestSelectedColumns();
[Listing 2: An example of requesting multiple multi-variate analyses.]
并行统计算法构建全统计模型的目的之一是提高并行计算效率。在我们的方法中,处理器间通信和更新仅对主统计信息执行。从主要统计数据获得派生统计数据的计算通常是快速和简单的,并且只需在完成所有主要变量的并行更新后计算一次,无需通信。假设要评估的数据在参与计算的所有进程之间并行分布,因此在每个进程评估自己的驻留数据时不需要通信。
因此,在统计引擎的并行版本中,仅在学习阶段需要处理器间通信,而由于数据并行性,派生和评估都以令人尴尬的并行方式执行。这种设计与用于在VTK中实现并行的数据并行方法是一致的,尤其是在ParaView中。以下5个并行统计类目前在VTK中可用:
•vtkPDescriptiveStatistics
•vtkPCorrelativeStatistics
•vtkPContingencyStatistics
•vtkPMultiCorrelativeStatistics
•vtkPPCAStatistics
这些并行算法中的每一个都被实现为算法的相应串行版本的子类,并包含一个vtkMultiProcessController来处理处理器间通信。在每个并行统计类中,由于派生和评估阶段中固有的数据并行性,学习阶段是唯一行为发生变化的阶段(通过重新实现其虚拟方法)。并行算法的学习阶段执行两个主要任务:
1;通过执行超类的Learn代码计算本地数据的相关统计信息。
2. 如果需要并行更新(即进程数大于1),则执行必要的数据收集,并将本地统计信息聚合为全局统计信息。请注意,统计算法的并行版本可以使用与串行超类相同的语法。所有需要的是一个并行构建的VTK和一个版本的MPI安装在您的系统上。
8.7多维数据处理
许多信息可视化、科学、工程和经济问题都涉及隐式或显式多维数据。在文本分析领域,文档的语料库通常表示为一个矩阵(2D数组),该矩阵存储语料库中的每个术语(词)在每个文档中出现的次数。这种类型的术语频率数据可以扩展到更高的维度,例如在对公共Wiki编辑的分析中,将特定作者在特定日期使用术语的次数编码为3D张量。
在物理实验中,一系列测量通常会形成一个三维或多维张量,即在多个站点在多个时间进行的天气测量。在经济学中,包含股票代码、日期和时间范围的不同组合的股票价值变化的数据集也可以表示为3D张量。尽管这些例子起源于广泛不同的领域,但使用多维数组表示它们的数据使得从多线性代数中引入一组通用方法来进行分析成为可能。强大的算法,如单数值Decomposition、PARAFAC、DEDICOM和TUCKER可以用来找出数据中的相关性,否则这些相关性会对用户隐藏起来。
注意,在这些示例中,数据可能或多或少是“稀疏的”。在文本分析用例中,并非语料库中的每个术语都会在每个文档中使用;在科学实验中,仪器故障可能导致数据在不同的时间范围内未定义或为“零”。同样,股票定期进出市场,导致数据中出现未定义的点。任何表示多维数据的系统都必须能够显式地表示这些未定义的点,以提高内存和计算的效率,并确保计算的正确性。忽略未定义或“null”值的系统将不会产生正确的结果。
设计
为了应对上述挑战,vtkArray类及其派生类提供了存储和操作任意维的稀疏和密集数组的功能(图8-17)。首先注意,vtkArray和它的子类是完全独立于传统的VTK数组类型,如vtkDataArray, vtkIntArray,, vtkFloatArray,和其他数组类型派生自vtkAbstractArray。在未来的某个版本中,我们计划统一这两个层次结构。
在n维数组层次结构的顶端,vtkArray提供了对所有数组通用的方法和功能,无论存储值的类型或使用的存储类型如何。使用vtkArray,您可以:
•创建阵列的异构容器。图8-15将三维时间-作者期数据映射到张量进行文本分析图8-16将三维年站测量气象数据映射到张量进行分析
•实现不同类型数组之间的转换算法。
•实现不需要知道数组包含什么类型的值就可以修改数组结构的算法,例如矩阵转置算法。
vtkTypedArray< T>模板类派生自vtkArray,用于提供对存储在数组中的值的强类型访问,而忽略所使用的存储类型。使用vtktypearray <T>,您可以有效地操作包含特定类型(int, double, string等)的数组,而忽略数组数据的存储方式(密集,稀疏等)。
最后,VTK目前提供了vtktypearray <T>的两个具体衍生物,vtkDenseArray<T>和vtkSparseArray<T>,它们实现了特定的存储策略:
vtkDenseArray< T>使用单个连续内存块存储值,使用Fortran排序,以便与许多设计用于Fortran排序内存的线性代数库(如BLAS和LAPACK)兼容。vtkDenseArray< T>提供有效的0(1)值检索,最适合处理为数组中的每个位置定义良好的密集数据。vtkDenseArray<T>使用的内存与数组沿每个维度扩展的乘积成正比。
vtkSparseArray< T>当没有为数组中的每个位置定义数据时,使用稀疏坐标存储来有效地存储数据。每个非空值存储在一个由值及其坐标组成的无序列表中。单个'null'值用于表示数组的剩余内容。只要它足够稀疏,高维数据集就不可能使用vtkDenseArray<T>存储在内存中,可以使用vtkSparseArray<T>轻松操作。这是因为所使用的内存与数组中非空值的数量成正比,而不是与数组的大小成正比。
vtkDenseArray< T>和vtkSparseArray< T>存储类包括与VTK的设计,以提供良好的全方位性能在各种各样的用例,并为任意数量的维度。在实践中,可能存在这样的情况:自定义存储类可以以牺牲通用性为代价提供更好的性能,而vtkArray接口的设计就是考虑到这一点。用户可以通过派生vtktypearray <T>并实现了一些纯虚拟方法。自定义数组存储的假设用例可能涉及创建压缩行或压缩列存储,以便与操纵库集成图8-17 VTK n维数组类图8-18 vtkDenseArray<T>使用Fortran排序存储一个3x3矩阵。8.7处理多维数据201以其中一种格式提供了矩阵。自定义数组的另一个用例是创建一个只读的“过程性”数组,它封装了一个计算序列,比如斐波那契序列——这样的数组实际上不会存储任何信息,但可以用作其他计算的输入。
使用多维数组
您通过实例化所需的具体数组类(vtkDenseArray<T> vtkSparseArray<T>或类似)在您希望存储的值类型(int, double, string等)上创建多维数组,然后指定结果数组的范围(维度数和每个维度的大小):
// Creating a dense vector (1D array) of strings:
vtkDenseArray<vtkStdString>* vector =
vtkDenseArray<vtkStdString>::New();
vector->Resize(10);
// Creating a dense 10 x 20 matrix (2D array) of integers:
vtkDenseArray<int>* matrix = vtkDenseArray<int>::New();
matrix->Resize(10, 20);
// Creating a sparse 10 x 20 x 30 x 40 tensor
// (4D array) of floating-point values:
vtkArrayExtents extents;
Extents.SetDimensions(4);
extents[0] = 10;
extents[1] = 20;
extents[2] = 30;
extents[3] = 40;
vtkSparseArray<double>* tensor = vtkSparseArray<double>::New();
tensor->Resize(extents);
注意,vtkArray::Resize()方法已经被重载,因此您可以通过简单地指定每个维度的大小来轻松地创建一个、两个或三维数组。对于四个或更多维度,必须使用vtkArrayExtents helper类的实例来编码维度和范围的数量。
调整大小后,必须正确初始化新数组。这个过程将根据数组存储类型而变化-例如,vtkDenseArray<T>的内容将在调整大小后未定义,因此vtkDenseArray<T>提供了一个Fill()方法,可用于用单个值覆盖整个数组:
matrix->Fill(0);
在调整大小后,稀疏数组将完全为空(所有值将为未定义或'null'值),并且明确指定'null'值(每当调用者访问未定义的坐标集时返回的值)应该是什么总是一个好主意:
tensor->SetNullValue(0.0);
初始化数组后,下一步是使用SetValue()填充它:
// Overwrite vector[5] with "Hello World!":
vector->SetValue(5, "Hello, World!");
// Overwrite matrix[4, 3] with "22":
matrix->SetValue(4, 3, 22);
// Overwrite tensor[3, 7, 1, 2] with "1.5":
vtkArrayCoordinates
coordinates;
coordinates.SetDimensions(4);
coordinates[0] = 3;
coordinates[1] = 7;
coordinates[2] = 1;
coordinates[3] = 2;
tensor->SetValue(coordinates, 1.5);
请注意,与Resize()一样,SetValue()也有重载版本,可以处理一维、二维或三维数据,并且有一个辅助类——vtkArrayCoordinates——用于为高维数组的操作提供坐标。毫不奇怪,GetValue()用于从数组中检索数据:
// Access array value [5]:
vtkStdString vector_value = vector->GetValue(5);
// Access matrix value [4, 3]:
int matrix_value = matrix->GetValue(4, 3);
// Access tensor value [3, 7, 1, 2]:
double tensor_value = tensor->GetValue(coordinates);
SetValue()和GetValue()是由vtktypearray <T>提供的强类型方法,并且假设您提前知道存储在数组中的数据类型,或者因为您自己创建了数组,或者您使用SafeDownCast()从vtkArray转换为vtktypearray <T>对于某些特定的T.对于您正在使用未知类型的数组的情况,有SetVariantValue() / GetVariantValue()方法由vtkArray提供,允许您方便地设置和获取任何数组的值,无论类型如何,尽管有转换到和从变量值的开销:
// Print value [8] from any one-dimensional array
// to the console, regardless of type:
vtkArray* generic_array = /* Defined elsewhere */
cout << generic_array->GetVariantValue(8).ToString() << endl;
在这个例子中,GetVariantValue()返回一个vtkVariant对象,而vtkVariant::ToString()方法将底层值转换为字符串,而不考虑原始类型。类似地,你可以使用变量来洗牌数组中的数据,而不必知道它包含的数据类型:
// Swap values [3] and [7] within an array, regardless of array type:
vtkVariant temp = generic_array->GetVariantValue(3);
generic_array->SetVariantValue(3, generic_array->GetVariantValue(7));
generic_array->SetVariantValue(7, temp);
表现
尽管SetValue()和GetValue()为所有数组提供了易于理解的统一接口,而不管它们的存储类型如何,但这种方法的便利性带来了抽象上的损失。在接下来的内容中,我们将介绍一些用于改进与数组相关的代码性能的重要技术。
填充密集数组
您经常需要操作vtkDenseArray<T>作为一个简单的内存块,用于I/O操作或与其他库的互操作性。对于这些情况,vkdensearch <提供GetStorage()方法,该方法返回指向存储数组内容的内存块的指针。你可以使用这个指针将数组的(二进制)内容作为一个连续的块写入文件:
// Write the contents of a dense int array as binary data to a stream
void WriteDenseArray(vtkDenseArray<int>* array, ostream& stream)
{
stream.write(
reinterpret_cast<char*>(array->GetStorage()),
array->GetSize() * sizeof(int));
}
或者,您可以将内存块传递给执行密集数组计算的库,只要内存块(Fortran)中的值的顺序符合库的期望即可。只要可行,就应该尝试使用这种方法,因为它避免了在将数据传递给库并检索结果时对数据进行深度复制。
填充稀疏数组
回想一下,vtkSparseArray<使用非空值列表及其相应坐标在内部存储值。这意味着无论何时调用SetValue(), vtkSparseArray<必须首先确定这些坐标上的现有值是否已经存在。如果是,旧值将被替换为新值;否则,新值及其坐标将被追加到列表的末尾。这种对现有值的线性搜索使得SetValue()对于稀疏数组来说是一个开销很大的操作,而对于密集数组来说则是一个常数时间的操作。简单地使用SetValue()会使稀疏数组的创建慢得令人无法接受。
幸运的是,vtkSparseArray< T>提供AddValue()方法,该方法向内部列表添加值,而不执行对现有值的搜索,并在平摊常数时间内执行。这提供了出色的性能,但意味着调用者有责任避免使用同一组坐标多次调用AddValue()。在实践中,这意味着AddValue()应该只在从头开始填充数组时才在数组上使用(如果要实现创建新的稀疏数组的管道源,就会这样做)。永远不要对具有未知内容的数组(例如过滤器的输入)调用AddValue(),因为您有可能将具有重复坐标的值添加到数组的内部列表中(这是不允许的)。下面的代码演示了如何使用AddValue()高效地创建一个10000 x 10000的对角矩阵:
vtkSparseArray<double>* array = vtkSparseArray<double>::New();
array->Resize(10000, 10000);
array->SetNullValue(0.0);
for(vtkIdType i = 0; i != 10000; ++i)
{
array->AddValue(i, i, 1.0);
}
迭代
前面的示例演示了如何通过避免SetValue()方法的缺陷来有效地填充数组。然而,当使用GetValue()访问数组时也会出现类似的问题——因为vtkSparseArray在无序列表中存储非空值,所以每次调用GetValue()时都必须执行线性搜索,从而导致无法接受的缓慢性能。为了解决这个问题,VTK提供了另一种技术-迭代-只要满足某些条件,就可以在恒定时间内对密集和稀疏数组进行读写。使用迭代,我们可以:
•消除获取/设置稀疏数组值时线性查找的成本。
•仅访问稀疏数组中的非空值。
•在密集和稀疏数组之间使用一致的接口实现过滤器。
•实现对任意维度数据操作的过滤器。
为VTK多维数组提供的迭代接口通过将存储在数组中的值暴露为单个无序列表来工作。数组中的每个值在半开放范围内被分配一个索引[0,N],其中N是存储在数组中的非空值的数量,vtkArray和vtkTypedArray<T>类提供了“按索引”访问值的方法:SetValueN()、GetValueN()和GetCoordinatesN()。使用这些方法,您可以使用单个循环“访问”数组中的每个值,而不考虑数组存储的类型,也不考虑数组中的维数。例如,下面的代码有效地将整数数组中的每个值加1,而不知道它的维度,也不知道数组是稀疏的还是密集的:
vtkTypedArray<int>* array = /* Defined elsewhere */
for(vtkIdType n = 0; n != array->GetNonNullSize(); ++n)
{
array->SetValueN(n, array->GetValueN(n) + 1);
}
注意,我们使用GetValueN() / SetValueN()“访问”数组中的值的顺序是故意未定义的,上面的代码之所以有效是因为顺序不重要——不管底层数组的类型是密集的还是稀疏的,是一维的还是11维的,值[3]是在值[300]之前还是之后递增都无关紧要,只要两者最终以一致的方式修改即可。
虽然您不能控制访问值的顺序,但是您可以使用GetCoordinatesN()在遍历任何数组的内容时发现“您在哪里”,这对于大多数算法实现来说通常已经足够了。例如,下面的代码计算矩阵中每行值的和,并将结果存储在密集向量中。虽然我们以任意顺序访问矩阵值,但我们可以使用每个值的坐标作为常量时间查找来累积结果向量中的值:
vtkTypedArray<double>* matrix = /* Defined elsewhere */
vtkIdType row_count = matrix->GetExtents()[0];
vtkTypedArray<double>* vector = vtkDenseArray<double>::New();
vector->Resize(row_count);
vector->Fill(0.0);
for(vtkIdType n = 0; n != matrix->GetNonNullSize(); ++n)
{
vtkArrayCoordinates coordinates;
matrix->GetCoordinatesN(n, coordinates);
vtkIdType row = coordinates[0];
vector->SetValue(row, vector->GetValue(row) + matrix->GetValueN(n));
}
缺乏特定的迭代顺序乍一看似乎是有限制的,但是可以编写大量的算法来在此约束下工作,从而受益于恒定时间查找、维度和存储类型独立性。
阵列数据
现在我们已经可以创建和操作多维数组了,现在是时候将它们移动到
VTK管道。像vtkAbstractArray一样,vtkArray不是vtkDataObject,所以它不能被管道直接使用。相反,VTK提供vtkArrayData作为数组的容器,vtkArrayDataAlgorithm可用于实现vtkarray数据源和过滤器:
课题名称
•vtkDiagonalMatrixSource -产生稀疏或密集的任意大小的矩阵,与用户指定的对角线,超对角线和亚对角线的值。
206信息可视化
•vtkBoostRandomSparseArraySource -产生稀疏矩阵与任意大小和维数。提供单独的参数来控制随机值和随机稀疏模式的生成。
•vtkTableToSparseArray -将包含坐标和值的vtkTable转换为任意维度的稀疏数组
数组算法
•vtkAdjacencyMatrixToEdgeTable -转换密集矩阵到vtkTable适合使用与vtkTableToGraph。输入矩阵中的维度标签被映射到输出表中的列名。
•vtkArrayVectorNorm -为稀疏双矩阵中的每个列向量计算l -范数。
•vtkcosinessimilarity -将矩阵中的每一行或列视为向量,并计算每对向量之间的点积相似性,生成适合与vtkTableToGraph一起使用的vtkTable。注意:在5.4之后的VTK版本中,vtkcosinessimilarity已被重命名为vtkDotProductSimilarity,以更好地描述其功能
•vtkDotProductSimilarity -将矩阵中的每一行或列视为向量,并计算每对向量之间的点积相似性,生成适合与vtkTableToGraph一起使用的vtkTable。
•vtkBoostLogWeighting -用p+1的自然对数替换数组中的每个值p。一个过滤器的好例子,它可以处理包含任意维数的任何数组。
•vtkMatricizeArray -转换稀疏的任意维度的双数组稀疏矩阵。例如,一个ix j x k张量可以转换成一个ix jk, j x ik,或ij x k矩阵。
•vtkNormalizeMatrixVectors -归一化矩阵中的行向量或列向量。一个过滤器的好例子,它可以有效地处理稀疏和密集的输入矩阵。这是一个处理行向量或列向量的过滤器的好例子。
•vtktransposemmatrix -计算矩阵的转置。
本书为英文翻译而来,供学习vtk.js的人参考。