mplot3d(三维绘图)

Generating 3D plots using the mplot3d toolkit.
mpl_toolkits.mplot3d provides some basic 3D plotting (scatter, surf, line, mesh) tools. Not the fastest or most feature complete 3D library out there, but it ships with Matplotlib and thus may be a lighter weight solution for some use cases. Check out the mplot3d tutorial for more information.
mplot3d模块主要包含4个部分(api):

  • mpl_toolkits.mplot3d.axes3d
  • mpl_toolkits.mplot3d.axis3d
  • mpl_toolkits.mplot3d.art3d
  • mpl_toolkits.mplot3d.proj3d

其中,axes3d()中主要包含了绘制各种3-D图像的类和方法。axis3d()主要包含了与坐标轴相关的类和方法。art3d()包含了一些可将2D图像转换并用于3D绘制的类和方法。proj3d()中包含一些零碎的类和方法,例如计算三维向量长度等。
一般情况下,用得最多的就是mpl_toolkits.mplot3d.axes3d()下的mpl_toolkits.mplot3d.axes3d.Axes3D()类,而Axes3D()下面又存在绘制不同类型3D图的方法。你可以通过下面的方式导入Axes3D():

from mpl_toolkits.mplot3d.axes3d import Axes3D

由于Axes3D()十分常用,所以还支持更加方便的导入形式:

from mpl_toolkits.mplot3d import Axes3D
In [2]:
import matplotlib.pyplot as plt
%matplotlib
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.mplot3d.art3d import Poly3DCollection,Line3DCollection
from matplotlib import colors as mcolors
import numpy as np
plt.rcParams['font.sans-serif']=['SimHei']
plt.rcParams['axes.unicode_minus']=False
Using matplotlib backend: Qt5Agg

开始吧

正如2-D中的axes,Axes3D对象也是我们3-D绘图的核心之一,顾名思义,Axes3D是一个“三维轴域对象”,是我们的“画布”,同样它位于一个figure容器对象中,一个figure可以容纳多个Axes3D。创建Axes3D就像创建其它任何类型的轴域对象一样,使用关键字projection='3d',然后添加到figure中即可:

plt.subplot(111,projection='3d') # 自动添加到figure
# 或者:
fig=plt.figure()
ax=fig.add_subplot(111,projection='3d')
# 或者:
fig=plt.figure()
ax=fig.gca(projection='3d')  # 获取当前axes并映射为3D

注:此方法(推荐)为1.0.0版本的更新,对于使用更早之前旧版本的人,创建Axes3D画布的方法如下:

fig=plt.figure()
ax=Axes3D(fig) # 替代"add_subplot()"步骤

绘制线形图(Line plots)

函数原型:Axes3D.plot(xs, ys, *args, zdir='z', **kwargs)
参数说明:
xs,ys:点在x轴和y轴上的坐标
zs:点在z轴上的坐标,可以是一个标量,适用于全部点,否则xs,ys,zs(序列)应具有相同的长度
zdir:zs实际上是可选参数,即Axes3D.plot()可以用于创建2-D图象,只不过显示在三维坐标系空间中罢了,这就涉及谁去谁留的问题,zdir参数决定将哪个轴去掉,而保留下另外两个轴用于绘制2-D数据集,譬如zdir='x',则绘制出来的2-D图像将显示在yoz平面上。虽然默认情况下三维坐标空间没有显示轴信息,但是你需要知道哪个是x轴,哪个是y轴,哪个又是z轴
注:plot()主要用于绘制空间曲线,曲线方程:z=F(x,y),参数xs,ys,zs分别对应自变量x,y和因变量z
其它关键字参数都将被传递给axes.plot()函数(pyplot.plot()),其中axes特指2-D轴域对象(因为mplot3d是利用matpotlib的2-D图形绘制方法绘制3-D图像)

