matplotlib (độ dài đơn vị bằng nhau): với tỷ lệ khung hình 'bằng nhau', trục z không bằng x- và y-


87

Khi tôi thiết lập tỷ lệ co bằng nhau cho đồ thị 3d, trục z không thay đổi thành 'bằng'. Vì vậy, điều này:

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

cho tôi những thứ sau: nhập mô tả hình ảnh ở đây

trong đó rõ ràng là độ dài đơn vị của trục z không bằng đơn vị x- và y.

Làm thế nào tôi có thể làm cho độ dài đơn vị của cả ba trục bằng nhau? Tất cả các giải pháp tôi có thể tìm thấy đều không hoạt động. Cảm ơn bạn.

Câu trả lời:


69

Tôi tin rằng matplotlib chưa đặt đúng trục bằng trong 3D ... Nhưng tôi đã tìm thấy một mẹo cách đây vài lần (tôi không nhớ ở đâu) mà tôi đã điều chỉnh bằng cách sử dụng nó. Khái niệm này là tạo một hộp giới hạn hình khối giả xung quanh dữ liệu của bạn. Bạn có thể kiểm tra nó bằng đoạn mã sau:

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

dữ liệu z về thứ tự độ lớn lớn hơn x và y, nhưng ngay cả với tùy chọn trục bằng nhau, trục z tự động matplotlib:

xấu

Nhưng nếu bạn thêm hộp giới hạn, bạn sẽ có được tỷ lệ chính xác:

nhập mô tả hình ảnh ở đây


Trong trường hợp này, bạn thậm chí không cần equalcâu lệnh - nó sẽ luôn bằng nhau.

1
Điều này hoạt động tốt nếu bạn chỉ vẽ một tập dữ liệu nhưng còn khi có nhiều tập dữ liệu hơn tất cả trên cùng một biểu đồ 3d thì sao? Trong câu hỏi, có 2 tập dữ liệu nên việc kết hợp chúng là một điều đơn giản nhưng điều đó có thể trở nên bất hợp lý một cách nhanh chóng nếu vẽ nhiều tập dữ liệu khác nhau.
Steven C. Howell

@ stvn66, tôi đã vẽ tối đa năm tập dữ liệu trong một biểu đồ với các giải pháp này và nó hoạt động tốt đối với tôi.

1
Điều này hoạt động hoàn hảo. Đối với những người muốn điều này ở dạng hàm, lấy một đối tượng trục và thực hiện các thao tác ở trên, tôi khuyến khích họ xem câu trả lời @karlo bên dưới. Nó là một giải pháp sạch hơn một chút.
spurra

@ user1329187 - Tôi thấy điều này không hiệu quả với tôi nếu không có equaltuyên bố.
supergra

58

Tôi thích các giải pháp trên, nhưng chúng có nhược điểm là bạn cần theo dõi phạm vi và phương tiện trên tất cả dữ liệu của mình. Điều này có thể phức tạp nếu bạn có nhiều tập dữ liệu sẽ được vẽ cùng nhau. Để khắc phục điều này, tôi đã sử dụng các phương thức ax.get_ [xyz] lim3d () và đặt toàn bộ vào một hàm độc lập có thể được gọi chỉ một lần trước khi bạn gọi hàm plt.show (). Đây là phiên bản mới:

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

Lưu ý rằng việc sử dụng phương tiện làm điểm trung tâm sẽ không hoạt động trong mọi trường hợp, bạn nên sử dụng điểm giữa. Xem bình luận của tôi về câu trả lời của tauran.
Rainman Noodles

1
Đoạn mã của tôi ở trên không lấy giá trị trung bình của dữ liệu, nó lấy giá trị trung bình của các giới hạn ô hiện có. Do đó, chức năng của tôi được đảm bảo để theo dõi bất kỳ điểm nào được xem theo các giới hạn cốt truyện được đặt trước khi nó được gọi. Nếu người dùng đã đặt giới hạn âm mưu quá hạn chế để xem tất cả các điểm dữ liệu, đó là một vấn đề riêng biệt. Chức năng của tôi cho phép linh hoạt hơn vì bạn có thể chỉ muốn xem một tập hợp con của dữ liệu. Tất cả những gì tôi làm là mở rộng giới hạn trục để tỷ lệ khung hình là 1: 1: 1.
karlo

Nói một cách khác: nếu bạn chỉ lấy giá trị trung bình của 2 điểm, cụ thể là các giới hạn trên một trục duy nhất, thì điều đó có nghĩa LÀ điểm giữa. Vì vậy, theo như tôi có thể nói, hàm của Dalum dưới đây phải tương đương về mặt toán học với của tôi và không có gì để `` sửa chữa ''.
karlo

11
Rất vượt trội so với giải pháp hiện đang được chấp nhận là một mớ hỗn độn khi bạn bắt đầu có nhiều đồ vật có bản chất khác nhau.
P-Gn

1
Tôi thực sự thích giải pháp này, nhưng sau khi tôi cập nhật anaconda, ax.set_aspect ("bằng") đã báo lỗi: NotImplementedError: Hiện không thể thiết lập thủ công khía cạnh trên trục 3D
Ewan

