hub/venv/lib/python3.7/site-packages/trimesh/repair.py

352 lines
11 KiB
Python
Raw Normal View History

"""
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