使用Matplotlib繪製3D圖形

paulquei發表於2018-12-03

本文是Matplotlib的第二篇文章,會講解如何通過Matplotlib繪製3D圖形。關於Matplotlib的第一篇文章,請看這裡:Python繪相簿Matplotlib入門教程

測試環境

由於這是一個Python語言的軟體包,因此需要你的機器上首先安裝好Python語言的環境。關於這一點,請自行在網路上搜尋獲取方法。

關於如何安裝Matplotlib請參見這裡:Matplotlib Installing

筆者推薦大家通過pip或者anaconde的方式進行安裝。

本文中的原始碼和測試資料可以在這裡獲取:Github: matplotlib_tutorial

本文的程式碼示例會用到其他一些Python庫。建議讀者先對其有一定的熟悉,我的部落格中也有一些相關文章。

本文的程式碼在如下環境中測試:

  • Apple OS X 10.13
  • Python 3.6.2
  • matplotlib 2.2.3
  • numpy 1.14.1

準備

繪製3D圖形的時候我們通常都會包含下面這個程式碼片段,這裡我們先對其進行說明。

import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

fig = plt.figure()
ax = fig.gca(projection=`3d`)

這4行程式碼說明如下:

  • 第一行自然不必說,就是匯入matplotlib.pyplot
  • 第二行from mpl_toolkits.mplot3d import Axes3D是匯入Axes3D類。我們後面在繪製3D圖形的時候,相應的函式都位於這個介面上。
  • fig = plt.figure()是獲取到當前figure物件。
  • ax = fig.gca(projection=`3d`)這一行是比較關鍵的。fig.gca是獲取圖中的當前極軸。如果不存在,或者不是極軸,則將建立相應的軸,然後返回。此時得到的ax物件的型別是Axes3D的子類,這個物件將是繪製3D圖形的入口。

Colormap

繪製圖形的時候,常常會需要對圖形著色。Matplotlib中內建了很多的Colormap來簡化這個工作,具體可以看這裡:Choosing Colormaps

下面是一些Colormap示例:

lightness_05.png

通過指定相應的名稱我們就可以直接使用這裡的Colormap了。

線形圖

Axes3D.plot 函式用來繪製線形圖。

首先我們來看最簡單的圖形 – 線形圖。

由於這是三維空間中的線,所以需要若干個(x, y, z)座標的值。

下面這段程式碼生成了一條三維空間中的直線。

# line.py

import matplotlib.pyplot as plt
import numpy as np

from mpl_toolkits.mplot3d import Axes3D

fig = plt.figure()
ax = fig.gca(projection=`3d`)

x = np.linspace(-10, 10, 1000)
y = np.linspace(-10, 10, 1000)
z = np.add(x, y)

ax.plot(x, y, z)
plt.show()

從這段程式碼可以看出,這條線的x和y軸的範圍都是[-10, 10]。我們共計取樣了1000個點。

需要注意的是,由於線是通過點來描繪的,每一個點都由[x,y,z]三個座標值來確定,因此這裡x,y,z三個陣列的元素數量應該是一樣多的。

z軸取值為np.add(x, y)。請注意,np.add是元素級(element-wise)的運算:它是將x和y兩個陣列的元素逐個相加,所以得到的結果仍然是包含了1000個元素的陣列。

這段程式碼得到的結果如下:

line.png

散點圖

Axes3D.scatter 函式用來繪製散點圖。

下面我們再來看一下散點圖。

和線形圖類似,它也是展示若干個(x, y, z)座標的值。區別在於,這裡僅僅是一些點,沒有通過線連在一起。

下面是一段程式碼示例:

# scatter.py

import matplotlib.pyplot as plt
import numpy as np

from mpl_toolkits.mplot3d import Axes3D

fig = plt.figure()
ax = fig.gca(projection=`3d`)

count = 100
range = 100

xs = np.random.rand(count) * range
ys = np.random.rand(count) * range
zs = np.random.rand(count) * range

ax.scatter(xs, ys, zs, s=zs, c=zs)

ax.set_xlabel(`X Label`)
ax.set_ylabel(`Y Label`)
ax.set_zlabel(`Z Label`)

