3D庫WxGL的demo——用3D給思維插上想象的翅膀

1 前言

上週冒着零星小雨去附近的公園賞花,估計腦子裏多少進了一些雨水,以至於連 z=xyz=xy 這樣的曲面是什麼樣子,都想象不出來了。無奈之下,只好跑去問女兒。彼時,她正在ipad上整理課堂筆記。我湊近瞄了一眼,瞬間感覺頭暈目眩,幾乎暈倒。這個課堂筆記,將數學的險惡展示得一覽無餘!
在這裏插入圖片描述
聽完我的問題,女兒笑了:用一方手帕表示這個曲面,手帕的左下角、右上角高高提起,左上角、右下角自然垂落,大概就是 z=xyz=xy 的樣子。xxyy同號則zz大於零,異號則zz小於零,這麼簡單的問題,你都搞不懂,腦子是不是進水了?

天哪,腦子進水這事兒,居然被她猜到了!

“別亂說,下雨那天我打傘了。”

我一邊小聲反駁着,一邊落荒式地逃離了女兒的房間。

這事兒雖然有損於我在女兒心目中的光輝象形,倒也反映了一個事實:有些看似簡單的數學方程,卻可以構造出極其複雜的曲面或幾何體,如果沒有3D工具的輔助,即便腦子沒有進水,人類也很難憑空想象出它的樣子。另外,在學習或研究過程中,我們關注的數據往往會藏身於茫茫“數海”中,如果不借助於3D技術,我們很難想象它們是什麼樣子的,又是如何分佈的。

WxGL正是這樣一個用於應對上述需求的3D數據可視化工具,可以很方便地畫一些點面線體及其組合。關於WxGL,更多的信息請參考《開源我的3D庫WxGL:40行代碼將疫情地圖變成三維地球模型》。WxGL最初是我們的開發團隊自用的小工具,所以開源以後既沒有像樣的文檔,也沒有簡單的例子。本文就算是開源3D庫WxGL的demo吧,全部應用實例集成在一個腳本中,同時增加了3D系統信息顯示和位置姿態設置。本文僅對部分代碼做解讀,並沒有提供完整源碼。源碼已經更新到了GitHub,感興趣的同學可以去下載。
在這裏插入圖片描述

2 正弦曲線 y=sinxy=sinx

繪製xxooyy平面上的正弦曲線,首先要約定xx的值域範圍,從中(等距離)取出一定數量的點,計算各點對應的yy值。不用說,每個點對應的zz值一定是零。將各點xxyyzz拼合成頂點集vv,顏色集ccyy的大小做映射,用drawLine()就可以輕鬆畫出正弦曲線了。

x = np.linspace(-2*np.pi, 2*np.pi, 1000)
y = np.sin(x)
z = np.zeros(1000)
v = np.dstack((x,y,z))[0]
c = self.cm.map(y, self.cm_curr, mode='RGBA')

self.master.drawLine('sin', v, c, method='SINGLE')
self.master.update()

顯示效果:
在這裏插入圖片描述

3 最簡單的曲面 z=xyz=xy

xx[1,1][-1, 1]之間均勻取51個點,yy[1,1][-1, 1]之間均勻取51個點,生成xxyy的網格,分別計算網格上每個點的xxyy的積作爲zz,顏色集cczz的大小做映射,用drawMesh()繪製網格。

y, x = np.mgrid[-1:1:51j, -1:1:51j]
z = x*y
c = self.cm.map(z, self.cm_curr, mode='RGBA')
            
self.master.drawMesh('z=xy', x, y, z, c, mode=self.render)
self.master.update()

下圖使用了“前面顯示線條後面填充顏色”渲染效果。如果兩面都使用顏色或者線條,視覺效果會比較平淡。
在這裏插入圖片描述

4 稍微有點難度的曲面 z=sin(x)+cos(y)z=sin(x)+cos(y)

xx[π,π][-\pi,\pi]之間均勻取51個點,yy[π,π][-\pi,\pi]之間均勻取51個點,生成xxyy的網格,分別計算網格上每個點的sin(x)sin(x)cos(y)cos(y)的和作爲zz,顏色集cczz的大小做映射,用drawMesh()繪製網格。

