现在基于VTK的MFC程序框架已经搭建起来。这一节我们来对上节的程序进行扩展,实现图像的重采样。重采样是图像处理中的一个常见功能,尤其是在医学图像处理中,一些三维图像数据量非常大,对于内存的要求比较高,在处理时为了时间和空间效率的平衡,对图像进行重采样(降采样);另外还有的应用如配准中,为了保持图像的维数一致,还可能对图像进行升采样。但是在目前常使用到的医学图像处理软件中,并没有提供重采样的功能。这个也是本程序开发的初衷,在讲述重采样的原理的同时,提供一个方便的图像重采样软件。
关于图像重采样的内容在VTK在图像处理中的应用(5)一文中也已经介绍过。图像重采样实际上是一个插值的过程。原图像的像素可以看做一些规则分布的离散点,而重采样则意味在这些离散点的基础上通过插值的方式来计算新的离散点的灰度值。最简单的差值方式为最近邻法,即取原图像中距离当前采样点最近的像素值作为当前采样点的输出,这种方式计算简单,但是误差比较大,图像失真比较严重;第二种也是一个最常用的方式是线性差值方式,二维图像对应双线性插值(利用当前新采样点周围的四个原图像像素点进行线性插值),三维图像则是利用周围的8个像素点进行三次线性插值。采用线性插值方式计算相对也比较简单,但是同样会对图像具有平滑效果。另外还有三次多项式插值,B样条插值等方式,这种方式计算量比较大,但是精度比较高。这些方式可以根据不同的需要进行选择。
本篇内容将以线性插值方式为主,实现一个图像重采样应用程序,利用该程序可以方便的进行图像的重采样;同时,在本篇中还会介绍怎样利用CMake在MFC框架添加一个新类。关于如果搭建基于VTK的单文档程序,请参阅基于VTK的MFC应用程序开发(1)。
添加相应的控件,设置对话框及控件ID。界面设计如上。用户首先需要选择改变图像的维数或者图像的像素间隔。这两个参数是相互关联的,即在每个方向上修改其中一个时,另一个也会随之改变,而两者的乘积保持不变。因此这里用户可以选择修改图像的维数还是像素间隔来进行重采样。采样参数共有6个,分别为X方向维数,Y方向维数,Z方向维数,X方向像素间隔,Y方向像素间隔和Z方向像素间隔。像素间隔在医学图像中是一个很重要的参数,表示像素的物理尺寸,普通图像较少使用,默认值为1。维数或者图像像素间隔,每次只能一项。当图像的某个方向的维数为1时,则不能进行重采样。
设置类的名字CResampleDialog,并选择基类为CDialog。这里需要注意的是,由于MFC工程通常代码文件与工程文件是在一个路径下,这里添加对话框类文件后,这些文件的路径将会是在工程路径下即bin目录中。而我们采用CMake来管理工程,代码文件与工程文件是分离的,因此这里不妨将.h文件和.cpp文件的路径设置为代码路径下,如下图所示红色区域显示:
点击完成后,可以发现在代码路径中多出了两个文件,即新添加的类文件。与此同时,在工程的解决方案中,同样会多出了两个文件,如下图中所示。
这样为了使用CMake进行管理,将两个文件从工程的解决方案中删除,并添加至CMakeLists.txt文件中,此时该工程的CMakeLists.txt文件内容如下。使用CMake对MFC工程进行管理时,添加类的过程相对比较麻烦。不知道是否还有更方便的方法,如果有,也欢迎各位赐教。
#----------------------------------------------------------------------------------
cmake_minimum_required( VERSION 2.8 )
project( vtkSDI )
#----------------------------------------------------------------------------------
# 查找并包含VTK工具包
find_package( VTK )
if (VTK_FOUND)
include (${VTK_USE_FILE})
else (VTK_FOUND)
message (FATAL_ERROR "Cannot build without VTK. Please set VTK_DIR")
endif( VTK_FOUND )
#----------------------------------------------------------------------------------
# 这里添加本工程的文件
# 主要分为两部分:
# 一是新建的单文档程序中的非工程文件
# 二是用户后续添加的类文件
SET( PROJECT_SRCS
MainFrm.h
MainFrm.cpp
stdafx.h
stdafx.cpp
vtkSDI.h
vtkSDI.cpp
vtkSDIDoc.h
vtkSDIDoc.cpp
vtkSDIView.h
vtkSDIView.cpp
ResampleDialog.h
ResampleDialog.cpp
targetver.h
Resource.h
vtkSDI.rc
res/vtkSDI.rc2
res/vtkSDI.ico
res/vtkSDIDoc.ico
res/Toolbar.bmp
res/Toolbar256.bmp
)
#----------------------------------------------------------------------------------
# 设置工程包含的vtk模块,这里根据需要加载对应的模块
include("${VTK_DIR}/GUISupport/MFC/VTKMFCSettings.cmake")
set( VTK_LIBS ${vtk_libraries}
vtkMFC
vtkIO
vtkRendering
vtkGraphics
vtkHybrid
vtkFiltering
vtkCommon
vtkImaging
)
VTK_MFC_ADD_DELAYLOAD_FLAGS(CMAKE_EXE_LINKER_FLAGS
vtkMFC.dll
vtkIO.dll
vtkRendering.dll
vtkGraphics.dll
vtkHybrid.dll
vtkFiltering.dll
vtkCommon.dll
vtkImaging.dll
)
#----------------------------------------------------------------------------------
ADD_EXECUTABLE( vtkSDI WIN32 ${PROJECT_SRCS} )
TARGET_LINK_LIBRARIES ( vtkSDI ${VTK_LIBS} )
#----------------------------------------------------------------------------------
主要实现构造函数,初始化函数,Radio消息响应,文本框消息响应,确定和取消按钮消息响应。由于维数与间隔两个参数是互斥的,在修改一个同时另外一个要随之改变。例如一个图像X方向的维数为200,像素间隔为1.0,当用户改变其维数为100时,像素间隔就要相应的变为2.0。
为了尽可能多的支持图像类型,而不是单一的支持一种图像,对图像加载函数进行修改。代码中出现了一个新的类vtkImageReader2Factory,该类能够根据设置的图像路径名字来返回一个可用的图像读取类对象,找不到则返回NULL。这样的话便能够根据不同的图像类型自适应读取图像。
void CvtkSDIDoc::OnFileOpen()
{
// TODO: Add your command handler code here
CString FilePathName;
m_pImageData = NULL;
CFileDialog dlg(TRUE, NULL, NULL,
OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT,
(LPCTSTR)_TEXT("All Files (*.*)|*.*||"),
NULL);
if(dlg.DoModal()==IDOK)
{
FilePathName=dlg.GetPathName();
// Read file
vtkSmartPointer readerFactory =
vtkSmartPointer::New();
vtkImageReader2 * reader =
readerFactory->CreateImageReader2(FilePathName.GetBuffer(0));
if (reader)
{
reader->SetFileName(FilePathName.GetBuffer(0));
reader->Update();
m_pImageData = reader->GetOutput();
reader->Delete();
UpdateAllViews(0);
}
else
{
AfxMessageBox("Image Format is not supported now!");
}
}
else
{
return;
}
}
将菜单的响应函数响应到CvtkSDIDoc类中。当用户调用该菜单时,弹出重采样设置对话框,由用户设置参数。根据用户设置的参数对图像进行重采样,代码如下。
void CvtkSDIDoc::OnSizeResample()
{
// TODO: Add your command handler code here
//get dimension and space of original image
int dims[3];
m_pImageData->GetDimensions(dims);
double spaces[3];
m_pImageData->GetSpacing(spaces);
CResampleDialog dlg(dims[0], dims[1], dims[2], spaces[0], spaces[1], spaces[2]);
if(dlg.DoModal() == IDOK)
{
//get the new dimension and space information
int dx = dlg.m_iDx;
int dy = dlg.m_iDy;
int dz = dlg.m_iDz;
float sx = dlg.m_fSx;
float sy = dlg.m_fSy;
float sz = dlg.m_fSz;
vtkSmartPointer resample =
vtkSmartPointer::New();
resample->SetInput(m_pImageData);
resample->SetAxisOutputSpacing(0, sx);
resample->SetAxisOutputSpacing(1, sy);
resample->SetAxisOutputSpacing(2, sz);
resample->SetInterpolationModeToLinear();
resample->Update();
m_pImageData = NULL;
m_pImageData = resample->GetOutput();
UpdateAllViews(0);
}
}
首先根据当前图像的维数和像素间隔信息初始化重采样对话框,当用户设置完毕新的图像维数和间隔并确定后,开始执行重采样。重采样采用vtkImageResample,该类继承自vtkImageReslice。vtkImageReslice在图像处理章节中介绍过,可以提取图像的任意切面,是一个功能非常强大的类。vtkImageResample中通过SetAxisOutputSpacing()函数设置坐标轴的像素宽度,如0对应x轴,1对应y轴,2对应z轴。而函数SetInterpolationModeToLinear()则可以设置采样的插值函数类型,这里采用线性插值方式。最后将执行的结果进行保存,并更新视图显示新的采样图像。但是,当更新视图后,我们发现图像显示的大小并没有发生变化。这是因为,在改变图像维数的时候,像素间隔也随之改变。维数增大,像素间隔变小;反之,像素间隔增大。而图像的物理大小(维数*像素间隔)是不变的,所以在显示的时候,图像是没有改变的。这与一般的图像采样有所区别。不过,当我们把图像保存为一般的图像格式时,便可以看出变化了。
重新响应菜单File中的Save菜单,实现保存当前图像。这里对保存图像进行分析,获取文件的后缀,根据不同的格式调用相应的类进行保存,主要支持bmp,jpg,png,tif,mhd和mha。
void CvtkSDIDoc::OnFileSave()
{
// TODO: Add your command handler code here
if(m_pImageData == NULL) return;
CString FilePathName;
CFileDialog dlg(FALSE, NULL, NULL,
OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT,
(LPCTSTR)_TEXT("All Files (*.*)|*.*||"),
NULL);
if(dlg.DoModal()==IDOK)
{
FilePathName=dlg.GetPathName();
m_ImageFormat =
FilePathName.Right(FilePathName.GetLength()-FilePathName.ReverseFind('.')-1);
if(m_ImageFormat == "") m_ImageFormat = "mhd";
if(m_ImageFormat.Compare("bmp") == 0)
{
vtkSmartPointer writer
= vtkSmartPointer::New();
writer->SetFileName(FilePathName.GetBuffer(0));
writer->SetInput(m_pImageData);
writer->Write();
}
if(m_ImageFormat.Compare("jpg") == 0)
{
vtkSmartPointer writer
= vtkSmartPointer::New();
writer->SetFileName(FilePathName.GetBuffer(0));
writer->SetInput(m_pImageData);
writer-> Write ();
}
if(m_ImageFormat.Compare("tif") == 0)
{
vtkSmartPointer writer
= vtkSmartPointer::New();
writer->SetFileName(FilePathName.GetBuffer(0));
writer->SetInput(m_pImageData);
writer-> Write ();
}
if(m_ImageFormat.Compare("png") == 0)
{
vtkSmartPointer writer
= vtkSmartPointer::New();
writer->SetFileName(FilePathName.GetBuffer(0));
writer->SetInput(m_pImageData);
writer->Update();
}
if(m_ImageFormat.Compare("mhd") == 0)
{
vtkSmartPointer writer
= vtkSmartPointer::New();
writer->SetFileName(FilePathName.GetBuffer(0));
writer->SetInput(m_pImageData);
writer-> Write ();
}
}
else
{
return;
}
}
下图中左侧图像的大小为1024*768,采用本篇的程序进行重采样为512*768,保存的结果如右图所示。可以看出图像在X方向上维数减少一半。
到此,基于VTK的图像重采样程序已经实现完毕。该程序支持基本的图像格式,并可以将当前图像根据需要进行存储,存储格式目前支持bmp,jpg,png,tif,mhd和mha。对图像进行重采样时,需要选择改变维数或者改变像素间隔,满足用户不同的需求。程序执行效果如下。