Migrating from v1 to v2
This guide covers every breaking change between TorchIO v1 and v2.
Report bugs or request features
Hit a snag migrating, or have an idea to improve TorchIO? Please start a discussion or open an issue on GitHub to report bugs or request features.
Quick checklist
- Replace
Random*transform names with their base names (RandomFlip→Flip) - Pass explicit ranges for augmentation (renamed transforms are a no-op without arguments):
RandomAffine()→Affine(degrees=(-10, 10), scales=(0.9, 1.1)) - Replace
path=with positional arg orsource=in Image constructors - Replace
.affine(numpy array) with.affine.datawhere a raw array is needed - Replace
RescaleIntensity(out_min_max=...)withNormalize(out_min=..., out_max=...) - Replace
SubjectsDatasetwith anyDatasetpassed toSubjectsLoader - Replace
GridAggregatorwithPatchAggregator - Rewrite custom transforms to accept a
SubjectsBatchinmake_params(batch)andapply_transform(batch, params)
Image construction
v1:
image = tio.ScalarImage(path="t1.nii.gz")
image = tio.ScalarImage(tensor=tensor, affine=affine_array)
v2:
image = tio.ScalarImage("t1.nii.gz")
image = tio.ScalarImage(tensor, affine=tio.AffineMatrix(affine_array))
Changes:
- First positional argument accepts a path, tensor, numpy array,
NiBabel image, SimpleITK image, or
bytes. Thepathandtensorkeyword names are gone: use positional orsource=. typeparameter removed. UseScalarImageorLabelMapdirectly.affineacceptsAffineMatrixobjects in addition to arrays.- New
channels_lastparameter for tensor sources shaped(I, J, K, C).
Affine access
v1:
affine_array = image.affine # np.ndarray (4, 4)
spacing = image.spacing # tuple
direction = image.direction # 9-tuple of floats
v2:
affine_obj = image.affine # AffineMatrix object
affine_array = image.affine.data # np.ndarray (4, 4)
spacing = image.spacing # tuple (unchanged)
orientation = image.affine.orientation # e.g. ("R", "A", "S")
The .affine property now returns an AffineMatrix object. Use
.affine.data when you need the raw 4×4 numpy array.
Subject construction
v1:
subject = tio.Subject({"t1": image, "seg": label}) # dict positional arg
subject = tio.Subject(t1=image, seg=label)
v2:
The positional dictionary form is removed. Use keyword arguments.
Transform naming
v2 removes the Random* prefix. Stochasticity is controlled by
parameter type: a scalar is deterministic, a tuple samples uniformly,
and a Distribution or Choice gives full control.
Renaming a Random* transform changes its default behavior
In v1, RandomAffine() (no arguments) applied random augmentation.
In v2, the renamed Affine() (no arguments) is a deterministic
identity (no-op) that emits a warning (randomness is opt-in). Pass a
range like (a, b) for random augmentation, or a scalar for a fixed
effect. Transforms that draw a random realisation rather than
sampling a scalar parameter (e.g. Noise, BiasField,
ElasticDeformation, Swap) still apply with their default
parameters.
| v1 | v2 |
|---|---|
RandomFlip |
Flip |
RandomAffine |
Affine |
RandomElasticDeformation |
ElasticDeformation |
RandomNoise |
Noise |
RandomBlur |
Blur |
RandomMotion |
Motion |
RandomGhosting |
Ghosting |
RandomBiasField |
BiasField |
RandomGamma |
Gamma |
RandomSpike |
Spike |
RandomSwap |
Swap |
RandomAnisotropy |
Anisotropy |
RandomLabelsToImage |
LabelsToImage |
RescaleIntensity |
Normalize (alias RescaleIntensity available) |
ZNormalization |
Standardize (alias ZNormalization available) |
Transform parameter changes
Flip
flip_probability default changed from 0.5 to 1.0. If you
relied on the old default, set it explicitly:
# v1 (implicit 0.5)
tio.RandomFlip(axes=(0, 1, 2))
# v2 (explicit 0.5)
tio.Flip(axes=(0, 1, 2), flip_probability=0.5)
Affine
scales and degrees now expect explicit ranges instead of
half-widths:
# v1: scales=0.1 means range (0.9, 1.1)
tio.RandomAffine(scales=0.1, degrees=10)
# v2: specify the range directly
tio.Affine(scales=(0.9, 1.1), degrees=(-10, 10))
Normalize (was RescaleIntensity)
Tuple parameters are split into individual keyword arguments:
# v1
tio.RescaleIntensity(
out_min_max=(0, 1),
percentiles=(0.5, 99.5),
)
# v2
tio.Normalize(
out_min=0,
out_max=1,
percentile_low=0.5,
percentile_high=99.5,
)
Each parameter can independently be a scalar (fixed), a tuple
(uniform range), a Distribution, or a Choice.
HistogramStandardization
Landmark computation is now a standalone function instead of a classmethod:
# v1
landmarks = tio.HistogramStandardization.train(paths)
transform = tio.HistogramStandardization({"t1": landmarks})
# v2
from torchio.transforms.intensity.histogram_standardization import (
compute_histogram_landmarks,
)
landmarks = compute_histogram_landmarks(images)
transform = tio.HistogramStandardization(landmarks, include=["t1"])
One instance per modality. For multi-modal subjects, compose:
tio.Compose([
tio.HistogramStandardization(t1_landmarks, include=["t1"]),
tio.HistogramStandardization(t2_landmarks, include=["t2"]),
])
Custom transforms
Rewrite the transform hooks
In v1, custom transforms implemented a subject-level hook:
In v2, parameter creation and application are separate batch-level hooks:
# v2
def make_params(self, batch: tio.SubjectsBatch) -> dict[str, Any]:
...
def apply_transform(
self,
batch: tio.SubjectsBatch,
params: dict[str, Any],
) -> tio.SubjectsBatch:
...
Call the transform normally rather than calling either hook yourself. For a single subject, TorchIO performs this conversion automatically:
The following complete transform works for both a single Subject and
a SubjectsBatch:
from typing import Any
import torch
import torchio as tio
class AddValue(tio.Transform):
"""Add a fixed value to every batched image."""
def __init__(self, value: float) -> None:
super().__init__()
self.value = value
self.received_shape: tuple[int, ...] | None = None
def make_params(self, batch: tio.SubjectsBatch) -> dict[str, Any]:
"""Return the value to add."""
return {"value": self.value}
def apply_transform(
self,
batch: tio.SubjectsBatch,
params: dict[str, Any],
) -> tio.SubjectsBatch:
"""Add the value to every image tensor."""
for image_batch in batch.images.values():
self.received_shape = tuple(image_batch.data.shape)
image_batch.data = image_batch.data + params["value"]
return batch
subject = tio.Subject(image=tio.ScalarImage(torch.zeros(1, 2, 3, 4)))
transform = AddValue(2)
result = transform(subject)
assert isinstance(result, tio.Subject)
assert transform.received_shape == (1, 1, 2, 3, 4)
assert result.image.data.shape == (1, 2, 3, 4)
assert torch.all(result.image.data == 2)
The image is 4D (C, I, J, K) before and after the public call, but it
is 5D (B, C, I, J, K) inside apply_transform. For a single subject,
B is 1.
apply_transform is not a public replay method
It is the low-level batch kernel. Calling it directly bypasses wrapping, copying, probability handling, history recording, and output-type restoration. Pass a supported input to the transform itself instead.
Migrate metadata access
In v1, subject metadata values were scalars or arbitrary objects. In a v2 batch, each metadata key maps to a list containing one value per element:
import torch
import torchio as tio
subjects = [
tio.Subject(
image=tio.ScalarImage(torch.zeros(1, 2, 3, 4)),
site="A",
age=30,
),
tio.Subject(
image=tio.ScalarImage(torch.ones(1, 2, 3, 4)),
site="B",
age=40,
),
]
batch = tio.SubjectsBatch.from_subjects(subjects)
assert batch.metadata == {"site": ["A", "B"], "age": [30, 40]}
Treat batch.metadata as dict[str, list[Any]]. Metadata transforms
must keep each list aligned with the batch dimension. Subjects in one
batch must have equivalent image names and metadata keys. The first
subject determines the shared key order; later subjects may use a
different local order, but custom transforms should preserve the batch
schema rather than adding, removing, or renaming keys for only some
elements.
Choose deterministic or per-instance behavior
A fixed scalar is not sampled: transforms such as Gamma use that
value for every batch element. For built-in stochastic transforms that
support per-instance sampling, ranges and distributions produce
independent parameters for each batch element by default. Set
per_instance=False to share one sampled parameter set:
import torchio as tio
independent = tio.Gamma(log_gamma=(-0.3, 0.3))
shared = tio.Gamma(log_gamma=(-0.3, 0.3), per_instance=False)
deterministic = tio.Gamma(log_gamma=0.2)
Custom transforms do not gain per-instance sampling automatically. Unless a transform explicitly implements and advertises that capability, its parameters remain batch-shared. See Per-instance augmentation for the capability contract and stochastic-realisation caveats.
Migrate inherently per-subject logic
Prefer vectorized operations on 5D tensors or metadata lists. If logic must call a subject-oriented external API, the current low-level escape hatch is to unbatch, process every subject without changing its schema, restack, and adopt the prior history:
from typing import Any
import torch
import torchio as tio
class StripIdentifier(tio.Transform):
"""Strip whitespace from subject identifiers."""
def make_params(self, batch: tio.SubjectsBatch) -> dict[str, Any]:
"""Return no parameters."""
return {}
def apply_transform(
self,
batch: tio.SubjectsBatch,
params: dict[str, Any],
) -> tio.SubjectsBatch:
"""Process metadata one subject at a time."""
subjects = batch.unbatch()
for subject in subjects:
identifier = subject.metadata["identifier"]
subject.metadata["identifier"] = identifier.strip()
rebuilt = tio.SubjectsBatch.from_subjects(subjects)
rebuilt.adopt_history(batch, subjects)
return rebuilt
subject = tio.Subject(
image=tio.ScalarImage(torch.zeros(1, 2, 3, 4)),
identifier=" sub-01 ",
)
result = StripIdentifier()(subject)
assert result.identifier == "sub-01"
This pattern is more expensive than vectorized code and requires every resulting subject to retain a compatible image and metadata schema. A supported mapping utility is planned, but it is not part of the current API.
New features
Choice
Sample from a discrete set of values:
SomeOf
Apply a random subset of transforms:
tio.SomeOf(
[tio.Flip(axes=(0,)), tio.Noise(std=0.1), tio.Gamma(log_gamma=(-0.3, 0.3))],
num_transforms=(1, 2),
)
Operator sugar
pipeline = tio.Flip(axes=(0,)) + tio.Noise(std=0.1) # Compose
artifact = tio.Ghosting(intensity=(0.5, 1)) | tio.Spike(intensity=(1, 3)) # OneOf
Compose copy control
Compose deep-copies the input once, then all inner transforms
operate in-place. Disable with copy=False for nested pipelines:
inner = tio.Compose([tio.Flip(axes=(0,))], copy=False)
outer = tio.Compose([inner, tio.Noise(std=0.1)])
Data loading
SubjectsDataset removed
v1 required wrapping subjects in SubjectsDataset.
v2 removes this class. Pass any Dataset returning Subject instances to
SubjectsLoader:
# v1
dataset = tio.SubjectsDataset(subjects, transform=augment)
loader = DataLoader(dataset, batch_size=4)
# v2
loader = tio.SubjectsLoader(subjects, transform=augment, batch_size=4)
Queue
# v1
dataset = tio.SubjectsDataset(subjects)
sampler = tio.UniformSampler(patch_size=96)
queue = tio.Queue(dataset, max_length=300, samples_per_volume=10)
# v2
queue = tio.Queue(
subjects,
patch_sampler=tio.UniformSampler(patch_size=96),
max_length=300,
patches_per_volume=10,
)
PatchAggregator (was GridAggregator)
Transform history
v2 simplifies the history API:
# Both versions
restored = subject.apply_inverse_transform()
inverse = subject.get_inverse_transform()
# v1 only (removed in v2)
subject.history
subject.get_applied_transforms()
subject.get_composed_history()
Imports
All transforms are available at the top level:
# v1
from torchio.transforms import RandomFlip, RandomAffine
from torchio.transforms.augmentation.intensity import RandomNoise
# v2
import torchio as tio
tio.Flip
tio.Affine
tio.Noise
New exports in v2:
AffineMatrix: the affine matrix classPoints,BoundingBoxes,BoundingBoxFormat: annotation typesSubjectsBatch,ImagesBatch: batch containersChoice: discrete parameter sampling utilitySomeOf: random subset compositionPatchAggregator: renamed fromGridAggregatorapply_inverse_transform: standalone inverse function