摘要:CT等医学图像都是三维立体形式的,本篇文章简要介绍了如何使用VTK工具对这类三维图像进行可视化。

一、VTK在Python中的安装

VTK的全称为Visualization Toolkit,内核是通过C++构造的,但可以自由地通过Python使用VTK,考虑到笔者对python编程语言更为熟悉,因此最先尝试使用Python调用VTK。

使用pip安装命令如下:

  1. pip install vtk

学习VTK的一些例子:

二、对实际数据的三维可视化

VTK进行数据三维可视化时,总体而言顺序为source—filter——mapper——actor——render——renderwindow——interactor,以下通过一些具体的例子进行实际说明。

  1. VTK自带的例子

https://gitlab.kitware.com/vtk/vtk/-/blob/master/Examples/Medical/Python/Medical3.py
对头骨数据的可视化
读入的数据是图像序列文件,每张图像代表每一帧,均为RAW格式的文件
image.png

image.png

  1. 医院收集的DICOM文件

由于从医院采集的CT DICOM文件为压缩格式的,无法使用vtk.vtkDICOMImageReader()进行读取,因此考虑将一次扫描中所有帧DICOM文件合并成为一份nii文件

  1. import numpy
  2. import os
  3. import cv2
  4. import pydicom
  5. import SimpleITK
  6. # 路径和列表声明
  7. # 与python文件同一个目录下的文件夹,存储dicom文件,该文件路径最好不要含有中文
  8. PathDicom = "D:/Data_windows/3D-vtk/5"
  9. # 与python文件同一个目录下的文件夹,用来存储nii文件,该文件路径最好不要含有中文
  10. SaveNii = "D:/Data_windows/3D-vtk/5-nii"
  11. lstFilesDCM = []
  12. # 将PathDicom文件夹下的dicom文件地址读取到lstFilesDCM中
  13. for dirName, subdirList, fileList in os.walk(PathDicom):
  14. for filename in fileList:
  15. if ".dcm" in filename.lower(): # 判断文件是否为dicom文件
  16. print(filename)
  17. lstFilesDCM.append(os.path.join(dirName, filename)) # 加入到列表中
  18. # 第一步:将第一张图片作为参考图片,并认为所有图片具有相同维度
  19. RefDs = pydicom.read_file(lstFilesDCM[0]) # 读取第一张dicom图片
  20. # 第二步:得到dicom图片所组成3D图片的维度
  21. ConstPixelDims = (int(RefDs.Rows), int(RefDs.Columns), len(lstFilesDCM)) # ConstPixelDims是一个元组
  22. # 第三步:得到x方向和y方向的Spacing并得到z方向的层厚
  23. ConstPixelSpacing = (float(RefDs.PixelSpacing[0]), float(RefDs.PixelSpacing[1]), float(RefDs.SliceThickness))
  24. # 第四步:得到图像的原点
  25. Origin = RefDs.ImagePositionPatient
  26. # 根据维度创建一个numpy的三维数组,并将元素类型设为:pixel_array.dtype
  27. ArrayDicom = numpy.zeros(ConstPixelDims, dtype=RefDs.pixel_array.dtype) # array is a numpy array
  28. # 第五步:遍历所有的dicom文件,读取图像数据,存放在numpy数组中
  29. i = 0
  30. for filenameDCM in lstFilesDCM:
  31. ds = pydicom.read_file(filenameDCM)
  32. ArrayDicom[:, :, lstFilesDCM.index(filenameDCM)] = ds.pixel_array
  33. # cv2.imwrite("out_" + str(i) + ".png", ArrayDicom[:, :, lstFilesDCM.index(filenameDCM)])
  34. i += 1
  35. # 第六步:对numpy数组进行转置,即把坐标轴(x,y,z)变换为(z,y,x),这样是dicom存储文件的格式,即第一个维度为z轴便于图片堆叠
  36. ArrayDicom = numpy.transpose(ArrayDicom, (2, 0, 1))
  37. # 第七步:将现在的numpy数组通过SimpleITK转化为mhd和raw文件
  38. sitk_img = SimpleITK.GetImageFromArray(ArrayDicom, isVector=False)
  39. sitk_img.SetSpacing(ConstPixelSpacing)
  40. sitk_img.SetOrigin(Origin)
  41. SimpleITK.WriteImage(sitk_img, os.path.join(SaveRawDicom, "sample" + ".mhd"))
  42. SimpleITK.WriteImage(sitk_img, os.path.join(SaveNii, "sample" + ".nii"))

