mesh = tcmesh.ObjMesh.read_obj("datasets/movie_example/initial_uv.obj")Warning: readOBJ() ignored non-comment line 4:
o mesh_01_cylinder_seams_uv
diffgeo - Surface differential geometryWhen analyzing data in cartographic projections, we have to make sure we are always accounting for mapping distortion and curvature. This notebook builds the relevant tools, including for vector calculus on curved surfaces.
When analyzing quantities computed in the UV cartographic projection (e.g. cell areas, orientation, tissue flow speed obtained from optical flow), you need to account for the cartographic projection. I generally recommend doing this by mapping every object back to 3d as soon as possible. This will minimize the number of possible errors. Example: you want to evaluate the angle between two edges of a cell. 1. You could do so in the UV map using the induced metric (see below). This is error-prone: did you calculate the metric correctly? Did you use the metric or its inverse? 2. You could map back the location of the cell vertices to 3d, and then calculate the edge vectors in 3d. This minimizes possibilities for confusion.
A map between two meshes is defined on a per-triangle basis (i.e. which triangle of the “source” mesh gets mapped to which one of the “target” mesh). The map between a 3d mesh and its UV unwrapping is a special case of this.
The mapping distortion is described by the Jacobian, defined per triangle. For more details, see here. The simplest kind of mapping distortion is area distortion, which is simply the ratio of (target triangle area) / (source triangle area).
In general, many mapping-related quantities, like area distortion, are most easily computed per triangle. We can “move” them to mesh vertices by interpolation. If we keep all of our quantities of interest defined per vertex, it will simplify bookkeeping.
The tools in this notebook only work for triangular meshes! Only for triangular meshes is the per-face map distortion uniquely defined.
Flatten a list of lists into a list.
Also works with inhomogeneous lists, e.g., [[0,1],2]. The argument depth determines how “deep” to flatten the list, e.g. with max_depth=1: [[(1,0), (1,0)]] -> [(1,0), (1,0)].
def compute_per_vertex_area_distortion(
source_vertices, # Source mesh vertices.
source_faces, # Spurce mesh faces. Must be triangular.
target_vertices, # Target mesh vertices.
target_faces, # Target mesh faces. Must be triangular.
evaluate_at:str='source', # Whether to evaluate the result at the source or target
mesh vertices.
cutoff:float=1e-15, # Numerical cutoff for small target areas (avoid 0 division error)
): # Area distortion factor (source area/target area) evaluated on source or target mesh vertices.
Compute area distortion factor for a map between meshes.
The result is evaluated at the vertices of the source or target mesh. Faces must be such that faces[i] is mapped to target_faces[i]. This function computes (target area / source area).
This can be used to (a) visualize map distortion and (b) correct for it, for example by using it as a weighting factor for averages.
Example (compute distortion of UV map): compute_per_vertex_area_distortion(mesh.texture_vertices, mesh.texture_tris, mesh.vertices, mesh.tris)
Replace values of per_vertex_field at boundary vertices by average of its non-boundary neighbors. If there are no non-boundary neighbors, use fill_value.
def get_area_distortion_in_UV(
mesh, # Input mesh with UV coordinates.
uv_grid_steps:int=1024, # Size of UV grid. Determines resolution of result.
map_back:bool=True, # Map back the UV coordinates to [0,1]^2. Else, coordinates outside [0,1] are ignored.
): # Area distortion across [0,1]^2 UV grid, with uniform step size. UV positions that don't
correspond to any value are set to np.nan.
Get area distortion of UV map, interpolated across the UV square.
Used to measure area distortion of your cartographic mapping. This can be used to (a) visualize map distortion and (b) correct for it, for example by using it as a weighting factor for averages.
Assumes the map \(x,y,z \mapsto u,v\) to be invertible. This is not guaranteed - you can create overlapping UV coordinates in blender. The provided UV coordinates will be mapped back to [0, 1]^2 if map_back is True. Else, coordinates outside [0,1] are ignored.
Warning: readOBJ() ignored non-comment line 4:
o mesh_01_cylinder_seams_uv
/home/nikolas/Documents/UCSB/streichan/numerics/code/python_code/jupyter_notebooks/blender-tissue-cartography/blender_tissue_cartography/interpolation.py:217: RuntimeWarning: UV map has self-intersections, 33 flipped triangles. Try use_fallback=True?
warnings.warn("UV map has self-intersections, {} flipped triangles. Try use_fallback=True?".format(

def compute_per_vertex_angle_distortion(
source_vertices, # Source mesh vertices.
source_faces, # Spurce mesh faces. Must be triangular.
target_vertices, # Target mesh vertices.
target_faces, # Target mesh faces. Must be triangular.
evaluate_at:str='source', # Whether to evaluate the result at the source or target
mesh vertices.
cutoff:float=1e-15
): # Area distortion factor (source area/target area) evaluated on source or target mesh vertices.
Compute angle distortion for a map between meshes.
The result is evaluated at the vertices of the source or target mesh. Faces must be such that faces[i] is mapped to target_faces[i]. This function computes np.abs(target angles - source angles), averaged over each triangle (in radians).
This can be used to visualize map distortion.
Example (compute distortion of UV map): compute_per_vertex_angle_distortion(mesh.texture_vertices, mesh.texture_tris, mesh.vertices, mesh.tris)
In general, in addition to getting inflated or shrunk, during the mapping process, a triangle is also rotated and sheared. The full transformation properties are encoded by the Jacobian matrix.
You need the Jacobian for example to map vectors (e.g. displacement from optical flow) that you calculated in UV space back into 3d.
Using the tcinterp.interpolate_per_vertex_field_to_UV, you can interpolate the Jacobian from mesh vertices into the whole UV square.
((40420, 3, 2), (40420, 3, 3))
array([0., 0., 0.])
def compute_per_vertex_jacobian(
source_vertices, # Source mesh vertices.
source_faces, # Spurce mesh faces. Must be triangular.
target_vertices, # Target mesh vertices.
target_faces, # Target mesh faces. Must be triangular.
evaluate_at:str='source', # Whether to evaluate the result at the source or target
mesh vertices.
): # Jacobian. Shape is (n_vertices, d_target, d_source),
where d is the spatial dimension (e.g. 2, 3).
Compute Jacobian factor for a map between meshes.
Faces must be such that faces[i] is mapped to target_faces[i]. This function computes the Jacobian mapping tangent vectors of the source mesh to tangent vectors of the target mesh. The result is evaluated at the vertices of the source mesh.
Important: after you apply the per-vertex Jacobian, you may still need to permute the result, defined per source vertex, so it is defined per target mesh, e.g. using mesh.get_vertex_to_texture_vertex_indices() if mapping from 3d to UV.
Example (compute distortion of UV map): compute_per_vertex_jacobian(mesh.texture_vertices, mesh.texture_tris mesh.vertices, mesh.tris)
def compute_per_face_jacobian(
source_vertices, # Source mesh vertices.
source_faces, # Spurce mesh faces. Must be triangular.
target_vertices, # Target mesh vertices.
target_faces, # Target mesh faces. Must be triangular.
): # Jacobian. Shape is (n_faces, d_target, d_source),
where d is the spatial dimension (e.g. 2, 3).
Compute Jacobian factor for a map between meshes.
Faces must be such that faces[i] is mapped to target_faces[i]. This function computes the Jacobian mapping tangent vectors of the source mesh to tangent vectors of the target mesh. The result is evaluated at the mesh faces.
Example (compute distortion of UV map): compute_per_vertex_jacobian(mesh.texture_vertices, mesh.texture_tris mesh.vertices, mesh.tris)
array([0.99769177, 0.99467172, 0.99815593, ..., 1. , 0.99925048,
1. ])
/home/nikolas/Documents/UCSB/streichan/numerics/code/python_code/jupyter_notebooks/blender-tissue-cartography/blender_tissue_cartography/interpolation.py:217: RuntimeWarning: UV map has self-intersections, 33 flipped triangles. Try use_fallback=True?
warnings.warn("UV map has self-intersections, {} flipped triangles. Try use_fallback=True?".format(
The induced metric is defined via \(g = J^T \cdot J\) where \(J\) is the Jacobian of the map from UV-space to 3d space.
Note: when doing calculations like those shown below, np.einsum is invaluable.
Compute induced metric, evaluated at textures vertices.
See https://en.wikipedia.org/wiki/Induced_metric.
Compute angle in radians between two vector fields (shape (…, d)) in degrees using metric g (shape (…, d, d)).
Compute norm of vector field vf (shape (…, d)) using metric g (shape (…, d, d)).
array([0.99769177, 0.99467172, 0.99815593, ..., 1. , 0.99925048,
1. ])
array([ 898.28014016, 854.89157326, 1314.41088305, ...,
50596.27040954, 81144.22642543, 8305.58231028])
array([ 898.28014016, 854.89157326, 1314.41088305, ...,
50596.27040954, 81144.22642543, 8305.58231028])
How to generalize the familiar operations of vector calculus (div, rot, grad, etc) to curved surfaces is a key part of differential geometry. There are different approaches for doing surface differential geometry numerically:
Here, we take a less elegant, but simpler approach that takes advantage of the fact that our surfaces are embedded in 3d cartesian space. All vector and tensor fields are mapped back into 3d so that their components are defined in \(x,y,z\) coordinates. Then we can imagine “extending” from the surface into full 3d space by defining them to be constant along the local surface normal. Then we are back to normal vector calculus!
In practice, we don’t need to do this extension explicitly. We can compute the derivative of any quantity defined on mesh vertices in such a way that the derivative along the normal direction is 0. This is implemented by standard “finite-element” gradient operators. See here: https://libigl.github.io/libigl-python-bindings/tut-chapter1/#gradient
def tri_grad(
field, # scalar, vector, or tensor field defined at mesh vertices
vertices, # vertices.
faces, # Triangular faces.
grad_matrix:NoneType=None, # Gradient operator. The default is None (calculate from vertices, faces).
): # Gradient of scalar function/tensor, defined on vertices.
Axis 1 comprises the gradients along x,y,z.
Calculate the gradient of a function defined on vertices of a triangular mesh.
If a vector or tensor field is passed, the gradient is applied to each component individually.
See https://libigl.github.io/libigl-python-bindings/tut-chapter1/#gradient
Warning: readOBJ() ignored non-comment line 4:
o mesh_01_cylinder_seams_uv
# get some random fields - eigenfunctions of the laplacian
laplacian = igl.cotmatrix(mesh.vertices, mesh.tris)
mass = igl.massmatrix(mesh.vertices, mesh.tris)
eigen_vals, eigen_vecs = sparse.linalg.eigsh(-laplacian, M=mass, k=10, which="SM")
eigen_vals, eigen_vecs = (eigen_vals[1:], eigen_vecs[:, 1:])
np.savetxt('datasets/diffgeo/eigen_vals.txt', eigen_vals)
np.savetxt('datasets/diffgeo/eigen_vecs.txt', eigen_vecs)# Here is how you would map the gradients to 2d. You first need to apply the Jacobian,
# and then you still need to reindex.
jac = compute_per_vertex_jacobian(mesh.vertices, mesh.tris, mesh.texture_vertices, mesh.texture_tris, )
scalar_gradient_projected = np.einsum('vij,vj->vi', jac, scalar_gradient)[mesh.get_vertex_to_texture_vertex_indices()]Get projection matrix that removes components normal to the surface
Mathematically, 1-n.n^T where n is the unit surface normal. Defined per vertex.
def separate_tangential_normal(
field, # Vector or rank-2 tensor field defined at vertices
vertices:NoneType=None, # vertices. If None, must supply normals
faces:NoneType=None, # Triangular faces. If None, must supply normals
normals:NoneType=None, # If None, recompute normals from vertices and faces
): # tangential_component : np.array of shape (#vertices, dim) or (#vertices, dim, dim)
normal_component : np.array of shape (#vertices, dim) or (#vertices, dim, dim)
Separate tangential and normal components of field defined at vertices.
Vector and rank-2 tensor fields are supported. For a rank-2 tensor, normal-tangential cross components are discarded.
Calculate the gradient of a scalar field, rotated by 90 deg around the surface normal.
As occurs e.g. when calculating vector field from stream function.
Calculate tangent-plane rotation of vector field defined on vertices of triangular mesh.
This result is a scalar, equal to (Nabla x field).normals
Calculate tangent-plane divergence of vector field defined on vertices of triangular mesh.
array([-0.00132528, -0.00133886, -0.00139996, ..., -0.01188986,
-0.01150177, -0.00547512])