Source code for entente.landmarks.landmarker
"""
Functions for transferring landmarks from one mesh to another.
This module requires libspatialindex and rtree. See note in `trimesh_search.py`.
"""
from cached_property import cached_property
[docs]class Landmarker(object):
"""
An object which encapsulates a source mesh and a set of landmarks on that
mesh. Its function is to transfer those landmarks onto a new mesh.
The resultant landmarks will always be on or near the surface of the mesh.
Args:
source_mesh (lace.mesh.Mesh): The source mesh
landmarks (dict): A mapping of landmark names to the points, which are
`3x1` arraylike objects.
"""
def __init__(self, source_mesh, landmarks):
from ._trimesh_search import require_trimesh_with_rtree
require_trimesh_with_rtree()
self.source_mesh = source_mesh
self.landmarks = landmarks
[docs] @classmethod
def load(cls, source_mesh_path, landmark_path):
"""
Create a landmarker using the given paths to a source mesh and landmarks.
Args:
source_mesh_path (str): File path to the source mesh.
landmark_path (str): File path to a meshlab ``.pp`` file containing
the landmark points.
"""
from lace.mesh import Mesh
from lace.serialization import meshlab_pickedpoints
return cls(
source_mesh=Mesh(filename=source_mesh_path),
landmarks=meshlab_pickedpoints.load(landmark_path),
)
@cached_property
def _regressor(self):
"""
Find the face on which each landmarks sits. Then describe its
position as a linear combination of the three vertices of that face.
Represent this as a sparse matrix with a column for each coordinate
of the source vertices and a row for each coordinate of the
landmarks.
Pushing target vertices through the matrix transfers the original
landmarks to the target mesh.
"""
import numpy as np
from scipy.sparse import csc_matrix
from polliwog.tri import barycentric_coordinates_of_points
from ._trimesh_search import faces_nearest_to_points
landmark_coords = np.array(list(self.landmarks.values()))
face_indices = faces_nearest_to_points(self.source_mesh, landmark_coords)
vertex_indices = self.source_mesh.f[face_indices]
vertex_coeffs = barycentric_coordinates_of_points(
self.source_mesh.v[vertex_indices], landmark_coords
)
# Note the `.transpose()` at the end. The matrix is initially created
# from data organized along columns of the result, not rows.
values = np.repeat(vertex_coeffs, 3, axis=0).ravel()
indices = (
(3 * vertex_indices).reshape(-1, 1, 3)
+ np.arange(3, dtype=np.uint64).reshape(-1, 1)
).ravel()
indptr = np.arange(len(values) + 1, step=3)
shape = (3 * len(self.source_mesh.v), 3 * len(landmark_coords))
return csc_matrix((values, indices, indptr), shape=shape).transpose()
[docs] def transfer_landmarks_onto(self, target):
"""
Transfer landmarks onto the given target mesh, which must be in the same
topology as the source mesh.
Args:
target (lace.mesh.Mesh): Target mesh
Returns:
dict: A mapping of landmark names to a np.ndarray with shape `3x1`.
"""
from ..equality import have_same_topology
if not have_same_topology(self.source_mesh, target):
raise ValueError("Target mesh must have the same topology")
target_landmark_coords = (self._regressor * target.v.reshape(-1)).reshape(-1, 3)
return dict(zip(self.landmarks.keys(), target_landmark_coords))