plt.show()

這段程式碼中,我們還設定了三個座標軸的Label。

另外,所有點的x,y,z軸都是隨機的。範圍都在100以內。並且,我們在顯示這些點的時候,根據z值的大小設定了點的顏色和尺寸以示區分。

最終我們得到的圖形如下所示:

scatter.png

線框圖

Axes3D.plot_wireframe 函式用來繪製線框圖。

線框圖要比前面的圖形要複雜一些。

線框圖展示的是一個曲面的框架結構,由於是一個面,因此它在x,y兩個座標的整個面上都應該有所取值。

前面兩種圖形的x,y軸的值都是一維的陣列,而對於線框圖來說,其x,y軸的取值應該是一個二維的矩陣。

例如,我們設定x的範圍是[1, 3]之間,y的範圍是[11, 15]之間。並且,每一個整數座標取一個點,那麼如下的所有點上都會對應一個z值:

$$
egin{pmatrix}
[1,11], [2,11], [3,11] \
[1,12], [2,12], [3,12] \
[1,13], [2,13], [3,13] \
[1,14], [2,14], [3,14] \
[1,15], [2,15], [3,15]
end{pmatrix}
$$

而對於描述x,y軸的兩個陣列來說,它們各自應該是下面這樣的矩陣:

$$
egin{pmatrix}
1, 2, 3 \
1, 2, 3 \
1, 2, 3 \
1, 2, 3 \
1, 2, 3
end{pmatrix}
$$

$$
egin{pmatrix}
11, 11, 11 \
12, 12, 12 \
13, 13, 13 \
14, 14, 14 \
15, 15, 15
end{pmatrix}
$$

這裡的兩個矩陣其實是互相由對方資料的數量而確定尺寸的。

numpy中的meshgrid函式剛好可以幫我們完成這個功能,下面是一段程式碼示例:

# meshgrid_demo.py

import numpy as np

x = np.arange(1, 4)
y = np.arange(11, 16)
print(x)
print(y)

X, Y = np.meshgrid(x, y)
print(X)
print(Y)

請仔細觀察一下它的輸出:

[1 2 3]
[11 12 13 14 15]
[[1 2 3]
 [1 2 3]
 [1 2 3]
 [1 2 3]
 [1 2 3]]
[[11 11 11]
 [12 12 12]
 [13 13 13]
 [14 14 14]
 [15 15 15]]

有了這個基礎之後,我們就可以以此來產生我們需要的線框圖了。

假設我們要展示的函式如下:

$$

z = -x^3 + y^4 , -10 lt x,y lt 10

$$

我們可以通過下面這段程式碼來生成這個函式的圖形:

# wireframe.py

import matplotlib.pyplot as plt
import numpy as np

from mpl_toolkits.mplot3d import Axes3D

fig = plt.figure()
ax = fig.gca(projection=`3d`)

x = np.arange(-10, 10, 0.1)
y = np.arange(-10, 10, 0.1)
X, Y = np.meshgrid(x, y)

Z = np.add(-np.power(X, 3), np.power(Y, 4))

surf = ax.plot_wireframe(X, Y, Z)

plt.show()

請注意,這段程式碼中關於np的函式都是元素級(element-wise)的運算。

請讀者思考一下,np.power(X, 3)X**3的含義分別是什麼。

這段程式碼所得到的圖形如下所示:

wireframe.png

曲面圖

Axes3D.plot_surface 函式用來繪製曲面圖。

曲面圖和線框圖類似,它們都是描述三維空間中的曲面的。區別在於:曲面圖中的面是著色的。

下面這段程式碼繪製出了下面這個函式的圖形:

$$

z = -x^3 + y^2, -10 lt x, y lt 10

$$

# surface.py

import matplotlib.pyplot as plt
import numpy as np

from matplotlib import cm
from mpl_toolkits.mplot3d import Axes3D

fig = plt.figure()
ax = fig.gca(projection=`3d`)

x = np.arange(-10, 10, 0.1)
y = np.arange(-10, 10, 0.1)
X, Y = np.meshgrid(x, y)

Z = np.add(-np.power(X, 3), np.power(Y, 2))

