今天依旧是在摸索医学图像可视化的一天呢。这个笔记主要介绍了VTK上做法向量标记以及做切割平面的方法。
在读取和使用.stl文件过程中,我们经常要用到法向量。这个例子展示了我们应该如何计算多边形数据的法向量并用vtkGlyph3D绘制圆锥型状将其标记出来。
#include "vtkSmartPointer.h"
#include "vtkPolyDataReader.h"
#include "vtkPolyDataNormals.h"
#include "vtkPolyDataMapper.h"
#include "vtkMaskPoints.h"
#include "vtkConeSource.h"
#include "vtkGlyph3D.h"
#include "vtkActor.h"
#include "vtkRenderer.h"
#include "vtkRenderWindow.h"
#include "vtkRenderWindowInteractor.h"
#include "vtkSTLReader.h"
#include "vtkProperty.h"
#include "vtkAutoInit.h"
VTK_MODULE_INIT(vtkRenderingOpenGL2);
VTK_MODULE_INIT(vtkInteractionStyle);
int main()
{
//读取模型文件
vtkSmartPointer obj = vtkSmartPointer::New();
obj->SetFileName("D:\\ct\\20201102113826651_3d\\pelvis.stl");
// 计算模型每个面的法矢量,创建一个法向量对象
vtkSmartPointer normals = vtkSmartPointer::New();
//计算单元法向量时,要保持单元法向量一致才能得到合理的法向量。SetConsistency()可以自动调整单元点的顺序;SetAutoOrientNormals()可以自动调整法向量方向。
//类vtkPolyDataNormals自动开启对锐边缘处理,如果检测到锐边缘,会将其分裂,使图形更加平滑,可通过SetSplitting()函数关闭该功能。
//三维平面的法向量是指垂直该平面的向量。某点的法向量为垂直该点切平面的法向量。
// 将多边形数据集作为法向量对象的数据输入
normals->SetInputConnection(obj->GetOutputPort());
// 指定锐边角度为30度
normals->SetFeatureAngle(30);
// 关闭单元法向量计算(单元法向量可以通过组成每个单元的任意两条边的叉乘向量归并化来表示)
normals->SetComputeCellNormals(0);
// 开启点法向量计算(点的法向量则是由使用该点的单元单元法向量的平均值表示)
normals->SetComputePointNormals(1);
// 开启正常方向的全局翻转
normals->SetFlipNormals(1);
// 如果检测到锐边缘,会将其分裂,使图形更加平滑
normals->SetSplitting(1);
vtkSmartPointer objMapper = vtkSmartPointer::New();
objMapper->SetInputConnection(normals->GetOutputPort());
//由于读入的模型数据比较大,点比较多,因此使用vtkMaskPoints类采样部分数据,该类保留输入数据中的点数据及其属性,并支持点数据的采样
vtkSmartPointer ptMask = vtkSmartPointer::New();
ptMask->SetInputConnection(normals->GetOutputPort());
//创建符号——圆锥形
vtkSmartPointer cone = vtkSmartPointer::New();
cone->SetAngle(26.5651);
cone->SetHeight(1);
cone->SetRadius(0.5);
cone->SetResolution(6);
cone->SetCapping(1);
vtkSmartPointer glyph = vtkSmartPointer::New();
//设置被标识的点
glyph->SetInputConnection(normals->GetOutputPort());
//设置符号
glyph->SetSourceConnection(cone->GetOutputPort());
vtkSmartPointer glyphMapper = vtkSmartPointer::New();
glyphMapper->SetInputConnection(glyph->GetOutputPort());
vtkSmartPointer glyphActor = vtkSmartPointer::New();
glyphActor->SetMapper(glyphMapper);
glyphActor->GetProperty()->SetColor(1, 0, 0);
vtkSmartPointer normalActor = vtkSmartPointer::New();
normalActor->SetMapper(objMapper);
// Setup render window, renderer, and interactor
vtkSmartPointer renderer = vtkSmartPointer::New();
renderer->AddActor(normalActor);
renderer->AddActor(glyphActor);
vtkSmartPointer renderWindow = vtkSmartPointer::New();
renderWindow->AddRenderer(renderer);
vtkSmartPointer renderWindowInteractor = vtkSmartPointer::New();
renderWindowInteractor->SetRenderWindow(renderWindow);
renderWindow->Render();
renderWindowInteractor->Start();
return 0;
}
这里要特别注意.stl文件的读取问题。我一开始的写法是:
obj->SetFileName("D:\ct\20201102113826651_3d\femur.stl");
就会出现以下问题:
需要将代码里的地址改为双斜杠,即可正常读取文件。
obj->SetFileName("D:\\ct\\20201102113826651_3d\\pelvis.stl");
在VTK中,如果我们需要构建一个切割面,需要做的步骤有:
1. 指定切割面中心SetOrigin()及切割面的法向量SetNormal()。
2. 设定切割面的个数及位置SetValue()和GenerateValue()。
3. 设置切割过滤器对象vtkCutter, 指定被切割的数据集,及切割面隐函数vtkPlane。
4. 计算数据集在指定点的位置的属性vtkProbeFilter,指定切割对象的数据集及被切割的数据集。
5. 绘制vtkProbeFilter对象计算后的数据集(绘制的是切割对象的轮廓)。
#include "vtkPlane.h"
#include "vtkSTLReader.h"
#include "vtkPlane.h"
#include "vtkPolyData.h"
#include "vtkProbeFilter.h"
#include "vtkDataSetMapper.h"
#include "vtkPointData.h"
#include "vtkSmartPointer.h"
#include "vtkCutter.h"
#include "vtkActor.h"
#include "vtkRenderer.h"
#include "vtkRenderWindow.h"
#include "vtkRenderWindowInteractor.h"
#include "vtkAutoInit.h"
VTK_MODULE_INIT(vtkRenderingOpenGL2);
VTK_MODULE_INIT(vtkInteractionStyle);
int main()
{
//读取模型文件,被切割的数据
vtkSmartPointer obj = vtkSmartPointer::New();
obj->SetFileName("D:\\ct\\20201102113826651_3d\\femur.stl");
obj->Update();
//创建切割平面
vtkSmartPointer plane = vtkSmartPointer::New();
//设定剪切平面在被剪切的数据中心位置
vtkSmartPointer polyData = obj->GetOutput();
plane->SetOrigin(polyData->GetCenter());
plane->SetNormal(-0.287, 0, 0.9579);
// Print the plane parameters
std::cout << "Plane Origin: " << plane->GetOrigin() << std::endl;
std::cout << "Plane Normal: " << plane->GetNormal() << std::endl;
//过滤器类,利用隐函数对数据进行剪切
vtkSmartPointer planeCut = vtkSmartPointer::New();
// 设置被裁剪的数据集
planeCut->SetInputConnection(obj->GetOutputPort());
//设定隐函数
planeCut->SetCutFunction(plane);
planeCut->SetValue(0, 50);
planeCut->GenerateValues(10, 0, 500);
//计算数据集在指定点的位置的属性
vtkSmartPointer probe = vtkSmartPointer::New();
//计算平面在数据集通过点的属性
probe->SetInputConnection(planeCut->GetOutputPort());
//被计算的数据集
probe->SetSourceConnection(obj->GetOutputPort());
vtkSmartPointer cutMapper = vtkSmartPointer::New();
cutMapper->SetInputConnection(probe->GetOutputPort());
//得到数据点集属性值的范围
vtkSmartPointer pData = vtkSmartPointer::New();
pData = polyData->GetPointData();
//设定属性值范围
//cutMapper->SetScalarRange(pData->GetScalars()->GetRange());
cutMapper->ScalarVisibilityOn();
vtkSmartPointer cutActor = vtkSmartPointer::New();
cutActor->SetMapper(cutMapper);
// 可视化
vtkSmartPointer renderer = vtkSmartPointer::New();
renderer->SetBackground(.0, .0, .0);
vtkSmartPointer renderWindow = vtkSmartPointer::New();
renderWindow->AddRenderer(renderer);
vtkSmartPointer renderWindowInteractor = vtkSmartPointer::New();
renderWindowInteractor->SetRenderWindow(renderWindow);
renderer->AddActor(cutActor);
renderWindow->Render();
renderWindowInteractor->Start();
return 0;
}
如果想修改切面数量或者其他属性,修改这几行的参数即可:
planeCut->SetCutFunction(plane);
planeCut->SetValue(0, 50);
planeCut->GenerateValues(50, 0, 500);
运行后,
一开始我运行代码的时候,出来的总是一片黑乎乎的,就很头疼。改了很久,发现是管道没有及时更新。这里要特别注意的是:在VTK的某些版本中,可能需要手动更新管道。在渲染之前,我们可能需要调用Update
方法来确保所有的过滤器和读取器都处理了数据。
果然我就加上了这一行,就可以正常出来结果了:
vtkSmartPointer obj = vtkSmartPointer::New();
obj->SetFileName("D:\\ct\\20201102113826651_3d\\femur.stl");
obj->Update();// 更新!!!确保STL文件被读取