當前位置:
首頁 > 知識 > wxPython + PyOpenGL 打造三維數據分析的利器!| CSDN 博文精選

wxPython + PyOpenGL 打造三維數據分析的利器!| CSDN 博文精選

wxPython + PyOpenGL 打造三維數據分析的利器!| CSDN 博文精選

wxPython + PyOpenGL 打造三維數據分析的利器!| CSDN 博文精選

作者 | 天元浪子

責編 | 伍杏玲

出品 | 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 + PyOpenGL 打造三維數據分析的利器!| CSDN 博文精選

關於 wxPython

我一直認為,wxPython 是最適合 Python 的GUI庫,並為此專門寫過一篇博文。詳情見《wxPython:Python首選的GUI庫》。這裡不再討論如何使用 wxPython,只貼出幾張開發項目的截圖,展示一下 wxPython 的風格。

下圖為 wxPython + PyOpenGL 開發的項目截圖(隱去敏感信息):

wxPython + PyOpenGL 打造三維數據分析的利器!| CSDN 博文精選

下圖為界面細節展示(隱去敏感信息):

wxPython + PyOpenGL 打造三維數據分析的利器!| CSDN 博文精選

wxPython + PyOpenGL 打造三維數據分析的利器!| CSDN 博文精選

下圖為 wxPython 的傳統風格:

wxPython + PyOpenGL 打造三維數據分析的利器!| CSDN 博文精選

wxPython + PyOpenGL 打造三維數據分析的利器!| CSDN 博文精選

關於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 打造三維數據分析的利器!| CSDN 博文精選

wxPython + PyOpenGL 打造三維數據分析的利器!| CSDN 博文精選

wxPython + PyOpenGL 打造三維數據分析的利器!| CSDN 博文精選

wxPython + PyOpenGL 打造三維數據分析的利器!| CSDN 博文精選

wxPython + PyOpenGL 打造三維數據分析的利器!| CSDN 博文精選

wxPython + PyOpenGL 打造三維數據分析的利器!| CSDN 博文精選

架起溝通 wxPython 和 PyOpenGL 的橋樑

wx.glcanvas.GLCanvas 是 wxPython 為顯示 OpenGL 提供的類,顧名思義,我們可以將其理解為 OpenGL 的畫板。有了這個畫板,我們就可以使用 OpenGL 提供的各種工具在上面繪製各種三維模型了。

下面這段代碼,從 wx.glcanvas.GLCanvas 派生了新類 WxGLScene,綁定了滑鼠滾輪事件,並以立即渲染模式(Immediate mode)畫了兩個三角形。受限於篇幅,刪去了滑鼠拖拽操作,僅保留了滾輪縮放功能。

# -*- coding: utf-8 -*-

import wx
from wx import glcanvas
from OpenGL.GL import *
from OpenGL.GLU import *

class WxGLScene(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, stylex=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) # 綁定滑鼠滾輪事件

def onResize(self, evt):
"""響應窗口尺寸改變事件"""

if self.context:
self.SetCurrent(self.context)
self.size = self.GetClientSize
self.Refresh(False)

evt.Skip

def onErase(self, evt):
"""響應背景擦除事件"""

pass

def onPaint(self, evt):
"""響應重繪事件"""

self.SetCurrent(self.context)
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT) # 清除屏幕及深度緩存
self.drawGL # 繪圖
self.SwapBuffers # 切換緩衝區,以顯示繪製內容
evt.Skip

def onLeftDown(self, evt):
"""響應滑鼠左鍵按下事件"""

self.CaptureMouse
self.mpos = evt.GetPosition

def onLeftUp(self, evt):
"""響應滑鼠左鍵彈起事件"""

try:
self.ReleaseMouse
except:
pass

def onRightUp(self, evt):
"""響應滑鼠右鍵彈起事件"""

pass

def onMouseMotion(self, evt):
"""響應滑鼠移動事件"""

if evt.Dragging and evt.LeftIsDown:
pos = evt.GetPosition
try:
dx, dy = pos - self.mpos
except:
return
self.mpos = pos

# 限於篇幅省略改變觀察者位置的代碼

self.Refresh(False)

def onMouseWheel(self, evt):
"""響應滑鼠滾輪事件"""

if evt.WheelRotation < 0:
self.zoom *= 1.1
if self.zoom > 100:
self.zoom = 100
elif evt.WheelRotation > 0:
self.zoom *= 0.9
if self.zoom < 0.01:
self.zoom = 0.01

self.Refresh(False)

def initGL(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)

