forked from s_ranjbar/city_retrofit
352 lines
11 KiB
Python
352 lines
11 KiB
Python
|
"""
|
||
|
repair.py
|
||
|
-------------
|
||
|
|
||
|
Fill holes and fix winding and normals of meshes.
|
||
|
"""
|
||
|
|
||
|
import numpy as np
|
||
|
|
||
|
from . import graph
|
||
|
from . import triangles
|
||
|
|
||
|
from .constants import log
|
||
|
from .grouping import group_rows
|
||
|
from .geometry import faces_to_edges
|
||
|
|
||
|
|
||
|
try:
|
||
|
import networkx as nx
|
||
|
except BaseException as E:
|
||
|
# create a dummy module which will raise the ImportError
|
||
|
# or other exception only when someone tries to use networkx
|
||
|
from .exceptions import ExceptionModule
|
||
|
nx = ExceptionModule(E)
|
||
|
|
||
|
|
||
|
def fix_winding(mesh):
|
||
|
"""
|
||
|
Traverse and change mesh faces in-place to make sure winding
|
||
|
is correct, with edges on adjacent faces in
|
||
|
opposite directions.
|
||
|
|
||
|
Parameters
|
||
|
-------------
|
||
|
mesh: Trimesh object
|
||
|
|
||
|
Alters
|
||
|
-------------
|
||
|
mesh.face: will reverse columns of certain faces
|
||
|
"""
|
||
|
# anything we would fix is already done
|
||
|
if mesh.is_winding_consistent:
|
||
|
return
|
||
|
|
||
|
graph_all = nx.from_edgelist(mesh.face_adjacency)
|
||
|
flipped = 0
|
||
|
|
||
|
faces = mesh.faces.view(np.ndarray).copy()
|
||
|
|
||
|
# we are going to traverse the graph using BFS
|
||
|
# start a traversal for every connected component
|
||
|
for components in nx.connected_components(graph_all):
|
||
|
# get a subgraph for this component
|
||
|
g = graph_all.subgraph(components)
|
||
|
# get the first node in the graph in a way that works on nx's
|
||
|
# new API and their old API
|
||
|
start = next(iter(g.nodes()))
|
||
|
|
||
|
# we traverse every pair of faces in the graph
|
||
|
# we modify mesh.faces and mesh.face_normals in place
|
||
|
for face_pair in nx.bfs_edges(g, start):
|
||
|
# for each pair of faces, we convert them into edges,
|
||
|
# find the edge that both faces share and then see if edges
|
||
|
# are reversed in order as you would expect
|
||
|
# (2, ) int
|
||
|
face_pair = np.ravel(face_pair)
|
||
|
# (2, 3) int
|
||
|
pair = faces[face_pair]
|
||
|
# (6, 2) int
|
||
|
edges = faces_to_edges(pair)
|
||
|
overlap = group_rows(np.sort(edges, axis=1),
|
||
|
require_count=2)
|
||
|
if len(overlap) == 0:
|
||
|
# only happens on non-watertight meshes
|
||
|
continue
|
||
|
edge_pair = edges[overlap[0]]
|
||
|
if edge_pair[0][0] == edge_pair[1][0]:
|
||
|
# if the edges aren't reversed, invert the order of one face
|
||
|
flipped += 1
|
||
|
faces[face_pair[1]] = faces[face_pair[1]][::-1]
|
||
|
|
||
|
if flipped > 0:
|
||
|
mesh.faces = faces
|
||
|
|
||
|
log.debug('flipped %d/%d edges', flipped, len(mesh.faces) * 3)
|
||
|
|
||
|
|
||
|
def fix_inversion(mesh, multibody=False):
|
||
|
"""
|
||
|
Check to see if a mesh has normals pointing "out."
|
||
|
|
||
|
Parameters
|
||
|
-------------
|
||
|
mesh: Trimesh object
|
||
|
multibody: bool, if True will try to fix normals on every body
|
||
|
|
||
|
Alters
|
||
|
-------------
|
||
|
mesh.face: may reverse faces
|
||
|
"""
|
||
|
if multibody:
|
||
|
groups = graph.connected_components(mesh.face_adjacency)
|
||
|
# escape early for single body
|
||
|
if len(groups) == 1:
|
||
|
if mesh.volume < 0.0:
|
||
|
mesh.invert()
|
||
|
return
|
||
|
# mask of faces to flip
|
||
|
flip = np.zeros(len(mesh.faces), dtype=np.bool)
|
||
|
# save these to avoid thrashing cache
|
||
|
tri = mesh.triangles
|
||
|
cross = mesh.triangles_cross
|
||
|
# indexes of mesh.faces, not actual faces
|
||
|
for faces in groups:
|
||
|
# calculate the volume of the submesh faces
|
||
|
volume = triangles.mass_properties(
|
||
|
tri[faces],
|
||
|
crosses=cross[faces],
|
||
|
skip_inertia=True)['volume']
|
||
|
# if that volume is negative it is either
|
||
|
# inverted or just total garbage
|
||
|
if volume < 0.0:
|
||
|
flip[faces] = True
|
||
|
# one or more faces needs flipping
|
||
|
if flip.any():
|
||
|
with mesh._cache:
|
||
|
# flip normals of necessary faces
|
||
|
if 'face_normals' in mesh._cache:
|
||
|
mesh.face_normals[flip] *= -1.0
|
||
|
# flip faces
|
||
|
mesh.faces[flip] = np.fliplr(mesh.faces[flip])
|
||
|
# save wangled normals
|
||
|
mesh._cache.clear(exclude=['face_normals'])
|
||
|
|
||
|
elif mesh.volume < 0.0:
|
||
|
# reverse every triangles and flip every normals
|
||
|
mesh.invert()
|
||
|
|
||
|
|
||
|
def fix_normals(mesh, multibody=False):
|
||
|
"""
|
||
|
Fix the winding and direction of a mesh face and
|
||
|
face normals in-place.
|
||
|
|
||
|
Really only meaningful on watertight meshes but will orient all
|
||
|
faces and winding in a uniform way for non-watertight face
|
||
|
patches as well.
|
||
|
|
||
|
Parameters
|
||
|
-------------
|
||
|
mesh : trimesh.Trimesh
|
||
|
Mesh to fix normals on
|
||
|
multibody : bool
|
||
|
if True try to correct normals direction
|
||
|
on every body rather than just one
|
||
|
|
||
|
Alters
|
||
|
--------------
|
||
|
mesh.faces
|
||
|
Will flip columns on inverted faces
|
||
|
"""
|
||
|
# traverse face adjacency to correct winding
|
||
|
fix_winding(mesh)
|
||
|
# check to see if a mesh is inverted
|
||
|
fix_inversion(mesh, multibody=multibody)
|
||
|
|
||
|
|
||
|
def broken_faces(mesh, color=None):
|
||
|
"""
|
||
|
Return the index of faces in the mesh which break the
|
||
|
watertight status of the mesh.
|
||
|
|
||
|
Parameters
|
||
|
--------------
|
||
|
mesh: Trimesh object
|
||
|
color: (4,) uint8, will set broken faces to this color
|
||
|
None, will not alter mesh colors
|
||
|
|
||
|
Returns
|
||
|
---------------
|
||
|
broken: (n, ) int, indexes of mesh.faces
|
||
|
"""
|
||
|
adjacency = nx.from_edgelist(mesh.face_adjacency)
|
||
|
broken = [k for k, v in dict(adjacency.degree()).items()
|
||
|
if v != 3]
|
||
|
broken = np.array(broken)
|
||
|
if color is not None and broken.size != 0:
|
||
|
# if someone passed a broken color
|
||
|
color = np.array(color)
|
||
|
if not (color.shape == (4,) or color.shape == (3,)):
|
||
|
color = [255, 0, 0, 255]
|
||
|
mesh.visual.face_colors[broken] = color
|
||
|
return broken
|
||
|
|
||
|
|
||
|
def fill_holes(mesh):
|
||
|
"""
|
||
|
Fill single- triangle holes on triangular meshes by adding
|
||
|
new triangles to fill the holes. New triangles will have
|
||
|
proper winding and normals, and if face colors exist the color
|
||
|
of the last face will be assigned to the new triangles.
|
||
|
|
||
|
Parameters
|
||
|
---------
|
||
|
mesh : trimesh.Trimesh
|
||
|
Mesh will be repaired in- place
|
||
|
"""
|
||
|
|
||
|
def hole_to_faces(hole):
|
||
|
"""
|
||
|
Given a loop of vertex indices representing a hole, turn it into
|
||
|
triangular faces.
|
||
|
If unable to do so, return None
|
||
|
|
||
|
Parameters
|
||
|
---------
|
||
|
hole: ordered loop of vertex indices
|
||
|
|
||
|
Returns
|
||
|
---------
|
||
|
(n, 3) new faces
|
||
|
(m, 3) new vertices
|
||
|
"""
|
||
|
hole = np.asanyarray(hole)
|
||
|
# the case where the hole is just a single missing triangle
|
||
|
if len(hole) == 3:
|
||
|
return [hole], []
|
||
|
# the hole is a quad, which we fill with two triangles
|
||
|
if len(hole) == 4:
|
||
|
face_A = hole[[0, 1, 2]]
|
||
|
face_B = hole[[2, 3, 0]]
|
||
|
return [face_A, face_B], []
|
||
|
return [], []
|
||
|
|
||
|
if len(mesh.faces) < 3:
|
||
|
return False
|
||
|
|
||
|
if mesh.is_watertight:
|
||
|
return True
|
||
|
|
||
|
# we know that in a watertight mesh every edge will be included twice
|
||
|
# thus every edge which appears only once is part of a hole boundary
|
||
|
boundary_groups = group_rows(
|
||
|
mesh.edges_sorted, require_count=1)
|
||
|
|
||
|
# mesh is not watertight and we have too few edges
|
||
|
# edges to do a repair
|
||
|
# since we haven't changed anything return False
|
||
|
if len(boundary_groups) < 3:
|
||
|
return False
|
||
|
|
||
|
boundary_edges = mesh.edges[boundary_groups]
|
||
|
index_as_dict = [{'index': i} for i in boundary_groups]
|
||
|
|
||
|
# we create a graph of the boundary edges, and find cycles.
|
||
|
g = nx.from_edgelist(
|
||
|
np.column_stack((boundary_edges,
|
||
|
index_as_dict)))
|
||
|
cycles = np.array(nx.cycle_basis(g))
|
||
|
new_faces = []
|
||
|
new_vertex = []
|
||
|
for hole in cycles:
|
||
|
# convert the hole, which is a polygon of vertex indices
|
||
|
# to triangles and new vertices
|
||
|
faces, vertex = hole_to_faces(hole=hole)
|
||
|
if len(faces) == 0:
|
||
|
continue
|
||
|
# remeshing returns new vertices as negative indices, so change those
|
||
|
# to absolute indices which won't be screwed up by the later appends
|
||
|
faces = np.array(faces)
|
||
|
faces[faces < 0] += len(new_vertex) + len(mesh.vertices) + len(vertex)
|
||
|
new_vertex.extend(vertex)
|
||
|
new_faces.extend(faces)
|
||
|
new_faces = np.array(new_faces)
|
||
|
new_vertex = np.array(new_vertex)
|
||
|
|
||
|
if len(new_faces) == 0:
|
||
|
# no new faces have been added, so nothing further to do
|
||
|
# the mesh is NOT watertight, as boundary groups exist
|
||
|
# but we didn't add any new faces to fill them in
|
||
|
return False
|
||
|
|
||
|
for face_index, face in enumerate(new_faces):
|
||
|
# we compare the edge from the new face with
|
||
|
# the boundary edge from the source mesh
|
||
|
edge_test = face[0:2]
|
||
|
edge_boundary = mesh.edges[g.get_edge_data(*edge_test)['index']]
|
||
|
|
||
|
# in a well construtced mesh, the winding is such that adjacent triangles
|
||
|
# have reversed edges to each other. Here we check to make sure the
|
||
|
# edges are reversed, and if they aren't we simply reverse the face
|
||
|
reversed = edge_test[0] == edge_boundary[1]
|
||
|
if not reversed:
|
||
|
new_faces[face_index] = face[::-1]
|
||
|
|
||
|
# stack vertices into clean (n, 3) float
|
||
|
if len(new_vertex) != 0:
|
||
|
new_vertices = np.vstack((mesh.vertices, new_vertex))
|
||
|
else:
|
||
|
new_vertices = mesh.vertices
|
||
|
|
||
|
# try to save face normals if we can
|
||
|
if 'face_normals' in mesh._cache.cache:
|
||
|
cached_normals = mesh._cache.cache['face_normals']
|
||
|
else:
|
||
|
cached_normals = None
|
||
|
|
||
|
# also we can remove any zero are triangles by masking here
|
||
|
new_normals, valid = triangles.normals(new_vertices[new_faces])
|
||
|
# all the added faces were broken
|
||
|
if not valid.any():
|
||
|
return False
|
||
|
|
||
|
# this is usually the case where two vertices of a triangle are just
|
||
|
# over tol.merge apart, but the normal calculation is screwed up
|
||
|
# these could be fixed by merging the vertices in question here:
|
||
|
# if not valid.all():
|
||
|
if mesh.visual.defined and mesh.visual.kind == 'face':
|
||
|
color = mesh.visual.face_colors
|
||
|
else:
|
||
|
color = None
|
||
|
|
||
|
# apply the new faces and vertices
|
||
|
mesh.faces = np.vstack((mesh._data['faces'], new_faces[valid]))
|
||
|
mesh.vertices = new_vertices
|
||
|
|
||
|
# dump the cache and set id to the new hash
|
||
|
mesh._cache.verify()
|
||
|
|
||
|
# save us a normals recompute if we can
|
||
|
if cached_normals is not None:
|
||
|
mesh.face_normals = np.vstack((cached_normals,
|
||
|
new_normals))
|
||
|
|
||
|
# this is usually the case where two vertices of a triangle are just
|
||
|
# over tol.merge apart, but the normal calculation is screwed up
|
||
|
# these could be fixed by merging the vertices in question here:
|
||
|
# if not valid.all():
|
||
|
if color is not None:
|
||
|
# if face colors exist, assign the last face color to the new faces
|
||
|
# note that this is a little cheesey, but it is very inexpensive and
|
||
|
# is the right thing to do if the mesh is a single color.
|
||
|
color_shape = np.shape(color)
|
||
|
if len(color_shape) == 2:
|
||
|
new_colors = np.tile(color[-1], (np.sum(valid), 1))
|
||
|
new_colors = np.vstack((color,
|
||
|
new_colors))
|
||
|
mesh.visual.face_colors = new_colors
|
||
|
|
||
|
log.debug('Filled in mesh with %i triangles', np.sum(valid))
|
||
|
return mesh.is_watertight
|