city_retrofit/venv/lib/python3.7/site-packages/trimesh/geometry.py

439 lines
13 KiB
Python
Raw Normal View History

import numpy as np
from . import util
from .constants import log
try:
import scipy.sparse
except BaseException as E:
from . import exceptions
# raise E again if anyone tries to use sparse
scipy = exceptions.ExceptionModule(E)
def plane_transform(origin, normal):
"""
Given the origin and normal of a plane find the transform
that will move that plane to be coplanar with the XY plane.
Parameters
----------
origin : (3,) float
Point that lies on the plane
normal : (3,) float
Vector that points along normal of plane
Returns
---------
transform: (4,4) float
Transformation matrix to move points onto XY plane
"""
transform = align_vectors(normal, [0, 0, 1])
transform[0:3, 3] = -np.dot(transform,
np.append(origin, 1))[0:3]
return transform
def align_vectors(a, b, return_angle=False):
"""
Find the rotation matrix that transforms one 3D vector
to another.
Parameters
------------
a : (3,) float
Unit vector
b : (3,) float
Unit vector
return_angle : bool
Return the angle between vectors or not
Returns
-------------
matrix : (4, 4) float
Homogeneous transform to rotate from `a` to `b`
angle : float
If `return_angle` angle in radians between `a` and `b`
"""
a = np.array(a, dtype=np.float64)
b = np.array(b, dtype=np.float64)
if a.shape != (3,) or b.shape != (3,):
raise ValueError('vectors must be (3,)!')
# find the SVD of the two vectors
au = np.linalg.svd(a.reshape((-1, 1)))[0]
bu = np.linalg.svd(b.reshape((-1, 1)))[0]
if np.linalg.det(au) < 0:
au[:, -1] *= -1.0
if np.linalg.det(bu) < 0:
bu[:, -1] *= -1.0
# put rotation into homogeneous transformation
matrix = np.eye(4)
matrix[:3, :3] = bu.dot(au.T)
if return_angle:
# projection of a onto b
# first row of SVD result is normalized source vector
dot = np.dot(au[0], bu[0])
# clip to avoid floating point error
angle = np.arccos(np.clip(dot, -1.0, 1.0))
if dot < -1e-5:
angle += np.pi
return matrix, angle
return matrix
def faces_to_edges(faces, return_index=False):
"""
Given a list of faces (n,3), return a list of edges (n*3,2)
Parameters
-----------
faces : (n, 3) int
Vertex indices representing faces
Returns
-----------
edges : (n*3, 2) int
Vertex indices representing edges
"""
faces = np.asanyarray(faces)
# each face has three edges
edges = faces[:, [0, 1, 1, 2, 2, 0]].reshape((-1, 2))
if return_index:
# edges are in order of faces due to reshape
face_index = np.tile(np.arange(len(faces)),
(3, 1)).T.reshape(-1)
return edges, face_index
return edges
def vector_angle(pairs):
"""
Find the angles between pairs of unit vectors.
Parameters
----------
pairs : (n, 2, 3) float
Unit vector pairs
Returns
----------
angles : (n,) float
Angles between vectors in radians
"""
pairs = np.asanyarray(pairs, dtype=np.float64)
if len(pairs) == 0:
return np.array([])
elif util.is_shape(pairs, (2, 3)):
pairs = pairs.reshape((-1, 2, 3))
elif not util.is_shape(pairs, (-1, 2, (2, 3))):
raise ValueError('pairs must be (n,2,(2|3))!')
# do the dot product between vectors
dots = util.diagonal_dot(pairs[:, 0], pairs[:, 1])
# clip for floating point error
dots = np.clip(dots, -1.0, 1.0)
# do cos and remove arbitrary sign
angles = np.abs(np.arccos(dots))
return angles
def triangulate_quads(quads):
"""
Given a set of quad faces, return them as triangle faces.
Parameters
-----------
quads: (n, 4) int
Vertex indices of quad faces
Returns
-----------
faces : (m, 3) int
Vertex indices of triangular faces
"""
if len(quads) == 0:
return quads
quads = np.asanyarray(quads)
faces = np.vstack((quads[:, [0, 1, 2]],
quads[:, [2, 3, 0]]))
return faces
def vertex_face_indices(vertex_count,
faces,
faces_sparse):
"""
Find vertex face indices from the faces array of vertices
Parameters
-----------
vertex_count : int
The number of vertices faces refer to
faces : (n, 3) int
List of vertex indices
faces_sparse : scipy.sparse.COO
Sparse matrix
Returns
-----------
vertex_faces : (vertex_count, ) int
Face indices for every vertex
Array padded with -1 in each row for all vertices with fewer
face indices than the max number of face indices.
"""
# Create 2D array with row for each vertex and
# length of max number of faces for a vertex
try:
counts = np.bincount(
faces.flatten(), minlength=vertex_count)
except TypeError:
# casting failed on 32 bit Windows
log.warning('casting failed, falling back!')
# fall back to np.unique (usually ~35x slower than bincount)
counts = np.unique(faces.flatten(), return_counts=True)[1]
assert len(counts) == vertex_count
assert faces.max() < vertex_count
# start cumulative sum at zero and clip off the last value
starts = np.append(0, np.cumsum(counts)[:-1])
# pack incrementing array into final shape
pack = np.arange(counts.max()) + starts[:, None]
# pad each row with -1 to pad to the max length
padded = -(pack >= (starts + counts)[:, None]).astype(np.int64)
try:
# do most of the work with a sparse dot product
identity = scipy.sparse.identity(len(faces), dtype=int)
sorted_faces = faces_sparse.dot(identity).nonzero()[1]
# this will fail if any face was degenerate
# TODO
# figure out how to filter out degenerate faces from sparse
# result if sorted_faces.size != faces.size
padded[padded == 0] = sorted_faces
except BaseException:
# fall back to a slow loop
log.warning('vertex_faces falling back to slow loop! ' +
'mesh probably has degenerate faces',
exc_info=True)
sort = np.zeros(faces.size, dtype=np.int64)
flat = faces.flatten()
for v in range(vertex_count):
# assign the data in order
sort[starts[v]:starts[v] + counts[v]] = (np.where(flat == v)[0] // 3)[::-1]
padded[padded == 0] = sort
return padded
def mean_vertex_normals(vertex_count,
faces,
face_normals,
sparse=None,
**kwargs):
"""
Find vertex normals from the mean of the faces that contain
that vertex.
Parameters
-----------
vertex_count : int
The number of vertices faces refer to
faces : (n, 3) int
List of vertex indices
face_normals : (n, 3) float
Normal vector for each face
Returns
-----------
vertex_normals : (vertex_count, 3) float
Normals for every vertex
Vertices unreferenced by faces will be zero.
"""
def summed_sparse():
# use a sparse matrix of which face contains each vertex to
# figure out the summed normal at each vertex
# allow cached sparse matrix to be passed
if sparse is None:
matrix = index_sparse(vertex_count, faces)
else:
matrix = sparse
summed = matrix.dot(face_normals)
return summed
def summed_loop():
# loop through every face, in tests was ~50x slower than
# doing this with a sparse matrix
summed = np.zeros((vertex_count, 3))
for face, normal in zip(faces, face_normals):
summed[face] += normal
return summed
try:
summed = summed_sparse()
except BaseException:
log.warning(
'unable to use sparse matrix, falling back!',
exc_info=True)
summed = summed_loop()
# invalid normals will be returned as zero
vertex_normals = util.unitize(summed)
return vertex_normals
def weighted_vertex_normals(vertex_count,
faces,
face_normals,
face_angles,
use_loop=False):
"""
Compute vertex normals from the faces that contain that vertex.
The contibution of a face's normal to a vertex normal is the
ratio of the corner-angle in which the vertex is, with respect
to the sum of all corner-angles surrounding the vertex.
Grit Thuerrner & Charles A. Wuethrich (1998)
Computing Vertex Normals from Polygonal Facets,
Journal of Graphics Tools, 3:1, 43-46
Parameters
-----------
vertex_count : int
The number of vertices faces refer to
faces : (n, 3) int
List of vertex indices
face_normals : (n, 3) float
Normal vector for each face
face_angles : (n, 3) float
Angles at each vertex in the face
Returns
-----------
vertex_normals : (vertex_count, 3) float
Normals for every vertex
Vertices unreferenced by faces will be zero.
"""
def summed_sparse():
# use a sparse matrix of which face contains each vertex to
# figure out the summed normal at each vertex
# allow cached sparse matrix to be passed
# fill the matrix with vertex-corner angles as weights
corner_angles = face_angles[np.repeat(np.arange(len(faces)), 3),
np.argsort(faces, axis=1).ravel()]
# create a sparse matrix
matrix = index_sparse(vertex_count, faces).astype(np.float64)
# assign the corner angles to the sparse matrix data
matrix.data = corner_angles
return matrix.dot(face_normals)
def summed_loop():
summed = np.zeros((vertex_count, 3), np.float64)
for vertex_idx in np.arange(vertex_count):
# loop over all vertices
# compute normal contributions from surrounding faces
# obviously slower than with the sparse matrix
face_idxs, inface_idxs = np.where(faces == vertex_idx)
surrounding_angles = face_angles[face_idxs, inface_idxs]
summed[vertex_idx] = np.dot(
surrounding_angles /
surrounding_angles.sum(),
face_normals[face_idxs])
return summed
# normals should be unit vectors
face_ok = (face_normals ** 2).sum(axis=1) > 0.5
# don't consider faces with invalid normals
faces = faces[face_ok]
face_normals = face_normals[face_ok]
face_angles = face_angles[face_ok]
if not use_loop:
try:
return util.unitize(summed_sparse())
except BaseException:
log.warning(
'unable to use sparse matrix, falling back!',
exc_info=True)
# we either crashed or were asked to loop
return util.unitize(summed_loop())
def index_sparse(columns, indices, data=None):
"""
Return a sparse matrix for which vertices are contained in which faces.
A data vector can be passed which is then used instead of booleans
Parameters
------------
columns : int
Number of columns, usually number of vertices
indices : (m, d) int
Usually mesh.faces
Returns
---------
sparse: scipy.sparse.coo_matrix of shape (columns, len(faces))
dtype is boolean
Examples
----------
In [1]: sparse = faces_sparse(len(mesh.vertices), mesh.faces)
In [2]: sparse.shape
Out[2]: (12, 20)
In [3]: mesh.faces.shape
Out[3]: (20, 3)
In [4]: mesh.vertices.shape
Out[4]: (12, 3)
In [5]: dense = sparse.toarray().astype(int)
In [6]: dense
Out[6]:
array([[1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0],
[0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0],
[0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1],
[1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0],
[0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0],
[0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1],
[0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1]])
In [7]: dense.sum(axis=0)
Out[7]: array([3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3])
"""
indices = np.asanyarray(indices)
columns = int(columns)
row = indices.reshape(-1)
col = np.tile(np.arange(len(indices)).reshape(
(-1, 1)), (1, indices.shape[1])).reshape(-1)
shape = (columns, len(indices))
if data is None:
data = np.ones(len(col), dtype=np.bool)
# assemble into sparse matrix
matrix = scipy.sparse.coo_matrix((data, (row, col)),
shape=shape,
dtype=data.dtype)
return matrix