利用VTK和PyQt5对医学体数据进行渲染并展示
简介
在一些医学相关的简单的项目(也许是学生的作业?毕业设计?)中,有时候可能需要集成一些可视化的功能,本文简单介绍一下,如何利用PyQt5和VTK来渲染体数据(三维数据),并集成进PyQt的UI框架中。
环境
主要依赖两个python的包
- PyQt5
 - VTK
 
最好用 Anaconda 来管理你的 python 的环境,可以利用 pip 来安装上述的包,如何安装网上有许多教程,这里不介绍了。
功能展示
- 添加体数据
 - 删除体数据
 - 选择合适的预制的颜色函数
 - 缩放、旋转
 


代码介绍
Ui_MainWindow这个类是主要用来定义UI的一些布局,VolumeRendering这个类主要用来处理一些渲染的逻辑,主要是用了VTK的一些现有的渲染方法。readXML函数用来读取 xml 文件,因为人体不同的组织、器官在医学影像中,呈现的强度不同,因此根据这些强度值可以设定一些渲染参数,来绘制不同的人体组织。这些渲染参数如何设置?是有现成的设定好的参数可以参考的,本文采用了 3DSlicer 这个软件中的设置方式,参数被保存在preset.xml文件中,这个文件是我从 3DSlicer 中拷贝过来的。buildProperty这个函数,根据preset.xml中的参数,构建 VTK 的渲染参数,主要是各种传输函数,这里不过多的对传输函数进行解释了,例如颜色传输函数可以理解为,例如强度在0-100的区域用红色,100-200的用蓝色...这个意思。注意:这个函数里有一个常数值 + 1000,这个和我仓库里的test data 的数据分布有关,因为我的数据是从0开始的,0 代表正常数据的 -1000,所以如果你的数据是正常的范围的话,可以不加 1000,可以自己试试。渲染:这里用的是
vtkGPUVolumeRayCastMapper这个函数进行渲染,利用GPU加速,绘制的会比较快。数据:这里使用的 mha 数据的文件,其实用格式的文件都可以,这里为了方便采用了mha,如果需要读取别的格式,改一下读取数据部分就可以了,对格式不太了解的同学可以看看我之前写的一篇关于不同医学图像格式读写的文章。
其他:其他的相关部分,可以直接看代码,代码并不复杂,只有 200+行,可能会一些小 bug,不过不要紧,这里主要的目的是为了展示:
- 如何在 python 中用 VTK 渲染体数据;
 - 如何集成 VTK 渲染进 PyQt5;
 - 如何构建合适的渲染参数(和3DSlicer一样)。
 
