Хобрук: Ваш путь к мастерству в программировании

Постройте трехмерные выпуклые замкнутые области в библиотеке matplot

Я пытаюсь построить в 3D многогранник, определяемый набором неравенств. По сути, я пытаюсь воспроизвести функциональность этого матлаба plotregion библиотека в matplotlib.

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

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

Есть ли какой-нибудь простой способ просто напечатать «внешние» ребра многогранника без необходимости консолидировать самостоятельно, один за другим, все копланарные симплексы?

Спасибо

from scipy.spatial import HalfspaceIntersection
from scipy.spatial import ConvexHull
import scipy as sp
import numpy as np
import matplotlib.pyplot as plt
import mpl_toolkits.mplot3d as a3
import matplotlib.colors as colors


w = np.array([1., 1., 1.])


# ∑ᵢ hᵢ wᵢ qᵢ - ∑ᵢ gᵢ wᵢ <= 0 
#  qᵢ - ubᵢ <= 0
# -qᵢ + lbᵢ <= 0 
halfspaces = np.array([
                    [1.*w[0], 1.*w[1], 1.*w[2], -10 ],
                    [ 1.,  0.,  0., -4],
                    [ 0.,  1.,  0., -4],
                    [ 0.,  0.,  1., -4],
                    [-1.,  0.,  0.,  0],
                    [ 0., -1.,  0.,  0],
                    [ 0.,  0., -1.,  0]
                    ])
feasible_point = np.array([0.1, 0.1, 0.1])
hs = HalfspaceIntersection(halfspaces, feasible_point)
verts = hs.intersections
hull = ConvexHull(verts)
faces = hull.simplices

ax = a3.Axes3D(plt.figure())
ax.dist=10
ax.azim=30
ax.elev=10
ax.set_xlim([0,5])
ax.set_ylim([0,5])
ax.set_zlim([0,5])

for s in faces:
    sq = [
        [verts[s[0], 0], verts[s[0], 1], verts[s[0], 2]],
        [verts[s[1], 0], verts[s[1], 1], verts[s[1], 2]],
        [verts[s[2], 0], verts[s[2], 1], verts[s[2], 2]]
    ]

    f = a3.art3d.Poly3DCollection([sq])
    f.set_color(colors.rgb2hex(sp.rand(3)))
    f.set_edgecolor('k')
    f.set_alpha(0.1)
    ax.add_collection3d(f)

plt.show()

Результат приведенного выше кода


Ответы:


1

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

введите описание изображения здесь

import numpy as np
from sympy import Plane, Point3D
import networkx as nx


def simplify(triangles):
    """
    Simplify an iterable of triangles such that adjacent and coplanar triangles form a single face.
    Each triangle is a set of 3 points in 3D space.
    """

    # create a graph in which nodes represent triangles;
    # nodes are connected if the corresponding triangles are adjacent and coplanar
    G = nx.Graph()
    G.add_nodes_from(range(len(triangles)))
    for ii, a in enumerate(triangles):
        for jj, b in enumerate(triangles):
            if (ii < jj): # test relationships only in one way as adjacency and co-planarity are bijective
                if is_adjacent(a, b):
                    if is_coplanar(a, b, np.pi / 180.):
                        G.add_edge(ii,jj)

    # triangles that belong to a connected component can be combined
    components = list(nx.connected_components(G))
    simplified = [set(flatten(triangles[index] for index in component)) for component in components]

    # need to reorder nodes so that patches are plotted correctly
    reordered = [reorder(face) for face in simplified]

    return reordered


def is_adjacent(a, b):
    return len(set(a) & set(b)) == 2 # i.e. triangles share 2 points and hence a side


def is_coplanar(a, b, tolerance_in_radians=0):
    a1, a2, a3 = a
    b1, b2, b3 = b
    plane_a = Plane(Point3D(a1), Point3D(a2), Point3D(a3))
    plane_b = Plane(Point3D(b1), Point3D(b2), Point3D(b3))
    if not tolerance_in_radians: # only accept exact results
        return plane_a.is_coplanar(plane_b)
    else:
        angle = plane_a.angle_between(plane_b).evalf()
        angle %= np.pi # make sure that angle is between 0 and np.pi
        return (angle - tolerance_in_radians <= 0.) or \
            ((np.pi - angle) - tolerance_in_radians <= 0.)


flatten = lambda l: [item for sublist in l for item in sublist]


