matplotlib (ความยาวหน่วยเท่ากัน): ด้วยอัตราส่วน 'เท่ากัน' แกน z ไม่เท่ากับ x- และ y-


89

เมื่อฉันตั้งค่าอัตราส่วนเท่ากันสำหรับกราฟ 3 มิติแกน 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

ฉันจะทำให้ความยาวหน่วยของทั้งสามแกนเท่ากันได้อย่างไร วิธีแก้ปัญหาทั้งหมดที่ฉันหาไม่ได้ผล ขอขอบคุณ.

คำตอบ:


71

ฉันเชื่อว่า matplotlib ยังไม่ได้ตั้งค่าแกนที่เท่ากันอย่างถูกต้องใน 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 แต่ถึงแม้จะมีตัวเลือกแกนเท่ากันแกน z ของ matplotlib autoscale:

ไม่ดี

แต่ถ้าคุณเพิ่มกล่องขอบเขตคุณจะได้มาตราส่วนที่ถูกต้อง:

ป้อนคำอธิบายภาพที่นี่


ในกรณีนี้คุณไม่จำเป็นต้องมีequalคำสั่ง - มันจะเท่ากันเสมอ

1
วิธีนี้ใช้งานได้ดีถ้าคุณกำลังพล็อตข้อมูลเพียงชุดเดียว แต่จะเกิดอะไรขึ้นเมื่อมีชุดข้อมูลเพิ่มเติมทั้งหมดบนพล็อต 3 มิติเดียวกัน ในคำถามมีชุดข้อมูล 2 ชุดดังนั้นจึงเป็นเรื่องง่ายที่จะรวมเข้าด้วยกัน แต่อาจทำให้เกิดความไม่สมเหตุสมผลได้อย่างรวดเร็วหากวางแผนชุดข้อมูลหลายชุด
Steven C. Howell

@ stvn66 ฉันวางแผนชุดข้อมูลมากถึงห้าชุดในกราฟเดียวด้วยโซลูชันนี้และมันก็ใช้ได้ดีสำหรับฉัน

1
นี้ทำงานได้อย่างสมบูรณ์ สำหรับผู้ที่ต้องการสิ่งนี้ในรูปแบบฟังก์ชันซึ่งใช้วัตถุแกนและดำเนินการด้านบนฉันขอแนะนำให้ตรวจสอบคำตอบของ @karlo ด้านล่าง เป็นทางออกที่สะอาดกว่าเล็กน้อย
spurra

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

โปรดทราบว่าการใช้จุดกึ่งกลางจะไม่ได้ผลในทุกกรณีคุณควรใช้จุดกึ่งกลาง ดูความคิดเห็นของฉันเกี่ยวกับคำตอบของ tauran
Rainman Noodles

1
รหัสของฉันด้านบนไม่ได้ใช้ค่าเฉลี่ยของข้อมูล แต่ใช้ค่าเฉลี่ยของขีด จำกัด การลงจุดที่มีอยู่ ดังนั้นฟังก์ชันของฉันจึงรับประกันได้ว่าจะคอยดูจุดใด ๆ ที่อยู่ในมุมมองตามขีด จำกัด ของพล็อตที่ตั้งไว้ก่อนที่จะถูกเรียกใช้ หากผู้ใช้ตั้งค่าขีด จำกัด การลงจุดอย่างเข้มงวดเกินไปที่จะดูจุดข้อมูลทั้งหมดนั่นคือปัญหาที่แยกต่างหาก ฟังก์ชันของฉันช่วยให้มีความยืดหยุ่นมากขึ้นเนื่องจากคุณอาจต้องการดูข้อมูลเพียงบางส่วน สิ่งที่ฉันทำคือขยายขีด จำกัด ของแกนดังนั้นอัตราส่วนภาพคือ 1: 1: 1
karlo

อีกวิธีหนึ่งในการระบุ: ถ้าคุณใช้ค่าเฉลี่ยเพียง 2 จุดคือขอบเขตบนแกนเดียวนั่นหมายความว่าเป็นจุดกึ่งกลาง เท่าที่ฉันสามารถบอกได้ฟังก์ชันของ Dalum ด้านล่างควรจะเทียบเท่าทางคณิตศาสตร์กับของฉันและไม่มีอะไรต้อง `` แก้ไข ''
karlo

12
เหนือกว่าวิธีการแก้ปัญหาที่ยอมรับในปัจจุบันอย่างมากนั่นคือความยุ่งเหยิงเมื่อคุณเริ่มมีวัตถุมากมายที่มีลักษณะแตกต่างกัน
P-Gn

1
ฉันชอบวิธีแก้ปัญหานี้มาก แต่หลังจากที่ฉันอัปเดต anaconda แล้ว ax.set_aspect ("เท่ากับ") รายงานข้อผิดพลาด: NotImplementedError: ขณะนี้ยังไม่สามารถตั้งค่าขนาดภาพบนแกน 3 มิติด้วยตนเองได้
Ewan

