-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' into mesh-generation
- Loading branch information
Showing
7 changed files
with
291 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, return_maps=True) | ||
I = I[IM.ravel()<W.shape[0]] | ||
G = IM.ravel(order='F')[FF] | ||
|
||
return G,I | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
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], [2,1,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, 2, 1, 1, 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) | ||
print(b) | ||
self.assertTrue(b[0].size == 2*E.shape[0]) | ||
self.assertTrue(np.all(np.sort(b[0]) == np.sort( | ||
np.array([ | ||
557, | ||
278, | ||
641, | ||
437, | ||
507, | ||
642 | ||
])))) | ||
|
||
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(np.sort(b[0]) == np.sort( | ||
np.array([ | ||
761, | ||
790, | ||
811, | ||
865, | ||
927, | ||
929, | ||
952, | ||
1038, | ||
2501, | ||
2578, | ||
2596, | ||
2613 | ||
])))) | ||
|
||
|
||
if __name__ == '__main__': | ||
unittest.main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |