将图像映射到球体上并绘制3D轨迹

时间:2018-10-31 01:08:54

标签: python plot texture-mapping mayavi

我想做的是在3D坐标系的中心定义一个球体(半径= 1),将圆柱状行星贴图包裹在球体的表面上(即在球体上执行纹理贴图),然后绘制3D物体周围的轨迹(如卫星轨迹)。有什么办法可以使用matplotlib或mayavi做到这一点吗?

1 个答案:

答案 0 :(得分:14)

一旦有了行星,就可以使用mayavi.mlab.plot3d绘制轨迹,因此,我将集中精力使用mayavi将纹理映射到球体上。 (原则上,我们可以使用matplotlib执行任务,但是与mayavi相比,性能和质量要差得多,请参阅此答案的结尾。)

一个不错的场景:球体上的球体

事实证明,如果要将球形参数化的图像映射到球体,则必须使您的手变脏并使用一些裸露的vtk,但是实际上要做的工作很少,结果看起来不错。我将使用Blue Marble image from NASA进行演示。他们的自述说这些图片有

  

基于相等纬度的地理(PlateCarrée)投影-   经度网格间距(不是等面积投影!)

在Wikipedia中查找它,结果发现它也被称为equirectangular projection。换句话说,沿x的像素直接对应于经度,沿y的像素直接对应于纬度。这就是我所说的“球形参数化”。

因此,在这种情况下,我们可以使用低级TexturedSphereSource来生成可以将纹理映射到的球体。自己构造一个球体网格可能会导致映射中出现伪像(稍后会详细介绍)。

对于低级vtk工作,我进行了重做the official example found here。这就是所有的事情:

from mayavi import mlab
from tvtk.api import tvtk # python wrappers for the C++ vtk ecosystem

def auto_sphere(image_file):
    # create a figure window (and scene)
    fig = mlab.figure(size=(600, 600))

    # load and map the texture
    img = tvtk.JPEGReader()
    img.file_name = image_file
    texture = tvtk.Texture(input_connection=img.output_port, interpolate=1)
    # (interpolate for a less raster appearance when zoomed in)

    # use a TexturedSphereSource, a.k.a. getting our hands dirty
    R = 1
    Nrad = 180

    # create the sphere source with a given radius and angular resolution
    sphere = tvtk.TexturedSphereSource(radius=R, theta_resolution=Nrad,
                                       phi_resolution=Nrad)

    # assemble rest of the pipeline, assign texture    
    sphere_mapper = tvtk.PolyDataMapper(input_connection=sphere.output_port)
    sphere_actor = tvtk.Actor(mapper=sphere_mapper, texture=texture)
    fig.scene.add_actor(sphere_actor)


if __name__ == "__main__":
    image_file = 'blue_marble_spherical.jpg'
    auto_sphere(image_file)
    mlab.show()

结果正是我们所期望的:

blue marble mapped on a sphere

不太好的场景:不是球体

不幸的是,我无法弄清楚如何通过上述方法使用非球形映射。此外,可能会发生这样的情况,我们不想在完美的球体上进行映射,而是在椭圆形或类似的圆形物体上进行映射。对于这种情况,我们可能必须自己构造表面并尝试在其上进行纹理贴图。剧透警报:不会那么漂亮。

从手动生成的球体开始,我们可以以与以前相同的方式加载纹理,并使用由mlab.mesh构造的高级对象:

import numpy as np
from mayavi import mlab
from tvtk.api import tvtk
import matplotlib.pyplot as plt # only for manipulating the input image

def manual_sphere(image_file):
    # caveat 1: flip the input image along its first axis
    img = plt.imread(image_file) # shape (N,M,3), flip along first dim
    outfile = image_file.replace('.jpg', '_flipped.jpg')
    # flip output along first dim to get right chirality of the mapping
    img = img[::-1,...]
    plt.imsave(outfile, img)
    image_file = outfile  # work with the flipped file from now on

    # parameters for the sphere
    R = 1 # radius of the sphere
    Nrad = 180 # points along theta and phi
    phi = np.linspace(0, 2 * np.pi, Nrad)  # shape (Nrad,)
    theta = np.linspace(0, np.pi, Nrad)    # shape (Nrad,)
    phigrid,thetagrid = np.meshgrid(phi, theta) # shapes (Nrad, Nrad)

    # compute actual points on the sphere
    x = R * np.sin(thetagrid) * np.cos(phigrid)
    y = R * np.sin(thetagrid) * np.sin(phigrid)
    z = R * np.cos(thetagrid)

    # create figure
    mlab.figure(size=(600, 600))

    # create meshed sphere
    mesh = mlab.mesh(x,y,z)
    mesh.actor.actor.mapper.scalar_visibility = False
    mesh.actor.enable_texture = True  # probably redundant assigning the texture later

    # load the (flipped) image for texturing
    img = tvtk.JPEGReader(file_name=image_file)
    texture = tvtk.Texture(input_connection=img.output_port, interpolate=0, repeat=0)
    mesh.actor.actor.texture = texture

    # tell mayavi that the mapping from points to pixels happens via a sphere
    mesh.actor.tcoord_generator_mode = 'sphere' # map is already given for a spherical mapping
    cylinder_mapper = mesh.actor.tcoord_generator
    # caveat 2: if prevent_seam is 1 (default), half the image is used to map half the sphere
    cylinder_mapper.prevent_seam = 0 # use 360 degrees, might cause seam but no fake data
    #cylinder_mapper.center = np.array([0,0,0])  # set non-trivial center for the mapping sphere if necessary

