Поиск ближайших соседей треугольной тензеляции

У меня треугольная тесселяция, как показано на рисунке. enter image description here

Учитывая N количество треугольников в тесселяции, у меня есть массив NX 3 X 3 который хранит (x, y, z) координаты всех трех вершин каждого треугольника. Моя цель - найти для каждого треугольника соседний треугольник, разделяющий один и тот же край. Замысловатая часть - это вся настройка, что я не повторяю счет соседа. То есть, если треугольник j уже считается соседом треугольника i, то треугольник i не должен снова считаться соседним треугольником j. Таким образом, я хотел бы иметь карту, хранящую список соседей для каждого индексного треугольника. Если я начну с треугольника в индексе i, тогда индекс i будет иметь трех соседей, а все остальные будут иметь два или меньше. В качестве иллюстрации предположим, что у меня есть массив, в котором хранятся вершины треугольника:

import numpy as np
vertices = np.array([[[2.0, 1.0, 3.0],[3.0, 1.0, 2.0],[1.2, 2.5, -2.0]],
                     [[3.0, 1.0, 2.0],[1.0, 2.0, 3.0],[1.2, -2.5, -2.0]],
                     [[1.0, 2.0, 3.0],[2.0, 1.0, 3.0],[3.0, 1.0, 2.0]],
                     [[1.0, 2.0, 3.0],[2.0, 1.0, 3.0],[2.2, 2.0, 1.0]],
                     [[1.0, 2.0, 3.0],[2.2, 2.0, 1.0],[4.0, 1.0, 0.0]],
                     [[2.0, 1.0, 3.0],[2.2, 2.0, 1.0],[-4.0, 1.0, 0.0]]])

Предположим, что я запускаю свой счет из вершинного индекса 2, то есть с вершинами [[1.0, 2.0, 3.0],[2.0, 1.0, 3.0],[3.0, 1.0, 2.0]], тогда я хотел бы, чтобы мой вывод что-то вроде:

neighbour = [[], [], [0, 1, 3], [4, 5], [], []].

Обновление: после ответа от @Ajax1234, я считаю, что хороший способ хранения вывода - это то, как продемонстрировал Ajax1234. Однако в этом выпуске есть двусмысленность, в некотором смысле, что невозможно узнать, чей сосед - это какой. Хотя массив примеров не очень хорош, у меня есть фактические вершины из икосаэдра, тогда, если я начну с заданного треугольника, я гарантированно будет иметь 3 соседей для первого, а два соседа для отдыха (пока весь треугольник не будет исчерпан), В связи с этим предположим, что у меня есть следующий массив:

vertices1 = [[[2, 1, 3], [3, 1, 2], [1, 2, -2]], 
            [[3, 1, 2], [1, 2, 3], [1, -2, 2]], 
            [[1, 2, 3], [2, 1, 3], [3, 1, 2]], 
            [[1, 2, 3], [2, 1, 3], [2, 2, 1]],
            [[1, 2, 3], [2, 2, 1], [4, 1, 0]], 
            [[2, 1, 3], [2, 2, 1], [-4, 1, 0]],
            [[3, 1, 3], [2, 2, 1], [-4, 1, 0]],
            [[8, 1, 2], [1, 2, 3], [1, -2, 2]]]

Алгоритм BFS, показанный в ответе ниже на @Ajax1234, дает результат

[0, 1, 3, 7, 4, 5, 6]

а если я просто поменяю позицию последнего элемента так, что

vertices2 = [[[2, 1, 3], [3, 1, 2], [1, 2, -2]], 
            [[3, 1, 2], [1, 2, 3], [1, -2, 2]], 
            [[1, 2, 3], [2, 1, 3], [3, 1, 2]], 
            [[1, 2, 3], [2, 1, 3], [2, 2, 1]],
            [[1, 2, 3], [2, 2, 1], [4, 1, 0]], 
            [[8, 1, 2], [1, 2, 3], [1, -2, 2]],
            [[2, 1, 3], [2, 2, 1], [-4, 1, 0]],
            [[3, 1, 3], [2, 2, 1], [-4, 1, 0]]]

который дает результат

[0, 1, 3, 4, 5, 6, 7].

Это довольно неоднозначно, поскольку позиции в приправе не были изменены вообще, они просто поменялись местами. Поэтому я хотел бы иметь последовательный способ поиска. Например, поиск в первый раз соседей по индексу 2 дает [0, 1, 3] для обеих vertices1 и vertices2, теперь я хотел бы, чтобы поиск находился в индексе 0, который ничего не нашел и, следовательно, перешел к следующему элементу 1, должен найти индекс 7 для vertices1 и индекса 5 для vertices2. Таким образом, текущий вывод должен быть [0, 1, 3, 7], [0, 1, 3, 5] для vertices1 и vertices2 соответственно. Затем мы переходим к индексу 3 и т.д. После того как мы исчерпали весь поиск, окончательный вывод для первого должен быть

[0, 1, 3, 7, 4, 5, 6]

и что для второго следует

