在 matplotlib 中显示最大表面?

Display the maximum surface in matplotlib?

我正在使用 matplotlib 在同一个图形上绘制多个曲面,我只想看到最上面的曲面,如 matlab 所示。

Matlab 3D视图:

Matlab顶视图:

Matplotlib 3D 视图:

Matplotlib 俯视图:

如何让 Matplotlib 显示类似于 Matlab 的结果,其中最顶部的 class 显示在顶部,而不是一个 class 优先于另一个?

回答

正如问题的评论中所指出的,matplotlib 并没有真正进行 3d 绘图,它所做的近似可以给你有限的结果。您遇到的问题实际上已在 mplot3d module's FAQ.

中得到确认

如果您想进行严肃的 3D 绘图,他们还会将您引导至 MayaVi。如果您真的不需要 3D 绘图并且只关心顶视图,那么我会按照 Bensciens 在评论中的建议直接进行 2D 绘图...

肮脏的解决方法

当然,如果您愿意用程序员的灵魂付出代价,几乎总有一个涉及一些黑魔法的解决方案...:P

选项 1

如果你真的只需要你作为例子的两个视图,并且表面与那些相似,你可以先绘制表面 A 后面的部分,然后是所有表面 B,然后是表面的部分在表面 A 之上...让我解释一下:

正如所指出的,here and here plot_surfaces() 不关心掩码,但您可以使用 NaN 值来获得类似的效果。您可以使用它首先仅绘制低于另一个表面的值,然后仅绘制高于...

from mpl_toolkits.mplot4d import Axes3D
import matplotlib.pyplot as plt
import numpy as np

fig = plt.figure()
ax = fig.gca(projection='3d')
X = np.arange(-5, 5, 0.25)
Y = np.arange(-5, 5, 0.25)
X, Y = np.meshgrid(X, Y)

R = (X+Y)
Z1 = R/R.max()
Z2 = -R/R.max()

surfA_bottom = ax.plot_surface(X, Y, np.where(Z1<=Z2,Z1, np.nan),
                               rstride=1, cstride=1, color='r', linewidth=0)

surfB = ax.plot_surface(X, Y, Z2,
                        rstride=1, cstride=1, color='b', linewidth=0)

surfA_top = ax.plot_surface(X, Y, np.where(Z1>=Z2,Z1, np.nan),
                            rstride=1, cstride=1, color='r', linewidth=0)

ax.set_zlim3d(-1, 1)
ax.set_ylim(-5,5)
ax.set_xlim(-5,5)

plt.show()

选项 2

(里面有解释,想知道答案的直接跳到最后一段!)

这个解决方案稍微复杂一些,但对于更复杂的表面也更稳健......问题是 matplotlib 中的 3d 图不能很好地处理 different[= 的深度80=] 对象...对吗?但它适用于 单个 对象... 那么如何将两个表面绘制为 单个 表面呢??

为此,您需要将所有点合并到一个曲面中(对于重复的 X-Y 组合,您可以有多个 Z 值)。为了区分我们新表面的两个部分(我们以前的两个表面),我们可以使用 facecolors kwarg。 (我添加了一些 alpha 值以更清楚地看到发生了什么)

from mpl_toolkits.mplot4d import Axes3D
import matplotlib.pyplot as plt
import numpy as np

fig = plt.figure()
ax = fig.gca(projection='3d')
X = np.arange(-5, 5, 0.25)
Y = np.arange(-5, 5, 0.25)
X, Y = np.meshgrid(X, Y)

Z1 = np.sin(np.sqrt(X**2+Y**2))
Z2 = np.ones_like(Z1)*0.6

C1 = np.empty_like(X, dtype=str)
C1.fill('b')
C2 = C1.copy()
C2.fill('r')

X3 = np.vstack([X,X])
Y3 = np.vstack([Y,Y])
Z3 = np.vstack([Z1,Z2])
C3 = np.vstack([C1,C2])


surf3 = ax.plot_surface(X3, Y3, Z3, rstride=1, cstride=1,
                       facecolors=C3, linewidth=0,
                       antialiased=False, alpha=0.5)

ax.set_zlim3d(-1, 2)
plt.show()

如您所见,结果非常好,但有一些奇怪的效果,因为一个表面的一个极端连接到另一个表面的另一个极端。如何摆脱它? 透明胶片不是一种选择,因为据我所知,plot_surface() 只允许影响整个表面的 alpha 值。我还尝试使用 X、Y 和 Z 中的一行 NaN 值来 mask 转换,其方式与 中的解决方法 1[=81= 类似],但随后渲染被破坏。你可以试试,也许这取决于我的安装。

编辑: 我找到了一个不太优雅且问题更多的解决方案,但正如@will 指出的那样,您 can 仅在通过使用 rgba 语法指定颜色来桥接区域。我将把我的版本留作评论历史,因为答案已经足够长了...:P