完整代码
import os
from PyQt5 import QtCore
from PyQt5 import QtGui
from PyQt5 import QtCore, QtGui, QtWidgets
from functools import partial
from PyQt5.QtWidgets import QFrame,QVBoxLayout,QFileDialog, QMainWindow,QApplication
from vtk.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor
from vtkmodules.vtkCommonColor import vtkNamedColors
from vtkmodules.vtkCommonDataModel import vtkPiecewiseFunction
from vtkmodules.vtkIOImage import vtkMetaImageReader
from vtkmodules.vtkRenderingCore import (
    vtkColorTransferFunction,
    vtkRenderer,
    vtkVolume,
    vtkVolumeProperty
)
from vtkmodules.vtkRenderingVolume import vtkGPUVolumeRayCastMapper
from xml.dom.minidom import parse
class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(1140, 887)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.horizontalLayout = QtWidgets.QHBoxLayout(self.centralwidget)
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.Tapw = QtWidgets.QTabWidget(self.centralwidget)
        font = QtGui.QFont()
        font.setPointSize(12)
        self.Tapw.setFont(font)
        self.Tapw.setObjectName("Tapw")
        self.Rendering = QtWidgets.QWidget()
        self.Rendering.setObjectName("Rendering")
        self.VTK = QtWidgets.QWidget(self.Rendering)
        self.VTK.setGeometry(QtCore.QRect(0, 10, 931, 771))
        self.VTK.setObjectName("VTK")
        self.RenPreset = QtWidgets.QComboBox(self.Rendering)
        self.RenPreset.setGeometry(QtCore.QRect(950, 510, 161, 31))
        self.RenPreset.setObjectName("RenPreset")
        self.RenList = QtWidgets.QListWidget(self.Rendering)
        self.RenList.setGeometry(QtCore.QRect(950, 160, 161, 281))
        self.RenList.setObjectName("RenList")
        self.RenDelVolBtn = QtWidgets.QPushButton(self.Rendering)
        self.RenDelVolBtn.setGeometry(QtCore.QRect(940, 80, 171, 51))
        self.RenDelVolBtn.setObjectName("RenDelVolBtn")
        self.RenLoadVolBtn = QtWidgets.QPushButton(self.Rendering)
        self.RenLoadVolBtn.setGeometry(QtCore.QRect(940, 20, 171, 51))
        self.RenLoadVolBtn.setObjectName("RenLoadVolBtn")
        self.label_4 = QtWidgets.QLabel(self.Rendering)
        self.label_4.setGeometry(QtCore.QRect(990, 460, 101, 31))
        self.label_4.setObjectName("label_4")
        self.RenChange = QtWidgets.QPushButton(self.Rendering)
        self.RenChange.setGeometry(QtCore.QRect(940, 570, 171, 51))
        self.RenChange.setObjectName("RenChange")
        self.Tapw.addTab(self.Rendering, "")
        self.horizontalLayout.addWidget(self.Tapw)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 1140, 23))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)
        self.retranslateUi(MainWindow)
        self.Tapw.setCurrentIndex(1)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)
    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "Volume Rendering"))
        self.RenDelVolBtn.setText(_translate("MainWindow", "Remove Volume"))
        self.RenLoadVolBtn.setText(_translate("MainWindow", "Add Volume"))
        self.label_4.setText(_translate("MainWindow", "Transfer Func"))
        self.RenChange.setText(_translate("MainWindow", "Change Transfer Func"))
        self.Tapw.setTabText(self.Tapw.indexOf(self.Rendering), _translate("MainWindow", "Volume Rendering"))
