【问题标题】:matplotlib (equal unit length): with 'equal' aspect ratio z-axis is not equal to x- and y-matplotlib (equal unit length): with 'equal' aspect ratio z-axis is not equal to x- and y-
【发布时间】:2012-11-21 01:04:36
【问题描述】:

当我为 3d 图形设置相等的纵横比时,z-axis 不会更改为“相等”。所以这个:

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-单位。

如何使所有三个轴的单位长度相等?我找到的所有解决方案都不起作用。

【问题讨论】:

    标签: python matplotlib graph axis aspect-ratio


    【解决方案1】:

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

    但是如果你添加边界框,你会得到一个正确的缩放:

    【讨论】:

    • 在这种情况下,您甚至不需要 equal 语句 - 它总是相等的。
    • 如果您只绘制一组数据,这可以正常工作,但是当有更多数据集都在同一个 3d 图上时呢?有问题的是,有 2 个数据集,因此将它们组合起来很简单,但如果绘制多个不同的数据集,这可能会很快变得不合理。
    • @stvn66,我用这个解决方案在一张图中最多绘制了五个数据集,它对我来说很好。
    • 这非常有效。对于那些想要函数形式的人,它接受一个轴对象并执行上述操作,我鼓励他们查看下面的@karlo 答案。这是一种稍微清洁的解决方案。
    • 我更新anaconda后,ax.set_aspect("equal")报错:NotImplementedError:目前无法手动设置3D轴的aspect
    【解决方案2】:

    我使用 set_x/y/zlim functions 简化了 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()
    

    【讨论】:

    • 我喜欢简化的代码。请注意,某些(极少数)数据点可能不会被绘制出来。例如,假设 X=[0, 0, 0, 100] 使得 X.mean()=25。如果 max_range 为 100(来自 X),那么您的 x 范围将为 25 +- 50,因此 [-25, 75] 并且您将错过 X[3] 数据点。这个想法非常好,而且很容易修改以确保你得到所有的分数。
    • 注意以手段为中心是不正确的。您应该使用 midpoint_x = np.mean([X.max(),X.min()]) 之类的东西,然后将限制设置为 midpoint_x +/- max_range。仅当均值位于数据集的中点时才使用均值,这并不总是正确的。另外,提示:如果边界附近或边界上有点,您可以缩放 max_range 以使图形看起来更好。
    • 我更新anaconda后,ax.set_aspect("equal")报错:NotImplementedError:目前无法手动设置3D轴的aspect
    • 与其调用set_aspect('equal'),不如使用set_box_aspect([1,1,1]),如下面的回答所述。它在 matplotlib 3.3.1 版中为我工作!
    【解决方案3】:

    我喜欢上述解决方案,但它们确实有一个缺点,即您需要跟踪所有数据的范围和均值。如果您有多个要一起绘制的数据集,这可能会很麻烦。为了解决这个问题,我使用了 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.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)
    
    set_axes_equal(ax)
    plt.show()
    

    【讨论】:

    • 请注意,使用均值作为中心点并非在所有情况下都有效,您应该使用中点。请参阅我对 tauran 回答的评论。
    • 我上面的代码没有取数据的平均值,而是取现有绘图限制的平均值。因此,我的函数可以保证根据调用之前设置的绘图限制来查看任何可见的点。如果用户已经将绘图限制设置得太严格而无法查看所有数据点,这是一个单独的问题。我的函数允许更大的灵活性,因为您可能只想查看数据的子集。我所做的只是扩展轴限制,因此纵横比为 1:1:1。
    • 另一种说法:如果你只取2个点的平均值,即单轴上的边界,那么这个平均值就是中点。所以,据我所知,下面 Dalum 的函数在数学上应该与我的函数等价,并且没有什么需要“修复”的。
    • 大大优于当前接受的解决方案,当您开始拥有大量不同性质的对象时会变得一团糟。
    • 我很喜欢这个解决方案,但是在我更新了anaconda之后,ax.set_aspect("equal") 报错:NotImplementedError: It is not current possible to manually set the aspect on 3D axes
    【解决方案4】:

    编辑: 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') 否则刻度值可能会被搞砸。否则很好的解决方案。谢谢,
    【解决方案5】:

    改编自@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 中合并的更改(在issue #17172issue #1077 中跟踪),此答案不适用于较新版本的 Matplotlib。作为一种临时解决方法,可以删除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 我在答案底部添加了一些链接以帮助调查。看起来 MPL 的人似乎因为某种原因在没有正确解决问题的情况下打破了变通方法。 ¯\\_(ツ)_/¯
    • 我想我找到了 NotImplementedError 的解决方法(不需要修改源代码)(下面我的答案中的完整描述);基本上在调用set_axes_equal之前添加ax.set_box_aspect([1,1,1])
    • 刚找到这篇文章并尝试过,在 ax.set_aspect('equal') 上失败了。如果您只是从脚本中删除 ax.set_aspect('equal') 但保留两个自定义函数 set_axes_equal 和 _set_axes_radius...确保在 plt.show() 之前调用它们,这不是问题。对我来说很好的解决方案!几年来,我一直在寻找一段时间,终于。我总是使用 python 的 vtk 模块进行 3D 绘图,尤其是当事情的数量变得极端时。
    【解决方案6】:

    简单修复!

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

    看起来这个问题在PR#17172 中可能已经解决了;您可以使用ax.set_box_aspect([1,1,1]) 函数来确保方面正确(请参阅set_aspect 函数的注释)。当与@karlo 和/或@Matee Ulhaq 提供的边界框功能结合使用时,这些图现在在 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.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()
    

    【讨论】:

    • 是的,终于!谢谢-如果我只能将您投票给顶部:)
    • ax.set_box_aspect([np.ptp(i) for i in data]) # 等宽比
    【解决方案7】:

    从 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))
    

    【讨论】:

    • 2021 年的方式。像魅力一样工作。
    【解决方案8】:

    我认为自从这些答案发布以来,这个功能已经被添加到 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将图形缩放了两倍。您可以将此比例因子设置为您想要的任何值。

    注意:这只适用于有一个情节的人物。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-05-14
      • 2011-05-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-01-09
      • 1970-01-01
      相关资源
      最近更新 更多