使用开源工具进行3D数据可视化:使用VTK的教程

熟练的数据科学家查尔斯·库克(Charles Cook)在最近在Toptal博客上发表的文章中,谈到了使用开源工具进行科学计算。他的教程重点介绍了开源工具及其在轻松处理数据和获取结果中可以发挥的作用。

但是,一旦我们解决了所有这些复杂的微分方程,就会出现另一个问题。我们如何理解和解释来自这些模拟的大量数据?我们如何可视化潜在的千兆字节数据,例如在大型模拟中具有数百万个网格点的数据?

在为硕士论文解决类似问题的过程中,我接触了Visualization Toolkit或VTK –一个专门用于数据可视化的功能强大的图形库。

在本教程中,我将快速介绍VTK及其管道体系结构,并继续讨论使用叶轮泵中模拟流体数据得出的真实3D可视化示例。最后,我将列出库的优点以及遇到的缺点。

1. 数据可视化和VTK管道

开源库VTK包含具有许多复杂的可视化算法的可靠处理和渲染管道。但是,它的功能不止于此,因为随着时间的流逝,还添加了图像和网格处理算法。在我目前与一家牙科研究公司合作的项目中,我正在利用VTK在基于Qt的,类似于CAD的应用程序中执行基于网格的处理任务。该VTK案例研究显示广泛的合适的应用。

VTK的架构围绕着强大的管道概念展开。此概念的基本轮廓如下所示:

来源是管道的最开始,它创造了“一无所有的东西”。例如,avtkConeSource创建一个3D圆锥体,然后vtkSTLReader读取*.stl3D几何文件。

筛选器将源或其他筛选器的输出转换为新的内容。例如,a使用诸如vtkCutter平面的隐式函数在算法中剪切先前对象的输出。VTK随附的所有处理算法均实现为过滤器,并且可以自由链接在一起。

映射器将数据转换为图形基元。例如,它们可用于指定用于为科学数据着色的查找表。它们是指定显示内容的抽象方法。

角色表示场景中的对象(几何形状和显示属性)。此处指定了颜色,不透明度,阴影或方向之类的东西。

渲染器和Windows最终以独立于平台的方式在屏幕上描述了渲染。

典型的VTK渲染管道从一个或多个源开始,使用各种过滤器将它们处理成几个输出对象,然后使用映射器和角色分别进行渲染。这个概念背后的力量是更新机制。如果更改了滤镜或源的设置,则会自动更新所有相关的滤镜,映射器,actor和渲染窗口。另一方面,如果在管道下游的对象需要信息来执行其任务,则可以轻松获得它。

另外,不需要直接处理像OpenGL这样的渲染系统。VTK以平台和(部分)与系统无关的方式封装了所有低级任务。开发人员的工作水平更高。

2. 转子泵数据集的代码示例

让我们看一个数据可视化示例,该示例使用来自IEEE Visualization Contest 2011的旋转叶轮泵中的流体流数据集。数据本身是计算流体动力学仿真的结果,非常类似于Charles Cook的文章中所述。

特色泵的压缩模拟数据大小超过30 GB。它包含多个部分和多个时间步长,因此尺寸较大。在本指南中,我们将介绍这些时间步之一的转子部分,压缩后的大小约为150 MB。

我选择使用VTK的语言是C ++,但是还有其他几种语言(例如Tcl / Tk,Java和Python)的映射。如果目标只是单个数据集的可视化,则完全不需要编写代码,而可以使用Paraview(VTK大部分功能的图形前端)。

3. 数据集以及为什么需要64位

通过在Paraview中打开一个时间步并将转子零件提取到一个单独的文件中,我从上面提供的30 GB数据集中提取了转子数据集。它是一个非结构化的网格文件,即由点和3D单元(如六面体,四面体等)组成的3D体积。每个3D点都有关联的值。有时单元格也具有关联的值,但在这种情况下没有。该培训将集中于这些点的压力和速度,并尝试在其3D上下文中可视化这些点。

加载了VTK时,压缩文件的大小约为150 MB,内存中的大小约为280 MB。但是,通过在VTK中进行处理,数据集在VTK管道中被多次缓存,并且对于32位程序,我们很快达到了2 GB的内存限制。使用VTK时有多种节省内存的方法,但为了简单起见,我们仅以64位编译并运行示例。

