Skip to content

Commit 467872b

Browse files
committed
ADD skeletonization with vertex mapping
1 parent 5866f21 commit 467872b

File tree

9 files changed

+224
-11
lines changed

9 files changed

+224
-11
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## Unreleased
99

1010
### Added
11+
* Added `compas_cgal.skeletonization.mesh_skeleton_with_mapping`.
1112

1213
### Changed
1314

439 KB
Loading
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import math
2+
from pathlib import Path
3+
4+
from compas.datastructures import Mesh
5+
from compas.geometry import Polyline, Point
6+
from compas.geometry import Rotation, Scale, Translation
7+
from compas_viewer import Viewer
8+
9+
from compas_cgal.skeletonization import mesh_skeleton_with_mapping
10+
11+
# Load and transform mesh
12+
input_file = Path(__file__).parent.parent.parent / "data" / "elephant.off"
13+
14+
rotation = Rotation.from_axis_and_angle([0, 1, 0], math.radians(5)) * Rotation.from_axis_and_angle([1, 0, 0], math.radians(60))
15+
transform = Translation.from_vector([0, 0, 2]) * rotation * Scale.from_factors([5, 5, 5])
16+
17+
mesh = Mesh.from_off(input_file).transformed(transform)
18+
v, f = mesh.to_vertices_and_faces(triangulated=True)
19+
20+
# Compute skeleton with vertex mapping
21+
skeleton_edges, vertex_indices = mesh_skeleton_with_mapping((v, f))
22+
23+
# Create polylines for skeleton edges
24+
polylines = [Polyline([start, end]) for start, end in skeleton_edges]
25+
26+
# Select edge to highlight (100th or last if fewer edges)
27+
edge_idx = min(100, len(vertex_indices) - 1)
28+
start_indices, end_indices = vertex_indices[edge_idx]
29+
30+
print(f"Mesh: {len(v)} vertices, Skeleton: {len(skeleton_edges)} edges")
31+
print(f"Edge {edge_idx}: {len(start_indices)} vertices → start, {len(end_indices)} vertices → end")
32+
33+
# Visualize
34+
viewer = Viewer()
35+
viewer.renderer.camera.target = [0, 0, 1.5]
36+
viewer.renderer.camera.position = [-5, -5, 1.5]
37+
38+
# Show mesh as backdrop
39+
viewer.scene.add(mesh, opacity=0.2, show_points=False, facecolor=[0.9, 0.9, 0.9])
40+
41+
# Show vertices that map to selected skeleton edge
42+
for idx in start_indices:
43+
viewer.scene.add(Point(*v[idx]), pointcolor=[1.0, 0.0, 0.0], pointsize=15) # red = start
44+
for idx in end_indices:
45+
viewer.scene.add(Point(*v[idx]), pointcolor=[0.0, 0.0, 1.0], pointsize=15) # blue = end
46+
47+
# Show skeleton edges
48+
for i, polyline in enumerate(polylines):
49+
if i == edge_idx:
50+
viewer.scene.add(polyline, linewidth=10, linecolor=[1.0, 1.0, 0.0], show_points=True, pointsize=20) # yellow
51+
else:
52+
viewer.scene.add(polyline, linewidth=3, linecolor=[0.5, 0.5, 0.5], show_points=False) # gray
53+
54+
viewer.show()
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
Mesh Skeletonization with Vertex Mapping
2+
=========================================
3+
4+
This example demonstrates how to compute the geometric skeleton of a triangle mesh with vertex correspondence mapping using COMPAS CGAL.
5+
6+
Key Features:
7+
8+
* Computing skeleton with vertex correspondence information
9+
* Understanding which original mesh vertices map to each skeleton vertex
10+
* Colored visualization showing vertex mapping
11+
* Printing mapping statistics to understand the correspondence
12+
13+
.. figure:: /_images/example_skeletonization_with_mapping.png
14+
:figclass: figure
15+
:class: figure-img img-fluid
16+
17+
.. literalinclude:: example_skeletonization_with_mapping.py
18+
:language: python

