如何为所有轴(x、y、z)设置“相等”纵横比

114
当我为3D图形设置等比例时,z轴不会改变为“相等”。所以这样:
fig = pylab.figure()
mesFig = fig.gca(projection='3d', adjustable='box')
mesFig.axis('equal')
mesFig.plot(xC, yC, zC, 'r.')
mesFig.plot(xO, yO, zO, 'b.')
pyplot.show()

给我以下内容:

img1

很明显,Z轴的单位长度不等于X和Y轴的单位长度。

我该如何使所有三个轴的单位长度相等?我找到的所有解决方案都无法解决这个问题。


2
从matplotlib 3.3.0开始,建议使用set_box_aspect()。请参见以下更新的答案。 - bert
仅通过一行(功能性)编辑,我修复了top answer,让它使用set_box_aspect函数可以在matplotlib 3.3.0及以后版本中工作。 - karlo
11个回答

86

我喜欢之前发布的一些解决方案,但它们有一个缺点,那就是你需要跟踪所有数据的范围和均值。如果你有多个数据集要一起绘制,这可能会很麻烦。为了解决这个问题,我使用了 ax.get_[xyz]lim3d() 方法,并将整个过程放入一个独立的函数中,在调用 plt.show() 之前只需调用一次。以下是新版本:

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

def set_axes_equal(ax):
    """
    Make axes of 3D plot have equal scale so that spheres appear as spheres,
    cubes as cubes, etc.

    Input
      ax: a matplotlib axis, e.g., as output from plt.gca().
    """

    x_limits = ax.get_xlim3d()
    y_limits = ax.get_ylim3d()
    z_limits = ax.get_zlim3d()

    x_range = abs(x_limits[1] - x_limits[0])
    x_middle = np.mean(x_limits)
    y_range = abs(y_limits[1] - y_limits[0])
    y_middle = np.mean(y_limits)
    z_range = abs(z_limits[1] - z_limits[0])
    z_middle = np.mean(z_limits)

    # The plot bounding box is a sphere in the sense of the infinity
    # norm, hence I call half the max range the plot radius.
    plot_radius = 0.5*max([x_range, y_range, z_range])

    ax.set_xlim3d([x_middle - plot_radius, x_middle + plot_radius])
    ax.set_ylim3d([y_middle - plot_radius, y_middle + plot_radius])
    ax.set_zlim3d([z_middle - plot_radius, z_middle + plot_radius])

fig = plt.figure()
ax = fig.add_subplot(projection="3d")

# Use this for matplotlib prior to 3.3.0 only.
#ax.set_aspect("equal'")
#
# Use this for matplotlib 3.3.0 and later.
# https://github.com/matplotlib/matplotlib/pull/17515
ax.set_box_aspect([1.0, 1.0, 1.0])

X = np.random.rand(100)*10+5
Y = np.random.rand(100)*5+2.5
Z = np.random.rand(100)*50+25

scat = ax.scatter(X, Y, Z)

set_axes_equal(ax)
plt.show()

1
我的代码不会计算数据的平均值,而是计算现有图表限制的平均值。因此,我的函数保证可以查看在它被调用之前设置的绘图限制中可见的任何点。如果用户已经将绘图限制设置得太严格,无法查看所有数据点,则这是一个单独的问题。我的函数允许更多的灵活性,因为您可能只想查看数据的子集。我所做的就是扩展轴限制,使纵横比为1:1:1。 - karlo
换句话说:如果您仅对单个轴上的边界取2个点的平均值,则该平均值就是中点。因此,据我所知,Dalum在下面的函数应该在数学上等同于我的函数,并且没有任何需要“修复”的问题。 - karlo
14
目前被广泛接受的解决方案在涉及不同类型对象众多时会变得混乱,相比之下这个方案要优越得多。 - P-Gn
9
我喜欢这个解决方案,但是在我更新了Anaconda之后,ax.set_aspect("equal")报错: NotImplementedError:目前无法在3D坐标轴上手动设置宽高比。 - Ewan
谢谢@Ewan。我刚刚更新了当前版本的matplotlib的解决方案。 - karlo
显示剩余2条评论

80

我认为Matplotlib在3D图形中不能正确地设置等轴线...但是我有一次偶然发现了一个技巧(我不记得在哪里看到的),我对此进行了改进。这个技巧的概念是在数据周围创建一个虚假的立方体边界框。

你可以使用以下代码进行测试:

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

fig = plt.figure()
ax = fig.add_subplot(projection='3d')
ax.set_aspect('equal')

X = np.random.rand(100)*10+5
Y = np.random.rand(100)*5+2.5
Z = np.random.rand(100)*50+25

scat = ax.scatter(X, Y, Z)

# Create cubic bounding box to simulate equal aspect ratio
max_range = np.array([X.max()-X.min(), Y.max()-Y.min(), Z.max()-Z.min()]).max()
Xb = 0.5*max_range*np.mgrid[-1:2:2,-1:2:2,-1:2:2][0].flatten() + 0.5*(X.max()+X.min())
Yb = 0.5*max_range*np.mgrid[-1:2:2,-1:2:2,-1:2:2][1].flatten() + 0.5*(Y.max()+Y.min())
Zb = 0.5*max_range*np.mgrid[-1:2:2,-1:2:2,-1:2:2][2].flatten() + 0.5*(Z.max()+Z.min())
# Comment or uncomment following both lines to test the fake bounding box:
for xb, yb, zb in zip(Xb, Yb, Zb):
   ax.plot([xb], [yb], [zb], 'w')

plt.grid()
plt.show()

z数据的数量级大约比x和y大一个数量级,但即使使用相等的轴选项,matplotlib也会自动缩放z轴:

bad

但是如果添加边界框,就可以获得正确的缩放:

enter image description here


在这种情况下,您甚至不需要equal语句 - 它将始终相等。 - user1329187
1
如果您只绘制一个数据集,那么这个方法很好用。但是如果有多个数据集都在同一个3D图中呢?在这个问题中,有两个数据集,所以将它们组合起来很简单,但是如果要绘制几个不同的数据集,这种方法可能会变得不切实际。 - Steven C. Howell
@stvn66,我用这个解决方案在一个图表中绘制了最多五个数据集,效果很好。 - user1329187
2
这个完美地运作。对于那些想要将其转换为函数形式的人,可以查看下面@karlo的答案。它是一个稍微更简洁的解决方案。 - spurra
@user1329187 -- 我发现如果没有 equal 语句,这对我不起作用。 - supergra
6
在我更新了Anaconda之后,ax.set_aspect("equal")报错了: NotImplementedError: 目前无法在3D坐标轴上手动设置纵横比。 - Ewan

68

简单解决方案!

我已经在3.3.1版本中成功实现了这个功能。

看起来这个问题在PR#17172中可能已经得到解决;您可以使用ax.set_box_aspect([1,1,1])函数来确保比例正确(请参阅set_aspect函数的注释)。当与@karlo和/或@Matee Ulhaq提供的边界框函数一起使用时,现在3D图形看起来是正确的!

matplotlib 3d plot with equal axes

最小工作示例

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

# Functions from @Mateen Ulhaq and @karlo
def set_axes_equal(ax: plt.Axes):
    """Set 3D plot axes to equal scale.

    Make axes of 3D plot have equal scale so that spheres appear as
    spheres and cubes as cubes.  Required since `ax.axis('equal')`
    and `ax.set_aspect('equal')` don't work on 3D.
    """
    limits = np.array([
        ax.get_xlim3d(),
        ax.get_ylim3d(),
        ax.get_zlim3d(),
    ])
    origin = np.mean(limits, axis=1)
    radius = 0.5 * np.max(np.abs(limits[:, 1] - limits[:, 0]))
    _set_axes_radius(ax, origin, radius)

def _set_axes_radius(ax, origin, radius):
    x, y, z = origin
    ax.set_xlim3d([x - radius, x + radius])
    ax.set_ylim3d([y - radius, y + radius])
    ax.set_zlim3d([z - radius, z + radius])

# Generate and plot a unit sphere
u = np.linspace(0, 2*np.pi, 100)
v = np.linspace(0, np.pi, 100)
x = np.outer(np.cos(u), np.sin(v)) # np.outer() -> outer vector product
y = np.outer(np.sin(u), np.sin(v))
z = np.outer(np.ones(np.size(u)), np.cos(v))

fig = plt.figure()
ax = fig.add_subplot(projection='3d')
ax.plot_surface(x, y, z)

ax.set_box_aspect([1,1,1]) # IMPORTANT - this is the new, key line
# ax.set_proj_type('ortho') # OPTIONAL - default is perspective (shown in image above)
set_axes_equal(ax) # IMPORTANT - this is also required
plt.show()

4
太好了,终于!谢谢 - 如果我能将您投票到最顶端就好了 :) - N. Jonas Figge
1
ax.set_box_aspect([np.ptp(i) for i in data]) # 等比例箱形图 - msch
AttributeError: 'Axes3DSubplot' object has no attribute 'set_box_aspect' as of matplotlib==3.2.2, but works on later versions including at least matplotlib==3.6.3 - skainswo

57

我通过使用 set_x/y/zlim 函数 简化了 Remy F 的解决方案。

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