之后使用VTK对得到的nii文件进行3D可视化

  1. #// 3d reconstruction used by vtk
  2. from vtk.util.vtkImageImportFromArray import *
  3. import vtk
  4. import SimpleITK as sitk
  5. import numpy as np
  6. import time
  7. # path = '../vtk/nii_data_low/1_1.nii' #segmentation volume
  8. path = "D:\\Data_windows\\3D-vtk\\5-nii\\sample.nii"#segmentation volume
  9. ds = sitk.ReadImage(path) #读取nii数据的第一个函数sitk.ReadImage
  10. print('ds: ',ds)
  11. data = sitk.GetArrayFromImage(ds) #把itk.image转为array
  12. print('data: ',data)
  13. print('shape_of_data',data.shape)
  14. spacing = ds.GetSpacing() #三维数据的间隔
  15. print('spacing_of_data',spacing)
  16. # data = data[50:]
  17. # data = data[:,:,300:]
  18. srange = [np.min(data),np.max(data)]
  19. print('shape_of_data_chenged',data.shape)
  20. img_arr = vtkImageImportFromArray() #创建一个空的vtk类-----vtkImageImportFromArray
  21. print('img_arr: ',img_arr)
  22. img_arr.SetArray(data) #把array_data塞到vtkImageImportFromArray(array_data)
  23. img_arr.SetDataSpacing(spacing) #设置spacing
  24. origin = (0,0,0)
  25. img_arr.SetDataOrigin(origin) #设置vtk数据的坐标系原点
  26. img_arr.Update()
  27. # srange = img_arr.GetOutput().GetScalarRange()
  28. print('spacing: ',spacing)
  29. print('srange: ',srange)
  30. # 键盘控制交互式操作
  31. class KeyPressInteractorStyle(vtk.vtkInteractorStyleTrackballCamera):
  32. def __init__(self,parent=None):
  33. self.parent = vtk.vtkRenderWindowInteractor()
  34. if(parent is not None):
  35. self.parent = parent
  36. self.AddObserver("KeyPressEvent",self.keyPress)
  37. def keyPress(self,obj,event):
  38. key = self.parent.GetKeySym()
  39. if key == 'Up':
  40. gradtfun.AddPoint(-100, 1.0)
  41. gradtfun.AddPoint(10, 1.0)
  42. gradtfun.AddPoint(20, 1.0)
  43. volumeProperty.SetGradientOpacity(gradtfun)
  44. #下面这一行是关键,实现了actor的更新
  45. renWin.Render()
  46. if key == 'Down':
  47. tfun.AddPoint(1129, 0)
  48. tfun.AddPoint(1300.0, 0.1)
  49. tfun.AddPoint(1600.0, 0.2)
  50. tfun.AddPoint(2000.0, 0.1)
  51. tfun.AddPoint(2200.0, 0.1)
  52. tfun.AddPoint(2500.0, 0.1)
  53. tfun.AddPoint(2800.0, 0.1)
  54. tfun.AddPoint(3000.0, 0.1)
  55. #下面这一行是关键,实现了actor的更新
  56. renWin.Render()
  57. def StartInteraction():
  58. renWin.SetDesiredUpdateRate(10)
  59. def EndInteraction():
  60. renWin.SetDesiredUpdateRate(0.001)
  61. def ClipVolumeRender(obj):
  62. obj.GetPlanes(planes)
  63. volumeMapper.SetClippingPlanes(planes)
  64. ren = vtk.vtkRenderer()
  65. renWin= vtk.vtkRenderWindow()
  66. renWin.AddRenderer(ren) #把一个空的渲染器添加到一个空的窗口上
  67. renWin.AddRenderer(ren)
  68. iren = vtk.vtkRenderWindowInteractor()
  69. iren.SetRenderWindow(renWin) #把上面那个窗口加入交互操作
  70. iren.SetInteractorStyle(KeyPressInteractorStyle(parent = iren)) #在交互操作里面添加这个自定义的操作例如up,down
  71. min = srange[0]
  72. max = srange[1]
  73. # diff = max - min #体数据极差
  74. # slope = 4000 / diff
  75. # inter = -slope * min
  76. # shift = inter / slope
  77. # print(min, max, slope, inter, shift) #这几个数据后面有用
  78. diff = max - min #体数据极差
  79. inter = 4200 / diff
  80. shift = -min
  81. print(min, max, inter, shift) #这几个数据后面有用
  82. shifter = vtk.vtkImageShiftScale() # 对偏移和比例参数来对图像数据进行操作 数据转换,之后直接调用shifter
  83. shifter.SetShift(shift)
  84. shifter.SetScale(inter)
  85. shifter.SetOutputScalarTypeToUnsignedShort()
  86. shifter.SetInputData(img_arr.GetOutput())
  87. shifter.ReleaseDataFlagOff()
  88. shifter.Update()
  89. tfun = vtk.vtkPiecewiseFunction() # 不透明度传输函数---放在tfun
  90. tfun.AddPoint(1129, 0)
  91. tfun.AddPoint(1300.0, 0.1)
  92. tfun.AddPoint(1600.0, 0.12)
  93. tfun.AddPoint(2000.0, 0.13)
  94. tfun.AddPoint(2200.0, 0.14)
  95. tfun.AddPoint(2500.0, 0.16)
  96. tfun.AddPoint(2800.0, 0.17)
  97. tfun.AddPoint(3000.0, 0.18)
  98. gradtfun = vtk.vtkPiecewiseFunction() # 梯度不透明度函数---放在gradtfun
  99. gradtfun.AddPoint(-1000, 9)
  100. gradtfun.AddPoint(0.5, 9.9)
  101. gradtfun.AddPoint(1, 10)
  102. ctfun = vtk.vtkColorTransferFunction() # 颜色传输函数---放在ctfun
  103. ctfun.AddRGBPoint(0.0, 0.5, 0.0, 0.0)
  104. ctfun.AddRGBPoint(600.0, 1.0, 0.5, 0.5)
  105. ctfun.AddRGBPoint(1280.0, 0.9, 0.2, 0.3)
  106. ctfun.AddRGBPoint(1960.0, 0.81, 0.27, 0.1)
  107. ctfun.AddRGBPoint(2200.0, 0.9, 0.2, 0.3)
  108. ctfun.AddRGBPoint(2500.0, 1, 0.5, 0.5)
  109. ctfun.AddRGBPoint(3024.0, 0.5, 0.5, 0.5)
  110. volumeMapper = vtk.vtkGPUVolumeRayCastMapper() #映射器volumnMapper使用vtk的管线投影算法
  111. volumeMapper.SetInputData(shifter.GetOutput()) #向映射器中输入数据:shifter(预处理之后的数据)
  112. volumeProperty = vtk.vtkVolumeProperty() #创建vtk属性存放器,向属性存放器中存放颜色和透明度
  113. volumeProperty.SetColor(ctfun)
  114. volumeProperty.SetScalarOpacity(tfun)
  115. # volumeProperty.SetGradientOpacity(gradtfun)
  116. volumeProperty.SetInterpolationTypeToLinear() #???
  117. volumeProperty.ShadeOn()
  118. newvol = vtk.vtkVolume() #演员
  119. newvol.SetMapper(volumeMapper)
  120. newvol.SetProperty(volumeProperty)
  121. outline = vtk.vtkOutlineFilter()
  122. outline.SetInputConnection(shifter.GetOutputPort())
  123. outlineMapper = vtk.vtkPolyDataMapper()
  124. outlineMapper.SetInputConnection(outline.GetOutputPort())
  125. outlineActor = vtk.vtkActor()
  126. outlineActor.SetMapper(outlineMapper)
  127. ren.AddActor(outlineActor)
  128. #
  129. outlineActor.GetProperty().SetOpacity(0.1)
  130. #
  131. ren.AddVolume(newvol)
  132. ren.SetBackground(1, 1, 1)
  133. renWin.SetSize(600, 600)
  134. #
  135. # Now create a lookup table that consists of the full hue circle (from
  136. # HSV).
  137. hueLut = vtk.vtkLookupTable()
  138. hueLut.SetTableRange(0, 2000)
  139. hueLut.SetHueRange(0, 1)
  140. hueLut.SetSaturationRange(1, 1)
  141. hueLut.SetValueRange(1, 1)
  142. hueLut.Build()
  143. axialColors = vtk.vtkImageMapToColors()
  144. axialColors.SetInputConnection(shifter.GetOutputPort())
  145. axialColors.SetLookupTable(hueLut)
  146. axial = vtk.vtkImageActor()
  147. axial.GetMapper().SetInputConnection(axialColors.GetOutputPort())
  148. axial.SetDisplayExtent(0, 512, 0, 512, 23, 23)
  149. ren.AddActor(axial)
  150. #
  151. planes = vtk.vtkPlanes()
  152. boxWidget = vtk.vtkBoxWidget()
  153. boxWidget.SetInteractor(iren)
  154. boxWidget.SetPlaceFactor(1.0)
  155. boxWidget.PlaceWidget(0,0,0,0,0,0)
  156. boxWidget.InsideOutOn()
  157. boxWidget.AddObserver("StartInteractionEvent", StartInteraction)
  158. boxWidget.AddObserver("InteractionEvent", ClipVolumeRender)
  159. boxWidget.AddObserver("EndInteractionEvent", EndInteraction)
  160. outlineProperty = boxWidget.GetOutlineProperty()
  161. outlineProperty.SetRepresentationToWireframe()
  162. outlineProperty.SetAmbient(1.0)
  163. outlineProperty.SetAmbientColor(1, 1, 1)
  164. outlineProperty.SetLineWidth(9)
  165. selectedOutlineProperty = boxWidget.GetSelectedOutlineProperty()
  166. selectedOutlineProperty.SetRepresentationToWireframe()
  167. selectedOutlineProperty.SetAmbient(1.0)
  168. selectedOutlineProperty.SetAmbientColor(1, 0, 0)
  169. selectedOutlineProperty.SetLineWidth(3)
  170. ren.ResetCamera()
  171. iren.Initialize()
  172. renWin.Render()
  173. iren.Start()

