肺部CT图像分割及重建系统
一、 系统概况
我们实现了一个系统,可以从CT图像中将肺部从胸腔中分离出来,并且通过三维重建实现可视化。该系统是基于Visual Studio 2013平台,借助VTK-7.0和Qt5.6开源库通过C++语言实现。
二、 系统设计
肺部CT图像分割及重建系统的实现需要几个方面的工作,一是CT图像的肺部分割;二是CT图像的三维重建;三是可视化界面的设计。
根据工作内容的不同,需要用到不同的开源库。我们通过VTK实现了CT图像的三维重建,包含体绘制和面绘制两种重建算法;通过Qt实现了UI的设计。因此在windows10环境下用VS2013编译支持Qt的VTK库,是前期的准备工作。
在肺部分割这一部分工作上,我们采用以阈值分割为主,其余简单图像处理操作如滤波操作等为辅,进行肺部的分割。
三、系统实现(含源代码)
3.1 前期准备工作——Windows10环境下用VS2013编译支持Qt的VTK库
网上有大量的相关文章,但由于VTK,VS,Qt的版本不同,可能导致Qt与VTK的联动失败。在此,我们参考了两位博主的搭建方式:
(1) http://www.cnblogs.com/tianhu9102/p/7641397.html
(2) https://blog.****.net/u011017966/article/details/40984473
这两篇文章对应两种不同的搭建方式,一种是在两种库编译成功后通过直接添加库目录,包含目录的方法实现VTK与Qt联动;另一种则跳过了添加库目录,包含目录等繁琐操作,程序全部用cmake来管理,直接通过cmake进行编译实现一个工程文件。
经过验证,两种方法均可实现VTK + QT的联动。
3.2三维重建的实现
我们采用了两种三维重建方法:体绘制和面绘制。
面绘制是将感兴趣的部分以等值面的形式抽取出来便于利用真实感技术,通过任意旋转和变换光照效果来生成高质量的三维图像,并可以方便的对其进行观察和分析。
面绘制的代码如下:
void MainWindow::open(){
//抽取等值面为骨头的信息
vtkSmartPointer< vtkMarchingCubes > boneExtractor =
vtkSmartPointer< vtkMarchingCubes >::New();
boneExtractor->SetInputConnection(threshould->GetOutputPort());
//boneExtractor->SetValue(0, 500); //设置提取的等值信息
boneExtractor->SetValue(0, 1000);
boneExtractor->SetNumberOfContours(1);
boneExtractor->Update();
//剔除旧的或废除的数据单元,提高绘制速度(可略去这一步)
vtkSmartPointer< vtkStripper > boneStripper =
vtkSmartPointer< vtkStripper >::New(); //三角带连接
boneStripper->SetInputConnection(boneExtractor->GetOutputPort());
boneStripper->Update();
//建立映射
vtkSmartPointer< vtkPolyDataMapper > boneMapper =
vtkSmartPointer< vtkPolyDataMapper >::New();
boneMapper->SetInputData(boneStripper->GetOutput());
boneMapper->ScalarVisibilityOff();
//建立角色
vtkSmartPointer< vtkActor > bone =
vtkSmartPointer< vtkActor >::New();
bone->SetMapper(boneMapper);
bone->GetProperty()->SetDiffuseColor(.1, .94, .52);
bone->GetProperty()->SetSpecular(.3);
bone->GetProperty()->SetSpecularPower(20);
bone->GetProperty()->SetOpacity(1.0);
bone->GetProperty()->SetColor(1,1,1);
bone->GetProperty()->SetRepresentationToSurface();
//定义绘制器
vtkSmartPointer< vtkRenderer > aRenderer =
vtkSmartPointer< vtkRenderer >::New();
//定义绘制窗口
vtkSmartPointer< vtkRenderWindow > renWin = ui->qvtkWidget_Volume->GetRenderWindow();
//vtkSmartPointer< vtkRenderWindow >::New();
renWin->AddRenderer(aRenderer);
//定义窗口交互器
vtkSmartPointer< vtkRenderWindowInteractor > iren =
vtkSmartPointer< vtkRenderWindowInteractor >::New();
iren->SetRenderWindow(renWin);
//创建一个camera
vtkSmartPointer< vtkCamera > aCamera =
vtkSmartPointer< vtkCamera >::New();
aCamera->SetViewUp(0, 0, -1);
aCamera->SetPosition(0, 1, 0);
aCamera->SetFocalPoint(0, 0, 0);
aRenderer->AddActor(bone);
aRenderer->SetActiveCamera(aCamera);
aRenderer->ResetCamera();
aCamera->Dolly(1.5);
aRenderer->SetBackground(0, 0, 0);
aRenderer->ResetCameraClippingRange();
vtkInteractorStyleTrackballCamera *style = //设置交互方式
vtkInteractorStyleTrackballCamera::New();
iren->SetInteractorStyle(style);
iren->Initialize();
iren->Start();
}
体绘制的原理和面绘制完全不相同。面绘制需要生成中间图元,而体绘制则是直接在原图上进行绘制,内容需求较面绘制小。每切换一个视角需要重新对所有的像素点进行颜色和透明度计算,需要时间比面绘制长。VTK中基于体绘制实现三维重建,使用的是光线投射法(Ray-casting)。
体绘制的具体代码如下:
void MainWindow::volume(){
VTK_MODULE_INIT(vtkRenderingVolumeOpenGL2);
vtkSmartPointer<vtkFixedPointVolumeRayCastMapper> volumeMapper =
vtkSmartPointer<vtkFixedPointVolumeRayCastMapper>::New();
volumeMapper->SetInputConnection(threshould->GetOutputPort());
volumeMapper->SetBlendModeToComposite();
volumeMapper->SetSampleDistance(0.3);
volumeMapper->AutoAdjustSampleDistancesOff();
vtkSmartPointer<vtkVolumeProperty> volumeProperty =
vtkSmartPointer<vtkVolumeProperty>::New();
volumeProperty->SetInterpolationTypeToLinear();
volumeProperty->ShadeOn(); //打开或者关闭阴影测试
volumeProperty->SetAmbient(0.2);
volumeProperty->SetDiffuse(1.2); //漫反射
volumeProperty->SetSpecular(0.1); //镜面反射
volumeProperty->SetSpecularPower(10);
//设置不透明度
vtkSmartPointer<vtkPiecewiseFunction> compositeOpacity =
vtkSmartPointer<vtkPiecewiseFunction>::New();
compositeOpacity->AddPoint(70, 0.00);
compositeOpacity->AddPoint(90, 0.40);
compositeOpacity->AddPoint(180, 0.60);
volumeProperty->SetScalarOpacity(compositeOpacity); //设置不透明度传输函数
//设置梯度不透明属性
vtkSmartPointer<vtkPiecewiseFunction> volumeGradientOpacity =
vtkSmartPointer<vtkPiecewiseFunction>::New();
volumeGradientOpacity->AddPoint(10, 0.0);
volumeGradientOpacity->AddPoint(90, 0.5);
volumeGradientOpacity->AddPoint(100, 1.0);
volumeProperty->SetGradientOpacity(volumeGradientOpacity);//设置梯度不透明度效果对比
//设置颜色属性
vtkSmartPointer<vtkColorTransferFunction> color =
vtkSmartPointer<vtkColorTransferFunction>::New();
color->AddRGBPoint(64.00, 1.00, 0.52, 0.30);
volumeProperty->SetColor(color);
/********************************************************************************/
vtkSmartPointer<vtkVolume> volume =
vtkSmartPointer<vtkVolume>::New();
volume->SetMapper(volumeMapper);
volume->SetProperty(volumeProperty);
vtkSmartPointer<vtkRenderer> ren = vtkSmartPointer<vtkRenderer>::New();
ren->SetBackground(0, 0, 0);
ren->AddVolume(volume);
vtkSmartPointer<vtkRenderWindow> rw =ui->qvtkWidget_Volume_6->GetRenderWindow();
rw->AddRenderer(ren);
rw->Render();
vtkSmartPointer<vtkRenderWindowInteractor> rwi =
vtkSmartPointer<vtkRenderWindowInteractor>::New();
rwi->SetRenderWindow(rw);
vtkInteractorStyleTrackballCamera *st = //设置交互方式
vtkInteractorStyleTrackballCamera::New();
rwi->SetInteractorStyle(st);
/********************************************************************************/
ren->ResetCamera();
rw->Render();
}
3.3 UI设计
Qt的UI界面设计如下图所示:
我们主要用到了Qt的几个常用Widget:
PushButton:用于启动某个功能
horizontalScrollBar: 用于手动调整上界阈值和下界阈值
textEdit:用于输入图像分割的矩形框大小(后面分割会提到)
textBrowser: 用于显示程序运行状态
3.4 分割操作的实现
为了尽可能提高分割的效果,我们采用了多管齐下的方法。
首先,我们设置了以交互的方式,让用户输入上界阈值和下界阈值,则CT图像的CT值在这个阈值内的保留原值不变,小于下界阈值的变黑,大于上界阈值的变白。
void MainWindow::segmentBetween(){
int upper, lower;
// 从滑条里分别获取上界和下界阈值
upper = ui->horizontalScrollBar->value();
lower = ui->horizontalScrollBar_2->value();
threshould->ThresholdBetween(lower, upper);// 利用ThresholdBetween进行分割
threshould->SetInValue(1024);
threshould->SetOutValue(-1024);
threshould->Update(); //算法执行后必须添加的更新消息
slice();
}
其次,因为CT原图像中有CT机床等干扰,我们在原CT图像上进行截取,只需输入矩形框的两个对角顶点的坐标,就可以轻松截取框内的图像,框外的图像全部置黑。
最后,我们发现通过简单的阈值分割并不能很好地去除一些细节上的噪声和胸腔组织,因此我们写了一个简单的滤波算法,进行滤波操作,根据该像素点的邻域是否具有像素值进行判断,从而极大程度上去除了噪声,取得了不错的分割效果。
void MainWindow::Filter(){
int dims[3];
int upperNum, upperNum2, upperNum3, upperNum4;
QString upper, upper2, upper3, upper4;
upper = ui->textEdit->document()->toPlainText();
upperNum = upper.toInt();
upper2 = ui->textEdit_2->document()->toPlainText();
upperNum2 = upper2.toInt();
upper3 = ui->textEdit_3->document()->toPlainText();
upperNum3 = upper3.toInt();
upper4 = ui->textEdit_4->document()->toPlainText();
upperNum4 = upper4.toInt();
reader->GetOutput()->GetDimensions(dims);
for (int k = 0; k < dims[2] ; k++)
{
for (int j = 0; j < dims[1] ; j++)
{
for (int i = 0; i < dims[0] ; i++)
{
short *pixel = (short *)(threshould->GetOutput()->GetScalarPointer(i, j, k));
if (j < upperNum2 || j > upperNum4)
*pixel = -1024;
if (i < upperNum || i > upperNum3)
*pixel = -1024;
if ((j > 5) && (j < dims[1] - 6))
{
short *pixel2 = (short *)(threshould->GetOutput()->GetScalarPointer(i, j + 5, k));
short *pixel3 = (short *)(threshould->GetOutput()->GetScalarPointer(i, j - 5, k));
if ((*pixel2 < -500) && (*pixel3 <-500))
*pixel = -1024;
}
}
}
}
threshould->Update();
slice();
}
四、结果展示
分割:手动设置上界阈值和下界阈值,并在去噪输入框内输入矩形框的对角顶点坐标,则可达到分割的效果。
显示:左上为三维重建的体绘制(左)和面绘制(右),可以通过鼠标拖动,从不同视角观看肺部结构。右边为水平面,矢状面,冠状面的显示,通过拖动进度条可以看不同层的截面显示。