TTK(Topology ToolKit)

奇怪的概念

TTK和VTK

  TTK(The Topology ToolKit)是一个用于拓扑数据分析的开源库,专注于处理和分析复杂数据集中的拓扑结构。它提供了各种算法和工具,用于从数据集中提取和分析拓扑特征。TTK 是一个功能强大的库,能够处理大规模的数据集,并用于科学计算和可视化。

  VTK(Visualization Toolkit)是一个开源的库,用于科学数据的可视化。VTK 提供了广泛的功能,包括数据处理、数据可视化和图形渲染。它是科学计算和可视化领域中非常重要的工具之一。

  • TTK 是一个拓扑分析工具包,专注于从复杂数据集中提取和分析拓扑特征。

  • VTK 是一个科学数据可视化工具,提供数据处理和渲染功能。

  • TTK 使用 VTK 提供的基础设施来实现高级拓扑分析,VTK 则通过 TTK 扩展了其功能以支持更多的数据分析和可视化应用。

过滤器

  简单的说就是一个程序(好像是对象),对输入进行处理,变成输出。

  在 VTK(Visualization Toolkit)中,过滤器是一种处理数据的对象,它可以对输入数据进行操作或转换,并生成输出数据。可以将其视为数据处理的“管道”或“处理单元”。每个 VTK 过滤器通常执行一个具体的任务,比如数据转换、数据处理、数据分析等。

vtk module

  方便编译用的,说明当前模块的依赖项,使得在编译的时候只需要编译依赖的即可。

  vtk.module 文件 是描述一个 VTK 模块的配置文件,定义了模块的基本信息、依赖关系和构建要求。在该文件中,包含了模块的名称、版本号、依赖的其他模块,以及构建时需要的编译标志。

1
2
3
4
5
6
NAME
VTK::FiltersSources

DEPENDS
VTK::CommonDataModel
VTK::CommonCore

这个文件告诉构建系统:

  • 模块名是 VTK::FiltersSources
  • 这个模块依赖于两个其他模块:VTK::CommonDataModelVTK::CommonCore

vtk.module 文件中的关键部分

  • NAME:表示模块的名称。通常模块名称与其所在的路径相关联。
  • DEPENDS:列出该模块依赖的其他模块。这些模块需要在编译时一同加载。
  • PRIVATE_DEPENDS:列出在模块内部使用的依赖模块(这些依赖不会暴露给模块的使用者)。

  VTK module 是一个用于管理 VTK 代码依赖、组织和编译过程的模块化系统,通过 vtk.module 文件来定义模块的依赖和功能。TTK module类似。

三角化对象

  把复杂的几何对象转化为便于计算的三角化对象。

  三角化对象(ttk::Triangulation)用于表示输入数据集(vtkDataSet)的几何和拓扑结构。具体来说,它将输入的几何对象(例如点、边、面等)转化为一种便于计算的格式,即三角网格。这个三角化对象可以用于后续的几何和拓扑分析,如查找邻居、顶点之间的关系等。

  三角化对象的作用就是将复杂的几何结构或数据集分割成较小的三角形或三角面片。这些分割后的三角形或面片构成了三角化对象。在数据处理的过程中,通过操作这些三角化对象,能够简化并加速几何计算,如插值、网格处理、以及其他几何分析。

输入对象、输入数据集、输入数组

  数据在不同层次上的组织方式和作用范围。

  输入对象 包含了 输入数据集输入数组。输入对象(如 .vti 文件)描述了整体的数据结构,而输入数据集描述了网格点及其连接关系,输入数组则存储了与这些网格点或单元关联的具体数值数据。

  .vti文件就属于输入对象。它是一个 VTK Image Data 格式的文件,用于存储规则网格数据(也称为结构化网格数据)。这种格式存储的是具有规则结构的三维网格数据,通常包括每个网格点的坐标和网格点或网格单元的相关属性(如密度、温度、速度等)。

  在VTK中,vtkDataObject 是所有数据对象的基类,vtkDataSet 是其中一种常见的子类,表示具体的数据集形式,如网格数据、图像数据等。.vti 文件 就是一个 输入对象,属于 vtkImageData 类型,它是 vtkDataSet 的子类。

  输入数据集是指输入对象中的几何和拓扑结构信息,它描述了数据的空间分布和组织形式。VTK支持多种类型的数据集,例如:

  • vtkImageData(结构化网格,规则网格,如 .vti 文件)
  • vtkUnstructuredGrid(非结构化网格)
  • vtkPolyData(多边形数据)
  • vtkRectilinearGrid(直角网格)

这些数据集对象定义了点(顶点)和单元(如三角形、四边形)的空间位置以及它们之间的连接关系,但不包含具体的数值数据(如标量场或矢量场),这些数值数据由输入数组表示。

  输入数组表示存储在数据集中的具体数值信息,如标量场或矢量场。每个数组可能包含与数据集的点或单元关联的不同属性(如每个顶点的温度或每个单元的压力)。在 VTK 中,这些数组可以是:

  • 点数据数组(PointData):每个点(顶点)的属性。
  • 单元数据数组(CellData):每个单元(如三角形、四边形等)的属性。

.vti 文件中,通常包含了一个或多个输入数组,用来存储与每个网格点(或单元)相关的属性值,例如密度、压力、温度等。代码中使用的 GetInputArrayToProcess 方法就是为了获取这样的一个数组。

