Skip to content

Commit f4492fa

Browse files
author
pv
committed
CHANGE parametrization is hidden from user, only pattern and target mesh required.
1 parent 3b13d50 commit f4492fa

File tree

8 files changed

+163
-83
lines changed

8 files changed

+163
-83
lines changed

docs/examples/example_mapping.py

Lines changed: 12 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -39,45 +39,21 @@
3939
pv = tessagon_mesh["vert_list"]
4040
pf = tessagon_mesh["face_list"]
4141

42-
4342
# ==============================================================================
44-
# Place the pattern mesh at the center of the 3D mesh
43+
# Place the pattern mesh at the bottom left corner of the mesh
4544
# ==============================================================================
45+
4646
pattern2d = Mesh.from_vertices_and_faces(pv, pf)
4747
c = pattern2d.aabb().corner(0)
4848
pattern2d.translate([-c[0], -c[1], -c[2]])
4949
pv, pf = pattern2d.to_vertices_and_faces()
5050

51-
52-
# ==============================================================================
53-
# Parametrization
54-
# ==============================================================================
55-
56-
uv = igl.trimesh_lscm((v, f))
57-
58-
mesh_flattened = mesh.copy()
59-
mesh_flattened.vertices_attribute("z", 0)
60-
61-
for i in range(mesh.number_of_vertices()):
62-
mesh_flattened.vertex_attributes(i, "xy", uv[i])
63-
64-
# ==============================================================================
65-
# Rescale the flattened uv mesh to 1x1 scale
66-
# ==============================================================================
67-
box = mesh_flattened.aabb()
68-
c = box.corner(0)
69-
mesh_flattened.translate([-c[0], -c[1], -c[2]])
70-
mesh_flattened.scale(1.0 / box.xsize, 1.0 / box.ysize, 1)
71-
72-
for i in range(mesh.number_of_vertices()):
73-
uv[i] = mesh_flattened.vertex_attributes(i, "xy")
74-
7551
# ==============================================================================
7652
# Mapping: 3D Mesh, 2D Pattern, UV
7753
# ==============================================================================
7854

79-
p_uv = igl.trimesh_simple((pv, pf))
80-
mesh_mapped = igl.map_mesh(v, f, uv, pv, pf, p_uv)
55+
mv, mf = igl.map_mesh((v, f), (pv, pf))
56+
mesh_mapped = Mesh.from_vertices_and_faces(mv, mf)
8157

8258
# ==============================================================================
8359
# Viewer
@@ -86,6 +62,13 @@
8662
viewer = Viewer()
8763
viewer.scene.add(mesh, name="mesh", show_faces=False, linecolor=Color.grey(), opacity=0.2)
8864
viewer.scene.add(pattern2d, name="pattern2d")
89-
viewer.scene.add(mesh_flattened, name="mesh_flattened")
9065
viewer.scene.add(mesh_mapped, name="mesh_mapped", facecolor=Color.red())
66+
67+
# To see where the pattern is mapped:
68+
uv = igl.trimesh_lscm((v, f))
69+
mesh_flattened = mesh.copy()
70+
for i in range(mesh.number_of_vertices()):
71+
mesh_flattened.vertex_attributes(i, "xyz", [uv[i][0], uv[i][1], 0])
72+
73+
viewer.scene.add(mesh_flattened, name="mesh_flattened")
9174
viewer.show()

docs/examples/example_parametrisation.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@
3131
for index, key in enumerate(mesh.vertices()):
3232
mesh_lscm.vertex_attributes(key, "xy", lscm_uv[index])
3333

34-
mesh_lscm.transform(Scale.from_factors([3, 3, 3]))
35-
3634
# ==============================================================================
3735
# Visualization
3836
# ==============================================================================

src/compas_libigl/mapping.py

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,43 @@
11
import numpy as np
2-
from compas.datastructures import Mesh
32

43
from compas_libigl import _mapping
54

65