def reorder(vertices):
    """
    Reorder nodes such that the resulting path corresponds to the "hull" of the set of points.

    Note:
    -----
    Not tested on edge cases, and likely to break.
    Probably only works for convex shapes.

    """
    if len(vertices) <= 3: # just a triangle
        return vertices
    else:
        # take random vertex (here simply the first)
        reordered = [vertices.pop()]
        # get next closest vertex that is not yet reordered
        # repeat until only one vertex remains in original list
        vertices = list(vertices)
        while len(vertices) > 1:
            idx = np.argmin(get_distance(reordered[-1], vertices))
            v = vertices.pop(idx)
            reordered.append(v)
        # add remaining vertex to output
        reordered += vertices
        return reordered


def get_distance(v1, v2):
    v2 = np.array(list(v2))
    difference = v2 - v1
    ssd = np.sum(difference**2, axis=1)
    return np.sqrt(ssd)

Применительно к вашему примеру:

from scipy.spatial import HalfspaceIntersection
from scipy.spatial import ConvexHull
import scipy as sp
import numpy as np
import matplotlib.pyplot as plt
import mpl_toolkits.mplot3d as a3
import matplotlib.colors as colors


w = np.array([1., 1., 1.])


# ∑ᵢ hᵢ wᵢ qᵢ - ∑ᵢ gᵢ wᵢ <= 0
#  qᵢ - ubᵢ <= 0
# -qᵢ + lbᵢ <= 0
halfspaces = np.array([
                    [1.*w[0], 1.*w[1], 1.*w[2], -10 ],
                    [ 1.,  0.,  0., -4],
                    [ 0.,  1.,  0., -4],
                    [ 0.,  0.,  1., -4],
                    [-1.,  0.,  0.,  0],
                    [ 0., -1.,  0.,  0],
                    [ 0.,  0., -1.,  0]
                    ])
feasible_point = np.array([0.1, 0.1, 0.1])
hs = HalfspaceIntersection(halfspaces, feasible_point)
verts = hs.intersections
hull = ConvexHull(verts)
faces = hull.simplices

ax = a3.Axes3D(plt.figure())
ax.dist=10
ax.azim=30
ax.elev=10
ax.set_xlim([0,5])
ax.set_ylim([0,5])
ax.set_zlim([0,5])

triangles = []
for s in faces:
    sq = [
        (verts[s[0], 0], verts[s[0], 1], verts[s[0], 2]),
        (verts[s[1], 0], verts[s[1], 1], verts[s[1], 2]),
        (verts[s[2], 0], verts[s[2], 1], verts[s[2], 2])
    ]
    triangles.append(sq)

new_faces = simplify(triangles)
for sq in new_faces:
    f = a3.art3d.Poly3DCollection([sq])
    f.set_color(colors.rgb2hex(sp.rand(3)))
    f.set_edgecolor('k')
    f.set_alpha(0.1)
    ax.add_collection3d(f)

# # rotate the axes and update
# for angle in range(0, 360):
#     ax.view_init(30, angle)
#     plt.draw()
#     plt.pause(.001)

Примечание

Если подумать, функция reordered, вероятно, нуждается в доработке. Почти уверен, что это сломается для странных/невыпуклых форм, и я даже не уверен на 100%, что это всегда будет работать для выпуклых форм. Хотя отдых должен быть в порядке.

