Нанесите изображение на сферу и нанесите трехмерные траектории - PullRequest
0 голосов
/ 31 октября 2018

То, что я хотел бы сделать, это определить сферу в центре моей трехмерной системы координат (с радиусом = 1), обернуть цилиндрическую карту планеты на поверхность сферы (т.е. выполнить наложение текстуры на сфере) и построить 3D траектории вокруг объекта (например, спутниковые траектории). Есть ли способ сделать это с помощью matplotlib или mayavi?

1 Ответ

0 голосов
/ 05 ноября 2018

Построение траекторий легко, используя 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()

Результат - именно то, что мы ожидаем:

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 градусов и вертикальная проекция координаты. Более ранние замечания, касающиеся шва, остаются в силе.

Однако, если бы мне пришлось делать такое цилиндрическое отображение, я бы определенно попытался использовать первый метод. В худшем случае это означает, что мы должны преобразовать цилиндрически параметризованное отображение в сферически параметризованное. Опять же, это нужно сделать только один раз для каждой карты, и это можно легко сделать с помощью 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 version

Кроме того, matplotlib использует 2-мерное средство визуализации, что подразумевает, что для сложных 3D-рендеринга объектов часто возникают странные артефакты (в частности, расширенные объекты могут быть либо полностью перед, либо позади друг друга, поэтому взаимосвязанные геометрии обычно выглядят нарушенными) , Учитывая это, я бы определенно использовал Mayavi для построения текстурированной сферы. (Хотя отображение в случае matplotlib работает лицом к лицу на поверхности, поэтому его можно напрямую применить к произвольным поверхностям.)

...