Ответ 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()
Это результат:
Обратите внимание, что восстановление значений z не совсем корректно, так как нам также нужно будет проверить, действительно ли вершина x, y является частью исходного набора данных, и в этом случае должно быть выполнено его исходное значение z. Однако было бы намного проще изменить код С++ алгоритма контура, чтобы отслеживать значения z. Это было бы небольшим изменением, хотя попытка охватить все случаи в Python - не что иное, как кошмар.
Что касается эффективности, мы пытаемся сделать работу с графической картой на уровне Python, так что это будет ужасно. Но это то же самое со всеми mplot3d
. Если вам нужна реализация производительности, я рекомендую BandedContourFilter()
из VTK. Это работает невероятно быстро и может быть использовано и на Python.