[0, 1, 3, 5, 4, 6, 7].

Каким будет эффективный способ достичь этого?

Ответы

Ответ 1

Если вы хотите использовать библиотеку networkx, вы можете воспользоваться ее быстрой реализацией bfs. Я знаю, добавление другой зависимости раздражает, но прирост производительности кажется огромным, см. Ниже.

import numpy as np
from scipy import spatial
import networkx

vertices = np.array([[[2.0, 1.0, 3.0],[3.0, 1.0, 2.0],[1.2, 2.5, -2.0]],
                     [[3.0, 1.0, 2.0],[1.0, 2.0, 3.0],[1.2, -2.5, -2.0]],
                     [[1.0, 2.0, 3.0],[2.0, 1.0, 3.0],[3.0, 1.0, 2.0]],
                     [[1.0, 2.0, 3.0],[2.0, 1.0, 3.0],[2.2, 2.0, 1.0]],
                     [[1.0, 2.0, 3.0],[2.2, 2.0, 1.0],[4.0, 1.0, 0.0]],
                     [[2.0, 1.0, 3.0],[2.2, 2.0, 1.0],[-4.0, 1.0, 0.0]]])


vertices1 = np.array([[[2, 1, 3], [3, 1, 2], [1, 2, -2]], 
                      [[3, 1, 2], [1, 2, 3], [1, -2, 2]], 
                      [[1, 2, 3], [2, 1, 3], [3, 1, 2]], 
                      [[1, 2, 3], [2, 1, 3], [2, 2, 1]],
                      [[1, 2, 3], [2, 2, 1], [4, 1, 0]], 
                      [[2, 1, 3], [2, 2, 1], [-4, 1, 0]],
                      [[3, 1, 3], [2, 2, 1], [-4, 1, 0]],
                      [[8, 1, 2], [1, 2, 3], [1, -2, 2]]])


def make(N=3000):
    """create a N random points and triangulate"""
    points= np.random.uniform(-10, 10, (N, 3))
    tri = spatial.Delaunay(points[:, :2])
    return points[tri.simplices]

