Граница, охватывающая заданный набор точек

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

Вот пример текущего поведения:

https://i.imgur.com/wrhH4wh.png

Вот MSPaint пример желаемого поведения:

Разыскиваемое поведение

Текущий код выпуклой оболочки в C#: https://hastebin.com/dudejesuja.cs

Итак, вот мои вопросы:

1) это вообще возможно?

Р: Да

2) Это даже называется выпуклой оболочкой? (Я так не думаю)

Р: Нет, это называется граница, ссылка: https://www.mathworks.com/help/matlab/ref/boundary.html

3) Будет ли это менее удобным для исполнения, чем обычный выпуклый корпус?

R: Ну, насколько я исследовал, это должно быть то же самое исполнение

4) Пример этого алгоритма в псевдокоде или что-то подобное?

R: Еще не ответили, или я еще не нашел решение

6 ответов

Решение

Вот некоторый код Python, который вычисляет альфа-форму (вогнутую оболочку) и сохраняет только внешнюю границу. Это, вероятно, то, что делает граница Matlab внутри.

from scipy.spatial import Delaunay
import numpy as np


def alpha_shape(points, alpha, only_outer=True):
    """
    Compute the alpha shape (concave hull) of a set of points.
    :param points: np.array of shape (n,2) points.
    :param alpha: alpha value.
    :param only_outer: boolean value to specify if we keep only the outer border
    or also inner edges.
    :return: set of (i,j) pairs representing edges of the alpha-shape. (i,j) are
    the indices in the points array.
    """
    assert points.shape[0] > 3, "Need at least four points"

    def add_edge(edges, i, j):
        """
        Add an edge between the i-th and j-th points,
        if not in the list already
        """
        if (i, j) in edges or (j, i) in edges:
            # already added
            assert (j, i) in edges, "Can't go twice over same directed edge right?"
            if only_outer:
                # if both neighboring triangles are in shape, it's not a boundary edge
                edges.remove((j, i))
            return
        edges.add((i, j))

    tri = Delaunay(points)
    edges = set()
    # Loop over triangles:
    # ia, ib, ic = indices of corner points of the triangle
    for ia, ib, ic in tri.vertices:
        pa = points[ia]
        pb = points[ib]
        pc = points[ic]
        # Computing radius of triangle circumcircle
        # www.mathalino.com/reviewer/derivation-of-formulas/derivation-of-formula-for-radius-of-circumcircle
        a = np.sqrt((pa[0] - pb[0]) ** 2 + (pa[1] - pb[1]) ** 2)
        b = np.sqrt((pb[0] - pc[0]) ** 2 + (pb[1] - pc[1]) ** 2)
        c = np.sqrt((pc[0] - pa[0]) ** 2 + (pc[1] - pa[1]) ** 2)
        s = (a + b + c) / 2.0
        area = np.sqrt(s * (s - a) * (s - b) * (s - c))
        circum_r = a * b * c / (4.0 * area)
        if circum_r < alpha:
            add_edge(edges, ia, ib)
            add_edge(edges, ib, ic)
            add_edge(edges, ic, ia)
    return edges

Если вы запустите его со следующим тестовым кодом, вы получите этот рисунок, который выглядит так, как вам нужно:

from matplotlib.pyplot import *

