Skip to content

How to turn a segmentation mask into a mesh in Python

A label map's boundary at value k is an exact surface in the volume's world coordinates. Marching cubes on a label map at isovalue 0.5 between k and not-k gives it; the staircase is removed by Taubin smoothing, which keeps the volume; decimation then halves the triangle count without visible change.

import pardesline as pl

case = pl.load("msd_task04_hippocampus").filter(split="Tr")[0]
mask = case.mask  # LabelMap, labels {1: 'anterior', 2: 'posterior'}
anterior = mask.to_mesh(label=1)  # exact boundary, LPS millimetres
anterior = anterior.smooth().decimate()  # Taubin 20 iterations, then 50 % fewer triangles
anterior.save("hippocampus_anterior.stl")  # + .pardesline.json: frame, licence, the three steps

Your own mask: pl.load("seg.nii.gz").as_labels({1: "liver", 2: "tumor"}).to_mesh(label=2). A grey-level volume uses volume.to_mesh(isovalue=300) (bone on CT in Hounsfield units).

What the sidecar says

{"step": "to_mesh", "label": 1, "label_name": "anterior", "isovalue": null, ...}
{"module": "pyvista_mesh_processing", "method": "pv_smooth_taubin", "params": {}, ...}
{"module": "pyvista_mesh_processing", "method": "pv_decimate", "params": {}, ...}

Empty params means every catalog default was used; pl.describe("pyvista_mesh_processing.pv_smooth_taubin") says what they are and why.

Units and frames

The mesh is in the volume's physical frame (LPS millimetres for NIfTI and DICOM through SimpleITK). To compare with a scanner mesh in RAS, flip x and y; obj.frame tells you which one you have. Volumes in millimetres from a mesh: anterior.to_pyvista().volume.

Several labels, several meshes

for label, name in mask.labels.items():
    mask.to_mesh(label=label).smooth().save(f"hippocampus_{name}.stl")

TotalSegmentator ships one mask role per structure: s.liver.to_mesh() works without a label argument because the role has a single named label.

When it looks wrong

Symptom Cause Do
staircase surface no smoothing .smooth(); more iterations (n_iterations=40) for coarse slices
small structures shrink Laplacian smoothing use .smooth() (Taubin); avoid pv_smooth_laplacian for thin anatomy
holes in the surface the label touches the volume border pad the volume, or accept an open surface
wrong size voxel indices instead of world coordinates to_mesh always uses world coordinates; check mask.info()["spacing"]

Related: Hippocampus example · Mesh processing · Guide: medical volumes