def bfs_tree(triangles, root=0, return_short=True):
    """convert triangle list to graph with vertices = triangles,
    edges = pairs of triangles with shared edge and compute bfs tree
    rooted at triangle number root"""
    # use the old view as void trick to merge triplets, so they can
    # for example be easily compared
    tr_as_v = triangles.view(f'V{3*triangles.dtype.itemsize}').reshape(
        triangles.shape[:-1])
    # for each triangle write out its edges, this involves doubling the
    # data becaues each vertex occurs twice
    tr2 = np.concatenate([tr_as_v, tr_as_v], axis=1).reshape(-1, 3, 2)
    # sort vertices within edges ...
    tr2.sort(axis=2)
    # ... and glue them together
    tr2 = tr2.view(f'V{6*triangles.dtype.itemsize}').reshape(
        triangles.shape[:-1])
    # to find shared edges, sort them ...
    idx = tr2.ravel().argsort()
    tr2s = tr2.ravel()[idx]
    # ... and then compare consecutive ones
    pairs, = np.where(tr2s[:-1] == tr2s[1:])
    assert np.all(np.diff(pairs) >= 2)
    # these are the edges of the graph, the vertices are implicitly 
    # named 0, 1, 2, ...
    edges = np.concatenate([idx[pairs,None]//3, idx[pairs+1,None]//3], axis=1)
    # construct graph ...
    G = networkx.Graph(edges.tolist())
    # ... and let networkx do its magic
    res = networkx.bfs_tree(G, root)
    if return_short:
        # sort by distance from root and then by actual path
        sp = networkx.single_source_shortest_path(res, root)
        sp = [sp[i] for i in range(len(sp))]
        sp = [(len(p), p) for p in sp]
        res = sorted(range(len(res.nodes)), key=sp.__getitem__)
    return res

Демо-версия:

# OP second example:
>>> bfs_tree(vertices1, 2)
[2, 0, 1, 3, 7, 4, 5, 6]
>>> 
# large random example
>>> random_data = make()
>>> random_data.shape
(5981, 3, 3)
>>> bfs = bfs_tree(random_data)
# returns instantly

Ответ 2

Я понял ответ, благодаря руководству @Ajax1234. Была небольшая сложность, основанная на том, как вы сравниваете элементы списка. Вот один рабочий подход:

import numpy as np
from collections import deque
import time
d = [[[2, 1, 3], [3, 1, 2], [1, 2, -2]], 
     [[3, 1, 2], [1, 2, 3], [1, -2, 2]], 
     [[1, 2, 3], [2, 1, 3], [3, 1, 2]], 
     [[1, 2, 3], [2, 1, 3], [2, 2, 1]],
     [[1, 2, 3], [2, 2, 1], [4, 1, 0]],
     [[2, 1, 3], [2, 2, 1], [-4, 1, 0]],
     [[3, 1, 3], [2, 2, 1], [-4, 1, 0]]]
def bfs(d, start):
  queue = deque([d[start]])
  seen = [start]
  results = []
  while queue:
    _vertices = queue.popleft()
    current = [[i, a] for i, a in enumerate(d) if len([x for x in a if x in _vertices])==2 and i not in seen]
    if len(current)>0:
        current_array = np.array(current, dtype=object)
        current0 = list(current_array[:, 0])
        current1 = list(current_array[:, 1])
        results.extend(current0)
        queue.extend(current1)
        seen.extend(current0)
  return results

time1 = time.time()
xo = bfs(d, 2)
print(time.time()-time1)
print(bfs(d, 2))

Для массива размером (3000, 3, 3) код в настоящий момент занимает 18 секунд. Если я добавлю @numba.jit(parallel = True, error_model='numpy'), то на самом деле это займет 30 секунд. Вероятно, потому, что queue не поддерживается numba. Я был бы рад, если бы кто-нибудь мог предложить, как этот код можно сделать быстрее.

Обновить

В коде были некоторые избыточности, которые теперь были удалены, а код за 14 секунд, вместо 30 секунд, для a d размера (4000 X 3 X 3). Все еще не звездный, но хороший прогресс, и теперь код выглядит чище.

Ответ 3

Процесс, который вы описываете, похож на поиск в ширину, который можно использовать для поиска треугольников, которые являются соседями. Результат просто дает индексы, однако, поскольку неясно, как пустые списки попадают в конечный результат:

from collections import deque
d = [[[2.0, 1.0, 3.0], [3.0, 1.0, 2.0], [1.2, 2.5, -2.0]], [[3.0, 1.0, 2.0], [1.0, 2.0, 3.0], [1.2, -2.5, -2.0]], [[1.0, 2.0, 3.0], [2.0, 1.0, 3.0], [3.0, 1.0, 2.0]], [[1.0, 2.0, 3.0], [2.0, 1.0, 3.0], [2.2, 2.0, 1.0]], [[1.0, 2.0, 3.0], [2.2, 2.0, 1.0], [4.0, 1.0, 0.0]], [[2.0, 1.0, 3.0], [2.2, 2.0, 1.0], [-4.0, 1.0, 0.0]]]
def bfs(d, start):
  queue = deque([d[start]])
  seen = [start]
  results = []
  while queue:
    _vertices = queue.popleft()
    exists_at = [i for i, a in enumerate(d) if a == _vertices][0]
    current = [i for i, a in enumerate(d) if any(c in a for c in _vertices) and i != exists_at and i not in seen]
    results.extend(current)
    queue.extend([a for i, a in enumerate(d) if any(c in a for c in _vertices) and i != exists_at and i not in seen])
    seen.extend(current)
  return results

print(bfs(d, 2))

Выход:

[0, 1, 3, 4, 5]        

Ответ 4

Вы можете использовать trimesh

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

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

пример

import trimesh
import numpy as np

vertices = np.array([[[2.0, 1.0, 3.0],[3.0, 1.0, 2.0],[1.2, 2.5, -2.0]],
                     [[3.0, 1.0, 2.0],[1.0, 2.0, 3.0],[1.2, -2.5, -2.0]],
                     [[1.0, 2.0, 3.0],[2.0, 1.0, 3.0],[3.0, 1.0, 2.0]],
                     [[1.0, 2.0, 3.0],[2.0, 1.0, 3.0],[2.2, 2.0, 1.0]],
                     [[1.0, 2.0, 3.0],[2.2, 2.0, 1.0],[4.0, 1.0, 0.0]],
                     [[2.0, 1.0, 3.0],[2.2, 2.0, 1.0],[-4.0, 1.0, 0.0]]])

#generate_faces
# I assume here that your input format is N x NPoints x xyz
faces=np.arange(vertices.shape[0]*3).reshape(-1,3)
#reshape_vertices (nx3)
vertices=vertices.reshape(-1,3)

#Create mesh object
mesh=trimesh.Trimesh(vertices=vertices, faces=faces)

# Set the tolerance to define which vertices are equal (default 1e-8)
# It is easy to prove that int(5)==int(5) but is 5.000000001 equal to 5.0 or not?
# This depends on the algorithm/ programm from which you have imported the mesh....
# To find a proper value for the tolerance and to heal the mesh if necessary, will 
# likely be the most complicated part
trimesh.constants.tol.merge=tol

#merge the vertices
mesh.merge_vertices()

# At this stage you may need some sort of healing algorithm..

# eg. reconstruct the input
mesh.vertices[mesh.faces]

#get for example the faces, vertices
mesh.faces #These are indices to the vertices
mesh.vertices

# get the faces which touch each other on the edges
mesh.face_adjacency

Это дает простой массив 2d, который сталкивается с разделом. Я бы лично использовал этот формат для дальнейших вычислений. Если вы хотите придерживаться своего определения, я бы создал массив nx3 numpy (каждый треугольник должен иметь максимум 3 соседних края). Пропущенные значения могут быть заполнены, например, NaN или что-то значимое.

Я могу добавить эффективный способ, если вы действительно этого хотите.