matplotlib(等しい単位長):「等しい」アスペクト比の場合、z軸はx-およびy-と等しくありません


89

3Dグラフに等しいアスペクト比を設定すると、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単位に等しくありません。

3つの軸すべての単位長さを等しくするにはどうすればよいですか?私が見つけたすべての解決策はうまくいきませんでした。ありがとうございました。

回答:


71

matplotlibはまだ3Dで正しく等しい軸を設定していないと思います...しかし、私はそれを使用して適応したトリックを数回前に見つけました(どこにあるかは覚えていません)。コンセプトは、データの周りに偽の3次バウンディングボックスを作成することです。次のコードでテストできます。

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よりも約1桁大きいですが、等軸オプションを使用しても、matplotlibはz軸を自動スケールします。

悪い

ただし、バウンディングボックスを追加すると、正しいスケーリングが得られます。

ここに画像の説明を入力してください


この場合、equalステートメントは必要ありません。常に等しくなります。

1
これは、1セットのデータのみをプロットする場合は正常に機能しますが、同じ3Dプロットにすべてのデータセットがさらにある場合はどうでしょうか。問題は、2つのデータセットがあったので、それらを組み合わせるのは簡単なことですが、複数の異なるデータセットをプロットするとすぐに不合理になる可能性があります。
スティーブンC.ハウエル

@ stvn66、私はこのソリューションで1つのグラフに最大5つのデータセットをプロットしていましたが、それは私にとってはうまくいきました。

1
これは完全に機能します。軸オブジェクトを取り、上記の操作を実行する関数形式でこれが必要な場合は、以下の@karlo回答を確認することをお勧めします。少しクリーンなソリューションです。
spurra 2018

@ user1329187-equalステートメントがないとこれは機能しないことがわかりました。
supergra

60

私は上記のソリューションが好きですが、すべてのデータの範囲と平均を追跡する必要があるという欠点があります。一緒にプロットされる複数のデータセットがある場合、これは面倒な場合があります。これを修正するために、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 2016

別の言い方をすれば、2点だけの平均、つまり1つの軸の境界を取る場合、その平均は中点です。したがって、私が知る限り、以下のDalumの関数は数学的に私のものと同等であり、「修正」するものは何もありませんでした。
karlo 2016

12
さまざまな性質のオブジェクトがたくさんあり始めると混乱する、現在受け入れられているソリューションよりもはるかに優れています。
p-Gn 2017

1
私はこのソリューションが本当に好きですが、anacondaを更新した後、ax.set_aspect( "equal")がエラーを報告しました:NotImplementedError:現在、3D軸にアスペクトを手動で設定することはできません
Ewan

52

set_x/y/zlim 関数を使用して、RemyFのソリューションを簡略化しました。

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が(Xから)100になると、x-rangeは25 + -50になるため、[-25、75]になり、X [3]データポイントが失われます。ただし、このアイデアは非常に優れており、すべてのポイントを確実に取得できるように簡単に変更できます。
TravisJ 2015

1
手段を中心として使用することは正しくないことに注意してください。次のようなものを使用してmidpoint_x = np.mean([X.max(),X.min()])から、制限をmidpoint_x+/-に設定する必要がありますmax_range。平均の使用は、平均がデータセットの中点にある場合にのみ機能しますが、これは常に正しいとは限りません。また、ヒント:境界の近くまたは境界上にポイントがある場合は、max_rangeをスケーリングして、グラフの見栄えを良くすることができます。
Rainman Noodles 2016年

anacondaを更新した後、ax.set_aspect( "equal")がエラーを報告しました:NotImplementedError:現在、3D軸にアスペクトを手動で設定することはできません
Ewan

以下の私の答えで説明されているように、呼び出すのset_aspect('equal')ではなく、を使用set_box_aspect([1,1,1])してください。それはmatplotlibバージョン3.3.1で私のために働いています!
AndrewCox

18

物事をさらにきれいにするために@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 #17172issue #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')

これが大好きですが、anacondaを更新した後、ax.set_aspect( "equal")がエラーを報告しました:NotImplementedError:現在、3D軸にアスペクトを手動で設定することはできません
Ewan

@Ewan調査に役立つように、回答の下部にいくつかのリンクを追加しました。MPLの人々は、何らかの理由で問題を適切に修正せずに回避策を破っているように見えます。¯\\ _(ツ)_ /¯
Mateen Ulhaq

NotImplementedError(以下の私の回答の完全な説明)の回避策(ソースコードを変更する必要はありません)を見つけたと思います。基本的に追加ax.set_box_aspect([1,1,1])呼び出す前にset_axes_equal
AndrewCox

この投稿を見つけて試行しましたが、ax.set_aspect( 'equal')で失敗しました。スクリプトからax.set_aspect( 'equal')を削除するだけで、2つのカスタム関数set_axes_equalと_set_axes_radiusを保持する場合は問題ありません...必ずplt.show()の前に呼び出してください。私にとって素晴らしい解決策です!ついに、私は数年にわたってしばらくの間探していました。特に物事の数が極端になるとき、私は常に3DプロットのためにPythonのvtkモジュールに戻りました。
トニーA

15

簡単な修正!

私はこれをバージョン3.3.1で動作させることができました。

この問題はおそらくPR#17172で解決されたようです。このax.set_box_aspect([1,1,1])関数を使用して、アスペクトが正しいことを確認できます(set_aspect関数の注を参照してください)。@karloおよび/または@MateeUlhaqによって提供されるバウンディングボックス関数と組み合わせて使用​​すると、プロットが3Dで正しく表示されるようになりました。

等しい軸を持つmatplotlib3dプロット

最小限の作業例

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

はい、ついに!ありがとう-私があなたをトップに投票することができれば:)
N. JonasFigge20年

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')またはティック値が台無しにされるかもしれません。そうでなければ良い解決策。おかげで、
トニー・パワー

2

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

2021年の方法。チャームのように機能します。
ヤン・ジョーンズ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.