为两点之间的点以及旋转的地球设置动画

Animate a point between two points along with rotating earth

这是 的扩展,其中只有点在移动。

现在我也想移动地球以及动画点,使移动点始终位于中心。 例如:

现在我可以创建每个帧并将它们添加在一起以创建动画图像,使用此代码。

import matplotlib.pyplot as plt
import cartopy.crs as ccrs

lonlats = np.array([[-73.134961, 40.789142],  [-75.46884485, 41.13443837],
  [-77.825617, 41.43196017],  [-80.20222645, 41.68077343],
  [-82.5953765, 41.88007994],  [-85.00155934, 42.02922872],
  [-87.4170967, 42.12772575],  [-89.83818577, 42.17524151],
  [-92.26094893, 42.17161608],  [-94.68148587, 42.11686169],
  [-97.09592644, 42.01116249],  [-99.50048253, 41.8548717],
  [-101.89149735, 41.6485061],  [-104.26549065, 41.39273816],
  [-106.61919861, 41.08838607],  [-108.94960746, 40.73640202],
  [-111.25398017, 40.33785904],  [-113.52987601, 39.89393695],
  [-115.7751629, 39.40590768],  [-117.98802295, 38.87512048],
  [-120.16695169, 38.3029872],  [-122.3107517, 37.6909682]])

for frame in range(0, len(lonlats)):
  ax = plt.axes(projection=ccrs.Orthographic(central_longitude=lonlats[frame][0], central_latitude=30))
  ax.background_img(name='BM', resolution='medium')
  line = plt.plot(lonlats[:frame + 1, 0], lonlats[:frame + 1, 1], color='red', transform=ccrs.PlateCarree())[0]
  plt.savefig(f'img{frame:03}.png')
  #print(f'img{frame:03}.png')
  plt.close()

有什么方法可以在不保存图像的情况下在剧情中获得这个动画window?

在我们开始之前,重要的是要记住 cartopy 的强项是处理投影数据。无论正交投影看起来如何 3d,它实际上只是数据的 2D 表示 - matplotlib 和 cartopy 的 matplotlib 接口基本上以 2D space 运行,因此必须计算任何 3D 外观(在 CPU,而不是 GPU)用于每个视角。例如,如果将一些海岸线投影到正交投影,然后想要稍微旋转正交投影,则需要重新进行所有投影计算。在 3d 中做这样的事情(使用 OpenGL 之类的东西)是可以想象的,但是在 cartopy / matpltolib 中没有任何东西可以使它成为你可以现成使用的东西。

好吧,抛开警告,也许关于这个问题要注意的最重要的事情是:cartopy 的 GeoAxes 是为单一的、不可变的投影而设计的。有 no API 允许您在实例化后更改投影。因此,我们唯一能做的就是为每个旋转创建一个新的 GeoAxes。

实现这样的模式可能类似于:

def decorate_axes(ax):
    ax.coastlines()
    ...

def animate(i):
    ax = plt.gca()
    ax.remove()

    ax = plt.axes(projection=...)
    decorate_axes(ax)

ani = animation.FuncAnimation(
    plt.gcf(), animate,
    ...)

概念的快速证明:

import cartopy.crs as ccrs
import matplotlib.animation as animation
import matplotlib.pyplot as plt
import numpy as np

plt.figure(figsize=(6, 6))


def decorate_axes(ax):
    ax.set_global()
    ax.coastlines()


def animate(i):
    lon = i

    ax = plt.gca()
    ax.remove()

    ax = plt.axes([0, 0, 1, 1], projection=ccrs.Orthographic(
        central_latitude=0, central_longitude=lon))
    decorate_axes(ax)


ani = animation.FuncAnimation(
    plt.gcf(), animate,
    frames=np.linspace(0, 360, 40),
    interval=125, repeat=False)

ani.save('poc.gif', writer='imagemagick', dpi=plt.gcf().dpi)

现在我们已经掌握了基础知识,让我们在 的基础上使用上面开发的模式根据大圆的路径为正交投影的旋转设置动画...

import cartopy.crs as ccrs
import cartopy.feature as cfeature
import matplotlib.animation as animation
import matplotlib.image as mimage
import matplotlib.pyplot as plt
import matplotlib.transforms as mtransforms
import numpy as np
import shapely.geometry as sgeom


plt.figure(figsize=(6, 6))

line = sgeom.LineString([[0, 15], [-140, -40], [120, -20],
                         [0, -20], [-140, 15], [90, 45],
                         [0, 15]])


class HighResPC(ccrs.PlateCarree):
    @property
    def threshold(self):
        return super(HighResPC, self).threshold / 100


projected_line = HighResPC().project_geometry(line, ccrs.Geodetic())


verts = np.concatenate([np.array(l.coords) for l in projected_line])


def setup_axes(ax, x, y):
    ax.set_global()

    ax.add_feature(cfeature.LAND)
    ax.add_feature(cfeature.OCEAN)

    # Add the projected line to the map.
    ax.add_geometries(
        [projected_line], HighResPC(),
        edgecolor='blue', facecolor='none')

    # Image from http://madmen.wikia.com/wiki/File:Superman.gif.
    superman = plt.imread('superman.png')

    # Scale the actual image down a little.
    img_size = np.array(superman.shape) / 2

    x, y = ax.projection.transform_point(x, y, ccrs.PlateCarree())
    # Convert the projected coordinates into pixels.
    x_pix, y_pix = ax.transData.transform((x, y))

    # Make the extent handle the appropriate image size.
    extent = [x_pix - 0.5 * img_size[1], y_pix - 0.5 * img_size[0],
              x_pix + 0.5 * img_size[1], y_pix + 0.5 * img_size[0]]

    bbox = mtransforms.Bbox.from_extents(extent)
    img = mimage.BboxImage(bbox, zorder=10)
    img.set_data(superman)
    ax.add_artist(img)

    return img


def animate_superman(i):
    i = i % verts.shape[0]

    ax = plt.gca()
    ax.remove()

    ax = plt.axes([0, 0, 1, 1], projection=ccrs.Orthographic(
        central_latitude=verts[i, 1], central_longitude=verts[i, 0]))
    ax.coastlines()

    img = setup_axes(ax, verts[i, 0], verts[i, 1])


ani = animation.FuncAnimation(
    plt.gcf(), animate_superman,
    frames=verts.shape[0],
    interval=125, repeat=False)

ani.save('superman.gif', writer='imagemagick', dpi=plt.gcf().dpi)

我真正喜欢这个动画的地方在于,被跟踪的大圆圈在第一次出现在地球上时非常弯曲,一旦包含投影的中心点,它们就会变成直线。更一般地说,对于所有方位角投影(正交投影是其中之一),从中心点开始的大圆总是直线......维基百科指出:

therefore great circles through the central point are represented by straight lines on the map.

Source: https://en.wikipedia.org/wiki/Map_projection#Azimuthal_.28projections_onto_a_plane.29

这个动画有几个值得注意的问题:

  • 动画不是特别流畅:解决办法是用自定义投影(HighResPC)的阈值提高分辨率。
  • 地球没有以恒定速度旋转:解决方案可能是使用 geographiclib 工具根据特定 step-size(以米为单位)
  • 生成大圆
  • 动画不是特别快:虽然 cartopy 可以以合理的帧速率处理投影 low-resolution 海岸线,但图像转换的东西却没有(它实际上根本没有针对性能进行优化).因此,您将无法在接近交互式动画所需频率的任何地方执行类似 ax.stock_img() 的操作。