作者 | 天元浪子

责编 | 伍杏玲

出品 | 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 使用立即渲染模式(Immediate mode,也就是固定渲染管线),概念清晰易于理解,绘制图形也很方便,但效率太低。从 OpenGL3.2 开始,规范文档开始废弃立即渲染模式,并鼓励开发者在 OpenGL 的核心模式(Core-profile)下进行开发,这个分支的规范完全移除了旧的特性。

VBO 是 OpenGL 核心模式的基础。VBO 将顶点数据集存储在 GPU 中,这意味着渲染 VBO 数据会很快。不过,数据从 RAM 传送到 GPU 是有代价的。VBO 虽然在 GPU 上,但并没有使用 GPU 的运算功能。在 VBO 之上,还有 VAO 的概念,即Vertex Array Object,顶点数组对象。这个概念很复杂,我们可以简单把 VAO 理解为 VBO 管理者。由于 VAO 依赖于显卡,通用性较差,我选择绕过它。

说实话,我对 OpenGL 的核心模式了解不多,对于着色器语言 GLSL 更是畏之如虎,对 VBO 的理解也不见得正确。虽然在模型拾取、体数据绘制、三维重建等方面,我的代码跑出来的效果还算差强人意,我仍然觉得我的方法与主流思路不同。很多时候,我喜欢说我的方法是“独辟蹊径”。

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

架起沟通 wxPython 和 PyOpenGL 的桥梁

wx.glcanvas.GLCanvas 是 wxPython 为显示 OpenGL 提供的类,顾名思义,我们可以将其理解为 OpenGL 的画板。有了这个画板,我们就可以使用 OpenGL 提供的各种工具在上面绘制各种三维模型了。

下面这段代码,从 wx.glcanvas.GLCanvas 派生了新类 WxGLScene,绑定了鼠标滚轮事件,并以立即渲染模式(Immediate mode)画了两个三角形。受限于篇幅,删去了鼠标拖拽操作,仅保留了滚轮缩放功能。

# -*- coding: utf-8 -*-import wxfrom wx import glcanvasfrom OpenGL.GL import *from OpenGL.GLU import *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):"""响应窗口尺寸改变事件"""if self.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):"""响应鼠标移动事件"""if evt.Dragging() and evt.LeftIsDown(): pos = evt.GetPosition()try: dx, dy = pos - self.mposexcept:return self.mpos = pos# 限于篇幅省略改变观察者位置的代码 self.Refresh(False)defonMouseWheel(self, evt):"""响应鼠标滚轮事件"""if evt.WheelRotation < 0: self.zoom *= 1.1if self.zoom > 100: self.zoom = 100elif evt.WheelRotation > 0: self.zoom *= 0.9if self.zoom < 0.01: self.zoom = 0.01 self.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]if k > 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 -*-import wxfrom scene import *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()

WxGLScene 类的使用示例:

#-*- coding: utf-8 -*-import wxfrom scene import *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()

界面效果如下:

场景、视区和模型

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

三维重建的实例

手头有 109 张头部 CT 的断层扫描图片,我打算用这些图片尝试头部的三维重建。基础工作之一,就是要把这些图片数据读出来,组织成一个三维的数据结构(实际上是四维的,因为每个像素有 RGBA 四个通道)。

这个数据结构,自然是 numpy 的 ndarray 对象,读取图像文件我习惯使用 PIL。因此,需要导入两个模块:

import numpy as npfrom PIL import Image

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

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

三维重建代码如下:

# -*- coding: utf-8 -*-import numpy as npfrom PIL import Imageimport wximport win32apiimport sys, osfrom wxgl.scene import *from wxgl.colormap import *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()# 以下代码处理图标if hasattr(sys, "frozen") and getattr(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)) for i in range(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()

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

如果对这个话题感兴趣,请直接联系我吧:xufive@sdysit.com

原文:https://blog.csdn.net/xufive/article/details/97020456

声明:本文系CSDN博客原创文章,转载请联系原作者。

