14

I have some data that looks like this:

vertex_numbers = [1, 2, 3, 4, 5, 6]

# all order here is unimportant - this could be a set of frozensets and it would
# not affect my desired output. However, that would be horribly verbose!
edges = [
    (1, 2),
    (1, 3),
    (1, 4),
    (1, 5),

    (2, 3),
    (3, 4),
    (4, 5),
    (5, 2),

    (2, 6),
    (3, 6),
    (4, 6),
    (5, 6)
]

The example above describes an octohedron - numbering the vertices 1 to 6, with 1 and 6 opposite each other, each entry describes the vertex numbers at the ends of each edge.

From this data, I want to produce a list of faces. The faces are guaranteed to be triangular. Here's one such face list for the input above, determined by hand:

faces = [
    (1, 2, 3),
    (1, 3, 4),
    (1, 4, 5),
    (1, 5, 2),
    (2, 5, 6),
    (3, 2, 6),
    (4, 3, 6),
    (5, 4, 6)
]

Diagramatically, this can be represented as follows:

graph

For any face, follow the direction of the curled arrow, and you can read off the vertex numbers above. This doesn't really work for the outer face, 1, 3, 4, but you can fix that by drawing on the surface of a sphere


I can get close with this:

edge_lookup = defaultdict(set)
for a, b in edges:
    edge_lookup[a] |= {b}
    edge_lookup[b] |= {a}

faces = set()
for a in vertex_numbers:
    for b in edge_lookup[a]:
        for c in edge_lookup[a]:
            if b in edge_lookup[c]:
                faces.add(frozenset([a, b, c]))

faces = map(tuple, faces)

Giving (reordered from output for ease of comparison with the original):

[
    (1, 2, 3),  # ok
    (1, 3, 4),  # ok
    (1, 4, 5),  # ok
    (1, 2, 5),  # cyclically incorrect!
    (2, 5, 6),  # ok
    (2, 3, 6),  # cyclically incorrect!
    (3, 4, 6),  # cyclically incorrect!
    (4, 5, 6),  # cyclically incorrect!
}

However, this is bad for two reasons:

  1. It's at least O(N³)

    In this particular case, that's not a problem, since N = 10242, it completes in less than 5 seconds

  2. It doesn't determine face ordering

    I'm using frozensets there, which are inherently orderless. I need to produce faces with the same cyclic order as my example output.

    The face sequences generated are used to render one-sided surface with OpenGL. As a result, it's essential that all the faces vertices are in the same rotary order (whether that ends up being clockwise or anticlockwise is a property of the vertices themselves - all I care about is that each face is the same)

  3. It assumes all edges that form a triangle must be a face

    As @Bartosz points out in the comments, this needn't be the case - take any two triangular meshes, and join them at a face, and you have something that is no longer a face.


What method should I be using to construct a list of faces with the correct rotational order?