ttkhelloworld.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
/// TODO 4: 提供你的信息并**更新**文档(特别是当输入数组需要用标准的 VTK 函数 SetInputArrayToProcess() 指定时,更新顺序约定)。
///
/// \ingroup vtk
/// \class ttkHelloWorld
/// \author 这里填上你的名字 <你的邮箱地址>
/// \date 填上日期
///
/// \brief TTK 的 VTK 过滤器,封装了 ttk::HelloWorld 模块。
///
/// 该 VTK 过滤器使用 ttk::HelloWorld 模块来对输入 vtkDataSet 中定义的输入点数据数组进行数据值的平均计算。
///
/// \param Input vtkDataSet。
/// \param Output vtkDataSet。
///
/// 该过滤器可以像其他 VTK 过滤器一样使用(例如通过调用 SetInputData(), Update(), GetOutputDataObject() 的顺序)。
///
/// 输入数据数组需要通过标准的 VTK 调用 vtkAlgorithm::SetInputArrayToProcess() 来指定,参数如下:
/// \param idx 0(固定值:算法所需的第一个数组)
/// \param port 0(固定值:第一个端口)
/// \param connection 0(固定值:第一个连接)
/// \param fieldAssociation 0(固定值:点数据)
/// \param arrayName(动态值:输入数组的字符串标识符)
///
/// 参见相应的独立程序使用示例:
/// - standalone/HelloWorld/main.cpp
///
/// 参见相关的 ParaView 示例状态文件,以了解 VTK 管道中使用的示例。
///
/// \sa ttk::HelloWorld
/// \sa ttkAlgorithm

#pragma once

// VTK 模块
#include <ttkHelloWorldModule.h>

// VTK 包含文件
#include <ttkAlgorithm.h>

/* 关于包含 VTK 模块的说明
*
* 每个包含头文件的 VTK 模块都需要在该模块的 vtk.module 文件中指定,放在 DEPENDS 或 PRIVATE_DEPENDS(如果头文件仅包含在 cpp 文件中)部分。
*
* 为了找到相应的模块,检查头文件在 VTK 源代码中的位置。VTK 模块名称是由头文件的路径组成的。你也可以在与头文件位于相同目录的 vtk.module 文件中找到模块名称。
*
* 例如,vtkSphereSource.h 位于目录 VTK/Filters/Sources/ 中,因此它相应的 VTK 模块名称是 VTK::FiltersSources。在这种情况下,vtk.module 文件需要扩展为
*
* NAME
* ttkHelloWorld
* DEPENDS
* ttkAlgorithm
* VTK::FiltersSources
*/

// TTK 基础包含文件
#include <HelloWorld.h>

class TTKHELLOWORLD_EXPORT ttkHelloWorld
: public ttkAlgorithm // 我们继承自泛型 ttkAlgorithm 类
,
protected ttk::HelloWorld // 并且我们继承自基础类
{
private:
/**
* TODO 5: 将所有过滤器参数仅作为私有成员变量添加,并在这里初始化。
*/
std::string OutputArrayName{"AveragedScalarField"}; // 输出数组名称,初始化为 "AveragedScalarField"

public:
/**
* TODO 6: 通过 vtk 宏自动生成过滤器参数的 getter 和 setter。
*/
vtkSetMacro(OutputArrayName, const std::string &); // 设置输出数组名称的函数
vtkGetMacro(OutputArrayName, std::string); // 获取输出数组名称的函数

/**
* 这个静态方法和下面的宏是 VTK 约定的如何实例化 VTK 对象的方式。你不需要修改它们。
*/
static ttkHelloWorld *New(); // 用于创建 ttkHelloWorld 实例的静态方法
vtkTypeMacro(ttkHelloWorld, ttkAlgorithm); // VTK 类型宏,定义类 RTTI 信息

protected:
/**
* TODO 7: 实现过滤器的构造函数和析构函数(参见 cpp 文件)。
*/
ttkHelloWorld(); // 构造函数
~ttkHelloWorld() override = default; // 析构函数,默认行为

/**
* TODO 8: 指定每个输入端口的数据类型(参见 cpp 文件)。
*/
int FillInputPortInformation(int port, vtkInformation *info) override; // 填充输入端口信息

/**
* TODO 9: 指定每个输出端口的数据对象类型(参见 cpp 文件)。
*/
int FillOutputPortInformation(int port, vtkInformation *info) override; // 填充输出端口信息

/**
* TODO 10: 将 VTK 数据传递到基础代码并将基础代码的输出转换为 VTK 格式(参见 cpp 文件)。
*/
int RequestData(vtkInformation *request, // 请求数据处理的主要方法
vtkInformationVector **inputVector,
vtkInformationVector *outputVector) override;
};

override

  用于标识一个成员函数是用于重写基类中的虚函数。目的是提高代码的安全性、可维护性、可读性。

  在 override = default 的情况下,default 用于告诉编译器使用默认的实现。这通常用于默认构造函数、拷贝构造函数、析构函数等。

ttkHelloWorld.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
#include <ttkHelloWorld.h>

#include <vtkInformation.h>

#include <vtkDataArray.h>
#include <vtkDataSet.h>
#include <vtkObjectFactory.h>
#include <vtkPointData.h>
#include <vtkSmartPointer.h>

#include <ttkMacros.h>
#include <ttkUtils.h>

// 一个 VTK 宏,用于通过 ::New() 实例化此类
// 你无需修改这个
vtkStandardNewMacro(ttkHelloWorld);

/**
* TODO 7: 在 cpp 文件中实现过滤器的构造函数和析构函数。
*
* 构造函数需要通过 SetNumberOfInputPorts 和 SetNumberOfOutputPorts 函数指定
* 输入和输出端口的数量。它还应该为所有过滤器参数设置默认值。
*
* 析构函数通常为空,除非你想显式管理内存,例如,分配堆上的内存,在过滤器销毁时
* 需要释放。
*/
ttkHelloWorld::ttkHelloWorld() {
this->SetNumberOfInputPorts(1); // 设置输入端口的数量
this->SetNumberOfOutputPorts(1); // 设置输出端口的数量
}

