首页 > 解决方案 > 动画两点之间的点以及旋转的地球

问题描述

这是对这个问题的扩展,只有点在移动。

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

用动画路径旋转地球

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

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()

有没有办法只在绘图窗口中获取此动画而不保存图像?

标签: python-3.xmatplotlibcartopy

解决方案


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

好的,除此之外,关于这个问题需要注意的最重要的一点可能是:cartopy 的 GeoAxes 是为单一的、不可变的投影而设计的。一旦实例化,没有 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)

使用 cartopy 的动画旋转地球

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

因此,通过中心点的大圆圈在地图上用直线表示。

资料来源:https ://en.wikipedia.org/wiki/Map_projection#Azimuthal_.28projections_onto_a_plane.29

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

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

推荐阅读