init
This commit is contained in:
@@ -0,0 +1,385 @@
|
||||
import numpy as np
|
||||
|
||||
from .typed import ArrayLike, Optional
|
||||
|
||||
try:
|
||||
from scipy.sparse import coo_matrix, eye
|
||||
from scipy.sparse.linalg import spsolve
|
||||
except ImportError as E:
|
||||
from .exceptions import ExceptionWrapper
|
||||
|
||||
wrapper = ExceptionWrapper(E)
|
||||
eye, spsolve, coo_matrix = wrapper, wrapper, wrapper
|
||||
|
||||
from . import graph, triangles
|
||||
from .base import Trimesh
|
||||
from .geometry import index_sparse
|
||||
from .triangles import mass_properties
|
||||
from .util import unitize
|
||||
|
||||
|
||||
def filter_laplacian(
|
||||
mesh,
|
||||
lamb=0.5,
|
||||
iterations=10,
|
||||
implicit_time_integration=False,
|
||||
volume_constraint=True,
|
||||
laplacian_operator=None,
|
||||
):
|
||||
"""
|
||||
Smooth a mesh in-place using laplacian smoothing.
|
||||
Articles
|
||||
1 - "Improved Laplacian Smoothing of Noisy Surface Meshes"
|
||||
J. Vollmer, R. Mencl, and H. Muller
|
||||
2 - "Implicit Fairing of Irregular Meshes using Diffusion
|
||||
and Curvature Flow". M. Desbrun, M. Meyer,
|
||||
P. Schroder, A.H.B. Caltech
|
||||
Parameters
|
||||
------------
|
||||
mesh : trimesh.Trimesh
|
||||
Mesh to be smoothed in place
|
||||
lamb : float
|
||||
Diffusion speed constant
|
||||
If 0.0, no diffusion
|
||||
If > 0.0, diffusion occurs
|
||||
implicit_time_integration: boolean
|
||||
if False: explicit time integration
|
||||
-lamb <= 1.0 - Stability Limit (Article 1)
|
||||
if True: implicit time integration
|
||||
-lamb no limit (Article 2)
|
||||
iterations : int
|
||||
Number of passes to run filter
|
||||
laplacian_operator : None or scipy.sparse.coo.coo_matrix
|
||||
Sparse matrix laplacian operator
|
||||
Will be autogenerated if None
|
||||
"""
|
||||
|
||||
# if the laplacian operator was not passed create it here
|
||||
if laplacian_operator is None:
|
||||
laplacian_operator = laplacian_calculation(mesh)
|
||||
|
||||
# save initial volume
|
||||
if volume_constraint:
|
||||
vol_ini = mesh.volume
|
||||
|
||||
# get mesh vertices and faces as vanilla numpy array
|
||||
vertices = mesh.vertices.copy().view(np.ndarray)
|
||||
faces = mesh.faces.copy().view(np.ndarray)
|
||||
|
||||
# Set matrix for linear system of equations
|
||||
if implicit_time_integration:
|
||||
dlap = laplacian_operator.shape[0]
|
||||
AA = eye(dlap) + lamb * (eye(dlap) - laplacian_operator)
|
||||
|
||||
# Number of passes
|
||||
for _index in range(iterations):
|
||||
# Classic Explicit Time Integration - Article 1
|
||||
if not implicit_time_integration:
|
||||
dot = laplacian_operator.dot(vertices) - vertices
|
||||
vertices += lamb * dot
|
||||
|
||||
# Implicit Time Integration - Article 2
|
||||
else:
|
||||
vertices = spsolve(AA, vertices)
|
||||
|
||||
# volume constraint
|
||||
if volume_constraint:
|
||||
# find the volume with new vertex positions
|
||||
vol_new = triangles.mass_properties(vertices[faces], skip_inertia=True)[
|
||||
"volume"
|
||||
]
|
||||
# scale by volume ratio
|
||||
vertices *= (vol_ini / vol_new) ** (1.0 / 3.0)
|
||||
|
||||
# assign modified vertices back to mesh
|
||||
mesh.vertices = vertices
|
||||
return mesh
|
||||
|
||||
|
||||
def filter_humphrey(mesh, alpha=0.1, beta=0.5, iterations=10, laplacian_operator=None):
|
||||
"""
|
||||
Smooth a mesh in-place using laplacian smoothing
|
||||
and Humphrey filtering.
|
||||
Articles
|
||||
"Improved Laplacian Smoothing of Noisy Surface Meshes"
|
||||
J. Vollmer, R. Mencl, and H. Muller
|
||||
Parameters
|
||||
------------
|
||||
mesh : trimesh.Trimesh
|
||||
Mesh to be smoothed in place
|
||||
alpha : float
|
||||
Controls shrinkage, range is 0.0 - 1.0
|
||||
If 0.0, not considered
|
||||
If 1.0, no smoothing
|
||||
beta : float
|
||||
Controls how aggressive smoothing is
|
||||
If 0.0, no smoothing
|
||||
If 1.0, full aggressiveness
|
||||
iterations : int
|
||||
Number of passes to run filter
|
||||
laplacian_operator : None or scipy.sparse.coo.coo_matrix
|
||||
Sparse matrix laplacian operator
|
||||
Will be autogenerated if None
|
||||
"""
|
||||
# if the laplacian operator was not passed create it here
|
||||
if laplacian_operator is None:
|
||||
laplacian_operator = laplacian_calculation(mesh)
|
||||
|
||||
# get mesh vertices as vanilla numpy array
|
||||
vertices = mesh.vertices.copy().view(np.ndarray)
|
||||
# save original unmodified vertices
|
||||
original = vertices.copy()
|
||||
|
||||
# run through iterations of filter
|
||||
for _index in range(iterations):
|
||||
vert_q = vertices.copy()
|
||||
vertices = laplacian_operator.dot(vertices)
|
||||
vert_b = vertices - (alpha * original + (1.0 - alpha) * vert_q)
|
||||
vertices -= beta * vert_b + (1.0 - beta) * laplacian_operator.dot(vert_b)
|
||||
|
||||
# assign modified vertices back to mesh
|
||||
mesh.vertices = vertices
|
||||
return mesh
|
||||
|
||||
|
||||
def filter_taubin(mesh, lamb=0.5, nu=0.5, iterations=10, laplacian_operator=None):
|
||||
"""
|
||||
Smooth a mesh in-place using laplacian smoothing
|
||||
and taubin filtering.
|
||||
Articles
|
||||
"Improved Laplacian Smoothing of Noisy Surface Meshes"
|
||||
J. Vollmer, R. Mencl, and H. Muller
|
||||
Parameters
|
||||
------------
|
||||
mesh : trimesh.Trimesh
|
||||
Mesh to be smoothed in place.
|
||||
lamb : float
|
||||
Controls shrinkage, range is 0.0 - 1.0
|
||||
nu : float
|
||||
Controls dilation, range is 0.0 - 1.0
|
||||
Nu shall be between 0.0 < 1.0/lambda - 1.0/nu < 0.1
|
||||
iterations : int
|
||||
Number of passes to run the filter
|
||||
laplacian_operator : None or scipy.sparse.coo.coo_matrix
|
||||
Sparse matrix laplacian operator
|
||||
Will be autogenerated if None
|
||||
"""
|
||||
# if the laplacian operator was not passed create it here
|
||||
if laplacian_operator is None:
|
||||
laplacian_operator = laplacian_calculation(mesh)
|
||||
|
||||
# get mesh vertices as vanilla numpy array
|
||||
vertices = mesh.vertices.copy().view(np.ndarray)
|
||||
|
||||
# run through multiple passes of the filter
|
||||
for index in range(iterations):
|
||||
# do a sparse dot product on the vertices
|
||||
dot = laplacian_operator.dot(vertices) - vertices
|
||||
# alternate shrinkage and dilation
|
||||
if index % 2 == 0:
|
||||
vertices += lamb * dot
|
||||
else:
|
||||
vertices -= nu * dot
|
||||
|
||||
# assign updated vertices back to mesh
|
||||
mesh.vertices = vertices
|
||||
return mesh
|
||||
|
||||
|
||||
def filter_mut_dif_laplacian(
|
||||
mesh, lamb=0.5, iterations=10, volume_constraint=True, laplacian_operator=None
|
||||
):
|
||||
"""
|
||||
Smooth a mesh in-place using laplacian smoothing using a
|
||||
mutable diffusion laplacian.
|
||||
|
||||
Articles
|
||||
Barroqueiro, B., Andrade-Campos, A., Dias-de-Oliveira,
|
||||
J., and Valente, R. (January 21, 2021).
|
||||
"Bridging between topology optimization and additive
|
||||
manufacturing via Laplacian smoothing." ASME. J. Mech. Des.
|
||||
|
||||
|
||||
Parameters
|
||||
------------
|
||||
mesh : trimesh.Trimesh
|
||||
Mesh to be smoothed in place
|
||||
lamb : float
|
||||
Diffusion speed constant
|
||||
If 0.0, no diffusion
|
||||
If > 0.0, diffusion occurs
|
||||
iterations : int
|
||||
Number of passes to run filter
|
||||
laplacian_operator : None or scipy.sparse.coo.coo_matrix
|
||||
Sparse matrix laplacian operator
|
||||
Will be autogenerated if None
|
||||
"""
|
||||
|
||||
# if the laplacian operator was not passed create it here
|
||||
if laplacian_operator is None:
|
||||
laplacian_operator = laplacian_calculation(mesh)
|
||||
|
||||
# Set volume constraint
|
||||
if volume_constraint:
|
||||
v_ini = mesh.volume
|
||||
|
||||
# get mesh vertices as vanilla numpy array
|
||||
vertices = mesh.vertices.copy().view(np.ndarray)
|
||||
faces = mesh.faces.copy().view(np.ndarray)
|
||||
eps = 0.01 * (np.max(mesh.area_faces) ** 0.5)
|
||||
|
||||
# Number of passes
|
||||
for _index in range(iterations):
|
||||
# Mutable diffusion
|
||||
normals = get_vertices_normals(mesh)
|
||||
qi = laplacian_operator.dot(vertices)
|
||||
pi_qi = vertices - qi
|
||||
|
||||
adil = np.abs((normals * pi_qi).dot(np.ones((3, 1))))
|
||||
adil = 1.0 / np.maximum(1e-12, adil)
|
||||
lamber = np.maximum(0.2 * lamb, np.minimum(1.0, lamb * adil / np.mean(adil)))
|
||||
|
||||
# Filter
|
||||
dot = laplacian_operator.dot(vertices)
|
||||
vertices += lamber * (dot - vertices)
|
||||
|
||||
# Volume constraint
|
||||
if volume_constraint:
|
||||
vol = mass_properties(vertices[faces], skip_inertia=True)["volume"]
|
||||
if _index == 0:
|
||||
slope = dilate_slope(vertices, faces, normals, vol, eps)
|
||||
vertices += normals * slope * (v_ini - vol)
|
||||
|
||||
# assign modified vertices back to mesh
|
||||
mesh.vertices = vertices
|
||||
|
||||
return mesh
|
||||
|
||||
|
||||
def laplacian_calculation(
|
||||
mesh: Trimesh,
|
||||
equal_weight: bool = True,
|
||||
pinned_vertices: Optional[ArrayLike] = None,
|
||||
):
|
||||
"""
|
||||
Calculate a sparse matrix for laplacian operations.
|
||||
|
||||
Note that setting equal_weight to False significantly hampers performance.
|
||||
|
||||
Parameters
|
||||
-------------
|
||||
mesh : trimesh.Trimesh
|
||||
Input geometry
|
||||
equal_weight : bool
|
||||
If True, all neighbors will be considered equally
|
||||
If False, all neighbors will be weighted by inverse distance
|
||||
pinned_vertices : None or list of ints
|
||||
If None, no vertices are pinned
|
||||
If list, vertices will be pinned, such that they will not be moved
|
||||
Returns
|
||||
----------
|
||||
laplacian : scipy.sparse.coo.coo_matrix
|
||||
Laplacian operator
|
||||
"""
|
||||
if equal_weight:
|
||||
laplacian = graph.edges_to_coo(mesh.edges)
|
||||
|
||||
if pinned_vertices is not None:
|
||||
# Set pinned vertices to only have themselves as neighbours
|
||||
laplacian.data[np.isin(laplacian.row, pinned_vertices)] = False
|
||||
laplacian.row = np.concatenate((laplacian.row, pinned_vertices))
|
||||
laplacian.col = np.concatenate((laplacian.col, pinned_vertices))
|
||||
laplacian.data = np.concatenate(
|
||||
(laplacian.data, np.ones(len(pinned_vertices), dtype=bool))
|
||||
)
|
||||
|
||||
# using this instead of the line below
|
||||
# laplacian = laplacian / laplacian.sum(axis=1)
|
||||
# because on Python 3.8 that returns a `numpy.matrix`
|
||||
# value instead of a `sparse.coo_matrix`
|
||||
laplacian = laplacian.multiply(1.0 / laplacian.sum(axis=1))
|
||||
|
||||
else:
|
||||
# get the vertex neighbors from the cache
|
||||
neighbors = mesh.vertex_neighbors
|
||||
|
||||
# if a node is pinned, it will average his coordinates by himself
|
||||
# in practice it will not move
|
||||
if pinned_vertices is not None:
|
||||
for i in pinned_vertices:
|
||||
neighbors[i] = [i]
|
||||
|
||||
# avoid hitting crc checks in loops
|
||||
vertices = mesh.vertices.view(np.ndarray)
|
||||
|
||||
# stack neighbors to 1D arrays
|
||||
col = np.concatenate(neighbors)
|
||||
row = np.concatenate([[i] * len(n) for i, n in enumerate(neighbors)])
|
||||
|
||||
# umbrella weights, distance-weighted
|
||||
# use dot product of ones to replace array.sum(axis=1)
|
||||
ones = np.ones(3)
|
||||
# the distance from verticesex to neighbors
|
||||
norms = [
|
||||
1.0
|
||||
/ np.maximum(1e-6, np.sqrt(np.dot((vertices[i] - vertices[n]) ** 2, ones)))
|
||||
for i, n in enumerate(neighbors)
|
||||
]
|
||||
# normalize group and stack into single array
|
||||
data = np.concatenate([i / i.sum() for i in norms])
|
||||
|
||||
# create the sparse matrix
|
||||
laplacian = coo_matrix((data, (row, col)), shape=[len(vertices)] * 2)
|
||||
|
||||
return laplacian
|
||||
|
||||
|
||||
def get_vertices_normals(mesh):
|
||||
"""
|
||||
Compute Vertex normals using equal weighting of neighbors faces.
|
||||
Parameters
|
||||
-------------
|
||||
mesh : trimesh.Trimesh
|
||||
Input geometry
|
||||
Returns
|
||||
----------
|
||||
vertices_normals: array
|
||||
Vertices normals
|
||||
"""
|
||||
|
||||
# get mesh vertices and faces
|
||||
vertices = mesh.vertices
|
||||
faces = mesh.faces
|
||||
|
||||
# get face normals
|
||||
face_normals = mesh.face_normals
|
||||
|
||||
# Compute Vert normals
|
||||
vert_normals = index_sparse(len(vertices), faces).dot(face_normals)
|
||||
|
||||
return unitize(vert_normals)
|
||||
|
||||
|
||||
def dilate_slope(vertices, faces, normals, v, eps):
|
||||
"""
|
||||
Get the derivate of dilation scalar by the volume variation by finite differences
|
||||
Thus, Vertices += vertex_normals*dilate_slope*(Initial_Volume - Srinked_Volume)
|
||||
Parameters
|
||||
-------------
|
||||
mesh : trimesh.Trimesh
|
||||
Input geometry
|
||||
vertices: mesh.vertices
|
||||
faces: mesh.faces
|
||||
normals: array
|
||||
vertices normals
|
||||
Returns
|
||||
----------
|
||||
dilate_slope: float
|
||||
derivative
|
||||
"""
|
||||
|
||||
# finite difference derivative
|
||||
vertices2 = vertices + normals * eps
|
||||
v2 = mass_properties(vertices2[faces], skip_inertia=True)["volume"]
|
||||
|
||||
return (eps) / (v2 - v)
|
||||
Reference in New Issue
Block a user