matplotlib (eşit birim uzunluk): 'eşit' en boy oranı ile z ekseni, x ve y- 'ye eşit değildir


89

3d grafik için eşit en boy oranı ayarladığımda, z ekseni 'eşit' olarak değişmiyor. Yani, bu:

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

bana şunu veriyor: görüntü açıklamasını buraya girin

açıkça burada z ekseninin birim uzunluğu x ve y birimlerine eşit değildir.

Her üç eksenin de birim uzunluğunu nasıl eşit yapabilirim? Bulabildiğim tüm çözümler işe yaramadı. Teşekkür ederim.

Yanıtlar:


71

Matplotlib'in 3B'de henüz doğru şekilde eşit ekseni ayarlamadığına inanıyorum ... Ama birkaç kez önce (nerede olduğunu hatırlamıyorum) onu kullanarak adapte ettiğim bir numara buldum. Kavram, verilerinizin etrafında sahte bir kübik sınırlayıcı kutu oluşturmaktır. Aşağıdaki kodla test edebilirsiniz:

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 verileri, x ve y'den daha büyük bir büyüklük sırası hakkındadır, ancak eşit eksen seçeneği ile bile matplotlib otomatik ölçekleme z ekseni:

kötü

Ancak sınırlayıcı kutuyu eklerseniz, doğru bir ölçeklendirme elde edersiniz:

görüntü açıklamasını buraya girin


Bu durumda equalifadeye bile ihtiyacınız yoktur - her zaman eşit olacaktır.

1
Bu, yalnızca bir veri kümesini çiziyorsanız iyi çalışır, ancak aynı 3B arsa üzerinde daha fazla veri kümesi olduğunda ne olur? Söz konusu olduğunda, 2 veri seti vardı, bu yüzden onları birleştirmek basit bir şey, ancak birkaç farklı veri seti çizilirse, bu hızlı bir şekilde mantıksız hale gelebilir.
Steven C. Howell

@ stvn66, bu çözümlerle bir grafikte beş veri setine kadar çizim yapıyordum ve benim için iyi çalıştı.

1
Bu mükemmel çalışıyor. Bunu bir eksen nesnesi alan ve yukarıdaki işlemleri yapan fonksiyon formunda isteyenler için aşağıdaki @karlo cevabına bakmalarını tavsiye ederim. Biraz daha temiz bir çözümdür.
spurra

@ user1329187 - Bunun benim için equalifade olmadan işe yaramadığını buldum .
supergra

60

Yukarıdaki çözümleri beğendim, ancak tüm verileriniz üzerinden aralıkları ve araçları takip etmeniz gereken dezavantajları var. Birlikte çizilecek birden fazla veri kümeniz varsa, bu külfetli olabilir. Bunu düzeltmek için ax.get_ [xyz] lim3d () yöntemlerini kullandım ve hepsini plt.show () 'u çağırmadan hemen önce çağrılabilecek bağımsız bir işleve koydum. İşte yeni versiyon:

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

Merkez noktası her durumda işe yaramayacağı için araç kullanmanın her durumda işe yaramayacağını unutmayın, orta noktaları kullanmalısınız. Tauran'ın cevabı hakkındaki yorumuma bakın.
Rainman Noodles

1
Yukarıdaki kodum verilerin ortalamasını almıyor, mevcut çizim sınırlarının ortalamasını alıyor. Bu nedenle, işlevim çağrılmadan önce belirlenen çizim sınırlarına göre görüş alanında olan noktaları göz önünde bulundurur. Kullanıcı zaten tüm veri noktalarını görmek için çok kısıtlayıcı bir şekilde çizim sınırlarını ayarlamışsa, bu ayrı bir konudur. İşlevim daha fazla esneklik sağlar çünkü verilerin yalnızca bir alt kümesini görüntülemek isteyebilirsiniz. Tek yaptığım eksen sınırlarını genişletmek, böylece en boy oranı 1: 1: 1.
karlo

Başka bir deyişle, sadece 2 puanlık bir ortalama alırsanız, yani tek bir eksendeki sınırlar, o zaman orta nokta IS anlamına gelir. Dolayısıyla, söyleyebileceğim kadarıyla, Dalum'un aşağıdaki işlevi matematiksel olarak benimkine denk olmalı ve `` düzeltilecek '' hiçbir şey yoktu.
karlo

12
Şu anda kabul edilen çözümden çok daha üstün olan, farklı nitelikte birçok nesneye sahip olmaya başladığınızda karmaşa yaratır.
P-Gn

1
Çözümü gerçekten beğendim, ancak anaconda'yı güncelledikten sonra ax.set_aspect ("eşit") hata bildirdi: NotImplementedError: Şu anda 3B eksenlerde görünümü manuel olarak ayarlamak mümkün değil
Ewan

52

set_x/y/zlim Fonksiyonları kullanarak Remy F'nin çözümünü basitleştirdim .

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

görüntü açıklamasını buraya girin


