【问题标题】:Draw line over surface plot在曲面图上画线
【发布时间】:2018-03-31 09:43:53
【问题描述】:

我希望能够看到位于曲面顶部(第二张图像)而不是后面(第一张图像)的 3D 曲面上的线(和点)。 这是我的 3D 函数:

def f(x, y): 
    return np.sin(2*x) * np.cos(2*y)

3D 表面的 X、Y、Z:

x = np.linspace(-2, 2, 100)
y = np.linspace(-2, 2, 100)
X, Y = np.meshgrid(x, y)
Z = f(X, Y)

我生成了一个由 x 点 (xx) 和 y 点 (yy) 组成的向量,其中 zz = f(xx,yy)

fig = plt.figure(figsize=(8,6))
ax = plt.axes(projection='3d')
ax.scatter(xx, yy, zz, c='r', marker='o')
#ax.plot(xx,yy,zz, c= 'r')

ax.plot_surface(X, Y, Z, rstride=1, cstride=1,
                cmap='viridis', edgecolor='none')

如您所见,点在情节的后面,数字覆盖了点。我想看看情节上的要点。我该怎么办?

我希望能够看到这样的点和线:

编辑: 这是我生成积分的方式:

for i in range(1,2000):
    [a, b] =  np.random.rand(2,1)*np.sign(np.random.randn(2,1))*2
    xx = np.append(xx, a)
    yy = np.append(yy, b)

我注意到如果我写zz = f(xx,yy) + epsilon 我可以看到要点。如果epsilon = 0,那么从数学上讲,这些点在表面上,我看不清它们,就像在第一张图片中一样。如果epsilon > 0.05,我可以看到点,但这意味着将点向上移动。我真的不喜欢这个解决方案。如果一个点在一个曲面上,则该曲面具有优先权,她的曲面似乎在该点之上。我希望我的图形是相反的。

【问题讨论】:

  • 既然你知道你的点在表面上,你可以通过改变你的点来绘制一个不可见的量......浮点错误使你的目标有点反正不可能。最后,表面是用平面绘制的,因此即使在精确的场景中,您的点也会隐藏在函数凸出的位置。
  • 是的,我知道我可以做到,这就是我编辑问题的原因。但是,很难选择一个好的 epsilon 来使表面上“存在”的所有点都可见。有时,对于一个小的非零 epsilon,一些点仍然在表面“之下”。这就是为什么我正在寻找更优雅的解决方案

标签: python matplotlib plot 3d


【解决方案1】:

首先让我说你想要的东西有点不明确。您想精确地在底层表面上绘制点,使其始终显示在图中,即刚好在表面上方,而不是显式地将它们向上移动。这已经是个问题了,因为

  1. 浮点运算意味着您的点和表面的精确坐标可能会随着机器精度的顺序而变化,因此试图依赖精确等式是行不通的。
  2. 即使数字精确到无限精确,表面也是用一组近似平面绘制的。这意味着您的精确数据点将在函数凸出的近似表面之下。

然而,最大的问题是 matplotlib 中的 3d 绘图是 known to be unreliable 在一个图形中绘制多个或复杂对象时。特别是,渲染器本质上是 2d 的,当试图找出对象的相对表观位置时,它经常会遇到问题。要克服这个问题,可以尝试 hacking around the problem,或者使用适当的 3d 渲染器切换到 mayavi 之类的东西。

不幸的是,zorder 可选关键字参数通常被 3d 坐标区对象忽略。所以我在 pyplot 中唯一能想到的就是你几乎拥有的东西,注释掉:使用ax.plot 而不是ax.scatter。虽然后者会生成第一个图中所示的图(每个散点由于某种原因被隐藏,无论视角如何),前者会导致第二个图中所示的图(点可见)。通过从绘图样式中删除线条,我们几乎得到你想要的:

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

def f(x, y):                        
    return np.sin(2*x) * np.cos(2*y)

# data for the surface
x = np.linspace(-2, 2, 100)
X, Y = np.meshgrid(x, x)
Z = f(X, Y)

# data for the scatter
xx = 4*np.random.rand(1000) - 2
yy = 4*np.random.rand(1000) - 2
zz = f(xx,yy)

fig = plt.figure(figsize=(8,6))
ax = plt.axes(projection='3d')
#ax.scatter(xx, yy, zz, c='r', marker='o')
ax.plot(xx, yy, zz, 'ro', alpha=0.5) # note the 'ro' (no '-') and the alpha

ax.plot_surface(X, Y, Z, rstride=1, cstride=1,
                cmap='viridis', edgecolor='none')

但不完全是:很快就会发现,在这种情况下,这些点总是可见,即使它们应该隐藏在表面的一部分后面:

# note the change in points: generate only in the "back" quadrant
xx = 2*np.random.rand(1000) - 2
yy = 2*np.random.rand(1000)
zz = f(xx,yy)

fig = plt.figure(figsize=(8,6))
ax = plt.axes(projection='3d')
ax.plot(xx,yy,zz, 'ro', alpha=0.5)

ax.plot_surface(X, Y, Z, rstride=1, cstride=1,
                cmap='viridis', edgecolor='none')

很容易看出前面的凹凸应该隐藏背景中的一大块点,但是这些点是可见的。这正是 pyplot 在复杂的 3d 可视化中遇到的问题。因此,我的底线是你不能可靠地使用 matplotlib 做你想做的事。对于它的价值,我不确定这样的情节是多么容易理解。


以更积极的方式结束,以下是使用 mayavi 执行此操作的方法(为此,您需要安装 vtk,最好通过包管理器完成):

import numpy as np
from mayavi import mlab
from matplotlib.cm import get_cmap # for viridis

def f(x, y):
    return np.sin(2*x) * np.cos(2*y)

# data for the surface
x = np.linspace(-2, 2, 100)
X, Y = np.meshgrid(x, x)
Z = f(X, Y)

# data for the scatter
xx = 4*np.random.rand(1000) - 2
yy = 4*np.random.rand(1000) - 2
zz = f(xx,yy)

fig = mlab.figure(bgcolor=(1,1,1))
# note the transpose in surf due to different conventions compared to meshgrid
su = mlab.surf(X.T, Y.T, Z.T)
sc = mlab.points3d(xx, yy, zz, scale_factor=0.1, scale_mode='none',
                   opacity=1.0, resolution=20, color=(1,0,0))

# manually set viridis for the surface
cmap_name = 'viridis'
cdat = np.array(get_cmap(cmap_name,256).colors)
cdat = (cdat*255).astype(int)
su.module_manager.scalar_lut_manager.lut.table = cdat

mlab.show()

如您所见,结果是一个交互式 3d 图,其中表面上的数据点是适当的球体。可以使用不透明度和球体比例设置来获得令人满意的可视化效果。由于正确的 3D 渲染,无论视角如何,您都可以看到适当数量的每个点。

【讨论】:

  • 您的回复非常好,期待下次使用 mayavi。非常感谢你!我只有一个问题。我不明白您所说的“交互式 3D 绘图”是什么意思。我可以使用 mayavi 轻松地从另一个角度旋转图形/视图吗? (这将是惊人的)
  • @Zkillt 完全一样,就像在 matplotlib 中一样。好吧,不是完全正确,因为在 matplotlib 中 z 轴始终是垂直的,但在 mayavi (vtk) 中,您可以根据 yaw/pitch/roll 获得任意摄像机角度 :)
【解决方案2】:

Andras Deak 给出了一个非常全面的answer 讨论了 matplotlibs 3D 绘图功能在手头任务中的问题/限制。在他的回答结束时——以肯定的结尾——他给出了一个使用替代库的解决方案。

我开始尝试在 matplotlib 中找到一个 hacky/专业的解决方案。让我先说明原因。我想在 2D 表面上绘制轨迹,我开始使用 matplotlib。我将它用于我所有的 2D 绘图,并希望为这个特定的 3D 绘图应用程序找到解决方案。 matplotlibs 3D 图的好处在于它们是矢量化的,因为它们基本上只是通过将 3D 元素投影到相机平面上并覆盖它们(根据它们与相机的距离按顺序绘制它们)生成的 2D 图。可以为绘图的每个元素单独控制光栅化,而不会影响轴、标签等。使用光线追踪的“真实”3D 绘图库通常无法生成完全矢量化的绘图。我认为mayavi 就是一个例子,我知道 Mathematica 在这方面也非常有限。

