Morph one surface mesh onto another via map to reference shape (disk, sphere). Part of the pipeline for dynamic data.
In this notebook, we present some additional wrapping algorithms. So far, we have wrapped two meshes by simply projection each source mesh vertex onto the closest position on the target mesh and carrying out some on-surface smoothing. This works but is not necessarily extremely robust, and can lead to very deformed UV maps when there are large, localized deformations.
Here, we’ll implement an alternative algorithm that works by (a) mapping each mesh into a common reference in a mathematically standardized way -harmonic maps- and then (b) chaining together the map from the source mesh to the common reference and from the reference to the target mesh to get a surface-surface map.
We provide algorithms for meshes of disk, cylindrical, and spherical topology (potentially with holes). For arbitrary genus (i.e. handles, like a torus), one can take a look at hyperbolic orbifolds https://github.com/noamaig/hyperbolic_orbifolds/. Not implemented here.
Loading test data
Let’s load the test meshes from the fly midgut dataset.
mesh_initial_UV = tcmesh.ObjMesh.read_obj("datasets/movie_example/initial_uv.obj")mesh_final_UV = tcmesh.ObjMesh.read_obj("datasets/movie_example/final_uv.obj") # this is a UV map defined for tpt 20mesh_1 = tcmesh.read_other_formats_without_uv(f"datasets/movie_example/meshes/mesh_{str(1).zfill(2)}.ply")mesh_2 = tcmesh.read_other_formats_without_uv(f"datasets/movie_example/meshes/mesh_{str(2).zfill(2)}.ply")mesh_60 = tcmesh.read_other_formats_without_uv(f"datasets/movie_example/meshes/mesh_{str(60).zfill(2)}.ply")
Warning: readOBJ() ignored non-comment line 4:
o mesh_01_cylinder_seams_uv
Warning: readOBJ() ignored non-comment line 3:
o mesh_20_uv
Disk
Following https://libigl.github.io/libigl-python-bindings/tut-chapter4/. We first map each mesh to the unit disk using harmonic coordinates. Boundary conditions are such that the boundary loop is mapped to the unit circle isometrically (i.e. relative distances are preserved).
There is still a remaining rotation degree of freedom. This is fixed by optimizing the match of the conformal factors (i.e. the area distortion of the maps to the disk) using phase correlation.
*Map mesh to unit disk by computing harmonic UV coordinates.
The longest boundary loop of the mesh is mapped to the unit circle. Follows https://libigl.github.io/libigl-python-bindings/tut-chapter4/. Note: the disk is centered at (1/2, 1/2).
The disk rotation angle is arbitrary*
Type
Default
Details
mesh
tcmesh.ObjMesh
Mesh. Must be topologically a disk (potentially with holes), and should be triangular.
bnd
NoneType
None
Boundary to map to the unit circle. If None, computed automatically.
set_uvs
bool
False
whether to set the disk coordinates as UV coordinates of the mesh.
Returns
np.array
2d vertex coordinates mapping the mesh to the unit disk in [0,1]^1
“We next seek the rotation that best aligns the two parameterizations. To do so, we first sample the conformal factors of each mesh onto a regular spherical grid (Figure 6, top right). We then find the rotation that maximizes the correlation between conformal factors, via a fast spectral transform.”
*Rotationally align two UV maps to the disk by the conformal factor.
Computes aligned UV coordinates. Assumes that the UV coordinates are in [0,1]^2. This works by computing the conformal factor (how much triangle size changes as it is mapped to the plane, and finding the optimal rotation to align the conformal factors via phase correlation.*
Type
Default
Details
mesh_source
tcmesh.ObjMesh
Mesh. Must be topologically a disk (potentially with holes), and should be triangular.
mesh_target
tcmesh.ObjMesh
Mesh. Must be topologically a disk (potentially with holes), and should be triangular.
disk_uv_source
NoneType
None
Disk coordinates for each vertex in the source mesh. Optional. If None, the UV coordinates of mesh_source are used.
disk_uv_target
NoneType
None
Disk coordinates for each vertex in the target mesh. Optional. If None, the UV coordinates of disk_uv_target are used.
allow_flip
bool
True
Whether to allow flips (improper rotations). If a flip occurs, np.linalg.det(rot_mat) < 0
q
float
0.01
Conformal factors are clipped at this quantile to avoid outliers.
n_grid
int
1024
Grid for interpolation of conformal factor during alignment
Map a cylinder to the disk: fill one boundary with an extra vertex, map to the disk harmonically, use Moebius to move the extra vertex to the disk center.
Warning: readOBJ() ignored non-comment line 3:
o mesh_01
# determine the boundary first_boundary = igl.boundary_loop(mesh.tris)all_boundary_edges = igl.boundary_facets(mesh.tris)second_boundary = igl.edges_to_path(np.stack([e for e in all_boundary_edges ifnot e[0] in first_boundary]))[0][:-1]
# add an extra vertex and trianglesfilled_tris = igl.topological_hole_fill(mesh.tris, [second_boundary])center_second_boundary = mesh.vertices[second_boundary].mean(axis=0)filled_vertices = np.vstack([mesh.vertices, [center_second_boundary]])
# map to disk via harmonic mapbnd_uv =1* igl.map_vertices_to_circle(filled_vertices, first_boundary)uv = igl.harmonic(filled_vertices, filled_tris, first_boundary, bnd_uv, 1)# map the center of the second boundary to disk centeruv = moebius_disk(uv, -uv[-1])
uv[-1] # last point -at the center of filled cylinder opening- is mapped to center of
*Map cylinder mesh to unit disk by computing harmonic UV coordinates.
One boundary loop of the mesh is mapped to the circle with a diameter 1/2. The second boundary is filled by adding an extra vertex at its center, which is mapped to the disk center.
Which of the two circular boundaries is mapped to the center resp. the outer circle is set by the option outer_boundary.
The disk rotation angle is arbitrary.*
Type
Default
Details
mesh
tcmesh.ObjMesh
Mesh. Must be topologically a disk (potentially with holes), and should be triangular.
outer_boundary
str
longest
Boundary to map to the unit circle. If “longest”/“shortest”, the longer/shorter one is mapped to the unit circle. If int, the boundary containing the vertex defined by the int is used.
first_boundary
NoneType
None
First boundary loop of cylinder. If None, computed automatically.
second_boundary
NoneType
None
Second boundary loop of cylinder. If None, computed automatically.
set_uvs
bool
False
whether to set the disk coordinates as UV coordinates of the mesh.
return_filled
bool
False
Whether to return vertices and faces with filled hole.
Returns
np.array, np.array, np.array
uv : np.array 2d vertex coordinates mapping the mesh to the unit disk in [0,1]^1. If filled_vertices is True, the last entry is the coordinate of the added point. filled_vertices : np.array 3d vertices with extra vertex to fill second boundary filled_faces : np.array Faces with added faces to fill the second boundary.
## let's try a more challenging example - midgut mesh with two cuts to make it a cylindermesh = tcmesh.ObjMesh.read_obj("datasets/movie_example/mesh_1_cylinder_cut.obj") # cylinder_clean
Warning: readOBJ() ignored non-comment line 3:
o mesh_01
*Map 3d coordinates of source mesh to target mesh via an annulus parametrization.
Annulus parametrization can be provided or computed on the fly via harmonic coordinates. If desired, the two disks are also rotationally aligned. Meshes must be cylindrical. Two choices exist for mapping a cylinder to the plane (depending on which boundary circle is mapped to the disk boundary resp. center). Both options are tried, and the one leading to better alignment is used.
If you already have a map to the disk, use wrap_coords_via_disk instead.*
Type
Default
Details
mesh_source
tcmesh.ObjMesh
Mesh. Must be topologically a cylinder and should be triangular.
mesh_target
tcmesh.ObjMesh
Mesh. Must be topologically a cylinder and should be triangular.
q
float
0.01
Conformal factors are clipped at this quantile to avoid outliers.
n_grid
int
1024
Grid for interpolation of conformal factor during alignment. Higher values increase alignment precision.
Returns
np.array, float
new_coords : np.array New 3d vertex coordinates for mesh_source, lying on the surface defined by mesh_target overlap : float Measure of geometry overlap. 1 = perfect alignment
For the sphere, we follow https://www.cs.cmu.edu/~kmcrane/Projects/MobiusRegistration/paper.pdf. The Riemann mapping theorem guarantees there is a conformal map of our surface to the unit sphere. This map is unique up to Moebius transformations (inversion about a point in the unit ball, and rotations). We fix the inversions by choosing the conformal map with the least amount of area distortion, and the rotation by registering the conformal factors as functions on the unit sphere using spherical harmonics
Cut sphere to disk by removing a single vertex
Map disk to plane conformally using harmonic coordinates. Turns out the least-squares conformal map is lousy at preserving angles (at least in igl)
Map disk to a sphere using stereographic projection, adding the removed vertex at the north pole
Fix Moebius inversion by choosing a conformal map with minimal distortion using Algorithm 1 from https://www.cs.cmu.edu/~kmcrane/Projects/MobiusRegistration/paper.pdf
Rotational registration using spherical harmonics (see notebook 03c)
Interpolation of vertex positions using spherical parametrization
mesh = deepcopy(mesh_final_UV)
# this is what the map of the mesh sans north pole to the plane looks likefig = plt.figure(figsize=(4,4))plt.triplot(*uv.T, faces_disk, lw=0.5)
*Stereographic projection from unit sphere to plane from the north pole (0,0,1).
See https://en.wikipedia.org/wiki/Stereographic_projection. Convention: the plane is at z=0, unit sphere centered at the origin. pts should be an array of shape (…, 3)*
*Stereographic projection from plane to the unit sphere from the north pole (0,0,1).
See https://en.wikipedia.org/wiki/Stereographic_projection. Convention: plane is at z=0, unit sphere centered at origin. uv should be an array of shape (…, 2)*
*Apply Moeboius inversions to minimize area distortion of a map from mesh to sphere.
Implementation of Algorithm 1 from: https://www.cs.cmu.edu/~kmcrane/Projects/MobiusRegistration/paper.pdf*
Type
Default
Details
vertices_3d
np.array of shape (n_verts, 3)
3d mesh vertices
vertices_sphere
np.array of shape (n_verts, 3)
Initial vertex positions on unit sphere
tris
np.array of shape (n_faces, 3) and type int
Faces of a triangular mesh
n_iter_centering
int
10
Centering algorithm iterations.
alpha
float
0.5
Learning rate. Lower values make the algorithm more stable
Returns
np.array, float
vertices_sphere_centered : np.array of shape (n_verts, 3) Centered sphere coordinates com_norm : float Distance of sphere vertex center of mass from origin. Low values indicate convergence of the algorithm.
First, remove one vertex (the last one), and map the resulting disk-topology mesh to the plane using least squares conformal maps. Then map the plane to the sphere using stereographic projection.
The conformal map is chosen so that area distortion is as small as possible by (a) optimizing over the scale (“radius”) of the map to the disk and (b) “centering” the map using Algorithm 1 from cs.cmu.edu/~kmcrane/Projects/MobiusRegistration/paper.pdf.
This means the map is canonical up to rotations of the sphere.*
Type
Default
Details
mesh
tcmesh.ObjMesh
Mesh. Must be topologically a sphere, and should be triangular.
method
str
harmonic
Method for computing the map from mesh without the north pole to the plane. Recommended: harmonic.
R_max
int
100
Maximum radius to consider when computing inverse stereographic projection. If you get weird results, try a lower value.
n_iter_centering
int
20
Centering algorithm iterations. If 0, no centering is performed
alpha
float
0.5
Learning rate. Lower values make the centering algorithm more stable
angles_3d = igl.internal_angles(mesh.vertices, mesh.tris)angles_sphere = igl.internal_angles(coords_sphere, mesh.tris)np.nanmean(np.abs(angles_sphere.flatten()-angles_3d.flatten())) # map to sphere is close to conformal/angle-preserving
Rotation alignment using rotation module
For a map to the sphere, we can compute the amount of area distortion at each point on the sphere. We can then use this “conformal factor” as a signal to rotationally align two maps (from different meshes) to the sphere.
# this is what the conformal factor (area distortion) looks like plotted vs phi, theta, for an example# (the same mesh mapped to the sphere, but arbitrarily rotated)fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(8, 4))ax1.scatter(phi_source, theta_source, c=signal_source, s=1, vmin=-3, vmax=3)ax2.scatter(phi_target, theta_target, c=signal_target, s=1, vmin=-3, vmax=3)ax1.axis("equal");ax2.axis("equal");
*Rotationally align two UV maps to the sphere by the conformal factor.
Computes aligned spherical coordinates. Rotational alignment works by computing the conformal factor (how much triangle size changes as it is mapped to the sphere) and optimizing over rotations to find the one which leads to the best alignment. This works via an expansion in spherical harmonics. See tcrot.rotational_alignment*
Type
Default
Details
mesh_source
tcmesh.ObjMesh
Mesh. Must be topologically a sphere (potentially with holes), and should be triangular.
mesh_target
tcmesh.ObjMesh
Mesh. Must be topologically a sphere (potentially with holes), and should be triangular.
coords_sphere_source
np.array or None
Sphere coordinates for each vertex in the source mesh. If None, the UV coordinates are interpreted as angles 2piu=phi, 2piv=theta.
coords_sphere_target
np.array or None
Sphere coordinates for each vertex in the source mesh. If None, the UV coordinates are interpreted as angles 2piu=phi, 2piv=theta.
allow_flip
bool
False
Whether to allow improper rotations with determinant -1.
max_l
int
10
Maximum angular momentum. If None, the maximum value available in the input spherical harmonics is used.
n_angle
int
100
Number of trial rotation angles [0,…, 2*pi]
n_subdiv_axes
int
1
Controls the number of trial rotation axes. Rotation axes are vertices of the icosphere which can be subdivided. There will be roughly 40*4**n_subdiv_axes trial axes. This parameter has the strongest influence on the run time.
maxfev
int
100
Number of function evaluations during fine optimization.
Returns
np.array, np.array, float
coords_sphere_source_rotated : np.array Rotationally aligned sphere vertices rot_mat : np.array of shape (3,3) Rotation matrix overlap : float How well the conformal factors overlap. 1 = perfect overlap.
# first test case: same mesh, but map to the sphere rotatatedmesh_source = deepcopy(mesh_final_UV)mesh_target = deepcopy(mesh_final_UV)coords_sphere_source = map_to_sphere(mesh_source)rot_mat = stats.special_ortho_group.rvs(3)coords_sphere_target = coords_sphere_source @ rot_mat.T
# second test case: meshes for two frames of a moviemesh_source = deepcopy(mesh_2) # mesh_final_UVmesh_target = deepcopy(mesh_1) # mesh_initial_UVcoords_sphere_source = map_to_sphere(mesh_source)coords_sphere_target = map_to_sphere(mesh_target)
Now that we have (a) mapped both meshes to the sphere, (b) minized distortion via Moebius centering, and (c) rotationally aligned the two maps via spherical harmonics, we can interpolate the coordinates of mesh B onto mesh A.
*Map 3d coordinates of source mesh to target mesh via a sphere parametrization.
Sphere parametrizations can be provided or computed on the fly using the least-area distorting conformal map to the sphere (see map_to_sphere). If desired, the two parametrizations are also aligned with respect to 3d rotations using mesh shape, using spherical harmonics. See rotational_align_sphere for details.*
Type
Default
Details
mesh_source
tcmesh.ObjMesh
Mesh. Must be topologically a disk (potentially with holes), and should be triangular.
mesh_target
tcmesh.ObjMesh
Mesh. Must be topologically a disk (potentially with holes), and should be triangular.
coords_sphere_source
NoneType
None
Sphere coordinates for each vertex in the source mesh. Optional. If None, computed via map_to_sphere.
coords_sphere_target
NoneType
None
Sphere coordinates for each vertex in the target mesh. Optional. If None, computed via map_to_sphere.
method
str
harmonic
Method for comuting the map from sphere without north pole to plane
n_iter_centering
int
10
Centering algorithm iterations for computing map to the sphere. If 0, no centering is performed
alpha
float
0.5
Learning rate for computing map to the sphere. Lower values make the centering algorithm more stable.
align
bool
True
Whether to rotationally align the parametrizations. If False, they are used as-is.
allow_flip
bool
False
Whether to allow improper rotations with determinant -1 for rotational alignment.
max_l
int
10
Maximum angular momentum. If None, the maximum value available in the input spherical harmonics is used.
n_angle
int
100
Number of trial rotation angles [0,…, 2*pi]
n_subdiv_axes
int
1
Controls the number of trial rotation axes. Rotation axes are vertices of the icosphere which can be subdivided. There will be roughly 40*4**n_subdiv_axes trial axes. This parameter has the strongest influence on the run time.
maxfev
int
100
Number of function evaluations during fine optimization for rotational alignment.
Returns
np.array, float
new_coords : np.array New 3d vertex coordinates for mesh_source, lying on the surface defined by mesh_target overlap : np.array Overlap of conformal factor (area distortion) on sphere of the two meshes. Only returned if align is True. 1 indicates perfect overlap.
Try it out first using trimesh, then do myself to avoid dependency. Use trimesh.registration.nricp_sumner
-> No good, takes forever and my laptop runs out of memory. Also lots of parameters to tune whose meaning I don’t know.
Laplace Beltrami descriptors
Idea - compute the eigenfunctions \(\phi_i\) of the Laplace operator \(\Delta\) on the surface, and the embed each point \(p\) on the surface as \(p\mapsto \phi_i(p)/\sqrt{\lambda_i}\).
Not suitable - the spectral shapes of the midgut example mesh become extremely degenerate.
See https://www.cs.jhu.edu/~misha/ReadingSeminar/Papers/Rustamov07.pdf, https://web.archive.org/web/20100626223753id_/http://www.cs.jhu.edu/~misha/Fall07/Notes/Rustamov07.pdf
Functional mapping
[https://people.csail.mit.edu/jsolomon/assets/fmaps.pdf] - uses Laplace Beltrami descriptors. There appears to be an existing implementation in python. Let’s try out their example notebook
It does not work that well. Ok for matching from mapping timepoints 1->2, bad for 1->20, or 20->30. I get “patchy correspondences”: the map mesh A -> mesh B is not continuous and tears up mesh A into multiple patches that are stitched together in somewhat random order on mesh B. Also kinda slow.
As Dillon pointed out, shape descriptors of Laplace-Beltrami kind fundamentally use metric information. Laplace Beltrami is equivalent to a metric. Good for isometric deformations, like joint movements, bad for non-isometric, which is often the case for us.
So in principle, this is a nice method, but not quite what we need.