致谢:数据集由德国克劳斯塔尔大学应用力学研究所(Dipl。Wirtsch.-Ing。Andreas Lucius)提供。

4. 目标

使用VTK作为工具,我们将实现的是下图所示的可视化效果。作为3D上下文,使用部分透明的线框渲染显示数据集的轮廓。然后,使用数据集的左侧部分通过简单的表面颜色编码显示压力。(在此示例中,我们将跳过更复杂的体积渲染)。为了可视化速度场,数据集的右侧填充了流线,这些流线通过其速度的大小进行颜色编码。这种可视化选择在技术上并不理想,但我希望保持VTK代码尽可能简单。另外,该示例有理由成为可视化挑战的一部分,即,流动中存在许多湍流。

5. 一步步

我将逐步讨论VTK代码,以显示渲染输出在每个阶段的外观。完整的源代码可以在培训结束时下载。

让我们从包含VTK所需的一切开始,然后打开main函数。

  
  
  
  1. #include<vtkActor.h>

  2. #include <vtkArrayCalculator.h>

  3. #include<vtkCamera.h>

  4. #include<vtkClipDataSet.h>

  5. #include<vtkCutter.h>

  6. #include<vtkDataSetMapper.h>

  7. #include<vtkInteractorStyleTrackballCamera.h>

  8. #include<vtkLookupTable.h>

  9. #include<vtkNew.h>

  10. #include <vtkPlane.h>

  11. #include <vtkPointData.h>

  12. #include <vtkPointSource.h>

  13. #include <vtkPolyDataMapper.h>

  14. #include <vtkProperty.h>

  15. #include <vtkRenderer.h>

  16. #include <vtkRenderWindow.h>

  17. #include <vtkRenderWindowInteractor.h>

  18. #include <vtkRibbonFilter.h>

  19. #include <vtkStreamTracer.h>

  20. #include <vtkSmartPointer.h>

  21. #include <vtkUnstructuredGrid.h>

  22. #include <vtkXMLUnstructuredGridReader.h>


  23. int main(int argc, char** argv)

  24. {

接下来,我们设置渲染器和渲染窗口以显示我们的结果。我们设置背景色和渲染窗口大小。

  
  
  
  1. // Setup the renderer


  2. vtkNew<vtkRenderer> renderer;


  3. renderer->SetBackground(0.9, 0.9, 0.9);




  4. // Setup the render window


  5. vtkNew<vtkRenderWindow> renWin;


  6. renWin->AddRenderer(renderer.Get());


  7. renWin->SetSize(500, 500);

使用此代码,我们已经可以显示一个静态渲染窗口。相反,我们选择添加一个vtkRenderWindowInteractor以交互旋转,缩放和平移场景。

  
  
  
  1. // Setup the render window interactor

  2. vtkNew<vtkRenderWindowInteractor> interact;

  3. vtkNew<vtkInteractorStyleTrackballCamera> style;

  4. interact->SetRenderWindow(renWin.Get());

  5. interact->SetInteractorStyle(style.Get());

现在,我们有一个正在运行的示例,显示了一个灰色的空渲染窗口。

接下来,我们使用VTK附带的众多阅读器之一加载数据集。

  
  
  
  1. // Read the file

  2. vtkSmartPointer<vtkXMLUnstructuredGridReader> pumpReader = vtkSmartPointer<vtkXMLUnstructuredGridReader>::New();

  3. pumpReader->SetFileName("rotor.vtu");

6. 简短介绍VTK内存管理

VTK使用便利的自动内存管理概念,围绕参考计数。与大多数其他实现不同,引用计数保留在VTK对象本身中,而不是智能指针类中。这样做的好处是,即使VTK对象作为原始指针传递,也可以增加引用计数。创建托管VTK对象有两种主要方法。vtkNew 和vtkSmartPointer ::New(),主要区别在于avtkSmartPointer 是可隐式强制转换为原始指针的T*,并且可以从函数中返回。对于的实例,vtkNew 我们必须调用.Get()以获取原始指针,并且只能通过将其包装到vtkSmartPointer。在我们的示例中,我们从不从函数返回并且所有对象始终存在,因此我们将使用short vtkNew,仅将上述例外用于演示目的。

此时,尚未从文件中读取任何内容。我们或更进一步的过滤器将不得不要求Update()文件读取实际发生。通常,这是让VTK类自己处理更新的最佳方法。但是,有时我们想直接访问过滤器的结果,例如以获取此数据集中的压力范围。然后,我们需要Update()手动致电。(我们不会因Update()多次调用而失去性能,因为结果被缓存了)。

  
  
  
  1. // Get the pressure range

  2. pumpReader->Update();

  3. double pressureRange[2];

  4. pumpReader->GetOutput()->GetPointData()->GetArray("Pressure")->GetRange(pressureRange);

接下来,我们需要使用提取数据集的左半部分vtkClipDataSet。为此,我们首先定义一个vtkPlane定义拆分的。然后,我们将首次看到VTK管道如何连接在一起:successor->SetInputConnection(predecessor->GetOutputPort())。现在,无论何时我们要求clipperLeft此连接进行更新,都将确保所有先前的过滤器也都是最新的。

  
  
  
  1. // Clip the left part from the input

  2. vtkNew<vtkPlane> planeLeft;

  3. planeLeft->SetOrigin(0.0, 0.0, 0.0);

  4. planeLeft->SetNormal(-1.0, 0.0, 0.0);


  5. vtkNew<vtkClipDataSet> clipperLeft;

  6. clipperLeft->SetInputConnection(pumpReader->GetOutputPort());

  7. clipperLeft->SetClipFunction(planeLeft.Get());

最后,我们创建第一个actor和mapper来显示左半部分的线框渲染。注意,映射器以与彼此过滤器完全相同的方式连接到其过滤器。大多数时候,渲染器本身会触发所有actor,映射器和基础过滤器链的更新!

唯一不能解释的行可能是leftWireMapper->ScalarVisibilityOff();-它禁止通过设置为当前活动数组的压力值对线框进行着色。

  
  
  
  1. // Create the wireframe representation for the left part

  2. vtkNew<vtkDataSetMapper> leftWireMapper;

  3. leftWireMapper->SetInputConnection(clipperLeft->GetOutputPort());

  4. leftWireMapper->ScalarVisibilityOff();


  5. vtkNew<vtkActor> leftWireActor;

  6. leftWireActor->SetMapper(leftWireMapper.Get());

  7. leftWireActor->GetProperty()->SetRepresentationToWireframe();

  8. leftWireActor->GetProperty()->SetColor(0.8, 0.8, 0.8);

  9. leftWireActor->GetProperty()->SetLineWidth(0.5);

  10. leftWireActor->GetProperty()->SetOpacity(0.8);

  11. renderer->AddActor(leftWireActor.Get());

此时,渲染窗口最终将显示一些内容,即左侧部分的线框。

右部分的线框渲染以类似的方式创建,方法是将(新创建的)平面法线切换vtkClipDataSet到相反的方向,并稍微更改(新创建的)映射器和actor的颜色和不透明度。注意,在这里,我们的VTK管道从同一输入数据集分为两个方向(左右)。

  
  
  
  1. // Clip the right part from the input

  2. vtkNew<vtkPlane> planeRight;

  3. planeRight->SetOrigin(0.0, 0.0, 0.0);

  4. planeRight->SetNormal(1.0, 0.0, 0.0);


  5. vtkNew<vtkClipDataSet> clipperRight;

  6. clipperRight->SetInputConnection(pumpReader->GetOutputPort());

  7. clipperRight->SetClipFunction(planeRight.Get());


  8. // Create the wireframe representation for the right part

  9. vtkNew<vtkDataSetMapper> rightWireMapper;

  10. rightWireMapper->SetInputConnection(clipperRight->GetOutputPort());

  11. rightWireMapper->ScalarVisibilityOff();


  12. vtkNew<vtkActor> rightWireActor;

  13. rightWireActor->SetMapper(rightWireMapper.Get());

  14. rightWireActor->GetProperty()->SetRepresentationToWireframe();

  15. rightWireActor->GetProperty()->SetColor(0.2, 0.2, 0.2);

  16. rightWireActor->GetProperty()->SetLineWidth(0.5);

  17. rightWireActor->GetProperty()->SetOpacity(0.1);

  18. renderer->AddActor(rightWireActor.Get());

现在,输出窗口将显示两个线框部件,如预期的那样。

现在我们准备可视化一些有用的数据!要将压力可视化添加到左侧部分,我们不需要做太多事情。我们创建了一个新的映射器并将其也连接到该映射器clipperLeft,但是这次我们通过压力数组进行着色。也正是在这里,我们终于利用了pressureRange上面得出的结果。

  
  
  
  1. // Create the pressure representation for the left part

  2. vtkNew<vtkDataSetMapper> pressureColorMapper;

  3. pressureColorMapper->SetInputConnection(clipperLeft->GetOutputPort());

  4. pressureColorMapper->SelectColorArray("Pressure");

  5. pressureColorMapper->SetScalarRange(pressureRange);


  6. vtkNew<vtkActor> pressureColorActor;

  7. pressureColorActor->SetMapper(pressureColorMapper.Get());

  8. pressureColorActor->GetProperty()->SetOpacity(0.5);

  9. renderer->AddActor(pressureColorActor.Get());

现在,输出如下图所示。中间的压力非常低,将物料吸入泵中。然后,这种材料被输送到外部,迅速增加压力。(当然应该有一个带有实际值的颜色图例,但是为了使示例更短,我省略了它)。

现在,棘手的部分开始了。我们想在右侧绘制速度流线。流线是通过在矢量场中从源点积分而生成的。向量字段已经是“速度”向量数组形式的数据集的一部分。因此,我们只需要生成源点。vtkPointSource生成一个随机点的范围。我们将生成1500个源点,因为它们中的大多数都不会位于数据集中,并且将被流跟踪器忽略。

  
  
  
  1. // Create the source points for the streamlines

  2. vtkNew<vtkPointSource> pointSource;

  3. pointSource->SetCenter(0.0, 0.0, 0.015);

  4. pointSource->SetRadius(0.2);

  5. pointSource->SetDistributionToUniform();

  6. pointSource->SetNumberOfPoints(1500);

接下来,我们创建streamtracer并设置其输入连接。您可能会说:“等等,多个连接?” 是的-这是我们遇到的第一个具有多个输入的VTK过滤器。普通输入连接用于矢量场,源连接用于种子点。由于“速度”是中的“活动”向量数组clipperRight,因此我们无需在此处明确指定。最后,我们指定从种子点开始在两个方向上进行积分,并将积分方法设置为Runge-Kutta-4.5。

  
  
  
  1. vtkNew<vtkStreamTracer> tracer;

  2. tracer->SetInputConnection(clipperRight->GetOutputPort());

  3. tracer->SetSourceConnection(pointSource->GetOutputPort());

  4. tracer->SetIntegrationDirectionToBoth();

  5. tracer->SetIntegratorTypeToRungeKutta45();

我们的下一个问题是通过速度大小为流线着色。由于没有向量大小的数组,因此我们将简单地将这些大小计算为一个新的标量数组。您已经猜到了,该任务也有一个VTK过滤器:vtkArrayCalculator。它获取一个数据集并将其输出保持不变,但恰好添加了一个从一个或多个现有数组计算出的数组。我们配置此数组计算器以获取“ Velocity”矢量的大小,并将其输出为“ MagVelocity”。最后,我们Update()再次手动调用,以导出新数组的范围。

  
  
  
  1. // Compute the velocity magnitudes and create the ribbons

  2. vtkNew<vtkArrayCalculator> magCalc;

  3. magCalc->SetInputConnection(tracer->GetOutputPort());

  4. magCalc->AddVectorArrayName("Velocity");

  5. magCalc->SetResultArrayName("MagVelocity");

  6. magCalc->SetFunction("mag(Velocity)");


  7. magCalc->Update();

  8. double magVelocityRange[2];

  9. magCalc->GetOutput()->GetPointData()->GetArray("MagVelocity")->GetRange(magVelocityRange);

vtkStreamTracer直接输出折线,vtkArrayCalculator并在不改变的情况下传递折线。因此,我们可以只magCalc使用新的映射器和actor直接显示的输出。

相反,在本培训中,我们选择通过显示功能区来使输出更好一些。vtkRibbonFilter生成2D单元格以显示其输入的所有折线的功能区。

  
  
  
  1. // Create and render the ribbons

  2. vtkNew<vtkRibbonFilter> ribbonFilter;

  3. ribbonFilter->SetInputConnection(magCalc->GetOutputPort());

  4. ribbonFilter->SetWidth(0.0005);


  5. vtkNew<vtkPolyDataMapper> streamlineMapper;

  6. streamlineMapper->SetInputConnection(ribbonFilter->GetOutputPort());

  7. streamlineMapper->SelectColorArray("MagVelocity");

  8. streamlineMapper->SetScalarRange(magVelocityRange);


  9. vtkNew<vtkActor> streamlineActor;

  10. streamlineActor->SetMapper(streamlineMapper.Get());

  11. renderer->AddActor(streamlineActor.Get());

现在仍然缺少,实际上也需要产生中间渲染,而实际上是渲染场景并初始化交互器的最后五行。

  
  
  
  1. // Render and show interactive window

  2. renWin->Render();

  3. interact->Initialize();

  4. interact->Start();

  5. return0;

  6. }