提出我的解决方案:我查看了 plot_surface 的代码,该代码最终基于 Poly3DCollection,以了解 matplotlib 如何决定首先绘制表面上的哪些多边形/元素。的方法_do_3d_projection Poly3DCollection 将投影到 2d 相机平面上的多边形按(原始 3D 对象的)到相机的距离排序。首先绘制远离相机的元素,然后绘制靠近相机的元素。这对于大多数绘图都可以很好地创建正确的视角(但该方法有局限性,例如,请参阅mplot3d FAQ。但是,这种排序是我解决方案的关键。给定一组点 pts和一个表面surf(必须使用showsavefig 绘制才能设置其相机/投影变量):

  1. surf 中所有 3D 多边形的 2D 投影 segments_2d 到相机平面的计算包括它们基于到相机的距离的排序(存储在 segments_idxs )。
  2. 所有点都与 3D 表面上的元素/多边形相关联。
  3. 计算 3D 点到相机平面的 2D 投影。
  4. 为了确定一个点是否可见,我们检查它是否被一个多边形覆盖在它所关联的那个之后(从第 2 步开始)。为此,我们使用来自matplotlib.pathcontains_points 方法,另请参阅相关问题What's the fastest way of checking if a point is inside a polygon in python
  5. 我包含了一个动态更新(改编自How to obscure a line behind a surface plot in matplotlib?)。警告:具有大量多边形的表面的代码/绘图可能会变得非常缓慢。

这里是必要的代码/最小工作示例,表面由 OP 给出,样本点位于单位圆。

import matplotlib.pyplot as plt
import numpy as np
import copy
import matplotlib.path as mpltPath
from mpl_toolkits.mplot3d import proj3d
from matplotlib import cm


def clip_on_surface(surf,pts):
    ## Get projection of 3d surface onto 2d camera plane
    ## [Code form [mpl_toolkits/mplot3d/art3d.py -  Poly3DCollection._do_3d_projection(self, renderer=None)] to ]
    txs, tys, tzs = proj3d._proj_transform_vec(surf._vec, surf.axes.M)
    xyzlist = [(txs[sl], tys[sl], tzs[sl]) for sl in surf._segslices]
    cface = surf._facecolor3d
    cedge = surf._edgecolor3d
    if len(cface) != len(xyzlist):
        cface = cface.repeat(len(xyzlist), axis=0)
    if len(cedge) != len(xyzlist):
        if len(cedge) == 0:
            cedge = cface
        else:
            cedge = cedge.repeat(len(xyzlist), axis=0)
                    
    if xyzlist:
        # sort by depth (furthest drawn first)
        z_segments_2d = sorted(
            ((surf._zsortfunc(zs), np.column_stack([xs, ys]), fc, ec, idx)
              for idx, ((xs, ys, zs), fc, ec)
              in enumerate(zip(xyzlist, cface, cedge))),
            key=lambda x: x[0], reverse=True)
        
    # z_segments_2d = sorted(z_segments_2d,key=lambda x:x[4])    
    segments_zorder, segments_2d, facecolors2d, edgecolors2d, segments_idxs =  zip(*z_segments_2d)
    segments_paths = [mpltPath.Path(x) for x in segments_2d]
    
    ## Get polygons in 3d space
    xs, ys, zs = surf._vec[0:3,:]
    xyzlist = [(xs[sl], ys[sl], zs[sl]) for sl in surf._segslices]
    segments_3d=[]
    segments_3d_centroid=[]
    for q in xyzlist:    
        vertices = np.transpose(np.array([q[0],q[1],q[2]]))
        segments_3d.append( vertices )
        segments_3d_centroid.append( sum(list(vertices))/len(list(vertices)) ) # centroid of polygon (mean of vertices)
    
    ## Process points
    pts_info = [[0,0,True] for x in range(len(pts))] 
        # 0: index of closest 3d polygon
        # 1: index of closest 3d polygon in segments_idxs: drawing order
        # 2: True if visible (not overlapped by polygons drawn after associated polygon), False else
    
    pts_visible = copy.copy(pts) # visible points (invisible set to np.nan)
    pts_invisible = copy.copy(pts) # invisible points (visible set to np.nan)
    
  
    # compute pts_info[:,0] and  pts_info[:,1] -- index of closest 3d polygon and its position in segments_idxs
    for i in range(len(pts)):
        # Associate by distance
        dist = np.inf
        x=[pts[i][0],pts[i][1],pts[i][2]]
        for j in range(len(segments_3d_centroid)):
            yc=segments_3d_centroid[j]
            dist_tmp = np.sqrt( (x[0]-yc[0])**2 + (x[1]-yc[1])**2 + (x[2]-yc[2])**2 )
            if dist_tmp<dist:
                dist=dist_tmp
                pts_info[i][0]=j        
        pts_info[i][1] = segments_idxs.index( pts_info[i][0] )
    
    # compute projection of 3d points into 2d camera plane
    pts_2d_x, pts_2d_y, pts_2d_z = proj3d._proj_transform_vec(np.transpose(np.array([[x[0],x[1],x[2],1.0] for x in pts])), surf.axes.M) 
  
    # decide visibility    
    for i in range(len(pts_info)):
        for j in range(pts_info[i][1]+1,len(segments_paths)):
            b=segments_paths[j].contains_points( [[pts_2d_x[i],pts_2d_y[i]]] )
            if b==True:
                pts_info[i][2]=False
                break
        if pts_info[i][2]:
            pts_invisible[i][0]=np.nan
            pts_invisible[i][1]=np.nan
            pts_invisible[i][2]=np.nan
        else:
            pts_visible[i][0]=np.nan
            pts_visible[i][1]=np.nan
            pts_visible[i][2]=np.nan
            
            
    return { 'pts_visible': pts_visible, 'pts_invisible':pts_invisible, 'pts_info':pts_info }

def f(x, y):                        
    return np.sin(2*x) * np.cos(2*y)

fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.view_init(elev=30., azim=55.)

# Generate surface plot (surf)
xs = np.linspace(-2, 2, 25)
ys = np.linspace(-2, 2, 25)
Xs, Ys = np.meshgrid(xs, ys)
zs = np.array(f(np.ravel(Xs), np.ravel(Ys)))
Zs = zs.reshape(Xs.shape)

ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')

surf = ax.plot_surface(Xs, Ys, Zs, rstride=1, cstride=1, 
                        cmap=cm.get_cmap('viridis'),linewidth=0.0,edgecolor='black',
                        antialiased=True,rasterized=False)

# Generate pts on surf
t = np.linspace(0, 1, 200)
xp = np.sin(t*2*np.pi)
yp = np.cos(t*2*np.pi)
zp = f(xp,yp)
pts=np.transpose(np.array([xp,yp,zp]))


def rotate(event):
    if event.inaxes == ax:
        surf_pts=clip_on_surface(surf,pts)
        ax.plot(surf_pts['pts_visible'][:,0],surf_pts['pts_visible'][:,1],surf_pts['pts_visible'][:,2],'.', zorder=10,c='red',markersize=2)
        ax.plot(surf_pts['pts_invisible'][:,0],surf_pts['pts_invisible'][:,1],surf_pts['pts_invisible'][:,2],'.', zorder=10,c='green',markersize=2)
        
        fig.canvas.draw_idle()
        
c1 = fig.canvas.mpl_connect('motion_notify_event', rotate)
plt.show()

代码仍然有点混乱,它仍然不能完美运行,但这里有一些结果,表面上有 25*25=625 个四边形,单位圆上有 200 个点。 红色点是可见点,绿色点是不可见点(此处为说明目的而绘制,但为了最初的问题/问题,人们会诅咒从图中省略它们)。有些点应该清晰可见,但被检测为不可见。我还不确定那里出了什么问题,但对我来说,这种有限的未检测到并没有太大问题,因为我最终想绘制很多(任意密集)点的线/轨迹。如果未命中的不聚集,我可以忍受一些丢失的。

另一个固有的问题/限制是,当前的方法没有真正的概念,即点是在表面之上还是之下,这意味着从表面下方看时,表面之上/之上的点是可见的。这是此行为的示例:

这与 Andras Deak 已经提出的观点相关,即当前的问题在没有额外限制的情况下有些不明确或至少模棱两可。例如,可以要求将所有点放置在指向相机的表面上。在目前的方法中实现这一点是困难的。在几何方面,当前的实现将有限大小的球放置在无穷小的多边形上,使它们从两侧都可见(这在某些用例中实际上可能是可行的/理想的)。

代码仍在进行中,如果我发现重大改进,我可能会更新此答案。非常欢迎对一般方法和/或实施发表评论。我绝不是 python 专家(我几乎只将它用于绘图和相关的非常轻量级的数据处理),因此它们在代码性能和范围方面可能有很大的改进空间。

【讨论】:

  • 很好地尝试找到一个 matplotlib 解决方案。我是否正确理解您基本上必须重新实现部分渲染器?我实际上已经从 mayavi 切换到 pyvista,我可能应该用它来更新我的一半/答案:)
  • 我并没有真正重新实现渲染器,但我使用了它的一部分。我提取了表面上的元素被渲染的顺序,并根据这个顺序,我通过检查它是否被稍后渲染的表面元素覆盖来决定表面上的一个点是否应该是可见的。将这种方法推广到其他复合 3D 绘图的一种方法是将所有绘图元素拆分为尽可能小的子元素(多边形、点等),然后按照与相机的距离确定的顺序渲染所有这些元素。
  • 我明白了,谢谢你的解释。是的,这是有道理的:渲染错误来自复杂的表面,它们要么在彼此的后面,要么在彼此的前面。如果你把所有东西都分解成小的原语,问题就消失了。我想知道这方面的内存开销......但这听起来确实是一个合理的解决方法。
【解决方案3】:

从您的图表来看,您似乎愿意展示非线性优化器局部解决方案的路径,因此我认为您应该考虑在等高线图上绘制线框:

...

ax.scatter(xx, yy, zz, c='r', marker='o') ### 这将只绘制点,而不是线

ax.plot_surface(X, Y, Z, rstride=1, cstride=1,cmap='viridis', edgecolor='none')

ax.plot_wireframe(xx,yy,zz) ### 这将绘制线条和可能的点。

...

were (xx,yy,zz) 包含到达局部最大值的路径,如我所料,从非线性后悔法获得。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-07-15
    • 2019-11-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多