9
votes

I am having a bit of a problem with an algorithm that I am currently using. I wanted it to make a boundary.

Here is an example of the current behavior:

Current behavior

Here is an MSPaint example of wanted behavior:

Wanted behavior

Current code of Convex Hull in C#:https://hastebin.com/dudejesuja.cs

So here are my questions:

1) Is this even possible?

R: Yes

2) Is this even called Convex Hull? (I don't think so)

R: Nope it is called boundary, link: https://www.mathworks.com/help/matlab/ref/boundary.html

3) Will this be less performance friendly than a conventional convex hull?

R: Well as far as I researched it should be the same performance

4) Example of this algorithm in pseudo code or something similar?

R: Not answered yet or I didn't find a solution yet

4
1. Indeed this is not a convex hull. Convex hull would have no convexity, i.e., no parts that cave in. It would just be the exterior four points. 2. I don't understand what you actually want. Why are just those two points in the middle the ones you chose? Why not any of the other points? This may make more sense to you in your actual use case, but as far as connecting the dots go in this example, your choice seems completely arbitrary. Why use 6 points instead of 5 like the left? Why not 4 points like a true convex hull would? - alkasm
@AlexanderReynolds Yes i am sorry, i drew it wrongly, and it was a bad example, i re did the example, i think this one is better, and you can imagine countless (not infinity), inside of the lines that i did, i would like to have that curve it does on right image. - Mário Gabriel
Like why isn't your drawing on the right this instead: imgur.com/a/Ot9W9d9 ? Of course you can continue to apply this same idea to other points. Do you actually start with anything OTHER than points in this process, or is truly the only info you have these points? Just trying to make sure this isn't an XY problem. What I would try to do if I were you is draw connections between every vertex (make a complete graph). Then start removing the exterior lines you don't want, and see if that follows a rule you can use. - alkasm
It looks like you maybe want a contour, but that would be hard to define on discrete points like that. However, whatever rule that made the points may be able to help make the contours. Where do those points come from in the image? Also again just to be clear---I know it's not what you want---but your title and question (minimum convex hull) is what you are getting in the first image, so what you want is not a convex hull. - alkasm
Look for alpha shapes - MBo

4 Answers

24
votes

Here is some Python code that computes the alpha-shape (concave hull) and keeps only the outer boundary. This is probably what matlab's boundary does inside.

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

If you run it with the following test code you will get this figure, which looks like what you need: enter image description here

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()

EDIT: Following a request in a comment, here is some code that "stitches" the output edge set into sequences of consecutive edges.

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

You can then go over the list of boundary lists and append the points corresponding to the first index in each edge to get a boundary polygon.

0
votes

I would use a different approach to solve this problem. Since we are working with a 2-D set of points, it is straightforward to compute the bounding rectangle of the points’ region. Then I would divide this rectangle into “cells” by horizontal and vertical lines, and for each cell simply count the number of pixels located within its bounds. Since each cell can have only 4 adjacent cells (adjacent by cell sides), then the boundary cells would be the ones that have at least one empty adjacent cell or have a cell side located at the bounding rectangle boundary. Then the boundary would be constructed along boundary cell sides. The boundary would look like a “staircase”, but choosing a smaller cell size would improve the result. As a matter of fact, the cell size should be determined experimentally; it could not be too small, otherwise inside the region may appear empty cells. An average distance between the points could be used as a lower boundary of the cell size.

0
votes

Consider using an Alpha Shape, sometimes called a Concave Hull. https://en.wikipedia.org/wiki/Alpha_shape

It can be built from the Delaunay triangulation, in time O(N log N).

0
votes

Here is the JavaScript code that builds concave hull: https://github.com/AndriiHeonia/hull Probably you can port it to C#.