src/compas_cgal/skeletonization.py

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from compas_cgal import _types_std # noqa: F401
66

77
from .types import PolylinesNumpySkeleton
8+
from .types import SkeletonVertexMapping
89
from .types import VerticesFaces
910

1011

@@ -46,8 +47,8 @@ def mesh_skeleton(mesh: VerticesFaces) -> PolylinesNumpySkeleton:
4647
V_numpy = np.asarray(V, dtype=np.float64, order="C") # Ensure C-contiguous
4748
F_numpy = np.asarray(F, dtype=np.int32, order="C") # Ensure C-contiguous
4849

49-
# Get start and end points as flattened vectorS
50-
start_points, end_points = _skeletonization.mesh_skeleton(V_numpy, F_numpy)
50+
# Get start and end points as flattened vectors
51+
start_points, end_points, _, _ = _skeletonization.mesh_skeleton(V_numpy, F_numpy)
5152

5253
# Convert flattened vectors to list of point coordinates
5354
edges = []
@@ -57,3 +58,69 @@ def mesh_skeleton(mesh: VerticesFaces) -> PolylinesNumpySkeleton:
5758
edges.append((start, end))
5859

5960
return edges
61+
62+
63+
@plugin(category="mesh")
64+
def mesh_skeleton_with_mapping(mesh: VerticesFaces) -> tuple[PolylinesNumpySkeleton, SkeletonVertexMapping]:
65+
"""Compute the geometric skeleton of a triangle mesh with vertex correspondence mapping.
66+
67+
Parameters
68+
----------
69+
mesh : VerticesFaces
70+
A tuple containing:
71+
* vertices: Nx3 array of vertex coordinates
72+
* faces: Mx3 array of vertex indices
73+
74+
Returns
75+
-------
76+
PolylinesSkeletonWithMapping
77+
A tuple containing:
78+
* edges: List of polylines representing the skeleton edges.
79+
Each polyline is a tuple of start and end point coordinates.
80+
* vertex_indices: List of tuples, each containing two lists of
81+
vertex indices corresponding to the start and end vertices of
82+
each skeleton edge. These are the original mesh vertices that
83+
contracted to form each skeleton vertex.
84+
85+
Raises
86+
------
87+
TypeError
88+
If the input mesh is not a tuple of vertices and faces.
89+
ValueError
90+
If the vertices array is not Nx3.
91+
If the faces array is not Mx3.
92+
If the face indices are out of range.
93+
If the mesh is not manifold and closed.
94+
RuntimeError
95+
If the mesh contraction fails to converge.
96+
97+
Notes
98+
-----
99+
The input mesh must be manifold and closed.
100+
The skeleton is computed using mean curvature flow.
101+
Each skeleton vertex corresponds to a set of original mesh vertices
102+
that were contracted to that point during the skeletonization process.
103+
(The set might be empty for some skeleton vertices that don't correspond
104+
to any original vertex.)
105+
"""
106+
V, F = mesh
107+
V_numpy = np.asarray(V, dtype=np.float64, order="C") # Ensure C-contiguous
108+
F_numpy = np.asarray(F, dtype=np.int32, order="C") # Ensure C-contiguous
109+
110+
# Get start and end points and vertex indices
111+
start_points, end_points, start_vertex_indices, end_vertex_indices = _skeletonization.mesh_skeleton(V_numpy, F_numpy)
112+
113+
# Convert flattened vectors to list of point coordinates
114+
edges = []
115+
vertex_indices = []
116+
117+
for i in range(0, len(start_points), 3):
118+
start = [start_points[i], start_points[i + 1], start_points[i + 2]]
119+
end = [end_points[i], end_points[i + 1], end_points[i + 2]]
120+
edges.append((start, end))
121+
122+
# Process vertex indices - convert VectorInt to Python lists
123+
for start_indices, end_indices in zip(start_vertex_indices, end_vertex_indices):
124+
vertex_indices.append((list(start_indices), list(end_indices)))
125+
126+
return edges, vertex_indices

