# Test .tio_transform property
# CustomDictTransform-based wrappers
test_eq(type(RandomAffine(degrees=10).tio_transform), tio.RandomAffine)
test_eq(type(RandomFlip(p=0.5).tio_transform), tio.RandomFlip)
test_eq(type(RandomElasticDeformation(p=0.5).tio_transform), tio.RandomElasticDeformation)
# DisplayedTransform-based wrappers
test_eq(type(PadOrCrop([64, 64, 64]).tio_transform), tio.CropOrPad)
test_eq(type(SpatialPad([64, 64, 64]).tio_transform).__name__, '_TioSpatialPad')
test_eq(type(ZNormalization().tio_transform), tio.ZNormalization)
test_eq(type(RescaleIntensity((-1, 1), (-1000, 1000)).tio_transform), tio.RescaleIntensity)
test_eq(type(RandomGamma(p=0.5).tio_transform), tio.RandomGamma)
test_eq(type(RandomNoise(p=0.5).tio_transform), tio.RandomNoise)
test_eq(type(RandomBiasField(p=0.5).tio_transform), tio.RandomBiasField)
test_eq(type(RandomBlur(p=0.5).tio_transform), tio.RandomBlur)
test_eq(type(RandomGhosting(p=0.5).tio_transform), tio.RandomGhosting)
test_eq(type(RandomSpike(p=0.5).tio_transform), tio.RandomSpike)
test_eq(type(RandomMotion(p=0.5).tio_transform), tio.RandomMotion)Data augmentation
Transforms wrapper
CustomDictTransform
CustomDictTransform (aug)
A class that serves as a wrapper to perform an identical transformation on both the image and the target (if it’s a mask).
Vanilla transforms
do_pad_or_crop
do_pad_or_crop (o, target_shape, padding_mode, mask_name, dtype=<class 'torch.Tensor'>)
PadOrCrop
PadOrCrop (size, padding_mode=0, mask_name=None)
Resize image using TorchIO CropOrPad.
SpatialPad
SpatialPad (spatial_size, mode='constant')
Pad image to minimum size without cropping using MONAI’s SpatialPad.
This transform pads each dimension to AT LEAST the specified size. Dimensions already larger than spatial_size are left unchanged (no cropping), making it ideal for patch-based training where images must be at least as large as the patch size.
Uses zero padding (constant value 0) by default.
Args: spatial_size: Minimum size [x, y, z] for each dimension. Can be int (same for all dims) or list. Usually set to patch_size for patch-based training. mode: Padding mode. Default ‘constant’ means zero padding. Other options: ‘edge’, ‘reflect’, ‘symmetric’.
Example: >>> # Image 512×512×48, need at least 256×256×96 for patches >>> transform = SpatialPad(spatial_size=[256, 256, 96]) >>> # Result: 512×512×96 (x,y unchanged, z padded from 48→96)
Note: This differs from PadOrCrop which will crop dimensions larger than the target size. Use SpatialPad when you want to preserve large dimensions and only pad small ones.
ZNormalization
ZNormalization (masking_method=None, channel_wise=True)
Apply TorchIO ZNormalization.
RescaleIntensity
RescaleIntensity (out_min_max:tuple[float,float], in_min_max:tuple[float,float])
Apply TorchIO RescaleIntensity for robust intensity scaling.
Args: out_min_max (tuple[float, float]): Output intensity range (min, max) in_min_max (tuple[float, float]): Input intensity range (min, max)
Example for CT images: # Normalize CT from air (-1000 HU) to bone (1000 HU) into range (-1, 1) transform = RescaleIntensity(out_min_max=(-1, 1), in_min_max=(-1000, 1000))
NormalizeIntensity
NormalizeIntensity (nonzero:bool=True, channel_wise:bool=True, subtrahend:float=None, divisor:float=None)
Apply MONAI NormalizeIntensity.
Args: nonzero (bool): Only normalize non-zero values (default: True) channel_wise (bool): Apply normalization per channel (default: True) subtrahend (float, optional): Value to subtract
divisor (float, optional): Value to divide by
BraTSMaskConverter
BraTSMaskConverter (enc=None, dec=None, split_idx=None, order=None)
Convert BraTS masks.
BinaryConverter
BinaryConverter (enc=None, dec=None, split_idx=None, order=None)
Convert to binary mask.
RandomGhosting
RandomGhosting (intensity=(0.5, 1), p=0.5)
Apply TorchIO RandomGhosting.
RandomSpike
RandomSpike (num_spikes=1, intensity=(1, 3), p=0.5)
Apply TorchIO RandomSpike.
RandomNoise
RandomNoise (mean=0, std=(0, 0.25), p=0.5)
Apply TorchIO RandomNoise.
RandomBiasField
RandomBiasField (coefficients=0.5, order=3, p=0.5)
Apply TorchIO RandomBiasField.
RandomBlur
RandomBlur (std=(0, 2), p=0.5)
Apply TorchIO RandomBlur.
RandomGamma
RandomGamma (log_gamma=(-0.3, 0.3), p=0.5)
Apply TorchIO RandomGamma.
RandomIntensityScale
RandomIntensityScale (scale_range:tuple[float,float]=(0.5, 2.0), p:float=0.5)
Randomly scale image intensities by a multiplicative factor.
Useful for domain generalization across different acquisition protocols with varying intensity ranges.
Args: scale_range (tuple[float, float]): Range of scale factors (min, max). Values > 1 increase intensity, < 1 decrease intensity. p (float): Probability of applying the transform (default: 0.5)
Example: # Scale intensities randomly between 0.5x and 2.0x transform = RandomIntensityScale(scale_range=(0.5, 2.0), p=0.3)
RandomMotion
RandomMotion (degrees=10, translation=10, num_transforms=2, image_interpolation='linear', p=0.5)
Apply TorchIO RandomMotion.
RandomCutout
RandomCutout (holes=1, max_holes=3, spatial_size=8, max_spatial_size=16, fill='min', mask_only=True, p=0.2)
Randomly erase spherical regions in 3D medical images with mask-aware placement.
Simulates post-operative surgical cavities by filling random ellipsoid volumes with specified values. When mask_only=True (default), cutouts only affect voxels inside the segmentation mask, ensuring no healthy tissue is modified.
Args: holes: Minimum number of cutout regions. Default: 1. max_holes: Maximum number of regions. Default: 3. spatial_size: Minimum cutout diameter in voxels. Default: 8. max_spatial_size: Maximum cutout diameter. Default: 16. fill: Fill value - ‘min’, ‘mean’, ‘random’, or float. Default: ‘min’. mask_only: If True, cutouts only affect mask-positive voxels (tumor tissue). If False, cutouts can affect any voxel (original behavior). Default: True. p: Probability of applying transform. Default: 0.2.
Example: >>> # Simulate post-op cavities only within tumor regions >>> tfm = RandomCutout(holes=1, max_holes=2, spatial_size=10, … max_spatial_size=25, fill=‘min’, mask_only=True, p=0.2)
>>> # Original behavior - cutouts anywhere in the volume
>>> tfm = RandomCutout(mask_only=False, p=0.2)
Dictionary transforms
RandomElasticDeformation
RandomElasticDeformation (num_control_points=7, max_displacement=7.5, image_interpolation='linear', p=0.5)
Apply TorchIO RandomElasticDeformation.
RandomAffine
RandomAffine (scales=0, degrees=10, translation=0, isotropic=False, image_interpolation='linear', default_pad_value=0.0, p=0.5)
Apply TorchIO RandomAffine.
RandomFlip
RandomFlip (axes='LR', p=0.5)
Apply TorchIO RandomFlip.
OneOf
OneOf (transform_dict, p=1)
Apply only one of the given transforms using TorchIO OneOf.
# Test SpatialPad (pad-only, no cropping)
# Test 1: Pad small dimensions only (your exact scenario: 512×512×48 → 512×512×96)
test_img = MedImage(torch.randn(1, 512, 512, 48))
test_mask = MedMask(torch.zeros(1, 512, 512, 48))
test_mask[0, 200:300, 200:300, 20:30] = 1.0
transform = SpatialPad(spatial_size=[256, 256, 96])
padded_img = transform(test_img)
padded_mask = transform(test_mask)
# Verify: large dims preserved (512, 512), small dim padded (48 → 96)
test_eq(padded_img.shape, torch.Size([1, 512, 512, 96]))
test_eq(padded_mask.shape, torch.Size([1, 512, 512, 96]))
test_eq(type(padded_img), MedImage)
test_eq(type(padded_mask), MedMask)
# Test 2: All dimensions larger than min_size - no padding
test_img2 = MedImage(torch.randn(1, 300, 300, 150))
transform2 = SpatialPad(spatial_size=[256, 256, 96])
padded_img2 = transform2(test_img2)
test_eq(padded_img2.shape, torch.Size([1, 300, 300, 150])) # Unchanged
# Test 3: All dimensions smaller - pad all
test_img3 = MedImage(torch.randn(1, 32, 32, 32))
transform3 = SpatialPad(spatial_size=[64, 64, 64])
padded_img3 = transform3(test_img3)
test_eq(padded_img3.shape, torch.Size([1, 64, 64, 64]))
# Test 4: Verify zero padding
test_img4 = MedImage(torch.ones(1, 10, 10, 10) * 5.0) # All values = 5.0
transform4 = SpatialPad(spatial_size=[15, 15, 15])
padded_img4 = transform4(test_img4)
# Check original region still has value 5.0
test_eq(padded_img4[0, 2:12, 2:12, 2:12].mean().item(), 5.0)
# Check padded corners have value 0.0 (zero padding)
test_eq(padded_img4[0, 0, 0, 0].item(), 0.0)
# Test 5: TorchIO compatibility for patch-based workflows
tio_transform = transform.tio_transform
test_eq(isinstance(tio_transform, tio.SpatialTransform), True)
print("✓ All SpatialPad tests passed!")# Test .tio_transform property
# CustomDictTransform-based wrappers
test_eq(type(RandomAffine(degrees=10).tio_transform), tio.RandomAffine)
test_eq(type(RandomFlip(p=0.5).tio_transform), tio.RandomFlip)
test_eq(type(RandomElasticDeformation(p=0.5).tio_transform), tio.RandomElasticDeformation)
# DisplayedTransform-based wrappers
test_eq(type(PadOrCrop([64, 64, 64]).tio_transform), tio.CropOrPad)
test_eq(type(ZNormalization().tio_transform), tio.ZNormalization)
test_eq(type(RescaleIntensity((-1, 1), (-1000, 1000)).tio_transform), tio.RescaleIntensity)
test_eq(type(RandomGamma(p=0.5).tio_transform), tio.RandomGamma)
test_eq(type(RandomNoise(p=0.5).tio_transform), tio.RandomNoise)
test_eq(type(RandomBiasField(p=0.5).tio_transform), tio.RandomBiasField)
test_eq(type(RandomBlur(p=0.5).tio_transform), tio.RandomBlur)
test_eq(type(RandomGhosting(p=0.5).tio_transform), tio.RandomGhosting)
test_eq(type(RandomSpike(p=0.5).tio_transform), tio.RandomSpike)
test_eq(type(RandomMotion(p=0.5).tio_transform), tio.RandomMotion)# Test RandomCutout (ItemTransform - expects tuple input)
import numpy as np
# Create test data
test_img = MedImage(torch.randn(1, 32, 32, 32))
test_mask = MedMask(torch.zeros(1, 32, 32, 32))
test_mask[0, 10:20, 10:20, 10:20] = 1.0 # Tumor region
# Test mask_only=True (default): only tumor voxels affected
cutout = RandomCutout(holes=1, spatial_size=8, fill='min', mask_only=True, p=1.0)
result_img, result_mask = cutout.encodes((test_img, test_mask))
test_eq(type(result_img), MedImage)
test_eq(type(result_mask), MedMask)
test_eq(result_img.shape, test_img.shape)
# Verify: healthy tissue (mask==0) unchanged (use numpy for comparison)
healthy_region = test_mask.numpy()[0] == 0
test_eq(np.array_equal(result_img.numpy()[0, healthy_region], test_img.numpy()[0, healthy_region]), True)
# Verify: mask unchanged
test_eq(torch.equal(result_mask, test_mask), True)
# Test empty mask skips cutout (mask_only=True)
empty_mask = MedMask(torch.zeros(1, 32, 32, 32))
result_img, _ = cutout.encodes((test_img, empty_mask))
test_eq(torch.equal(result_img, test_img), True) # Unchanged
# Test mask_only=False: cutouts can affect any voxel
cutout_any = RandomCutout(mask_only=False, p=1.0)
result_img, _ = cutout_any.encodes((test_img, test_mask))
test_eq(result_img.shape, test_img.shape)
# Test with TensorCategory target (classification task with mask_only=False)
test_label = TensorCategory(1)
cutout_cls = RandomCutout(mask_only=False, p=1.0)
result_img, result_label = cutout_cls.encodes((test_img, test_label))
test_eq(type(result_img), MedImage)
test_eq(result_label, test_label)
# Test TensorCategory with mask_only=True skips cutout (no mask available)
cutout_mask_only = RandomCutout(mask_only=True, p=1.0)
result_img, result_label = cutout_mask_only.encodes((test_img, test_label))
test_eq(torch.equal(result_img, test_img), True) # Unchanged - no mask to intersect
# tio_transform property
test_eq(isinstance(cutout.tio_transform, tio.IntensityTransform), True)
# Test fill modes with mask_only=False
for fill_mode in ['min', 'mean', 'random', 0.0]:
cutout = RandomCutout(fill=fill_mode, mask_only=False, p=1.0)
result_img, _ = cutout.encodes((test_img, test_mask))
test_eq(result_img.shape, test_img.shape)