matplotlib (равная единичная длина): при "равном" соотношении сторон ось z не равна x- и y-

Когда я устанавливаю равное соотношение сторон для трехмерного графика, ось 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-единицам.

Как я могу сделать единицу длины всех трех осей равной? Все решения, которые я мог найти, не работали. Спасибо.

10 ответов

Решение

Я полагаю, что 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 Auto Scale:

плохой

Но если вы добавите ограничивающий прямоугольник, вы получите правильное масштабирование:

Мне нравятся вышеупомянутые решения, но у них есть недостаток, заключающийся в том, что вам необходимо отслеживать диапазоны и средние значения по всем вашим данным. Это может быть громоздким, если у вас есть несколько наборов данных, которые будут отображаться вместе. Чтобы это исправить, я использовал методы 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()

Простое исправление!

Мне удалось заставить это работать в версии 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.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()

Я упростил решение Реми Ф. с помощью set_x/y/zlim функции

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

Что касается 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))

Адаптировано из ответа @karlo, чтобы сделать вещи еще чище:

def set_axes_radius(ax, origin, radius):
    ax.set_xlim3d([origin[0] - radius, origin[0] + radius])
    ax.set_ylim3d([origin[1] - radius, origin[1] + radius])
    ax.set_zlim3d([origin[2] - radius, origin[2] + radius])

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

    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)

Использование:

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

# ...draw here...

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

Начиная с matplotlib 3.6.0, эта функция была добавлена ​​с помощью командыax.set_aspect('equal'). Другие варианты'equalxy','equalxz', и'equalyz', чтобы установить только два направления с одинаковым соотношением сторон. Это изменяет ограничения данных, пример ниже.

В предстоящем 3.7.0 вы сможете изменить соотношение сторон графического окна, а не ограничения данных с помощью командыax.set_aspect('equal', adjustable='box'). Чтобы получить исходное поведение, используйтеadjustable='datalim'.

РЕДАКТИРОВАТЬ: код 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])

Я думаю, что эта функция была добавлена ​​в matplotlib с тех пор, как были опубликованы эти ответы. Если кто-то все еще ищет решение, вот как я это делаю:

      import matplotlib.pyplot as plt 
import numpy as np
    
fig = plt.figure(figsize=plt.figaspect(1)*2)
ax = plt.gca(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. *2 после figaspect(1)масштабирует фигуру в два раза. Вы можете установить этот коэффициент масштабирования по своему усмотрению.

ПРИМЕЧАНИЕ . Это работает только для фигур с одним графиком.

  • на данный моментax.set_aspect('equal')выдает ошибку (версия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)кажется возможным (см. мой фрагмент кода)

  • Будем надеяться, что версия3.7с функциями, упомянутыми @Scott, скоро будет успешным.

            import numpy as np
    import matplotlib.pyplot as plt
    from mpl_toolkits.mplot3d import Axes3D
    
    #---- generate data
    nn = 100
    X = np.random.randn(nn)*20 +  0
    Y = np.random.randn(nn)*50 + 30
    Z = np.random.randn(nn)*10 + -5
    
    #---- check aspect ratio
    asx, asy, asz = np.ptp(X), np.ptp(Y), np.ptp(Z)
    
    fig = plt.figure(figsize=(15,15))
    ax = fig.add_subplot(projection='3d')
    
    #---- set box aspect ratio
    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-axis'); ax.set_ylabel('Y-axis'); ax.set_zlabel('Z-axis')
    plt.show()
    

Другие вопросы по тегам