/**
* TODO 8: 指定每个输入端口所需的数据类型
*
* 此方法通过将 vtkAlgorithm::INPUT_REQUIRED_DATA_TYPE() 键添加到端口信息来
* 指定过滤器所需的输入对象数据类型。
*/
int ttkHelloWorld::FillInputPortInformation(int port, vtkInformation *info) {
if(port == 0) {
info->Set(vtkAlgorithm::INPUT_REQUIRED_DATA_TYPE(), "vtkDataSet"); // 设置输入端口0所需的数据类型为 vtkDataSet
return 1;
}
return 0;
}

/**
* TODO 9: 指定每个输出端口的数据对象类型
*
* 此方法通过在端口信息对象中指定对应输出对象的数据类型来实现。可以通过添加
* `vtkDataObject::DATA_TYPE_NAME()` 关键字来显式指定类型,例如:
*
* info->Set( vtkDataObject::DATA_TYPE_NAME(), "vtkUnstructuredGrid" );
*
* 或者通过添加 `ttkAlgorithm::SAME_DATA_TYPE_AS_INPUT_PORT()` 关键字将输入端口的类型
* 传递给输出端口(见下文)。
*
* 注意:在执行 `RequestData` 方法之前,管道会基于这些信息初始化空的输出数据对象。
*/
int ttkHelloWorld::FillOutputPortInformation(int port, vtkInformation *info) {
if(port == 0) {
info->Set(ttkAlgorithm::SAME_DATA_TYPE_AS_INPUT_PORT(), 0); // 设置输出端口0的数据类型与输入端口相同
return 1;
}
return 0;
}

/**
* TODO 10: 将 VTK 数据传递给基础代码,并将基础代码输出转换为 VTK
*
* 在管道执行过程中调用此方法,以根据给定的输入数据对象和过滤器参数更新
* 已初始化的输出数据对象。
*
* 注意:
* 1) 传递的输入数据对象基于 `FillInputPortInformation` 方法提供的信息进行验证。
* 2) 输出对象已经基于 `FillOutputPortInformation` 方法提供的信息初始化。
*/
int ttkHelloWorld::RequestData(vtkInformation *ttkNotUsed(request),
vtkInformationVector **inputVector,
vtkInformationVector *outputVector) {

// 从输入向量中获取输入对象
// 注意:必须是 FillInputPortInformation 所要求的 vtkDataSet 类型
vtkDataSet *inputDataSet = vtkDataSet::GetData(inputVector[0]);
if(!inputDataSet)
return 0;

// 获取将被处理的输入数组
//
// 注意:VTK 提供了处理数组选择的抽象功能,但这个基本功能
// 遗憾的是文档不够完善。在继续阅读之前,请注意 TTK 开发者
// 团队不对现有的 VTK API 负责 ;-)
//
// 简而言之,在 RequestData 执行之前,必须调用
//
// SetInputArrayToProcess (
// int idx,
// int port,
// int connection,
// int fieldAssociation,
// const char *name
// )
//
// 参数 'idx' 常被误解:假设过滤器需要 n 个数组,那么 idx 从 0 到 n-1。
//
// 'port' 是输入端口的索引,表示从哪个输入端口获取数组。
//
// 'connection' 是在该端口的连接索引(需要指定,因为 VTK 允许
// 同一输入端口有多个连接)。
//
// 'fieldAssociation' 整数指定数组应从 0:点数据,1:单元格数据,或 2:字段数据中获取。
//
// 最后一个参数是数组的 'name'。
//
// 例如:SetInputArrayToProcess(3,1,0,1,"EdgeLength") 将指定
// 对于第 3 个数组,过滤器需要从 vtkDataObject 的输入端口 1(第一个连接)
// 获取名为 "EdgeLength" 的单元格数据数组。在 RequestData
// 方法中,可以通过调用 GetInputArrayToProcess 实际获取需要的
// 第 3 个数组。
//
// 如果这个过滤器在 ParaView 中运行,那么 UI 会自动调用 SetInputArrayToProcess
// (参见 HelloWorld.xml 文件)。
//
// 在 RequestData 执行期间,可以通过方法 "GetInputArrayToProcess" 实际检索
// 需要的数组。
vtkDataArray *inputArray = this->GetInputArrayToProcess(0, inputVector);
if(!inputArray) {
this->printErr("无法检索输入数组。");
return 0;
}

// 确保选定的数组可以被该过滤器处理,还应检查数组的关联和格式是否正确。
if(this->GetInputArrayAssociation(0, inputVector) != 0) {
this->printErr("输入数组需要是点数据数组。");
return 0;
}
if(inputArray->GetNumberOfComponents() != 1) {
this->printErr("输入数组需要是标量数组。");
return 0;
}

// 如果所有检查通过,则记录要处理的数组。
this->printMsg("开始计算...");
this->printMsg(" 标量数组: " + std::string(inputArray->GetName()));

// 创建一个与输入数组数据类型相同的输出数组
// 注意:vtkSmartPointers 有详细的文档
// (https://vtk.org/Wiki/VTK/Tutorials/SmartPointers)
vtkSmartPointer<vtkDataArray> const outputArray
= vtkSmartPointer<vtkDataArray>::Take(inputArray->NewInstance());
outputArray->SetName(this->OutputArrayName.data()); // 设置数组名称
outputArray->SetNumberOfComponents(1); // 每个元组只有一个组件
outputArray->SetNumberOfTuples(inputArray->GetNumberOfTuples());

// 获取输入 vtkDataSet 的 ttk::triangulation(如果尚不存在则创建一个)。
ttk::Triangulation *triangulation
= ttkAlgorithm::GetTriangulation(inputDataSet);
if(!triangulation)
return 0;

// 对三角化进行预处理(例如,启用获取顶点邻居)
this->preconditionTriangulation(triangulation); // 在基类中实现

// 对不同的输入数组数据类型进行模板化,并调用基础代码
int status = 0; // 此整数检查基础代码是否返回错误
ttkVtkTemplateMacro(inputArray->GetDataType(), triangulation->getType(),
(status = this->computeAverages<VTK_TT, TTK_TT>(
(VTK_TT *)ttkUtils::GetVoidPointer(outputArray),
(VTK_TT *)ttkUtils::GetVoidPointer(inputArray),
(TTK_TT *)triangulation->getData())));

// 如果出现错误,取消过滤器执行
if(status != 1)
return 0;

// 获取输出 vtkDataSet(已经基于 FillOutputPortInformation 提供的信息进行实例化)
vtkDataSet *outputDataSet = vtkDataSet::GetData(outputVector, 0);

// 对输入进行浅拷贝
outputDataSet->ShallowCopy(inputDataSet);

// 将计算出的输出数组添加到输出点数据中
outputDataSet->GetPointData()->AddArray(outputArray);

// 返回成功
return 1;
}