wxpython dataviewmodel_wxPython + PyOpenGL 打造三维数据分析的利器!|CSDN 博文精选相关推荐

  1. wxPython + PyOpenGL 打造三维数据分析的利器!| CSDN 博文精选

    作者 | 天元浪子 责编 | 伍杏玲 出品 | CSDN博客 在三维显示领域,OpenGL 是神一样的存在,其地位就像编程语言里面的 C 一样.基于 OpenGL 衍生出来的分支.派系,林林总总. P ...

  2. wxPython+PyOpenGL打造三维数据分析的利器!| CSDN博文精选

    作者 | 天元浪子 责编 | 伍杏玲 出品 | CSDN博客 在三维显示领域,OpenGL 是神一样的存在,其地位就像编程语言里面的 C 一样.基于 OpenGL 衍生出来的分支.派系,林林总总. P ...

  3. wxPython + pyOpenGL,打造三维数据分析的利器

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

  4. wxPython:Python首选的GUI库 | CSDN博文精选

    作者 | 天元浪子 来源 | CSDN博客 文章目录 概述 窗口程序的基本框架 事件和事件驱动 菜单栏/工具栏/状态栏 动态布局 AUI布局 DC绘图 定时器和线程 后记 概述 跨平台的GUI工具库, ...

  5. pv3d 打造三维全景图

    做三维全景图,需要找到一张全景照片,利用pv3d的立方体或球体贴图即可轻松实现.这里用的是球体,图片是在网上找的,有兴趣的朋友也可以自己做. 来源:(http://blog.sina.com.cn/s ...

  6. python的三维坐标轴设置_使用PyOpenGL绘制三维坐标系实例

    我就废话不多说了,直接上代码吧! def drawCoordinate(): ''' 绘制三维的坐标系,并绘制由坐标轴构成的平面的网格,各个坐标轴的颜色以及由坐标轴所引出的网格线的颜色为: x: (1 ...

  7. 智慧煤矿数字孪生打造三维可视化管理平台

    如今各行业都在逐步推进信息化.数字化转型与升级,煤矿行业也不例外.现阶段煤矿企业智能化发展迅速,智能设备仪器产生数据量级大,数字孪生通过数据孪生对数据进行全面监控和管理,将报警数据.设备全生命周期数据 ...

  8. python画三维坐标_使用PyOpenGL绘制三维坐标系实例

    我就废话不多说了,直接上代码吧! def drawCoordinate(): ''' 绘制三维的坐标系,并绘制由坐标轴构成的平面的网格,各个坐标轴的颜色以及由坐标轴所引出的网格线的颜色为: x: (1 ...

  9. 打造APP广告变现利器,了解聚合广告SDK的选择技巧与优势

    ​随着移动互联网的快速发展和普及,APP已成为了人们日常生活中不可缺少的一部分,其涉及新闻.购物.游戏.生活服务等等. 而对于各类APP开发者来说,如何对自己的应用进行更好的广告变现,从而提升预算为客 ...

最新文章

  1. 网络推广外包专员浅析为什么说早在建站初期就要做好网络推广外包
  2. 单片机中断机制对日常生活的启示_单片机原理与接口技术(高起专)阶段性作业1...
  3. ElementUI中弹窗使用textarea原样显示SpringBoot后台带换行的StringBuilder内容
  4. C++智能指针简单介绍
  5. 2019《去哪儿》春季校招第一站(含笔试题)
  6. 普元部署包部署找不到构建_让我们在5分钟内构建和部署AutoML解决方案
  7. Membership学习记录
  8. 实验5 matlab程序设计2,实验5 Matlab程序设计2
  9. Yocto Project - basic - 01 - Quick Start
  10. Path(2)之verp中path position和path length的区别
  11. Mysql的存储过程(以Mysql为例进行讲解)
  12. 黑帽python第二版(Black Hat Python 2nd Edition)读书笔记 之 第七章 GitHub命令与控制(2)构建基于GitHub的特洛伊木马框架
  13. MySQL 通过 jemalloc 管理内存
  14. 有道词典翻译功能数字有时无法翻译出来解决方案
  15. 基于Python网络爬虫的小说网站数据分析
  16. 我们到底能从《别逗了,费曼先生》中学到什么?
  17. 使用dockpanel动态添加picturebox并绑定图片
  18. 【微信小程序】-- 全局配置 -- window - 下拉刷新 上拉触底(十六)
  19. DialogFragment中通过dataBinding绑定View,设置点击事件无效,通过getWindow设置dialog位置和大小无效。
  20. 手机摄影技巧总结——永远拍出美美的照片

热门文章

  1. 抖音名字怎么改不了_抖音名字怎么改不了怎么办
  2. 基于聚合数据API的星座配对(Python)
  3. lstm结构图_ON-LSTM:能表示语言层次的LSTM
  4. 果蔬去皮机的总体设计、玉米秸秆粉碎机设计、采煤机截割部的设计、曲柄压力机设计、剪式液压升降台设计、榨汁机的结构设计、全液压升降机设计、垃圾压榨机液压系统设计、链板输送机设计、无级变速器结构设计……
  5. geoserver发布tif格式数据
  6. Unity 用代码设置物体的标签 Tag: is not defined.
  7. RTB matlab工具箱(1)——demo与常用函数
  8. 在线翻译图片怎么做?再不怕外文看不懂咯
  9. 热议!互联网大厂46分钟裁员内部录音曝光!“制定一个完不成的目标”、“明确他是能力不行!”...
  10. MySQL的使用(1)