Fast and Robust Isocontours on a Triangle Mesh in Python
Extract isocontours (isolines) of any scalar field on a triangle mesh in Python. One level or many at once, plus isobands that cut the mesh along the curves. One pip install, NumPy arrays in and out.
An isocontour is the curve where a scalar field on a mesh takes a given value. Height gives contour lines. Distance from a point gives rings. Curvature gives the borders between shapes. trueform extracts isocontours of any per-vertex field, at one level or many, and cuts the mesh into isobands along them. One pip install. NumPy arrays in, NumPy arrays out. This tutorial covers single and multiple levels, the curve format, and isobands.

Install
pip install trueform
See the installation guide for all options.
Loading the mesh
import trueform as tf
import numpy as np
mesh = tf.Mesh(*tf.read_stl("terrain.stl"))
tf.read_obj works the same way. If you already have NumPy arrays, pass them to tf.Mesh(faces, points). To follow along without a file, make a small wavy terrain:
faces, points = tf.make_plane_mesh(2.0, 2.0, 200, 200)
x, y = points[:, 0], points[:, 1]
points[:, 2] = 0.2 * np.sin(3 * x) * np.cos(3 * y)
mesh = tf.Mesh(faces, points)
The scalar field
A scalar field is one value per point, as a NumPy array of shape (num_points,). It must have the mesh's dtype. Most meshes are float32, so most fields are float32.
Height is the simplest field:
field = mesh.points[:, 2].copy()

Any field works. A few that come from trueform itself:
# signed distance to a plane: x = 0
plane = tf.Plane(np.array([1, 0, 0, 0], dtype=np.float32))
across = tf.distance(tf.Point(mesh.points), plane)
# geodesic distance from a point
along = tf.geodesic_distances(mesh, [len(mesh.points) // 2])
# shape index, from curvature
shape = tf.shape_index(mesh)
If your field is float64 and the mesh is float32, tf.isocontours raises a TypeError. Convert with field.astype(mesh.points.dtype).
One level
paths, curve_points = tf.isocontours(mesh, field, 0.0)

curve_points is an (n, 3) array of points on the surface. paths holds one index list per curve:
for path_ids in paths:
curve = curve_points[path_ids] # (m, 3) points along one curve
closed = path_ids[0] == path_ids[-1] # a closed loop repeats its first id
A curve that runs off the edge of an open mesh ends at the boundary. A curve on a closed region closes on itself.
paths is an OffsetBlockedArray. If you want flat arrays, paths.offsets and paths.data give them directly. Curve i is paths.data[paths.offsets[i]:paths.offsets[i + 1]].
Many levels
Pass an array of levels. One call extracts them all.
levels = np.linspace(-0.15, 0.15, 7, dtype=np.float32)
paths, curve_points = tf.isocontours(mesh, field, levels)

The result does not say which level each curve belongs to, and the curves are not in level order. When you need that, call once per level:
by_level = {
float(t): tf.isocontours(mesh, field, float(t)) for t in levels
}
For height, you can also read the level from the curve itself: every point on a height contour has the level as its z.
Isobands
Isobands are the regions between levels. tf.isobands cuts the mesh along the isocontours, so the curves become edges, and labels each face by band.
cuts = np.array([-0.1, 0.0, 0.1], dtype=np.float32)
(band_faces, band_points), labels, face_labels = \
tf.isobands(mesh, field, cuts)

Three cuts give four bands. Band 0 is below -0.1, band 3 is above 0.1. labels gives the band of each output face. face_labels gives the original face it came from.
Keep only some bands:
(band_faces, band_points), labels, face_labels = \
tf.isobands(mesh, field, cuts, selected_bands=[0, 3])
Get the boundary curves in the same call:
(band_faces, band_points), labels, face_labels, (paths, curve_points) = \
tf.isobands(mesh, field, cuts, return_curves=True)
Working with results
Split the bands into separate meshes:
band_mesh = tf.Mesh(band_faces, band_points)
parts, part_labels = tf.split_into_components(band_mesh, labels)
# parts[i] is a (faces, points) tuple for band part_labels[i]
Carry per-face data from the input to the output with face_labels:
material = np.zeros(len(faces), dtype=np.int32) # your per-face data
band_material = material[face_labels]
Write to file:
tf.write_obj((band_faces, band_points), "bands.obj")
Contours of height are slices. See mesh slicing and cross-sections for layers and sections. Contours of geodesic distance are rings. Contours of the shape index separate cups, saddles, and caps. To cut one mesh by another mesh instead of by a field, use booleans.
Related tutorials
- Fast and Accurate Geodesic Distances in Python — distance along the surface from one seed or many.
- Fast and Robust Mesh Slicing and Cross-Sections in Python — plane sections, layers and slabs of a mesh.
- Fast and Robust Principal Curvatures of a Mesh in Python — curvature values and directions at every vertex.
- Fast and Exact Mesh Intersection Curves in Python — exact curves where two or more meshes cross.
Performance
Apple M4 Max 16 threads

Full benchmarks · Documentation
@article{polydera:isocontours-on-a-triangle-mesh-in-python,
title={Fast and Robust Isocontours on a Triangle Mesh in Python},
author={Sajovic, {\v{Z}}iga, Polydera},
year={2026},
url={https://polydera.com/tutorials/isocontours-on-a-triangle-mesh-in-python},
organization={Polydera}
}