skeletor.pre
The skeletor.pre
module contains functions to pre-process meshes before
skeletonization.
Fixing faulty meshes
Some skeletonization methods are susceptible to faulty meshes (degenerate faces,
wrong normals, etc.). If your skeleton looks off, it might be worth a shot
trying to fix the mesh using skeletor.pre.fix_mesh()
.
Mesh contraction
As a rule of thumb: the more your mesh looks like a skeleton, the easier it is
to extract one (duh). Mesh contraction using skeletor.pre.contract()
[1] can
help you to get your mesh "in shape".
References
[1] Au OK, Tai CL, Chu HK, Cohen-Or D, Lee TY. Skeleton extraction by mesh contraction. ACM Transactions on Graphics (TOG). 2008 Aug 1;27(3):44.
1# This script is part of skeletor (http://www.github.com/navis-org/skeletor). 2# Copyright (C) 2018 Philipp Schlegel 3# 4# This program is free software: you can redistribute it and/or modify 5# it under the terms of the GNU General Public License as published by 6# the Free Software Foundation, either version 3 of the License, or 7# (at your option) any later version. 8# 9# This program is distributed in the hope that it will be useful, 10# but WITHOUT ANY WARRANTY; without even the implied warranty of 11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12# GNU General Public License for more details. 13# 14# You should have received a copy of the GNU General Public License 15# along with this program. 16 17""" 18The `skeletor.pre` module contains functions to pre-process meshes before 19skeletonization. 20 21#### Fixing faulty meshes 22 23Some skeletonization methods are susceptible to faulty meshes (degenerate faces, 24wrong normals, etc.). If your skeleton looks off, it might be worth a shot 25trying to fix the mesh using `skeletor.pre.fix_mesh()`. 26 27#### Mesh contraction 28 29As a rule of thumb: the more your mesh looks like a skeleton, the easier it is 30to extract one (duh). Mesh contraction using `skeletor.pre.contract()` [1] can 31help you to get your mesh "in shape". 32 33References 34---------- 35[1] Au OK, Tai CL, Chu HK, Cohen-Or D, Lee TY. Skeleton extraction by mesh 36 contraction. ACM Transactions on Graphics (TOG). 2008 Aug 1;27(3):44. 37 38""" 39 40from .meshcontraction import contract 41from .preprocessing import fix_mesh, simplify, remesh 42 43__docformat__ = "numpy" 44__all__ = ['fix_mesh', 'simplify', 'remesh', 'contract']
37def fix_mesh(mesh, remote_infinite=True, merge_duplicate_verts=True, 38 remove_degenerate_faces=True, remove_unreferenced_verts=True, 39 drop_winglets=True, fix_normals=False, remove_disconnected=False, 40 inplace=False): 41 """Try to fix some common problems with mesh. 42 43 1. Remove infinite values 44 2. Merge duplicate vertices 45 3. Remove duplicate and degenerate faces 46 4. Remove unreference vertices 47 5. Drop winglets (faces that have only one adjacent face) 48 5. Fix normals (Optional) 49 6. Remove disconnected fragments (Optional) 50 51 Parameters 52 ---------- 53 mesh : mesh-like object 54 Mesh to fix. Must have `.vertices` and `.faces` 55 properties. 56 remove_disconnected : False | int 57 If a number is given, will iterate over the mesh's 58 connected components and remove those consisting of 59 less than the given number of vertices. For example, 60 ``remove_disconnected=5`` will drop parts of the 61 mesh that consist of five or less connected 62 vertices. 63 inplace : bool 64 If True, will perform fixes on the input mesh. 65 If False, will make a copy first. This is silently 66 ignored if `mesh` is not already a trimesh. 67 68 Returns 69 ------- 70 fixed mesh : trimesh.Trimesh 71 72 """ 73 if not isinstance(mesh, tm.Trimesh): 74 mesh = make_trimesh(mesh, validate=False) 75 elif not inplace: 76 mesh = mesh.copy() 77 78 if remove_disconnected: 79 to_drop = [] 80 for c in nx.connected_components(mesh.vertex_adjacency_graph): 81 if len(c) <= remove_disconnected: 82 to_drop += list(c) 83 84 # Remove dropped vertices 85 remove = np.isin(np.arange(mesh.vertices.shape[0]), to_drop) 86 mesh.update_vertices(~remove) 87 88 if remote_infinite: 89 mesh.remove_infinite_values() 90 91 if merge_duplicate_verts: 92 mesh.merge_vertices() 93 94 if remove_degenerate_faces: 95 mesh.remove_duplicate_faces() 96 mesh.remove_degenerate_faces() 97 98 if remove_unreferenced_verts: 99 mesh.remove_unreferenced_vertices() 100 101 if drop_winglets: 102 mesh = remove_winglets(mesh) 103 104 # This should be done after all clean-up operations 105 if fix_normals: 106 mesh.fix_normals() 107 108 return mesh
Try to fix some common problems with mesh.
- Remove infinite values
- Merge duplicate vertices
- Remove duplicate and degenerate faces
- Remove unreference vertices
- Drop winglets (faces that have only one adjacent face)
- Fix normals (Optional)
- Remove disconnected fragments (Optional)
Parameters
- mesh (mesh-like object):
Mesh to fix. Must have
.vertices
and.faces
properties. - remove_disconnected (False | int):
If a number is given, will iterate over the mesh's
connected components and remove those consisting of
less than the given number of vertices. For example,
remove_disconnected=5
will drop parts of the mesh that consist of five or less connected vertices. - inplace (bool):
If True, will perform fixes on the input mesh.
If False, will make a copy first. This is silently
ignored if
mesh
is not already a trimesh.
Returns
- fixed mesh (trimesh.Trimesh):
192def simplify(mesh, ratio): 193 """Simplify mesh using Blender 3D. 194 195 Uses Blender's "decimate" modifier in "collapse" mode. 196 197 Parameters 198 ---------- 199 mesh : trimesh.Trimesh 200 Mesh to simplify. 201 ratio : float 202 Factor to which to reduce faces. For example, a ratio of 0.5 will 203 reduce the number of faces to 50%. 204 205 Returns 206 ------- 207 trimesh.Trimesh 208 Simplified mesh. 209 210 """ 211 if not tm.interfaces.blender.exists: 212 raise ImportError('No Blender available (executable not found).') 213 _blender_executable = tm.interfaces.blender._blender_executable 214 215 assert ratio < 1 and ratio > 0, 'ratio must be between 0 and 1' 216 217 # We need to import here to avoid circular imports 218 from ..utilities import make_trimesh 219 mesh = make_trimesh(mesh, validate=False) 220 assert isinstance(mesh, tm.Trimesh) 221 222 # Load the template 223 temp_name = 'blender_decimate.py.template' 224 if temp_name in _cache: 225 template = _cache[temp_name] 226 else: 227 with open(os.path.join(_pwd, 'templates', temp_name), 'r') as f: 228 template = f.read() 229 _cache[temp_name] = template 230 231 # Replace placeholder with actual ratio 232 script = template.replace('$RATIO', str(ratio)) 233 234 # Let trimesh's MeshScript take care of exectution and clean-up 235 with tm.interfaces.generic.MeshScript(meshes=[mesh], 236 script=script, 237 debug=False) as blend: 238 result = blend.run(_blender_executable 239 + ' --background --python $SCRIPT') 240 241 # Blender apparently returns actively incorrect face normals 242 result.face_normals = None 243 244 return result
Simplify mesh using Blender 3D.
Uses Blender's "decimate" modifier in "collapse" mode.
Parameters
- mesh (trimesh.Trimesh): Mesh to simplify.
- ratio (float): Factor to which to reduce faces. For example, a ratio of 0.5 will reduce the number of faces to 50%.
Returns
- trimesh.Trimesh: Simplified mesh.
247def remesh(mesh, voxel_size=50, adaptivity=5): 248 """Remesh mesh using Blender 3D. 249 250 Uses Blender's "remesh" modifier in "voxel" mode. 251 252 Parameters 253 ---------- 254 mesh : trimesh.Trimesh 255 Mesh to remesh. 256 voxel_size : float 257 Size of individual voxels (edge length). 258 adaptivity: float 259 Reduces final face count where detail is not important. 260 261 Returns 262 ------- 263 trimesh.Trimesh 264 Remeshed mesh. 265 266 """ 267 if not tm.interfaces.blender.exists: 268 raise ImportError('No Blender available (executable not found).') 269 _blender_executable = tm.interfaces.blender._blender_executable 270 271 assert voxel_size > 0, '`voxel_size` must be a positive number' 272 assert adaptivity > 0, '`adaptivity` must be a positive number' 273 274 # We need to import here to avoid circular imports 275 from ..utilities import make_trimesh 276 mesh = make_trimesh(mesh, validate=False) 277 assert isinstance(mesh, tm.Trimesh) 278 279 # Load the template 280 temp_name = 'blender_remesh.py.template' 281 if temp_name in _cache: 282 template = _cache[temp_name] 283 else: 284 with open(os.path.join(_pwd, 'templates', temp_name), 'r') as f: 285 template = f.read() 286 _cache[temp_name] = template 287 288 # Replace placeholder with actual ratio 289 script = template.replace('$VOXEL_SIZE', str(voxel_size) 290 ).replace('$ADAPTIVITY', str(adaptivity)) 291 292 # Let trimesh's MeshScript take care of exectution and clean-up 293 with tm.interfaces.generic.MeshScript(meshes=[mesh], 294 script=script, 295 debug=False) as blend: 296 result = blend.run(_blender_executable 297 + ' --background --python $SCRIPT') 298 299 # Blender apparently returns actively incorrect face normals 300 result.face_normals = None 301 302 return result
Remesh mesh using Blender 3D.
Uses Blender's "remesh" modifier in "voxel" mode.
Parameters
- mesh (trimesh.Trimesh): Mesh to remesh.
- voxel_size (float): Size of individual voxels (edge length).
- adaptivity (float): Reduces final face count where detail is not important.
Returns
- trimesh.Trimesh: Remeshed mesh.
37def contract(mesh, epsilon=1e-06, iter_lim=100, time_lim=None, precision=1e-07, 38 SL=2, WH0=1, WL0='auto', operator='cotangent', progress=True, 39 validate=True): 40 """Contract mesh. 41 42 In a nutshell: this function contracts the mesh by applying rounds of 43 _constraint_ Laplacian smoothing. This function can be fairly expensive 44 and I highly recommend you play around with ``SL`` to contract the 45 mesh in as few steps as possible. The contraction doesn't have to be perfect 46 for good skeletonization results (<10%, i.e. `epsilon<0.1`). 47 48 Also: parameterization matters a lot! Default parameters will get you there 49 but playing around with `SL` and `WH0` might speed things up by an order of 50 magnitude. 51 52 Parameters 53 ---------- 54 mesh : mesh obj 55 The mesh to be contracted. Can be any object (e.g. 56 a trimesh.Trimesh) that has ``.vertices`` and ``.faces`` 57 properties or a tuple ``(vertices, faces)`` or a dictionary 58 ``{'vertices': vertices, 'faces': faces}``. 59 Vertices and faces must be (N, 3) numpy arrays. 60 epsilon : float (0-1), optional 61 Target contraction rate as measured by the sum of all face 62 areas in the contracted versus the original mesh. Algorithm 63 will stop once mesh is contracted below this threshold. 64 Depending on your mesh (number of faces, shape) reaching a 65 strong contraction can be extremely costly with comparatively 66 little benefit for the subsequent skeletonization. Note that 67 the algorithm might stop short of this target if ``iter_lim`` 68 or ``time_lim`` is reached first or if the sum of face areas 69 is increasing from one iteration to the next instead of 70 decreasing. 71 iter_lim : int (>1), optional 72 Maximum rounds of contractions. 73 time_lim : int, optional 74 Maximum run time in seconds. Note that this limit is not 75 checked during but after each round of contraction. Hence, 76 the actual total time will likely overshoot ``time_lim``. 77 precision : float, optional 78 Sets the precision for finding the least-square solution. 79 This is the main determinant for speed vs quality: lower 80 values will take (much) longer but will get you closer to an 81 optimally contracted mesh. Higher values will be faster but 82 the iterative contractions might stop early. 83 SL : float, optional 84 Factor by which the contraction matrix is multiplied for 85 each iteration. Higher values = quicker contraction, lower 86 values = more likely to get you an optimal contraction. 87 WH0 : float, optional 88 Initial weight factor for the attraction constraints. 89 The ratio of the initial weights ``WL0`` and ``WH0`` 90 controls the smoothness and the degree of contraction of the 91 first iteration result, thus it determines the amount of 92 details retained in subsequent and final contracted meshes: 93 higher ``WH0`` = more details retained. 94 WL0 : "auto" | float 95 Initial weight factor for the contraction constraints. By 96 default ("auto"), this will be set to ``1e-3 * sqrt(A)`` 97 with ``A`` being the average face area. This ensures that 98 contraction forces scale with the coarseness of the mesh. 99 operator : "cotangent" | "umbrella" 100 Which Laplacian operator to use: 101 102 - The "cotangent" operator (default) takes both topology 103 and geometry of the mesh into account and is hence a 104 better descriptor of the curvature flow. This is the 105 operator used in the original paper. 106 - The "umbrella" operator (aka "uniform weighting") uses 107 only topological features of the mesh. This also makes 108 it more robust against flaws in the mesh! Use it when 109 the cotangent operator produces oddly contracted meshes. 110 111 progress : bool 112 Whether or not to show a progress bar. 113 validate : bool 114 If True, will try to fix potential issues with the mesh 115 (e.g. infinite values, duplicate vertices, degenerate faces) 116 before collapsing. Degenerate meshes can lead to effectively 117 infinite runtime for this function! 118 119 Returns 120 ------- 121 trimesh.Trimesh 122 Contracted copy of original mesh. The final contraction rate 123 is attached to the mesh as ``.epsilon`` property. 124 125 References 126 ---------- 127 [1] Au OK, Tai CL, Chu HK, Cohen-Or D, Lee TY. Skeleton extraction by mesh 128 contraction. ACM Transactions on Graphics (TOG). 2008 Aug 1;27(3):44. 129 130 """ 131 assert operator in ('cotangent', 'umbrella') 132 start = time.time() 133 134 # Force into trimesh 135 m = make_trimesh(mesh, validate=validate) 136 n = len(m.vertices) 137 138 # Initialize attraction weights 139 zeros = np.zeros((n, 3)) 140 WH0_diag = np.zeros(n) 141 WH0_diag.fill(WH0) 142 WH0 = sp.sparse.spdiags(WH0_diag, 0, WH0_diag.size, WH0_diag.size) 143 # Make a copy but keep original values 144 WH = sp.sparse.dia_matrix(WH0) 145 146 # Initialize contraction weights 147 if WL0 == 'auto': 148 WL0 = 1e-03 * np.sqrt(averageFaceArea(m)) 149 #WL0 = 1.0 / 10.0 * np.sqrt(averageFaceArea(m)) 150 WL_diag = np.zeros(n) 151 WL_diag.fill(WL0) 152 WL = sp.sparse.spdiags(WL_diag, 0, WL_diag.size, WL_diag.size) 153 154 # Copy mesh 155 dm = m.copy() 156 157 area_ratios = [1.0] 158 originalRingAreas = getOneRingAreas(dm) 159 goodvertices = dm.vertices 160 bar_format = ("{l_bar}{bar}| [{elapsed}<{remaining}, " 161 "{postfix[0]}/{postfix[1]}it, " 162 "{rate_fmt}, epsilon {postfix[2]:.2g}") 163 with tqdm(total=100, 164 bar_format=bar_format, 165 disable=progress is False, 166 postfix=[1, iter_lim, 1]) as pbar: 167 for i in range(iter_lim): 168 # Get Laplace weights 169 170 if operator == 'cotangent': 171 L = laplacian_cotangent(dm, normalized=True) 172 else: 173 L = laplacian_umbrella(dm) 174 """ 175 import robust_laplacian 176 L, M_ = robust_laplacian.mesh_laplacian(np.array(dm.vertices), 177 np.array(dm.faces), 178 mollify_factor=1e-3) 179 """ 180 V = getMeshVPos(dm) 181 A = sp.sparse.vstack([WL.dot(L), WH]) 182 b = np.vstack((zeros, WH.dot(V))) 183 184 cpts = np.zeros((n, 3)) 185 for j in range(3): 186 """ 187 # Solve A*x = b 188 # Note that we force scipy's lsqr() to use current vertex 189 # positions as start points - this speeds things up and 190 # without it we get suboptimal solutions that lead to early 191 # termination 192 cpts[:, j] = lsqr(A, b[:, j], 193 atol=precision, btol=precision, 194 damp=1, 195 x0=dm.vertices[:, j])[0] 196 """ 197 # The solution below is recommended in scipy's lsqr docstring 198 # for when we have an initial estimate 199 # Gives use the same results as above but is slightly faster 200 201 # Initial estimate (i.e. our current positions) 202 x0 = dm.vertices[:, j] 203 # Compute residual vector 204 r0 = b[:, j] - A * x0 205 # Use LSQR to solve the system 206 dx = lsqr(A, r0, 207 atol=precision, btol=precision, 208 damp=1)[0] 209 # Add the correction dx to obtain a final solution 210 cpts[:, j] = x0 + dx 211 212 # Update mesh with new vertex position 213 dm.vertices = cpts 214 215 # Update iteration in progress bar 216 if progress: 217 pbar.postfix[0] = i + 1 218 219 # Break if face area has increased compared to the last iteration 220 new_eps = dm.area / m.area 221 if (new_eps > area_ratios[-1]): 222 dm.vertices = goodvertices 223 if progress: 224 tqdm.write("Total face area increased from last iteration." 225 f" Contraction stopped prematurely after {i} " 226 f"iterations at epsilon {area_ratios[-1]:.2g}.") 227 break 228 area_ratios.append(new_eps) 229 230 # Update progress bar 231 if progress: 232 pbar.postfix[2] = area_ratios[-1] 233 prog = round((area_ratios[-2] - area_ratios[-1]) / (1 - epsilon) * 100) 234 pbar.update(min(prog, 100-pbar.n)) 235 236 goodvertices = cpts 237 238 # Update contraction weights -> at each iteration the contraction 239 # forces increase to counteract the increased attraction forces 240 WL = sp.sparse.dia_matrix(WL.multiply(SL)) 241 242 # Update attraction weights -> the smaller the one ring areas 243 # the higher the attraction forces 244 changeinarea = np.sqrt(originalRingAreas / getOneRingAreas(dm)) 245 WH = sp.sparse.dia_matrix(WH0.multiply(changeinarea)) 246 247 # Stop if we reached our target contraction rate 248 if (area_ratios[-1] <= epsilon): 249 break 250 251 # Stop if time limit is reached 252 if not isinstance(time_lim, (bool, type(None))): 253 if (time.time() - start) >= time_lim: 254 break 255 256 # Keep track of final epsilon 257 dm.epsilon = area_ratios[-1] 258 259 return dm
Contract mesh.
In a nutshell: this function contracts the mesh by applying rounds of
_constraint_ Laplacian smoothing. This function can be fairly expensive
and I highly recommend you play around with SL
to contract the
mesh in as few steps as possible. The contraction doesn't have to be perfect
for good skeletonization results (<10%, i.e. epsilon<0.1
).
Also: parameterization matters a lot! Default parameters will get you there
but playing around with SL
and WH0
might speed things up by an order of
magnitude.
Parameters
- mesh (mesh obj):
The mesh to be contracted. Can be any object (e.g.
a trimesh.Trimesh) that has
.vertices
and.faces
properties or a tuple(vertices, faces)
or a dictionary{'vertices': vertices, 'faces': faces}
. Vertices and faces must be (N, 3) numpy arrays. - epsilon (float (0-1), optional):
Target contraction rate as measured by the sum of all face
areas in the contracted versus the original mesh. Algorithm
will stop once mesh is contracted below this threshold.
Depending on your mesh (number of faces, shape) reaching a
strong contraction can be extremely costly with comparatively
little benefit for the subsequent skeletonization. Note that
the algorithm might stop short of this target if
iter_lim
ortime_lim
is reached first or if the sum of face areas is increasing from one iteration to the next instead of decreasing. - iter_lim (int (>1), optional): Maximum rounds of contractions.
- time_lim (int, optional):
Maximum run time in seconds. Note that this limit is not
checked during but after each round of contraction. Hence,
the actual total time will likely overshoot
time_lim
. - precision (float, optional): Sets the precision for finding the least-square solution. This is the main determinant for speed vs quality: lower values will take (much) longer but will get you closer to an optimally contracted mesh. Higher values will be faster but the iterative contractions might stop early.
- SL (float, optional): Factor by which the contraction matrix is multiplied for each iteration. Higher values = quicker contraction, lower values = more likely to get you an optimal contraction.
- WH0 (float, optional):
Initial weight factor for the attraction constraints.
The ratio of the initial weights
WL0
andWH0
controls the smoothness and the degree of contraction of the first iteration result, thus it determines the amount of details retained in subsequent and final contracted meshes: higherWH0
= more details retained. - WL0 ("auto" | float):
Initial weight factor for the contraction constraints. By
default ("auto"), this will be set to
1e-3 * sqrt(A)
withA
being the average face area. This ensures that contraction forces scale with the coarseness of the mesh. operator ("cotangent" | "umbrella"): Which Laplacian operator to use:
- The "cotangent" operator (default) takes both topology and geometry of the mesh into account and is hence a better descriptor of the curvature flow. This is the operator used in the original paper.
- The "umbrella" operator (aka "uniform weighting") uses only topological features of the mesh. This also makes it more robust against flaws in the mesh! Use it when the cotangent operator produces oddly contracted meshes.
- progress (bool): Whether or not to show a progress bar.
- validate (bool): If True, will try to fix potential issues with the mesh (e.g. infinite values, duplicate vertices, degenerate faces) before collapsing. Degenerate meshes can lead to effectively infinite runtime for this function!
Returns
- trimesh.Trimesh: Contracted copy of original mesh. The final contraction rate
is attached to the mesh as
.epsilon
property.
References
[1] Au OK, Tai CL, Chu HK, Cohen-Or D, Lee TY. Skeleton extraction by mesh contraction. ACM Transactions on Graphics (TOG). 2008 Aug 1;27(3):44.