fig = plt.figure()
ax = fig.add_subplot(projection='3d')
ax.set_aspect('equal')

X = np.random.rand(100)*10+5
Y = np.random.rand(100)*5+2.5
Z = np.random.rand(100)*50+25

scat = ax.scatter(X, Y, Z)

max_range = np.array([X.max()-X.min(), Y.max()-Y.min(), Z.max()-Z.min()]).max() / 2.0

mid_x = (X.max()+X.min()) * 0.5
mid_y = (Y.max()+Y.min()) * 0.5
mid_z = (Z.max()+Z.min()) * 0.5
ax.set_xlim(mid_x - max_range, mid_x + max_range)
ax.set_ylim(mid_y - max_range, mid_y + max_range)
ax.set_zlim(mid_z - max_range, mid_z + max_range)

plt.show()

输入图像描述


1
我喜欢简化的代码。只需注意有些(非常少量)数据点可能不会被绘制。例如,假设X=[0, 0, 0, 100],因此X.mean()=25。如果max_range从X中得出为100,则您的x范围将是25+-50,即[-25, 75],您将错过X[3]数据点。不过这个想法很好,很容易修改以确保获取所有点。 - TravisJ
1
请注意,以平均值作为中心是不正确的。您应该使用类似 midpoint_x = np.mean([X.max(),X.min()]) 的方法,然后将限制设置为 midpoint_x +/- max_range。仅使用平均值仅在平均值位于数据集的中点时才有效,这并不总是正确的。另外,一个提示:如果边界附近有点,则可以缩放 max_range 以使图形看起来更好。 - Rainman Noodles
在我更新了anaconda之后,ax.set_aspect("equal")报告了错误: NotImplementedError:目前无法在3D轴上手动设置方面。 - Ewan
2
不要调用 set_aspect('equal'),而是使用我在下面回答中描述的 set_box_aspect([1,1,1])。在 matplotlib 版本 3.3.1 中对我有效! - AndrewCox

27

截至matplotlib 3.3.0版本,Axes3D.set_box_aspect 似乎是推荐的方法。

import numpy as np

xs, ys, zs = <your data>
ax = <your axes>

# Option 1: aspect ratio is 1:1:1 in data space
ax.set_box_aspect((np.ptp(xs), np.ptp(ys), np.ptp(zs)))

# Option 2: aspect ratio 1:1:1 in view space
ax.set_box_aspect((1, 1, 1))

