contourf в 3D-картопии

Мне нужна помощь в нанесении (переменного) количества заполненных контуров на трехмерный график. Проблема в том, что точки должны иметь правильную географическую привязку. У меня есть 2D-случай, работающий с использованием Cartopy, но нельзя просто использовать mpl_toolkits.mplot3d, так как можно передать только одну проекцию в метод figure().

Этот вопрос был полезен, но он сосредоточен в основном на построении шейп-файла, в то время как я иметь все точки и значения в каждой точке для использования при конструировании.

Этот вопрос также выглядел многообещающим, но не касается трехмерной оси.

У меня есть метод, работающий с использованием прямого mpl_toolkits.mplot3d, но он искажает данные, поскольку находится в неправильном CRS. Я бы использовал Basemap, но по какой-то причине он не очень хорошо обрабатывает проекции UTM.

Хотя это выглядит примерно так (график оказывается гораздо менее заметным, данные образуют линейные объекты, но это должно служить, чтобы дать представление о том, как это работает):

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d  Axes3D

the_data = {'grdx': range(0, 100),
            'grdy': range(0, 100),
            'grdz': [[np.random.rand(100) for ii in range(100)]
                       for jj in range(100)]}
data_heights = range(0, 300, 50)

fig = plt.figure(figsize=(17, 17))
ax = fig.add_subplot(111, projection='3d')
x = the_data['grdx']
y = the_data['grdy']
ii = 0
for height in data_heights:
    print(height)
    z = the_data['grdz'][ii]
    shape = np.shape(z)
    print(shape)
    flat = np.ravel(z)
    flat[np.isclose(flat, 0.5, 0.2)] = height
    flat[~(flat == height)] = np.nan
    z = np.reshape(flat, shape)
    print(z)
    ax.contourf(y, x, z, alpha=.35)
    ii += 1
plt.show()

Итак, как я могу сделать значения x и y для contourf() чем-то, что cartopy может обрабатывать в 3D?


person mtb-za    schedule 15.01.2018    source источник
comment
Ни картография, ни базовая карта не могут управлять трехмерными фигурами. Единственное решение - вычислить координаты до их построения на декартовом графике matplotlib 3D.   -  person ImportanceOfBeingErnest    schedule 15.01.2018
comment
Хорошо, так что я могу каким-то образом получить очки от GeoAxes и просто использовать их как мои grdx и grdy. Это не похоже на то, что встроено? scitools.org.uk/cartopy/docs/v0.13/ matplotlib / geoaxes.html по крайней мере не показывает очевидного метода.   -  person mtb-za    schedule 16.01.2018
comment
На самом деле я думаю, что решение не будет слишком отличаться от тот, на который вы ссылаетесь в том смысле, что вам нужно создать некоторый путь из заданных точек данных, используя cartopy crs.   -  person ImportanceOfBeingErnest    schedule 16.01.2018


Ответы (2)


Предостережения:

  1. 3D-материал в matplotlib часто упоминается как 2.5d, когда я говорю с основным сопровождающим (Беном Рутом, @weathergod на GitHub). Это должно дать вам представление о том, что есть несколько проблем с его способностью по-настоящему рендерить в 3D, и маловероятно, что matplotlib когда-либо сможет решить некоторые из этих проблем (например, художники, имеющие непостоянный z-порядок). Когда рендеринг работает, это довольно круто. Когда этого не происходит, с этим мало что можно сделать.
  2. В Cartopy и Basemap есть хаки, которые позволяют визуализировать в режиме 3D в matplotlib. Это действительно хаки - YMMV, и я полагаю, что это не то, что, вероятно, войдет в базовую карту или картопию.

После этого я взял свой ответ из Cartopy + Matplotlib (contourf) - Карта Переопределение данных, на которые вы ссылались и создавали их.

Поскольку вы хотите строить поверх контуров, я использовал подход, состоящий из двух экземпляров Axes (и двух фигур). Первая - это примитивные 2d (картографические) GeoAxes, вторая - это некартопические 3D оси. Прямо перед тем, как сделать plt.show (или сохранить файл), я просто закрываю 2d GeoAxes (с помощью plt.close(ax)).

Затем я использую тот факт, что возвращаемое значение из plt.contourf - это набор художников, из которых мы можем взять координаты и свойства (включая цвет) контуров.