(增加点数可以获得更柔和的边缘)

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

fig = plt.figure()
ax = fig.gca(projection='3d')
X = np.arange(-5, 5, 0.25)
Y = np.arange(-5, 5, 0.25)
X, Y = np.meshgrid(X, Y)

# Complex shape from examples in matplotlib gallery
Z1 = np.sin(np.sqrt(X**2+Y**2))
Z2 = np.ones_like(Z1)*0.6

# Define the color for each one of our surfaces
# (it doesn't need to be a gradient)
color1 = np.empty_like(X, dtype=str)
color1.fill('b')
color2 = np.empty_like(X, dtype=str)
color2.fill('r')

# Create a white bridge region
X_bridge = np.vstack([X[-1,:],X[0,:]])
Y_bridge = np.vstack([Y[-1,:],Y[0,:]])
Z_bridge = np.vstack([Z1[-1,:],Z2[0,:]])
color_bridge = np.empty_like(Z_bridge, dtype=object)
color_bridge.fill((1,1,1,0))

# Join the two surfaces (using also the bridge)
X_full = np.vstack([X, X_bridge, X])
Y_full = np.vstack([Y, Y_bridge, Y])
Z_full = np.vstack([Z1, Z_bridge, Z2])
color_full = np.vstack([color1, color_bridge, color2])

surf_full = ax.plot_surface(X_full, Y_full, Z_full, rstride=1, cstride=1,
                                    facecolors=color_full, linewidth=0,
                                                                antialiased=False)

ax.set_zlim3d(-1, 2)
ax.set_ylim(-5,5)
ax.set_xlim(-5,5)

plt.show()

我本来想考虑一些 肮脏的 hacks,比如 mgab 在他们的回答中提到的,但后来决定走一条相当简单的路线:

单纯使用透明度也可以得到类似的效果,只需要确保透明度足够低,否则仍然会出现明显的重叠现象:

from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
from matplotlib.ticker import LinearLocator, FormatStrFormatter
import matplotlib.pyplot as plt
import numpy as np
from scipy.special import erf

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

X = np.arange(0, 6, 0.25)
Y = np.arange(0, 6, 0.25)
X, Y = np.meshgrid(X, Y)

Z1 = np.zeros_like(X)
Z2 = np.ones_like(X)

for i in range(len(X)):
  for j in range(len(X[0])):
    Z1[i,j] = 0.5*(erf((X[i,j]+Y[i,j]-4.5)*0.5)+1)
    Z2[i,j] = 0.5*(erf((-X[i,j]-Y[i,j]+4.5)*0.5)+1)


alpha = 0.25

surf1 = ax.plot_surface(X, Y, Z1, cstride=2, rstride=1, cmap=cm.Oranges, linewidth=0, antialiased=False, alpha=alpha)

surf2 = ax.plot_surface(X, Y, Z2, cstride=2, rstride=1, cmap=cm.Blues, linewidth=0, antialiased=False, alpha=alpha)

ax.zaxis.set_major_locator(LinearLocator(10))
ax.zaxis.set_major_formatter(FormatStrFormatter('%.02f'))

fig.colorbar(surf1, shrink=0.5, aspect=5)
fig.colorbar(surf2, shrink=0.5, aspect=5)

plt.show()

添加相交线会是一个很好的添加,但我目前没有简单的方法来添加它。

编辑:从 mgab 的答案中大量窃取,使用他的 "bridge" 解决方案,然后还对表面使用颜色贴图,并使用 RGBA 元组将桥面设置为透明,你几乎可以得到你想要的:

from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
import numpy as np
from scipy.special import erf

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

X = np.arange(0, 6, 0.25)
Y = np.arange(0, 6, 0.25)
X, Y = np.meshgrid(X, Y)

Z1 = np.empty_like(X)
Z2 = np.empty_like(X)
C1 = np.empty_like(X, dtype=object)
C2 = np.empty_like(X, dtype=object)

for i in range(len(X)):
  for j in range(len(X[0])):
    z1 = 0.5*(erf((X[i,j]+Y[i,j]-4.5)*0.5)+1)
    z2 = 0.5*(erf((-X[i,j]-Y[i,j]+4.5)*0.5)+1)
    Z1[i,j] = z1
    Z2[i,j] = z2

    # If you want to grab a colour from a matplotlib cmap function, 
    # you need to give it a number between 0 and 1. z1 and z2 are 
    # already in this range, so it just works.
    C1[i,j] = plt.get_cmap("Oranges")(z1)
    C2[i,j] = plt.get_cmap("Blues")(z2)


# Create a transparent bridge region
X_bridge = np.vstack([X[-1,:],X[-1,:]])
Y_bridge = np.vstack([Y[-1,:],Y[-1,:]])
Z_bridge = np.vstack([Z1[-1,:],Z2[-1,:]])
color_bridge = np.empty_like(Z_bridge, dtype=object)