正如您在代码中看到的注释一样,有一些警告。第一个是球面贴图模式出于某种原因翻转输入图像(这会导致地球反射)。因此,使用此方法,我们首先必须创建输入图像的翻转版本。每个图像只需要执行一次,但是我在上面的函数顶部保留了相应的代码块。

第二个警告是,如果纹理映射器的prevent_seam属性保留在默认值1上,则映射发生在0到180方位角之间,并且球体的另一半得到了反射映射。我们显然不希望这样:我们想将整个球体从0​​方位角映射到360方位角。碰巧的是,此映射可能暗示我们在phi=0(即地图的边缘)处的映射中看到接缝(不连续)。这是为什么应尽可能使用第一种方法的另一个原因。无论如何,这是包含phi=0点的结果(表明没有接缝):

meshed version

圆柱映射

以上球面贴图的工作方式是通过空间上的给定点将表面上的每个点投影到球体上。对于第一个示例,该点是原点,对于第二种情况,我们可以将一个3长度的数组设置为cylinder_mapper.center的值,以便映射到非以原点为中心的球体。

现在,您的问题提到了圆柱映射。原则上,我们可以使用第二种方法做到这一点:

mesh.actor.tcoord_generator_mode = 'cylinder'
cylinder_mapper = mesh.actor.tcoord_generator
cylinder_mapper.automatic_cylinder_generation = 0 # use manual cylinder from points
cylinder_mapper.point1 = np.array([0,0,-R])
cylinder_mapper.point2 = np.array([0,0,R])
cylinder_mapper.prevent_seam = 0 # use 360 degrees, causes seam but no fake data

这会将球形映射更改为圆柱映射。它根据设置圆柱体的轴和范围的两个点([0,0,-R][0,0,R])定义投影。每个点均根据其圆柱坐标(phi,z)进行映射:0到360度的方位角和坐标的垂直投影。先前有关接缝的说明仍然适用。

但是,如果我必须进行这样的圆柱映射,那么我肯定会尝试使用第一种方法。在最坏的情况下,这意味着我们必须将圆柱参数化的贴图转换为球面参数化的贴图。同样,每个地图只需要做一次,并且可以使用2d插值轻松完成,例如使用scipy.interpolate.RegularGridInterpolator。对于特定的转换,您必须了解非球面投影的细节,但是将其转换为球面投影并不难,然后可以根据第一种情况使用TexturedSphereSource进行使用。

附录:matplotlib

为了完整起见,您可以使用matplotlib来做您想做的事,但是会占用更多的内存和CPU(请注意,您必须使用mayavi或matplotlib,但不能在图形中将两者混用)。这个想法是定义一个与输入图的像素相对应的网格,并将图像作为facecolors的{​​{1}}关键字参数传递。这种构造使得球体的分辨率直接与映射的分辨率耦合。我们只能使用少量的点来使内存需求易于处理,但是结果看起来将很难像素化。无论如何:

Axes3D.plot_surface

上面的import numpy as np import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D def mpl_sphere(image_file): img = plt.imread(image_file) # define a grid matching the map size, subsample along with pixels theta = np.linspace(0, np.pi, img.shape[0]) phi = np.linspace(0, 2*np.pi, img.shape[1]) count = 180 # keep 180 points along theta and phi theta_inds = np.linspace(0, img.shape[0] - 1, count).round().astype(int) phi_inds = np.linspace(0, img.shape[1] - 1, count).round().astype(int) theta = theta[theta_inds] phi = phi[phi_inds] img = img[np.ix_(theta_inds, phi_inds)] theta,phi = np.meshgrid(theta, phi) R = 1 # sphere x = R * np.sin(theta) * np.cos(phi) y = R * np.sin(theta) * np.sin(phi) z = R * np.cos(theta) # create 3d Axes fig = plt.figure() ax = fig.add_subplot(111, projection='3d') ax.plot_surface(x.T, y.T, z.T, facecolors=img/255, cstride=1, rstride=1) # we've already pruned ourselves # make the plot more spherical ax.axis('scaled') if __name__ == "__main__": image_file = 'blue_marble.jpg' mpl_sphere(image_file) plt.show() 参数定义了地图的下采样和渲染球体的相应大小。通过上面的180设置,我们得到下图:

matplotlib version

此外,matplotlib使用2d渲染器,这意味着对于复杂的3d对象,渲染通常会以怪异的伪像结束(特别是,扩展的对象可以完全位于彼此之间,也可以彼此之间互斥,因此互锁的几何图形通常看起来是破碎的) 。考虑到这些,我肯定会使用mayavi绘制纹理球体。 (尽管在matplotlib情况下,映射在表面上并排工作,因此可以直接应用于任意表面。)