forked from s_ranjbar/city_retrofit
439 lines
13 KiB
Python
439 lines
13 KiB
Python
|
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
|