vtkStandardNewMacro

1
2
3
// 一个 VTK 宏,用于通过 ::New() 实例化此类
// 你无需修改这个
vtkStandardNewMacro(ttkHelloWorld);

  vtkStandardNewMacro 是 VTK (Visualization Toolkit) 中一个常用的宏,用于自动生成标准的构造函数和 New 方法。

构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* TODO 7: 在 cpp 文件中实现过滤器的构造函数和析构函数。
*
* 构造函数需要通过 SetNumberOfInputPorts 和 SetNumberOfOutputPorts 函数指定
* 输入和输出端口的数量。它还应该为所有过滤器参数设置默认值。
*
* 析构函数通常为空,除非你想显式管理内存,例如,分配堆上的内存,在过滤器销毁时
* 需要释放。
*/
ttkHelloWorld::ttkHelloWorld() {
this->SetNumberOfInputPorts(1); // 设置输入端口的数量
this->SetNumberOfOutputPorts(1); // 设置输出端口的数量
}

设置输入端口指定的数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* TODO 8: 指定每个输入端口所需的数据类型
*
* 此方法通过将 vtkAlgorithm::INPUT_REQUIRED_DATA_TYPE() 键添加到端口信息来
* 指定过滤器所需的输入对象数据类型。
*/
int ttkHelloWorld::FillInputPortInformation(int port, vtkInformation *info) {
if(port == 0) {
info->Set(vtkAlgorithm::INPUT_REQUIRED_DATA_TYPE(), "vtkDataSet"); // 设置输入端口0所需的数据类型为 vtkDataSet
return 1;
}
return 0;
}

  通过 FillInputPortInformation 方法为 VTK 过滤器的输入端口指定所需的数据类型。

输出端口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* TODO 9: 指定每个输出端口的数据对象类型
*
* 此方法通过在端口信息对象中指定对应输出对象的数据类型来实现。可以通过添加
* `vtkDataObject::DATA_TYPE_NAME()` 关键字来显式指定类型,例如:
*
* info->Set( vtkDataObject::DATA_TYPE_NAME(), "vtkUnstructuredGrid" );
*
* 或者通过添加 `ttkAlgorithm::SAME_DATA_TYPE_AS_INPUT_PORT()` 关键字将输入端口的类型
* 传递给输出端口(见下文)。
*
* 注意:在执行 `RequestData` 方法之前,管道会基于这些信息初始化空的输出数据对象。
*/
int ttkHelloWorld::FillOutputPortInformation(int port, vtkInformation *info) {
if(port == 0) {
info->Set(ttkAlgorithm::SAME_DATA_TYPE_AS_INPUT_PORT(), 0); // 设置输出端口0的数据类型与输入端口相同
return 1;
}
return 0;
}

  当 port == 0 时,函数将指定输出端口 0 的数据类型与输入端口 0 的数据类型相同,使用的是 ttkAlgorithm::SAME_DATA_TYPE_AS_INPUT_PORT() 关键字。

ttkHelloWorld::RequestData 函数

  它是 VTK 过滤器管道中的一个核心方法,用于执行过滤器的主要计算逻辑。该函数负责从输入中提取数据、进行预处理、调用实际计算代码,并将计算结果放入输出对象。

  这段代码从输入数据集中提取标量数组,进行预处理,并调用 computeAverages 函数进行计算。计算结果被存储到输出数组中,并最终添加到输出数据集中。

函数定义

1
2
3
int ttkHelloWorld::RequestData(vtkInformation *ttkNotUsed(request),
vtkInformationVector **inputVector,
vtkInformationVector *outputVector)
  • inputVector 是指向输入数据向量的指针数组,包含输入数据对象的信息。

  • outputVector 是指向输出数据向量的指针,用于存储计算结果。

步骤1:获取输入数据对象

1
2
3
4
5
// 从输入向量中获取输入对象
// 注意:必须是 FillInputPortInformation 所要求的 vtkDataSet 类型
vtkDataSet *inputDataSet = vtkDataSet::GetData(inputVector[0]);
if(!inputDataSet)
return 0;

  调用 vtkDataSet::GetData(inputVector[0]),从输入向量的第 0 号端口中获取输入数据对象(必须是 vtkDataSet 类型,已在 FillInputPortInformation 方法中指定)。

