在Python的3D图像处理中,通常用numpy array来进行非常方便的计算或者转化,这里记录一下numpy数据的VTK可视化基本流程,包括面绘制(Surfase Rendering)和体绘制(Volume Rendering)。除去数据格式转化,面绘制和体绘制在C++中也是类似的处理方法。
vtkImageData
首先得把numpy数据转成vtk里可以用的格式:numpy array -> vtkIImageData。这里的numpy array是一个离散的三维空间数据场,0代表背景,非0代表前景点。
numpy_to_vtk
可以直接使用vtk.util.numpy_support
里的转换方法,非常方便。这是它的说明文档:
numpy_to_vtk(num_array, deep=0, array_type=None)
Converts a contiguous real numpy Array to a VTK array object.
This function only works for real arrays that are contiguous.
Complex arrays are NOT handled. It also works for multi-component
arrays. However, only 1, and 2 dimensional arrays are supported.
This function is very efficient, so large arrays should not be a
problem.
If the second argument is set to 1, the array is deep-copied from
from numpy. This is not as efficient as the default behavior
(shallow copy) and uses more memory but detaches the two arrays
such that the numpy array can be released.
WARNING: You must maintain a reference to the passed numpy array, if
the numpy data is gc'd and VTK will point to garbage which will in
the best case give you a segfault.
Parameters
----------
- num_array : a contiguous 1D or 2D, real numpy array.
对于3D array,可以用flatten
或者ravel
先转成1D array就可以使用了。这里的1D array得是C order(row-major order),最好使用deep copy以免出现一些内存管理的问题。
import numpy as np
import vtk
from vtk.util import numpy_support
# numpy_data is a 3D numpy array
shape = numpy_data.shape[::-1]
vtk_data = numpy_support.numpy_to_vtk(numpy_data.ravel(), 1, vtk.VTK_SHORT)
vtk_image_data = vtk.vtkImageData()
vtk_image_data.SetDimensions(shape)
vtk_image_data.SetSpacing(spacing)
vtk_image_data.SetOrigin(origin)
vtk_image_data.GetPointData().SetScalars(vtk_data)
# vtk_image_data: ready to use
vtkImageImport
使用Python也可以用vtkImageImport
来直接导入C array,就是要先把numpy array变成string。
import numpy as np
import vtk
numpy_str = numpy_data.astype(np.int16).tostring()
x_extent = numpy_data.shape[2]
y_extent = numpy_data.shape[1]
z_extent = numpy_data.shape[0]
image_import = vtk.vtkImageImport()
image_import.SetImportVoidPointer(numpy_str, len(numpy_str))
# 也可以使用CopyImportVoidPointer() 会copy一份numpy_str
image_import.SetWholeExtent(0, x_extent-1, 0, y_extent-1, 0, z_extent-1)
image_import.SetDataExtent(0, x_extent-1, 0, y_extent-1, 0, z_extent-1)
image_import.SetDataScalarTypeToShort() # 根据需求指定数据类型
image_import.SetNumberOfScalarComponents(1)
# 如果是RGB数据的话,SetNumberOfScalarComponents(3)
image_import.Update()
vtk_image_data = vtk.vtkImageData()
vtk_image_data.SetSpacing(spacing)
vtk_image_data.SetOrigin(origin)
# vtk_image_data: ready to use
参考:https://www.cnblogs.com/XDU-Lakers/p/10822840.html
这个作者总结的很全面,还有代码示例
面绘制的意思是根据数据建立三角网格模型,再渲染网格。通俗来讲就是根据算法进行轮廓识别和提取,生成了三维物体的表面,这个表面本质是由非常多的小三角面构成。
如何抽取物体表面(轮廓/等值面)?VTK提供了一些算法(filter):
vtkMarchingCubes
vtkMarchingSquares
vtkDiscreteMarchingCubdes
,这个filter会给每个cell生成对应的scalar值,可以用vtkThreshold
来得到不同的标签vtkContourFilter
vtkPolyDataNormals
得到vtkFlyingEdges3D
vtkFlyingEdges2D
什么是轮廓值?其实就是根据这个值来提取轮廓。比如对于一个二值图像,所有值为1的点是前景点(要显示的物体),所有值为0的点是背景点。此时轮廓值是1,算法会找到值为1的点所构成物体的轮廓。如果是多标签图像,可以把多个标签的值当作轮廓值。如果是其它医学图像,可以根据需求取不同的值,比如人体皮肤所对应的value值为500,人体骨骼所对应的value值为1150。设置多个轮廓值,这样多个等值面都可以被提取出来。
经过这些filter,数据就被处理成了表现三维物体表面的vtkPolyData
,再经过vtkPolyDataMapper
,vtkActor
、vtkRenderer
、vtkRenderWindow
、vtkRenderWindowInteractor
显示出来。
官方使用范例:(多标签数据 面绘制 上色 / multilabel data)
# https://kitware.github.io/vtk-examples/site/Python/Modelling/SmoothDiscreteMarchingCubes/
import vtk
def main():
n = 20
radius = 8
blob = make_blob(n, radius)
discrete = vtk.vtkDiscreteMarchingCubes()
discrete.SetInputData(blob)
discrete.GenerateValues(n, 1, n)
smoothing_iterations = 15
pass_band = 0.001
feature_angle = 120.0
smoother = vtk.vtkWindowedSincPolyDataFilter()
smoother.SetInputConnection(discrete.GetOutputPort())
smoother.SetNumberOfIterations(smoothing_iterations)
smoother.BoundarySmoothingOff()
smoother.FeatureEdgeSmoothingOff()
smoother.SetFeatureAngle(feature_angle)
smoother.SetPassBand(pass_band)
smoother.NonManifoldSmoothingOn()
smoother.NormalizeCoordinatesOn()
smoother.Update()
lut = make_colors(n)
mapper = vtk.vtkPolyDataMapper()
mapper.SetInputConnection(smoother.GetOutputPort())
mapper.SetLookupTable(lut)
mapper.SetScalarRange(0, lut.GetNumberOfColors())
# Create the RenderWindow, Renderer and both Actors
#
ren = vtk.vtkRenderer()
ren_win = vtk.vtkRenderWindow()
ren_win.AddRenderer(ren)
ren_win.SetWindowName('SmoothDiscreteMarchingCubes')
iren = vtk.vtkRenderWindowInteractor()
iren.SetRenderWindow(ren_win)
actor = vtk.vtkActor()
actor.SetMapper(mapper)
ren.AddActor(actor)
colors = vtk.vtkNamedColors()
ren.SetBackground(colors.GetColor3d('Burlywood'))
ren_win.Render()
iren.Start()
def make_blob(n, radius):
blob_image = vtk.vtkImageData()
max_r = 50 - 2.0 * radius
random_sequence = vtk.vtkMinimalStandardRandomSequence()
random_sequence.SetSeed(5071)
for i in range(0, n):
sphere = vtk.vtkSphere()
sphere.SetRadius(radius)
x = random_sequence.GetRangeValue(-max_r, max_r)
random_sequence.Next()
y = random_sequence.GetRangeValue(-max_r, max_r)
random_sequence.Next()
z = random_sequence.GetRangeValue(-max_r, max_r)
random_sequence.Next()
sphere.SetCenter(int(x), int(y), int(z))
sampler = vtk.vtkSampleFunction()
sampler.SetImplicitFunction(sphere)
sampler.SetOutputScalarTypeToFloat()
sampler.SetSampleDimensions(100, 100, 100)
sampler.SetModelBounds(-50, 50, -50, 50, -50, 50)
thres = vtk.vtkImageThreshold()
thres.SetInputConnection(sampler.GetOutputPort())
thres.ThresholdByLower(radius * radius)
thres.ReplaceInOn()
thres.ReplaceOutOn()
thres.SetInValue(i + 1)
thres.SetOutValue(0)
thres.Update()
if i == 0:
blob_image.DeepCopy(thres.GetOutput())
max_value = vtk.vtkImageMathematics()
max_value.SetInputData(0, blob_image)
max_value.SetInputData(1, thres.GetOutput())
max_value.SetOperationToMax()
max_value.Modified()
max_value.Update()
blob_image.DeepCopy(max_value.GetOutput())
return blob_image
def make_colors(n):
"""
Generate some random colors
:param n: The number of colors.
:return: The lookup table.
"""
lut = vtk.vtkLookupTable()
lut.SetNumberOfColors(n)
lut.SetTableRange(0, n - 1)
lut.SetScaleToLinear()
lut.Build()
lut.SetTableValue(0, 0, 0, 0, 1)
random_sequence = vtk.vtkMinimalStandardRandomSequence()
random_sequence.SetSeed(5071)
for i in range(1, n):
r = random_sequence.GetRangeValue(0.4, 1)
random_sequence.Next()
g = random_sequence.GetRangeValue(0.4, 1)
random_sequence.Next()
b = random_sequence.GetRangeValue(0.4, 1)
random_sequence.Next()
lut.SetTableValue(i, r, g, b, 1.0)
return lut
if __name__ == '__main__':
main()
体绘制是为每一个体素指定一个不透明度,并考虑每一个体素对光线的透射、发射和反射作用,实现三维重建。简单来说就是能够更完整的展示出整个物体,而不仅仅是表面。
VTK采用的是光线投射算法。光线投射算法(Ray-casting)原理:从图像平面的每个像素都沿着视线方向发出一条射线,此射线穿过体数据集,按一定步长进行采样,由内插计算每个采样点的颜色值和不透明度,然后由前向后或由后向前逐点计算累计的颜色值和不透明度值,直至光线完全被吸收或穿过物体。该方法能很好地反映物质边界的变化,使用Phong模型,引入镜面反射、漫反射和环境反射能得到很好的光照效果,在医学上可将各组织器官的性质属性、形状特征及相互之间的层次关系表现出来,从而丰富了图像的信息。(百度百科)
这个算法集成在vtkVolumeMapper
,包括vtkVolumeRayCastMapper
、vtkFixedPointVolumeRayCastMapper
、vtkGPUVolumeRayCastMapper
等。把vtkImageData
经过volumeMapper,再放入vtkVolume
,经过vtkRenderer
、vtkRenderWindow
、vtkRenderWindowInteractor
显示出体绘制图像。
官方使用范例:
# https://kitware.github.io/vtk-examples/site/Python/VolumeRendering/SimpleRayCast/
import vtk
def main():
fileName = get_program_parameters()
colors = vtk.vtkNamedColors()
# This is a simple volume rendering example that
# uses a vtkFixedPointVolumeRayCastMapper
# Create the standard renderer, render window
# and interactor.
ren1 = vtk.vtkRenderer()
renWin = vtk.vtkRenderWindow()
renWin.AddRenderer(ren1)
iren = vtk.vtkRenderWindowInteractor()
iren.SetRenderWindow(renWin)
# Create the reader for the data.
reader = vtk.vtkStructuredPointsReader()
reader.SetFileName(fileName)
# Create transfer mapping scalar value to opacity.
opacityTransferFunction = vtk.vtkPiecewiseFunction()
opacityTransferFunction.AddPoint(20, 0.0)
opacityTransferFunction.AddPoint(255, 0.2)
# Create transfer mapping scalar value to color.
colorTransferFunction = vtk.vtkColorTransferFunction()
colorTransferFunction.AddRGBPoint(0.0, 0.0, 0.0, 0.0)
colorTransferFunction.AddRGBPoint(64.0, 1.0, 0.0, 0.0)
colorTransferFunction.AddRGBPoint(128.0, 0.0, 0.0, 1.0)
colorTransferFunction.AddRGBPoint(192.0, 0.0, 1.0, 0.0)
colorTransferFunction.AddRGBPoint(255.0, 0.0, 0.2, 0.0)
# The property describes how the data will look.
volumeProperty = vtk.vtkVolumeProperty()
volumeProperty.SetColor(colorTransferFunction)
volumeProperty.SetScalarOpacity(opacityTransferFunction)
volumeProperty.ShadeOn()
volumeProperty.SetInterpolationTypeToLinear()
# The mapper / ray cast function know how to render the data.
volumeMapper = vtk.vtkFixedPointVolumeRayCastMapper()
volumeMapper.SetInputConnection(reader.GetOutputPort())
# The volume holds the mapper and the property and
# can be used to position/orient the volume.
volume = vtk.vtkVolume()
volume.SetMapper(volumeMapper)
volume.SetProperty(volumeProperty)
ren1.AddVolume(volume)
ren1.SetBackground(colors.GetColor3d('Wheat'))
ren1.GetActiveCamera().Azimuth(45)
ren1.GetActiveCamera().Elevation(30)
ren1.ResetCameraClippingRange()
ren1.ResetCamera()
renWin.SetSize(600, 600)
renWin.SetWindowName('SimpleRayCast')
renWin.Render()
iren.Start()
def get_program_parameters():
import argparse
description = 'Volume rendering of a high potential iron protein.'
epilogue = '''
This is a simple volume rendering example that uses a vtkFixedPointVolumeRayCastMapper.
'''
parser = argparse.ArgumentParser(description=description, epilog=epilogue,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('filename', help='ironProt.vtk.')
args = parser.parse_args()
return args.filename
if __name__ == '__main__':
main()