In [106]:
plt.subplot(111,projection='3d')
plt.plot([1,2,3],[2,4,6],zdir='y') # 注意,plot命令是作用在当前轴域对象上的,也就是Axes3D,因此相当于Axes3D.plot(),即此plot()非彼plot()(pyplot模块)
plt.xlabel('x轴')
plt.ylabel('y轴')
plt.gca().set_zlabel('z轴') # plt.zlabel('z轴') 无效命令,只有通过Axes3D对象方法set_zlabel('z轴')才行!
Out[106]:
Text(0.5,0,'z轴')
In [37]:
# 分别创建2D和3D坐标系,可以在同一figure中,也可以在不同figure中(执行plt.figure()将创建并切换至新的figure),另外请勿更改一个已经定性的轴域对象的映射,譬如已经指定'rectilinear'就不能再修改映射为'3d'
plt.subplot(121,projection='3d')
plt.plot([1,2,3],[2,4,6],zdir='y')
plt.subplot(122,projection='rectilinear') # 普通二维轴域对象
plt.plot([1,2,3,4,4,3,2,1,1],[4,3,2,1,4,3,2,1,4])
Out[37]:
[<matplotlib.lines.Line2D at 0x1e4a4cd4240>]

螺旋曲线

参数方程:
$\begin{cases} x=a\cos\theta \\ y=a\sin\theta \\ z=b\theta \end{cases}$

In [40]:
theta=np.linspace(0,6*np.pi,100)
x=np.sin(theta)
y=np.cos(theta)
z=theta
Axes3D(plt.gcf()) # 虽然这种方法已经过时了,不过是真的能省力,少写两个单词感觉真好~
plt.plot(x,y,z,color='green',marker='o')
Out[40]:
[<mpl_toolkits.mplot3d.art3d.Line3D at 0x1f77bfa2b70>]

绘制散点图(Scatter plots)

函数原型:Axes3D.scatter(xs, ys, zs=0, zdir='z', s=20, c=None, depthshade=True, *args, **kwargs)
参数说明:
xs,ys:点在x轴和y轴上的坐标
zs:点在z轴上的坐标,可以是一个标量,适用于全部点,否则xs,ys,zs(序列)应具有相同的长度,缺省值为0
zdir:当zs参数缺省时,表示绘制二维数据集及图像,默认抛弃多余的z轴,即在xoy平面绘图,若指定zdir='x',表示抛弃x轴,若指定zdir='y',表示抛弃y轴
s:设置点的尺寸大小,单位为points^2,可以是一个标量适用于全部点,否则应与xs或ys具有相同的长度
c:设置颜色,可以是单个颜色字符串,譬如'red',也可以是颜色字符串序列,长度与xs一致。参数c还可以是一个数值序列,映射至默认或指定的colormap。Note that c should not be a single numeric RGB or RGBA sequence because that is indistinguishable from an array of values to be colormapped. c can be a 2-D array in which the rows are RGB or RGBA, however, including the case of a single row to specify the same color for all points.
depthshade:是否根据点标记在空间中的远近程度以不同的颜色深浅度装饰外观(阴影效果),默认为True
其它关键字参数都被传递给pyplot.scatter()
Returns a Patch3DCollection

In [92]:
ax=Axes3D(plt.gcf())
n=40
ax.scatter(np.random.rand(n),np.random.rand(n),np.random.rand(n)-0.3,s=40,c=np.random.rand(n),cmap='cool',marker='*') # 我有一个十分困惑的地方,此前我一直使用命令模式(所谓命令风格,就是当你使用 from matplotlib.pyplot import * 的形式导入模块的时候,你可以直接输入函数指令绘图,而不必再加plt.前缀),
# 也就是将本句替换成 plt.scatter(...,s=30,...)将会导致错误:TypeError: scatter() got multiple values for argument 's'
# 真希望有人能够解释一下具体的原因?这里也给我一个提醒,就是在绘制3D图像的时候,尽量使用对象编程,上面的显示z轴标签的例子再次说明了这一点!
ax.scatter(np.random.rand(n),np.random.rand(n),np.random.rand(n)+0.3,s=20,c='red')
Out[92]:
<mpl_toolkits.mplot3d.art3d.Path3DCollection at 0x1e4ad873f98>

绘制线框图(Wireframe plots)