Eric
  • 95,302
  • 53
  • 242
  • 374
  • 1
    I'm confused as to why `(1, 2, 5)` is cyclically incorrect, but `(1, 5, 2)` isn't. They both have two edges going one way and one the other. Or is there something else that determines cyclical correctness that I'm missing? – Sam Mussmann Dec 21 '13 at 20:35
  • @Sam: Consider flattening the graph out onto a plane -you can either draw faces in a clockwise order, or an anticlockwise. I should stress the order of the vertices in `edges` is meaningless - this is _not_ a directed graph – Eric Dec 21 '13 at 20:42
  • 1
    I think I'm still confused. :-) Wouldn't that change based on how you flattened the graph onto the plane? It's quite possible I'm just a noob at this kind of stuff too -- is there a reference on this kind of stuff you could point me to? – Sam Mussmann Dec 21 '13 at 20:47
  • I don't think it's possible to find the correct cyclic orientation without more information of the vertices. Your expected orientation comes from, that you already know what kind of geometric figure these points should represent. I don't think there is enough information in the edges to deduce the geometry. – M4rtini Dec 21 '13 at 20:52
  • @SamMussmann: No, it only depends which way up you hold the plane when you draw it. WHen you project it, it looks like [this](https://upload.wikimedia.org/wikipedia/commons/2/2a/Octahedron_graph.png). The solution I'm looking for has all the faces described clockwise, except the outermost one, which will be anticlockwise by virtue of the projection – Eric Dec 21 '13 at 20:53
  • @M4rtini: See my edit - as a human, you can trivially solve it graphically (although admittedly there will always be two solutions, depending on whether you hold the paper upside down) – Eric Dec 21 '13 at 21:00
  • In case it's useful--this is relatively straightforward with a set of coordinates for each facet (triangle) as you could simply calculate the convex hull of the facet which is guaranteed to have vertices in counterclockwise order in 2D using scipy (http://docs.scipy.org/doc/scipy-dev/reference/generated/scipy.spatial.ConvexHull.html). I suspect you could work out the generalized algorithm you want above by using real 3D points & then mapping back by assigning vertex numbers to the original certesian co-ordinates. – treddy Dec 21 '13 at 21:04
  • 2
    If I understand your code correctly, it is wrong. Considerate edges (1, 2), (2, 3), (3, 1), (1, 4), (2, 4), (3, 4), (1, 5), (2, 5), (3, 5). You don't want to get (1, 2, 3) as a face. (If you want to draw it - it looks like two pyramids sharing a face) – Bartosz Marcinkowski Dec 21 '13 at 21:29
  • @BartoszMarcinkowski: Oops, you're right - for my particular application, that fortunately never arises, but it's still a valid remark – Eric Dec 21 '13 at 21:35

3 Answers3

5

I can give you a clue with the second part; once you have the faces, there is a simple way of making it cyclically correct.

Start with choosing one face (a, b, c) to be correct, then no other face can contain (a, b), (b, c) or (c, a) in that order. In other words, find face that contain vertices a, b then make it be (b, a, x), and so on.

In case you don't get what I mean - use the following fact: each edge (x, y) is contained by two faces, and if they are cyclically correct, one of the faces has it as (x, y), the other as (y, x).

Possible implementation: Start with creating a graph where faces are vertices and edges mean that two faces share an edge in the original problem. Then use DFS or BFS.

Bartosz Marcinkowski
  • 6,651
  • 4
  • 39
  • 69
1

Given the information from Bartosz, this is what i came up with.

class vertex(object):
    def __init__(self, ID):
        self.ID = ID
        self.connected = set()

    def connect(self, cVertex):
        self.connected.add(cVertex.ID)

vertex_list = [vertex(ID) for ID in range(1,6+1)]
face_list = set()
edge_list = set()
edges.sort(key=lambda tup: tup[0] + tup[1]/10.0)
for (a,b) in edges:
    vertex_list[a-1].connect(vertex_list[b-1])
    vertex_list[b-1].connect(vertex_list[a-1])
    common = vertex_list[a-1].connected & vertex_list[b-1].connected
    if (common):
        for x in common:
            if not set([(x, a),(a, b),(b, x)]) & edge_list:
                face_list.add((x, a, b))
                edge_list.update([(x, a),(a, b),(b, x)])

            elif not set([(a, x),(x, b),(b, a)]) & edge_list:
                face_list.add((a, x, b))
                edge_list.update([(a, x),(x, b),(b, a)])

for face in face_list:
    print face
M4rtini
  • 13,186
  • 4
  • 35
  • 42
  • What's the final else clause covering? – Eric Dec 21 '13 at 21:46
  • The case where x,a,b and a,x,b is a invalid ordering. The last permutation should then be the correct one. b,x,a – M4rtini Dec 21 '13 at 21:48
  • Pretty sure this makes assumptions about the initial ordering of the `edges` array – Eric Dec 21 '13 at 21:48
  • If `b,x,a` is a valid ordering, then `x,a,b` must also be one - they are cyclic permutations of each other – Eric Dec 21 '13 at 21:48
  • [here's](https://gist.github.com/eric-wieser/21856f063beb144aa38b) my version of your code. Your answer's definitely helpful, but it's not quite there yet – Eric Dec 21 '13 at 21:52
  • ahh of course, i'll just remove the else clause then. Is this giving your wrong results? – M4rtini Dec 21 '13 at 21:54
  • I added a `random.shuffle` on the `edges` array, and it unfortunately hits that else sometimes - the problem arises when you try and build two disconnected "islands" of correct faces and edges, and then find they don't match – Eric Dec 21 '13 at 21:57
  • Yep, tested on my large dataset - this correctly works out large groups of faces, but some groups have opposite orderings to others – Eric Dec 21 '13 at 22:01
  • Not only must you check that there are no conflicting edges with the current face, but also that there is at least one _matching_ edge – Eric Dec 21 '13 at 22:05
  • hmm, so you could do the test to check for one matching edge, and if there is no matching edge put it back in the queue? Or there might be a smarter way to do the connecting so you always connect to the previously defined faces – M4rtini Dec 21 '13 at 22:08
  • Exactly, I was going for a queue based solution, but I think that may be too slow – Eric Dec 21 '13 at 22:09
  • Is it enough to simply sort the edge list? – M4rtini Dec 21 '13 at 22:10
  • How are you determining what's wrong for the bigger dataset? Sorting the edge list gave the right result for the sample data. – M4rtini Dec 21 '13 at 22:24
  • I have a large mesh I'm rendering with opengl, that I can inspect for holes. I think you've pushed me towards a solution though – Eric Dec 21 '13 at 22:36
  • Just tried creating islands of combined faces and then merging them when they overlap, much like I did in [this question](http://stackoverflow.com/q/2254697/102441), but that was too slow – Eric Dec 21 '13 at 22:40
1

Implementation of this answer

from collections import defaultdict, deque
import itertools

def facetize(edges):
    """turn a set of edges into a set of consistently numbered faces"""

    # build lookups for vertices
    adjacent_vertices = defaultdict(set)
    for a, b in edges:
        adjacent_vertices[a] |= {b}
        adjacent_vertices[b] |= {a}

    orderless_faces = set()
    adjacent_faces = defaultdict(set)

    for a, b in edges:
        # create faces initially with increasing vertex numbers
        f1, f2 = (
            tuple(sorted([a, b, c]))
            for c in adjacent_vertices[a] & adjacent_vertices[b]
        )

        orderless_faces |= {f1, f2}
        adjacent_faces[f1] |= {f2}
        adjacent_faces[f2] |= {f1}


    def conflict(f1, f2):
        """returns true if the order of two faces conflict with one another"""
        return any(
            e1 == e2
            for e1, e2 in itertools.product(
                (f1[0:2], f1[1:3], f1[2:3] + f1[0:1]),
                (f2[0:2], f2[1:3], f2[2:3] + f2[0:1])
            )
        )

    # state for BFS
    processed = set()
    to_visit = deque()

    # result of BFS
    needs_flip = {}

    # define the first face as requiring no flip
    first = next(orderless_faces)
    needs_flip[first] = False
    to_visit.append(first)

    while to_visit:
        face = to_visit.popleft()
        for next_face in adjacent_faces[face]:
            if next_face not in processed:
                processed.add(next_face)
                to_visit.append(next_face)
                if conflict(next_face, face):
                    needs_flip[next_face] = not needs_flip[face]
                else:
                    needs_flip[next_face] = needs_flip[face]


    return [f[::-1] if needs_flip[f] else f for f in orderless_faces]
Community
  • 1
  • 1
Eric
  • 95,302
  • 53
  • 242
  • 374