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

223 lines
6.7 KiB
Python
Raw Normal View History

"""
convex.py
Deal with creating and checking convex objects in 2, 3 and N dimensions.
Convex is defined as:
1) "Convex, meaning "curving out" or "extending outward" (compare to concave)
2) having an outline or surface curved like the exterior of a circle or sphere.
3) (of a polygon) having only interior angles measuring less than 180
"""
import numpy as np
from .constants import tol
from . import util
from . import triangles
try:
from scipy import spatial
except ImportError as E:
from .exceptions import ExceptionModule
spatial = ExceptionModule(E)
def convex_hull(obj, qhull_options='QbB Pp Qt'):
"""
Get a new Trimesh object representing the convex hull of the
current mesh attempting to return a watertight mesh with correct
normals.
Details on qhull options:
http://www.qhull.org/html/qh-quick.htm#options
Arguments
--------
obj : Trimesh, or (n,3) float
Mesh or cartesian points
qhull_options : str
Options to pass to qhull.
Returns
--------
convex : Trimesh
Mesh of convex hull
"""
from .base import Trimesh
if isinstance(obj, Trimesh):
points = obj.vertices.view(np.ndarray)
else:
# will remove subclassing
points = np.asarray(obj, dtype=np.float64)
if not util.is_shape(points, (-1, 3)):
raise ValueError('Object must be Trimesh or (n,3) points!')
hull = spatial.ConvexHull(points,
qhull_options=qhull_options)
# hull object doesn't remove unreferenced vertices
# create a mask to re- index faces for only referenced vertices
vid = np.sort(hull.vertices)
mask = np.zeros(len(hull.points), dtype=np.int64)
mask[vid] = np.arange(len(vid))
# remove unreferenced vertices here
faces = mask[hull.simplices].copy()
# rescale vertices back to original size
vertices = hull.points[vid].copy()
# qhull returns faces with random winding
# calculate the returned normal of each face
crosses = triangles.cross(vertices[faces])
# qhull returns zero magnitude faces like an asshole
normals, valid = util.unitize(crosses, check_valid=True)
# remove zero magnitude faces
faces = faces[valid]
crosses = crosses[valid]
# each triangle area and mean center
triangles_area = triangles.area(crosses=crosses, sum=False)
triangles_center = vertices[faces].mean(axis=1)
# since the convex hull is (hopefully) convex, the vector from
# the centroid to the center of each face
# should have a positive dot product with the normal of that face
# if it doesn't it is probably backwards
# note that this sometimes gets screwed up by precision issues
centroid = np.average(triangles_center,
weights=triangles_area,
axis=0)
# a vector from the centroid to a point on each face
test_vector = triangles_center - centroid
# check the projection against face normals
backwards = util.diagonal_dot(normals,
test_vector) < 0.0
# flip the winding outward facing
faces[backwards] = np.fliplr(faces[backwards])
# flip the normal
normals[backwards] *= -1.0
# save the work we did to the cache so it doesn't have to be recomputed
initial_cache = {'triangles_cross': crosses,
'triangles_center': triangles_center,
'area_faces': triangles_area,
'centroid': centroid}
# create the Trimesh object for the convex hull
convex = Trimesh(vertices=vertices,
faces=faces,
face_normals=normals,
initial_cache=initial_cache,
process=True,
validate=False)
# we did the gross case above, but sometimes precision issues
# leave some faces backwards anyway
# this call will exit early if the winding is consistent
# and if not will fix it by traversing the adjacency graph
convex.fix_normals(multibody=False)
# sometimes the QbB option will cause precision issues
# so try the hull again without it and
# check for qhull_options is None to avoid infinite recursion
if (qhull_options is not None and
not convex.is_winding_consistent):
return convex_hull(convex, qhull_options=None)
return convex
def adjacency_projections(mesh):
"""
Test if a mesh is convex by projecting the vertices of
a triangle onto the normal of its adjacent face.
Parameters
----------
mesh : Trimesh
Input geometry
Returns
----------
projection : (len(mesh.face_adjacency),) float
Distance of projection of adjacent vertex onto plane
"""
# normals and origins from the first column of face adjacency
normals = mesh.face_normals[mesh.face_adjacency[:, 0]]
# one of the vertices on the shared edge
origins = mesh.vertices[mesh.face_adjacency_edges[:, 0]]
# faces from the second column of face adjacency
vid_other = mesh.face_adjacency_unshared[:, 1]
vector_other = mesh.vertices[vid_other] - origins
# get the projection with a dot product
dots = util.diagonal_dot(vector_other, normals)
return dots
def is_convex(mesh):
"""
Check if a mesh is convex.
Parameters
-----------
mesh : Trimesh
Input geometry
Returns
-----------
convex : bool
Was passed mesh convex or not
"""
# non-watertight meshes are not convex
if not mesh.is_watertight:
return False
# don't consider zero- area faces
nonzero = mesh.area_faces > tol.merge
# adjacencies with two nonzero faces
adj_ok = nonzero[mesh.face_adjacency].all(axis=1)
# make threshold of convexity scale- relative
threshold = tol.planar * mesh.scale
# if projections of vertex onto plane of adjacent
# face is negative, it means the face pair is locally
# convex, and if that is true for all faces the mesh is convex
convex = bool(mesh.face_adjacency_projections[adj_ok].max() < threshold)
return convex
def hull_points(obj, qhull_options='QbB Pp'):
"""
Try to extract a convex set of points from multiple input formats.
Parameters
---------
obj: Trimesh object
(n,d) points
(m,) Trimesh objects
Returns
--------
points: (o,d) convex set of points
"""
if hasattr(obj, 'convex_hull'):
return obj.convex_hull.vertices
initial = np.asanyarray(obj, dtype=np.float64)
if len(initial.shape) != 2:
raise ValueError('points must be (n, dimension)!')
hull = spatial.ConvexHull(initial, qhull_options=qhull_options)
points = hull.points[hull.vertices]
return points