Coverage for trimesh/smoothing.py: 94%

114 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-08-31 18:21 +0000

1import numpy as np 

2 

3from .typed import ArrayLike 

4 

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 

10 

11 wrapper = ExceptionWrapper(E) 

12 eye, spsolve, coo_matrix = wrapper, wrapper, wrapper 

13 

14from . import graph, triangles 

15from .base import Trimesh 

16from .geometry import index_sparse 

17from .triangles import mass_properties 

18from .util import unitize 

19 

20 

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 """ 

59 

60 # if the laplacian operator was not passed create it here 

61 if laplacian_operator is None: 

62 laplacian_operator = laplacian_calculation(mesh) 

63 

64 # save initial volume and center of mass 

65 if volume_constraint: 

66 vol_ini = mesh.volume 

67 center_mass = mesh.center_mass 

68 

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) 

72 

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) 

77 

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 

84 

85 # Implicit Time Integration - Article 2 

86 else: 

87 vertices = spsolve(AA, vertices) 

88 

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 

98 

99 # assign modified vertices back to mesh 

100 mesh.vertices = vertices 

101 return mesh 

102 

103 

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) 

132 

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() 

137 

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) 

144 

145 # assign modified vertices back to mesh 

146 mesh.vertices = vertices 

147 return mesh 

148 

149 

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) 

175 

176 # get mesh vertices as vanilla numpy array 

177 vertices = mesh.vertices.copy().view(np.ndarray) 

178 

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 

188 

189 # assign updated vertices back to mesh 

190 mesh.vertices = vertices 

191 return mesh 

192 

193 

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. 

200 

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. 

206 

207 

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 """ 

222 

223 # if the laplacian operator was not passed create it here 

224 if laplacian_operator is None: 

225 laplacian_operator = laplacian_calculation(mesh) 

226 

227 # Set volume constraint 

228 if volume_constraint: 

229 v_ini = mesh.volume 

230 

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) 

235 

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 

242 

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))) 

246 

247 # Filter 

248 dot = laplacian_operator.dot(vertices) 

249 vertices += lamber * (dot - vertices) 

250 

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) 

257 

258 # assign modified vertices back to mesh 

259 mesh.vertices = vertices 

260 

261 return mesh 

262 

263 

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. 

271 

272 Note that setting equal_weight to False significantly hampers performance. 

273 

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) 

291 

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 ) 

300 

301 laplacian = laplacian / laplacian.sum(axis=1) 

302 

303 else: 

304 # get the vertex neighbors from the cache 

305 neighbors = mesh.vertex_neighbors 

306 

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] 

312 

313 # avoid hitting crc checks in loops 

314 vertices = mesh.vertices.view(np.ndarray) 

315 

316 # stack neighbors to 1D arrays 

317 col = np.concatenate(neighbors) 

318 row = np.concatenate([[i] * len(n) for i, n in enumerate(neighbors)]) 

319 

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]) 

331 

332 # create the sparse matrix 

333 laplacian = coo_matrix((data, (row, col)), shape=[len(vertices)] * 2) 

334 

335 return laplacian 

336 

337 

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 """ 

350 

351 # get mesh vertices and faces 

352 vertices = mesh.vertices 

353 faces = mesh.faces 

354 

355 # get face normals 

356 face_normals = mesh.face_normals 

357 

358 # Compute Vert normals 

359 vert_normals = index_sparse(len(vertices), faces).dot(face_normals) 

360 

361 return unitize(vert_normals) 

362 

363 

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 """ 

381 

382 # finite difference derivative 

383 vertices2 = vertices + normals * eps 

384 v2 = mass_properties(vertices2[faces], skip_inertia=True)["volume"] 

385 

386 return (eps) / (v2 - v)