51

Tôi đã đơn giản hóa giải pháp của Remy F bằng cách sử dụng các set_x/y/zlim chức năng .

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

nhập mô tả hình ảnh ở đây


1
Tôi thích mã được đơn giản hóa. Chỉ cần lưu ý rằng một số (rất ít) điểm dữ liệu có thể không được vẽ biểu đồ. Ví dụ: giả sử rằng X = [0, 0, 0, 100] sao cho X.mean () = 25. Nếu max_range xuất hiện là 100 (từ X), thì phạm vi x của bạn sẽ là 25 + - 50, vì vậy [-25, 75] và bạn sẽ bỏ lỡ điểm dữ liệu X [3]. Mặc dù vậy, ý tưởng này rất hay và dễ sửa đổi để đảm bảo bạn nhận được tất cả các điểm.
TravisJ

1
Lưu ý rằng việc sử dụng phương tiện làm trung tâm là không đúng. Bạn nên sử dụng một cái gì đó như midpoint_x = np.mean([X.max(),X.min()])và sau đó đặt giới hạn thành midpoint_x+/- max_range. Sử dụng giá trị trung bình chỉ hoạt động nếu giá trị trung bình nằm ở điểm giữa của tập dữ liệu, điều này không phải lúc nào cũng đúng. Ngoài ra, một mẹo: bạn có thể chia tỷ lệ max_range để làm cho biểu đồ trông đẹp hơn nếu có các điểm gần hoặc trên ranh giới.
Rainman Noodles

Sau khi tôi cập nhật anaconda, ax.set_aspect ("bằng") đã báo lỗi: NotImplementedError: Hiện không thể thiết lập thủ công khía cạnh trên trục 3D
Ewan

Thay vì gọi set_aspect('equal'), hãy sử dụng set_box_aspect([1,1,1]), như được mô tả trong câu trả lời của tôi bên dưới. Nó hoạt động cho tôi trong matplotlib phiên bản 3.3.1!
AndrewCox

17

Phỏng theo câu trả lời của @ karlo để làm cho mọi thứ trở nên sạch sẽ hơn:

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

Sử dụng:

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

# ...draw here...

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

CHỈNH SỬA: Câu trả lời này không hoạt động trên các phiên bản mới hơn của Matplotlib do những thay đổi được hợp nhất pull-request #13474, được theo dõi trong issue #17172issue #1077. Như một giải pháp tạm thời cho điều này, người ta có thể xóa các dòng mới được thêm vào 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')

Thích điều này, nhưng sau khi tôi cập nhật anaconda, ax.set_aspect ("bằng") đã báo lỗi: NotImplementedError: Hiện không thể thiết lập thủ công khía cạnh trên trục 3D
Ewan

@Ewan Tôi đã thêm một số liên kết ở cuối câu trả lời của mình để giúp điều tra. Có vẻ như những người MPL đang phá vỡ các giải pháp thay thế mà không khắc phục sự cố đúng cách vì một số lý do. ¯ \\ _ (ツ) _ / ¯
Mateen Ulhaq

Tôi nghĩ rằng tôi đã tìm thấy một giải pháp thay thế (không yêu cầu sửa đổi mã nguồn) cho NotImplementedError (mô tả đầy đủ trong câu trả lời của tôi bên dưới); về cơ bản thêm ax.set_box_aspect([1,1,1])trước khi gọiset_axes_equal
AndrewCox

11

Cách sửa đơn giản!

Tôi đã quản lý để làm cho điều này hoạt động trong phiên bản 3.3.1.

Có vẻ như vấn đề này có lẽ đã được giải quyết trong PR # 17172 ; Bạn có thể sử dụng ax.set_box_aspect([1,1,1])hàm để đảm bảo khía cạnh là chính xác (xem ghi chú cho hàm set_aspect ). Khi được sử dụng cùng với (các) chức năng hộp giới hạn được cung cấp bởi @karlo và / hoặc @Matee Ulhaq, các ô giờ đây trông chính xác trong 3D!

Biểu đồ 3d matplotlib với các trục bằng nhau

Ví dụ làm việc tối thiểu

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

Vâng, cuối cùng! Cảm ơn - nếu tôi chỉ có thể ủng hộ bạn lên hàng đầu :)
N. Jonas Figge

7

CHỈNH SỬA: mã của user2525140 sẽ hoạt động hoàn toàn tốt, mặc dù câu trả lời này được cho là đã cố gắng sửa một lỗi không tồn tại. Câu trả lời dưới đây chỉ là một triển khai trùng lặp (thay thế):

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

Bạn vẫn cần phải làm: ax.set_aspect('equal')hoặc các giá trị đánh dấu có thể bị vặn. Nếu không thì giải pháp tốt. Cảm ơn,
Tony Power

1

Kể từ matplotlib 3.3.0, Axes3D.set_box_aspect dường như là cách tiếp cận được khuyến nghị.

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))
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.