VTK 医学图像处理---DICOM图像显示
对第一个DICOM显示例子的展开(替换掉vtkImageViewer2类)
两个例子实现的效果对比,其中右侧是对第一个例子展开后的显示效果,展示了一个完整的VTK渲染管线的过程。
目录
简介:
在第000章节VTK的安装中,我们运行了第一个例子,来显示DICOM图像,从而让我们可以快速的看到输入、输出和交互,建立学习兴趣,在该例子中使用vtkImageViewer2来显示DICOM图像,在实际开发中我们基本不会用vtkImageViewer2,因为该类对很多内容进行了封装,使用起来不够灵活,同时也使得我们无法深入理解如何来显示一张DICOM 图像,从这一章开始,我们将完整的熟悉VTK的渲染管线。接下来我们将对vtkImageViewer2进行展开,使用完成的VTK渲染管线来渲染同样的DICOM图像,并开始熟悉VTK中的物体和相机的设置。
本章的组成为,我们先一起来看下vtkImageViewer2显示DICOM图 和 对vtkImageViewer2展开后的代码对比,读者也可以自己新建项目运行下这两种实现方式。然后我们对展开后的代码进行解释和说明。
1 代码对比
1、第000章节VTK的安装中第一个例子的代码
#include <vtkNew.h>
#include <vtkDICOMImageReader.h>
#include <vtkImageViewer2.h>
#include <vtkRenderWindow.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkRenderer.h>
void main()
{
// 读取指定文件夹"D:\\234"内的所有DICOM文件.
vtkNew<vtkDICOMImageReader> reader;
reader->SetDirectoryName("D:\\DicomFiles");
reader->Update();
// 创建一个交互对象,用了处理鼠标和键盘事件
vtkNew<vtkRenderWindowInteractor> renderWindowInteractor;
// 显示DICOM数据
vtkNew<vtkImageViewer2> imageViewer;
imageViewer->SetInputConnection(reader->GetOutputPort());
imageViewer->SetupInteractor(renderWindowInteractor);
// 初始化渲染和相机
imageViewer->Render();
imageViewer->GetRenderer()->ResetCamera();
/* 指定窗口的比较颜色为灰色(.2,.2,.2)*/
imageViewer->GetRenderer()->SetBackground(.2,.2,.2);
/* 设置窗体的大小为宽800,高800*/
imageViewer->GetRenderWindow()->SetSize(800, 800);
/* 设置窗口的标题*/
imageViewer->GetRenderWindow()->SetWindowName("Read DICOM Series");
imageViewer->Render();
renderWindowInteractor->Start();
}
2、 替换掉vtkImageViewer2类后,展开的代码
#include "vtkImageMapToWindowLevelColors.h"
#include "vtkImageActor.h"
#include "vtkImageMapper3D.h"
#include "vtkImageData.h"
#include <vtkNew.h>
#include <vtkDICOMImageReader.h>
#include <vtkRenderWindow.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkRenderer.h>
int ImageSlice = 0;
void main()
{
vtkNew<vtkDICOMImageReader> reader;
reader->SetDirectoryName("D:\\DicomFiles");
reader->Update();
int* ext = reader->GetOutput()->GetExtent();
// map the input image through a lookup table and window / level it
vtkNew<vtkImageMapToWindowLevelColors> windowLevel;
windowLevel->SetInputConnection(reader->GetOutputPort());
//vtkImageActor: draw an image in a rendered 3D scene
vtkNew<vtkImageActor> imageActor;
imageActor->SetDisplayExtent(ext[0], ext[1], ext[2], ext[3], ImageSlice, ImageSlice);
imageActor->GetMapper()->SetInputConnection(windowLevel->GetOutputPort());
// The renderer generates the image
// which is then displayed on the render window.
// It can be thought of as a scene to which the actor is added
vtkNew<vtkRenderer> renderer;
renderer->AddActor(imageActor);
renderer->SetBackground(.2,.2,.2);
// The render window is the actual GUI window
// that appears on the computer screen
vtkNew<vtkRenderWindow> renderWindow;
renderWindow->SetSize(800, 800);
renderWindow->AddRenderer(renderer);
renderWindow->SetWindowName("Dicom Image");
// The render window interactor captures mouse events
// and will perform appropriate camera or actor manipulation
// depending on the nature of the events.
vtkNew<vtkRenderWindowInteractor> interactor;
interactor->SetRenderWindow(renderWindow);
// This starts the event loop and as a side effect causes an initial render.
renderWindow->Render();
interactor->Start();
}
3、两个代码运行的结果是一样的,都显示了同一张DICOM图像,但显示效果又差异,这个差异是有什么造成的呢?是由以下两行代码造成的,可以通过注释掉这两行代码,重新编译运行看下显示效果,结果和第一个例子的效果是一致的。
windowLevel->SetWindow(300);
windowLevel->SetLevel(40);
大家可以在VTK的源代码中搜索 vtkImageViewer2,会搜索到以下两个文件:
打开vtkImageViewer2.h 后,通过查看其源代码,会发现,该类封装了所有展开后例子中使用的类,各个类之间的使用可查看vtkImageViewer2.cxx中的源码,对于初学者来说,vtkImageViewer2.cxx中的很多代码看起来比较费劲,大家可以先跳过这个环节,等后面对VTK熟悉后,再回过头来查看和学习,通过对vtkImageViewer2的学习,大家可以自己来继承vtkObject对象实现简单的封装。
2 VTK完整的渲染管线
VTK渲染管线,通俗来说就像一节一节的水管,把它们对接起来,连接上水龙头就可以流水了。代码里每个对象就是一节水管,把对象的输入和输出连接起来,就形成了管道,最后启动下,类似连接水龙头,就可以看到渲染的图像,并通过鼠标进行交互了。
在上面的例子里面,
- 首先是 vtkDICOMImageReader,该类主要负责读取文件夹内的DICOM文件;
- 然后把读取的数据传递给 vtkImageMapToWindowLevelColors(windowLevel->SetInputConnection(reader->GetOutputPort());); vtkImageMapToWindowLevelColors主要负责把输入的数据转换为RGB或RGBA数据,因为DICOM数据很多时候都是short类型的数据,无法直接在显示器上显示,因此要转换为可在显示器显示的RGB类型数据。
- 将数据转换为RGB类型之后在接上下一个处理的类vtkImageActor( imageActor->GetMapper()->SetInputConnection(windowLevel->GetOutputPort()); ),vtkImageActor负责如何把RGB类型的数据绘制出来;
- 然后将imageActor输入给 vtkRenderer,vtkRenderer可以添加多个Actor, 还可以设置窗口的背景颜色,设置相机等;
- 最后把vtkRenderer传给vtkRenderWindow就结束了,vtkRenderWindow主要用来管理最后显示窗口的。
- 这里还有个vtkRenderWindowInteractor类,它主要是用来交互的,后面会单独有一个章节来讲这个类。
- 最后连接并打开水龙头, 这两行代码完成: renderWindow->Render(); interactor->Start();
3 动手测试一下
- 任务一:修改窗体的背景颜色;
- 任务二:修改 vtkImageMapToWindowLevelColors对象中SetWindow 和 SetLevel的值,观察结果;
- 任务三:通过查找文档,熟悉vtkImageActor 类中方法 SetDisplayExtent的作用是什么?
4 VTK中相机和物体
通常来说,到这里就要开始讲VTK的坐标系统了,但为了让大家更容易上手,我们还是慢慢来。不同于传统的GDI/GDI+在屏幕上绘制图像,VTK有自己的一套坐标系统,在这里我们主要先熟悉下被渲染的物体(DCIM图像)和相机。在上面的代码中是没有相机的,哪来的相机呢,我们慢慢来,反正大家先在心里记住VTK里面是有一个相机的,要想看到物体,那么相机必须对准物体,屏幕上才可以显示物体。类似下图:
在我们的例子里面,物体就是我们要绘制的DICOM图像,既然要相机对准物体才能显示图像,那我们肯定得先知道物体在哪里,然后根据物体的位置来设置相机。我们可以通过下面的代码来获取物体的坐标信息。 先上代码
#include "vtkImageMapToWindowLevelColors.h"
#include "vtkImageActor.h"
#include "vtkImageMapper3D.h"
#include "vtkImageData.h"
#include "vtkNew.h"
#include "vtkDICOMImageReader.h"
#include "vtkRenderWindow.h"
#include "vtkRenderWindowInteractor.h"
#include "vtkRenderer.h"
#include "vtkCamera.h"
int ImageSlice = 0;
void main()
{
vtkNew<vtkDICOMImageReader> reader;
reader->SetDirectoryName("D:\\234");
reader->Update();
int* ext = reader->GetOutput()->GetExtent();
// map the input image through a lookup table and window / level it
vtkNew<vtkImageMapToWindowLevelColors> windowLevel;
windowLevel->SetWindow(300);
windowLevel->SetLevel(40);
windowLevel->SetInputConnection(reader->GetOutputPort());
//vtkImageActor: draw an image in a rendered 3D scene
vtkNew<vtkImageActor> imageActor;
imageActor->SetDisplayExtent(ext[0], ext[1], ext[2], ext[3], ImageSlice, ImageSlice);
imageActor->GetMapper()->SetInputConnection(windowLevel->GetOutputPort());
// The renderer generates the image
// which is then displayed on the render window.
// It can be thought of as a scene to which the actor is added
vtkNew<vtkRenderer> renderer;
renderer->AddActor(imageActor);
renderer->SetBackground(.2,.2,.2);
vtkCamera *cam = renderer->GetActiveCamera();
if (cam)
{
// 获取物体在三维空间中的原点,XYZ范围和中心
vtkImageData* idata = reader->GetOutput();
double* origins = idata->GetOrigin(); // 三维坐标中的起点
double* bounds = idata->GetBounds(); // 包围盒的xyz范围
double* center = idata->GetCenter(); // 中心
// 输出
std::cout << "Actor origins: " << origins[0] << " " << origins[1] << " " << origins[2] << std::endl;
std::cout << "Actor center: " << center[0] << " " << center[1] << " " << center[2] << std::endl;
std::cout << "Actor bounds: " << bounds[0] << " " << bounds[1] << " " << bounds[2]
<< bounds[3] << " " << bounds[4] << " " << bounds[5] << std::endl;
cam->SetFocalPoint(center);
cam->SetPosition(center[0], center[1], center[2] - bounds[5]); // -1 if medical ?
cam->SetViewUp(0, 1, 0);
cam->SetClippingRange(0.1,1000);
renderer->ResetCamera();
}
// The render window is the actual GUI window
// that appears on the computer screen
vtkNew<vtkRenderWindow> renderWindow;
renderWindow->SetSize(512, 512);
renderWindow->AddRenderer(renderer);
renderWindow->SetWindowName("Dicom Image");
// The render window interactor captures mouse events
// and will perform appropriate camera or actor manipulation
// depending on the nature of the events.
vtkNew<vtkRenderWindowInteractor> interactor;
interactor->SetRenderWindow(renderWindow);
// This starts the event loop and as a side effect causes an initial render.
renderWindow->Render();
interactor->Start();
}
在我们增加的代码中,我们先获取物体的坐标信息,然后再获取相机,将相机的焦点对准了物体的中心,将相机的位置设置再Z方向上原理物体bounds[5]的位置上,然后设置相机的正方向指向Y轴,最后设置相机的成像范围(SetClippingRange),之后就可以显示图像了。
小任务:通过VTK的官网文档熟悉 vtkImageData 和 vtkCamera两个类,后面我们会重点介绍vtkImageData类
总结:
主要介绍了完整的VTK渲染管线、VTK中的物体和相机的设置。这三部分内容是需要大家重点去掌握和熟悉的,可以配合VTK电子书的章节一起来学习:Chapter 3 - Computer Graphics Primerexamples.vtk.org/site/VTKBook/03Chapter3/
后面我们会对vtkImageActor 类进行展开,来从更基础的角度看图像的渲染。