步骤 2:获取输入数组

1
2
3
4
5
vtkDataArray *inputArray = this->GetInputArrayToProcess(0, inputVector);
if(!inputArray) {
this->printErr("无法检索输入数组。");
return 0;
}

  使用 GetInputArrayToProcess(0, inputVector) 获取输入数组。第一个参数 0 表示这是第一个数组。

步骤 3:检查输入数组的格式

1
2
3
4
5
6
7
8
9
// 确保选定的数组可以被该过滤器处理,还应检查数组的关联和格式是否正确。
if(this->GetInputArrayAssociation(0, inputVector) != 0) {
this->printErr("输入数组需要是点数据数组。");
return 0;
}
if(inputArray->GetNumberOfComponents() != 1) {
this->printErr("输入数组需要是标量数组。");
return 0;
}

  检查输入数组的关联性,确保它是点数据(0 表示点数据),检查数组是否是标量数组,即每个元组只能有一个分量。如果 GetNumberOfComponents() 返回的组件数量不是 1,则返回错误。

步骤 4:输出日志信息

1
2
this->printMsg("开始计算...");
this->printMsg(" 标量数组: " + std::string(inputArray->GetName()));

步骤 5:创建输出数组

1
2
3
4
5
6
7
8
// 创建一个与输入数组数据类型相同的输出数组
// 注意:vtkSmartPointers 有详细的文档
// (https://vtk.org/Wiki/VTK/Tutorials/SmartPointers)
vtkSmartPointer<vtkDataArray> const outputArray
= vtkSmartPointer<vtkDataArray>::Take(inputArray->NewInstance());
outputArray->SetName(this->OutputArrayName.data()); // 设置数组名称
outputArray->SetNumberOfComponents(1); // 每个元组只有一个组件
outputArray->SetNumberOfTuples(inputArray->GetNumberOfTuples());
  • 使用 vtkSmartPointer 创建一个新的输出数组,类型与输入数组相同。NewInstance() 创建与输入数组相同类型的对象。
  • 设置输出数组的名称和组件数量(1),并为输出数组分配与输入数组相同数量的元组。

步骤 6:获取和预处理三角化对象

1
2
3
4
5
6
7
8
// 获取输入 vtkDataSet 的 ttk::triangulation(如果尚不存在则创建一个)。
ttk::Triangulation *triangulation
= ttkAlgorithm::GetTriangulation(inputDataSet);
if(!triangulation)
return 0;

// 对三角化进行预处理(例如,启用获取顶点邻居)
this->preconditionTriangulation(triangulation); // 在基类中实现
  • 调用 ttkAlgorithm::GetTriangulation(inputDataSet) 获取输入数据集的三角化对象(ttk::Triangulation),如果不存在则创建它。如果获取失败,则返回 0
  • 调用 this->preconditionTriangulation(triangulation) 对三角化对象进行预处理,这在基类中实现,可能涉及准备一些邻接表等数据结构。

步骤 7:调用计算逻辑

1
2
3
4
5
6
7
8
9
10
11
// 对不同的输入数组数据类型进行模板化,并调用基础代码
int status = 0; // 此整数检查基础代码是否返回错误
ttkVtkTemplateMacro(inputArray->GetDataType(), triangulation->getType(),
(status = this->computeAverages<VTK_TT, TTK_TT>(
(VTK_TT *)ttkUtils::GetVoidPointer(outputArray),
(VTK_TT *)ttkUtils::GetVoidPointer(inputArray),
(TTK_TT *)triangulation->getData())));

// 如果出现错误,取消过滤器执行
if(status != 1)
return 0;
  • ttkVtkTemplateMacro 是一个宏,用于处理不同的数据类型(模板化处理)。
  • 宏会根据 inputArraytriangulation 的数据类型调用 computeAverages 方法,执行实际的计算。computeAverages 方法使用模板类型来进行数据操作。
  • ttkUtils::GetVoidPointer 获取数组的原始数据指针(C 风格的指针)。

  标量值是指从输入数组中获取的点数据(例如温度、压力等物理量)的具体数值。computeAverages 函数负责计算三角化对象中每个顶点的标量值的平均值。具体来说,对于每个顶点,程序会找到与该顶点相邻的其他顶点,获取它们的标量值,然后计算这些邻居点的标量值的平均值,作为该顶点的新标量值。

步骤 8:输出数据处理

1
2
3
4
5
6
7
8
// 获取输出 vtkDataSet(已经基于 FillOutputPortInformation 提供的信息进行实例化)
vtkDataSet *outputDataSet = vtkDataSet::GetData(outputVector, 0);

// 对输入进行浅拷贝
outputDataSet->ShallowCopy(inputDataSet);

// 将计算出的输出数组添加到输出点数据中
outputDataSet->GetPointData()->AddArray(outputArray);
  • outputVector 中获取输出数据集,并进行浅拷贝,意味着 outputDataSet 将复制输入数据集的结构信息。
  • 将计算得到的输出数组添加到输出数据集的点数据中。

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
/// TODO 12: 添加您的信息
/// \author 您的名字 <您的电子邮件地址>
/// \date 日期
///
/// \brief 示例程序

// TTK 包含文件
#include <CommandLineParser.h>
#include <ttkHelloWorld.h>

// VTK 包含文件
#include <vtkCellData.h>
#include <vtkDataArray.h>
#include <vtkDataSet.h>
#include <vtkPointData.h>
#include <vtkSmartPointer.h>
#include <vtkXMLDataObjectWriter.h>
#include <vtkXMLGenericDataObjectReader.h>

