Skip to content

Add isotropic remeshing that keeps the topology - #126

Merged
m-reuter merged 4 commits into
Deep-MI:mainfrom
m-reuter:remesh
Oct 10, 2026
Merged

m-reuter merged 4 commits into
Deep-MI:mainfrom
m-reuter:remesh

Conversation

@m-reuter

Copy link
Copy Markdown
Member

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.

@codecov

codecov Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Codecov Report

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

Files with missing lines Patch % Lines
lapy/remesh.py 95.14% 6 Missing and 7 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #126      +/-   ##
==========================================
+ Coverage   62.67%   65.11%   +2.44%     
==========================================
  Files          17       18       +1     
  Lines        3287     3555     +268     
  Branches      429      455      +26     
==========================================
+ Hits         2060     2315     +255     
- Misses       1053     1059       +6     
- Partials      174      181       +7     
Flag Coverage Δ
unittests 65.11% <95.14%> (+2.44%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Boundary-only meshes fail, invalid target lengths are accepted, and disconnected tetrahedral components can be collapsed incorrectly.

3 open findings
What changed in this PR

Adds topology-preserving isotropic remeshing and surface projection utilities.

Changes:

  • Implements edge splitting, collapsing, flipping, smoothing, and projection.
  • Adds closest-point surface queries.
  • Adds remeshing tests and API documentation.
File Description
README.md Lists intersection and remeshing modules.
lapy/​remesh.py Implements remeshing and closest-point projection.
lapy/​utils/​tests/​test_remesh.py Tests geometry, topology, boundaries, and errors.
doc/​api/​lapy.modules.rst Adds remeshing to API documentation.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread lapy/remesh.py
Comment thread lapy/remesh.py
Comment thread lapy/remesh.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Undefined smoothing normals can corrupt meshes, and public projection inputs need validation.

0 open findings

3 resolved since last review
Previously missed (1)

In code that hasn't changed since last review

Medium severity Validate k as a positive integer

lapy/​remesh.py:138

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:

  • line 142
  • line 421

🧠 Review effort: Balanced

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The topology-sensitive geometry algorithms warrant final human validation despite the added tests.

0 open findings

🧠 Review effort: Lite

@m-reuter
m-reuter merged commit 89355e5 into Deep-MI:main Oct 10, 2026
15 checks passed
@m-reuter
m-reuter deleted the remesh branch October 10, 2026 21:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants