You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Add isotropic remeshing that keeps the topology - #126
Surfaces from marching cubes, decimation or topology repair often have very uneven triangles: slivers with angles of a few degrees, short and long edges side by side. Many later steps work better or faster on even triangles; for example FreeSurfer's mris_sphere needs far fewer steps to unfold a cortical surface after remeshing. This PR adds a module lapy.remesh with isotropic remeshing after Botsch and Kobbelt (2004), which keeps the topology and keeps the vertices on the input surface.
Changes
New module lapy/remesh.py, listed on the API page under Modules and in the README:
remesh(tria, target_length=None, iterations=5, max_rounds=10) returns a new TriaMesh with edges near target_length (the mean edge length of the input by default). Each iteration:
splits edges longer than 4/3 of the target at their midpoint;
collapses edges shorter than 4/5 of the target to their midpoint, but only if the link condition holds (the two end points share no neighbors other than the two vertices opposite the edge), no new edge is longer than 4/3 of the target and no triangle tilts by more than 60 degrees;
flips edges where that brings the vertex valences closer to 6, unless the opposite vertices are already joined or a triangle would tilt too far;
moves the vertices towards the area weighted centroid of their neighbors in the tangent plane, and projects them back onto the input surface.
Boundary vertices stay where they are, the result is oriented like the input, and a 2D mesh stays 2D. The mesh must be manifold and oriented, otherwise a ValueError points to is_manifold and orient_.
closest_points(tria, points, k=8) returns the closest point on the mesh and its triangle for each query point, testing the k triangles with the nearest centroids exactly. remesh uses it for the projection.
The link condition matters for thin parts: on a tube with a triangular cross-section, collapses along the tube barely tilt any triangle, and without the link condition they pinch the tube and break the mesh. The configuration of a triangle split at its center, where collapsing an outer edge folds the whole triangle, is caught as well.
Every operation runs in rounds over candidate edges whose neighborhoods do not overlap, picked so that the shortest (or most improving) candidate always wins. A round is a few vectorized numpy and scipy operations, with no Python loop over vertices or triangles and no new dependency. Rounds per operation are capped at max_rounds, since late rounds do little and the rest carries over to the next iteration.
On a 143k vertex cortical white matter surface from marching cubes, remeshing to the edge length of FreeSurfer's --desired-face-area 0.5 takes about 10 s and gives 93k vertices, genus 0 as before, no self-intersections, a smallest angle of 29 degrees (2.6 before) and 81% of the vertices with valence 6. All vertices lie on the input surface.
Tests
New tests in lapy/utils/tests/test_remesh.py:
a sphere made of slivers, remeshed to even edges and no small angles, with all vertices on the input surface and no self-intersections;
a thin torus with a triangular cross-section, which keeps genus 1 when coarsened (this test fails without the link condition);
a tetrahedron with one face split at its center, which collapses to a tetrahedron and not further;
a jittered 2D grid, which keeps its boundary vertices, its area and its 2D vertices;
ValueError for non-oriented and non-manifold meshes;
closest_points for points closest to the inside, each corner and each edge of a triangle.
The docs build with -W and numpydoc validation passes with the new module.
❌ Patch coverage is 95.14925% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.11%. Comparing base (1da47c8) to head (9ec70f8).
k is not constrained to a positive integer before it is passed to KDTree.query. Zero or negative values cannot select any candidate triangles and therefore produce an empty-candidate path or an opaque lower-level error rather than a valid closest point; reject them at this public API boundary.
This issue also appears in the following locations of the same file:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Surfaces from marching cubes, decimation or topology repair often have very uneven triangles: slivers with angles of a few degrees, short and long edges side by side. Many later steps work better or faster on even triangles; for example FreeSurfer's
mris_sphereneeds far fewer steps to unfold a cortical surface after remeshing. This PR adds a modulelapy.remeshwith isotropic remeshing after Botsch and Kobbelt (2004), which keeps the topology and keeps the vertices on the input surface.Changes
New module
lapy/remesh.py, listed on the API page under Modules and in the README:remesh(tria, target_length=None, iterations=5, max_rounds=10)returns a newTriaMeshwith edges neartarget_length(the mean edge length of the input by default). Each iteration:Boundary vertices stay where they are, the result is oriented like the input, and a 2D mesh stays 2D. The mesh must be manifold and oriented, otherwise a
ValueErrorpoints tois_manifoldandorient_.closest_points(tria, points, k=8)returns the closest point on the mesh and its triangle for each query point, testing thektriangles with the nearest centroids exactly.remeshuses it for the projection.The link condition matters for thin parts: on a tube with a triangular cross-section, collapses along the tube barely tilt any triangle, and without the link condition they pinch the tube and break the mesh. The configuration of a triangle split at its center, where collapsing an outer edge folds the whole triangle, is caught as well.
Every operation runs in rounds over candidate edges whose neighborhoods do not overlap, picked so that the shortest (or most improving) candidate always wins. A round is a few vectorized numpy and scipy operations, with no Python loop over vertices or triangles and no new dependency. Rounds per operation are capped at
max_rounds, since late rounds do little and the rest carries over to the next iteration.On a 143k vertex cortical white matter surface from marching cubes, remeshing to the edge length of FreeSurfer's
--desired-face-area 0.5takes about 10 s and gives 93k vertices, genus 0 as before, no self-intersections, a smallest angle of 29 degrees (2.6 before) and 81% of the vertices with valence 6. All vertices lie on the input surface.Tests
New tests in
lapy/utils/tests/test_remesh.py:ValueErrorfor non-oriented and non-manifold meshes;closest_pointsfor points closest to the inside, each corner and each edge of a triangle.The docs build with
-Wand numpydoc validation passes with the new module.