52

ฉันง่ายแก้ปัญหาเรมี่เรนไฮน์โดยใช้ฟังก์ชั่นset_x/y/zlim

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 ออกมาเป็น 100 (จาก X) แสดงว่า x-range ของคุณจะเป็น 25 + - 50 ดังนั้น [-25, 75] และคุณจะพลาดจุดข้อมูล X [3] แม้ว่าแนวคิดนี้ดีมากและง่ายต่อการปรับเปลี่ยนเพื่อให้แน่ใจว่าคุณได้รับคะแนนทั้งหมด
TravisJ

1
ระวังว่าการใช้วิธีการเป็นศูนย์กลางไม่ถูกต้อง คุณควรจะใช้สิ่งที่ต้องการmidpoint_x = np.mean([X.max(),X.min()])และจากนั้นตั้งข้อ จำกัด ในการ+/-midpoint_x max_rangeการใช้ค่าเฉลี่ยจะใช้ได้ผลก็ต่อเมื่อค่าเฉลี่ยอยู่ที่จุดกึ่งกลางของชุดข้อมูลซึ่งไม่เป็นความจริงเสมอไป นอกจากนี้เคล็ดลับ: คุณสามารถปรับขนาด max_range เพื่อให้กราฟดูดีขึ้นหากมีจุดใกล้หรือบนขอบเขต
Rainman Noodles

หลังจากที่ฉันอัปเดต anaconda แล้ว ax.set_aspect ("เท่ากับ") รายงานข้อผิดพลาด: NotImplementedError: ขณะนี้ยังไม่สามารถตั้งค่าขนาดภาพบนแกน 3 มิติด้วยตนเองได้
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 #17172 issue #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 ("เท่ากับ") รายงานข้อผิดพลาด: NotImplementedError: ขณะนี้ยังไม่สามารถตั้งค่าขนาดภาพบนแกน 3 มิติด้วยตนเองได้
Ewan

@Ewan ฉันได้เพิ่มลิงค์ที่ด้านล่างของคำตอบเพื่อช่วยในการตรวจสอบ ดูเหมือนว่าคน MPL กำลังทำลายวิธีแก้ปัญหาโดยไม่ได้แก้ไขปัญหาอย่างถูกต้องด้วยเหตุผลบางประการ ¯ \\ _ (ツ) _ / ¯
Mateen Ulhaq

ฉันคิดว่าฉันพบวิธีแก้ปัญหาชั่วคราว (ที่ไม่ต้องแก้ไขซอร์สโค้ด) สำหรับ NotImplementedError (คำอธิบายแบบเต็มในคำตอบของฉันด้านล่าง) โดยทั่วไปเพิ่มax.set_box_aspect([1,1,1])ก่อนโทรset_axes_equal
AndrewCox

เพิ่งพบโพสต์นี้และพยายามล้มเหลวใน ax.set_aspect ('เท่ากับ') ไม่ใช่ปัญหาแม้ว่าคุณจะลบ ax.set_aspect ('เท่ากับ') ออกจากสคริปต์ของคุณ แต่ยังคงใช้ฟังก์ชันที่กำหนดเองสองฟังก์ชัน set_axes_equal และ _set_axes_radius ... อย่าลืมเรียกใช้ก่อน plt.show () ทางออกที่ดีสำหรับฉัน! ฉันค้นหามานานกว่าสองปีในที่สุด ฉันเปลี่ยนกลับไปใช้โมดูล vtk ของ python สำหรับการพล็อต 3 มิติเสมอโดยเฉพาะอย่างยิ่งเมื่อมีสิ่งต่างๆมากเกินไป
Tony A

15

แก้ไขง่ายๆ!

ฉันจัดการเพื่อให้สิ่งนี้ทำงานได้ในเวอร์ชัน 3.3.1

ดูเหมือนว่าปัญหานี้ได้รับการแก้ไขในบางทีPR # 17172 ; คุณสามารถใช้ax.set_box_aspect([1,1,1])ฟังก์ชันเพื่อให้แน่ใจว่าส่วนภาพถูกต้อง (ดูหมายเหตุสำหรับฟังก์ชันset_aspect ) เมื่อใช้ร่วมกับฟังก์ชั่นกล่องขอบเขตที่มีให้โดย @karlo และ / หรือ @Matee Ulhaq ตอนนี้พล็อตจะถูกต้องในแบบ 3 มิติ

matplotlib พล็อต 3 มิติที่มีแกนเท่ากัน

ตัวอย่างการทำงานขั้นต่ำ

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. Jonas Figge

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')ฉะนั้นค่าเห็บอาจถูกทำให้เสียหาย ทางออกที่ดีอย่างอื่น ขอบคุณ
Tony Power

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 ใช้งานได้เหมือนมีเสน่ห์
Jan Joneš
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.