1
Basitleştirilmiş kodu beğendim. Bazı (çok az) veri noktasının çizilemeyebileceğini unutmayın. Örneğin, X.ortalama () = 25 olacak şekilde X = [0, 0, 0, 100] olduğunu varsayalım. Eğer maks_aralık 100 (X'ten) çıkarsa, o zaman x aralığınız 25 + - 50 olacak, yani [-25, 75] ve X [3] veri noktasını kaçıracaksınız. Yine de fikir çok güzel ve tüm puanları aldığınızdan emin olmak için değiştirilmesi kolaydır.
TravisJ

1
Merkez olarak araç kullanmanın doğru olmadığına dikkat edin. Gibi bir şey kullanmalı midpoint_x = np.mean([X.max(),X.min()])ve ardından sınırları midpoint_x+/- olarak ayarlamalısınız max_range. Ortalamanın kullanılması, yalnızca ortalama, veri kümesinin orta noktasında yer alıyorsa işe yarar, bu her zaman doğru değildir. Ayrıca bir ipucu: sınırların yakınında veya sınırlarında noktalar varsa grafiğin daha güzel görünmesi için maks_aralığı ölçekleyebilirsiniz.
Rainman Noodles

NotImplementedError: I ( "eşit") anaconda, ax.set_aspect güncellenen sonra hata bildirdi elle 3D üzerinde boy eksenleri ayarlamak O mümkün değil
Ewan

Aramak yerine set_aspect('equal'), set_box_aspect([1,1,1])aşağıdaki cevabımda açıklandığı gibi kullanın . Matplotlib sürüm 3.3.1'de benim için çalışıyor!
AndrewCox

18

İşleri daha da netleştirmek için @ karlo'nun cevabından uyarlanmıştır:

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

Kullanım:

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

# ...draw here...

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

DÜZENLEME: Bu cevap nedeniyle birleşen değişikliklere Matplotlib daha yeni sürümleri çalışmaz pull-request #13474izlenir, issue #17172ve issue #1077. Buna geçici bir çözüm olarak, yeni eklenen satırlar şuraya kaldırılabilir 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')

Bunu seviyorum, ancak anaconda'yı güncelledikten sonra ax.set_aspect ("eşit") hata bildirdi: NotImplementedError: Şu anda 3B eksenlerde görünümü manuel olarak ayarlamak mümkün değil
Ewan

@Ewan Soruşturmaya yardımcı olmak için cevabımın altına bazı bağlantılar ekledim. Görünüşe göre MPL çalışanları, sorunu herhangi bir nedenle düzgün bir şekilde düzeltmeden geçici çözümleri bozuyor. ¯ \\ _ (ツ) _ / ¯
Mateen Ulhaq

NotImplementedError için bir geçici çözüm (bu, kaynak kodunu değiştirmeyi gerektirmeyen) bulduğumu düşünüyorum (aşağıdaki cevabımın tam açıklaması); temelde ax.set_box_aspect([1,1,1])aramadan önce ekleyinset_axes_equal
AndrewCox

Bu gönderiyi buldum ve denedim, ax.set_aspect ('eşit') üzerinde başarısız oldu. Ax.set_aspect ('eşittir') komut dosyanızı betiğinizden kaldırırsanız, ancak iki özel işlevi set_axes_equal ve _set_axes_radius ... plt.show () 'dan önce çağırmayı unutmayın. Benim için harika bir çözüm! Sonunda birkaç yıldır bir süredir arıyordum. Her zaman 3D çizim için python'un vtk modülüne geri döndüm, özellikle de şeylerin sayısı aşırı olduğunda.
Tony A

15

Basit düzeltme!

Bunu 3.3.1 sürümünde çalıştırmayı başardım.

Görünüşe göre bu sorun PR # 17172'de çözülmüş olabilir ; En ax.set_box_aspect([1,1,1])boy oranının doğru olduğundan emin olmak için işlevi kullanabilirsiniz ( set_aspect işlevi için notlara bakın ). @Karlo ve / veya @Matee Ulhaq tarafından sağlanan sınırlayıcı kutu işlev (ler) i ile birlikte kullanıldığında, grafikler artık 3D olarak doğru görünüyor!

matplotlib eşit eksenli 3d arsa

Minimum Çalışma Örneği

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

Evet sonunda! Teşekkürler - keşke seni zirveye çıkarabilseydim :)
N. Jonas Figge

7

DÜZENLEME: user2525140'ın kodu, sözde var olmayan bir hatayı düzeltmeye çalışsa da, mükemmel şekilde çalışmalıdır. Aşağıdaki cevap, yalnızca yinelenen (alternatif) bir uygulamadır:

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

Yine de yapmanız gerekir: ax.set_aspect('equal')yoksa tik değerleri yanlış olabilir. Aksi takdirde iyi bir çözüm. Teşekkürler,
Tony Power

2

Matplotlib 3.3.0 itibariyle, Axes3D.set_box_aspect önerilen yaklaşım gibi görünmektedir.

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

2021 yolu. Tıkır tıkır çalışıyor.
Jan Joneš
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.