ShapeDNA

ShapeDNA is an n-dimensional intrinsic shape descriptor (see Reuter et al., CAD Journal, 2006). It can be used to compare two geometric objects independent of their pose or posture as the ShapeDNA is not affected by (near)-isometric deformations. This tutorial shows how you compute, normalize and re-weight Laplace-Beltrami spectra to obtain the ShapeDNA.

[1]:
# imports
from lapy import TriaMesh, TetMesh
from lapy import shapedna

First we load some data: a tria mesh representing the boundary of a cube and a tetrahedral mesh representing the full cube.

[2]:
# load data
tria = TriaMesh.read_vtk("../data/cubeTria.vtk")
tet = TetMesh.read_vtk("../data/cubeTetra.vtk")
--> VTK format         ...
 --> DONE ( V: 2402 , T: 4800 )

--> VTK format         ...
 --> DONE ( V: 9261 , T: 48000 )

Let’s compute the first three eigenvalues and eigenvectors of the triangle mesh…

[3]:
# compute eigenvalues and eigenvectors for tria mesh
ev = shapedna.compute_shapedna(tria, k=3)
ev['Eigenvectors']
ev['Eigenvalues']
TriaMesh with regular Laplace-Beltrami
Solver: spsolve (LU decomposition) ...
[3]:
array([-4.0165149e-05,  4.1696410e+00,  4.1704664e+00], dtype=float32)

Now we perform a normalization of the eigenvalues using the method “geometry” which is equal to surface area normalization for 2d meshes. The resulting eigenvalues are the same as when computing them on the same shape with unit surface area (=1).

[4]:
# volume / surface / geometry normalization of tria eigenvalues
shapedna.normalize_ev(tria, ev["Eigenvalues"], method="geometry")
[4]:
array([-2.4099089e-04,  2.5017845e+01,  2.5022799e+01], dtype=float32)

For surfaces, eigenvalues increase linearly with their ordering. In order to reduce the influence of higher (and probably more noise affected) eigenvalues it is common practice to perform a linear re-weighting.

[5]:
# linear reweighting of tria eigenvalues
shapedna.reweight_ev(ev["Eigenvalues"])
[5]:
array([-4.01651487e-05,  2.08482051e+00,  1.39015547e+00])

The normalized and re-weighted eigenvalues are called the ShapeDNA. We can now compute the distance between two shapes by comparing their ShapeDNA. The default is the Euclidean distance between two ShapeDNA vectors.

[6]:
# compute distance for tria eigenvalues (trivial case)
shapedna.compute_distance(ev["Eigenvalues"], ev["Eigenvalues"])
[6]:
0.0

Note, that usually more eigenvalues are used (in the order of 15 to 50) for shape comparison. Also you can do other analyses, e.g. find clusters in this shape space or project it via PCA for visualization.

We now repeat the above steps for a tetrahedral mesh, again computing the first three eigenvalues and -vectors.

[7]:
# compute eigenvalues and eigenvectors for tet mesh
evTet = shapedna.compute_shapedna(tet, k=3)
evTet['Eigenvectors']
evTet['Eigenvalues']
TetMesh with regular Laplace
Solver: spsolve (LU decomposition) ...
[7]:
array([8.4440224e-05, 9.8897915e+00, 9.8898811e+00], dtype=float32)

For 3d meshes the “geometry” normalization defaults to unit volume normalization. Since the cube is already unit volume, nothing happens.

[8]:
# volume / surface / geometry normalization of tet eigenvalues
shapedna.normalize_ev(tet, evTet["Eigenvalues"], method="geometry")
Found 4800 triangles on boundary.
Searched mesh after 79 flooding iterations (0.012834310531616211 sec).
[8]:
array([8.4440224e-05, 9.8897915e+00, 9.8898811e+00], dtype=float32)

Again we perform linear re-weighting. This is only meaningful for small eigenvalues as the asymtotic trend of eigenvalues of 3d solids is not linear.

[9]:
# linear reweighting of tet eigenvalues
shapedna.reweight_ev(evTet["Eigenvalues"])
[9]:
array([8.44402239e-05, 4.94489574e+00, 3.29662704e+00])

Now that we have the ShapeDNA of the 3D solid cube, we can compare it to other ShapeDNA (or to itself, which of course yields zero).

[10]:
# compute distance for tria eigenvalues (trivial case)
shapedna.compute_distance(evTet["Eigenvalues"], evTet["Eigenvalues"])
[10]:
0.0