函数原型:Axes3D.plot_wireframe(X, Y, Z, *args, **kwargs)
参数说明:
X,Y,Z:二维数组,是主要用于绘制二元函数图像的数据集,其中z=F(x,y),如果仅要显示函数在某一长方形区域(有定义)内的图像(如果不是在长方形区域中全部有定义,则不可以,譬如说我们要观察球上半球体,它的x,y的合法取值区域为圆而非长方形,这时使用meshgrid()和直角方程求解矩阵z那就是白瞎,不可能画出来半球面,而只能使用参数方程构建),则生成x,y的最佳办法是:x,y=np.meshgrid(xticks,yticks),meshgrid()生成一个网格坐标矩阵或者说对一个长方形平面区域中的点进行采样
rcount,ccount:设置每个方向上的最大采样数,默认为50,对于给定的x,y参数(x、y、z具有完全相同的形状),二维数组x的列数即是沿x轴的采样数量,x的行数即是沿y轴的采样数量,若其超出rcount或ccount的值,则对超出的那个方向进行“二次采样”,譬如x.shape[-1]=100且ccount为缺省值,那么“二次采样”将从100个采样中选出50个来,取舍的方法就是“切片”。如果将rcount或ccount设置为0,将不在相应方向上进行采样,两者不能同时设为0,具体的实验一下就明白了
rstride,cstride:这两个参数与rcount、ccount不能共存,用于设置切片slice操作的stride,同样用来控制采样数量。如果只设置rstride或cstride的其中一个,另一个则默认为1,若将步幅置为0,则不在相应方向上采样
其它关键字参数将传递给Line3DCollection

In [44]:
# 绘制二元函数z=x^2+y^2(抛物面)的线框图
xticks=np.arange(-100,100)
yticks=xticks
x,y=np.meshgrid(xticks,yticks)
z=x*x+y*y
ax=Axes3D(plt.gcf())
ax.plot_wireframe(x,y,z,rstride=50,cstride=50,colors=(1,0.5,0.5,1)) # colors: A sequence of RGBA tuples # 我们设置了采样区域范围为:
# {(x,y)|-100<=x<=100,-100<=y<=100},一个方形区域,并指定采样间距为50,所以采样点数为:(200/50+1)^2,加1的原因就不需要我多说了吧,一切见到图就清楚了
Out[44]:
<mpl_toolkits.mplot3d.art3d.Line3DCollection at 0x1cbda41c8d0>

In [ ]:
from mpl_toolkits.mplot3d import axes3d
ax=Axes3D(plt.gcf())
x,y,z=axes3d.get_test_data(0.05) # 来自文档(此步未看)
ax.plot_wireframe(x,y,z)

绘制曲面图(Surface plots)

