Modules | PY

Geometry

Geometric analysis, surface properties, and point cloud alignment.

The Geometry module provides tools for computing geometric properties of meshes, including normals, curvatures, triangulation, and point cloud alignment.

Normal Computation

Compute face normals and vertex normals for 3D meshes. Normals are lazily computed on first access and cached for subsequent use.

Face Normals

Access face normals via the normals property:

import trueform as tf
import numpy as np

faces = np.array([[0, 1, 2], [1, 3, 2]], dtype=np.int32)
points = np.array([
    [0, 0, 0], [1, 0, 0], [0.5, 1, 0], [1.5, 1, 0]
], dtype=np.float32)
mesh = tf.Mesh(faces, points)

# Get face normals (one unit normal per face)
normals = mesh.normals  # shape: (num_faces, 3)

# Prebuild for performance (optional)
mesh.build_normals()

# Set custom normals
mesh.normals = custom_normals

Vertex Normals

Vertex normals are computed by averaging adjacent face normals, weighted by face area:

# Get vertex normals (one unit normal per vertex)
point_normals = mesh.point_normals  # shape: (num_points, 3)

# Prebuild for performance (optional)
mesh.build_point_normals()

# Set custom vertex normals
mesh.point_normals = custom_point_normals
PropertyReturnsDescription
normalsndarrayUnit face normals, shape (num_faces, 3)
point_normalsndarrayUnit vertex normals, shape (num_points, 3)
Normals are only supported for 3D meshes. Accessing normals or point_normals on a 2D mesh raises ValueError.

Standalone Functions

For convenience, standalone functions accept either a Mesh or a (faces, points) tuple:

import trueform as tf

# From Mesh
normals = tf.normals(mesh)
point_normals = tf.point_normals(mesh)

# From tuple
normals = tf.normals((faces, points))
point_normals = tf.point_normals((faces, points))

# Dynamic mesh (OffsetBlockedArray)
normals = tf.normals((dyn_faces, points))

Curvature Analysis

Compute surface curvature properties at each vertex by fitting a quadric to the local k-ring neighborhood.

Principal Curvatures

Compute the two principal curvatures (k0, k1) at each vertex:

import trueform as tf

# Basic usage
k0, k1 = tf.principal_curvatures(mesh)

# With custom k-ring size (default k=2)
k0, k1 = tf.principal_curvatures(mesh, k=3)

# Also compute principal directions
k0, k1, d0, d1 = tf.principal_curvatures(mesh, directions=True)

# Works with tuples
k0, k1 = tf.principal_curvatures((faces, points))

# Derived curvature measures
gaussian_curvature = k0 * k1
mean_curvature = (k0 + k1) / 2
ParameterTypeDefaultDescription
dataMesh or tupleInput mesh or (faces, points) tuple
kint2k-ring neighborhood size
directionsboolFalseIf True, also return principal directions
ReturnsTypeDescription
k0ndarrayMaximum principal curvature, shape (num_points,)
k1ndarrayMinimum principal curvature, shape (num_points,)
d0ndarrayDirection of max curvature, shape (num_points, 3) (only if directions=True)
d1ndarrayDirection of min curvature, shape (num_points, 3) (only if directions=True)

Shape Index

Shape index maps principal curvatures to -1, 1, characterizing local surface type:

Index RangeSurface Type
[-1, -5/8)Concave ellipsoid (cup)
[-5/8, -3/8)Concave cylinder (trough)
[-3/8, 3/8)Hyperboloid (saddle)
[3/8, 5/8)Convex cylinder (ridge)
5/8, 1Convex ellipsoid (cap)
import trueform as tf

# Basic usage
si = tf.shape_index(mesh)

# With custom k-ring size
si = tf.shape_index(mesh, k=3)

# Works with tuples
si = tf.shape_index((faces, points))
ParameterTypeDefaultDescription
dataMesh or tupleInput mesh or (faces, points) tuple
kint2k-ring neighborhood size
ReturnsTypeDescription
shape_indexndarrayShape index at each vertex, shape (num_points,)

Orientation

Ensure Positive Orientation

For closed 3D meshes, ensure faces are oriented so that normals point outward (positive signed volume):

import trueform as tf

# Orient mesh to have outward-facing normals
new_faces = tf.ensure_positive_orientation(mesh)

# Or with tuple input
new_faces = tf.ensure_positive_orientation((faces, points))

# Skip consistency step if mesh is already consistently oriented
new_faces = tf.ensure_positive_orientation(mesh, is_consistent=True)
ParameterTypeDefaultDescription
dataMesh or tupleInput mesh or (faces, points) tuple (3D only)
is_consistentboolFalseSkip orient_faces_consistently step
ReturnsTypeDescription
facesndarray or OffsetBlockedArrayReoriented face indices

This function:

  1. Calls tf.orient_faces_consistently (unless is_consistent=True)
  2. Computes the signed volume of the mesh
  3. Reverses all face windings if the signed volume is negative
Only works with 3D meshes. Signed volume is positive when face normals point outward from a closed mesh.

Point Cloud Alignment

Compute transformations to align point sets. Methods are organized by whether they require known point correspondences.

With Correspondences

Rigid Alignment

Use the Kabsch/Procrustes algorithm for optimal rotation + translation. Requires paired points where X[i] corresponds to Y[i]:

import trueform as tf
import numpy as np

# X and Y must have the same number of points
# X[i] corresponds to Y[i]
cloud_x = tf.PointCloud(pts_x)
cloud_y = tf.PointCloud(pts_y)

# Compute optimal rigid transformation T such that T(X) ≈ Y
T = tf.fit_rigid_alignment(cloud_x, cloud_y)

# T is a 4x4 homogeneous transformation matrix
print(T.shape)  # (4, 4)

# Apply the transformation
cloud_x.transformation = T
ParameterTypeDescription
cloud0PointCloudSource point cloud
cloud1PointCloudTarget point cloud (same size as cloud0)
ReturnsTypeDescription
transformationndarray(3,3) for 2D or (4,4) for 3D homogeneous matrix

Without Correspondences

These methods work when point correspondences are unknown or point counts differ.

OBB-Based Alignment

Align using oriented bounding boxes (OBBs):

# Different point counts allowed
cloud_x = tf.PointCloud(pts_x)  # 1000 points
cloud_y = tf.PointCloud(pts_y)  # 500 points

# Align OBB axes and centers
T = tf.fit_obb_alignment(cloud_x, cloud_y)

# Apply transformation
cloud_x.transformation = T
ParameterTypeDefaultDescription
cloud0PointCloudSource point cloud
cloud1PointCloudTarget point cloud
sample_sizeint100Points to sample for orientation disambiguation
ReturnsTypeDescription
transformationndarray(3,3) for 2D or (4,4) for 3D homogeneous matrix
OBB alignment is inherently ambiguous up to 180° rotations about each axis. The function tests all orientations and selects the one with lowest chamfer distance.

KNN Alignment (ICP Step)

For iterative closest point (ICP) registration, use k-nearest neighbor alignment:

# Single ICP iteration
T_iter = tf.fit_knn_alignment(cloud_x, cloud_y)

# Soft correspondences with k > 1 neighbors
T_soft = tf.fit_knn_alignment(cloud_x, cloud_y, k=5)

# Custom sigma for Gaussian weighting
T_custom = tf.fit_knn_alignment(cloud_x, cloud_y, k=5, sigma=0.1)
ParameterTypeDefaultDescription
cloud0PointCloudSource point cloud
cloud1PointCloudTarget point cloud
kint1Number of nearest neighbors (k=1 is classic ICP)
sigmafloat or NoneNoneGaussian kernel width. If None, uses k-th neighbor distance
ReturnsTypeDescription
transformationndarray(3,3) for 2D or (4,4) for 3D homogeneous matrix

ICP Registration Example

Iteratively refine an initial alignment:

# Start from an initial transformation
cloud_source.transformation = T_initial

# ICP refinement loop
for i in range(50):
    T_iter = tf.fit_knn_alignment(cloud_source, cloud_target, k=5)
    cloud_source.transformation = T_iter @ cloud_source.transformation

    error = tf.chamfer_error(cloud_source, cloud_target)
    if error < threshold:
        break

Error Metrics

Chamfer Error

Compute one-way chamfer error (mean nearest-neighbor distance):

# One-way error: mean distance from X to nearest point in Y
error_xy = tf.chamfer_error(cloud_x, cloud_y)

# Symmetric chamfer distance
error_yx = tf.chamfer_error(cloud_y, cloud_x)
symmetric_error = (error_xy + error_yx) / 2
ParameterTypeDescription
cloud0PointCloudSource point cloud
cloud1PointCloudTarget point cloud
ReturnsTypeDescription
errorfloatMean nearest-neighbor distance
Chamfer error is asymmetric. For alignment quality assessment, compute both directions.

Transformations

All alignment functions respect transformations set on point clouds. The computation is performed in world space:

cloud_a.transformation = initial_transform

# Alignment computed in world space
T = tf.fit_knn_alignment(cloud_a, cloud_b)

# Accumulate transformations
cloud_a.transformation = T @ cloud_a.transformation

Triangulation

Convert polygon meshes to triangle meshes using ear-cutting triangulation.

Single Polygon

Triangulate a single polygon defined by its boundary vertices:

import trueform as tf
import numpy as np

# Single polygon (just points defining the polygon boundary)
polygon = np.array([
    [0, 0, 0],
    [1, 0, 0],
    [1, 1, 0],
    [0.5, 1.5, 0],
    [0, 1, 0]
], dtype=np.float32)

faces, points = tf.triangulated(polygon)
# faces.shape = (3, 3)  # 5-gon -> 3 triangles
# points.shape = (5, 3)

Polygon Mesh

For polygon meshes (quads, n-gons), pass a tuple of (faces, points):

# Quad mesh
quads = np.array([[0, 1, 2, 3], [1, 4, 5, 2]], dtype=np.int32)
pts = np.array([
    [0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0],
    [2, 0, 0], [2, 1, 0]
], dtype=np.float32)

faces, points = tf.triangulated((quads, pts))
# faces.shape = (4, 3)  # 2 quads -> 4 triangles

For variable-size polygons, use OffsetBlockedArray:

# Mixed n-gons (triangle + quad + pentagon)
offsets = np.array([0, 3, 7, 12], dtype=np.int32)
data = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], dtype=np.int32)
dyn_faces = tf.OffsetBlockedArray(offsets, data)

faces, points = tf.triangulated((dyn_faces, pts))
# Triangle (1) + Quad (2) + Pentagon (3) = 6 triangles
ParameterTypeDescription
datandarray, Mesh, or tupleInput polygon(s) to triangulate

Input formats:

FormatDescription
ndarray shape (N, D)Single polygon with N vertices in D dimensions
MeshTriangle mesh (returned as-is) or dynamic n-gon mesh
(faces, points) tupleFixed n-gon mesh (triangles, quads, etc.)
(OffsetBlockedArray, points) tupleVariable n-gon mesh
ReturnsTypeDescription
facesndarrayTriangle indices, shape (num_triangles, 3)
pointsndarrayVertex coordinates (copied from input)
Uses the ear-cutting algorithm. For polygons with more than 80 vertices, z-order curve indexing is used. For N-dimensional polygons, faces are projected to 2D before triangulation.