class VolumeRendering():
    def __init__(self,ui:Ui_MainWindow) -> None:
        self.frame = QFrame()
        self.ui = ui
        self.vl = QVBoxLayout()
        self.vtkWidget = QVTKRenderWindowInteractor(self.frame)
        self.vl.addWidget(self.vtkWidget)
        self.ren = vtkRenderer()
        self.vtkWidget.GetRenderWindow().AddRenderer(self.ren)
        self.iren = self.vtkWidget.GetRenderWindow().GetInteractor()
        self.colors = vtkNamedColors()
        self.vtkpros = {}
        self.colors.SetColor('BkgColor', [0, 0,0 , 255])
        self.ren.SetBackground(self.colors.GetColor3d('BkgColor'))
        self.frame.setLayout(self.vl)
        self.iren.Initialize()
        self.volumes = {}
        self.volume_property = vtkVolumeProperty()
        self.volume_color = vtkColorTransferFunction()
        self.volume_scalar_opacity = vtkPiecewiseFunction()
        self.volume_gradient_opacity = vtkPiecewiseFunction()
        self.setDefaultProperty()
        ui.RenLoadVolBtn.clicked.connect(partial(self.addVolumeBtn))
        ui.RenDelVolBtn.clicked.connect(partial(self.delVolumeBtn))
        ui.RenChange.clicked.connect(partial(self.changeBtn))
        self.readXML()
        self.volume_property = self.vtkpros["CT-AAA"]
    def readXML(self):
        # read preset property from preset.xml
        # the preset.xml is copied from 3D-Slicer
        cur_dir = os.path.dirname(os.path.abspath(__file__))
        domtree = parse(os.path.join(cur_dir,"preset.xml"))
        data = domtree.documentElement
        propertys = data.getElementsByTagName('VolumeProperty')
        for pro in propertys:
            name = pro.getAttribute('name')
            go = pro.getAttribute('gradientOpacity')
            so = pro.getAttribute('scalarOpacity')
            ctrans = pro.getAttribute('colorTransfer')
            vtkpro = self.buildProperty(go,so,ctrans)
            self.vtkpros[name] = vtkpro
            self.ui.RenPreset.addItem(name)
    def buildProperty(self,go,so,ctrans):
        vtkcoltrans = vtkColorTransferFunction()
        data = ctrans.split()
        data = [float(x) for x in data]
        for i in range(int(data[0]/4)):
            base = 1 + i * 4
            # add 1000 because the intensity of our test data is started from 0
            # if you use the normal data, you can delete the const 1000.
            vtkcoltrans.AddRGBPoint(data[base] + 1000, data[base+1], data[base+2], data[base+3])
        vtkgo = vtkPiecewiseFunction()
        data = go.split()
        data = [float(x) for x in data]
        for i in range(int(data[0]/2)):
            base = 1 + i * 2
            vtkgo.AddPoint(data[base]+ 1000, data[base+1])
        vtkso = vtkPiecewiseFunction()
        data = so.split()
        data = [float(x) for x in data]
        for i in range(int(data[0]/2)):
            base = 1 + i * 2
            vtkso.AddPoint(data[base]+ 1000, data[base+1])
        vtkpro = vtkVolumeProperty()
        vtkpro.SetColor(vtkcoltrans)
        vtkpro.SetScalarOpacity(vtkso)
        #vtkpro.SetGradientOpacity(vtkgo)
        vtkpro.SetInterpolationTypeToLinear()
        vtkpro.ShadeOn()
        vtkpro.SetAmbient(0.4)
        vtkpro.SetDiffuse(0.6)
        vtkpro.SetSpecular(0.2)
        return vtkpro
    # add volume to the scene
    def addVolumeBtn(self):
        file_name,_ =QFileDialog.getOpenFileName(
            self.ui.Rendering,"open file dialog",os.getcwd(),"Mha(*.mha)")
        if file_name == "":
            return
        self.addVolume(file_name)
        self.ui.RenList.addItem(file_name)
    # delete volume from scene
    def delVolumeBtn(self):
        if not self.ui.RenList.itemAt(0,0):
            return
        curVol = self.ui.RenList.currentItem()
        if not curVol:
            curVol = self.ui.RenList.itemAt(0,0)
        curVol = curVol.text()
        self.delVolume(curVol)
        print(curVol)
    #change transfer function
    def changeBtn(self):
        curVol = self.ui.RenList.currentItem()
        if not curVol:
            curVol = self.ui.RenList.itemAt(0,0)
        curVol = curVol.text()
        if curVol in self.volumes.keys():
            self.volumes[curVol].SetProperty(self.vtkpros[self.ui.RenPreset.currentText()])
    def setDefaultProperty(self):
        self.volume_color.AddRGBPoint(0, 0.0, 0.0, 0.0)
        self.volume_color.AddRGBPoint(500, 240.0 / 255.0, 184.0 / 255.0, 160.0 / 255.0)
        self.volume_color.AddRGBPoint(1000, 240.0 / 255.0, 184.0 / 255.0, 160.0 / 255.0)
        self.volume_color.AddRGBPoint(1150, 1.0, 1.0, 240.0 / 255.0)  # Ivory
        self.volume_scalar_opacity = vtkPiecewiseFunction()
        self.volume_scalar_opacity.AddPoint(0, 0.00)
        self.volume_scalar_opacity.AddPoint(500, 0.15)
        self.volume_scalar_opacity.AddPoint(1000, 0.15)
        self.volume_scalar_opacity.AddPoint(1150, 0.85)
        self.volume_gradient_opacity = vtkPiecewiseFunction()
        self.volume_gradient_opacity.AddPoint(0, 0.0)
        self.volume_gradient_opacity.AddPoint(90, 0.5)
        self.volume_gradient_opacity.AddPoint(100, 1.0)
        self.volume_property = vtkVolumeProperty()
        self.volume_property.SetColor(self.volume_color)
        self.volume_property.SetScalarOpacity(self.volume_scalar_opacity)
        self.volume_property.SetGradientOpacity(self.volume_gradient_opacity)
        self.volume_property.SetInterpolationTypeToLinear()
        self.volume_property.ShadeOn()
        self.volume_property.SetAmbient(0.4)
        self.volume_property.SetDiffuse(0.6)
        self.volume_property.SetSpecular(0.2)
    def delVolume(self,path):
        self.ren.RemoveViewProp(self.volumes[path])
        item = self.ui.RenList.findItems(path,QtCore.Qt.MatchExactly)[0]
        row = self.ui.RenList.row(item)
        self.ui.RenList.takeItem(row)
        self.volumes.pop(path)
        self.iren.Initialize()
    def addVolume(self,path):
        reader = vtkMetaImageReader()
        reader.SetFileName(path)
        volume_mapper = vtkGPUVolumeRayCastMapper()
        volume_mapper.SetInputConnection(reader.GetOutputPort())
        volume = vtkVolume()
        volume.SetMapper(volume_mapper)
        volume.SetProperty(self.volume_property)
        self.ren.AddViewProp(volume)
        self.volumes[path] = volume
        camera = self.ren.GetActiveCamera()
        c = volume.GetCenter()
        camera.SetViewUp(0, 0, -1)
        camera.SetPosition(c[0], c[1] - 800, c[2]-200)
        camera.SetFocalPoint(c[0], c[1], c[2])
        camera.Azimuth(30.0)
        camera.Elevation(30.0)
        self.iren.Initialize()