7
2021 的方法。 功能十分可靠。 - Jan Joneš
2
这是使用3.5.x的正确方法。 - JustLearning
对我不起作用,3.4.0 :( - Shai

23

以下内容参考@karlo的回答,为了让事情更加简洁:

def set_axes_equal(ax: plt.Axes):
    """Set 3D plot axes to equal scale.

    Make axes of 3D plot have equal scale so that spheres appear as
    spheres and cubes as cubes.  Required since `ax.axis('equal')`
    and `ax.set_aspect('equal')` don't work on 3D.
    """
    limits = np.array([
        ax.get_xlim3d(),
        ax.get_ylim3d(),
        ax.get_zlim3d(),
    ])
    origin = np.mean(limits, axis=1)
    radius = 0.5 * np.max(np.abs(limits[:, 1] - limits[:, 0]))
    _set_axes_radius(ax, origin, radius)

def _set_axes_radius(ax, origin, radius):
    x, y, z = origin
    ax.set_xlim3d([x - radius, x + radius])
    ax.set_ylim3d([y - radius, y + radius])
    ax.set_zlim3d([z - radius, z + radius])

用法:

fig = plt.figure()
ax = fig.add_subplot(projection='3d')
ax.set_aspect('equal')         # important!

# ...draw here...

set_axes_equal(ax)             # important!
plt.show()

编辑:由于在pull-request #13474中合并的更改,本回答不适用于更新版本的Matplotlib,该更改已在issue #17172issue #1077中跟踪。作为一种临时解决方法,可以删除lib/matplotlib/axes/_base.py中新添加的行:

  class _AxesBase(martist.Artist):
      ...

      def set_aspect(self, aspect, adjustable=None, anchor=None, share=False):
          ...

+         if (not cbook._str_equal(aspect, 'auto')) and self.name == '3d':
+             raise NotImplementedError(
+                 'It is not currently possible to manually set the aspect '
+                 'on 3D axes')

1
喜欢这个,但在更新anaconda之后,ax.set_aspect("equal")报错了: NotImplementedError:目前无法在3D轴上手动设置宽高比 - Ewan
@Ewan 我在我的回答底部添加了一些链接,以帮助调查。看起来MPL的人们正在打破解决方法,而没有适当地修复问题,原因不明。¯\_(ツ)_/¯ - Mateen Ulhaq
1
我认为我找到了一个解决方法(不需要修改源代码)来解决NotImplementedError(详细描述请看我的下面的答案); 基本上在调用“set_axes_equal”之前添加“ax.set_box_aspect([1,1,1])”。 - AndrewCox
刚看到这篇文章并尝试了一下,但在ax.set_aspect('equal')上失败了。不过如果你从脚本中删除ax.set_aspect('equal'),但保留两个自定义函数set_axes_equal和_set_axes_radius...确保在plt.show()之前调用它们,那也没问题。对我来说是个很好的解决方案!我已经搜索了一段时间,几年了,终于找到了。当事情变得极端时,我总是回到Python的vtk模块进行3D绘图。 - Tony A

7

编辑:user2525140的代码应该完全正常工作,尽管这个答案试图修复一个不存在的错误。下面的答案只是一个重复(替代)实现:

def set_aspect_equal_3d(ax):
    """Fix equal aspect bug for 3D plots."""

    xlim = ax.get_xlim3d()
    ylim = ax.get_ylim3d()
    zlim = ax.get_zlim3d()

    from numpy import mean
    xmean = mean(xlim)
    ymean = mean(ylim)
    zmean = mean(zlim)

    plot_radius = max([abs(lim - mean_)
                       for lims, mean_ in ((xlim, xmean),
                                           (ylim, ymean),
                                           (zlim, zmean))
                       for lim in lims])

    ax.set_xlim3d([xmean - plot_radius, xmean + plot_radius])
    ax.set_ylim3d([ymean - plot_radius, ymean + plot_radius])
    ax.set_zlim3d([zmean - plot_radius, zmean + plot_radius])

你仍然需要执行:ax.set_aspect('equal'),否则刻度值可能会出错。除此之外是个好的解决方案。谢谢。 - Tony Power

7

从matplotlib 3.6.0开始,这个功能已经添加了命令ax.set_aspect('equal')。其他选项是'equalxy''equalxz''equalyz',用于设置两个方向的等比例尺寸。这会改变数据限制,下面是一个示例。

在即将发布的3.7.0版本中,您将能够通过命令ax.set_aspect('equal', adjustable='box')更改绘图框的纵横比,而不是数据限制。要获得原始行为,请使用adjustable='datalim'

enter image description here


3.7.0版本已经发布,因此adjustable='datalim'adjustable='box'现在都是有效的选项。 - Scott

1

我认为自从这些答案发布以来,matplotlib已经添加了此功能。如果有人仍在寻找解决方案,以下是我的做法:

import matplotlib.pyplot as plt 
import numpy as np
    
fig = plt.figure(figsize=plt.figaspect(1)*2)
ax = fig.add_subplot(projection='3d', proj_type='ortho')
    
X = np.random.rand(100)
Y = np.random.rand(100)
Z = np.random.rand(100)
    
ax.scatter(X, Y, Z, color='b')

关键代码是 figsize=plt.figaspect(1),它将图形的长宽比设置为1:1。在 figaspect(1) 后面加上 *2 将图形缩放了两倍。您可以将此缩放因子设置为任何值。

注意:这仅适用于具有一个绘图的图形。

Random 3D scatter Plot


0
  • 目前,ax.set_aspect('equal')在Anaconda的版本3.5.1中会引发错误。

  • ax.set_aspect('auto',adjustable='datalim')也没有提供令人信服的解决方案。

  • 使用ax.set_box_aspect((asx,asy,asz))asx, asy, asz = np.ptp(X), np.ptp(Y), np.ptp(Z)的简单解决方法似乎是可行的(请参见我的代码片段)。

  • 希望Scott提到的功能在版本3.7中很快能够成功实现。

    import numpy as np
    import matplotlib.pyplot as plt
    from mpl_toolkits.mplot3d import Axes3D
    
    #---- 生成数据
    nn = 100
    X = np.random.randn(nn)*20 +  0
    Y = np.random.randn(nn)*50 + 30
    Z = np.random.randn(nn)*10 + -5
    
    #---- 检查纵横比
    asx, asy, asz = np.ptp(X), np.ptp(Y), np.ptp(Z)
    
    fig = plt.figure(figsize=(15,15))
    ax = fig.add_subplot(projection='3d')
    
    #---- 设置盒子纵横比
    ax.set_box_aspect((asx,asy,asz))
    scat = ax.scatter(X, Y, Z, c=X+Y+Z, s=500, alpha=0.8)
    
    ax.set_xlabel('X轴'); ax.set_ylabel('Y轴'); ax.set_zlabel('Z轴')
    plt.show()
    

enter image description here


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接