Coverage for trimesh/smoothing.py: 94%
114 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-08-31 18:21 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-08-31 18:21 +0000
1import numpy as np
3from .typed import ArrayLike
5try:
6 from scipy.sparse import coo_matrix, eye
7 from scipy.sparse.linalg import spsolve
8except ImportError as E:
9 from .exceptions import ExceptionWrapper
11 wrapper = ExceptionWrapper(E)
12 eye, spsolve, coo_matrix = wrapper, wrapper, wrapper
14from . import graph, triangles
15from .base import Trimesh
16from .geometry import index_sparse
17from .triangles import mass_properties
18from .util import unitize
21def filter_laplacian(
22 mesh,
23 lamb=0.5,
24 iterations=10,
25 implicit_time_integration=False,
26 volume_constraint=True,
27 laplacian_operator=None,
28):
29 """
30 Smooth a mesh in-place using laplacian smoothing.
31 Articles
32 1 - "Improved Laplacian Smoothing of Noisy Surface Meshes"
33 J. Vollmer, R. Mencl, and H. Muller
34 2 - "Implicit Fairing of Irregular Meshes using Diffusion
35 and Curvature Flow". M. Desbrun, M. Meyer,
36 P. Schroder, A.H.B. Caltech
37 Parameters
38 ------------
39 mesh : trimesh.Trimesh
40 Mesh to be smoothed in place
41 lamb : float
42 Diffusion speed constant
43 If 0.0, no diffusion
44 If > 0.0, diffusion occurs
45 implicit_time_integration: boolean
46 if False: explicit time integration
47 -lamb <= 1.0 - Stability Limit (Article 1)
48 if True: implicit time integration
49 -lamb no limit (Article 2)
50 iterations : int
51 Number of passes to run filter
52 volume_constraint : bool
53 If True, restore the initial volume after each iteration
54 by rescaling about the mesh center of mass
55 laplacian_operator : None or scipy.sparse.coo.coo_matrix
56 Sparse matrix laplacian operator
57 Will be autogenerated if None
58 """
60 # if the laplacian operator was not passed create it here
61 if laplacian_operator is None:
62 laplacian_operator = laplacian_calculation(mesh)
64 # save initial volume and center of mass
65 if volume_constraint:
66 vol_ini = mesh.volume
67 center_mass = mesh.center_mass
69 # get mesh vertices and faces as vanilla numpy array
70 vertices = mesh.vertices.copy().view(np.ndarray)
71 faces = mesh.faces.copy().view(np.ndarray)
73 # Set matrix for linear system of equations
74 if implicit_time_integration:
75 dlap = laplacian_operator.shape[0]
76 AA = eye(dlap) + lamb * (eye(dlap) - laplacian_operator)
78 # Number of passes
79 for _index in range(iterations):
80 # Classic Explicit Time Integration - Article 1
81 if not implicit_time_integration:
82 dot = laplacian_operator.dot(vertices) - vertices
83 vertices += lamb * dot
85 # Implicit Time Integration - Article 2
86 else:
87 vertices = spsolve(AA, vertices)
89 # volume constraint
90 if volume_constraint:
91 # find the volume with new vertex positions
92 vol_new = triangles.mass_properties(vertices[faces], skip_inertia=True)[
93 "volume"
94 ]
95 # scale by volume ratio
96 scale = (vol_ini / vol_new) ** (1.0 / 3.0)
97 vertices = (vertices - center_mass) * scale + center_mass
99 # assign modified vertices back to mesh
100 mesh.vertices = vertices
101 return mesh
104def filter_humphrey(mesh, alpha=0.1, beta=0.5, iterations=10, laplacian_operator=None):
105 """
106 Smooth a mesh in-place using laplacian smoothing
107 and Humphrey filtering.
108 Articles
109 "Improved Laplacian Smoothing of Noisy Surface Meshes"
110 J. Vollmer, R. Mencl, and H. Muller
111 Parameters
112 ------------
113 mesh : trimesh.Trimesh
114 Mesh to be smoothed in place
115 alpha : float
116 Controls shrinkage, range is 0.0 - 1.0
117 If 0.0, not considered
118 If 1.0, no smoothing
119 beta : float
120 Controls how aggressive smoothing is
121 If 0.0, no smoothing
122 If 1.0, full aggressiveness
123 iterations : int
124 Number of passes to run filter
125 laplacian_operator : None or scipy.sparse.coo.coo_matrix
126 Sparse matrix laplacian operator
127 Will be autogenerated if None
128 """
129 # if the laplacian operator was not passed create it here
130 if laplacian_operator is None:
131 laplacian_operator = laplacian_calculation(mesh)
133 # get mesh vertices as vanilla numpy array
134 vertices = mesh.vertices.copy().view(np.ndarray)
135 # save original unmodified vertices
136 original = vertices.copy()
138 # run through iterations of filter
139 for _index in range(iterations):
140 vert_q = vertices.copy()
141 vertices = laplacian_operator.dot(vertices)
142 vert_b = vertices - (alpha * original + (1.0 - alpha) * vert_q)
143 vertices -= beta * vert_b + (1.0 - beta) * laplacian_operator.dot(vert_b)
145 # assign modified vertices back to mesh
146 mesh.vertices = vertices
147 return mesh
150def filter_taubin(mesh, lamb=0.5, nu=0.5, iterations=10, laplacian_operator=None):
151 """
152 Smooth a mesh in-place using laplacian smoothing
153 and taubin filtering.
154 Articles
155 "Improved Laplacian Smoothing of Noisy Surface Meshes"
156 J. Vollmer, R. Mencl, and H. Muller
157 Parameters
158 ------------
159 mesh : trimesh.Trimesh
160 Mesh to be smoothed in place.
161 lamb : float
162 Controls shrinkage, range is 0.0 - 1.0
163 nu : float
164 Controls dilation, range is 0.0 - 1.0
165 Nu shall be between 0.0 < 1.0/lambda - 1.0/nu < 0.1
166 iterations : int
167 Number of passes to run the filter
168 laplacian_operator : None or scipy.sparse.coo.coo_matrix
169 Sparse matrix laplacian operator
170 Will be autogenerated if None
171 """
172 # if the laplacian operator was not passed create it here
173 if laplacian_operator is None:
174 laplacian_operator = laplacian_calculation(mesh)
176 # get mesh vertices as vanilla numpy array
177 vertices = mesh.vertices.copy().view(np.ndarray)
179 # run through multiple passes of the filter
180 for index in range(iterations):
181 # do a sparse dot product on the vertices
182 dot = laplacian_operator.dot(vertices) - vertices
183 # alternate shrinkage and dilation
184 if index % 2 == 0:
185 vertices += lamb * dot
186 else:
187 vertices -= nu * dot
189 # assign updated vertices back to mesh
190 mesh.vertices = vertices
191 return mesh
194def filter_mut_dif_laplacian(
195 mesh, lamb=0.5, iterations=10, volume_constraint=True, laplacian_operator=None
196):
197 """
198 Smooth a mesh in-place using laplacian smoothing using a
199 mutable diffusion laplacian.
201 Articles
202 Barroqueiro, B., Andrade-Campos, A., Dias-de-Oliveira,
203 J., and Valente, R. (January 21, 2021).
204 "Bridging between topology optimization and additive
205 manufacturing via Laplacian smoothing." ASME. J. Mech. Des.
208 Parameters
209 ------------
210 mesh : trimesh.Trimesh
211 Mesh to be smoothed in place
212 lamb : float
213 Diffusion speed constant
214 If 0.0, no diffusion
215 If > 0.0, diffusion occurs
216 iterations : int
217 Number of passes to run filter
218 laplacian_operator : None or scipy.sparse.coo.coo_matrix
219 Sparse matrix laplacian operator
220 Will be autogenerated if None
221 """
223 # if the laplacian operator was not passed create it here
224 if laplacian_operator is None:
225 laplacian_operator = laplacian_calculation(mesh)
227 # Set volume constraint
228 if volume_constraint:
229 v_ini = mesh.volume
231 # get mesh vertices as vanilla numpy array
232 vertices = mesh.vertices.copy().view(np.ndarray)
233 faces = mesh.faces.copy().view(np.ndarray)
234 eps = 0.01 * (np.max(mesh.area_faces) ** 0.5)
236 # Number of passes
237 for _index in range(iterations):
238 # Mutable diffusion
239 normals = get_vertices_normals(mesh)
240 qi = laplacian_operator.dot(vertices)
241 pi_qi = vertices - qi
243 adil = np.abs((normals * pi_qi).dot(np.ones((3, 1))))
244 adil = 1.0 / np.maximum(1e-12, adil)
245 lamber = np.maximum(0.2 * lamb, np.minimum(1.0, lamb * adil / np.mean(adil)))
247 # Filter
248 dot = laplacian_operator.dot(vertices)
249 vertices += lamber * (dot - vertices)
251 # Volume constraint
252 if volume_constraint:
253 vol = mass_properties(vertices[faces], skip_inertia=True)["volume"]
254 if _index == 0:
255 slope = dilate_slope(vertices, faces, normals, vol, eps)
256 vertices += normals * slope * (v_ini - vol)
258 # assign modified vertices back to mesh
259 mesh.vertices = vertices
261 return mesh
264def laplacian_calculation(
265 mesh: Trimesh,
266 equal_weight: bool = True,
267 pinned_vertices: ArrayLike | None = None,
268):
269 """
270 Calculate a sparse matrix for laplacian operations.
272 Note that setting equal_weight to False significantly hampers performance.
274 Parameters
275 -------------
276 mesh : trimesh.Trimesh
277 Input geometry
278 equal_weight : bool
279 If True, all neighbors will be considered equally
280 If False, all neighbors will be weighted by inverse distance
281 pinned_vertices : None or list of ints
282 If None, no vertices are pinned
283 If list, vertices will be pinned, such that they will not be moved
284 Returns
285 ----------
286 laplacian : scipy.sparse.coo.coo_matrix
287 Laplacian operator
288 """
289 if equal_weight:
290 laplacian = graph.edges_to_coo(mesh.edges)
292 if pinned_vertices is not None:
293 # Set pinned vertices to only have themselves as neighbours
294 laplacian.data[np.isin(laplacian.row, pinned_vertices)] = False
295 laplacian.row = np.concatenate((laplacian.row, pinned_vertices))
296 laplacian.col = np.concatenate((laplacian.col, pinned_vertices))
297 laplacian.data = np.concatenate(
298 (laplacian.data, np.ones(len(pinned_vertices), dtype=bool))
299 )
301 laplacian = laplacian / laplacian.sum(axis=1)
303 else:
304 # get the vertex neighbors from the cache
305 neighbors = mesh.vertex_neighbors
307 # if a node is pinned, it will average his coordinates by himself
308 # in practice it will not move
309 if pinned_vertices is not None:
310 for i in pinned_vertices:
311 neighbors[i] = [i]
313 # avoid hitting crc checks in loops
314 vertices = mesh.vertices.view(np.ndarray)
316 # stack neighbors to 1D arrays
317 col = np.concatenate(neighbors)
318 row = np.concatenate([[i] * len(n) for i, n in enumerate(neighbors)])
320 # umbrella weights, distance-weighted
321 # use dot product of ones to replace array.sum(axis=1)
322 ones = np.ones(3)
323 # the distance from verticesex to neighbors
324 norms = [
325 1.0
326 / np.maximum(1e-6, np.sqrt(np.dot((vertices[i] - vertices[n]) ** 2, ones)))
327 for i, n in enumerate(neighbors)
328 ]
329 # normalize group and stack into single array
330 data = np.concatenate([i / i.sum() for i in norms])
332 # create the sparse matrix
333 laplacian = coo_matrix((data, (row, col)), shape=[len(vertices)] * 2)
335 return laplacian
338def get_vertices_normals(mesh):
339 """
340 Compute Vertex normals using equal weighting of neighbors faces.
341 Parameters
342 -------------
343 mesh : trimesh.Trimesh
344 Input geometry
345 Returns
346 ----------
347 vertices_normals: array
348 Vertices normals
349 """
351 # get mesh vertices and faces
352 vertices = mesh.vertices
353 faces = mesh.faces
355 # get face normals
356 face_normals = mesh.face_normals
358 # Compute Vert normals
359 vert_normals = index_sparse(len(vertices), faces).dot(face_normals)
361 return unitize(vert_normals)
364def dilate_slope(vertices, faces, normals, v, eps):
365 """
366 Get the derivate of dilation scalar by the volume variation by finite differences
367 Thus, Vertices += vertex_normals*dilate_slope*(Initial_Volume - Srinked_Volume)
368 Parameters
369 -------------
370 mesh : trimesh.Trimesh
371 Input geometry
372 vertices: mesh.vertices
373 faces: mesh.faces
374 normals: array
375 vertices normals
376 Returns
377 ----------
378 dilate_slope: float
379 derivative
380 """
382 # finite difference derivative
383 vertices2 = vertices + normals * eps
384 v2 = mass_properties(vertices2[faces], skip_inertia=True)["volume"]
386 return (eps) / (v2 - v)