int main(int argc, char **argv)
{

// ---------------------------------------------------------------------------
// 程序变量
// ---------------------------------------------------------------------------
std::vector<std::string> inputFilePaths;
std::vector<std::string> inputArrayNames;
std::string outputArrayName{"AveragedArray"};
std::string outputPathPrefix{"output"};
bool listArrays{false};

// ---------------------------------------------------------------------------
// 根据命令行参数设置程序变量
// ---------------------------------------------------------------------------
{
ttk::CommandLineParser parser;

// -------------------------------------------------------------------------
// 标准选项和参数
// -------------------------------------------------------------------------
parser.setArgument(
"i", &inputFilePaths, "Input data-sets (*.vti, *vtu, *vtp)", false);
parser.setArgument("a", &inputArrayNames, "Input array names", true);
parser.setArgument(
"o", &outputPathPrefix, "Output file prefix (no extension)", true);
parser.setOption("l", &listArrays, "List available arrays");

// -------------------------------------------------------------------------
// TODO 13: 声明自定义参数和选项
// -------------------------------------------------------------------------
parser.setArgument("O", &outputArrayName, "Output array name", true);

parser.parse(argc, argv);
}

// ---------------------------------------------------------------------------
// 命令行输出信息
// ---------------------------------------------------------------------------
ttk::Debug msg;
msg.setDebugMsgPrefix("HelloWorld");

// ---------------------------------------------------------------------------
// 初始化 ttkHelloWorld 模块(调整参数)
// ---------------------------------------------------------------------------
auto helloWorld = vtkSmartPointer<ttkHelloWorld>::New();

// ---------------------------------------------------------------------------
// TODO 14: 将自定义参数和选项传递给模块
// ---------------------------------------------------------------------------
// helloWorld->SetOutputArrayName(outputArrayName);

// ---------------------------------------------------------------------------
// 读取输入 vtkDataObjects(可选:打印可用数组)
// ---------------------------------------------------------------------------
vtkDataArray *defaultArray = nullptr;
for(size_t i = 0; i < inputFilePaths.size(); i++)
{
// 初始化一个可以解析任何存储在 XML 格式中的 vtkDataObject 的读取器
auto reader = vtkSmartPointer<vtkXMLGenericDataObjectReader>::New();
reader->SetFileName(inputFilePaths[i].data());
reader->Update();

// 检查输入 vtkDataObject 是否成功读取
auto inputDataObject = reader->GetOutput();
if(!inputDataObject)
{
msg.printErr("Unable to read input file `" + inputFilePaths[i] + "' :(");
return 1;
}

auto inputAsVtkDataSet = vtkDataSet::SafeDownCast(inputDataObject);

// 如果需要,打印数组列表,否则继续执行
if(listArrays)
{
msg.printMsg(inputFilePaths[i] + ":");
if(inputAsVtkDataSet)
{
// 点数据
msg.printMsg(" PointData:");
auto pointData = inputAsVtkDataSet->GetPointData();
for(int j = 0; j < pointData->GetNumberOfArrays(); j++)
msg.printMsg(" - " + std::string(pointData->GetArrayName(j)));

// 单元数据
msg.printMsg(" CellData:");
auto cellData = inputAsVtkDataSet->GetCellData();
for(int j = 0; j < cellData->GetNumberOfArrays(); j++)
msg.printMsg(" - " + std::string(cellData->GetArrayName(j)));
}
else
{
msg.printErr("Unable to list arrays on file `" + inputFilePaths[i]
+ "'");
return 1;
}
}
else
{
// 将输入对象提供给 ttkHelloWorld 过滤器
helloWorld->SetInputDataObject(i, reader->GetOutput());

// 默认数组
if(!defaultArray)
{
defaultArray = inputAsVtkDataSet->GetPointData()->GetArray(0);
if(!defaultArray)
defaultArray = inputAsVtkDataSet->GetCellData()->GetArray(0);
}
}
}

// 如果只要求列出数组,则终止程序
if(listArrays)
{
return 0;
}

// ---------------------------------------------------------------------------
// 指定哪些输入 vtkDataObjects 的数组将被处理
// ---------------------------------------------------------------------------
if(!inputArrayNames.size())
{
if(defaultArray)
inputArrayNames.emplace_back(defaultArray->GetName());
}
for(size_t i = 0; i < inputArrayNames.size(); i++)
helloWorld->SetInputArrayToProcess(i, 0, 0, 0, inputArrayNames[i].data());

// ---------------------------------------------------------------------------
// 执行 ttkHelloWorld 过滤器
// ---------------------------------------------------------------------------
helloWorld->Update();

// ---------------------------------------------------------------------------
// 如果指定了输出前缀,则将所有输出对象写入磁盘
// ---------------------------------------------------------------------------
if(!outputPathPrefix.empty())
{
for(int i = 0; i < helloWorld->GetNumberOfOutputPorts(); i++)
{
auto output = helloWorld->GetOutputDataObject(i);
auto writer = vtkSmartPointer<vtkXMLWriter>::Take(
vtkXMLDataObjectWriter::NewWriter(output->GetDataObjectType()));

std::string outputFileName = outputPathPrefix + "_port_"
+ std::to_string(i) + "."
+ writer->GetDefaultFileExtension();
msg.printMsg("Writing output file `" + outputFileName + "'...");
writer->SetInputDataObject(output);
writer->SetFileName(outputFileName.data());
writer->Update();
}
}

return 0;
}

头文件