surf = ax.plot_surface(X, Y, Z, cmap=cm.gist_rainbow)
fig.colorbar(surf, shrink=0.5, aspect=5)

plt.show()

這段程式碼整體應該都不難理解,只有兩個地方需要說明一下:

  1. 這裡通過cmap=cm.gist_rainbow指定了曲面的顏色。更多的Colormap請到查閱這裡:Choosing Colormaps
  2. 通過 fig.colorbar(surf, shrink=0.5, aspect=5)新增了一個色彩條。shrink指定了色彩條與圖形高度的比例,aspect指定了色彩條本身的長寬比。

這段程式碼得到的圖形如下所示:

surface.png

等高線

Axes3D.contour 函式用來繪製等高線。

等高線顧名思義,就是描述高度相等的線。等高線通常伴隨主體圖形一起出現,輔助我們觀察主體圖形的一些特性。

有了前面的基礎,繪製等高線也就很容易了。

下面這段程式碼繪製出了以下這個函式的線框圖以及等高線:

$$

Z = -x^4 + Y^4, -10 lt x, y lt 10

$$

# contour.py

import matplotlib.pyplot as plt
import numpy as np

from matplotlib import cm
from mpl_toolkits.mplot3d import Axes3D

fig = plt.figure()
ax = fig.gca(projection=`3d`)

x = np.arange(-10, 10, 0.1)
y = np.arange(-10, 10, 0.1)
X, Y = np.meshgrid(x, y)

Z = np.add(-np.power(X, 4), np.power(Y, 4))

ax.set_xlabel("X")
ax.set_ylabel("Y")
ax.plot_wireframe(X, Y, Z, alpha=0.1)
ax.contour(X, Y, Z, cmap=cm.Accent, linewidths=2)

plt.show()

為了便於觀察等高線,我們將主體圖形的線框圖透明度設為0.1,然後將等高線的粗度設定為2。

上面這段程式碼得到的圖形如下所示:

contour.png

這個圖形比較複雜,單從一個角度不太容易看清楚其完整結構,文末我們會講解怎麼製作一副動態圖來展示圖形的全貌。

柱狀圖

Axes3D.bar 函式用來繪製柱狀圖。

柱狀圖也是很常用的圖。

下面這段程式碼展示了這樣一種場景:在一副圖中,對比一個城市四年期間每個月的降水量。

在這幅圖中,每一年的12個月是一組柱狀圖。四年的資料進行了前後的對比展示。

程式碼如下:

# bar.py

import matplotlib.pyplot as plt
import numpy as np

from matplotlib.collections import PolyCollection
from mpl_toolkits.mplot3d import Axes3D

fig = plt.figure()
ax = fig.gca(projection=`3d`)

np.random.seed(59)
month = np.arange(1, 12)
years = [2016, 2017, 2018, 2019]

def get_color(value_array):
    color = []
    for v in value_array:
        if (v < 50):
            color.append(`y`)
        elif (v < 100):
            color.append(`g`)
        elif (v < 150):
            color.append(`b`)
        elif (v < 200):
            color.append(`c`)
        elif (v < 250):
            color.append(`m`)
        else:
            color.append(`r`)
    return color

for year, c in zip(years, [`b`,`c`,`r`,`m`]):
    value = np.random.rand(len(month)) * 300
    ax.bar(month, value, year, zdir=`y`, color=get_color(value), alpha=0.7)
    for i in np.arange(0, 12):
        ax.bar

ax.set_xlabel(`Month`)
ax.set_xticks(np.arange(1, 13))
ax.set_ylabel(`Year`)
ax.set_yticks(np.arange(2016, 2020))
ax.set_zlabel(`Precipitation`)

plt.show()

在這段程式碼中,我們通過隨機數生成了每個月的降水量。並且根據降水量的程度設定了條柱的顏色以示區分。

我們最終得到的圖形如下所示:

bar.png

多邊形

Axes3D.add_collection3d 函式用來向圖形中新增3D集合物件。

對於某些資料(例如降水量)來說,我們也可能希望通過多邊形來了解其每個點的走勢。

下面這段程式碼通過多邊形的形式展示了和上面柱狀圖一樣的資料。

# poly.py

