Modules | PY

Volume

Dense scalar grids — signed distance fields, CSG, slicing, and isosurfaces.

The Volume module works on a dense scalar field sampled on a regular, axis-aligned voxel grid. It generates such a field from a mesh or an analytic solid, combines fields with CSG operators, slices one onto a plane, and extracts the geometry a field's level set names.

import numpy as np
import trueform as tf
FunctionReturnsDoes
tf.sphere_sdfVolumeThe analytic sphere field
tf.mesh_sdfVolumeThe signed distance field of a closed mesh
tf.volume_booleanVolumeUnion / intersection / difference of two fields
tf.isosurface(faces, points)The level set as a triangle mesh
tf.volume_slice_contours(paths, points)Isocontours on an oriented slice plane, as 3D curves
tf.resampled_volumeVolumeThe field regridded onto a stated grid
tf.read_nifti / tf.write_niftiVolume / boolNIfTI-1 interchange (I/O)
vol = tf.sphere_sdf((64, 64, 64), (0.1, 0.1, 0.1), (-3.2, -3.2, -3.2),
                    (0.0, 0.0, 0.0), 1.5)

faces, points = tf.isosurface(vol)     # the zero level set

A volume boolean is a sample-wise combine of two fields: exact on the zero level set and bound by the grid's resolution. The CSG module's mesh boolean is exact everywhere and carries no resolution. Use fields when the input is already a field, when the shapes are offsets or blends, or when a self-intersecting input must be resolved by resampling; use CSG when the operands are meshes you want back unchanged.

Volume

x, y, z = np.meshgrid(np.arange(64.0), np.arange(64.0), np.arange(64.0),
                      indexing="ij")
sdf = np.sqrt((x - 32)**2 + (y - 32)**2 + (z - 32)**2) - 12.0

vol = tf.Volume(sdf.astype(np.float32), spacing=(0.1, 0.1, 0.1),
                origin=(-3.2, -3.2, -3.2))
vol.dims          # (64, 64, 64)
vol.spacing       # one voxel step along x, y, z
vol.origin        # local-space position of sample (0, 0, 0)
vol.dtype         # dtype('float32')
vol.voxel_count   # 262144
vol.samples[10, 12, 14] = -0.5      # a view of the field, indexed [x, y, z]
ParameterTypeDescription
samplesnp.ndarray shape (nx, ny, nz)The field, indexed samples[x, y, z], dtype float32, float64, int16, uint16 or uint8. Any memory order; any other dtype is converted to float32
spacingsequence of 3 floatsPhysical size of one voxel step. Must be finite and positive. Default (1, 1, 1)
originsequence of 3 floatsLocal-space position of sample (0, 0, 0). Default (0, 0, 0)
transformationnp.ndarray shape (4, 4), optionalThe world pose, in coordinate_dtype (Transformations). Default None — unposed

A sample's local-space position is origin + (x, y, z) * spacing.

The samples are borrowed zero-copy when the array is already in the native layout — the module's own x-fastest order — so in-place writes through samples mutate the field. A C-ordered array (NumPy's default) is converted once at construction, and that converted array is what the volume then retains:

sdf = np.asfortranarray(sdf.astype(np.float32))   # borrowed, no copy
vol = tf.Volume(sdf)
vol.samples[0, 0, 0] = -1.0
sdf[0, 0, 0]                                      # -1.0 — the same memory

A Volume's Two dtypes

PropertyThe question it answers
dtypeWhat a sample is — the measurement's own type
coordinate_dtypeWhere the samples stand — the type spacing, origin and every emitted position answer in

They coincide for a real-valued field. They come apart where measurement and geometry have different natures — medical imaging, whose CT counts are int16 and whose grid is fractional millimetres:

ct = np.asfortranarray(counts)              # int16, e.g. Hounsfield units
vol = tf.Volume(ct, spacing=(0.7, 0.7, 1.5))

vol.dtype               # dtype('int16')   — borrowed, not one sample widened
vol.coordinate_dtype    # dtype('float32') — 0.7 mm is not an int16