Используя 2d координаты, которые генерируются контуром в 2d GeoAxes и коллекции contour, я вставляю размер z (уровень контура) в координаты и создаю Poly3DCollection.

Получается что-то вроде:

import cartopy.crs as ccrs
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
import numpy as np


def f(x,y):
    x, y = np.meshgrid(x, y)
    return (1 - x / 2 + x**5 + y**3 + x*y**2) * np.exp(-x**2 -y**2)

nx, ny = 256, 512
X = np.linspace(-180, 10, nx)
Y = np.linspace(-90, 90, ny)
Z = f(np.linspace(-3, 3, nx), np.linspace(-3, 3, ny))


fig = plt.figure()
ax3d = fig.add_axes([0, 0, 1, 1], projection='3d')

# Make an axes that we can use for mapping the data in 2d.
proj_ax = plt.figure().add_axes([0, 0, 1, 1], projection=ccrs.Mercator())
cs = proj_ax.contourf(X, Y, Z, transform=ccrs.PlateCarree(), alpha=0.4)


for zlev, collection in zip(cs.levels, cs.collections):
    paths = collection.get_paths()
    # Figure out the matplotlib transform to take us from the X, Y coordinates
    # to the projection coordinates.
    trans_to_proj = collection.get_transform() - proj_ax.transData

    paths = [trans_to_proj.transform_path(path) for path in paths]
    verts3d = [np.concatenate([path.vertices,
                               np.tile(zlev, [path.vertices.shape[0], 1])],
                              axis=1)
               for path in paths]
    codes = [path.codes for path in paths]
    pc = Poly3DCollection([])
    pc.set_verts_and_codes(verts3d, codes)

    # Copy all of the parameters from the contour (like colors) manually.
    # Ideally we would use update_from, but that also copies things like
    # the transform, and messes up the 3d plot.
    pc.set_facecolor(collection.get_facecolor())
    pc.set_edgecolor(collection.get_edgecolor())
    pc.set_alpha(collection.get_alpha())

    ax3d.add_collection3d(pc)

proj_ax.autoscale_view()

ax3d.set_xlim(*proj_ax.get_xlim())
ax3d.set_ylim(*proj_ax.get_ylim())
ax3d.set_zlim(Z.min(), Z.max())


plt.close(proj_ax.figure)
plt.show()

Контур с привязкой в ​​3D

Конечно, здесь мы можем сделать множество факторизации, а также добавить компонент с географической привязкой, о котором вы говорили (например, наличие береговых линий и т. Д.).

Обратите внимание, что, несмотря на то, что входные координаты - широта / долгота, координаты трехмерных осей совпадают с координатами системы координат Меркатора - это говорит нам, что мы на правильном пути в отношении преобразований, которые мы получаем для картографии.

Затем я беру код из упомянутого вами ответа, чтобы включить полигоны земли. 3D-оси matplotlib в настоящее время не имеют возможности обрезать полигоны, выходящие за пределы x / y, поэтому мне пришлось сделать это вручную.

Собираем все вместе:

import cartopy.crs as ccrs
import cartopy.feature
from cartopy.mpl.patch import geos_to_path

import itertools
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
from matplotlib.collections import PolyCollection
import numpy as np


def f(x,y):
    x, y = np.meshgrid(x, y)
    return (1 - x / 2 + x**5 + y**3 + x*y**2) * np.exp(-x**2 -y**2)

nx, ny = 256, 512
X = np.linspace(-180, 10, nx)
Y = np.linspace(-90, 90, ny)
Z = f(np.linspace(-3, 3, nx), np.linspace(-3, 3, ny))


fig = plt.figure()
ax3d = fig.add_axes([0, 0, 1, 1], projection='3d')

# Make an axes that we can use for mapping the data in 2d.
proj_ax = plt.figure().add_axes([0, 0, 1, 1], projection=ccrs.Mercator())
cs = proj_ax.contourf(X, Y, Z, transform=ccrs.PlateCarree(), alpha=0.4)


