Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cut edges functionality #87

Merged
merged 10 commits into from
Oct 19, 2023
Merged
2 changes: 2 additions & 0 deletions src/gpytoolbox/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,5 +107,7 @@
from .read_dmat import read_dmat
from .linear_blend_skinning import linear_blend_skinning
from .barycenters import barycenters
from .cut_edges import cut_edges
from .adjacency_matrix import adjacency_matrix
from .non_manifold_edges import non_manifold_edges
from .connected_components import connected_components
10 changes: 5 additions & 5 deletions src/gpytoolbox/boundary_loops.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import numpy as np
from gpytoolbox.boundary_edges import boundary_edges
from gpytoolbox.non_manifold_edges import non_manifold_edges


def boundary_loops(f, allow_wrong_orientations=True):
"""Computes oriented boundary loop for each boundary component of a triangle
mesh.
This function only works on manifold triangle meshes.

TODO: assert manifoldness when running this function
"""Computes a list containing the oriented boundary loop for each boundary component of a triangle mesh in the style of a sorted polyline. This function only works on connected (i.e., single component) manifold triangle meshes.

Parameters
----------
Expand All @@ -33,6 +30,9 @@ def boundary_loops(f, allow_wrong_orientations=True):
assert f.shape[0] > 0
assert f.shape[1] == 3

# check mesh is manifold
assert len(non_manifold_edges(f)) == 0, "Mesh is not manifold"

bE = boundary_edges(f)

#Loop through each boundary, edge, marking them as seen, until all have
Expand Down
105 changes: 105 additions & 0 deletions src/gpytoolbox/cut_edges.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import numpy as np
import scipy as sp

from .halfedges import halfedges
from .array_correspondence import array_correspondence
from .remove_unreferenced import remove_unreferenced

def cut_edges(F,E):
"""Cut a triangle mesh along a specified set of edges.

Given a triangle mesh and a set of edges, this returns a new mesh that has been "cut" along those edges; meaning, such that the two faces incident on that edge are no longer combinatorially connected. This is done by duplicating vertices along the cut edges (see note), and creating a new geometrically identical edge between the duplicated vertices.

Parameters
----------
F : (m,3) numpy int array
face index list of a triangle mesh
E : (k,2) numpy int array
index list of edges to cut, indexing the same array as F.
If E contains edges that are not present in F, they will be ignored.

Returns
-------
G : (m,3) numpy int array
face index list of cut triangle mesh
I : (l,) numpy int array
list of indices into V of vertices in new mesh such that V[I,:] are the
vertices in the new mesh.
This takes care of correctly duplicating vertices.

Notes
-----
Since only vertices that no longer share an edge in common are duplicated, you cannot cut a single interior edge. This function mirrors gptoolbox's cut_edges (https://github.com/alecjacobson/gptoolbox/blob/master/mesh/cut_edges.m)

Examples
--------
```python
_,F = gpy.read_mesh("mesh.obj")
E = np.array([[0,1], [1,2]])
G,I = gpy.cut_edges(F,G)
W = V[I,:]
# My new mesh is now W,G
```
"""

assert F.shape[0]>0, "F must be nonempty."
assert F.shape[1]==3, "Only works for triangle meshes."
n = np.max(F)+1
if E.size==0:
return np.arange(F.size[0]), np.arange(n)
assert E.shape[1]==2, "E is a (k,2) matrix."

# This code is a translation of https://github.com/alecjacobson/gptoolbox/blob/master/mesh/cut_edges.m by Alec Jacobson
he = halfedges(F)
flat_he = np.concatenate([he[:,0,:],he[:,1,:],he[:,2,:]], axis=0)
sorted_he = np.sort(flat_he, axis=1)
unique_he, unique_inverse = np.unique(sorted_he, axis=0,
return_inverse=True)
unique_he_to_F = sp.sparse.csr_matrix(
(np.ones(unique_inverse.shape[0]),
(unique_inverse,
np.tile(np.arange(F.shape[0]),3))),
shape=(unique_he.shape[0], F.shape[0])
)