The samples stay int16 while the grid is floating, so the spacing survives and the field costs what the measurement costs: a 512³ scan holds 256 MB of samples, not the 512 MB a float32 field would. A byte-swapped array of an accepted dtype is byteswapped into its own row rather than widened, so NIfTI's big-endian >i2 stays an int16 field.

int16, uint16 and uint8 are accepted wherever a field is consumed: isosurface and volume_slice_contours. A field generator emits what it computes, so sphere_sdf and mesh_sdf stay real-valued.

The dtype Argument

Every entry takes a dtype argument for the field or the geometry it emits; only float32 and float64 are accepted. Unstated, each entry falls back to its own input:

EntryUnstated dtype
isosurface, volume_slice_contoursthe volume's coordinate_dtype
volume_booleanA's coordinate_dtype
resampled_volumethe volume's sample dtype (it accepts float fields only, where the two coincide)
mesh_sdfthe mesh's dtype — the call takes no volume
sphere_sdffloat32 — it builds a field out of numbers, so there is nothing to read a type from
faces, points = tf.isosurface(vol)                      # float32 samples -> float32 points
faces, points = tf.isosurface(vol, dtype=np.float64)    # float64 points
faces, points = tf.isosurface(ct_volume, 300.0)         # int16 samples -> float32 points

A coordinate_dtype is float32 for an integer-sampled field, which is why the int16 scan above emits float32 points. Each sample is read through one cast into the emitted type, so a crossing always lands on the edge it belongs to. An isovalue is a field value in that type, not in the sample type, so a fractional threshold on an integer field is exactly expressible.

Transformations

A Volume mirrors Mesh: an optional 4x4 world pose in transformation, in the volume's coordinate_dtype.

vol.transformation = pose_4x4          # posed
faces, points = tf.isosurface(vol)     # world-space triangles
vol.transformation = None              # unposed again

The grid stays axis-aligned in its own local space. Positional inputs — a slice plane's origin and axes, an unposed volume's resample target grid — stay local. isosurface and volume_slice_contours emit world-space geometry, resampled_volume resamples through the pose onto a world-space target grid, and volume_boolean combines on one shared grid. Setting the identity is the same statement as setting None, so an identity pose reads back as None.

Signed Distance Fields

A signed distance field is negative inside the solid, positive outside, and zero on its surface; extracting the isosurface at 0 recovers the shape.

sphere_sdf

ball = tf.sphere_sdf((64, 64, 64), (0.1, 0.1, 0.1), (-3.2, -3.2, -3.2),
                     (0.0, 0.0, 0.0), 1.5)
ParameterDefaultDescription
dimsNumber of samples along x, y, z
spacingPhysical size of one voxel step along x, y, z
originLocal-space position of sample (0, 0, 0)
centerSphere centre, in the volume's local frame
radiusSphere radius
dtypenp.float32The sample type of the field, float32 or float64

Each sample holds distance(point, center) - radius.

mesh_sdf

faces, points = tf.make_sphere_mesh(1.0)
field = tf.mesh_sdf((faces, points), (64, 64, 64), (0.1, 0.1, 0.1),
                    (-3.2, -3.2, -3.2))

# a Mesh works the same way
field = tf.mesh_sdf(tf.Mesh(faces, points), (64, 64, 64), (0.1, 0.1, 0.1),
                    (-3.2, -3.2, -3.2))
ParameterDefaultDescription
dataA Mesh or a (faces, points) tuple
dimsNumber of samples along x, y, z
spacingPhysical size of one voxel step along x, y, z
originPosition of sample (0, 0, 0), in the mesh's own frame — world space when it carries a transformation
dtypethe mesh's own dtypeThe sample type of the field, float32 or float64
mode"exact""exact" measures every sample; "banded" measures a band and sweeps the rest
band2Banded only: voxels of exactly measured magnitude on each side of the surface

Each sample takes the distance to the nearest point of the surface, signed by exact crossing parity: negative inside by winding, so an inverted shell inverts its field and nested shells stay solid inside.

The mesh must be closed — an open mesh has no inside, and the parity sign is undefined on one. Repair an open mesh with tf.outer_shell first.