最后,我们到达完成的可视化效果,我将在这里再次展示:

可以在此处(https://bitbucket.org/B3ret/publicsamples/src/c189095649bb59ba2771b3b24b8779ac91bc2b20/main.cxx?at=master)找到上述可视化的完整源代码。

7. VTK 的优点和缺点

我将以我个人对VTK框架的优缺点的列表结束本文:

  • 优点:积极开发:VTK正在主要由研究社区的多个贡献者积极开发。这意味着可以使用一些最先进的算法,可以导入和导出许多3D格式,可以有效地修复错误,并且问题通常在讨论区中都有现成的解决方案。

  • 缺点:可靠性:将来自不同贡献者的许多算法与VTK的开放式管线设计结合在一起,可能会导致出现异常滤波器组合的问题。为了弄清楚为什么我的复杂过滤器链无法产生期望的结果,我不得不多次进入VTK源代码。我强烈建议您以允许调试的方式设置VTK。

  • 专业版:软件体系结构:VTK的管道设计和通用体系结构似乎经过深思熟虑,并且很高兴一起使用。几行代码会产生惊人的结果。内置的数据结构易于理解和使用。

  • 缺点:微体系结构:一些微体系结构设计决策使我无法理解。const正确性几乎不存在,数组作为输入和输出传递,没有明显的区别。我通过放弃一些性能并使用自己的包装程序(vtkMath例如使用的自定义3D类型) 减轻了我自己的算法的负担typedef std::array  Pnt3d

  • 专业版:微型文档:所有类和过滤器的Doxygen文档都是广泛且可用的,Wiki上的示例和测试用例也有助于您理解如何使用过滤器。

  • 缺点:宏文档:Web上有一些很好的VTK教程和介绍。但是据我所知,尚无大型参考文档来说明特定操作的完成方式。如果您想做一些新的事情,请期待一段时间后再做些什么。此外,很难找到任务的特定过滤器。但是,一旦找到它,Doxygen文档通常就足够了。探索VTK框架的一个好方法是下载并试用Paraview。

  • 优点:隐式并行支持:如果您的源代码可以分为几个部分,可以独立处理,那么并行化就像在处理单个部分的每个线程中创建单独的过滤器链一样简单。大多数大型可视化问题通常都属于此类。

  • 缺点:没有明确的并行化支持:如果您没有遇到大型的,可分割的问题,但是您想利用多个内核,则只能靠自己了。您必须确定哪些类是线程安全的,或者通过反复试验或阅读源代码来重新进入类。我曾经追踪到VTK过滤器的并行化问题,该过滤器使用静态全局变量来调用某些C库。

  • Pro:构建系统CMake:多平台元构建系统CMake也由Kitware(VTK的制造商)开发,并在Kitware之外的许多项目中使用。它与VTK很好地集成在一起,使为多个平台设置构建系统的工作变得轻松得多。

  • 优点:平台独立性,许可和寿命:VTK是开箱即用的平台独立性,并根据非常宽松的BSD样式许可进行许可。此外,专业支持可用于需要它的那些重要项目。Kitware得到了许多研究机构和其他公司的支持,并将持续一段时间。

8. 总结

总体而言,VTK是解决我喜欢的各种问题的最佳数据可视化工具。如果您遇到需要可视化,网格处理,图像处理或类似任务的项目,请尝试使用输入示例启动Paraview并评估VTK是否适合您。

原文:

  • https://www.toptal.com/data-science/3d-data-visualization-with-open-source-tools-an-example-using-vtk


本文分享自微信公众号 - 小弧光黑板报(gh_ba6067dca33c)。
如有侵权,请联系 [email protected] 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章