for zlev, collection in zip(cs.levels, cs.collections):
    paths = collection.get_paths()
    # Figure out the matplotlib transform to take us from the X, Y coordinates
    # to the projection coordinates.
    trans_to_proj = collection.get_transform() - proj_ax.transData

    paths = [trans_to_proj.transform_path(path) for path in paths]
    verts3d = [np.concatenate([path.vertices,
                               np.tile(zlev, [path.vertices.shape[0], 1])],
                              axis=1)
               for path in paths]
    codes = [path.codes for path in paths]
    pc = Poly3DCollection([])
    pc.set_verts_and_codes(verts3d, codes)

    # Copy all of the parameters from the contour (like colors) manually.
    # Ideally we would use update_from, but that also copies things like
    # the transform, and messes up the 3d plot.
    pc.set_facecolor(collection.get_facecolor())
    pc.set_edgecolor(collection.get_edgecolor())
    pc.set_alpha(collection.get_alpha())

    ax3d.add_collection3d(pc)

proj_ax.autoscale_view()

ax3d.set_xlim(*proj_ax.get_xlim())
ax3d.set_ylim(*proj_ax.get_ylim())
ax3d.set_zlim(Z.min(), Z.max())


# Now add coastlines.
concat = lambda iterable: list(itertools.chain.from_iterable(iterable))

target_projection = proj_ax.projection

feature = cartopy.feature.NaturalEarthFeature('physical', 'land', '110m')
geoms = feature.geometries()

# Use the convenience (private) method to get the extent as a shapely geometry.
boundary = proj_ax._get_extent_geom()

# Transform the geometries from PlateCarree into the desired projection.
geoms = [target_projection.project_geometry(geom, feature.crs)
         for geom in geoms]
# Clip the geometries based on the extent of the map (because mpl3d can't do it for us)
geoms = [boundary.intersection(geom) for geom in geoms]

# Convert the geometries to paths so we can use them in matplotlib.
paths = concat(geos_to_path(geom) for geom in geoms)
polys = concat(path.to_polygons() for path in paths)
lc = PolyCollection(polys, edgecolor='black',
                    facecolor='green', closed=True)
ax3d.add_collection3d(lc, zs=ax3d.get_zlim()[0])

plt.close(proj_ax.figure)
plt.show() 

3D-сюжет с геометрией со ссылками на картографию

Немного округляя это и абстрагируя несколько концепций от функций, мы делаем это довольно полезным:

import cartopy.crs as ccrs
import cartopy.feature
from cartopy.mpl.patch import geos_to_path
import itertools
import matplotlib.pyplot as plt
import mpl_toolkits.mplot3d
from matplotlib.collections import PolyCollection, LineCollection
import numpy as np


def add_contourf3d(ax, contour_set):
    proj_ax = contour_set.collections[0].axes
    for zlev, collection in zip(contour_set.levels, contour_set.collections):
        paths = collection.get_paths()
        # Figure out the matplotlib transform to take us from the X, Y
        # coordinates to the projection coordinates.
        trans_to_proj = collection.get_transform() - proj_ax.transData

        paths = [trans_to_proj.transform_path(path) for path in paths]
        verts = [path.vertices for path in paths]
        codes = [path.codes for path in paths]
        pc = PolyCollection([])
        pc.set_verts_and_codes(verts, codes)

        # Copy all of the parameters from the contour (like colors) manually.
        # Ideally we would use update_from, but that also copies things like
        # the transform, and messes up the 3d plot.
        pc.set_facecolor(collection.get_facecolor())
        pc.set_edgecolor(collection.get_edgecolor())
        pc.set_alpha(collection.get_alpha())

        ax3d.add_collection3d(pc, zs=zlev)

    # Update the limit of the 3d axes based on the limit of the axes that
    # produced the contour.
    proj_ax.autoscale_view()

    ax3d.set_xlim(*proj_ax.get_xlim())
    ax3d.set_ylim(*proj_ax.get_ylim())
    ax3d.set_zlim(Z.min(), Z.max())

