javaee论坛

普通会员

225648

帖子

331

回复

345

积分

楼主
发表于 2019-10-30 17:39:54 | 查看: 429 | 回复: 0

文章目录1.前言2.关于wxPython3.关于pyOpenGL4.架起沟通wxPython和pyOpenGL的桥梁5.场景、视区和模型6.三维重建的实例7.后记

1.前言

在三维显示领域,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就是这样的一对黄金搭档。有诗赞曰:

面壁十年图破壁,宝剑霜刃未曾试。秋风策马出京师,开启三维新天地。

2.关于wxPython

我一直认为,wxPython是最适合python的GUI库,并为此专门写过一篇博文。详情见《wxPython:python首选的GUI库》。这里不再讨论如何使用wxPython,只贴出几张开发项目的截图,展示一下wxPython的风格。

下图为wxPython+pyOpenGL开发的项目截图(隐去敏感信息):下图为界面细节展示(隐去敏感信息):下图为wxPython的传统风格:

3.关于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的理解也不见得正确。虽然在模型拾取、体数据绘制、三维重建等方面,我的代码跑出来的效果还算差强人意,我仍然觉得我的方法与主流思路不同。很多时候,我喜欢说我的方法是“独辟蹊径”。

下面是我在工作中绘制的一些三维效果图:

4.架起沟通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.WheelRotation<0:self.zoom*=1.1ifself.zoom>100:self.zoom=100elifevt.WheelRotation>0:self.zoom*=0.9ifself.zoom<0.01:self.zoom=0.01self.Refresh(False)definitGL(self):"""初始化GL"""self.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]ifk>1: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.366,-0.5)#设置三角形顶点glColor4f(0.0,1.0,0.0,1.0)#设置当前颜色为绿色不透明glVertex3f(0.5,-0.366,-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.366,0.5)#设置三角形顶点glEnd()#结束绘制三角形

WxGLScene类的使用示例:

#-*-coding:utf-8-*-importwxfromsceneimport*APP_TITLE=u'架起沟通wxPython和pyOpenGL的桥梁'classmainFrame(wx.Frame):"""程序主窗口类,继承自wx.Frame"""def__init__(self):"""构造函数"""wx.Frame.__init__(self,None,-1,APP_TITLE,style=wx.DEFAULT_FRAME_STYLE)self.SetBackgroundColour(wx.Colour(224,224,224))self.SetSize((800,600))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()

界面效果如下:

5.场景、视区和模型

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--6'T'-三角形0--23--5\/\/14'Q+'-边靠边的连续四边形0--2--4|||1--3--5'T+'-边靠边的连续三角形0--2--4\/_\/_\135'F'-扇形'P'-多边形mode-显示模式None-使用当前设置'FCBC'-前后面填充颜色FCBC'FLBL'-前后面显示线条FLBL'FCBL'-前面填充颜色,后面显示线条FCBL'FLBC'-前面显示线条,后面填充颜色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)"""

创建VBO和EBO的方法如下:

def_createVBO(self,vertices):"""创建顶点缓冲区对象"""id=uuid.uuid1().hexbuff=vbo.VBO(vertices)self.buffers.update({id:buff})returniddef_createEBO(self,indices):"""创建索引缓冲区对象"""id=uuid.uuid1().hexbuff=vbo.VBO(indices,target=GL_ELEMENT_ARRAY_BUFFER)self.buffers.update({id:buff})returnid6.三维重建的实例

手头有109张头部CT的断层扫描图片,我打算用这些图片尝试头部的三维重建。基础工作之一,就是要把这些图片数据读出来,组织成一个三维的数据结构(实际上是四维的,因为每个像素有RGBA四个通道)。这个数据结构,自然是numpy的ndarray对象,读取图像文件我习惯使用PIL。因此,需要导入两个模块:

importnumpyasnpfromPILimportImage

接下来,我用一行代码就把109张图片读到了一个109x256x256x4的numpy数组中,耗时172毫秒:

data=np.stack([np.array(Image.open('head%d.png'%i))foriinrange(109)],axis=0)

三维重建代码如下:

#-*-coding:utf-8-*-importnumpyasnpfromPILimportImageimportwximportwin32apiimportsys,osfromwxgl.sceneimport*fromwxgl.colormapimport*FONT_FILE=r"C:\Windows\Fonts\simfang.ttf"APP_TITLE=u'CT断层扫描三维重建工具'APP_ICON='res/head.ico'classmainFrame(wx.Frame):'''程序主窗口类,继承自wx.Frame'''def__init__(self):'''构造函数'''wx.Frame.__init__(self,None,-1,APP_TITLE,style=wx.DEFAULT_FRAME_STYLE)self.SetBackgroundColour(wx.Colour(224,224,224))self.SetSize((800,600))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,500])#self.scene.setPosture(elevation=30,azimuth=-45,save=True)self.master=self.scene.addRegion((0,0,1,1))#读取109张头部CT的断层扫描图片data=np.stack([np.array(Image.open('res/head%d.png'%i))foriinrange(109)],axis=0)#三维重建(本质上是体数据绘制)self.master.drawVolume('volume',data/255.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()

三维重建后的效果如下图:

7.后记

原本打算好好写一写纹理、拾取、体数据绘制的,结果写完第4章的时候,就已经感觉写不动了。这个题目实在太大,不是一篇一两万字的博文就可以说清楚的。如果不是因为有朋友在我的博客上留言说,想了解三维重构,本文也许会止于第4章。第5章简单展示了我的创作思路,只是为了讲解第6章的三维重建——事实上,也没有说明白。

行文至此,深为没有掰扯清楚关键问题而满怀愧疚。如果对这个话题感兴趣,请直接联系我吧:xufive@sdysit.com


您需要登录后才可以回帖 登录 | 立即注册

触屏版| 电脑版

技术支持 历史网 V2.0 © 2016-2017