# Constructing the input point data
np.random.seed(0)
x = 3.0 * np.random.rand(2000)
y = 2.0 * np.random.rand(2000) - 1.0
inside = ((x ** 2 + y ** 2 > 1.0) & ((x - 3) ** 2 + y ** 2 > 1.0)
points = np.vstack([x[inside], y[inside]]).T

# Computing the alpha shape
edges = alpha_shape(points, alpha=0.25, only_outer=True)

# Plotting the output
figure()
axis('equal')
plot(points[:, 0], points[:, 1], '.')
for i, j in edges:
    plot(points[[i, j], 0], points[[i, j], 1])
show()

РЕДАКТИРОВАТЬ: После запроса в комментарии, вот некоторый код, который "сшивает" выходной край, установленный в последовательности последовательных краев.

def find_edges_with(i, edge_set):
    i_first = [j for (x,j) in edge_set if x==i]
    i_second = [j for (j,x) in edge_set if x==i]
    return i_first,i_second

def stitch_boundaries(edges):
    edge_set = edges.copy()
    boundary_lst = []
    while len(edge_set) > 0:
        boundary = []
        edge0 = edge_set.pop()
        boundary.append(edge0)
        last_edge = edge0
        while len(edge_set) > 0:
            i,j = last_edge
            j_first, j_second = find_edges_with(j, edge_set)
            if j_first:
                edge_set.remove((j, j_first[0]))
                edge_with_j = (j, j_first[0])
                boundary.append(edge_with_j)
                last_edge = edge_with_j
            elif j_second:
                edge_set.remove((j_second[0], j))
                edge_with_j = (j, j_second[0])  # flip edge rep
                boundary.append(edge_with_j)
                last_edge = edge_with_j

            if edge0[0] == last_edge[1]:
                break

        boundary_lst.append(boundary)
    return boundary_lst

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

Я бы использовал другой подход для решения этой проблемы. Поскольку мы работаем с двумерным набором точек, вычислить ограничивающий прямоугольник области точек несложно. Затем я разделил бы этот прямоугольник на "ячейки" горизонтальными и вертикальными линиями, и для каждой ячейки просто посчитал количество пикселей, находящихся в ее границах. Поскольку каждая ячейка может иметь только 4 соседние ячейки (смежные по сторонам ячейки), то граничными ячейками будут те, которые имеют по меньшей мере одну пустую соседнюю ячейку или имеют сторону ячейки, расположенную на границе ограничивающего прямоугольника. Тогда граница будет построена вдоль граничных сторон ячейки. Граница будет выглядеть как "лестница", но выбор меньшего размера ячейки улучшит результат. На самом деле, размер ячейки должен быть определен экспериментально; он не может быть слишком маленьким, иначе внутри области могут появиться пустые ячейки. Среднее расстояние между точками может быть использовано в качестве нижней границы размера ячейки.

Подумайте об использовании Альфа-формы, иногда называемой вогнутым корпусом. https://en.wikipedia.org/wiki/Alpha_shape

Он может быть построен из триангуляции Делоне за время O(N log N).

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

[1] Alpha Shape Toolbox: набор инструментов для создания n-мерных альфа-форм.

[2] Plotly: он может генерировать объект Mesh3d, который в зависимости от значения ключа может быть выпуклой оболочкой этого набора, его триангуляцией Делоне или альфа-набором.

https://plotly.com/python/v3/альфа-формы/https://plotly.com/python/v3/альфа-формы/

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

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

https://xphilipp.developpez.com/contribuez/SnakeAnimation.gif

Анимация выше не прошла весь путь, но многие подобные алгоритмы можно настроить для этого.

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

      from scipy.spatial import Delaunay

points = np.array([[13.43, 12.89], [14.44, 13.86], [13.67, 15.87], [13.39, 14.95],\
[12.66, 13.86], [10.93, 14.24], [11.69, 15.16], [13.06, 16.24], [11.29, 16.35],\
[10.28, 17.33], [10.12, 15.49], [9.03, 13.76], [10.12, 14.08], [9.07, 15.87], \
[9.6, 16.68], [7.18, 16.19], [7.62, 14.95], [8.39, 16.79], [8.59, 14.51], \
[8.1, 13.43], [6.57, 11.59], [7.66, 11.97], [6.94, 13.86], [6.53, 14.84], \
[5.48, 12.84], [6.57, 12.56], [5.6, 11.27], [6.29, 10.08], [7.46, 10.45], \
[7.78, 7.21], [7.34, 8.72], [6.53, 8.29], [5.85, 8.83], [5.56, 10.24], [5.32, 7.8], \
[5.08, 9.86], [6.01, 5.75], [6.41, 7.48], [8.19, 5.69], [8.23, 4.72], [6.85, 6.34], \
[7.02, 4.07], [9.4, 3.2], [9.31, 4.99], [7.86, 3.15], [10.73, 2.82], [10.32, 4.88], \
[9.72, 1.58], [11.85, 5.15], [12.46, 3.47], [12.18, 1.58], [11.49, 3.69], \
[13.1, 4.99], [13.63, 2.61]])

tri = Delaunay(points,furthest_site=False)
res = []
for t in tri.simplices:
  A,B,C = points[t[0]],points[t[1]],points[t[2]]
  e1 = B-A; e2 = C-A
  num = np.dot(e1, e2)
  n1 = np.linalg.norm(e1); n2 = np.linalg.norm(e2)
  denom =  n1 * n2
  d1 = np.rad2deg(np.arccos(num/denom))
  e1 = C-B; e2 = A-B
  num = np.dot(e1, e2)
  denom = np.linalg.norm(e1) * np.linalg.norm(e2)
  d2 = np.rad2deg(np.arccos(num/denom))
  d3 = 180-d1-d2
  res.append([n1,n2,d1,d2,d3])

res = np.array(res)
m = res[:,[0,1]].mean()*res[:,[0,1]].std()

mask = np.any(res[:,[2,3,4]] > 110) & (res[:,0] < m) & (res[:,1] < m )

plt.triplot(points[:,0], points[:,1], tri.simplices[mask])

Затем заполните цветом и сегментом.

Вот код JavaScript, который создает вогнутый корпус: https://github.com/AndriiHeonia/hull Вероятно, вы можете перенести его на C#.

Другие вопросы по тегам