matplotlib (równa długość jednostki): z „równym” współczynnikiem proporcji oś z nie jest równa x- i y-

89

Kiedy ustawiam równe proporcje dla wykresu 3d, oś Z nie zmienia się na „równa”. Więc to:

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

daje mi następujące informacje: wprowadź opis obrazu tutaj

gdzie oczywiście jednostkowa długość osi z nie jest równa jednostkom x i y.

Jak wyrównać długość jednostkową wszystkich trzech osi? Wszystkie znalezione przeze mnie rozwiązania nie działały. Dziękuję Ci.


źródło

Odpowiedzi:

71

Wydaje mi się, że matplotlib nie ustawia jeszcze poprawnie równej osi w 3D ... Ale jakiś czas temu znalazłem sztuczkę (nie pamiętam gdzie), którą zaadaptowałem. Koncepcja polega na utworzeniu fałszywej sześciennej ramki ograniczającej wokół danych. Możesz to przetestować za pomocą następującego kodu:

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

dane z są o rząd wielkości większe niż xiy, ale nawet z opcją równych osi, matplotlib autoscale oś z:

zły

Ale jeśli dodasz obwiednię, uzyskasz poprawne skalowanie:

wprowadź opis obrazu tutaj

Remy F.
źródło
W takim przypadku nie potrzebujesz nawet equaloświadczenia - zawsze będzie równe.
1
Działa to dobrze, jeśli kreślisz tylko jeden zestaw danych, ale co w przypadku, gdy na tym samym wykresie 3D znajduje się więcej zestawów danych? Chodzi o to, że były 2 zestawy danych, więc połączenie ich jest proste, ale może to szybko stać się nierozsądne, jeśli wykreślono kilka różnych zestawów danych.
Steven C. Howell
@ stvn66, kreśliłem do pięciu zestawów danych na jednym wykresie z tymi rozwiązaniami i działało dobrze dla mnie.
1
To działa doskonale. Dla tych, którzy chcą to w formie funkcji, która przyjmuje obiekt osi i wykonuje powyższe operacje, zachęcam do sprawdzenia odpowiedzi @karlo poniżej. Jest to nieco czystsze rozwiązanie.
spurra
@ user1329187 - stwierdziłem, że to nie zadziałało bez equaloświadczenia.
supergra
60

Podobają mi się powyższe rozwiązania, ale mają one tę wadę, że musisz śledzić zakresy i średnie dla wszystkich danych. Może to być kłopotliwe, jeśli masz wiele zestawów danych, które zostaną razem wykreślone. Aby to naprawić, użyłem metod ax.get_ [xyz] lim3d () i umieściłem całość w samodzielnej funkcji, którą można wywołać tylko raz przed wywołaniem plt.show (). Oto nowa wersja:

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()
karlo
źródło
Pamiętaj, że używanie środków jako punktu środkowego nie zadziała we wszystkich przypadkach, powinieneś używać punktów środkowych. Zobacz mój komentarz do odpowiedzi Taurana.
Rainman Noodles
1
Mój kod powyżej nie bierze średniej z danych, bierze średnią z istniejących granic wykresu. W ten sposób moja funkcja ma gwarancję, że będzie widzieć wszystkie punkty, które były widoczne zgodnie z granicami wykresu ustawionymi przed jej wywołaniem. Jeśli użytkownik ustawił już limity wykresu zbyt restrykcyjnie, aby zobaczyć wszystkie punkty danych, jest to osobny problem. Moja funkcja zapewnia większą elastyczność, ponieważ możesz chcieć wyświetlić tylko podzbiór danych. Wszystko, co robię, to rozszerzanie granic osi, aby współczynnik proporcji wynosił 1: 1: 1.
karlo
Ujmując to inaczej: jeśli weźmiemy średnią tylko z 2 punktów, a mianowicie granic na jednej osi, to ta średnia JEST punktem środkowym. Tak więc, o ile wiem, poniższa funkcja Daluma powinna być matematycznie równoważna mojej i nie było nic do `` naprawienia ''.
karlo,
12
Znacznie lepsze od obecnie przyjętego rozwiązania, czyli bałaganu, gdy zaczynasz mieć wiele obiektów o różnym charakterze.
P-Gn
1
Naprawdę podoba mi się to rozwiązanie, ale po zaktualizowaniu anakondy, ax.set_aspect ("equal") zgłosił błąd: NotImplementedError: Obecnie nie jest możliwe ręczne ustawienie aspektu na osiach 3D
Ewan
52

Uprościłem rozwiązanie Remy F za pomocą set_x/y/zlim funkcji .

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

wprowadź opis obrazu tutaj