The spatial tree the measurement needs is built and cached on the mesh for you; pass a Mesh (rather than a tuple) when you call more than once, so the tree is paid for once. The grid samples the mesh in world space when the mesh carries a transformation and in its local frame otherwise, so build dims/spacing/origin from the mesh's bounds in that same space.

mesh = tf.Mesh(faces, points)
mesh.build_tree()                       # optional: pay for it when you choose
near = tf.mesh_sdf(mesh, (64, 64, 64), (0.1, 0.1, 0.1), (-3.2, -3.2, -3.2))
fast = tf.mesh_sdf(mesh, (64, 64, 64), (0.1, 0.1, 0.1), (-3.2, -3.2, -3.2),
                   mode="banded", band=4)

mode="banded" measures only the samples within band voxels of the surface and propagates the far field with a seeded distance sweep — an order of magnitude faster. The sign is exact everywhere and the magnitude is exact inside the band. Beyond it, on grids whose lines resolve the surface, the 99th percentile of the error is under about one voxel and shrinks with resolution; the deep interior near the medial axis may locally undershoot by a few voxels. A feature no grid line meets is not found when the band is measured, so its neighbourhood takes the swept far field's value instead.

Isosurface Extraction

isosurface

faces, points = tf.isosurface(vol)                          # the zero level set
faces, points = tf.isosurface(vol, 0.25)                    # an offset surface
faces, points = tf.isosurface(vol, method="dual_contouring")
ParameterDefaultDescription
volumeThe scalar volume
iso0.0The isovalue to extract
method"flying_edges""flying_edges" places every vertex on a grid edge — the fastest regular output, defined for any field. "dual_contouring" places one vertex per surface component of a cell, fitted to the field's own crossings, so creases and corners survive; it assumes a distance-like field
refineTrueDual contouring only: refit each feature vertex to the planes its neighbourhood's crossings state
stabilizer0.01Dual contouring only: the dimensionless pull toward the crossing centroid. Must be finite and nonnegative
dtypevolume's coordinate_dtypeThe coordinate type of the emitted points

faces comes back (N, 3) dtype int32 and points (M, 3) in the requested dtype — a welded, indexed triangle mesh in the volume's own frame, or world space when it carries a transformation. A corner is inside when sample < iso, so a signed distance field (negative inside) yields outward windings. For an SDF a nonzero isovalue is an offset surface: +d inflates by d, -d deflates by it.

Dual contouring's output is manifold by construction. Reach for it on an SDF of a machined or faceted shape, where flying edges rounds every crease off to the grid:

faces, points = tf.isosurface(vol, method="dual_contouring")
mesh = tf.Mesh(faces, points)
tf.is_closed(mesh), tf.is_manifold(mesh)      # (True, True)

Boolean CSG of Fields

volume_boolean

merged = tf.volume_boolean(a, b, "union")
carved = tf.volume_boolean(a, b, "difference")
faces, points = tf.isosurface(carved)
OperationCombinatorMeaning
"union"min(a, b)A ∪ B — inside either field
"intersection"max(a, b)A ∩ B — inside both
"difference"max(a, -b)A \ B — inside A, outside B
ParameterDefaultDescription
a, bThe two fields. Their sample dtypes must match
operation"union"One of the three above
dtypeA's coordinate_dtypeThe sample type of the combined field

The combinators are exact on the zero level set, so the extracted isosurface is exactly the boolean of the two solids.

The two fields need not share a grid or a pose. Matching grids and matching poses combine sample-wise and the result keeps that shared pose; anything else resamples both operands multilinearly onto a common world-axis-aligned grid spanning the union of their world domains at the finer of the two local spacings, and the result comes back unposed. Out of an operand's domain is outside its solid — each operand reads a far-outside sentinel past its own box — so a solid that reaches its own domain boundary stops there rather than continuing across the shared grid.

An SDF has a negative inside, so an unsigned field is not one: volume_boolean raises TypeError for uint16 and uint8 operands, naming the accepted dtypes. int16 operands are accepted; the combine is computed in a floating type, so the result carries the requested dtype rather than int16 samples.