1
2
3
4
5
6
7
8
9
10
11
12
// TTK 包含文件
#include <CommandLineParser.h>
#include <ttkHelloWorld.h>

// VTK 包含文件
#include <vtkCellData.h>
#include <vtkDataArray.h>
#include <vtkDataSet.h>
#include <vtkPointData.h>
#include <vtkSmartPointer.h>
#include <vtkXMLDataObjectWriter.h>
#include <vtkXMLGenericDataObjectReader.h>
  • TTK 相关头文件CommandLineParser.h 用于解析命令行参数,ttkHelloWorld.h 是 TTK 的示例过滤器类,负责执行简单的计算任务。
  • VTK 相关头文件:用于处理 VTK 格式的网格数据。vtkCellDatavtkPointData 用于访问单元和点上的数据数组,vtkDataArray 表示数据数组,vtkDataSet 表示数据集。vtkSmartPointer 是 VTK 中常用的智能指针,用于自动管理内存。vtkXMLGenericDataObjectReader 是用于读取通用 VTK XML 格式数据的读取器,vtkXMLDataObjectWriter 用于将数据写回 VTK 文件。

程序变量声明部分

1
2
3
4
5
6
7
// 程序变量
// ---------------------------------------------------------------------------
std::vector<std::string> inputFilePaths;
std::vector<std::string> inputArrayNames;
std::string outputArrayName{"AveragedArray"};
std::string outputPathPrefix{"output"};
bool listArrays{false};

命令行参数解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 根据命令行参数设置程序变量
// ---------------------------------------------------------------------------
{
ttk::CommandLineParser parser;

// -------------------------------------------------------------------------
// 标准选项和参数
// -------------------------------------------------------------------------
parser.setArgument(
"i", &inputFilePaths, "Input data-sets (*.vti, *vtu, *vtp)", false);
parser.setArgument("a", &inputArrayNames, "Input array names", true);
parser.setArgument(
"o", &outputPathPrefix, "Output file prefix (no extension)", true);
parser.setOption("l", &listArrays, "List available arrays");

// -------------------------------------------------------------------------
// TODO 13: 声明自定义参数和选项
// -------------------------------------------------------------------------
parser.setArgument("O", &outputArrayName, "Output array name", true);

parser.parse(argc, argv);
}
  • -i:指定输入文件路径,可以接受多个文件。
  • -a:指定要处理的数组名称。
  • -o:指定输出文件的前缀。
  • -l:列出输入文件中的可用数组。
  • -O:指定输出数组的名称。

调试信息输出设置

1
2
3
4
// 命令行输出信息
// ---------------------------------------------------------------------------
ttk::Debug msg;
msg.setDebugMsgPrefix("HelloWorld");

  TTK 中的 Debug 类用于调试信息的输出。这里设置了调试信息前缀为 "HelloWorld",后续的调试信息都会带上这个前缀。

初始化 TTK 模块

1
2
3
// 初始化 ttkHelloWorld 模块(调整参数)
// ---------------------------------------------------------------------------
auto helloWorld = vtkSmartPointer<ttkHelloWorld>::New();

