Skip to content

Commit c9322ab

Browse files
Keith RobertsCHLNDDEV
andauthored
mesh "cleaning" (#13)
* Some essential mesh "cleaning" abilities enabled. Co-authored-by: CHLNDDEV <40673418+CHLNDDEV@users.noreply.github.com>
1 parent d085a33 commit c9322ab

File tree

12 files changed

+615
-431
lines changed

12 files changed

+615
-431
lines changed

.circleci/config.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ version: 2
33
jobs:
44
lint:
55
docker:
6-
- image: circleci/python:3
6+
- image: circleci/python:3.8.1
77
steps:
88
- checkout
99
- run: pip install -U black flake8 --user
@@ -19,7 +19,8 @@ jobs:
1919
- run: sudo apt-get install libmpfr-dev
2020
- run: sudo apt-get install libboost-all-dev
2121
- run: git clone https://github.com/CGAL/cgal.git && cd cgal/ && mkdir build && cd build && cmake -DCMAKE_BUILD_TYPE=Release .. && sudo make install
22-
- run: pip install -e .
22+
- run: pip install numpy --user
23+
- run: pip install -e . --user
2324
- run: pip install -U pytest pytest-cov
2425
- run:
2526
command: pytest --cov oceanmesh

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ Build a simple mesh around New York witha minimum element size of 1 km expanding
5050

5151
**Here we use the GSHHS shoreline [here](http://www.soest.hawaii.edu/pwessel/gshhg/gshhg-shp-2.3.7.zip) and the Python package `meshio` to write the mesh to a VTK file for visualization in ParaView.**
5252

53-
![NewYorkMesh](https://user-images.githubusercontent.com/18619644/94013474-8196b400-fd80-11ea-8471-21fa8853f264.png)
53+
![NewYorkMesh](https://user-images.githubusercontent.com/18619644/102819581-7587b600-43b2-11eb-9410-fbf3cadf95b9.png)
54+
5455

5556
```
5657
import meshio
@@ -77,6 +78,9 @@ Build a simple mesh around New York witha minimum element size of 1 km expanding
7778
domain=domain, cell_size=cell_size, h0=1e3 / 111e3
7879
)
7980
81+
# remove degenerate mesh faces and other common problems in the mesh
82+
points, cells = make_mesh_boundaries_traversable(points, cells)
83+
8084
meshio.write_points_cells(
8185
"simple_new_york.vtk",
8286
points,

oceanmesh/__init__.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
1+
from .fix_mesh import fix_mesh, simp_vol
12
from oceanmesh.edgefx import distance_sizing_function
23
from oceanmesh.edges import draw_edges, get_poly_edges
34
from oceanmesh.geodata import DEM, Geodata, Shoreline
45
from oceanmesh.grid import Grid
56
from oceanmesh.signed_distance_function import signed_distance_function, Domain
7+
from oceanmesh.clean import (
8+
delete_interior_faces,
9+
delete_exterior_faces,
10+
make_mesh_boundaries_traversable,
11+
)
612

713
from .cpp.delaunay_class import DelaunayTriangulation
8-
from .fix_mesh import fix_mesh, simp_vol
9-
from .inpoly import inpoly
1014
from .mesh_generator import generate_mesh
1115

1216
__all__ = [
17+
"make_mesh_boundaries_traversable",
18+
"delete_interior_faces",
19+
"delete_exterior_faces",
1320
"SizeFunction",
1421
"Grid",
1522
"Geodata",
@@ -20,9 +27,9 @@
2027
"signed_distance_function",
2128
"get_poly_edges",
2229
"draw_edges",
23-
"inpoly",
2430
"DelaunayTriangulation",
2531
"generate_mesh",
2632
"fix_mesh",
2733
"simp_vol",
34+
"simp_qual",
2835
]

oceanmesh/clean.py

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
import copy
2+
3+
import numpy as np
4+
5+
from .fix_mesh import fix_mesh, simp_vol, simp_qual
6+
from . import edges
7+
8+
__all__ = [
9+
"make_mesh_boundaries_traversable",
10+
"delete_interior_faces",
11+
"delete_exterior_faces",
12+
]
13+
14+
15+
def _arg_sortrows(arr):
16+
"""Before a multi column sort like MATLAB's sortrows"""
17+
i = arr[:, 1].argsort() # First sort doesn't need to be stable.
18+
j = arr[i, 0].argsort(kind="mergesort")
19+
return i[j]
20+
21+
22+
def _face_to_face(t):
23+
"""Face to face connectivity table.
24+
Face `i` is connected to faces `ftof[ix[i]:ix[i+1], 1]`
25+
By connected, I mean shares a mutual edge.
26+
27+
Parameters
28+
----------
29+
t: array-like
30+
Mesh connectivity table.
31+
32+
Returns
33+
-------
34+
ftof: array-like
35+
Face numbers connected to faces.
36+
ix: array-like
37+
indices into `ftof`
38+
39+
"""
40+
nt = len(t)
41+
t = np.sort(t, axis=1)
42+
e = t[:, [[0, 1], [0, 2], [1, 2]]].reshape((nt * 3, 2))
43+
trinum = np.repeat(np.arange(nt), 3)
44+
j = _arg_sortrows(e)
45+
e = e[j, :]
46+
trinum = trinum[j]
47+
k = np.argwhere(~np.diff(e, axis=0).any(axis=1))
48+
ftof = np.concatenate((trinum[k], trinum[k + 1]), axis=1)
49+
dmy1 = ftof[:, 0].argsort()
50+
dmy2 = ftof[:, 1].argsort()
51+
tmp = np.vstack(
52+
(
53+
ftof[dmy1, :],
54+
np.fliplr(ftof[dmy2]),
55+
np.column_stack((np.arange(nt), np.arange(nt))),
56+
)
57+
)
58+
j = _arg_sortrows(tmp)
59+
ftof = tmp[j, :]
60+
ix = np.argwhere(np.diff(ftof[:, 0])) + 1
61+
ix = np.insert(ix, 0, 0)
62+
ix = np.append(ix, len(ftof))
63+
return ftof, ix
64+
65+
66+
def _vertex_to_face(vertices, faces):
67+
"""Determine which faces are connected to which vertices.
68+
69+
Parameters
70+
----------
71+
vertices: array-like
72+
Vertices of the mesh.
73+
faces: array-like
74+
Mesh connectivity table.
75+
76+
Returns
77+
-------
78+
vtoc: array-like
79+
face numbers connected to vertices.
80+
ix: array-like
81+
indices into `vtoc`
82+
83+
"""
84+
num_faces = len(faces)
85+
86+
ext = np.tile(np.arange(0, num_faces), (3, 1)).reshape(-1, order="F")
87+
ve = np.reshape(faces, (-1,))
88+
ve = np.vstack((ve, ext)).T
89+
ve = ve[ve[:, 0].argsort(), :]
90+
91+
idx = np.insert(np.diff(ve[:, 0]), 0, 0)
92+
vtoc_pointer = np.argwhere(idx)
93+
vtoc_pointer = np.insert(vtoc_pointer, 0, 0)
94+
vtoc_pointer = np.append(vtoc_pointer, num_faces * 3)
95+
96+
vtoc = ve[:, 1]
97+
98+
return vtoc, vtoc_pointer
99+
100+
101+
def make_mesh_boundaries_traversable(vertices, faces, dj_cutoff=0.05):
102+
"""
103+
A mesh described by vertices and faces is "cleaned" and returned.
104+
Alternates between checking "interior" and "exterior" portions
105+
of the mesh until convergence is obtained. Convergence is defined as:
106+
having no vertices connected to more than two boundary edges.
107+
108+
Parameters
109+
----------
110+
vertices: array-like
111+
The vertices of the "uncleaned" mesh.
112+
faces: array-like
113+
The "uncleaned" mesh connectivity.
114+
dj_cutoff: float
115+
A decimal percentage (max 1.0) used to decide whether to keep or remove
116+
disconnected portions of the meshing domain.
117+
118+
119+
Returns
120+
-------
121+
vertices: array-like
122+
The vertices of the "cleaned" mesh.
123+
124+
faces: array-like
125+
The "cleaned" mesh connectivity.
126+
127+
Notes
128+
-----
129+
130+
Interior Check: Deletes faces that are within the interior of the
131+
mesh so that no vertices are connected to more than two boundary edges. For
132+
example, a barrier island could become very thin in a middle portion so that you
133+
have a vertex connected to two faces but four boundary edges, in a
134+
bow-tie type formation.
135+
136+
This code will delete one of those connecting
137+
faces to ensure the spit is `clean` in the sense that two boundary edges
138+
are connected to that vertex. In the case of a choice between faces to
139+
delete, the one with the lowest quality is chosen.
140+
141+
Exterior Check: Finds small disjoint portions of the mesh and removes
142+
them using a depth-first search. The individual disjoint portions are
143+
removed based on `dj_cutoff` which is a decimal representing a fractional
144+
threshold component of the total mesh.
145+
146+
"""
147+
148+
boundary_edges, boundary_vertices = _external_topology(vertices, faces)
149+
150+
# NB: when this inequality is not met, the mesh boundary is not valid and non-manifold
151+
while len(boundary_edges) > len(boundary_vertices):
152+
153+
faces = delete_exterior_faces(vertices, faces, dj_cutoff)
154+
vertices, faces, _ = fix_mesh(vertices, faces, delete_unused=True)
155+
156+
faces, _ = delete_interior_faces(vertices, faces)
157+
vertices, faces, _ = fix_mesh(vertices, faces, delete_unused=True)
158+
159+
boundary_edges, boundary_vertices = _external_topology(vertices, faces)
160+
161+
return vertices, faces
162+
163+
164+
def _external_topology(vertices, faces):
165+
"""Get edges and vertices that make up the boundary of the mesh"""
166+
boundary_edges = edges.get_boundary_edges(faces)
167+
boundary_vertices = vertices[np.unique(boundary_edges.reshape(-1))]
168+
return boundary_edges, boundary_vertices
169+
170+
171+
def delete_exterior_faces(vertices, faces, dj_cutoff):
172+
"""Deletes portions of the mesh that are "outside" or not
173+
connected to the majority which represent a fractional
174+
area less than `dj_cutoff`.
175+
"""
176+
t1 = copy.deepcopy(faces)
177+
t = np.array([])
178+
# Calculate the total area of the patch
179+
A = np.sum(simp_vol(vertices, faces))
180+
An = A
181+
# Based on area proportion
182+
while (An / A) > dj_cutoff:
183+
# Perform the depth-First-Search to get `nflag`
184+
nflag = _depth_first_search(vertices, t1)
185+
186+
# Get new triangulation and its area
187+
t2 = t1[nflag == 1, :]
188+
An = np.sum(simp_vol(vertices, t2))
189+
190+
# If large enough, retain this component
191+
if (An / A) > dj_cutoff:
192+
if len(t) == 0:
193+
t = t2
194+
else:
195+
t = np.concatenate((t, t2))
196+
197+
# Delete where nflag == 1 from tmp t1 mesh
198+
t1 = np.delete(t1, nflag == 1, axis=0)
199+
print(f"ACCEPTED: Deleting {int(np.sum(nflag==0))} faces outside the main mesh")
200+
201+
# Calculate the remaining area
202+
An = np.sum(simp_vol(vertices, t1))
203+
204+
return t
205+
206+
207+
def delete_interior_faces(vertices, faces):
208+
"""Delete interior faces that have vertices with more than
209+
two vertices declared as boundary vertices
210+
"""
211+
# Get updated boundary topology
212+
boundary_edges, boundary_vertices = _external_topology(vertices, faces)
213+
etbv = boundary_edges.reshape(-1)
214+
# Count how many edges a vertex appears in.
215+
uebtv, count = np.unique(etbv, return_counts=True)
216+
# Get the faces connected to the vertices
217+
vtoc, nne = _vertex_to_face(vertices, faces)
218+
# Vertices which appear more than twice (implying they are shared by
219+
# more than two boundary edges)
220+
del_face_idx = []
221+
for ix in uebtv[count > 2]:
222+
conn_faces = vtoc[nne[ix] : nne[ix + 1]]
223+
del_face = []
224+
for conn_face in conn_faces:
225+
II = etbv == faces[conn_face, 0]
226+
JJ = etbv == faces[conn_face, 1]
227+
KK = etbv == faces[conn_face, 2]
228+
if np.any(II) and np.any(JJ) and np.any(KK):
229+
del_face.append(conn_face)
230+
231+
if len(del_face) == 1:
232+
del_face_idx.append(del_face[0])
233+
elif len(del_face) > 1:
234+
# Delete worst quality qualifying face.
235+
qual = simp_qual(vertices, faces[del_face])
236+
idx = np.argmin(qual)
237+
del_face_idx.append(del_face[idx])
238+
else:
239+
# No connected faces have all vertices on boundary edge so we
240+
# select the worst quality connecting face.
241+
qual = simp_qual(vertices, faces[conn_faces])
242+
idx = np.argmin(qual)
243+
del_face_idx.append(conn_faces[idx])
244+
245+
print(f"ACCEPTED: Deleting {len(del_face_idx)} faces inside the main mesh")
246+
faces = np.delete(faces, del_face_idx, 0)
247+
248+
return faces, del_face_idx
249+
250+
251+
def _depth_first_search(points, faces):
252+
"""Depth-First-Search (DFS) across the triangulation"""
253+
254+
# Get graph connectivity.
255+
ftof, idx = _face_to_face(faces)
256+
257+
nt = len(faces)
258+
259+
# select a random face
260+
selected = np.random.randint(0, nt, 1)
261+
262+
nflag = np.zeros(nt)
263+
264+
searching = True
265+
266+
visited = []
267+
visited.append(*selected)
268+
269+
# Traverse through connected mesh
270+
while searching:
271+
searching = False
272+
for c in visited:
273+
# Flag the current face as visited
274+
nflag[c] = 1
275+
# Search connected faces
276+
neis = [nei for nei in ftof[idx[c] : idx[c + 1], 1]]
277+
# Flag connected faces as visited
278+
for nei in neis:
279+
if nflag[nei] == 0:
280+
nflag[nei] = 1
281+
visited.append(nei)
282+
searching = True
283+
return nflag

0 commit comments

Comments
 (0)