import matplotlib.pyplot as plt
import numpy as np

from matplotlib.collections import PolyCollection
from mpl_toolkits.mplot3d import Axes3D

fig = plt.figure()
ax = fig.gca(projection=`3d`)

np.random.seed(59)
month = np.arange(0, 13)
years = [2016, 2017, 2018, 2019]

precipitation = []
for year in years:
    value = np.random.rand(len(month)) * 300
    value[0], value[-1] = 0, 0
    precipitation.append(list(zip(month, value)))

poly = PolyCollection(precipitation, facecolors=[`b`,`c`,`r`,`m`])
poly.set_alpha(0.7)

ax.add_collection3d(poly, zs=years, zdir=`y`)
ax.set_xlabel(`Month`)
ax.set_xlim3d(0, 12)
ax.set_ylabel(`Year`)
ax.set_ylim3d(2015, 2020)
ax.set_zlabel(`Precipitation`)
ax.set_zlim3d(0, 300)

plt.show()

Axes3D.add_collection3d 函式除了支援PolyCollection,還支援LineCollectionPatchCollection。這一點,讀者可以自行研究一下。

上面這段程式碼得到的圖形如下:

poly.png

製作動圖

很多時候,我們可能需要製作一張動畫圖來展示圖形的全貌,下面我們就來看一下如何做到。

生成不同角度的圖形

為了製作動圖,我們需要先有製作動圖的圖片素材。

下面我們就以前面等高線那個函式生成的複雜圖形為例,來看看如何生成一個關於這個圖形不同角度的動圖。

相關程式碼如下:

# surface_files.py

import matplotlib.pyplot as plt
import numpy as np

from matplotlib import cm
from mpl_toolkits.mplot3d import Axes3D

fig = plt.figure(figsize=(10, 8))
ax = fig.gca(projection=`3d`)

x = np.arange(-10, 10, 0.1)
y = np.arange(-10, 10, 0.1)
X, Y = np.meshgrid(x, y)

Z = np.add(-np.power(X, 4), np.power(Y, 4))

ax.set_xlabel("X")
ax.set_ylabel("Y")
ax.plot_surface(X, Y, Z, cmap=cm.hsv)

for angle in range(95, 180, 3):
    ax.set_zlabel("Angle: " + str(angle))
    ax.view_init(30, angle)
    filename = "./" + str(angle) + ".png"
    plt.savefig(filename)
    print("Save " + filename + " finish")

這段程式碼其實並不複雜,與前面的區別主要就是在於程式碼最後的for迴圈。

在這個for迴圈中,我們選取了從95到180這個範圍的角度,每隔3做一次取樣,每次取樣做如下的事情:

  • 通過set_zlabel設定了當前旋轉角度的Label
  • 通過ax.view_init(30, angle)設定圖形的視角
  • 根據當前角度生成一個單獨的檔名稱
  • 通過plt.savefig(filename)儲存檔案
  • 列印日誌

這段程式碼執行完成之後,我們就會得到一系列的png檔案。下面我們就通過這些png檔案來生成動圖。

使用ImageMagick

這裡通過一個免費的跨平臺工具ImageMagick來製作動圖。該工具支援 Linux,Windows,Mac OS X,iOS和Android等各個平臺。

首先,我們到這裡進行下載:Download ImageMagick

請根據你的平臺選擇下載哪個版本。

由於我是Mac使用者,所以直接通過下面的命令就可以安裝ImageMagick。

brew install ImageMagick

安裝好之後,命令列就會有convert工具。通過這個工具就可以生成動圖了。

相關命令如下:

convert -delay 50 *.png animated.gif

當然,你可以研究一下這個命令的其他引數和功能。這裡就不贅述了。

我們最終得到的動圖看起來像下面這個樣子:

animated.gif

結束語

能夠繪製3D圖形將是一項非常有用的技能。因為在今後的機器學習過程中,我們常常會將資料以圖形的形式展示出來,以便我們觀察和了解。

由於篇幅所限,本文只介紹了一些最基本的用法,但實際上Matplotlib所支援的功能遠不止這些,因此建議讀者朋友們以此為基礎繼續進行更多的探索。

參考資料與推薦讀物


相關文章