Построение траекторий легко, используя mayavi.mlab.plot3d
, когда у вас есть планета, поэтому я собираюсь сосредоточиться на отображении текстуры планеты на сфере с помощью майяви. (В принципе, мы можем выполнить задачу, используя matplotlib, но производительность и качество намного хуже по сравнению с Mayavi, см. Конец этого ответа.)
Хороший сценарий: шар на шаре
Оказывается, что если вы хотите отобразить сферически параметризованное изображение на сферу, вам нужно немного испачкать руку и использовать немного голого vtk, но на самом деле очень мало работы, и результат выглядит великолепно. Я собираюсь использовать изображение Blue Marble от NASA * 1009 * для демонстрации. Их readme говорит, что эти изображения имеют
географическая (Plate Carrée) проекция, основанная на равной широте
интервал сетки долготы (не равная площадь проекции!)
Глядя на это в википедии, выясняется, что это также известно как равносторонняя проекция . Другими словами, пиксели вдоль x
непосредственно соответствуют долготе, а пиксели вдоль y
непосредственно соответствуют широте. Это то, что я называю «сферически параметризованным».
Так что в этом случае мы можем использовать низкоуровневый TexturedSphereSource
, чтобы сгенерировать сферу, на которую можно наложить текстуру. Самостоятельное построение сферической сетки может привести к появлению артефактов в отображении (подробнее об этом позже).
Для работы на низком уровне vtk я переработал официальный пример, найденный здесь . Вот все, что нужно:
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
:
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
(демонстрирующую отсутствие шва):
Цилиндрическое отображение
Способ работы вышеуказанных сферических отображений состоит в том, что каждая точка на поверхности проецируется на сферу через заданную точку в пространстве. В первом примере эта точка является источником, во втором случае мы можем установить массив из 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 градусов и вертикальная проекция координаты. Более ранние замечания, касающиеся шва, остаются в силе.
Однако, если бы мне пришлось делать такое цилиндрическое отображение, я бы определенно попытался использовать первый метод. В худшем случае это означает, что мы должны преобразовать цилиндрически параметризованное отображение в сферически параметризованное. Опять же, это нужно сделать только один раз для каждой карты, и это можно легко сделать с помощью 2-мерной интерполяции, например, с помощью scipy.interpolate.RegularGridInterpolator
. Для конкретного преобразования вы должны знать специфику вашей несферической проекции, но не должно быть слишком сложно преобразовать ее в сферическую проекцию, которую вы затем можете использовать в соответствии с первым случаем с TexturedSphereSource
.
Приложение: matplotlib
Ради полноты вы можете делать то, что вы хотите, используя matplotlib, но это потребует намного больше памяти и ЦП (и учтите, что вы должны использовать либо mayavi, либо matplotlib, но вы не можете смешивать оба на рисунке) ). Идея состоит в том, чтобы определить меш, который соответствует пикселям входной карты, и передать изображение в качестве аргумента ключевого слова facecolors
для 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()
Параметр count
в приведенном выше примере определяет понижающую дискретизацию карты и соответствующий размер отображаемой сферы. С указанным выше значением 180 мы получаем следующий рисунок:
Кроме того, matplotlib использует 2-мерное средство визуализации, что подразумевает, что для сложных 3D-рендеринга объектов часто возникают странные артефакты (в частности, расширенные объекты могут быть либо полностью перед, либо позади друг друга, поэтому взаимосвязанные геометрии обычно выглядят нарушенными) , Учитывая это, я бы определенно использовал Mayavi для построения текстурированной сферы. (Хотя отображение в случае matplotlib работает лицом к лицу на поверхности, поэтому его можно напрямую применить к произвольным поверхностям.)