matplotlib (lunghezza unitaria uguale): con proporzioni 'uguali' l'asse z non è uguale a x- e y-


89

Quando imposto le proporzioni uguali per il grafico 3D, l'asse z non cambia in "uguale". Così questo:

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

mi dà quanto segue: inserisci qui la descrizione dell'immagine

dove ovviamente la lunghezza unitaria dell'asse z non è uguale alle unità xey.

Come posso rendere uguale la lunghezza unitaria di tutti e tre gli assi? Tutte le soluzioni che ho trovato non hanno funzionato. Grazie.

Risposte:


71

Credo che matplotlib non abbia ancora impostato correttamente l'asse uguale in 3D ... Ma ho trovato qualche volta fa (non ricordo dove) un trucco che ho adattato usandolo. Il concetto è creare un finto riquadro di delimitazione cubico attorno ai dati. Puoi testarlo con il codice seguente:

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

i dati z sono circa un ordine di grandezza maggiore di xey, ma anche con l'opzione asse uguale, matplotlib ridimensiona automaticamente l'asse z:

male

Ma se aggiungi il riquadro di delimitazione, ottieni un ridimensionamento corretto:

inserisci qui la descrizione dell'immagine


In questo caso non hai nemmeno bisogno equaldell'affermazione: sarà sempre uguale.

1
Funziona bene se si traccia solo un set di dati, ma cosa succede quando ci sono più set di dati tutti sullo stesso grafico 3D? In questione, c'erano 2 set di dati quindi è una cosa semplice combinarli, ma ciò potrebbe diventare rapidamente irragionevole se si tracciano diversi set di dati diversi.
Steven C. Howell,

@ stvn66, stavo tracciando fino a cinque set di dati in un grafico con questa soluzione e ha funzionato bene per me.

1
Funziona perfettamente. Per coloro che lo desiderano in forma di funzione, che prende un oggetto asse ed esegue le operazioni sopra, li incoraggio a dare un'occhiata alla risposta @karlo di seguito. È una soluzione leggermente più pulita.
spurra

@ user1329187 - Ho scoperto che non funzionava per me senza la equaldichiarazione.
supergra

60

Mi piacciono le soluzioni di cui sopra, ma hanno lo svantaggio di dover tenere traccia degli intervalli e dei mezzi su tutti i tuoi dati. Ciò potrebbe essere complicato se si dispone di più set di dati che verranno tracciati insieme. Per risolvere questo problema, ho utilizzato i metodi ax.get_ [xyz] lim3d () e ho inserito il tutto in una funzione standalone che può essere chiamata solo una volta prima di chiamare plt.show (). Ecco la nuova versione:

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

Tieni presente che l'utilizzo di mezzi come punto centrale non funzionerà in tutti i casi, dovresti utilizzare i punti medi. Vedi il mio commento sulla risposta di Tauran.
Rainman Noodles

1
Il mio codice sopra non prende la media dei dati, prende la media dei limiti di trama esistenti. La mia funzione è così garantita per tenere in vista tutti i punti che erano in vista secondo i limiti della trama fissati prima che fosse chiamato. Se l'utente ha già impostato limiti di trama troppo restrittivi per vedere tutti i punti dati, si tratta di un problema separato. La mia funzione consente una maggiore flessibilità perché potresti voler visualizzare solo un sottoinsieme dei dati. Tutto quello che faccio è espandere i limiti degli assi in modo che le proporzioni siano 1: 1: 1.
karlo

Un altro modo per dirla: se prendi una media di soli 2 punti, vale a dire i limiti su un singolo asse, allora quella media È il punto medio. Quindi, per quanto ne so, la funzione di Dalum di seguito dovrebbe essere matematicamente equivalente alla mia e non c'era nulla da `` aggiustare ''.
karlo

12
Di gran lunga superiore alla soluzione attualmente accettata che è un disastro quando inizi ad avere molti oggetti di diversa natura.
P-Gn

1
La soluzione mi piace molto, ma dopo aver aggiornato anaconda, ax.set_aspect ("equal") ha riportato un errore: NotImplementedError: Non è attualmente possibile impostare manualmente l'aspetto sugli assi 3D
Ewan

52

Ho semplificato la soluzione di Remy F utilizzando le set_x/y/zlim funzioni .

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

inserisci qui la descrizione dell'immagine