读取输入文件并解析数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
vtkDataArray *defaultArray = nullptr;
for(size_t i = 0; i < inputFilePaths.size(); i++)
{
// 初始化一个可以解析任何存储在 XML 格式中的 vtkDataObject 的读取器
auto reader = vtkSmartPointer<vtkXMLGenericDataObjectReader>::New();
reader->SetFileName(inputFilePaths[i].data());
reader->Update();

// 检查输入 vtkDataObject 是否成功读取
auto inputDataObject = reader->GetOutput();
if(!inputDataObject)
{
msg.printErr("Unable to read input file `" + inputFilePaths[i] + "' :(");
return 1;
}

  这段代码的核心功能是读取用户指定的 VTK 格式文件并确保文件读取成功。

  defaultArray是指向 vtkDataArray 类型的指针,初始值为 nullptr。这个指针将用于保存第一个找到的默认数组(点数据或单元数据),如果用户没有指定特定的数组,程序将使用这个默认数组进行处理。

  循环遍历 inputFilePaths,程序读取用户通过命令行传入的所有 VTK 格式文件。inputFilePaths 是一个字符串数组,存储了输入文件的路径。

  初始化 vtkXMLGenericDataObjectReader 读取器。vtkXMLGenericDataObjectReader 是一个 VTK 中的读取器类,它可以解析和读取 XML 格式的 VTK 数据对象(如 .vti, .vtu, .vtp 等)。reader->SetFileName(inputFilePaths[i].data()); 设置要读取的文件路径,这里通过 inputFilePaths[i].data() 获取当前文件路径的字符指针。reader->Update(); 执行更新操作,正式读取文件内容。

  最后检查文件是否成功读取。reader->GetOutput(); 获取读取的 VTK 数据对象。如果 inputDataObject 为空(即文件读取失败),则会通过 msg.printErr 打印错误信息,并且程序返回 1,表示程序执行错误。

读取VTK数据对象,并根据用户的选择执行不同的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
auto inputAsVtkDataSet = vtkDataSet::SafeDownCast(inputDataObject);

// 如果需要,打印数组列表,否则继续执行
if(listArrays)
{
msg.printMsg(inputFilePaths[i] + ":");
if(inputAsVtkDataSet)
{
// 点数据
msg.printMsg(" PointData:");
auto pointData = inputAsVtkDataSet->GetPointData();
for(int j = 0; j < pointData->GetNumberOfArrays(); j++)
msg.printMsg(" - " + std::string(pointData->GetArrayName(j)));

// 单元数据
msg.printMsg(" CellData:");
auto cellData = inputAsVtkDataSet->GetCellData();
for(int j = 0; j < cellData->GetNumberOfArrays(); j++)
msg.printMsg(" - " + std::string(cellData->GetArrayName(j)));
}
else
{
msg.printErr("Unable to list arrays on file `" + inputFilePaths[i]
+ "'");
return 1;
}
}
else
{
// 将输入对象提供给 ttkHelloWorld 过滤器
helloWorld->SetInputDataObject(i, reader->GetOutput());

// 默认数组
if(!defaultArray)
{
defaultArray = inputAsVtkDataSet->GetPointData()->GetArray(0);
if(!defaultArray)
defaultArray = inputAsVtkDataSet->GetCellData()->GetArray(0);
}
}

  这段代码主要负责根据用户输入的命令行参数,判断是否需要打印输入文件中的点数据和单元数据的数组列表。如果打印,则输出每个文件中包含的数组名称;否则,将输入数据传递给 ttkHelloWorld 模块,并选取默认的点或单元数据数组作为处理的目标。

  如果 defaultArray 还没有被初始化,选择输入数据中的一个默认数组作为处理的目标。

  • 首先尝试从点数据中获取第一个数组 GetPointData()->GetArray(0)
  • 如果点数据中没有数组,则尝试从单元数据中获取 GetCellData()->GetArray(0)

  目的是确保有一个默认的数组可以被处理,以防用户没有指定特定的数组。

指定要处理的输入数组

1
2
3
4
5
6
7
8
9
// 指定哪些输入 vtkDataObjects 的数组将被处理
// ---------------------------------------------------------------------------
if(!inputArrayNames.size())
{
if(defaultArray)
inputArrayNames.emplace_back(defaultArray->GetName());
}
for(size_t i = 0; i < inputArrayNames.size(); i++)
helloWorld->SetInputArrayToProcess(i, 0, 0, 0, inputArrayNames[i].data());

  检查用户是否指定了数组名称,如果没有,选择一个默认数组。遍历所有数组,将它们传递给模块进行处理。

SetInputArrayToProcess

  SetInputArrayToProcess 是 VTK(Visualization Toolkit)中一个常见的方法,用于指定输入数据中哪一个数组将被用于处理

  在 VTK 中,数据通常分为两类:

  • 点数据(PointData):存储与几何体的顶点(即点)相关的属性。例如,每个顶点的颜色、法线等。
  • 单元数据(CellData):存储与几何体的单元(例如多边形、四面体等)相关的属性。

每个数据集可以有多个点数据数组和单元数据数组。因此,在处理数据时,我们需要指定:

  • 我们要处理的是点数据还是单元数据
  • 我们要处理的是哪个具体的数组
1
2
3
4
5
6
7
helloWorld->SetInputArrayToProcess(
i, // 第i个输入端口
0, // 索引类型 (通常表示点数据或单元数据)
0, // 输入索引 (通常是当前正在处理的输入)
0, // 字段类别 (点数据或单元数据)
inputArrayNames[i].data() // 要处理的数组的名称
);

  参数解释:

  1. i(第 i 个输入端口)

  2. 0(索引类型)

    用来指定索引的类型。常见的值包括:

    • 0 表示点数据(vtkDataObject::FIELD_ASSOCIATION_POINTS)。
    • 1 表示单元数据(vtkDataObject::FIELD_ASSOCIATION_CELLS)。
  3. 0(输入索引)

    • 在复杂的 VTK 管道中,过滤器可以同时接受多个输入。这是指定要处理的输入数据集的索引(从 0 开始计数)。
    • 对于单一输入的过滤器(如这里的 ttkHelloWorld),输入索引通常设置为 0,表示第一个输入数据集。
  4. 0(字段类别)

    • 用于区分输入数据的字段类别。常见的值有:
      • 0 表示 vtkDataObject::POINT_DATA(即点数据)。
      • 1 表示 vtkDataObject::CELL_DATA(即单元数据)。
  5. inputArrayNames[i].data()(要处理的数组名称)

    • 这里指定了我们要处理的数组的名称。inputArrayNames[i] 是一个存储数组名称的字符串,data() 函数将这个字符串转换为 C 风格的字符数组(即 char*),以便传递给 VTK 的内部函数。

执行过滤器

1
2
3
// 执行 ttkHelloWorld 过滤器
// ---------------------------------------------------------------------------
helloWorld->Update();

  调用 helloWorld->Update() 将触发 ttkHelloWorld 过滤器执行其逻辑,并对用户指定的数组进行处理,调用 helloWorld->Update() 实际上是触发了 ttkHelloWorld::RequestData 函数。

输出数据写入磁盘中的文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 如果指定了输出前缀,则将所有输出对象写入磁盘
// ---------------------------------------------------------------------------
if(!outputPathPrefix.empty())
{
for(int i = 0; i < helloWorld->GetNumberOfOutputPorts(); i++)
{
//获取输出对象
auto output = helloWorld->GetOutputDataObject(i);
//创建写入器
auto writer = vtkSmartPointer<vtkXMLWriter>::Take(
vtkXMLDataObjectWriter::NewWriter(output->GetDataObjectType()));

//生成输出文件名
std::string outputFileName = outputPathPrefix + "_port_"
+ std::to_string(i) + "."
+ writer->GetDefaultFileExtension();
msg.printMsg("Writing output file `" + outputFileName + "'...");
//写入输出文件
writer->SetInputDataObject(output);
writer->SetFileName(outputFileName.data());
writer->Update();
}
}

  这段代码的功能是在 outputPathPrefix 不为空的情况下,将 ttkHelloWorld 过滤器的输出数据写入到磁盘的 XML 文件中。