I/O
The I/O module provides functions for reading and writing mesh data in STL format (both ASCII and binary) and OBJ format (ASCII), and volumes in NIfTI-1 (.nii, .nii.gz).
Reading
STL Files
Read an STL file and return mesh data:
import trueform as tf
import numpy as np
# Read STL file
faces, points = tf.read_stl("model.stl")
# faces: (N, 3) array with dtype np.int32
# points: (M, 3) array with dtype np.float32
# Create Mesh from loaded data
mesh = tf.Mesh(faces, points)
print(f"Loaded {len(faces)} faces, {len(points)} points")
# For large meshes (> 2 billion vertices), use int64 indices
faces, points = tf.read_stl("large_model.stl", index_dtype=np.int64)
OBJ Files
Read an OBJ file and return mesh data:
import trueform as tf
import numpy as np
# Read OBJ file (default: dynamic polygons)
faces, points = tf.read_obj("model.obj")
# faces: OffsetBlockedArray (variable-length face blocks)
# points: (M, 3) array with dtype np.float32
# Create Mesh from loaded data
mesh = tf.Mesh(faces, points)
print(f"Loaded {mesh.number_of_faces} faces, {mesh.number_of_points} points")
# Read as fixed-size triangles
faces, points = tf.read_obj("model.obj", ngon=3)
# faces: (N, 3) array with dtype np.int32
# Read as fixed-size quads
faces, points = tf.read_obj("quad_model.obj", ngon=4)
# faces: (N, 4) array with dtype np.int32
# For large meshes, use int64 indices
faces, points = tf.read_obj("large_model.obj", index_dtype=np.int64)
ngon=None returns an OffsetBlockedArray for faces, which supports mixed polygon sizes (triangles, quads, n-gons in the same file). Use ngon=3 or ngon=4 when you know all faces have the same size and want a regular 2D ndarray.NIfTI Volumes
tf.read_nifti(path, dtype=None) reads a NIfTI-1 medical volume — .nii or .nii.gz — in its native dtype: an int16 CT arrives int16 on a float32 grid, with no conversion pass — native-endian bytes, the header's scl scaling already applied. A posed file lands its affine in Volume.transformation, so the isosurface of a scan is a patient-space mesh in three lines. dtype= asks for another sample type and converts on read through one cast; it takes float32, float64, int16, uint16 or uint8, and anything else is refused with the accepted set named. A file whose own dtype has no row — int32 — reads converted to float32, since nothing was asked for.
scan = tf.read_nifti("ct.nii.gz") # int16, posed when the file is
faces, points = tf.isosurface(scan, iso=300) # patient-space mesh
tf.read_nifti_header(path) answers the file's facts — (dims, spacing, dtype, posed, reflecting, slope, inter, units) — without reading the samples; on a gzipped file only the head is inflated. A refusal raises ValueError naming its fact: truncated bytes, a foreign magic, a two-file header pair, an unsupported dtype, a real fourth dimension, a self-contradictory header.
Writing
STL Files
Write mesh data to binary STL format:
# Write from tuple (faces, points)
faces = np.array([[0, 1, 2]], dtype=np.int32)
points = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0]], dtype=np.float32)
tf.write_stl((faces, points), "output.stl")
# Write from Mesh object
mesh = tf.Mesh(faces, points)
tf.write_stl(mesh, "output_mesh.stl")
With transformation parameter:
# Apply transformation during write
transform = np.eye(4, dtype=np.float32)
transform[:3, 3] = [10, 0, 5] # Translate by (10, 0, 5)
tf.write_stl((faces, points), "translated.stl", transformation=transform)
Using mesh transformation property:
# Set transformation on mesh
mesh = tf.Mesh(faces, points)
mesh.transformation = transform
tf.write_stl(mesh, "mesh_with_transform.stl") # Transformation applied automatically
# Override mesh transformation
override_transform = np.eye(4, dtype=np.float32)
override_transform[2, 3] = 20 # Different Z translation
tf.write_stl(mesh, "overridden.stl", transformation=override_transform)
transformation parameter overrides mesh.transformation property.OBJ Files
Write mesh data to ASCII OBJ format:
# Write triangular mesh from tuple (faces, points)
faces = np.array([[0, 1, 2]], dtype=np.int32)
points = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0]], dtype=np.float32)
tf.write_obj((faces, points), "triangle.obj")
# Write dynamic mesh (mixed polygon sizes)
faces, points = tf.read_obj("mixed.obj")
tf.write_obj((faces, points), "output.obj")
# Write from Mesh object (fixed or dynamic)
mesh = tf.Mesh(faces, points)
tf.write_obj(mesh, "output_mesh.obj")
With transformation parameter:
# Apply transformation during write
transform = np.eye(4, dtype=np.float32)
transform[:3, 3] = [10, 0, 5] # Translate by (10, 0, 5)
tf.write_obj((faces, points), "translated.obj", transformation=transform)
Using mesh transformation property:
# Set transformation on mesh
mesh = tf.Mesh(faces, points)
mesh.transformation = transform
tf.write_obj(mesh, "mesh_with_transform.obj") # Transformation applied automatically
NIfTI Volumes
tf.write_nifti(volume, path) writes NIfTI-1, gzip by extension, and returns True when the file was written — as tf.write_stl and tf.write_obj do. A posed volume writes its transformation composed with the grid into the sform.
A file is one sampled function with one placement, and the read splits that placement: spacing and any axis-aligned translation onto the grid, the rest into the pose. So the composed world placement round-trips, not the split — a volume with a nonzero origin and a rotating pose reads back with the origin folded into the pose, and one whose pose is a pure translation reads back unposed with a shifted origin.
tf.write_nifti(scan, "out.nii.gz")
Round-trip Example
import trueform as tf
import numpy as np
# Read STL file
faces, points = tf.read_stl("input.stl")
# Create transformation (translate + rotate)
transform = np.eye(4, dtype=np.float32)
transform[:3, 3] = [5, 0, 0] # Translation
angle = np.radians(45)
transform[0, 0] = np.cos(angle)
transform[0, 1] = -np.sin(angle)
transform[1, 0] = np.sin(angle)
transform[1, 1] = np.cos(angle)
# Write with transformation
tf.write_stl((faces, points), "transformed.stl", transformation=transform)
# Or via Mesh object
mesh = tf.Mesh(faces, points)
mesh.transformation = transform
tf.write_stl(mesh, "transformed_mesh.stl")
