HOWTO · Matplotlib

如何在 Matplotlib 中创建曲面图

我们可以使用 plot_surface()方法在 Matplotlib 中创建表面图。

本页内容

要在 Matplotlib 中创建 3D 曲面图,请准备形状兼容的二维 X、Y 和 Z 数组,使用 projection="3d" 创建坐标轴,再将这些数组传给 Axes3D.plot_surface。下面的示例根据数学函数生成坐标网格,为坐标轴添加标签和颜色条,并保存图像。如果测量值的坐标和高度能够表示为矩形网格,同样可以使用这一流程。

配置环境

NumPy 用于数组和网格运算,Matplotlib 用于绘图。在终端中,针对你准备使用的 Python 环境运行 python -m pip install numpy matplotlib。如果你通过虚拟环境或笔记本管理软件包,请改为在相应环境中安装这两个软件包,确保运行脚本的解释器能够使用它们。

示例会导入 NumPy 和 matplotlib.pyplot。Matplotlib 自带 3D 绘图支持;使用 projection="3d" 即可创建 3D 坐标轴,无需单独导入 Axes3D。该坐标轴使用的方法是 Axes3D.plot_surface。

准备数据

曲面图会为二维坐标平面上的每个位置指定一个高度。对于等间距坐标,np.linspace() 可生成一维坐标值,np.meshgrid() 则将它们扩展为二维 X 和 Y 网格。你可以根据这些网格计算 Z,也可以提供与网格位置对应的测量高度。

三个数组的形状必须兼容:每个 Z[i, j] 都是坐标对 X[i, j]、Y[i, j] 处的高度。当前版本的 Matplotlib 也接受一维的 x、y 坐标向量,并会根据二维的 Z 将其展开;但高度数组本身仍必须与网格维度一致:如果 Z 是在与 X、Y 不同的网格上计算出来的,就会引发 ValueError,下方的故障排除一节会用实际运行结果来说明。应先创建坐标网格,再在相同的网格上计算或重塑高度数组。如果测量点是离散分布的,无法组成矩形网格,请先准备网格数据,或在使用 plot_surface 前对数据进行插值。

创建曲面图

这个完整示例使用平滑的径向函数 sin(sqrt(x² + y²))。它会创建网格、绘制曲面、标注三个维度、添加颜色刻度,并将结果保存到当前目录中的 surface-plot.png。请在已安装 NumPy 和 Matplotlib 的环境中运行。示例不会向控制台打印文本;它会保存图像,并在当前后端支持显示时打开 Matplotlib 图窗。示例中坐标轴、标题和颜色条上可见的标签均保持英文。

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(-5, 5, 100)
y = np.linspace(-5, 5, 100)
X, Y = np.meshgrid(x, y)
Z = np.sin(np.sqrt(X**2 + Y**2))

fig = plt.figure()
ax = fig.add_subplot(111, projection="3d")
surface = ax.plot_surface(X, Y, Z, cmap="viridis")
ax.set_xlabel("X")
ax.set_ylabel("Y")
ax.set_zlabel("Z")
ax.set_title("Surface Plot")
fig.colorbar(surface, ax=ax, label="Z value")
fig.savefig("surface-plot.png", dpi=150, bbox_inches="tight")
plt.show()

使用 viridis 颜色映射绘制的径向正弦函数 3D 曲面图,附带显示 Z 值在 -1 到 1 之间的颜色条。

由于此脚本不会打印文本,因此没有控制台输出块。颜色条将曲面颜色与对应数值关联起来;网格决定几何形状,颜色映射则控制曲面的外观。在笔记本中,绘图环境通常会显示图像;在脚本中,plt.show() 会请求已配置的 Matplotlib 后端显示图像。

自定义曲面图

下面的示例将最常见的自定义方式应用到同一个曲面。它把颜色映射切换为 plasma、移除网格边线,并在添加颜色条、将图形保存为 customized-surface-plot.png 之前,将视角旋转到固定的仰角和方位角。

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(-5, 5, 100)
y = np.linspace(-5, 5, 100)
X, Y = np.meshgrid(x, y)
Z = np.sin(np.sqrt(X**2 + Y**2))

fig = plt.figure()
ax = fig.add_subplot(111, projection="3d")
surface = ax.plot_surface(X, Y, Z, cmap="plasma", edgecolor="none")
ax.set_xlabel("X")
ax.set_ylabel("Y")
ax.set_zlabel("Z")
ax.set_title("Customized Surface Plot")
ax.view_init(elev=30, azim=45)
fig.colorbar(surface, ax=ax, label="Z value")
fig.savefig("customized-surface-plot.png", dpi=150, bbox_inches="tight")
plt.show()

使用 plasma 颜色映射、移除网格边线,并将视角旋转到仰角 30 度、方位角 45 度的同一个曲面图。

选择一种能清楚呈现 Z 变化的颜色映射。在此示例中,cmap="plasma" 取代了第一个示例的 viridis 渐变,颜色条可以帮助理解这种对应关系。edgecolor="none" 选项会移除绘制出的网格边线,这些边线在密集网格上可能干扰连续曲面的呈现;如果希望采样网格本身保持可见,请保留默认边线。