FF = np.arange(3*F.shape[0]).reshape((-1,3), order='F')
sorted_unique_he = np.sort(unique_he, axis=1)
sorted_E = np.sort(E, axis=1)
isin_unique_he_but_not_E = np.nonzero(
array_correspondence(sorted_unique_he, sorted_E, axis=0) < 0)[0]
noncut = sp.sparse.csr_matrix(
(np.ones(isin_unique_he_but_not_E.shape[0]),
(isin_unique_he_but_not_E, isin_unique_he_but_not_E)),
shape=(unique_he.shape[0], unique_he.shape[0])
)
unique_he_to_EE = sp.sparse.csr_matrix(
(np.ones(unique_inverse.shape[0]),
(unique_inverse, np.arange(3*F.shape[0]))),
shape=(unique_he.shape[0], 3*F.shape[0])
)
I = np.arange(3*F.shape[0]).reshape(F.shape[0], 3, order='F')
VV_to_EE = sp.sparse.csr_matrix(
(np.ones(6*F.shape[0]),
(np.concatenate((FF[:,0],FF[:,1],FF[:,2],FF[:,0],FF[:,1],FF[:,2])),
np.concatenate((I[:,1],I[:,2],I[:,0],I[:,2],I[:,0],I[:,1])))),
shape=(3*F.shape[0], 3*F.shape[0])
)
VV_to_V = sp.sparse.csr_matrix(
(np.ones(I.size), (I.ravel(), F.ravel())),
shape=(3*F.shape[0], n)
)
A = (VV_to_EE * (unique_he_to_EE.T * noncut * unique_he_to_EE)
* VV_to_EE.T).multiply(VV_to_V * VV_to_V.T)

I = F.flatten(order='F')
VV = np.zeros(np.max(FF)+1) #dummy vertex data for remove unreferenced
_,labels = sp.sparse.csgraph.connected_components(A, return_labels=True)
VV[labels] = labels
I[labels] = I
FF = labels[FF]
W,IM = remove_unreferenced(VV[:,None],FF)
I = I[IM.ravel()<W.shape[0]]
G = IM.ravel(order='F')[FF]

return G,I

43 changes: 43 additions & 0 deletions src/gpytoolbox/non_manifold_edges.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import numpy as np
from gpytoolbox.halfedges import halfedges

def non_manifold_edges(F):
"""Given a triangle mesh with face indices F, returns (unoriented) edges that are non-manifold; i.e., edges that are incident to more than two faces.

Parameters
----------
F : (m,3) numpy int array
face index list of a triangle mesh

Returns
-------
ne : (n,2) numpy int array
list of unique non-manifold edges. Columns are sorted in ascending index order, and rows are sorted in lexicographic order.

Notes
-----
It would be nice to also have a non_manifold_vertices function that wraps 2D and 3D functionality.

Examples
--------
```python
from gpy import non_manifold_edges
# bad mesh with one non-manifold edge in [0,2]
f = np.array([[0,1,2],[0,2,3],[2,0,4]],dtype=int)
ne = gpy.non_manifold_edges(f)
# ne is now np.array([[0,2]])
```

"""

assert F.shape[1] == 3

he = halfedges(F).reshape(-1,2)
he = np.sort(he, axis=1)
# print(he)
he_u = np.unique(he, axis=0, return_counts=True)
# print(he)
ne = he_u[0][he_u[1]>2]

return ne

36 changes: 36 additions & 0 deletions test/test_cut_edges.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from .context import gpytoolbox as gpy
from .context import numpy as np
from .context import unittest

class TestCutEdges(unittest.TestCase):