7-
def map_mesh(v, f, uv, pv, pf, p_uv):
8-
"""Map a 2D pattern mesh onto a 3D target mesh using simple UV parameterization.
6+
def map_mesh(target_mesh, pattern_mesh):
7+
"""Map a 2D pattern mesh onto a 3D target.
98
109
Parameters
1110
----------
12-
v : list[list[float]]
13-
The vertices of the target mesh.
14-
f : list[list[int]]
15-
The faces of the target mesh.
16-
uv : list[list[float]]
17-
The UV coordinates of the target mesh.
18-
pv : list[list[float]]
19-
The vertices of the pattern mesh.
20-
pf : list[list[int]]
21-
The faces of the pattern mesh.
22-
p_uv : list[list[float]]
23-
The UV coordinates of the pattern mesh.
11+
target_mesh : tuple
12+
A tuple of (vertices, faces) representing the target mesh.
13+
vertices : list[list[float]]
14+
The vertices of the target mesh.
15+
faces : list[list[int]]
16+
The triangle faces of the target mesh.
17+
pattern_mesh : tuple
18+
A tuple of (vertices, faces) representing the pattern mesh.
19+
vertices : list[list[float]]
20+
The vertices of the pattern mesh.
21+
faces : list[list[int]]
22+
The polygonal faces of the pattern mesh.
2423
2524
Returns
2625
-------
27-
pattern_mapped_cleaned : compas.datastructures.Mesh
28-
The mapped pattern mesh.
26+
tuple
27+
A tuple containing (vertices, faces) of the mapped pattern mesh.
2928
"""
29+
# Unpack mesh tuples
30+
v, f = target_mesh
31+
pv, pf = pattern_mesh
3032

33+
# Convert to numpy arrays
3134
v_numpy = np.array(v, dtype=np.float64)
3235
f_numpy = np.array(f, dtype=np.int32)
33-
uv_numpy = np.array(uv, dtype=np.float64)
3436
pattern_v_numpy = np.array(pv, dtype=np.float64)
3537
pattern_f_numpy = np.array(pf, dtype=np.int32)
36-
p_uv_numpy = np.array(p_uv, dtype=np.float64)
3738

38-
# Call the mapping function with the new signature (returns a tuple)
39-
pattern_f_numpy_cleaned = _mapping.map_mesh(v_numpy, f_numpy, uv_numpy, pattern_v_numpy, pattern_f_numpy, p_uv_numpy)
39+
# Perform the mapping
40+
pattern_f_numpy_cleaned = _mapping.map_mesh_with_automatic_parameterization(v_numpy, f_numpy, pattern_v_numpy, pattern_f_numpy)
4041

41-
pattern_mapped_cleaned = Mesh.from_vertices_and_faces(pattern_v_numpy, pattern_f_numpy_cleaned)
42-
return pattern_mapped_cleaned
42+
# Return the result as a tuple
43+
return pattern_v_numpy, pattern_f_numpy_cleaned

src/mapping.cpp

Lines changed: 75 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -160,21 +160,84 @@ std::vector<std::vector<int>> map_mesh(
160160
return pattern_polygonal_faces;
161161
}
162162

163-
// Create the nanobind module
164-
NB_MODULE(_mapping, m) {
165-
// Bind std::vector<std::vector<int>> type for Python interoperability
166-
nb::bind_vector<std::vector<int>>(m, "VectorInt");
167-
nb::bind_vector<std::vector<std::vector<int>>>(m, "VectorVectorInt");
163+
std::vector<std::vector<int>> map_mesh_with_automatic_parameterization(
164+
Eigen::Ref<const compas::RowMatrixXd> target_v,
165+
Eigen::Ref<const compas::RowMatrixXi> target_f,
166+
Eigen::Ref<compas::RowMatrixXd> pattern_v,
167+
Eigen::Ref<const compas::RowMatrixXi> pattern_f)
168+
{
169+
// Compute target mesh UV parameterization using LSCM
170+
Eigen::MatrixXd target_uv;
171+
172+
// Find the open boundary
173+
Eigen::VectorXi B;
174+
igl::boundary_loop(target_f, B);
175+
176+
// Fix two points on the boundary
177+
Eigen::VectorXi fixed(2, 1);
178+
fixed(0) = B(0);
179+
fixed(1) = B(B.size() / 2);
180+
181+
Eigen::MatrixXd fixed_uv(2, 2);
182+
fixed_uv << 0, 0, 1, 0;
183+
184+
// LSCM parametrization
185+
igl::lscm(target_v, target_f, fixed, fixed_uv, target_uv);
186+
187+
// Rescale target UV coordinates
188+
// Find min and max values for normalization
189+
Eigen::Vector2d min_coeff = target_uv.colwise().minCoeff();
190+
Eigen::Vector2d max_coeff = target_uv.colwise().maxCoeff();
191+
192+
// Compute the size of the bounding box
193+
Eigen::Vector2d size = max_coeff - min_coeff;
194+
195+
// Translate UV coordinates so minimum is at the origin
196+
target_uv.col(0) = target_uv.col(0).array() - min_coeff(0);
197+
target_uv.col(1) = target_uv.col(1).array() - min_coeff(1);
198+
199+
// Scale to fit in a 0-1 box while maintaining aspect ratio
200+
double scale_factor = 1.0 / std::max(size(0), size(1));
201+
target_uv *= scale_factor;
202+
203+
// Compute pattern mesh UV parameterization using simple method
204+
Eigen::MatrixXd pattern_uv = pattern_v.leftCols(2); // Use the first two columns (X and Y coordinates)
168205

206+
// Rescale pattern UV coordinates
207+
min_coeff = pattern_uv.colwise().minCoeff();
208+
max_coeff = pattern_uv.colwise().maxCoeff();
209+
210+
size = max_coeff - min_coeff;
211+
212+
pattern_uv.col(0) = pattern_uv.col(0).array() - min_coeff(0);
213+
pattern_uv.col(1) = pattern_uv.col(1).array() - min_coeff(1);
214+
215+
scale_factor = 1.0 / std::max(size(0), size(1));
216+
pattern_uv *= scale_factor;
217+
218+
// Now perform the mapping using the computed UV parameterizations
219+
return map_mesh(target_v, target_f, target_uv, pattern_v, pattern_f, pattern_uv);
220+
}
221+
222+
NB_MODULE(_mapping, m)
223+
{
169224
m.def(
170225
"map_mesh",
171226
&map_mesh,
172-
"Map a 2D pattern mesh onto a 3D target mesh using simple UV parameterization",
173-
"v"_a,
174-
"f"_a,
175-
"uv"_a,
227+
"Map a 2D pattern mesh onto a 3D target mesh.",
228+
"target_v"_a,
229+
"target_f"_a,
230+
"target_uv"_a,
176231
"pattern_v"_a,
177232
"pattern_f"_a,
178-
"pattern_uv"_a
179-
);
180-
}
233+
"pattern_uv"_a);
234+
235+
m.def(
236+
"map_mesh_with_automatic_parameterization",
237+
&map_mesh_with_automatic_parameterization,
238+
"Map a 2D pattern mesh onto a 3D target mesh with automatic parameterization.",
239+
"target_v"_a,
240+
"target_f"_a,
241+
"pattern_v"_a,
242+
"pattern_f"_a);
243+
}

src/mapping.hpp

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,22 @@ std::vector<std::vector<int>> map_mesh(
6565
Eigen::Ref<const compas::RowMatrixXd> uv,
6666
Eigen::Ref<compas::RowMatrixXd> pattern_v,
6767
Eigen::Ref<const compas::RowMatrixXi> pattern_f,
68-
Eigen::Ref<const compas::RowMatrixXd> pattern_uv);
68+
Eigen::Ref<const compas::RowMatrixXd> pattern_uv);
69+
70+
71+
/**
72+
* Map a 2D pattern mesh onto a 3D target mesh with automatic parameterization, mesh is always mapped to 1x1 unit.
73+
* This function automatically computes the UV parameterization for both meshes
74+
* and then performs mapping, eliminating the need for separate parameterization steps.
75+
*
76+
* @param target_v #V x 3 matrix of target mesh vertex coordinates
77+
* @param target_f #F x 3 matrix of target mesh triangle indices
78+
* @param pattern_v #V x 3 matrix of pattern mesh vertex coordinates
79+
* @param pattern_f #F x 3 matrix of pattern mesh triangle indices
80+
* @return A vector of face indices representing polygons in the mapped mesh
81+
*/
82+
std::vector<std::vector<int>> map_mesh_with_automatic_parameterization(
83+
Eigen::Ref<const compas::RowMatrixXd> target_v,
84+
Eigen::Ref<const compas::RowMatrixXi> target_f,
85+
Eigen::Ref<compas::RowMatrixXd> pattern_v,
86+
Eigen::Ref<const compas::RowMatrixXi> pattern_f);

src/parametrisation.cpp

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,24 @@
55
#include <igl/map_vertices_to_circle.h>
66
#include <Eigen/Core>
77

8+
void rescale(Eigen::MatrixXd &V_uv)
9+
{
10+
// Find min and max values for normalization
11+
Eigen::Vector2d min_coeff = V_uv.colwise().minCoeff();
12+
Eigen::Vector2d max_coeff = V_uv.colwise().maxCoeff();
13+
14+
// Compute the size of the bounding box
15+
Eigen::Vector2d size = max_coeff - min_coeff;
16+
17+
// Translate UV coordinates so minimum is at the origin
18+
V_uv.col(0) = V_uv.col(0).array() - min_coeff(0);
19+
V_uv.col(1) = V_uv.col(1).array() - min_coeff(1);
20+
21+
// Scale to fit in a 0-1 box while maintaining aspect ratio
22+
double scale_factor = 1.0 / std::max(size(0), size(1));
23+
V_uv *= scale_factor;
24+
}
25+
826
Eigen::MatrixXd
927
harmonic(
1028
Eigen::Ref<const compas::RowMatrixXd> V,
@@ -23,6 +41,8 @@ harmonic(
2341
// Harmonic parametrization for the internal vertices
2442
igl::harmonic(V, F, B, B_uv, 1, V_uv);
2543

44+
rescale(V_uv);
45+
2646
return V_uv;
2747
}
2848

@@ -47,6 +67,8 @@ lscm(
4767

4868
// LSCM parametrization
4969
igl::lscm(V, F, fixed, fixed_uv, V_uv);
70+
71+
rescale(V_uv);
5072

5173
return V_uv;
5274
}
@@ -59,17 +81,8 @@ simple(
5981
// Create UV coordinates matrix from XY coordinates
6082
Eigen::MatrixXd V_uv = V.leftCols(2); // Use the first two columns (X and Y coordinates)
6183

62-
// Find min and max values for normalization
63-
Eigen::Vector2d min_coeff = V_uv.colwise().minCoeff();
64-
Eigen::Vector2d max_coeff = V_uv.colwise().maxCoeff();
65-
66-
// Normalize to range [0,1]
67-
for(int id = 0; id < V_uv.rows(); id++)
68-
{
69-
V_uv(id, 0) = (V_uv(id, 0) - min_coeff(0)) / (max_coeff(0) - min_coeff(0));
70-
V_uv(id, 1) = (V_uv(id, 1) - min_coeff(1)) / (max_coeff(1) - min_coeff(1));
71-
}
72-
84+
rescale(V_uv);
85+
7386
return V_uv;
7487
}
7588

src/parametrisation.hpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@
33
#include "compas.hpp"
44
#include <Eigen/Core>
55

6+
/**
7+
* Rescale UV coordinates to fit in a 0-1 box while maintaining aspect ratio.
8+
*
9+
* @param V_uv #V x 2 matrix of UV coordinates
10+
*/
11+
void rescale(Eigen::MatrixXd &V_uv);
12+
613
/**
714
* Compute the harmonic parametrization of a triangle mesh.
815
*

tests/test_mapping.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,9 @@ def test_map_mesh():
2727
pv = tessagon_mesh["vert_list"]
2828
pf = tessagon_mesh["face_list"]
2929

30-
# Create UV parameterization
31-
uv = igl.trimesh_lscm((v, f))
32-
p_uv = igl.trimesh_simple((pv, pf))
33-
3430
# Map pattern onto target mesh
35-
mesh_mapped = igl.map_mesh(v, f, uv, pv, pf, p_uv)
31+
mv, mf = igl.map_mesh((v, f), (pv, pf))
32+
mesh_mapped = Mesh.from_vertices_and_faces(mv, mf)
3633

3734
# Verify the result is a valid mesh
3835
assert mesh_mapped is not None

0 commit comments

Comments
 (0)