05.03.2018
  • Я думаю, не слишком сложно это эвфемизм здесь? Что еще потребовалось бы для двух дополнительных пакетов и еще большего количества строк кода, чем первоначальная проблема, состоящая из того, что можно было бы квалифицировать? ;-) Я пробовал что-то подобное вчера, но без nx и сдался после того, как заблудился в 5-м подцикле или около того. 05.03.2018
  • Ух ты! Большое спасибо за ваши усилия, люди! Не эксперт в matplotlib, но я бы счел функциональность, описанную в этом разделе, очень полезной (по крайней мере, достаточно полезной), чтобы гарантировать себе место в библиотеке. Может стоит пропинговать разработчиков? Еще раз спасибо! 05.03.2018
  • ДА! Моя жизнь наполнена! Я победил там, где @ImportanceOfBeingErnest попал в тупик. Я могу отдохнуть сейчас. ;-) А если серьезно, то is_coplanar был бы намного короче, если бы не ошибки округления, а переупорядочивание узлов для получения выпуклого патча составляет почти половину оставшихся строк. Аккуратная часть с подключенными компонентами довольно короткая. 06.03.2018
  • @nikferrari: решение зависит от networkx и sympy. Я почти уверен, что это слишком тяжелые зависимости для команды matplotlib, чтобы рассмотреть вопрос о включении в основную ветку matplotlib. Заменить is_coplanar и, таким образом, избавиться от библиотеки sympy, вероятно, довольно просто (я просто не доверял своей математике в тот вечер); Однако реализация структуры графа и поиск связанных компонентов — это как минимум страница кода. 06.03.2018
  • На самом деле при ближайшем рассмотрении может быть не так уж и плохо. В scipy.sparse уже есть реализация подключенных компонентов. Однако reorder все еще нужно сделать надежным, и в данный момент у меня нет хорошей идеи. Любые предложения, @ImportanceOfBeingErnest (пока мы привлекаем ваше внимание)? 06.03.2018
  • Я действительно задавался вопросом, почему reorder вообще работает так, как есть, поскольку это кажется довольно произвольным. Когда я думал об этом вчера, у меня возникла идея использовать еще один convexhull, потому что каждая грань лежит в 2D-плоскости, а выпуклая оболочка дает упорядоченный набор точек в 2D. Но для этого требуется проецировать вперед и назад между 3D-точками и 2D-точками. Это то, что вы имели ввиду? 06.03.2018
  • Что касается того, что это часть matplotlib, я не думаю, что это достаточно общее, чтобы его можно было использовать. Что может быть полезным, так это расширение plot_trisurf, где соединяются копланарные треугольники (это также решило бы этот случай здесь, но было бы применимо ко многим другим случаям использования). Я сомневаюсь, что кто-то приложит к этому какие-либо усилия, matplotlib 3D на данный момент довольно мертв, но если есть запрос на его реализацию, это может приветствоваться. 06.03.2018
  • Итак, какую библиотеку люди используют для 3D-графики с помощью Python? Майави? 06.03.2018
  • @ImportanceOfBeingErnest: я могу что-то упустить, но выпуклая оболочка не будет работать, так как даже два треугольника не обязательно образуют выпуклый четырехугольник (это слово на английском языке?). Однако спасибо за все предложения; Я посмотрю на plot_trisurf. 06.03.2018
  • @Paul Я предполагаю, что предположение, которое я сделал здесь, заключается в том, что грани от пересечения полупространства всегда выпуклые. В случае, если это предположение не оправдано, подход действительно потерпит неудачу. Я не уверен в данный момент. 06.03.2018
  • @Paul Я добавил свое решение без использования nx и simpy в качестве ответа ниже. 06.03.2018
  • @ImportanceOfBeingErnest Ваше предположение верно. Для линейного набора неравенств полупространства всегда будут выпуклыми. 07.03.2018

  • 2

    Ниже будет моя версия решения. Это похоже на решение @Paul в том, что оно берет треугольники, группирует их по граням, которым они принадлежат, и соединяет их с одной гранью.

    Разница в основном будет заключаться в том, что это решение не использует nx или simpy. Многие из необходимых операций выполняются путем переиндексации, широкого использования unique и некоторой линейной алгебры.
    Порядок вершин конечных граней определяется ConvexHull. Я думаю, что это не должно быть ограничением, поскольку (я думаю, что) любое пересечение полупространства должно приводить только к выпуклым формам. Однако я также добавил еще один метод, который можно использовать, если формы не выпуклые (на основе идеи из этот вопрос).

    from scipy.spatial import HalfspaceIntersection
    from scipy.spatial import ConvexHull
    import numpy as np
    import matplotlib.pyplot as plt
    import mpl_toolkits.mplot3d as a3
    
    w = np.array([1., 1., 1.])
    # ∑ᵢ hᵢ wᵢ qᵢ - ∑ᵢ gᵢ wᵢ <= 0 
    #  qᵢ - ubᵢ <= 0
    # -qᵢ + lbᵢ <= 0 
    halfspaces = np.array([
                        [1.*w[0], 1.*w[1], 1.*w[2], -10 ],
                        [ 1.,  0.,  0., -4],
                        [ 0.,  1.,  0., -4],
                        [ 0.,  0.,  1., -4],
                        [-1.,  0.,  0.,  0],
                        [ 0., -1.,  0.,  0],
                        [ 0.,  0., -1.,  0]
                        ])
    feasible_point = np.array([0.1, 0.1, 0.1])
    hs = HalfspaceIntersection(halfspaces, feasible_point)
    verts = hs.intersections
    hull = ConvexHull(verts)
    simplices = hull.simplices
    
    org_triangles = [verts[s] for s in simplices]
    
    class Faces():
        def __init__(self,tri, sig_dig=12, method="convexhull"):
            self.method=method
            self.tri = np.around(np.array(tri), sig_dig)
            self.grpinx = list(range(len(tri)))
            norms = np.around([self.norm(s) for s in self.tri], sig_dig)
            _, self.inv = np.unique(norms,return_inverse=True, axis=0)
    
        def norm(self,sq):
            cr = np.cross(sq[2]-sq[0],sq[1]-sq[0])
            return np.abs(cr/np.linalg.norm(cr))
    
        def isneighbor(self, tr1,tr2):
            a = np.concatenate((tr1,tr2), axis=0)
            return len(a) == len(np.unique(a, axis=0))+2
    
        def order(self, v):
            if len(v) <= 3:
                return v
            v = np.unique(v, axis=0)
            n = self.norm(v[:3])
            y = np.cross(n,v[1]-v[0])
            y = y/np.linalg.norm(y)
            c = np.dot(v, np.c_[v[1]-v[0],y])
            if self.method == "convexhull":
                h = ConvexHull(c)
                return v[h.vertices]
            else:
                mean = np.mean(c,axis=0)
                d = c-mean
                s = np.arctan2(d[:,0], d[:,1])
                return v[np.argsort(s)]
    
        def simplify(self):
            for i, tri1 in enumerate(self.tri):
                for j,tri2 in enumerate(self.tri):
                    if j > i: 
                        if self.isneighbor(tri1,tri2) and \
                           self.inv[i]==self.inv[j]:
                            self.grpinx[j] = self.grpinx[i]
            groups = []
            for i in np.unique(self.grpinx):
                u = self.tri[self.grpinx == i]
                u = np.concatenate([d for d in u])
                u = self.order(u)
                groups.append(u)
            return groups
    
    
    f = Faces(org_triangles)
    g = f.simplify()
    
    ax = a3.Axes3D(plt.figure())
    
    colors = list(map("C{}".format, range(len(g))))
    
    pc = a3.art3d.Poly3DCollection(g,  facecolor=colors, 
                                       edgecolor="k", alpha=0.9)
    ax.add_collection3d(pc)
    
    ax.dist=10
    ax.azim=30
    ax.elev=10
    ax.set_xlim([0,5])
    ax.set_ylim([0,5])
    ax.set_zlim([0,5])
    plt.show()
    

    введите здесь описание изображения

    06.03.2018
    Новые материалы

    5 проектов на Python, которые нужно создать прямо сейчас!
    Добро пожаловать! Python — один из моих любимых языков программирования. Если вы новичок в этом языке, перейдите по ссылке ниже, чтобы узнать о нем больше:

    Dall-E 2: недавние исследования показывают недостатки в искусстве, созданном искусственным интеллектом
    DALL-E 2 — это всеобщее внимание в индустрии искусственного интеллекта. Люди в списке ожидания пытаются заполучить продукт. Что это означает для развития креативной индустрии? О применении ИИ в..

    «Очень простой» эволюционный подход к обучению с подкреплением
    В прошлом семестре я посетил лекцию по обучению с подкреплением (RL) в моем университете. Честно говоря, я присоединился к нему официально, но я редко ходил на лекции, потому что в целом я нахожу..

    Освоение информационного поиска: создание интеллектуальных поисковых систем (глава 1)
    Глава 1. Поиск по ключевым словам: основы информационного поиска Справочная глава: «Оценка моделей поиска информации: подробное руководство по показателям производительности » Глава 1: «Поиск..

    Фишинг — Упаковано и зашифровано
    Будучи старшим ИТ-специалистом в небольшой фирме, я могу делать много разных вещей. Одна из этих вещей: специалист по кибербезопасности. Мне нравится это делать, потому что в настоящее время я..

    ВЫ РЕГРЕСС ЭТО?
    Чтобы понять, когда использовать регрессионный анализ, мы должны сначала понять, что именно он делает. Вот простой ответ, который появляется, когда вы используете Google: Регрессионный..

    Не зря же это называют интеллектом
    Стек — C#, Oracle Опыт — 4 года Работа — Разведывательный корпус Мне пора служить Может быть, я немного приукрашиваю себя, но там, где я живу, есть обязательная военная служба на 3..