def test_two_triangles(self):
F = np.array([[0,1,2], [1,2,3]],dtype=int)
E = np.array([[1,2]])
G,I = gpy.cut_edges(F,E)
self.assertTrue(np.all(G==np.array([[0,2,4],[1,3,5]])))
self.assertTrue(np.all(I==np.array([0,1,1,2,2,3])))

def test_icosphere(self):
V,F = gpy.icosphere(3)
E = np.array([[371,573], [573,571], [571,219]])
G,I = gpy.cut_edges(F,E)

b = gpy.boundary_loops(G)
self.assertTrue(b[0].size == 2*E.shape[0])
self.assertTrue(np.all(b[0] == np.array([149, 163, 278, 348, 164, 398])))

def test_bunny(self):
V,F = gpy.read_mesh("test/unit_tests_data/bunny_oded.obj")
path = np.array([1575,1482,1394,1309,1310,1225,1141])
E = np.stack((path[:-1],path[1:]), axis=-1)
G,I = gpy.cut_edges(F,E)

b = gpy.boundary_loops(G)
self.assertTrue(b[0].size == 2*E.shape[0])
self.assertTrue(np.all(b[0] == np.array(
[753, 218, 757, 264, 814, 244, 772, 307, 345, 316, 288, 251])))


if __name__ == '__main__':
unittest.main()
72 changes: 72 additions & 0 deletions test/test_non_manifold_edges.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from .context import gpytoolbox as gpy
from .context import numpy as np
from .context import unittest

class TestNonManifoldEdges(unittest.TestCase):
# There is not much to test here that goes beyond just inputting the
# definition of the function, but we can make sure that a few conditions
# are fulfilled.

def test_single_triangle(self):
f = np.array([[0,1,2]],dtype=int)
ne = gpy.non_manifold_edges(f)
# no non-manifold edges
self.assertTrue(len(ne)==0)

def test_simple_nonmanifold(self):
f = np.array([[0,1,2],[0,2,3],[2,0,4]],dtype=int)
ne = gpy.non_manifold_edges(f)
ne_gt = np.array([[0,2]],dtype=int)
self.assertTrue(np.all(ne==ne_gt))

def test_bunny(self):
_,f = gpy.read_mesh("test/unit_tests_data/bunny_oded.obj")
num_faces = f.shape[0]
he = gpy.halfedges(f).reshape(-1,2)

for it in range(100):
# pick a random edge in he
i = np.random.randint(he.shape[0])
random_edge = he[i,:]
# now we add a new face that contains this edge
new_face = np.array([random_edge[0],random_edge[1],num_faces],dtype=int)
# insert this face into a random position in f
f_bad = np.insert(f,np.random.randint(f.shape[0]),new_face,axis=0)
# are there any non-manifold edges?
ne = gpy.non_manifold_edges(f_bad)
self.assertTrue(ne.shape[0]==1)
# sort random_edge
random_edge = np.sort(random_edge)
# check that ne is random edge
self.assertTrue(np.all(ne==random_edge))
# print(random_edge)

# now let's add them sequentially
f = f_bad.copy()
ne_gt = ne.copy()
rng = np.random.default_rng(5)
for it in range(10):
# pick a random edge in he
i = rng.integers(he.shape[0])
random_edge = he[i,:]
# now we add a new face that contains this edge
new_face = np.array([random_edge[0],random_edge[1],num_faces+it],dtype=int)
# insert this face into a random position in f
f = np.insert(f,np.random.randint(f.shape[0]),new_face,axis=0)
# are there any non-manifold edges?
ne = gpy.non_manifold_edges(f)

self.assertTrue(ne.shape[0]==it+2)
# sort random_edge
random_edge = np.sort(random_edge)
ne_gt = np.vstack((ne_gt,random_edge))
# sort ne_gt lexicographically
ne_gt = np.unique(ne_gt,axis=0,)
# should match
self.assertTrue(np.all(ne==ne_gt))




if __name__ == '__main__':
unittest.main()
Loading