matplotlib(相等单位长度):纵横比“相等”时,z轴不等于x-和y-


87

当我为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()

给我以下内容: 在此处输入图片说明

显然,z轴的单位长度不等于x和y单位。

如何使三个轴的单位长度相等?我能找到的所有解决方案都行不通。谢谢。

Answers:


69

我相信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.gca(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轴:

坏

但是,如果添加边界框,则会获得正确的缩放比例:

在此处输入图片说明


在这种情况下,您甚至不需要该equal语句-它将始终相等。

1
如果您仅绘制一组数据,那么效果很好,但是当同一3d绘图上都存在更多数据集时,该怎么办?有问题的是,有2个数据集,因此将它们组合起来很简单,但如果绘制多个不同的数据集,可能很快变得不合理。
史蒂文·霍维尔

@ stvn66,使用这种解决方案,我在一张图中最多绘制了五个数据集,对我来说效果很好。

1
这很完美。对于那些希望以函数形式(需要一个轴对象并执行上面的操作)的人,我鼓励他们查看下面的@karlo答案。这是一个稍微干净的解决方案。
spurra

@ user1329187-如果没有equal声明,我发现这对我不起作用。
supergra

58

我喜欢上面的解决方案,但是它们确实有一个缺点,那就是您需要跟踪所有数据的范围和均值。如果您有多个要绘制在一起的数据集,则可能会很麻烦。为了解决这个问题,我使用了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..  This is one possible solution to Matplotlib's
    ax.set_aspect('equal') and ax.axis('equal') not working for 3D.

    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.gca(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)

set_axes_equal(ax)
plt.show()

请注意,使用均值作为中心点并不能在所有情况下都有效,您应该使用中点。参见我对tauran答案的评论。
Rainman Noodles

1
我上面的代码没有取数据的平均值,而是取现有绘图限制的平均值。因此,可以确保我的函数可以根据调用之前设置的绘图限制保留所有可见的点。如果用户已经太严格地设置了绘图限制,以至于看不到所有数据点,那将是一个单独的问题。我的函数提供了更大的灵活性,因为您可能只想查看数据的一部分。我要做的只是扩展轴限制,因此宽高比为1:1:1。
karlo

换一种说法:如果您只取2个点的平均值,即单轴上的边界,则表示IS为中点。因此,据我所知,达卢姆(Dalum)的以下函数在数学上应与我的函数等效,并且没有什么可以``修复''的。
karlo 2016年

11
当您开始拥有许多不同性质的对象时,这比目前公认的解决方案要好得多。
P-Gn

1
我非常喜欢该解决方案,但是在更新anaconda之后,ax.set_aspect(“ equal”)报告了错误:NotImplementedError:当前无法在3D轴上手动设置长宽比
Ewan

51

我通过使用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.gca(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 2015年

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

而不是致电set_aspect('equal'),而是使用set_box_aspect([1,1,1]),如下面我的回答所述。在matplotlib版本3.3.1中为我工作!
AndrewCox

17

改编自@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.gca(projection='3d')
ax.set_aspect('equal')         # important!

# ...draw here...

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

编辑:这个答案不会因改变合并在Matplotlib的较新版本上工作pull-request #13474,这是在跟踪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')

喜欢这个,但是在更新anaconda之后,ax.set_aspect(“ equal”)报告了错误:NotImplementedError:当前无法在3D轴上手动设置长宽比
Ewan

@Ewan我在答案的底部添加了一些链接以帮助调查。看起来,MPL员工似乎出于某些原因在未正确解决问题的情况下突破了解决方法。¯\\ _(ツ)_ /¯
MATEEN Ulhaq

我认为我为NotImplementedError(以下答案中的完整说明)找到了一种解决方法(不需要修改源代码);基本上ax.set_box_aspect([1,1,1])在致电之前添加set_axes_equal
AndrewCox

11

简单修复!

我设法使它在3.3.1版中工作。

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

具有相等轴的matplotlib 3d图

最低工作实例

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.gca(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()

是的,终于!谢谢-如果我只能投票支持您到顶端:)
N. Jonas Figge

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

1

从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))
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.