tauran
źródło
1
Podoba mi się uproszczony kod. Należy tylko pamiętać, że niektóre (bardzo nieliczne) punkty danych mogą nie zostać wykreślone. Na przykład załóżmy, że X = [0, 0, 0, 100], tak że X.mean () = 25. Jeśli max_range wyniesie 100 (od X), wtedy zakres x będzie wynosił 25 + - 50, więc [-25, 75] i przegapisz punkt danych X [3]. Pomysł jest jednak bardzo fajny i łatwy do zmodyfikowania, aby mieć pewność, że zdobędziesz wszystkie punkty.
TravisJ,
1
Uważaj, używając środków jako środka nie jest poprawne. Powinieneś użyć czegoś podobnego, midpoint_x = np.mean([X.max(),X.min()])a następnie ustawić limity na midpoint_x+/- max_range. Użycie średniej działa tylko wtedy, gdy średnia znajduje się w środku zbioru danych, co nie zawsze jest prawdą. Wskazówka: możesz skalować max_range, aby wykres wyglądał ładniej, jeśli w pobliżu lub na granicach znajdują się punkty.
Rainman Noodles
Po zaktualizowaniu anacondy, ax.set_aspect ("equal") zgłosił błąd: NotImplementedError: Obecnie nie jest możliwe ręczne ustawienie aspektu na osiach 3D
Ewan
Zamiast dzwonić set_aspect('equal'), użyj set_box_aspect([1,1,1]), jak opisano w mojej odpowiedzi poniżej. U mnie działa w matplotlib w wersji 3.3.1!
AndrewCox
18

Na podstawie odpowiedzi @ karlo, aby uczynić rzeczy jeszcze czystszymi:

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

Stosowanie:

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

# ...draw here...

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

EDYTUJ: Ta odpowiedź nie działa w nowszych wersjach Matplotlib z powodu scalonych zmian pull-request #13474, które są śledzone w issue #17172i issue #1077. W ramach tymczasowego obejścia tego problemu można usunąć nowo dodane wiersze w 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')
Mateen Ulhaq
źródło
Uwielbiam to, ale po zaktualizowaniu anacondy ax.set_aspect ("equal") zgłosił błąd: NotImplementedError: Obecnie nie jest możliwe ręczne ustawienie aspektu na osiach 3D
Ewan
@Ewan Dodałem kilka linków na dole mojej odpowiedzi, aby pomóc w dochodzeniu. Wygląda na to, że użytkownicy MPL z jakiegoś powodu łamią obejścia bez prawidłowego naprawienia problemu. ¯ \\ _ (ツ) _ / ¯
Mateen Ulhaq
Myślę, że znalazłem obejście (które nie wymaga modyfikowania kodu źródłowego) dla NotImplementedError (pełny opis w mojej odpowiedzi poniżej); w zasadzie dodaj ax.set_box_aspect([1,1,1])przed zadzwonieniemset_axes_equal
AndrewCox
Właśnie znalazłem ten post i próbowałem, ale nie udało się na ax.set_aspect ('equal'). Nie stanowi to jednak problemu, jeśli po prostu usuniesz ax.set_aspect ('equal') ze swojego skryptu, ale zachowasz dwie niestandardowe funkcje set_axes_equal i _set_axes_radius ... upewniając się, że wywołałeś je przed plt.show (). Świetne rozwiązanie dla mnie! W końcu szukałem już od kilku lat. Zawsze wracałem do modułu vtk w Pythonie do drukowania 3D, zwłaszcza gdy liczba rzeczy staje się ekstremalna.
Tony A
15

Prosta naprawa!

Udało mi się to uruchomić w wersji 3.3.1.

Wygląda na to, że ten problem został prawdopodobnie rozwiązany w dokumencie PR # 17172 ; Możesz użyć tej ax.set_box_aspect([1,1,1])funkcji, aby upewnić się, że aspekt jest poprawny (zobacz uwagi dotyczące funkcji set_aspect ). W połączeniu z funkcjami obwiedni udostępnianymi przez @karlo i / lub @Matee Ulhaq, wykresy wyglądają teraz poprawnie w 3D!

matplotlib wykres 3d z równymi osiami

Minimalny przykład roboczy

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
źródło
Tak, w końcu! Dzięki - gdybym tylko mógł cię zagłosować na szczyt :)
N. Jonas Figge
7

EDYCJA: kod użytkownika2525140 powinien działać doskonale, chociaż ta odpowiedź rzekomo próbowała naprawić nieistniejący błąd. Poniższa odpowiedź to tylko zduplikowana (alternatywna) implementacja:

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])
dalum
źródło
Nadal musisz to zrobić: ax.set_aspect('equal')albo wartości tików mogą zostać schrzanione. W przeciwnym razie dobre rozwiązanie. Dzięki,
Tony Power
2

Od wersji matplotlib 3.3.0 Axes3D.set_box_aspect wydaje się być zalecanym podejściem.

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))
Matt Panzer
źródło
Sposób 2021. Działa jak marzenie.
Jan Joneš