y, x = np.mgrid[-np.pi:np.pi:51j, -np.pi:np.pi:51j]
z = np.sin(x) + np.cos(y)
c = self.cm.map(z, self.cm_curr, mode='RGBA')
            
self.master.drawMesh('z=xy', x, y, z, c, mode=self.render)
self.master.update()

這個效果像不像一隻大水母?
在這裏插入圖片描述

5 無法憑空想象的曲面 z=2xex2+y2z=\frac{2x}{e^{x^2+y^2}}

xx[1,1][-1, 1]之間均勻取51個點,yy[1,1][-1, 1]之間均勻取51個點,生成xxyy的網格,分別計算網格上每個點的2xex2+y2\frac{2x}{e^{x^2+y^2}}作爲zz,顏色集cczz的大小做映射,用drawMesh()繪製網格。

x, y = np.mgrid[-2:2:50j,-2:2:50j]
z = 2*x*np.exp(-x**2-y**2)
c = self.cm.map(z, self.cm_curr, mode='RGBA')
            
self.master.drawMesh('z=xy', x, y, z, c, mode=self.render)
self.master.update()

下圖使用了“前面填充顏色後面顯示線條”渲染效果:
在這裏插入圖片描述換個角度,換個ColorMap,看看效果:
在這裏插入圖片描述
兩面全用顏色試一試:
在這裏插入圖片描述

6 體數據 sin(x)+sin(y)+sin(z)sin(x)+sin(y)+sin(z)

對於空間中的一個點,其座標爲(x,y,z)(x,y,z),如果將 sin(x)+sin(y)+sin(z)sin(x)+sin(y)+sin(z) 映射爲該點的顏色,則該顏色集就可以成爲體數據。

y, x = np.mgrid[-10:10:101j, -10:10:101j]
z = np.linspace(-10, 10, 101)
v = np.sin(z).repeat(101*101).reshape((101,101,101)) + np.sin(x) + np.sin(y)
c = self.cm.map(v, self.cm_curr, mode='RGBA')

self.master.drawVolume('volume', c, x, y, z, smooth=False)
self.master.update()

這是以原點爲中心的 20×20×2020\times20\times20 的立方體,每一個點的顏色和 sin(x)+sin(y)+sin(z)sin(x)+sin(y)+sin(z) 的對應關係如ColorBar所示。
在這裏插入圖片描述
但是,很多時候,我們更關心在這數據體內,某一類數據,比如說 sin(x)+sin(y)+sin(z)=0sin(x)+sin(y)+sin(z)=0 的點有哪些?又是如何分佈的呢?很簡單,我們只需要把這些點之外的其他點的顏色的透明度置爲零,我們在視覺上就只會看到 sin(x)+sin(y)+sin(z)=0sin(x)+sin(y)+sin(z)=0 的點了。

c = self.cm.map(np.where((v>-0.1)&(v<0.1), v, np.nan), self.cm_curr, mode='RGBA')

由於我們在空間中的選取的點不是連續的,因此,我們把 sin(x)+sin(y)+sin(z)=0sin(x)+sin(y)+sin(z)=0 的條件改爲 0.5<sin(x)+sin(y)+sin(z)<0.5-0.5<sin(x)+sin(y)+sin(z)<0.5,顯示出來的結果是這樣的。
在這裏插入圖片描述
被剔除的另一部分是這樣的:
在這裏插入圖片描述

如果把篩選條件改爲 0.1<sin(x)+sin(y)+sin(z)<0.1-0.1<sin(x)+sin(y)+sin(z)<0.1,顯示出來的結果是這樣的。是不是感覺有點魔性呢?
在這裏插入圖片描述
如果把篩選條件改爲 0.01<sin(x)+sin(y)+sin(z)<0.01-0.01<sin(x)+sin(y)+sin(z)<0.01,顯示出來的結果是這樣的。感覺稍微正常了一點。
在這裏插入圖片描述

7 球和六面體的組合