def drawGL(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 wx
from scene import *

APP_TITLE = u"架起溝通 wxPython 和 pyOpenGL 的橋樑"

class mainFrame(wx.Frame):
"""程序主窗口類,繼承自wx.Frame"""

def __init__(self):
"""構造函數"""

wx.Frame.__init__(self, None, -1, APP_TITLE, stylex=wx.DEFAULT_FRAME_STYLE)

self.SetBackgroundColour(wx.Colour(224, 224, 224))
self.SetSize((800, 600))
self.Center

self.scene = WxGLScene(self)

class mainApp(wx.App):
def OnInit(self):
self.SetAppName(APP_TITLE)
self.Frame = mainFrame
self.Frame.Show
return True

if __name__ == "__main__":
app = mainApp
app.MainLoop

WxGLScene 類的使用示例:

#-*- coding: utf-8 -*-

import wx

from scene import *

APP_TITLE = u"架起溝通 wxPython 和 pyOpenGL 的橋樑"

class mainFrame(wx.Frame):

"""程序主窗口類,繼承自wx.Frame"""

def __init__(self):

"""構造函數"""

wx.Frame.__init__(self, None, -1, APP_TITLE, stylex=wx.DEFAULT_FRAME_STYLE)

self.SetBackgroundColour(wx.Colour(224, 224, 224))

self.SetSize((800, 600))

self.Center

self.scene = WxGLScene(self)

class mainApp(wx.App):

def OnInit(self):

self.SetAppName(APP_TITLE)

self.Frame = mainFrame

self.Frame.Show

return True

if __name__ == "__main__":

app = mainApp

app.MainLoop

界面效果如下:

wxPython + PyOpenGL 打造三維數據分析的利器!| CSDN 博文精選

wxPython + PyOpenGL 打造三維數據分析的利器!| CSDN 博文精選

場景、視區和模型

OpenGL 允許用戶使用 glViewport 命令設置多個視口,這意味著我們可以在顯示屏幕上分割出多個顯示區域,這些區域可以相互重疊,在邏輯上是完全獨立的。我們可以將 WxGLScene 稱作場景(scene),由 glViewport 命令創建的視口稱為視區(region),擁有相同名字的三維部件定義為模型(model)。一個場景可以添加多個視區,一個視區可以創建多個模型。

以曲面模型為例,函數原型如下:

def drawSurface(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)

"""

wxPython + PyOpenGL 打造三維數據分析的利器!| CSDN 博文精選

三維重建的實例

手頭有 109 張頭部 CT 的斷層掃描圖片,我打算用這些圖片嘗試頭部的三維重建。基礎工作之一,就是要把這些圖片數據讀出來,組織成一個三維的數據結構(實際上是四維的,因為每個像素有 RGBA 四個通道)。

wxPython + PyOpenGL 打造三維數據分析的利器!| CSDN 博文精選

這個數據結構,自然是 numpy 的 ndarray 對象,讀取圖像文件我習慣使用 PIL。因此,需要導入兩個模塊:

import numpy as np

from 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 np

from PIL import Image

import wx

import win32api

import sys, os

from wxgl.scene import *

from wxgl.colormap import *

FONT_FILE = r"C:WindowsFontssimfang.ttf"

APP_TITLE = u"CT斷層掃描三維重建工具"

APP_ICON = "res/head.ico"

class mainFrame(wx.Frame):

"""程序主窗口類,繼承自wx.Frame"""

def __init__(self):

"""構造函數"""

wx.Frame.__init__(self, None, -1, APP_TITLE, stylex=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

class mainApp(wx.App):

def OnInit(self):

self.SetAppName(APP_TITLE)

self.Frame = mainFrame

self.Frame.Show

return True

if __name__ == "__main__":

app = mainApp

app.MainLoop

三維重建後的效果如下圖:

wxPython + PyOpenGL 打造三維數據分析的利器!| CSDN 博文精選

如果對這個話題感興趣,請直接聯繫我吧:xufive@sdysit.com

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

聲明:本文系CSDN博客原創文章,轉載請聯繫原作者。

【END】

wxPython + PyOpenGL 打造三維數據分析的利器!| CSDN 博文精選

熱 文推 薦

?17 歲成為 iOS 越獄之父,25 歲造出無人車,黑客傳奇!

?React 是如何成為跨越前端開發鴻溝的橋樑?

?江蘇互聯網圖鑑

?打臉!特朗普要解禁華為?美七家科技公司聯合要求恢復供貨

?博士畢業最高201萬!華為頂級薪酬招「天才少年」

?別再說學不會:超棒的Numpy可視化學習教程來了!

?百度入局, 一文讀懂年交易過4億「超級鏈」究竟是什麼?

?無伺服器計算,如何節省時間和成本?

?中國第一程序員,微軟得不到他就要毀了他!

wxPython + PyOpenGL 打造三維數據分析的利器!| CSDN 博文精選

你點的每個「在看」,我都認真當成了喜歡

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 CSDN 的精彩文章:

紅帽:將開源進行到底
開發者,什麼是你真正關心的問題?| AI ProCon 2019

TAG:CSDN |