def add_feature3d(ax, feature, clip_geom=None, zs=None):
    """
    Add the given feature to the given axes.
    """
    concat = lambda iterable: list(itertools.chain.from_iterable(iterable))

    target_projection = ax.projection
    geoms = list(feature.geometries())

    if target_projection != feature.crs:
        # Transform the geometries from the feature's CRS into the
        # desired projection.
        geoms = [target_projection.project_geometry(geom, feature.crs)
                 for geom in geoms]

    if clip_geom:
        # Clip the geometries based on the extent of the map (because mpl3d
        # can't do it for us)
        geoms = [geom.intersection(clip_geom) for geom in geoms]

    # Convert the geometries to paths so we can use them in matplotlib.
    paths = concat(geos_to_path(geom) for geom in geoms)

    # Bug: mpl3d can't handle edgecolor='face'
    kwargs = feature.kwargs
    if kwargs.get('edgecolor') == 'face':
        kwargs['edgecolor'] = kwargs['facecolor']

    polys = concat(path.to_polygons(closed_only=False) for path in paths)

    if kwargs.get('facecolor', 'none') == 'none':
        lc = LineCollection(polys, **kwargs)
    else:
        lc = PolyCollection(polys, closed=False, **kwargs)
    ax3d.add_collection3d(lc, zs=zs)

Который я использовал для создания следующего забавного трехмерного сюжета Робинсона:

def f(x, y):
    x, y = np.meshgrid(x, y)
    return (1 - x / 2 + x**5 + y**3 + x*y**2) * np.exp(-x**2 -y**2)


nx, ny = 256, 512
X = np.linspace(-180, 10, nx)
Y = np.linspace(-89, 89, ny)
Z = f(np.linspace(-3, 3, nx), np.linspace(-3, 3, ny))


fig = plt.figure()
ax3d = fig.add_axes([0, 0, 1, 1], projection='3d')

# Make an axes that we can use for mapping the data in 2d.
proj_ax = plt.figure().add_axes([0, 0, 1, 1], projection=ccrs.Robinson())
cs = proj_ax.contourf(X, Y, Z, transform=ccrs.PlateCarree(), alpha=1)

ax3d.projection = proj_ax.projection
add_contourf3d(ax3d, cs)

# Use the convenience (private) method to get the extent as a shapely geometry.
clip_geom = proj_ax._get_extent_geom().buffer(0)


zbase = ax3d.get_zlim()[0]
add_feature3d(ax3d, cartopy.feature.OCEAN, clip_geom, zs=zbase)
add_feature3d(ax3d, cartopy.feature.LAND, clip_geom, zs=zbase)
add_feature3d(ax3d, cartopy.feature.COASTLINE, clip_geom, zs=zbase)

# Put the outline (neatline) of the projection on.
outline = cartopy.feature.ShapelyFeature(
    [proj_ax.projection.boundary], proj_ax.projection,
    edgecolor='black', facecolor='none')
add_feature3d(ax3d, outline, clip_geom, zs=zbase)


# Close the intermediate (2d) figure
plt.close(proj_ax.figure)
plt.show()

Робинзон в 3D

Ответить на этот вопрос было очень весело и напомнило мне о некоторых внутренних функциях преобразования matplotlib и cartopy. Нет сомнений в том, что он может создавать полезные визуализации, но я лично не стал бы использовать его в производстве из-за проблем, присущих реализации 3d (2.5d) matplotlib.

HTH

person pelson    schedule 17.01.2018
comment
Это отличный ответ. Выполнение приведенного выше кода теперь вызывает эту ошибку: ОШИБКА: shapely.geos: TopologyException: ввод geom 0 недействителен: Самопересечение кольца в точке или рядом с ней -10552526.438744986 5741130.6863556514 at -10552526.438744986 5741130.6863556514 - person Rcameron; 05.09.2020
comment
.... TopologicalError: Операция «GEOSIntersection_r» не может быть выполнена. Вероятная причина - недействительность геометрии ‹объекта shapely.geometry.multipolygon.MultiPolygon по адресу 0x1dc9e3278› Есть идеи, как решить эту ошибку? - person Rcameron; 05.09.2020

В моей среде ошибка «GEOSIntersection_r» не могла быть выполнена. Вероятная причина - недействительность геометрии ‹shapely.geometry.multipolygon.MultiPolygon по адресу 0x1dc9e3278› была решена простым удалением тех, которые вызывают ошибку

geoms2 = []
for i in range(len(geoms)) :
    if geoms[i].is_valid :
        geoms2.append(geoms[i])
geoms = geoms2 

перед перекрестком. Пока результаты мне кажутся хорошими.

person k_reiji    schedule 12.01.2021