一旦你有了你的行星,使用^{}绘制轨迹就很容易了,所以我将集中精力用mayavi纹理映射一个行星到一个球体。(原则上,我们可以使用matplotlib来执行任务,但是性能和质量比mayavi差得多,请参见本文的结尾部分。)
好的场景:球体上的球体
事实证明,如果你想把一个球体参数化的图像映射到一个球体上,你必须把你的手弄脏一点,并使用一些裸vtk,但实际上有很少的工作要做,结果看起来很好。我要用Blue Marble image from NASA来演示。他们的自述说这些图片a geographic (Plate Carrée) projection, which is based on an equal latitude-
longitude grid spacing (not an equal area projection!)
在wikipedia中查找它,结果发现它也被称为equirectangular projection。换句话说,沿着x的像素直接对应于经度,y的像素直接对应于纬度。这就是我所说的“球参数化”。在
所以在这个例子中,我们可以使用一个低级的^{}来生成一个可以映射纹理的球体。我们自己构建球体网格可能会导致贴图中出现瑕疵(稍后将详细介绍)。在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()
结果正是我们所期望的:
不太好的场景:不是球体
不幸的是,我无法想出如何使用上述方法的非球面映射。此外,我们可能不想在完美球体上映射,而是在椭球体或类似的圆形对象上映射。对于这种情况,我们可能需要自己构建曲面,并尝试在其上进行纹理映射。剧透警报:不会那么漂亮。在
从手动生成的球体开始,我们可以像以前一样加载纹理,并处理由mlab.mesh构造的高级对象:
^{pr2}$
正如您在代码中看到的注释一样,这里有一些注意事项。第一种是球面映射模式出于某种原因翻转输入图像(这会导致地球反射)。因此,使用这种方法,我们首先要创建输入图像的翻转版本。每个图像只需执行一次,但我将相应的代码块放在上面函数的顶部。在
第二个警告是,如果纹理映射器的prevent_seam属性保留为默认值1,则映射从0到180方位角发生,球体的另一半将获得反射贴图。我们显然不想这样:我们想从0到360方位映射整个球体。碰巧,这个映射可能意味着我们在phi=0的映射中看到了一个接缝(不连续),即在映射的边缘。这是在可能的情况下使用第一种方法的另一个原因。总之,下面是结果,包含phi=0点(证明没有接缝):
柱面映射
上述球面映射的工作方式是,曲面上的每个点都通过空间中的一个给定点投影到一个球体上。对于第一个例子,这个点是原点,对于第二个例子,我们可以设置一个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插值轻松完成,例如使用^{}。对于特定的变换,您必须知道非球面投影的细节,但将其转换为球面投影应该不太困难,然后可以根据TexturedSphereSource的第一种情况使用它。在
附录:matplotlib
出于完整性的考虑,您可以使用matplotlib做任何您想做的事情,但这将需要更多的内存和CPU(请注意,您必须使用mayavi或matplotlib,但不能在一个图形中混合使用这两者)。其思想是定义一个与输入映射的像素相对应的网格,并将图像作为Axes3D.plot_surface的facecolors关键字参数传递。这种结构使得球体的分辨率与映射的分辨率直接耦合。我们只能使用少量的点来保持内存需求的可处理性,但是这样的结果看起来会很糟糕。不管怎样: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()
上面的count参数定义了贴图的下采样和渲染球体的相应大小。在上述180设置下,我们得到下图:
此外,matplotlib使用2d渲染器,这意味着对于复杂的3d对象,渲染通常会产生奇怪的伪影(特别是,扩展对象可以完全在另一个对象的前面或后面,因此联锁的几何体通常看起来是不完整的)。考虑到这些,我肯定会使用mayavi绘制一个有纹理的球体。(尽管matplotlib中的映射在曲面上逐面工作,因此可以直接应用于任意曲面。)