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, але навіть за умови рівної осі, автомасштабування осі z matplotlib:

погано

Але якщо ви додасте обмежувальне поле, ви отримаєте правильне масштабування:

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


У цьому випадку вам навіть не потрібне equalтвердження - воно завжди буде рівним.

1
Це чудово працює, якщо ви складаєте графік лише одного набору даних, але як бути, коли на одному 3D-графіці є більше наборів даних? Запитано, було 2 набори даних, тож поєднати їх просто, але це може стати нерозумним швидко, якщо побудувати кілька різних наборів даних.
Steven C. Howell

@ stvn66, я складав до п'яти наборів даних в одному графіку з цими рішеннями, і це добре працювало для мене.

1
Це працює чудово. Для тих, хто хоче це у формі функції, яка бере об’єкт осі і виконує вищеописані операції, я закликаю їх перевірити відповідь @karlo нижче. Це трохи чистіше рішення.
spurra

@ user1329187 - Я виявив, що це не працює для мене без equalзаяви.
супергра

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 ("дорівнює") повідомив про помилку: NotImplementedError: В даний час неможливо вручну встановити аспект на 3D-осях
Єван,

51

Я спростив рішення Remy F, використовуючи 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]. Хоча ідея дуже приємна і її легко змінити, щоб переконатися, що ви отримали всі бали.
ТревісJ

1
Пам'ятайте, що використання засобів як центру є неправильним. Ви повинні використовувати щось на зразок, midpoint_x = np.mean([X.max(),X.min()])а потім встановити межі midpoint_x+/- max_range. Використання середнього значення працює лише в тому випадку, якщо середнє значення знаходиться в середині точки набору даних, що не завжди відповідає дійсності. Також порада: ви можете масштабувати max_range, щоб графік виглядав приємніше, якщо є точки біля або на межі.
Rainman Noodles

Після того, як я оновив анаконду, ax.set_aspect ("дорівнює") повідомив про помилку: NotImplementedError: В даний час неможливо вручну встановити аспект на 3D-осях
Еван,

Замість того, щоб дзвонити 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 ("дорівнює") повідомив про помилку: NotImplementedError: В даний час неможливо вручну встановити аспект на 3D-осях
Ewan

@Ewan Я додав кілька посилань внизу моєї відповіді, щоб допомогти у розслідуванні. Схоже, люди MPL вирішують обхідні шляхи без належного вирішення проблеми з якихось причин. ¯ \\ _ (ツ) _ / ¯
Mateen Ulhaq

Думаю, я знайшов обхідний шлях (який не вимагає модифікації вихідного коду) для 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()

Так, нарешті! Дякую - якби я міг проголосувати вас за вершину :)
Н. Йонас Фігге

7

EDIT: код користувача2525140 повинен працювати абсолютно нормально, хоча ця відповідь нібито намагалася виправити неіснуючу помилку. Відповідь нижче - це лише дублікат (альтернативна) реалізація:

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')або значення галочки можуть бути зіпсовані. В іншому випадку хороше рішення. Дякую,
Тоні Пауер

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))
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.