color_bridge.fill((1,1,1,0)) # RGBA colour, onlt the last component matters.

# Join the two surfaces flipping one of them (using also the bridge)
X_full = np.vstack([X, X_bridge, np.flipud(X)])
Y_full = np.vstack([Y, Y_bridge, np.flipud(Y)])
Z_full = np.vstack([Z1, Z_bridge, np.flipud(Z2)])
color_full = np.vstack([C1, color_bridge, np.flipud(C2)])

surf_full = ax.plot_surface(X_full, Y_full, Z_full, rstride=1, cstride=1,
                            facecolors=color_full, linewidth=0,
                            antialiased=False)


plt.show()

对相交表面进行颜色映射

首先感谢@will 和@mgab 解决问题。我用你的技术为我正在制定的商业计划增添了趣味(见图表)。我只是打听“alpha”问题。

是的,您可以在表面上使用不同的不透明度,方法是使用 RGBA 语法中的第四个属性。您还可以使用顺序颜色图,方法是向其传递一个 min-max 缩放 Z 值。

for i in range(len(X)):
    for j in range(len(X[0])):
        C1[i,j] = plt.get_cmap('RdYlGn')((Z1[i,j]-Z_min)/Z_range) 
        C2[i,j] = (0,0,1,0.5)

P.S。那个收入面不是平面。它会重新计算两个参数的每个组合的损益。

据我了解,ax.plplot_surface 方法只能为一个表面绘制好的图形,因此如果您需要绘制多个表面,则需要将它们组合成一个公共的 np.array。

我准备了一些代码,希望对您有所帮助:

    # normalize values to range [0;1] for getting color from cmap
    def norm_v(v) :
        v_min = v.min()
        v_max = v.max()
        if v_min-v_max == 0 :
            v.fill(0.5)
            return v
        return (v-v_min)/(v_max-v_min)

    # combine several surfaces in one for plotting at once
    def combine_in_one_graph(X,Y,*Z) :
        cmaps_name = ['viridis', 'plasma', 'inferno', 'magma', 'cividis'] 

        # transparent connection between grahps
        transparen_link = np.empty_like(X[0], dtype=object)
        transparen_link.fill((1,1,0,0))

        # include first graph
        combined_X = X
        combined_Y = Y
        combined_Z = Z[0]

        # prepare collor matrix for first graph (Z[0])
        combined_Color = np.empty_like(X, dtype=object)
        normed_Z = norm_v(Z[0])
        for i in range(len(combined_Color)) :
            for j in range(len(X[0])) :
                combined_Color[i,j] = plt.get_cmap(cmaps_name[0])(normed_Z[i,j])

        # first row of collor matrix is not used in ploting, and will displace transparent links
        # so we need to remove first row
        combined_Color = combined_Color[1:]

        # second aray combined with first in backward direction, so connection would on one side of graphs, not intersect them 
        direction = -1 
        cmap_index = 1
        for next_Z in Z[1:] :

            combined_X = np.vstack([combined_X, X[::direction][0], X[::direction]])
            combined_Y = np.vstack([combined_Y, Y[::direction][0], Y[::direction]])
            combined_Z = np.vstack([combined_Z, next_Z[::direction][0], next_Z[::direction]])

            # prepare collors for next Z_ 
            next_C = np.empty_like(X, dtype=object)
            normed_Z = norm_v(next_Z)

            for i in range(len(X)) :
                for j in range(len(X[0])) :
                    next_C[i,j] = plt.get_cmap(cmaps_name[cmap_index])(normed_Z[i,j])

            combined_Color = np.vstack([combined_Color ,transparen_link ,next_C[::direction]])

            direction *= -1
            cmap_index += 1

        fig = plt.figure(figsize=(15,15))
        ax = fig.gca(projection='3d') # get current axis
        surf = ax.plot_surface(combined_X, combined_Y, combined_Z, facecolors=combined_Color, rstride=1, cstride=1,
                                 linewidth=0,
                                antialiased=False )

        # rotate graph on angle in degrees
        ax.view_init(azim=-60)

        ax.set_xlabel('X')
        ax.set_ylabel('Y')
        ax.set_zlabel('Z')
        plt.show()


    X = np.arange(0.2, 1.06, 0.01)
    Y = np.arange(0.2, 1.06, 0.01)
    X, Y = np.meshgrid(X, Y)


    Z1 = 2*np.sin(np.sqrt(20*X**2+20*Y**2))
    Z2 = 2*np.cos(np.sqrt(20*X**2+20*Y**2))
    Z3 = X*0+1
    Z4 = Y*0+1.5

    combine_in_one_graph(X,Y,Z1,Z2,Z3,Z4)

some tests