def main():
    app = QApplication([])
    mainw =  QMainWindow()
    ui = Ui_MainWindow()
    ui.setupUi(mainw)
    mvtk = VolumeRendering(ui)
    mvtk.frame.setGeometry(0,0,1000,1000)
    mvtk.frame.setParent(ui.VTK)
    mainw.show()
    app.exec_()
if __name__ == "__main__":
    main()
												
											利用VTK和PyQt5对医学体数据进行渲染并展示的更多相关文章
- mha格式的CT体数据转为jpg切片
		
mha格式的CT体数据转为jpg切片 mha格式 .mha文件是一种体数据的存储格式,由一个描述数据的头和数据组成,一般我们拿到的原始医学影像的数据是.dcm也就是dicom文件,dicom文件很复杂 ...
 - 利用Aspose.Cells完成easyUI中DataGrid数据的Excel导出功能
		
我准备在项目中实现该功能之前,google发现大部分代码都是利用一般处理程序HttpHandler实现的服务器端数据的Excel导出,但是这样存在的问题是ashx读取的数据一般都是数据库中视图的数据, ...
 - 什么是体数据可视化(Volume data visualization)?及体绘制的各种算法和技术的特点?
		
该文对体数据进行综述,并介绍了体数据的各种算法和技术的特点. 前言 由于3D数据采集领域的高速发展,以及在具有交互式帧率的现代化工作站上执行高级可视化的可能性,体数据的重要性将继续迅速增长. 数据集可 ...
 - 利用AFNetworking框架去管理从聚合数据上面请求到的数据
		
数据从JSON文档中读取处理的过程称为“解码”过程,即解析和读取过程,来看一下如果利用AFNetworking框架去管理从聚合数据上面请求到的数据. 一.下载并导入AFNetworking框架 这部分 ...
 - 剔除list中相同的结构体数据
		
