可视化工具箱使用一种非常通用的执行机制。过滤器分为两个基本部分:算法和执行对象。算法对象,其类派生自vtkAlgorithm,负责处理信息和数据。执行对象的类派生自vtkExecutive,它负责告诉算法何时执行以及要处理哪些信息和数据。过滤器的执行组件可以独立于算法组件创建,允许自定义管道执行机制,而无需修改核心VTK类。
过滤器产生的信息和数据存储在一个或多个输出端口中。一个输出端口对应于过滤器的一个逻辑输出。例如,产生彩色图像和相应的二进制掩模图像的滤波器将定义两个输出端口,每个端口保存其中一个图像。与管道相关的信息存储在每个输出端口的vtkInformation实例中。输出端口的数据存储在从vtkDataObject派生的类的实例中。
过滤器使用的信息和数据通过一个或多个输入端口检索。一个输入端口对应于过滤器的一个逻辑输入。例如,字形过滤器将为字形本身定义一个输入端口,另一个输入端口提供指定字形位置的几何形状。输入端口存储输入连接,这些连接引用提供信息和数据的其他过滤器的输出端口。每个输入连接提供一个数据对象及其从连接到的输出端口获得的相应信息。由于连接是通过逻辑端口存储的,而不是存储在流经这些端口的数据中,因此在建立连接时不需要知道数据类型。这对于创建源为读取器的管道特别有用,读取器在读取文件之前不知道其输出数据类型。
图15-1从两个角度描述了过滤器的布局。顶部的图表显示了从算法对象查看的过滤器。过滤器的这个视图独立于管道,并且包含有关算法接口的所有信息。第二个图显示了从执行对象查看的过滤器。过滤器的这个视图独立于算法的细节,并包含有关管道连接和通过它们发送的数据的所有信息。
15.1信息对象
信息对象是整个VTK管道中用来保存各种信息的基本容器。所有信息对象都是vtkInformation类的实例。它们是异构的键到值映射,其中键的类型决定了值的类型。以下是使用信息对象的位置的枚举。
•管道信息对象保存管道执行的信息。它们存储在vtkExecutive或子类的实例中,并可通过方法vtkExecutive::GetOutputInformation()访问。每个输出端口有一个管道信息对象。它包含一个指向相应端口上的输出vtkDataObject的条目(如果已创建)。图15-1从(a)算法的角度和(b)执行人员的角度看到的过滤器布局。(a)算法对象的过滤器视图(b)执行对象的过滤器视图输入端口输出端口算法算法信息参数执行输出端口信息…输入端口信息…数据对象数据对象输出端口生产者消费者执行执行算法输出算法管道信息…输入管道信息…输入连接另一个Filter vtkDataObject包含一个指向其对应的管道信息对象的指针,可通过vtkDataObject::GetPipelineInformation()访问。管道信息对象还保存了当过滤器执行并生成输出时将填充数据对象的内容的信息。所包含的实际信息由输出数据类型和所使用的执行模型决定。输入连接的管道信息对象可以通过方法vtkExecutive::GetInputInformation()访问,它们是连接输入端口的输出端口上的管道信息对象。
•端口信息对象保存有关输出端口产生和输入端口使用的数据类型的信息。它们由vtkAlgorithm的实例存储。每个输入端口有一个输入端口信息对象,每个输出端口有一个输出端口信息对象。它们可以通过vtkAlgorithm::GetInputPortInformation()和vtkAlgorithm::GetOutputPortInformation()方法访问。端口信息对象通常由vtkAlgorithm的子类创建和填充,以指定过滤器的接口。
•请求信息对象保存有关发送给执行器或算法的特定请求的信息。有一个条目表明正在发送什么请求,可能还有其他条目提供有关特定请求的其他详细信息。这些信息对象不能通过任何公共方法访问,而是传递给实现请求的ProcessRequest()方法。
•数据信息对象保存当前存储在vtkDataObject中的信息。每个数据对象中有一个数据信息对象,可通过vtkDataObject::GetInformation()访问。实际包含的信息由数据对象类型决定。
•算法信息对象保存有关vtkAlgorithm实例的信息。每个算法对象有一个算法信息对象,可通过vtkAlgorithm::GetInformation()访问。实际包含的信息由算法对象类型决定。
15.2管道执行模型
基本的管道更新机制是请求。请求是基本的管道操作(或“管道传递”),它通常要求通过管道传播某些信息。执行模型是由特定执行人员定义的一组请求。
请求是由过滤器的执行对象生成的,由于某些用户调用,过滤器的算法显式地要求对其进行更新。例如,当调用写入器的Write()方法时,算法对象要求执行器更新管道,并通过调用->GetExecutive()-> update()来执行写入器。为了使管道更新,可能会通过管道发送多个请求。
请求被实现为信息对象。有一个类型为vtkInformationRequestKey的键指定请求本身。这个键通常由管理人员的类定义。关于请求的其他信息也可以存储在请求信息对象中。请求由每个过滤器的执行人员通过管道传播。在执行器上调用vtkExecutive::ProcessRequest()方法并给出请求信息对象。此方法由每个执行人员实现,并负责在其认为合适的情况下完成请求。只有在为提供其输入的过滤器满足了请求之后,才能满足对过滤器的许多请求。对于这些请求,执行器将请求传递给这些上游过滤器的执行器,然后自己处理请求。
管理管道执行一个执行器经常请求它的算法对象帮助完成一个请求。它通过调用vtkAlgorithm::ProcessRequest()方法将请求发送给算法对象。该方法由所有算法实现,并负责处理请求。输入和输出管道信息对象作为方法的参数提供。该算法必须仅使用自己的过滤器参数设置和给定的管道信息对象来处理请求。一个算法不允许向它的执行器请求任何额外的信息。
图15-2显示了请求通过管道发送时的典型路径。通常,请求起源于管道末端的消费者。它由执行人员通过管道发回。每个高管都要求自己的算法帮助处理请求。
图15-2请求通过管道的路径例如,假设消费者(在最右边)只需要该数据的单个片段(例如,4个片段中的1个);还假设生产者(最左边)是一个可以将其数据划分为多个部分的读取器。消费者向上游传递此请求,并继续向上游传递(通过执行者),直到到达可以满足请求的生产者。当读取器算法被要求获取数据片段时,它提供它,并将新数据(连同它是4块中的1块的信息)传递回管道。它在到达发出请求的消费者时停止。
15.3管道信息流
信息流通过VTK管道一个过滤器的时间。在处理请求时,过滤器可以修改随请求提供的管道信息。对输出管道信息的修改称为下游流,因为信息沿着管道向消费者发送,并在映射器处终止。类似地,对输入管道信息的修改被称为上游流,因为信息沿着管道向生产者发送,在管道的开始处与源终止。
执行人员定义的每个请求都要求通过管道传播某些信息。要求过滤器向下游发送信息的请求称为下游请求,而要求过滤器向上游发送信息的请求称为上游请求。例如,可以向图像处理过滤器提供有关图像几何形状信息的下游请求。如果过滤器不修改这个几何图形,它可以简单地将图像原点和间距(使用vtkDataObject:: origin()和vtkDataObject:: spacing()键)从其输入管道信息复制到其输出管道信息。类似地,可以向过滤器提供有关满足消费者所需的图像区域的信息的上游请求。执行ProcessRequest() ProcessRequest()算法图15-2请求通过管道发送的路径例如,假设消费者(在最右边)只需要该数据的单个片段(例如,4个片段中的1个);还假设生产者(最左边)是一个可以将其数据划分为多个部分的读取器。消费者向上游传递此请求,并继续向上游传递(通过执行者),直到到达可以满足请求的生产者。当读取器算法被要求获取数据片段时,它提供它,并将新数据(连同它是4块中的1块的信息)传递回管道。它在到达发出请求的消费者时停止。执行执行ProcessRequest() ProcessRequest()算法算法ProcessRequest() ProcessRequest() ProcessRequest()开始请求结束请求过滤器可以简单地将请求的图像范围从它的输出管道信息复制到它的输入管道信息中,或者如果它需要额外的输入来完成它的计算,它可以改变范围。
关于过滤器正在处理的数据的信息通常由过滤器实现来处理,因为它不能正常工作。一些算法可能希望通过管道发送额外的信息。例如,映射器可能决定它需要有关源可用时间步数的信息。此映射器可以通过覆盖虚拟方法并请求附加信息来修改默认信息请求。此扩展请求在不知道中间过滤器的情况下传播到源。然后,源可以自由地生成信息,如果它这样做了,管道将向下游传播它。
15.4信息对象接口
vtkInformation类提供了一个异构的键值映射。这个映射的键是抽象类vtkInformationKey的实例。键对象的地址用于存储和检索映射中的值,键对象的类型用于解释这些值。键由返回它的静态类方法命名。键本身提供了用于存储和检索键值的接口。例如,考虑vtkDataObject定义的这些信息键:
vtkInformationStringKey* FIELD_NAME();
vtkInformationDoubleVectorKey* ORIGIN();
我们可以创建要使用的vtkInformation实例。
vtkSmartPointer<vtkInformation> info =
vtkSmartPointer<vtkInformation>::New();
FIELD_NAME是访问“String”类型值的键:
vtkInformationStringKey* FIELD_NAME = vtkDataObject::FIELD_NAME();
FIELD_NAME->Has(info); // returns 0
FIELD_NAME->Set(info, "ABC"); // sets info{FIELD_NAME} to "ABC"
FIELD_NAME->Has(info); // returns 1
FIELD_NAME->Get(info); // returns a pointer to "ABC"
FIELD_NAME->Remove(info); // removes info{FIELD_NAME}
FIELD_NAME->Has(info); // returns 0
ORIGIN是访问“DoubleVector”类型值的键:
double origin[3] = {1,2,3};
vtkInformationDoubleVectorKey* ORIGIN = vtkDataObject::ORIGIN();
ORIGIN->Has(info); // returns 0
ORIGIN->Set(info, origin, 3); // sets info{ORIGIN} to {1,2,3}
ORIGIN->Has(info); // returns 1
ORIGIN->Get(info); // returns a pointer to {1,2,3}
ORIGIN->Get(info, origin); // stores {1,2,3} in origin
ORIGIN->Length(info); // returns 3
ORIGIN->Remove(info); // removes info{ORIGIN}
ORIGIN->Has(info); // returns 0
由于访问接口是由密钥提供的,可以在不修改类vtkInformation本身的情况下定义新的密钥类型。但是,语法有些不直观,因为没有修改键对象,而是修改给定的信息对象。为了简化在一般情况下对信息对象的访问,vtkInformation为VTK中定义的大多数键类型提供了一个方便的接口。上面的例子可以这样写
vtkInformationStringKey* FIELD_NAME = vtkDataObject::FIELD_NAME();
info->Has(FIELD_NAME); // returns 0
info->Set(FIELD_NAME, "ABC"); // sets info{FIELD_NAME} to "ABC"
info->Has(FIELD_NAME); // returns 1
info->Get(FIELD_NAME); // returns a pointer to "ABC"
info->Remove(FIELD_NAME); // removes info{FIELD_NAME}
info->Has(FIELD_NAME); // returns 0
double origin[3] = {1,2,3};
vtkInformationDoubleVectorKey* ORIGIN = vtkDataObject::ORIGIN();
info->Has(ORIGIN); // returns 0
info->Set(ORIGIN, origin, 3); // sets info{ORIGIN} to {1,2,3}
info->Has(ORIGIN); // returns 1
info->Get(ORIGIN); // returns a pointer to {1,2,3}
info->Get(ORIGIN, origin); // stores {1,2,3} in origin
info->Length(ORIGIN); // returns 3
info->Remove(ORIGIN); // removes info{ORIGIN}
info->Has(ORIGIN); // returns 0
键实例可以由类定义,通过创建一个静态方法命名键,并使用vtkInformationKeyMacro或vtkInformationKeyRestrictedMacro实现该方法。后一种形式可用于其构造函数接受附加参数的键类型,该参数指定了对允许的值的某些限制。例如,vtkDataObject实现了它的静态方法
FIELD_NAME()和ORIGIN()使用以下代码:
#include "vtkInformationStringKey.h"
#include "vtkInformationDoubleVectorKey.h"
vtkInformationKeyMacro(vtkDataObject, FIELD_NAME, String);
vtkInformationKeyRestrictedMacro(vtkDataObject, ORIGIN, DoubleVector,
3);
第一行声明FIELD_NAME是一个类型为“String”的键,第二行声明ORIGIN是一个类型为“DoubleVector”的键,其长度必须始终为3。通过添加前缀“vtkInformation”和后缀“Key”,将密钥类型名称转换为信息密钥类名称。在上面的例子中,FIELD_NAME的键类型是vtkInformationStringKey。类作者必须包括键定义中使用的每个键类型的头文件。
如果使用大多数调试器中可用的“监视”特性在特定信息条目更改时中断,则可以简化调试管道执行和算法实现。由于大多数信息项都没有存储在实例变量中,因此获取要监视的正确内存地址并不明显。一些信息键类型提供了一个受保护的方法GetWatchAddress(),该方法返回要监视的正确地址。每个键实例都由一个全局变量存储,该全局变量的名称由定义键的类的名称构造,后跟一个下划线,然后是键的名称。例如,为了观察管道信息对象中的WHOLE_EXTENT条目,可以从调试器中的以下表达式获得内存地址。
vtkStreamingDemandDrivenPipeline::WHOLE_EXTENT()
->GetWatchAddress(info)
在本例中,参数“info”可能是调试器停止程序所在范围内的一个局部变量。当整个范围发生变化时,返回的地址可用于自动中断。
15.5标准执行器
VTK提供了一些标准的非常强大的高管。大多数应用程序都可以使用本文介绍的执行器之一实现所需的管道更新行为。这个执行器实现了一个基本的需求驱动的隐式执行模型。随着参数的变化,每个过滤器都保持一个修改时间。
执行器跟踪其过滤器的每个输出端口上的信息和数据最后生成的时间。当过滤器的输出被请求且已过期时,将按需执行过滤器。以下请求由该执行人员定义。所有这些都是下游请求。为每个请求提供了一个默认实现,简化了通常情况下的过滤器。从不更改管道信息的过滤器中删除了维护管道信息的负担。
ComputePipelineMTime请求为过滤器计算管道修改时间。这是所有过滤器参数及其输入的最高(最近)修改时间。计算的时间将与从过滤器请求的任何输出的时间进行比较,以确定它是否是最新的。由于无论执行什么过滤器,每次更新时都要通过整个管道发送请求,因此它必须是快速的。实现以一种特殊的方式进行了优化:请求由ComputePipelineMTime()方法实现,而不是ProcessRequest()方法。请求所需的公共信息直接作为参数传递给方法。希望替换此请求的默认实现的执行器和算法都应该覆盖ComputePipelineMTime()方法。此请求的特殊设计纯粹是对其性能关键性质的优化,不应用作设计其他请求的模型。
REQUEST_DATA_OBJECT请求创建一个vtkDataObject,但不填充,并存储在所有输出端口的输出管道信息中。数据对象稍后将由REQUEST_DATA填充。默认实现由vtkDemandDrivenPipeline提供,它使用相应输出端口信息中由vtkDataObject::DATA_TYPE_NAME()键指定的数据对象类型。根据输入类型和参数设置改变其输出类型的过滤器必须实现此请求并计算适当的类型。应该使用键vtkDataObject::DATA_OBJECT()将数据对象存储在输出管道信息中。
REQUEST_INFORMATION请求关于每个输出端口中数据对象的输出管道信息。这是关于将存储在数据对象中的内容的信息,而不是当前数据对象中的内容。该信息的一个示例是图像或统一网格的原点和间距,分别使用键vtkDataObject:: origin()和vtkDataObject:: spacing()存储。默认实现由vtkDemandDrivenPipeline提供,它通过调用vtk-将每个输出数据对象的数据信息复制到相应的管道信息DataObject: CopyInformationToPipeline()。然后,如果存在任何输入连接,则将信息从第一个连接复制到每个输出管道信息对象。没有输入的源或从输入中更改此信息的过滤器必须实现此请求以计算正确的信息。
REQUEST_DATA请求用请求到达的输出端口上的实际输出数据填充输出数据对象。这个输出端口号是由请求信息对象中的键vtkExecutive::FROM_OUTPUT_PORT()给出的。必须填充请求中给定的输出端口中的数据对象,但也可以选择填充其他输出端口中的数据对象。(大多数过滤器总是填充所有输出。)当处理REQUEST_DATA时,vtkDemandDrivenPipeline执行以下步骤。
•发送一个vtkDemandDrivenPipeline::REQUEST_DATA_NOT_GENERATED()请求到过滤器的算法部分。请求包含原始REQUEST_DATA中的所有信息,包括vtkExecutive::FROM_OUTPUT_PORT()键。不总是填充所有输出的过滤器必须实现此请求,以在数据不填充的所有输出端口的管道信息中存储值为1的vtkDemandDrivenPipeline::DATA_NOT_GENERATED()。这告诉执行人员,这些输出将不会由这个REQUEST_DATA生成。大多数过滤器可以忽略此请求。
•对于管道信息不包含vtkDemandDrivenPipeline::DATA_NOT_GENERATED()标记的输出端口,通过调用vtkDataObject::PrepareForNewData来初始化输出数据对象。这些对象以前是由REQUEST_DATA_OBJECT创建的,并通过此步骤重新初始化为空状态。先前由REQUEST_INFORMATION设置并对应于每个数据对象的管道信息通过调用vtkDataObject::CopyInformationFromPipeline复制到对象的数据信息中。
•在算法对象上调用StartEvent,清除AbortExecute标志,并将进度更新为0。
•发送REQUEST_DATA给算法。所有算法必须实现此请求,以填充未被vtkDemandDrivenPipeline::DATA_NOT_GENERATED()标记的端口的输出管道信息中的数据对象。
•如果没有设置AbortExecute标志,则将进度更新为1,然后始终调用算法对象上的EndEvent。
•对于管道信息不包含vtkDemandDrivenPipeline::DATA_NOT_GENERATED()标记的输出端口,输出数据对象被标记为通过调用vtkDataObject::DataHasBeenGenerated()生成的。
•从所有输出管道信息中删除vtkDemandDrivenPipeline::DATA_NOT_GENERATED()标记。
•对于管道信息包含vtkDemandDrivenPipeline::RELEASE_DATA()标记的任何输入连接,通过调用vtkDataObject:: RELEASE_DATA()来释放数据。这个默认的事件序列处理过滤器执行中与管道相关的大部分部分。算法实现只需要处理实际数据并计算从输入到输出转换的数据的相关信息。
vtkStreamingDemandDrivenPipeline
这个执行器是vtkDemandDrivenPipeline的子类,它为管道添加了流能力。它实现了传统的VTK管道更新规则,并且是分配给算法的默认执行器,而不是由用户明确给出的。
除了vtkDemandDrivenPipeline定义的请求之外,下面的请求是由这个执行者定义的。除了REQUEST_UPDATE_EXTENT是上游请求外,它们都是下游请求。
REQUEST_INFORMATION是从vtkDemandDrivenPipeline定义的扩展而来的。它询问有关存储在每个输出端口的管道信息中的可用数据量的信息。在结构化数据的情况下,这将是整个范围,由vtkStreamingDemandDrivenPipeline::WHOLE_EXTENT()存储。在非结构化数据的情况下,这将是整个数据集的最大碎片数量和边界框,分别由vtkStreamingDemandDrivenPipeline::MAXIMUM_NUMBER_OF_PIECES()和vtkStreamingDemandDrivenPipeline::WHOLE_BOUNDING_BOX()存储。对于可以产生带有时间的数据的过滤器,信息包括关于可用的离散时间步数或由vtkStreamingDemandDrivenPipeline::TIME_STEPS()或vtkStreamingDemandDrivenPipeline::TIME_RANGE()分别存储的连续时间范围的信息。默认实现由vtkStreamingDemandDrivenPipeline提供,它首先从当前输出中的数据对象复制此信息,然后从第一个输入连接(如果存在)复制此信息。如果端口上的管道信息没有一个键vtkStreamingDemandDrivenPipeline::UPDATE_EXTENT_INITIALIZED()存储一个非零值,vtkStreamingDemandDrivenPipeline将初始化更新范围到整个范围。如果没有使用者请求特定的更新范围,这将导致处理所有数据。
REQUEST_UPDATE_EXTENT请求生成存储在输入管道信息中的给定输出更新范围所需的输入更新范围。要满足的输出更新范围必须从请求到达的端口上的输出管道信息中获得。这个端口号由请求信息对象中的vtkExecutive::FROM_OUTPUT_PORT()给出。更新范围存储使用结构化数据的vtkStreamingDemandDrivenPipeline::UPDATE_EXTENT()和非结构化数据的vtkStreamingDemandDrivenPipeline::UPDATE_PIECE_NUMBER(), vtkStreamingDemandDrivenPipeline::UPDATE_NUMBER_OF_PIECES()和vtkStreamingDemandDrivenPipeline::UPDATE_NUMBER_OF_GHOST_LEVELS()。如果过滤器需要它从输入连接请求的确切范围才能正确执行,它必须在该连接的输入管道信息中存储值为1的键vtkStreamingDemandDrivenPipeline::EXACT_EXTENT()。时间请求使用vtkStreamingDemanDrivenPipeline::UPDATE_TIME_STEPS()存储。默认实现由vtkStreamingDemandDrivenPipeline提供,它将从输出管道信息复制更新范围到所有输入连接。如果输出是非结构化数据,而输入连接是结构化数据,则使用键vtkStreamingDemandDrivenPipeline::EXTENT_TRANSLATOR()存储在输出管道信息中的区段转换器。
REQUEST_UPDATE_EXTENT_INFORMATION请求关于将在更新范围内生成的数据的信息。这是一个高级特性,用于帮助管道进行额外的分析,以选择如何执行某些过滤器。
REQUEST_DATA是从vtkDemandDrivenPipeline定义的扩展而来的。它通过为算法提供多次执行请求的选项来支持流,并对其输入请求不同的更新范围。当流过滤器第一次接收到
它只存储它将处理的第一个输入数据的更新范围。然后,当它接收到REQUEST_DATA时,它处理当前片段并在请求信息对象本身中存储值为1的键vtkStreamingDemandDrivenPipeline:: continue_execution()。这告诉vtkStreamingDemandDrivenPipeline发送额外的REQUEST_UPDATE_EXTENT和REQUEST_DATA对。过滤器通过每次一个请求和处理输入数据的剩余部分来响应这些后续请求。在处理完最后一部分之后,它从请求信息对象中删除了vtkStreamingDemandDrivenPipeline:: continue_execution()标记。
vtkCompositeDataPipeline
这个执行器是vtkStreamingDemandDrivenPipeline的子类,它增加了对处理复合数据集的支持。复合数据集是由其他数据集组成的数据集,例如多块数据集,AMR(自适应网格细化)数据集。该执行器支持识别复合数据集和不识别复合数据集的算法。不能识别复合数据集的算法需要支持所有可以作为输入复合数据集一部分的数据集类型,否则将在运行时抛出不兼容的输入类型错误。
对于复合数据集感知算法,即那些表明它们可以接受vtkCompositeDataSet或其任何子类作为输入数据集的算法,该执行器的行为完全类似于vtkStreamingDemandDrivenPipeline。只有在处理非复合感知算法时才会出现额外的复杂性。由于复合数据集的性质,通常可以通过简单地对输入复合数据集中的每个数据集执行非复合感知过滤器,然后将每次执行产生的结果组合到结构与输入相似的输出复合数据集中,从而获得正确的结果。这正是vtkcompositedatappipeline处理这种算法的方式。
REQUEST_DATA被扩展为在一个循环中调用超类vtkStreamingDemandDrivePipeline上的REQUEST_DATA,从输入复合数据集中传递一个不同的非复合块作为当前输入,然后在一个结构类似于输入的复合数据集中收集结果。
15.6选择默认执行器
vtkAlgorithm子类可以覆盖CreateDefaultExecute()来创建适合该算法的执行。默认情况下,使用vtkStreamingDeamandDrivenPipeline作为执行。可以使用vtkAlgorithm::SetDefaultExecutivePrototype()来设置执行人员默认使用的原型。例如,如果应用程序处理复合数据集,则需要将执行器更改为vtkcompositedatappipeline,以便可以使用非复合感知过滤器。
本书为英文翻译而来,供学习vtk.js的人参考。