在三維空間中生成一個球體表面上各個點的座標,需要藉助於參數方程。我們可以藉助於地球的經緯度概念,按照固定步長,經度從-180°變化到到180°,維度從-90變化到°到90°,就得到了經度和維度網格:

lat, lon = np.mgrid[-0.5*np.pi:0.5*np.pi:51j, -np.pi:np.pi:101j]

根據球體表面上每個點的經度緯度,很容易計算出每個點的空間座標:

z = np.sin(lat)
x = np.cos(lat)*np.cos(lon)
y = np.cos(lat)*np.sin(lon)

爲了讓球體表面顏色漂亮一點,我們用每一點上xxyy的乘積映射顏色:

c = self.cm.map(x*y, self.cm_curr, mode='RGBA')

使用drawMesh()畫出這個網格:

self.master.drawMesh('ball', x, y, z, c=c, mode=self.render)

六面體相對簡單一些,我們可以分開畫六個面,每個面的顏色隨機生成:

v0, v1, v2, v3 = [1,1,-1], [-1,1,-1], [-1,-1,-1], [1,-1,-1]
v4, v5, v6, v7 = [1,1,1], [-1,1,1], [-1,-1,1], [1,-1,1]
            
bottom = np.array([v0, v3, v2, v1])*0.75
top = np.array([v4, v5, v6, v7])*0.75
front = np.array([v7, v6, v2, v3])*0.75
back = np.array([v4, v0, v1, v5])*0.75
right = np.array([v4, v7, v3, v0])*0.75
left = np.array([v6, v5, v1, v2])*0.75
            
self.master.drawSurface('cubo', bottom, c=np.random.random(3), mode=self.render)
self.master.drawSurface('cubo', top, c=np.random.random(3), mode=self.render)
self.master.drawSurface('cubo', front, c=np.random.random(3), mode=self.render)
self.master.drawSurface('cubo', back, c=np.random.random(3), mode=self.render)
self.master.drawSurface('cubo', right, c=np.random.random(3), mode=self.render)
self.master.drawSurface('cubo', left, c=np.random.random(3), mode=self.render)

用線條勾勒出的球和六面體:
在這裏插入圖片描述
用顏色表現出的球和六面體:
在這裏插入圖片描述用“前面填充顏色後面顯示線條”的方式表現出的球和六面體:
在這裏插入圖片描述

8 地球

有了畫球的經驗,畫地球就輕車熟路了。唯一不同的是,球體表面每一個點的顏色,要對應到平面圖上。

# 從等經緯地圖上讀取經緯度網格上的每一個格點的顏色
c = np.array(Image.open('res/shadedrelief.png'))/255
            
# 生成和等經緯地圖分辨率一致的經緯度網格,計算經緯度網格上的每一個格點的空間座標(x,y,z)
lats, lons = np.mgrid[np.pi/2:-np.pi/2:complex(0,c.shape[0]), 0:2*np.pi:complex(0,c.shape[1])]
x = np.cos(lats)*np.cos(lons)
y = np.cos(lats)*np.sin(lons)
z = np.sin(lats)
            
self.master.drawMesh('earth', x, y, z, c)
self.master.update()

全球平面圖:
在這裏插入圖片描述

生成的地球效果:
在這裏插入圖片描述

9 三維重建

基於頭部CT斷層掃描圖片,可以完成頭部的三維重建。在這類,我使用了體數據繪製的方法。

# 讀取109張頭部CT的斷層掃描圖片
data = np.stack([np.array(Image.open('res/head%d.png'%i)) for i in range(109)], axis=0)
data = np.rollaxis(data, 2, start=0)[::-1] # 反轉數組軸(2軸變0軸),然後0軸逆序
            
# 三維重建(本質上是體數據繪製)
self.master.drawVolume('volume', data/255.0, method='Q', smooth=False)
self.master.update()

部分CT斷層掃描圖片:
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

三維重建後的效果如下圖。因爲斷層掃描不夠精細,也沒有插值,層與層之間的縫隙比較明顯。如果斷層數據足夠多,效果還可以更好一些。
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章