Интерполируя трехмерную поверхность, известную своими угловыми узлами, и раскрашивая ее цветовой схемой

Я хочу построить трехмерное представление экспериментальных данных для отслеживания деформации мембраны. Экспериментально известны только угловые узлы. Однако я хочу построить деформирование общей структуры, и поэтому я хочу, чтобы интерполировать мембрану, чтобы обеспечить хорошую цветопередачу. Побывав вокруг, я приблизился к нему со следующим кодом:

import numpy
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
import matplotlib.pyplot as plt
from matplotlib import cm
from scipy.interpolate import griddata

x=numpy.array([0, 0, 1, 1])
y=numpy.array([0.5, 0.75, 1, 0.5])
z=numpy.array([0, 0.5, 1,0])

fig = plt.figure()
ax = Axes3D(fig)
verts = [zip(x, y, z)]
PC = Poly3DCollection(verts)
ax.add_collection3d(PC)

xi = numpy.linspace(x.min(),x.max(),20)
yi = numpy.linspace(y.min(),y.max(),20)
zi = griddata((x,y),z, (xi[None,:], yi[:,None]), method='linear')
xig, yig = numpy.meshgrid(xi, -yi)
ax.plot_surface(xig, yig, zi, rstride=1, cstride=1,  linewidth=0,cmap=plt.cm.jet,norm=plt.Normalize(vmax=abs(yi).max(), vmin=-abs(yi).max()))
plt.show()

и получить следующий график:

enter image description here

Синий многоугольник - это поверхность, известная своими угловыми узлами, и что я хочу установить цвет. До сих пор поверхность с кодовым покрытием - мой лучший результат. Тем не менее, есть черные полигоны у верхней части поверхности, которые беспокоят меня. Я думаю, что это может быть связано с тем, что поверхность не соответствует мешгриду, и поэтому четвертый угол здесь Нан.

Есть ли обходной путь, чтобы избежать этих черных треугольников или даже лучше лучший способ цветокоррекции поверхности, известной только ее угловыми узлами?

EDIT: вот цифра с решением триангуляции, приведенная в моем первом комментарии, используя следующую команду

triang = tri.Triangulation(x, y)
ax.plot_trisurf(x, y, z, triangles=triang.triangles, cmap=cm.jet,norm=plt.Normalize(vmax=abs(yi).max(), vmin=-abs(yi).max()))

enter image description here

Ответы

Ответ 1

Вопрос сводится к тому, как сделать интерполированное затенение поверхности в matplotlib, т.е. эквивалент функции Matlab shading('interp'). Короткий ответ: вы не можете. Он не поддерживается изначально, поэтому лучше всего надеяться - это сделать это вручную, на что нацелены решения, представленные до сих пор.

Я пошел по этой дороге несколько лет назад, когда меня тоже расстроило Matlab shading('interp'): он работает, просто интерполируя 4 угловых цвета на каждом четырехугольниках, что означает, что направление градиента цвета может быть разные на соседних четырехугольниках. Я хотел, чтобы каждая цветовая полоса была точно между двумя четко определенными значениями на оси z, без визуальных разрывов между соседними ячейками.

Работа над триангуляцией - это, безусловно, правильная идея. Но вместо того, чтобы просто рафинировать сетку и надеяться достичь точки, где цвета соседних треугольников визуально неотличимы (не доходя до точки, где появляются артефакты), мой подход состоял в том, чтобы рассчитать контурные полосы триангуляции, а затем построить их в 3D.

Когда я впервые реализовал это, matplotlib не поддерживал контур по триангуляции. Теперь это происходит через _tri.TriContourGenerator. Если бы это также обеспечивало значения z извлеченных вершин полигона, мы бы это сделали. К сожалению, они недоступны на уровне Python, поэтому нам нужно попытаться их восстановить, сравнив выходы create_filled_contours() и create_contours(), которые выполняются в следующем коде:

import numpy as np
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
import matplotlib.pyplot as plt
from matplotlib import _tri, tri, cm