函数原型:Axes3D.plot_surface(X, Y, Z, *args, norm=None, vmin=None, vmax=None, lightsource=None, **kwargs)
参数说明:
X,Y,Z,rcount,ccount,rstride,cstride:参见plot_wireframe()函数
color:color-like,表面贴片的颜色
cmap:Colormap,表面贴片的颜色映射集(默认情况下,它将以纯色的阴影着色,但它也可以通过提供cmap参数来支持颜色映射)
facecolors:array-like of colors,每个表面贴片的颜色(?
norm:Normalize,用于颜色映射过程中的标准化操作
vmin,vmax:float,设置标准化的上下阈值
shade:bool,是否给face color加阴影效果,默认开启
其它关键字参数全部传递给Poly3DCollection

球体

对以(a,b,c)为圆心,R为半径的球体,其直角系方程为:
$(x-a)^2+(y-b)^2+(z-c)^2=R^2$
参数方程:
$\begin{cases} x=a + R \sin \varphi \cos \theta \\ y=b + R \sin \varphi \sin \theta \\ z=c + R \cos \varphi \end{cases}$
其中$\theta$和$\varphi$的取值范围为: $0 \leq \theta \leq 2\pi$,$0 \leq \varphi \leq \pi$

In [38]:
# 通过直角方程显然无法求出,我们使用参数方程:
a=0
b=0
c=0
R=4
# 采样区域
theta=np.linspace(0,2*np.pi,30)
phi=np.linspace(0,np.pi,20)
# theta和phi构成的采样区域内二元函数x=a+Rsinφcosθ的值矩阵x的计算利用
# “要计算x、y构成的样本区域内f(x)*g(y)的值矩阵z,有z=outer(f(x),g(y))”(具体见./numpy基础使用.ipynb#outer()),
# 于是:
x=a+R*np.outer(np.sin(phi),np.cos(theta))
y=b+R*np.outer(np.sin(phi),np.sin(theta))
z=c+R*np.outer(np.cos(phi),np.ones(len(theta)))
# 绘图:
ax1=plt.subplot(121,projection='3d')
ax1.plot_wireframe(x,y,z)
ax2=plt.subplot(122,projection='3d')
surf=ax2.plot_surface(x,y,z,cmap=plt.cm.coolwarm,linewidth=0,antialiased=False) # cmap=plt.get_cmap('cool') # antialiased ?
plt.gcf().colorbar(surf,shrink=0.5)
Out[38]:
<matplotlib.colorbar.Colorbar at 0x1f77984e978>

Tri-Surface plots

函数原型:Axes3D.plot_trisurf(*args, color=None, norm=None, vmin=None, vmax=None, lightsource=None, **kwargs)
参数说明:
X,Y,Z:Data values as 1D arrays???
color:表面贴片的颜色
cmap:表面贴片的颜色映射
norm:用于颜色映射的“标准化”实例对象(An instance of Normalize to map values to colors)
vmin:对输入数值进行颜色映射(标准化)的下阈值(Minimum value to map)
vmax:对输入数值进行颜色映射(标准化)的上域值(Maximum value to map)
shade:是否给face color加阴影效果(Whether to shade the facecolors) 以下内容我是一脸懵逼?
可选的triangulation参数可以用以下两种方式指定:
plot_trisurf(triangulation, ...)
其中triangulation参数是一个Triangulation对象,或者:
plot_trisurf(X, Y, ...)
plot_trisurf(X, Y, triangles, ...)
plot_trisurf(X, Y, triangles=triangles, ...)
在这种情况下,将创建Triangulation对象。有关这些可能性的解释,请参阅Triangulation
其余的参数是:
plot_trisurf(..., Z)
where Z is the array of values to contour, one per point in the triangulation.
其它参数都被传给Poly3DCollection

示例(来自文档):

'''
Plot a 3D surface with a triangular mesh.
'''

# This import registers the 3D projection, but is otherwise unused.
from mpl_toolkits.mplot3d import Axes3D  # noqa: F401 unused import

import matplotlib.pyplot as plt
import numpy as np

n_radii = 8
n_angles = 36

# Make radii and angles spaces (radius r=0 omitted to eliminate duplication).
radii = np.linspace(0.125, 1.0, n_radii)
angles = np.linspace(0, 2*np.pi, n_angles, endpoint=False)[..., np.newaxis]

# Convert polar (radii, angles) coords to cartesian (x, y) coords.
# (0, 0) is manually added at this stage,  so there will be no duplicate
# points in the (x, y) plane.
x = np.append(0, (radii*np.cos(angles)).flatten())
y = np.append(0, (radii*np.sin(angles)).flatten())

# Compute z to make the pringle surface.
z = np.sin(-x*y)

fig = plt.figure()
ax = fig.gca(projection='3d')
ax.plot_trisurf(x, y, z, linewidth=0.2, antialiased=True)

plt.show()

绘制轮廓图(Contour plots)

函数原型:Axes3D.contour(X, Y, Z, *args, extend3d=False, stride=5, zdir='z', offset=None, **kwargs)
参数说明:
X,Y:ndarray
Z
extend3d:Whether to extend contour in 3D (default: False)
stride:Stride (step size) for extending contour
zdir:The direction to use: x, y or z (default)
offset:If specified plot a projection of the contour lines on this position in plane normal to zdir
The positional and other keyword arguments are passed on to contour()
返回一个contour

Filled contour plots

绘制多边形图(Polygon plots)

函数原型:Axes3D.add_collection3d(col, zs=0, zdir='z')
将3D collection对象添加到三维绘图中
通过修改对象并添加z坐标信息,可以将2D collection类型转换为3D版本,支持的是:

zdir参数怎么没效果?,一点也不像bar()那样称心

In [138]:
# PolyCollection
# color setting(摘自文档)
def cc(arg): # 未看
    '''Shorthand to convert 'named' colors to rgba format at 60% opacity.'''
    return mcolors.to_rgba(arg, alpha=0.6)
colors=[cc('r'),cc('g'),cc('b')]
# prepaer data
n=40
# 2-D: 二维平面上的多边形的顶点(顶点依次连接即得该多边形,总是默认执行“最后一个点将与第一个点连接”的操作),一个"ploy"即可由其全部顶点的集合描述,将多个"ploy"的序列作为参数传入PolyCollection的构造函数即得PolyCollection对象
x=np.linspace(0,10,n)
np.random.seed(20190122)
y1=np.random.randint(1,10,n)
y1[0],y1[-1]=0,0
y2=np.random.randint(1,10,n)
y2[0],y2[-1]=0,0
y3=np.random.randint(1,10,n)
y3[0],y3[-1]=0,0
# add 'z' to 2-D plot: 为二维平面上的多边形的全部顶点添加第三个维度的信息,且相同
plot1=list(zip(x,np.zeros(n)-1,y1))
plot2=list(zip(x,np.zeros(n),y2))
plot3=list(zip(x,np.zeros(n)+1,y3))
# 按照构建二维PolyCollection的方式构建3D版本
poly3d=Poly3DCollection([plot1,plot2,plot3],facecolors=colors)
ax=plt.subplot(224,projection='3d')
# 将Poly3DCollection中全部的多边形绘制到三维坐标系中
ax.add_collection3d(poly3d)
ax.set_xlim3d(0, 10)
ax.set_zlim3d(0, 15)
ax.set_ylim3d(-2, 2)
# 若放在二维坐标系中,则需要三张子图显示
y=[y1,y2,y3]
for i in range(3):
    plt.subplot('22'+str(i+1))
    plt.fill_between(x,y[i],color=colors[i])

正方体

In [3]:
# 要构建出一个正方体出来,需要绘制6个面,也就是6个3-D poly,总共涉及8个点
# 假设8个顶点坐标分别如下(编号分别为0-7)
points=[(1,1,-1),(-1,1,-1),(-1,-1,-1),(1,-1,-1),(1,1,1),(-1,1,1),(-1,-1,1),(1,-1,1)]
# 6个面的构成(点的编号)
faces=[(0,4,7,3,0),(0,1,5,4,0),(1,2,6,5,1),(2,3,7,6,2),(4,5,6,7,4),(0,1,2,3,0)] # 如果是绘制polygon,那么faces可以是:[(0,4,7,3),(0,1,5,4),(1,2,6,5),(2,3,7,6),(4,5,6,7),(0,1,2,3)]
# 因为要填充区域,最后一个点总是会和第一个点连接以构成闭合区域,但是若绘制line,则必须给出全部点,否则会开个口子,这也是很明了的道理
plots=[[points[point_num] for point_num in face] for face in faces]
# 颜色
def cc(arg):
    return mcolors.to_rgba(arg, alpha=0.6)
colors=[cc('r'),cc('g'),cc('b'),cc('y'),cc('purple'),cc('pink')]
# 三维PolyCollection
poly3d=Poly3DCollection(plots,facecolors=colors)
line3d=Line3DCollection(plots) # 构建三维LineCollection,另外三维的PatchCollection可以参见"Bar Plots"
ax1=plt.subplot(121,projection='3d')
ax1.add_collection3d(poly3d)
ax1.set_xlim3d(-2, 2)
ax1.set_ylim3d(-2, 2)
ax1.set_zlim3d(-2, 2)
ax2=plt.subplot(122,projection='3d')
ax2.add_collection3d(line3d)
ax2.set_xlim3d(-2, 2)
ax2.set_ylim3d(-2, 2)
ax2.set_zlim3d(-2, 2)
for i,pos in enumerate(points):
    ax2.text(*pos,str(pos)+' 编号'+str(i))
for i,pos in enumerate([(1,0,0),(0,1,0),(-1,0,0),(0,-1,0),(0,0,1),(0,0,-1)]):
    ax2.text(*pos,'面'+str(i+1),color='red',fontsize=15,alpha=0.6)

绘制柱状图(Bar plots)

函数原型:Axes3D.bar(left, height, zs=0, zdir='z', *args, **kwargs)
参数说明:
left:“柱”左端的x轴坐标
height:“柱”的高度
zs:2-D柱状图的z轴坐标,一个标量。每幅柱状图只能平行于xoy面("Polygon plots"示例1也是如此,此即“2-D可以方便转化为3-D”的含义,即始终平行于xoy面)
zdir:Which direction to use as z ('x', 'y' or 'z') when plotting a 2D set.
Keyword arguments are passed onto bar().
Returns a Patch3DCollection

In [4]:
n=18
x=np.arange(0,n)
np.random.seed(19971028)
y1=np.random.rand(n)
y2=np.random.rand(n)
y3=np.random.rand(n)
ax=Axes3D(plt.gcf())
def cc(arg): # 好棒~
    return mcolors.to_rgba(arg, alpha=0.6)
colors=[cc('r'),cc('g'),cc('b')]
for i,y in enumerate([y1,y2,y3]):
    ax.bar(x,y,-1+i,zdir='y',color=[colors[i]]*n) # 沿着y轴放置一幅幅柱状图,即柱状图平行于xoz面
    ax.plot(x,[-1+i]*len(x),y,color='black')

Quiver

函数原型:Axes3D.quiver(*args, length=1, arrow_length_ratio=0.3, pivot='tail', normalize=False, **kwargs)

绘制文本(Text)

函数原型:Axes3D.text(x, y, z, s, zdir=None, **kwargs)
Add text to the plot. kwargs will be passed on to Axes.text, except for the zdir keyword, which sets the direction to be used as the z direction.

In [181]:
ax=Axes3D(plt.gcf())
# 以下来自文档
# Demo 1: zdir
zdirs = (None, 'x', 'y', 'z', (1, 1, 0), (1, 1, 1))
xs = (1, 4, 4, 9, 4, 1)
ys = (2, 5, 8, 10, 1, 2)
zs = (10, 3, 8, 9, 1, 8)
for zdir, x, y, z in zip(zdirs, xs, ys, zs):
    label = '(%d, %d, %d), dir=%s' % (x, y, z, zdir)
    ax.text(x, y, z, label, zdir)
# Demo 2: color
ax.text(9, 1, 7.5, "red", color='red',bbox=dict(boxstyle="Roundtooth",ec=(1., 0.5, 0.5),fc=(1., 0.8, 0.8)))
# Demo 3: text2D
# Placement 0, 0 would be the bottom left, 1, 1 would be the top right.
ax.text2D(0.05, 0.95, "2D Text", transform=ax.transAxes)
# Tweaking display region and labels
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
ax.set_zlim(0, 10)
ax.set_xlabel('X axis')
ax.set_ylabel('Y axis')
ax.set_zlabel('Z axis')
Out[181]:
Text(0.5,0,'Z axis')

动态图(交互式)

循环:     获取新数据     绘制     pause()

In [213]:
# 动态散点图
n=50
marker=['.',',','o','v','^','<','>','1','2','3','4','s','p','*','h','H','+','x','D','d','|','_']
ax=Axes3D(plt.gcf())
ax.set_xlim(0,1)
ax.set_ylim(0,1)
ax.set_zlim(0,1)
x=np.random.rand(n)
y=np.random.rand(n)
z=np.random.rand(n)
for i in range(n):
    ax.scatter(x[i],y[i],z[i],s=np.random.randint(10,60),marker=np.random.choice(marker))
    plt.pause(0.08)

SegmentLocal

注意事项

  • 涉及Axes3D,请使用对象编程(Axes3D_obj.对象方法)而非命令风格(plt.模块方法)