剔除list中相同的结构体数据,有三个思路:1.两层循环,逐个比较 2.使用set容器来剔除 3.使用unique方法去重 // deduplication.cpp : 定义控制台应用程序的入口点. ...
 - http  请求体数据--ngx
		
HTTP包体的长度有可能非常大,不同业务可能对包体读取 处理不相同, 比如waf, 也许会读取body内容或者只是读取很少的前几十字节.所以根据不同业务特性,对http body 数据包处理方式不同, ...
 - CSharpGL(8)使用3D纹理渲染体数据 (Volume Rendering) 初探
		
CSharpGL(8)使用3D纹理渲染体数据 (Volume Rendering) 初探 2016-08-13 由于CSharpGL一直在更新,现在这个教程已经不适用最新的代码了.CSharpGL源码 ...
 - 利用Python进行数据分析(12) pandas基础: 数据合并
		
pandas 提供了三种主要方法可以对数据进行合并: pandas.merge()方法:数据库风格的合并: pandas.concat()方法:轴向连接,即沿着一条轴将多个对象堆叠到一起: 实例方法c ...
 - Java利用POI导入导出Excel中的数据
		
首先谈一下今天发生的一件开心的事,本着一颗android的心我被分配到了PB组,身在曹营心在汉啊!好吧,今天要记录和分享的是Java利用POI导入导出Excel中的数据.下面POI包的下载地 ...
 
随机推荐
- Oracle入门基础(十一)一一PL/SQL基本语法
			
1.打印Hello World declare --说明部分 begin --程序 dbms_output.put_line('Hello World'); end; 2.引用型变量 查询并打印783 ...
 - 为什么 wait, notify 和 notifyAll 这些方法不在 thread  类里面?
			
一个很明显的原因是 JAVA 提供的锁是对象级的而不是线程级的,每个对象都有 锁,通过线程获得.由于 wait,notify 和 notifyAll 都是锁级别的操作,所以把他 们定义在 Object ...
 - Netty学习摘记 —— 再谈EventLoop 和线程模型
			
本文参考 本篇文章是对<Netty In Action>一书第七章"EventLoop和线程模型"的学习摘记,主要内容为线程模型的概述.事件循环的概念和实现.任务调度和 ...
 - 获取Java数据库中结果集的每个字段名和个数
			
/** * 查询到多条数据, 封装到List<Map> */public List<Map<String, Object>> queryForMapList(Str ...
 - BMZCTF SDNISC2020_过去和现在
			
SDNISC2020_过去和现在 打开附件就一张图片 根据题意感觉是图片中隐藏了什么信息 使用binwalk -e分离这里foremost不行 三个文件查看在第一个中发现flag
 - Arduino 烧写bootloader
			
什么是bootloader 一般情况下微处理器写入程序时都通过专门的编程器进行烧写,但是也可以通过在MCU中预先写入一些程序来实现某些基本功能,这些预先写入的程序代码就是bootloader.这样每次 ...
 - 手机上无法显示Toast信息
			
关于手机上无法显示Toast信息, 是因为手机上的权限没有开, 在应用管理处将所有权限都打开,就可以显示了.
 - java中自动插入一个默认的构造函数,这到底怎么回事?
			
1.2 当没有任何构造函数,java编译器,会插入一个默认的构造函数 见下面的例子: class Line { double x = 0.02; double y; } publ ...
 - Python Turtle库绘制蟒蛇
			
使用Python Turtle库来绘制蟒蛇 import turtle引入了海龟绘图体系 使用setup函数,设定了一个宽650像素和高350像素的窗体,其位置左上角坐标是200,200 说明位置在距 ...
 - 关于data自定义属性
			
新的HTML5标准允许你在普通的元素标签里,嵌入类似data-*的属性,来实现一些简单数据的存取.它的数量不受限制,并且也能由JavaScript动态修改,也支持CSS选择器进行样式设置.这使得dat ...