Working with annotations
This tutorial introduces Points and BoundingBoxes, two data
structures for storing spatial annotations alongside medical images.
Prerequisites
Why annotations?
Medical image analysis often involves more than just voxel intensities. You might need to track:
- Anatomical landmarks (fiducial points placed by a clinician)
- Lesion detections (bounding boxes from a detection model)
- Seed points for region growing algorithms
TorchIO's Points and BoundingBoxes classes keep these annotations
together with the images they refer to, inside a single Subject.
Step 1: Create some points
import torch
import torchio as tio
# Three landmarks in voxel coordinates (IJK)
landmarks = tio.Points(
torch.tensor([
[64.0, 64.0, 32.0],
[100.0, 80.0, 50.0],
[30.0, 120.0, 45.0],
]),
)
print(landmarks) # Points(num_points=3, axes='IJK')
print(landmarks.num_points) # 3
print(landmarks.axes) # 'IJK'
Points are stored as an \((N, 3)\) tensor:
Step 2: Convert between axis conventions
Points default to IJK (voxel indices). Convert to any other
convention:
# With identity affine, IJK == RAS numerically
ras = landmarks.to_axes("RAS")
print(ras.axes) # 'RAS'
# Or get world coordinates directly
world = landmarks.to_world()
When the points come from a real image, pass its affine:
image = tio.ScalarImage("t1.nii.gz")
landmarks = tio.Points(
torch.tensor([[64.0, 64.0, 32.0]]),
affine=image.affine,
)
ras = landmarks.to_axes("RAS") # now in mm
Supported axis conventions:
| Type | Examples |
|---|---|
| Voxel | IJK, KJI, JIK, ... (any permutation) |
| Anatomical | RAS, LPI, AIR, ... (one from each pair {R,L}, {A,P}, {S,I}) |
Step 3: Create bounding boxes
Bounding boxes are 6-element vectors. The format is defined by axes and representation:
| Predefined | Meaning |
|---|---|
IJKIJK |
Corners: \((i_1, j_1, k_1, i_2, j_2, k_2)\) |
IJKWHD |
Center + size: \((i_c, j_c, k_c, s_i, s_j, s_k)\) |
boxes = tio.BoundingBoxes(
torch.tensor([
[10, 20, 30, 50, 60, 70],
[80, 90, 40, 120, 130, 80],
]),
format=tio.BoundingBoxFormat.IJKIJK,
)
print(boxes) # BoundingBoxes(num_boxes=2, ...)
print(boxes.num_boxes) # 2
Convert between formats:
whd = boxes.to_format(tio.BoundingBoxFormat.IJKWHD)
print(whd.data[0]) # center=(30, 40, 50), size=(40, 40, 40)
Custom formats work too:
from torchio import BoundingBoxFormat
ras_corners = BoundingBoxFormat("RAS", "corners")
ras_boxes = boxes.to_format(ras_corners)
Step 4: Attach labels
Each box can carry a class label:
boxes = tio.BoundingBoxes(
torch.tensor([[10, 20, 30, 50, 60, 70]]),
format=tio.BoundingBoxFormat.IJKIJK,
labels=torch.tensor([1]),
)
print(boxes.labels) # tensor([1])
Step 5: Build a Subject with everything
A Subject automatically sorts its contents:
subject = tio.Subject(
t1=tio.ScalarImage(torch.randn(1, 64, 64, 64)),
seg=tio.LabelMap((torch.randn(1, 64, 64, 64) > 0).float()),
landmarks=landmarks,
tumors=boxes,
age=45,
)
Access each type:
subject.t1 # Image
subject.landmarks # Points
subject.tumors # BoundingBoxes
subject.age # metadata (45)
Inspect what the Subject contains:
List all entries of a given type:
subject.images() # {'t1': ..., 'seg': ...}
subject.points() # {'landmarks': ...}
subject.bounding_boxes() # {'tumors': ...}
Summary
classDiagram
class Subject {
images
points
bounding_boxes
metadata
}
class Image {
data: (C, I, J, K)
affine
}
class Points {
data: (N, 3)
axes: str
to_axes(target)
}
class BoundingBoxes {
data: (N, 6)
format: BoundingBoxFormat
labels
to_format(target)
}
Subject --> Image
Subject --> Points
Subject --> BoundingBoxes
You now know how to:
- Store landmark coordinates with
Points - Define regions of interest with
BoundingBoxes - Convert between axis conventions (
IJK,RAS,LPI, etc.) - Attach labels to bounding boxes
- Group everything in a
Subject