def contour_bands_3d(x, y, z, nbands=20):
    # obtain the contouring engine on a triangulation
    TRI = tri.Triangulation(x, y)
    C = _tri.TriContourGenerator(TRI.get_cpp_triangulation(), z)

    # define the band breaks
    brks = np.linspace(z.min(), z.max(), nbands+1)

    # the contour lines
    lines = [C.create_contour(b) for b in brks]

    # the contour bands
    bands = [C.create_filled_contour(brks[i], brks[i+1]) for i in xrange(nbands)]

    # compare the x, y vertices of each band with the x, y vertices of the upper
    # contour line; if matching, z = z1, otherwise z = z0 (see text for caveats)
    eps = 1e-6
    verts = []
    for i in xrange(nbands):
        b = bands[i][0]
        l = lines[i+1][0]
        z0, z1 = brks[i:i+2]
        zi = np.array([z1 if (np.abs(bb - l) < eps).all(1).any() else z0 for bb in b])
        verts.append(np.c_[b, zi[:,None]])
    return brks, verts

x = np.array([0, 0, 1, 1])
y = np.array([0.5, 0.75, 1, 0.5])
z = np.array([0, 0.5, 1,0])

fig = plt.figure()
ax = Axes3D(fig)
verts = [zip(x, y, z)]
PC = Poly3DCollection(verts)
ax.add_collection3d(PC)

# calculate the 3d contour bands
brks, verts = contour_bands_3d(x, -y, z)

cmap = cm.get_cmap('jet')
norm = plt.Normalize(vmax=abs(y).max(), vmin=-abs(y).max())

PC = Poly3DCollection(verts, cmap=cmap, norm=norm, edgecolors='none')
PC.set_array(brks[:-1])
ax.add_collection(PC)
ax.set_ylim((-1, 1))
plt.show()

Это результат:

Membrane with banded contours

Обратите внимание, что восстановление значений z не совсем корректно, так как нам также нужно будет проверить, действительно ли вершина x, y является частью исходного набора данных, и в этом случае должно быть выполнено его исходное значение z. Однако было бы намного проще изменить код С++ алгоритма контура, чтобы отслеживать значения z. Это было бы небольшим изменением, хотя попытка охватить все случаи в Python - не что иное, как кошмар.

Что касается эффективности, мы пытаемся сделать работу с графической картой на уровне Python, так что это будет ужасно. Но это то же самое со всеми mplot3d. Если вам нужна реализация производительности, я рекомендую BandedContourFilter() из VTK. Это работает невероятно быстро и может быть использовано и на Python.

Ответ 2

Действительно, кажется, что plot_trisurf должно быть идеальным для этой задачи! Кроме того, вы можете использовать tri.UniformTriRefiner для получения Triangulation с меньшими треугольниками:

import numpy
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
from matplotlib import tri, cm

x = numpy.array([0, 0, 1, 1])
y = numpy.array([0.5, 0.75, 1, 0.5])
z = numpy.array([0, 0.5, 1, 0])

triang = tri.Triangulation(x, y)
refiner = tri.UniformTriRefiner(triang)
new, new_z = refiner.refine_field(z, subdiv=4)

norm = plt.Normalize(vmax=abs(y).max(), vmin=-abs(y).max())
kwargs = dict(triangles=new.triangles, cmap=cm.jet, norm=norm, linewidth=0.2)

fig = plt.figure()
ax = Axes3D(fig)
pt = ax.plot_trisurf(new.x, new.y, new_z, **kwargs)
plt.show()

Результат на следующем изображении:

enter image description here

Уточнение треугольной сетки только недавно добавлено в matplotlib, поэтому вам понадобится версия 1.3 для ее использования. Хотя, если вы застряли с версией 1.2, вы также можете напрямую использовать источник из Github, если вы закомментируете строку import matplotlib.tri.triinterpolate и весь метод refine_field. Затем вам нужно использовать метод refine_triangulation и использовать griddata для интерполяции новых соответствующих Z-значений.


Изменить: В приведенном выше коде используется кубическая интерполяция для определения значений Z для новых треугольников, но для линейной интерполяции вы можете подставить/добавить эти строки:

interpolator = tri.LinearTriInterpolator(triang, z)
new, new_z = refiner.refine_field(z, interpolator, subdiv=4)

В качестве альтернативы сделать интерполяцию с помощью scipy.interpolate.griddata:

from scipy.interpolate import griddata

new = refiner.refine_triangulation(subdiv = 4)
new_z = griddata((x,y),z, (new.x, new.y), method='linear')