可视化的结果如下图所示:
3.PNG

  1. 对三维的binary mask文件进行可视化(tif格式)
import vtk

reader = vtk.vtkTIFFReader()
reader.SetFileDimensionality(3)
reader.SetFileName("masked.tif")
reader.SetDataExtent(0,512,0,512,0,134)
reader.SetDataSpacing(0.9765625,0.9765625,3) 


contour=vtk.vtkContourFilter()
contour.SetInputConnection(reader.GetOutputPort())
contour.ComputeNormalsOn()
contour.SetValue(0,1)

mapper = vtk.vtkPolyDataMapper()
mapper.SetInputConnection(contour.GetOutputPort())
mapper.ScalarVisibilityOff()

actor = vtk.vtkActor()
actor.SetMapper(mapper)

renderer=vtk.vtkRenderer()
renderer.SetBackground([0.1, 0.2, 0.3])
renderer.AddActor(actor)

window = vtk.vtkRenderWindow()
window.SetSize(1000, 1000)
window.AddRenderer(renderer)

interactor = vtk.vtkRenderWindowInteractor()
interactor.SetRenderWindow(window)

window.Render()
interactor.Initialize()
interactor.Start()

masked.tif 该3D binary mask文件可视化的结果如下所示:
捕获.PNG


PS:在kaggle上找到一个教程,有关对DICOM CT文件的VTK可视化教程,可供参考:https://www.kaggle.com/wrrosa/advanced-dicom-ct-3d-visualizations-with-vtk