src/compas_cgal/types.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@
4242
PolylinesNumpySkeleton = List[Tuple[List[float], List[float]]]
4343
"""A list of polylines, where each polyline is represented by a tuple of start and end point coordinates."""
4444

45+
SkeletonVertexMapping = List[Tuple[List[int], List[int]]]
46+
"""Vertex correspondence mapping for skeleton edges. Each tuple contains two lists:
47+
the first list has indices of original mesh vertices that contracted to the skeleton edge's start vertex,
48+
the second list has indices that contracted to the skeleton edge's end vertex."""
49+
4550
Planes = Union[
4651
Sequence[compas.geometry.Plane],
4752
Sequence[Tuple[compas.geometry.Point, compas.geometry.Vector]],

src/skeletonization.cpp

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ typedef Skeletonization::Skeleton Skeleton;
55
typedef boost::graph_traits<Skeleton>::vertex_descriptor SkeletonVertex;
66
typedef boost::graph_traits<Skeleton>::edge_descriptor SkeletonEdge;
77

8-
std::tuple<std::vector<double>, std::vector<double>>
8+
std::tuple<std::vector<double>, std::vector<double>, std::vector<std::vector<int>>, std::vector<std::vector<int>>>
99
pmp_mesh_skeleton(
1010
Eigen::Ref<const compas::RowMatrixXd> vertices,
1111
Eigen::Ref<const compas::RowMatrixXi> faces)
@@ -15,22 +15,35 @@ pmp_mesh_skeleton(
1515
Skeleton skeleton;
1616
Skeletonization mcs(mesh);
1717

18+
// Create a vertex index map for the mesh
19+
std::map<compas::Mesh::Vertex_index, int> vertex_index_map;
20+
for (auto v : mesh.vertices()) {
21+
vertex_index_map[v] = v.idx();
22+
}
23+
1824
mcs.contract_until_convergence();
1925
mcs.convert_to_skeleton(skeleton);
2026

21-
// Initialize vectors to store start and end points
27+
// Initialize vectors to store start and end points and vertex indices
2228
std::vector<double> start_points;
2329
std::vector<double> end_points;
30+
std::vector<std::vector<int>> start_vertex_indices;
31+
std::vector<std::vector<int>> end_vertex_indices;
2432

2533
// Reserve space for efficiency
2634
size_t num_edges = boost::num_edges(skeleton);
2735
start_points.reserve(num_edges * 3); // Each point has 3 coordinates
2836
end_points.reserve(num_edges * 3);
37+
start_vertex_indices.reserve(num_edges);
38+
end_vertex_indices.reserve(num_edges);
2939

3040
// Extract skeleton edges
3141
for (SkeletonEdge edge : CGAL::make_range(edges(skeleton))) {
32-
const compas::Kernel::Point_3& start = skeleton[source(edge, skeleton)].point;
33-
const compas::Kernel::Point_3& end = skeleton[target(edge, skeleton)].point;
42+
SkeletonVertex source_vertex = source(edge, skeleton);
43+
SkeletonVertex target_vertex = target(edge, skeleton);
44+
45+
const compas::Kernel::Point_3& start = skeleton[source_vertex].point;
46+
const compas::Kernel::Point_3& end = skeleton[target_vertex].point;
3447

3548
// Add start point coordinates
3649
start_points.push_back(start.x());
@@ -41,9 +54,23 @@ pmp_mesh_skeleton(
4154
end_points.push_back(end.x());
4255
end_points.push_back(end.y());
4356
end_points.push_back(end.z());
57+
58+
// Extract vertex indices for start vertex
59+
std::vector<int> start_indices;
60+
for (auto v : skeleton[source_vertex].vertices) {
61+
start_indices.push_back(vertex_index_map[v]);
62+
}
63+
start_vertex_indices.push_back(start_indices);
64+
65+
// Extract vertex indices for end vertex
66+
std::vector<int> end_indices;
67+
for (auto v : skeleton[target_vertex].vertices) {
68+
end_indices.push_back(vertex_index_map[v]);
69+
}
70+
end_vertex_indices.push_back(end_indices);
4471
}
4572

46-
return std::make_tuple(start_points, end_points);
73+
return std::make_tuple(start_points, end_points, start_vertex_indices, end_vertex_indices);
4774
};
4875

4976
NB_MODULE(_skeletonization, m) {

src/skeletonization.h

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,16 @@
88

99
/**
1010
* @brief Compute the geometric skeleton of a triangle mesh using mean curvature flow.
11-
*
11+
*
1212
* @param vertices Matrix of vertex positions as Nx3 matrix in row-major order (float64)
1313
* @param faces Matrix of face indices as Mx3 matrix in row-major order (int32)
14-
* @return std::tuple<std::vector<double>, std::vector<double>> containing:
14+
* @return std::tuple<std::vector<double>, std::vector<double>, std::vector<std::vector<int>>, std::vector<std::vector<int>>> containing:
1515
* - Start points of skeleton edges as vector of 3D coordinates (float64)
1616
* - End points of skeleton edges as vector of 3D coordinates (float64)
17+
* - Start vertex indices: for each skeleton edge, indices of original mesh vertices that contracted to the start vertex
18+
* - End vertex indices: for each skeleton edge, indices of original mesh vertices that contracted to the end vertex
1719
*/
18-
std::tuple<std::vector<double>, std::vector<double>>
20+
std::tuple<std::vector<double>, std::vector<double>, std::vector<std::vector<int>>, std::vector<std::vector<int>>>
1921
pmp_mesh_skeleton(
2022
Eigen::Ref<const compas::RowMatrixXd> vertices,
2123
Eigen::Ref<const compas::RowMatrixXi> faces);

tests/test_skeletonization.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from compas.geometry import Box
2-
from compas_cgal.skeletonization import mesh_skeleton
2+
from compas_cgal.skeletonization import mesh_skeleton, mesh_skeleton_with_mapping
33

44

55
def test_mesh_skeleton():
@@ -17,3 +17,42 @@ def test_mesh_skeleton():
1717
assert isinstance(edges[0], tuple)
1818
assert len(edges[0]) == 2
1919
assert len(edges[0][0]) == 3 # 3D points
20+
21+
22+
def test_mesh_skeleton_with_mapping():
23+
"""Test mesh skeletonization with vertex mapping."""
24+
# Create test box mesh
25+
box = Box.from_width_height_depth(2.0, 2.0, 2.0)
26+
mesh = box.to_vertices_and_faces(triangulated=True)
27+
vertices, faces = mesh
28+
29+
# Get skeleton with mapping
30+
edges, vertex_indices = mesh_skeleton_with_mapping(mesh)
31+
32+
# Basic validation of edges
33+
assert isinstance(edges, list)
34+
assert len(edges) > 0
35+
assert isinstance(edges[0], tuple)
36+
assert len(edges[0]) == 2
37+
assert len(edges[0][0]) == 3 # 3D points
38+
39+
# Validate vertex indices
40+
assert isinstance(vertex_indices, list)
41+
assert len(vertex_indices) == len(edges)
42+
43+
# Each edge should have vertex indices for start and end
44+
for indices in vertex_indices:
45+
assert isinstance(indices, tuple)
46+
assert len(indices) == 2
47+
start_indices, end_indices = indices
48+
49+
# Each skeleton vertex maps to one or more original vertices
50+
assert isinstance(start_indices, list)
51+
assert isinstance(end_indices, list)
52+
53+
# Indices should be valid
54+
num_vertices = len(vertices)
55+
for idx in start_indices:
56+
assert 0 <= idx < num_vertices
57+
for idx in end_indices:
58+
assert 0 <= idx < num_vertices

0 commit comments

Comments
 (0)