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

87

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

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


источник

Ответы:

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 autoscale z axis:

плохой

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

введите описание изображения здесь

Реми Ф
источник
В этом случае вам даже не нужно equalутверждение - оно всегда будет равно.
1
Это отлично работает, если вы строите только один набор данных, но как насчет того, чтобы на одном трехмерном графике было больше наборов данных? Речь идет о двух наборах данных, поэтому их просто объединить, но это может быстро стать неразумным при построении нескольких разных наборов данных.
Стивен С. Хауэлл,
@ stvn66, с помощью этого решения я рисовал до пяти наборов данных на одном графике, и у меня все получилось.
1
Это прекрасно работает. Для тех, кто хочет это в форме функции, которая принимает объект оси и выполняет указанные выше операции, я рекомендую им проверить ответ @karlo ниже. Это немного более чистое решение.
spurra 04
@ 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()
Карло
источник
Имейте в виду, что использование средств в качестве центральной точки не будет работать во всех случаях, вам следует использовать средние точки. См. Мой комментарий к ответу Таурана.
Rainman Noodles
1
Мой код выше не использует среднее значение данных, он принимает среднее значение существующих пределов графика. Таким образом, моя функция гарантированно учитывает любые точки, которые были в поле зрения в соответствии с ограничениями сюжета, установленными до ее вызова. Если пользователь уже установил пределы графика слишком строго, чтобы увидеть все точки данных, это отдельная проблема. Моя функция обеспечивает большую гибкость, потому что вы можете просматривать только часть данных. Все, что я делаю, это расширяю пределы оси, чтобы соотношение сторон составляло 1: 1: 1.
karlo
Другими словами: если вы возьмете среднее значение только из 2 точек, а именно границ на одной оси, то это будет означать, что ЯВЛЯЕТСЯ средней точкой. Итак, насколько я могу судить, приведенная ниже функция Далума должна быть математически эквивалентна моей, и нечего было `` исправить ''.
karlo
11
Значительно превосходит принятое в настоящее время решение, которое создает беспорядок, когда у вас появляется множество объектов разной природы.
P-Gn
1
Мне очень нравится решение, но после того, как я обновил anaconda, ax.set_aspect ("equal") сообщил об ошибке: NotImplementedError: в настоящее время невозможно вручную установить аспект на трехмерных осях
Эван
51

Я упростил решение Реми Ф, используя 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()

введите описание изображения здесь

тауран
источник
1
Мне нравится упрощенный код. Просто имейте в виду, что некоторые (очень немногие) точки данных могут не отображаться. Например, предположим, что X = [0, 0, 0, 100], так что X.mean () = 25. Если max_range оказывается равным 100 (от X), тогда ваш 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
После того, как я обновил анаконду, ax.set_aspect ("equal") сообщил об ошибке: NotImplementedError: в настоящее время невозможно вручную установить аспект на трехмерных осях
Эван
Вместо того, чтобы звонить 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 #17172и issue #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')
Матин Улхак
источник
Мне нравится это, но после того, как я обновил анаконду, ax.set_aspect ("equal") сообщил об ошибке: NotImplementedError: в настоящее время невозможно вручную установить аспект на трехмерных осях
Эван
@Ewan Я добавил несколько ссылок внизу своего ответа, чтобы помочь в расследовании. Похоже, что люди из MPL по какой-то причине ломают обходные пути, не исправляя проблему должным образом. ¯ \\ _ (ツ) _ / ¯
Матин Улхак
Я думаю, что нашел обходной путь (не требующий изменения исходного кода) для NotImplementedError (полное описание в моем ответе ниже); в основном добавляйте ax.set_box_aspect([1,1,1])перед set_axes_equal
звонком
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()
AndrewCox
источник
Да, наконец! Спасибо - если бы я только мог проголосовать за вас :)
Н. Йонас Фигге
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')или значения тиков могут быть напортачены. В остальном хорошее решение. Спасибо,
Тони Пауэр