Getting Started

This short tutorial computes a functional map between two meshes and converts it to a point-to-point correspondence. It is the condensed version of Computing a map without a correspondence, which explains every step and shows what each one buys.

Loading meshes

A TriMesh is read from a file with TriMesh.load, or built directly from raw vertex/face arrays:

from pyFM.mesh import TriMesh

mesh1 = TriMesh.load("cat-00.off", area_normalize=True, center=True)
mesh2 = TriMesh.load("lion-00.off", area_normalize=True, center=True)

Computing a functional map

FunctionalMapping drives the standard workflow: compute the Laplace-Beltrami spectrum and descriptors with preprocess, then optimize the map with fit.

from pyFM.functional import FunctionalMapping

model = FunctionalMapping(mesh1, mesh2)

# Spectrum + descriptors
model.preprocess(
    descr_type="WKS",    # "WKS" or "HKS"
    subsample_step=2,    # keep every 2nd descriptor
    verbose=True,
)

# Optimize the functional map
model.fit(
    K=30,           # eigenvectors kept on each shape
    w_descr=1e0,    # descriptor preservation
    w_lap=1e-1,     # Laplacian commutativity
    w_dcomm=1e0,    # descriptor commutativity
    w_orient=1e0,   # orientation term
    verbose=True,
)

Keep w_orient above zero. Descriptors alone cannot tell the left side of a shape from its right, so with w_orient=0 the optimization typically returns a symmetry-flipped map that refinement will not fix.

Getting a point-to-point map

get_p2p converts the functional map model.FM_12 into a vertex-to-vertex map p2p_21 (for each vertex of mesh2, the index of its match on mesh1):

p2p_21 = model.get_p2p()

Refinement

The output of fit is a starting point, not an answer — refinement is where most of the accuracy comes from. ICP and ZoomOut both return a new functional map rather than overwriting model.FM_12:

FM_icp = model.icp_refine(verbose=True)
p2p_21_icp = model.get_p2p(FM_icp)

FM_zo = model.zoomout_refine(nit=16, step=5, verbose=True)
p2p_21_zo = model.get_p2p(FM_zo)

ZoomOut grows the map by step at each of its nit iterations, so K = 30 ends at 30 + 16 * 5 = 110.

Evaluating accuracy

Given an (approximate) ground-truth map and a geodesic distance matrix on the source mesh, pyFM.eval.accuracy reports the mean geodesic error:

import numpy as np
import pyFM.eval

A_geod = mesh1.geodesic_matrix()
gt_p2p = np.loadtxt("lion2cat", dtype=int)

for name, p2p in [("fit", p2p_21), ("ICP", p2p_21_icp), ("ZoomOut", p2p_21_zo)]:
    acc = pyFM.eval.accuracy(p2p, gt_p2p, A_geod, sqrt_area=mesh1.sqrt_area)
    print(f"{name:8s}: {1e2 * acc:.2f}")

On this pair that prints roughly 23.85, 7.39 and 8.53, in percent of \(\sqrt{\text{area}}\): the raw fit is coarse, and refinement is what makes it usable.

A few matched landmarks, passed to preprocess as landmarks=, cut the error further still — see Computing a map without a correspondence.