forked from s_ranjbar/city_retrofit
185 lines
5.3 KiB
Python
185 lines
5.3 KiB
Python
|
"""
|
||
|
curvature.py
|
||
|
---------------
|
||
|
|
||
|
Query mesh curvature.
|
||
|
"""
|
||
|
import numpy as np
|
||
|
|
||
|
from . import util
|
||
|
|
||
|
try:
|
||
|
from scipy.sparse.coo import coo_matrix
|
||
|
except ImportError:
|
||
|
pass
|
||
|
|
||
|
|
||
|
def face_angles_sparse(mesh):
|
||
|
"""
|
||
|
A sparse matrix representation of the face angles.
|
||
|
|
||
|
Returns
|
||
|
----------
|
||
|
sparse: scipy.sparse.coo_matrix with:
|
||
|
dtype: float
|
||
|
shape: (len(mesh.vertices), len(mesh.faces))
|
||
|
"""
|
||
|
matrix = coo_matrix((mesh.face_angles.flatten(),
|
||
|
(mesh.faces_sparse.row, mesh.faces_sparse.col)),
|
||
|
mesh.faces_sparse.shape)
|
||
|
return matrix
|
||
|
|
||
|
|
||
|
def vertex_defects(mesh):
|
||
|
"""
|
||
|
Return the vertex defects, or (2*pi) minus the sum of the angles
|
||
|
of every face that includes that vertex.
|
||
|
|
||
|
If a vertex is only included by coplanar triangles, this
|
||
|
will be zero. For convex regions this is positive, and
|
||
|
concave negative.
|
||
|
|
||
|
Returns
|
||
|
--------
|
||
|
vertex_defect : (len(self.vertices), ) float
|
||
|
Vertex defect at the every vertex
|
||
|
"""
|
||
|
angle_sum = np.asarray(mesh.face_angles_sparse.sum(axis=1)).flatten()
|
||
|
defect = (2 * np.pi) - angle_sum
|
||
|
return defect
|
||
|
|
||
|
|
||
|
def discrete_gaussian_curvature_measure(mesh, points, radius):
|
||
|
"""
|
||
|
Return the discrete gaussian curvature measure of a sphere centered
|
||
|
at a point as detailed in 'Restricted Delaunay triangulations and normal
|
||
|
cycle', Cohen-Steiner and Morvan.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
points : (n,3) float, list of points in space
|
||
|
radius : float, the sphere radius
|
||
|
|
||
|
Returns
|
||
|
--------
|
||
|
gaussian_curvature: (n,) float, discrete gaussian curvature measure.
|
||
|
"""
|
||
|
|
||
|
points = np.asanyarray(points, dtype=np.float64)
|
||
|
if not util.is_shape(points, (-1, 3)):
|
||
|
raise ValueError('points must be (n,3)!')
|
||
|
|
||
|
nearest = mesh.kdtree.query_ball_point(points, radius)
|
||
|
gauss_curv = [mesh.vertex_defects[vertices].sum() for vertices in nearest]
|
||
|
|
||
|
return np.asarray(gauss_curv)
|
||
|
|
||
|
|
||
|
def discrete_mean_curvature_measure(mesh, points, radius):
|
||
|
"""
|
||
|
Return the discrete mean curvature measure of a sphere centered
|
||
|
at a point as detailed in 'Restricted Delaunay triangulations and normal
|
||
|
cycle', Cohen-Steiner and Morvan.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
points : (n,3) float, list of points in space
|
||
|
radius : float, the sphere radius
|
||
|
|
||
|
Returns
|
||
|
--------
|
||
|
mean_curvature: (n,) float, discrete mean curvature measure.
|
||
|
"""
|
||
|
|
||
|
points = np.asanyarray(points, dtype=np.float64)
|
||
|
if not util.is_shape(points, (-1, 3)):
|
||
|
raise ValueError('points must be (n,3)!')
|
||
|
|
||
|
# axis aligned bounds
|
||
|
bounds = np.column_stack((points - radius,
|
||
|
points + radius))
|
||
|
|
||
|
# line segments that intersect axis aligned bounding box
|
||
|
candidates = [list(mesh.face_adjacency_tree.intersection(b))
|
||
|
for b in bounds]
|
||
|
|
||
|
mean_curv = np.empty(len(points))
|
||
|
for i, (x, x_candidates) in enumerate(zip(points, candidates)):
|
||
|
endpoints = mesh.vertices[mesh.face_adjacency_edges[x_candidates]]
|
||
|
lengths = line_ball_intersection(
|
||
|
endpoints[:, 0],
|
||
|
endpoints[:, 1],
|
||
|
center=x,
|
||
|
radius=radius)
|
||
|
angles = mesh.face_adjacency_angles[x_candidates]
|
||
|
signs = np.where(mesh.face_adjacency_convex[x_candidates], 1, -1)
|
||
|
mean_curv[i] = (lengths * angles * signs).sum() / 2
|
||
|
|
||
|
return mean_curv
|
||
|
|
||
|
|
||
|
def line_ball_intersection(start_points, end_points, center, radius):
|
||
|
"""
|
||
|
Compute the length of the intersection of a line segment with a ball.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
start_points : (n,3) float, list of points in space
|
||
|
end_points : (n,3) float, list of points in space
|
||
|
center : (3,) float, the sphere center
|
||
|
radius : float, the sphere radius
|
||
|
|
||
|
Returns
|
||
|
--------
|
||
|
lengths: (n,) float, the lengths.
|
||
|
|
||
|
"""
|
||
|
|
||
|
# We solve for the intersection of |x-c|**2 = r**2 and
|
||
|
# x = o + dL. This yields
|
||
|
# d = (-l.(o-c) +- sqrt[ l.(o-c)**2 - l.l((o-c).(o-c) - r^**2) ]) / l.l
|
||
|
L = end_points - start_points
|
||
|
oc = start_points - center # o-c
|
||
|
r = radius
|
||
|
ldotl = np.einsum('ij, ij->i', L, L) # l.l
|
||
|
ldotoc = np.einsum('ij, ij->i', L, oc) # l.(o-c)
|
||
|
ocdotoc = np.einsum('ij, ij->i', oc, oc) # (o-c).(o-c)
|
||
|
discrims = ldotoc**2 - ldotl * (ocdotoc - r**2)
|
||
|
|
||
|
# If discriminant is non-positive, then we have zero length
|
||
|
lengths = np.zeros(len(start_points))
|
||
|
# Otherwise we solve for the solns with d2 > d1.
|
||
|
m = discrims > 0 # mask
|
||
|
d1 = (-ldotoc[m] - np.sqrt(discrims[m])) / ldotl[m]
|
||
|
d2 = (-ldotoc[m] + np.sqrt(discrims[m])) / ldotl[m]
|
||
|
|
||
|
# Line segment means we have 0 <= d <= 1
|
||
|
d1 = np.clip(d1, 0, 1)
|
||
|
d2 = np.clip(d2, 0, 1)
|
||
|
|
||
|
# Length is |o + d2 l - o + d1 l| = (d2 - d1) |l|
|
||
|
lengths[m] = (d2 - d1) * np.sqrt(ldotl[m])
|
||
|
|
||
|
return lengths
|
||
|
|
||
|
|
||
|
def sphere_ball_intersection(R, r):
|
||
|
"""
|
||
|
Compute the surface area of the intersection of sphere of radius R centered
|
||
|
at (0, 0, 0) with a ball of radius r centered at (R, 0, 0).
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
R : float, sphere radius
|
||
|
r : float, ball radius
|
||
|
|
||
|
Returns
|
||
|
--------
|
||
|
area: float, the surface are.
|
||
|
"""
|
||
|
x = (2 * R**2 - r**2) / (2 * R) # x coord of plane
|
||
|
if x >= -R:
|
||
|
return 2 * np.pi * R * (R - x)
|
||
|
if x < -R:
|
||
|
return 4 * np.pi * R**2
|