使用開源工具進行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源創計劃”,歡迎正在閱讀的你也加入,一起分享。

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