1
Mi piace il codice semplificato. Tieni presente che alcuni (pochissimi) punti dati potrebbero non essere tracciati. Ad esempio, supponiamo che X = [0, 0, 0, 100] in modo che X.mean () = 25. Se max_range risulta essere 100 (da X), allora il tuo intervallo x sarà 25 + - 50, quindi [-25, 75] e ti mancherà il punto dati X [3]. L'idea è molto carina e facile da modificare per assicurarti di ottenere tutti i punti.
TravisJ

1
Attenzione che usare mezzi come centro non è corretto. Dovresti usare qualcosa di simile midpoint_x = np.mean([X.max(),X.min()])e quindi impostare i limiti su midpoint_x+/- max_range. L'uso della media funziona solo se la media si trova nel punto medio del set di dati, il che non è sempre vero. Inoltre, un suggerimento: puoi ridimensionare max_range per rendere il grafico più bello se ci sono punti vicino o sui confini.
Rainman Noodles

Dopo aver aggiornato anaconda, ax.set_aspect ("equal") ha riportato un errore: NotImplementedError: non è attualmente possibile impostare manualmente l'aspetto sugli assi 3D
Ewan

Piuttosto che chiamare set_aspect('equal'), usa set_box_aspect([1,1,1]), come descritto nella mia risposta di seguito. Per me sta funzionando nella versione matplotlib 3.3.1!
AndrewCox

18

Adattato dalla risposta di @ karlo per rendere le cose ancora più pulite:

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

Utilizzo:

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

# ...draw here...

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

EDIT: questa risposta non funziona su versioni più recenti di Matplotlib a causa delle modifiche unite in pull-request #13474, che sono tracciate in issue #17172e issue #1077. Come soluzione temporanea a questo, è possibile rimuovere le righe appena aggiunte in 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')

Adoro questo, ma dopo aver aggiornato anaconda, ax.set_aspect ("equal") ha riportato un errore: NotImplementedError: Non è attualmente possibile impostare manualmente l'aspetto sugli assi 3D
Ewan

@ Ewan ho aggiunto alcuni link in fondo alla mia risposta per aiutare nelle indagini. Sembra che la gente di MPL stia rompendo soluzioni alternative senza risolvere correttamente il problema per qualche motivo. ¯ \\ _ (ツ) _ / ¯
Mateen Ulhaq

Penso di aver trovato una soluzione alternativa (che non richiede la modifica del codice sorgente) per NotImplementedError (descrizione completa nella mia risposta di seguito); fondamentalmente aggiungi ax.set_box_aspect([1,1,1])prima di chiamareset_axes_equal
AndrewCox

Ho appena trovato questo post e ho provato, senza successo su ax.set_aspect ('equal'). Non è un problema se rimuovi semplicemente ax.set_aspect ('equal') dal tuo script ma mantieni le due funzioni personalizzate set_axes_equal e _set_axes_radius ... assicurandoti di chiamarle prima di plt.show (). Ottima soluzione per me! Ho cercato per un po 'di tempo in un paio d'anni, finalmente. Sono sempre tornato al modulo vtk di Python per la stampa 3D, specialmente quando il numero di cose diventa estremo.
Tony A

15

Soluzione semplice!

Sono riuscito a farlo funzionare nella versione 3.3.1.

Sembra che questo problema sia stato forse risolto nel PR # 17172 ; È possibile utilizzare la ax.set_box_aspect([1,1,1])funzione per assicurarsi che l'aspetto sia corretto (vedere le note per la funzione set_aspect ). Se utilizzato insieme alle funzioni del riquadro di delimitazione fornite da @karlo e / o @Matee Ulhaq, i grafici ora sembrano corretti in 3D!

matplotlib trama 3d con assi uguali

Esempio minimo di lavoro

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

Si Finalmente! Grazie - se solo potessi votarti in alto :)
N. Jonas Figge

7

MODIFICA: il codice di user2525140 dovrebbe funzionare perfettamente, sebbene questa risposta abbia presumibilmente tentato di correggere un errore inesistente. La risposta di seguito è solo un'implementazione duplicata (alternativa):

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

Hai ancora bisogno di fare: ax.set_aspect('equal')o i valori di tick potrebbero essere sbagliati. Altrimenti buona soluzione. Grazie,
Tony Power

2

A partire da matplotlib 3.3.0, Axes3D.set_box_aspect sembra essere l'approccio consigliato.

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

Il modo 2021. Funziona come un fascino.
Jan Joneš
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.