04 — Segmentation labels¶
Everything so far was meshes. This is the other half: label maps — 3D images where each voxel holds an integer naming the structure it belongs to.
You will learn:
- how to load and save images with
io.image_io(same contract pattern as meshes) - the core label operations: extract, binarise, swap, connected components
- how these map onto the
imatools-segmentationCLI
Self-contained: builds its own label image with NumPy.
Setup¶
A synthetic label map: two cubes, labelled 1 and 2, in a 20^3 volume.
import tempfile
from pathlib import Path
import numpy as np
import SimpleITK as sitk
from imatools.core import label as clabel
from imatools.io import image_io
work = Path(tempfile.mkdtemp())
a = np.zeros((20, 20, 20), dtype=np.uint8)
a[3:9, 3:9, 3:9] = 1 # structure 1
a[12:18, 12:18, 12:18] = 2 # structure 2
image = sitk.GetImageFromArray(a)
image.SetSpacing((1.0, 1.0, 1.0))
print("size: ", image.GetSize())
print("labels:", np.unique(sitk.GetArrayFromImage(image)))
sitk.GetImageFromArraytakes NumPy in (z, y, x) order and reportsGetSize()in (x, y, z). The axes are reversed between the two APIs — a classic source of silent bugs.
Saving and loading¶
io.image_io mirrors io.mesh_io exactly: save_image / load_image, contracts by
default, return_contract=False for the raw object, and overwrite=False as the default.
path = work / "labels.nii.gz"
image_io.save_image(image, path, overwrite=True)
contract = image_io.load_image(path)
print("type:", type(contract).__name__)
raw = image_io.load_image(path, return_contract=False)
print("raw: ", type(raw).__name__, raw.GetSize())
Extracting one structure¶
extract_single_label keeps one label and zeroes everything else. With binarise=True the
kept label becomes 1; otherwise it retains its original value.
only_1 = clabel.extract_single_label(image, 1, binarise=False)
arr = sitk.GetArrayFromImage(only_1)
print("labels present:", np.unique(arr))
print("voxels kept: ", int((arr > 0).sum()), "(6x6x6 = 216)")
Binarising¶
binarise collapses everything non-background to one foreground value. Note this is the
function relabel_image was unified into — relabel_image still works as a deprecated alias.
mask = clabel.binarise(image, background=0, foreground=1)
print("labels:", np.unique(sitk.GetArrayFromImage(mask)), "- both cubes merged into one mask")
Swapping labels¶
Renumbering a structure — common when aligning conventions between tools.
swapped = clabel.swap_labels(image, 2, 7)
print("before:", np.unique(sitk.GetArrayFromImage(image)))
print("after: ", np.unique(sitk.GetArrayFromImage(swapped)), "- label 2 is now 7")
Connected components¶
bwlabeln (the name is a MATLAB inheritance) finds spatially connected blobs and numbers
them. Our mask has two disconnected cubes, so it finds two.
It returns a tuple, not a bare image.
result = clabel.bwlabeln(mask)
print("returns:", type(result).__name__, "of", len(result))
cc = result[0]
cc_arr = sitk.GetArrayFromImage(cc)
print("components found:", sorted(np.unique(cc_arr).tolist()), "(0 = background)")
From the CLI¶
imatools-segmentation exposes these as subcommands — 22 of them. The common ones:
imatools-segmentation extract-label -in labels.nii -l 1 -out label1.nii
imatools-segmentation mask -in image.nii -mask mask.nii
imatools-segmentation cc-extract -in mask.nii -out components.nii
imatools-segmentation swap -in labels.nii --old 2 --new 7 -out swapped.nii
Run imatools-segmentation --help for the full list. The CLI owns the file I/O and path
handling; the functions above are the pure workflows underneath.
Recap¶
| Task | Call |
|---|---|
| Load / save | image_io.load_image(p) / image_io.save_image(img, p, overwrite=True) |
| Keep one label | clabel.extract_single_label(img, 1) |
| Collapse to a mask | clabel.binarise(img, background=0, foreground=1) |
| Renumber a label | clabel.swap_labels(img, 2, 7) |
| Connected components | clabel.bwlabeln(mask)[0] |
Next: 05_mapping_comparison — relating two meshes to each other.