Masks

A label map or segmentation mask is a uint8 field whose foreground is a positive constant, which is the opposite of the SDF convention — a corner is inside when sample < iso. Thresholding a 0/255 mask at its midpoint therefore extracts the correct surface but winds it into the foreground. Negate the mask into a signed field to get the mask's own inside and outward windings, and to make it a legal boolean operand:

faces, points = tf.isosurface(tf.Volume(mask), 127.5)     # surface, inward winding

signed = np.asfortranarray(127.5 - mask.astype(np.float32))
faces, points = tf.isosurface(tf.Volume(signed), 0.0)     # outward winding
carved = tf.volume_boolean(tf.Volume(signed), other, "difference")

Resampling

resampled_volume

coarse = tf.resampled_volume(field, (32, 32, 32), (0.5, 0.5, 0.5), (0, 0, 0))
ParameterDefaultDescription
volumeThe source field. float32 or float64; integer-sampled fields raise TypeError
dimsNumber of samples along x, y, z of the target grid
spacingPhysical size of one target grid step
originPosition of target sample (0, 0, 0) — local space for an unposed volume, world space for a posed one
dtypevolume's own dtypeThe sample type of the resampled field

Each target node at origin + index * spacing takes the field's trilinear value. An unposed volume regrids in its own local space, clamped at the edge; a posed one regrids through its pose — the target grid is world space and a node outside the posed domain takes a sentinel above the field's maximum. The result is a new, unposed volume standing on the grid that was asked for. Use it to downsample for preview or to align two fields onto one grid; it is the regrid volume_boolean resamples its operands through.

Slice Contours

volume_slice_contours

paths, points = tf.volume_slice_contours(
    vol,
    plane_origin=(-3.2, -3.2, 0.0),
    u=(1.0, 0.0, 0.0),
    v=(0.0, 1.0, 0.0),
    dims2=(256, 256),
    spacing2=(0.025, 0.025),
    isovalues=[-0.25, 0.0, 0.25],
)
ParameterDefaultDescription
volumeThe scalar volume
plane_originLocal-space position of slice grid node (0, 0)
u, vUnit 3D directions of the slice grid's i and j axes
dims2Number of slice grid nodes along u and v
spacing2Slice grid step along u and v
isovaluesA single isovalue or a sequence of them, all contoured on the same slice
dtypevolume's coordinate_dtypeThe coordinate type of the emitted points

The volume is resampled once onto the plane's 2D grid and every isovalue is contoured on that slice. Node (i, j) samples the volume at plane_origin + i * spacing2[0] * u + j * spacing2[1] * v — the plane is stated in the volume's local space — and each contour point is lifted into world space for a posed volume, and into the volume's local frame otherwise.

The result is connected polylines: paths is an OffsetBlockedArray of point indices and points is (P, 3) in the requested dtype, so a path indexes the point array directly. A closed contour repeats its first index last. The crossings are welded, so the connection is topological rather than a coordinate search.

for path in paths:
    polyline = points[path]        # (len(path), 3)

Contours terminate cleanly at the volume boundary: slice nodes outside the grid receive a sentinel above the field's maximum, so no isovalue the field carries is crossed there.

Reading and Writing Volumes

tf.read_nifti and tf.write_nifti move volumes through NIfTI-1 as .nii or .nii.gz. See I/O for the full contract.

scan = tf.read_nifti("ct.nii.gz")           # native dtype, posed if the file is
faces, points = tf.isosurface(scan, 300.0)  # patient space
tf.write_nifti(scan, "copy.nii.gz")

A posed file lands its affine in transformation, so the isosurface of a scan is a patient-space mesh with no further step. tf.read_nifti_header(path) answers the file's facts — dtype, dims, spacing, units, whether it is posed — without reading the samples.

Naming

The Python names keep their carrier — volume_boolean, volume_slice_contours — where C++ distinguishes the same operations by overload (tf::make_boolean, tf::make_isocontours). Python has no type-distinguished overloading, and its mesh boolean is spelled boolean_union / boolean_intersection / boolean_difference rather than one boolean the volume could join.