作者
天元浪子
责编
伍杏玲
出品
CSDN博客
在三维显示领域,OpenGL是神一样的存在,其地位就像编程语言里面的C一样。基于OpenGL衍生出来的分支、派系,林林总总。
Python旗下,影响较大的三维库有pyOpenGl/VTK/Mayavi/Vispy等,它们各自拥有庞大的用户群体。VTK在医学领域应用广泛,Vispy在科研领域粉丝众多。
VTK和Vispy都是基于OpenGL的扩展,Mayavi则是基于VTK的,因此很多的医学影像应用都是采用Python+VTK+ITK+Mayavi的组合(ITK是图像处理库,类似于OpenCV或PIL)。
上述三维渲染库,包括PyOpenGl,都有一个共同的特点,那就是只专注于三维功能的实现,而疏于对UI的支持。比Vispy,虽然支持以wx或者Qt作为后端,但绑定后端以后,在窗口管理、交互操作等方面还是存在不少问题。PyOpenGl做得更简单,提供一个GLUT库就算是对UI的支持了。
事实上,在复杂的三维展示系统中,UI的重要性并不亚于OpenGL。如果能为OpenGL找到一位UI搭档,必将提高程序的可靠性和可操作性,增强用户感受。wxPython和PyOpenGL就是这样的一对*金搭档。有诗赞曰:
面壁十年图破壁,宝剑霜刃未曾试。
秋风策马出京师,开启三维新天地。
关于wxPython
我一直认为,wxPython是最适合Python的GUI库,并为此专门写过一篇博文。详情见《wxPython:Python首选的GUI库》。这里不再讨论如何使用wxPython,只贴出几张开发项目的截图,展示一下wxPython的风格。
下图为wxPython+PyOpenGL开发的项目截图(隐去敏感信息):
下图为界面细节展示(隐去敏感信息):
下图为wxPython的传统风格:
关于PyOpenGL
pyOpenGL的入门教程有很多,我也有一篇博文《写给Python程序员的OpenGL教程》。特别提醒一下,这篇博文最后提到顶点缓冲区对象VBO,并有演示代码。VBO的概念很重要很重要很重要,只有学会使用VBO,才能真正进入OpenGL的精彩世界。
早期的OpenGL使用立即渲染模式(Immediatemode,也就是固定渲染管线),概念清晰易于理解,绘制图形也很方便,但效率太低。从OpenGL3.2开始,规范文档开始废弃立即渲染模式,并鼓励开发者在OpenGL的核心模式(Core-profile)下进行开发,这个分支的规范完全移除了旧的特性。
VBO是OpenGL核心模式的基础。VBO将顶点数据集存储在GPU中,这意味着渲染VBO数据会很快。不过,数据从RAM传送到GPU是有代价的。VBO虽然在GPU上,但并没有使用GPU的运算功能。在VBO之上,还有VAO的概念,即VertexArrayObject,顶点数组对象。这个概念很复杂,我们可以简单把VAO理解为VBO管理者。由于VAO依赖于显卡,通用性较差,我选择绕过它。
说实话,我对OpenGL的核心模式了解不多,对于着色器语言GLSL更是畏之如虎,对VBO的理解也不见得正确。虽然在模型拾取、体数据绘制、三维重建等方面,我的代码跑出来的效果还算差强人意,我仍然觉得我的方法与主流思路不同。很多时候,我喜欢说我的方法是“独辟蹊径”。
下面是我在工作中绘制的一些三维效果图:
架起沟通wxPython和PyOpenGL的桥梁
wx.glcanvas.GLCanvas是wxPython为显示OpenGL提供的类,顾名思义,我们可以将其理解为OpenGL的画板。有了这个画板,我们就可以使用OpenGL提供的各种工具在上面绘制各种三维模型了。
下面这段代码,从wx.glcanvas.GLCanvas派生了新类WxGLScene,绑定了鼠标滚轮事件,并以立即渲染模式(Immediatemode)画了两个三角形。受限于篇幅,删去了鼠标拖拽操作,仅保留了滚轮缩放功能。
#-*-coding:utf-8-*-importwxfromwximportglcanvasfromOpenGL.GLimport*fromOpenGL.GLUimport*classWxGLScene(glcanvas.GLCanvas):GL场景类def__init__(self,parent,eye=[0,0,5],aim=[0,0,0],up=[0,1,0],view=[-1,1,-1,1,3.5,10]):构造函数parent-父级窗口对象eye-观察者的位置(默认z轴的正方向)up-对观察者而言的上方(默认y轴的正方向)view-视景体glcanvas.GLCanvas.__init__(self,parent,-1,style=glcanvas.WX_GL_RGBA
glcanvas.WX_GL_DOUBLEBUFFER
glcanvas.WX_GL_DEPTH_SIZE)self.parent=parent#父级窗口对象self.eye=eye#观察者的位置self.aim=aim#观察目标(默认在坐标原点)self.up=up#对观察者而言的上方self.view=view#视景体self.size=self.GetClientSize()#OpenGL窗口的大小self.context=glcanvas.GLContext(self)#OpenGL上下文self.zoom=1.0#视口缩放因子self.mpos=None#鼠标位置self.initGL()#画布初始化self.Bind(wx.EVT_SIZE,self.onResize)#绑定窗口尺寸改变事件self.Bind(wx.EVT_ERASE_BACKGROUND,self.onErase)#绑定背景擦除事件self.Bind(wx.EVT_PAINT,self.onPaint)#绑定重绘事件self.Bind(wx.EVT_LEFT_DOWN,self.onLeftDown)#绑定鼠标左键按下事件self.Bind(wx.EVT_LEFT_UP,self.onLeftUp)#绑定鼠标左键弹起事件self.Bind(wx.EVT_RIGHT_UP,self.onRightUp)#绑定鼠标右键弹起事件self.Bind(wx.EVT_MOTION,self.onMouseMotion)#绑定鼠标移动事件self.Bind(wx.EVT_MOUSEWHEEL,self.onMouseWheel)#绑定鼠标滚轮事件defonResize(self,evt):响应窗口尺寸改变事件ifself.context:self.SetCurrent(self.context)self.size=self.GetClientSize()self.Refresh(False)evt.Skip()defonErase(self,evt):响应背景擦除事件passdefonPaint(self,evt):响应重绘事件self.SetCurrent(self.context)glClear(GL_COLOR_BUFFER_BIT
GL_DEPTH_BUFFER_BIT)#清除屏幕及深度缓存self.drawGL()#绘图self.SwapBuffers()#切换缓冲区,以显示绘制内容evt.Skip()defonLeftDown(self,evt):响应鼠标左键按下事件self.CaptureMouse()self.mpos=evt.GetPosition()defonLeftUp(self,evt):响应鼠标左键弹起事件try:self.ReleaseMouse()except:passdefonRightUp(self,evt):响应鼠标右键弹起事件passdefonMouseMotion(self,evt):响应鼠标移动事件ifevt.Dragging()andevt.LeftIsDown():pos=evt.GetPosition()try:dx,dy=pos-self.mposexcept:returnself.mpos=pos#限于篇幅省略改变观察者位置的代码self.Refresh(False)defonMouseWheel(self,evt):响应鼠标滚轮事件ifevt.WheelRotation0:self.zoom*=1.1ifself.zoom:self.zoom=elifevt.WheelRotation0:self.zoom*=0.9ifself.zoom0.01:self.zoom=0.01self.Refresh(False)definitGL(self):初始化GLself.SetCurrent(self.context)glClearColor(0,0,0,0)#设置画布背景色glEnable(GL_DEPTH_TEST)#开启深度测试,实现遮挡关系glDepthFunc(GL_LEQUAL)#设置深度测试函数glShadeModel(GL_SMOOTH)#GL_SMOOTH(光滑着色)/GL_FLAT(恒定着色)glEnable(GL_BLEND)#开启混合glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA)#设置混合函数glEnable(GL_ALPHA_TEST)#启用Alpha测试glAlphaFunc(GL_GREATER,0.05)#设置Alpha测试条件为大于0.05则通过glFrontFace(GL_CW)#设置逆时针索引为正面(GL_CCW/GL_CW)glEnable(GL_LINE_SMOOTH)#开启线段反走样glHint(GL_LINE_SMOOTH_HINT,GL_NICEST)defdrawGL(self):绘制#清除屏幕及深度缓存glClear(GL_COLOR_BUFFER_BIT
GL_DEPTH_BUFFER_BIT)#设置视口glViewport(0,0,self.size[0],self.size[1])#设置投影(透视投影)glMatrixMode(GL_PROJECTION)glLoadIdentity()k=self.size[0]/self.size[1]ifk1:glFrustum(self.zoom*self.view[0]*k,self.zoom*self.view[1]*k,self.zoom*self.view[2],self.zoom*self.view[3],self.view[4],self.view[5])else:glFrustum(self.zoom*self.view[0],self.zoom*self.view[1],self.zoom*self.view[2]/k,self.zoom*self.view[3]/k,self.view[4],self.view[5])#设置视点gluLookAt(self.eye[0],self.eye[1],self.eye[2],self.aim[0],self.aim[1],self.aim[2],self.up[0],self.up[1],self.up[2])#设置模型视图glMatrixMode(GL_MODELVIEW)glLoadIdentity()#---------------------------------------------------------------glBegin(GL_LINES)#开始绘制线段(世界坐标系)#以红色绘制x轴glColor4f(1.0,0.0,0.0,1.0)#设置当前颜色为红色不透明glVertex3f(-0.8,0.0,0.0)#设置x轴顶点(x轴负方向)glVertex3f(0.8,0.0,0.0)#设置x轴顶点(x轴正方向)#以绿色绘制y轴glColor4f(0.0,1.0,0.0,1.0)#设置当前颜色为绿色不透明glVertex3f(0.0,-0.8,0.0)#设置y轴顶点(y轴负方向)glVertex3f(0.0,0.8,0.0)#设置y轴顶点(y轴正方向)#以蓝色绘制z轴glColor4f(0.0,0.0,1.0,1.0)#设置当前颜色为蓝色不透明glVertex3f(0.0,0.0,-0.8)#设置z轴顶点(z轴负方向)glVertex3f(0.0,0.0,0.8)#设置z轴顶点(z轴正方向)glEnd()#结束绘制线段#---------------------------------------------------------------glBegin(GL_TRIANGLES)#开始绘制三角形(z轴负半区)glColor4f(1.0,0.0,0.0,1.0)#设置当前颜色为红色不透明glVertex3f(-0.5,-0.,-0.5)#设置三角形顶点glColor4f(0.0,1.0,0.0,1.0)#设置当前颜色为绿色不透明glVertex3f(0.5,-0.,-0.5)#设置三角形顶点glColor4f(0.0,0.0,1.0,1.0)#设置当前颜色为蓝色不透明glVertex3f(0.0,0.5,-0.5)#设置三角形顶点glEnd()#结束绘制三角形#---------------------------------------------------------------glBegin(GL_TRIANGLES)#开始绘制三角形(z轴正半区)glColor4f(1.0,0.0,0.0,1.0)#设置当前颜色为红色不透明glVertex3f(-0.5,0.5,0.5)#设置三角形顶点glColor4f(0.0,1.0,0.0,1.0)#设置当前颜色为绿色不透明glVertex3f(0.5,0.5,0.5)#设置三角形顶点glColor4f(0.0,0.0,1.0,1.0)#设置当前颜色为蓝色不透明glVertex3f(0.0,-0.,0.5)#设置三角形顶点glEnd()#结束绘制三角形WxGLScene类的使用示例:#-*-coding:utf-8-*-importwxfromsceneimport*APP_TITLE=u架起沟通wxPython和pyOpenGL的桥梁classmainFrame(wx.Frame):程序主窗口类,继承自wx.Framedef__init__(self):构造函数wx.Frame.__init__(self,None,-1,APP_TITLE,style=wx.DEFAULT_FRAME_STYLE)self.SetBackgroundColour(wx.Colour(,,))self.SetSize((,))self.Center()self.scene=WxGLScene(self)classmainApp(wx.App):defOnInit(self):self.SetAppName(APP_TITLE)self.Frame=mainFrame()self.Frame.Show()returnTrueif__name__==__main__:app=mainApp()app.MainLoop()
WxGLScene类的使用示例:
#-*-coding:utf-8-*-importwxfromsceneimport*APP_TITLE=u架起沟通wxPython和pyOpenGL的桥梁classmainFrame(wx.Frame):程序主窗口类,继承自wx.Framedef__init__(self):构造函数wx.Frame.__init__(self,None,-1,APP_TITLE,style=wx.DEFAULT_FRAME_STYLE)self.SetBackgroundColour(wx.Colour(,,))self.SetSize((,))self.Center()self.scene=WxGLScene(self)classmainApp(wx.App):defOnInit(self):self.SetAppName(APP_TITLE)self.Frame=mainFrame()self.Frame.Show()returnTrueif__name__==__main__:app=mainApp()app.MainLoop()
界面效果如下:
场景、视区和模型
OpenGL允许用户使用glViewport()命令设置多个视口,这意味着我们可以在显示屏幕上分割出多个显示区域,这些区域可以相互重叠,在逻辑上是完全独立的。我们可以将WxGLScene称作场景(scene),由glViewport()命令创建的视口称为视区(region),拥有相同名字的三维部件定义为模型(model)。一个场景可以添加多个视区,一个视区可以创建多个模型。
以曲面模型为例,函数原型如下:
defdrawSurface(self,name,v,c=None,t=None,texture=None,method=Q,mode=None,display=True,pick=False):绘制曲面name-模型名v-顶点坐标集,numpy.ndarray类型,shape=(cols,3)c-顶点的颜色集,numpy.ndarray类型,shape=(3
4,)
(cols,3
4)t-顶点的纹理坐标集,numpy.ndarray类型,shape=(cols,2)texture-2D纹理对象method-绘制方法Q-四边形0--34--7
1--25--6T-三角形0--23--5\/\/14Q+-边靠边的连续四边形0--2--4
1--3--5T+-边靠边的连续三角形0--2--4\/_\/_\F-扇形P-多边形mode-显示模式None-使用当前设置FCBC-前后面填充颜色FCBCFLBL-前后面显示线条FLBLFCBL-前面填充颜色,后面显示线条FCBLFLBC-前面显示线条,后面填充颜色FLBCdisplay-是否显示pick-是否可以被拾取
生成曲面模型顶点集、索引集的函数原型如下:
def_createSurface(self,v,c,t):生成曲面的顶点集、索引集、顶点数组类型v-顶点坐标集,numpy.ndarray类型,shape=(clos,3)c-顶点的颜色集,None或numpy.ndarray类型,shape=(3
4,)
(cols,3
4)t-顶点的纹理坐标集,None或numpy.ndarray类型,shape=(cols,2)
三维重建的实例
手头有张头部CT的断层扫描图片,我打算用这些图片尝试头部的三维重建。基础工作之一,就是要把这些图片数据读出来,组织成一个三维的数据结构(实际上是四维的,因为每个像素有RGBA四个通道)。
这个数据结构,自然是numpy的ndarray对象,读取图像文件我习惯使用PIL。因此,需要导入两个模块:
importnumpyasnpfromPILimportImage
接下来,我用一行代码就把张图片读到了一个xxx4的numpy数组中,耗时毫秒:
data=np.stack([np.array(Image.open(head%d.png%i))foriinrange()],axis=0)
三维重建代码如下:
#-*-coding:utf-8-*-importnumpyasnpfromPILimportImageimportwximportwin32apiimportsys,osfromwxgl.sceneimport*fromwxgl.colormapimport*FONT_FILE=rC:\Windows\Fonts\simfang.ttfAPP_TITLE=uCT断层扫描三维重建工具APP_ICON=res/head.icoclassmainFrame(wx.Frame):程序主窗口类,继承自wx.Framedef__init__(self):构造函数wx.Frame.__init__(self,None,-1,APP_TITLE,style=wx.DEFAULT_FRAME_STYLE)self.SetBackgroundColour(wx.Colour(,,))self.SetSize((,))self.Center()#以下代码处理图标ifhasattr(sys,frozen)andgetattr(sys,frozen)==windows_exe:exeName=win32api.GetModuleFileName(win32api.GetModuleHandle(None))icon=wx.Icon(exeName,wx.BITMAP_TYPE_ICO)else:icon=wx.Icon(APP_ICON,wx.BITMAP_TYPE_ICO)self.SetIcon(icon)self.scene=WxGLScene(self,FONT_FILE,bg=[1,1,1,1])#self.scene.setView([-1,1,-1,1,2,])#self.scene.setPosture(elevation=30,azimuth=-45,save=True)self.master=self.scene.addRegion((0,0,1,1))#读取张头部CT的断层扫描图片data=np.stack([np.array(Image.open(res/head%d.png%i))foriinrange()],axis=0)#三维重建(本质上是提数据绘制)self.master.drawVolume(volume,data/.0,method=Q,smooth=False)self.master.update()classmainApp(wx.App):defOnInit(self):self.SetAppName(APP_TITLE)self.Frame=mainFrame()self.Frame.Show()returnTrueif__name__==__main__:app=mainApp()app.MainLoop()
三维重建后的效果如下图:
如果对这个话题感兴趣,请直接联系我吧:xufive
sdysit.