若要调整视角,请在保存或显示图像之前调用 ax.view_init(elev=30, azim=45)。elev 用于调整观察高度,azim 用于绕垂直轴旋转视角。调整这两个值,直到曲面形状和坐标轴清晰可见;合适的角度取决于数据以及你想要呈现的内容。与第一张图相比,第二张图旋转后的视角能看到更多中央高峰远侧的斜面。

对于密集网格,plot_surface 可以使用 rcount 和 ccount 等行列采样控制项。降低这些值会减少采样的行数和列数,可能加快大型曲面的渲染速度,但也会损失可见细节。应保留足够的采样点,以呈现数据中的重要变化。使用 fig.savefig("surface-plot.png", dpi=150, bbox_inches="tight") 保存图像以便分享;对于显示后会关闭图像的后端,应在调用 plt.show() 之前保存。若要保留多个版本,请使用不同的文件名或目录。

与线框图比较

线框图只绘制同一曲面的网格线,不填充表面,也不使用颜色映射。Axes3D.plot_wireframe 接受相同的 X、Y、Z 网格,因此不需要准备新数据就能在两种视图之间切换。当网格结构本身很重要、填充的曲面会遮住形状的远侧,或者需要在正式绘制曲面前快速检查陌生数据的结构时,线框图很有用。

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(-5, 5, 50)
y = np.linspace(-5, 5, 50)
X, Y = np.meshgrid(x, y)
Z = np.sin(np.sqrt(X**2 + Y**2))

fig = plt.figure()
ax = fig.add_subplot(111, projection="3d")
ax.plot_wireframe(X, Y, Z, rstride=2, cstride=2, color="tab:blue")
ax.set_xlabel("X")
ax.set_ylabel("Y")
ax.set_zlabel("Z")
ax.set_title("Wireframe Plot")
fig.savefig("wireframe-plot.png", dpi=150, bbox_inches="tight")
plt.show()

仅用蓝色网格线绘制、未填充表面的同一个径向正弦曲面线框图。

rstride 和 cstride 参数会跳过部分行和列,让线条保持易读;示例会绘制 50×50 网格中每隔一行和一列的线。与 plot_surface 不同,线框图没有表面颜色,因此没有需要颜色条说明的对象。对于最终呈现,填充的曲面通常能更好地传达高度差异,而线框图则更清楚地显示网格本身的采样方式。

修复形状不兼容的错误

plot_surface 最常见的失败,是 Z 数组的形状与坐标网格不匹配。下面的脚本在 50×50 的网格上计算 Z,却将它与 100×100 的 X、Y 网格一起传入:

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(-5, 5, 100)
y = np.linspace(-5, 5, 100)
X, Y = np.meshgrid(x, y)

x_small = np.linspace(-5, 5, 50)
y_small = np.linspace(-5, 5, 50)
X_small, Y_small = np.meshgrid(x_small, y_small)
Z = np.sin(np.sqrt(X_small**2 + Y_small**2))

fig = plt.figure()
ax = fig.add_subplot(111, projection="3d")
ax.plot_surface(X, Y, Z, cmap="viridis")
plt.show()

运行脚本时,会在 plot_surface 的调用处停止,并出现这个错误:

ValueError: shape mismatch: objects cannot be broadcast to a single shape.  Mismatch is between arg 0 with shape (100, 100) and arg 2 with shape (50, 50).

修复方法是像第一个示例那样,从传给 plot_surface 的同一个网格计算 Z,让 Z.shape 等于 X.shape 和 Y.shape。如果不确定,请在绘图前打印三个形状并比较:Z 的每个元素都需要恰好一对坐标,不能多也不能少。

总结

关键步骤是创建坐标网格、使 Z 与网格形状匹配、添加 3D 坐标轴,然后调用 Axes3D.plot_surface。颜色映射和颜色条有助于说明高度值,视角和采样控制项则有助于清晰呈现曲面形状。当你需要检查网格本身时,plot_wireframe 会以线条绘制相同的数据。需要图像文件时请保存图像,并根据所绘制的数据调整网格分辨率或视角。

常见问题

  1. 曲面图可以使用什么数据?
    使用由两个坐标定义的数值,并确保每个高度都对应矩形网格中的一个位置。用函数生成数据便于演示;测量值只要排列在兼容的坐标网格上也可以使用。离散点需要先转换为网格数据。

  2. 如何安装所需的库?
    在运行脚本的环境中使用 python -m pip install numpy matplotlib 安装 NumPy 和 Matplotlib。

  3. 可以更改曲面颜色吗?
    可以。设置 plot_surface 的 cmap 参数,再通过 fig.colorbar(surface, ax=ax) 添加颜色条,显示颜色与数值的对应关系。

  4. 如何调整视角?
    使用 ax.view_init(elev=..., azim=...)。仰角会改变相机高度,方位角会改变相机绕图形旋转的角度。

  5. 为什么 X、Y 和 Z 必须具有兼容的形状?
    Matplotlib 需要为网格中的每个位置提供一对坐标和一个高度。如果 Z 是在不同的网格上计算出来的,plot_surface 就会引发关于无法一起广播的形状的 ValueError。使用 np.meshgrid() 创建二维坐标网格,并根据相同的网格计算 Z,使对应位置的数组元素彼此对齐。