Generic selectors
Exact matches only
Search in title
Search in content
Post Type Selectors

Preprocessing 3D volumes for tumor segmentation in MONAI

Preprocessing 3D medical volumes for tumor segmentation using MONAI and PyTorch: essential transforms and data preparation.

This is MONAI 3D preprocessing for tumor / organ segmentation: load paired NIfTI, resample spacing, window HU, crop empty air, resize, then a Dataset + DataLoader. It is not random flip / rotate / noise — that is 3D volume augmentation in MONAI. It is not how a clinician calculates tumor volume.

The volumes here are CT NIfTI (a stack of slices = one patient). The same compose works on MRI if you change the intensity window. If you still have a DICOM series, convert it first — DICOM to NIfTI. DICOM vs NIfTI: that write-up.

Video of the same 2021 notebook: preprocessing 3D volumes in MONAI.

Install

pip install monai torch torchvision nibabel

Use a venv. AddChanneld below is the 2021 dictionary API; current MONAI uses EnsureChannelFirstd for the same channel dim. Docs: docs.monai.io.

2021 notebook imports: MONAI dictionary transforms, Dataset, DataLoader, first
import os
from glob import glob

import torch
from monai.transforms import (
    Compose,
    LoadImaged,
    ToTensord,
    AddChanneld,
    Spacingd,
    ScaleIntensityRanged,
    CropForegroundd,
    Resized,
)
from monai.data import Dataset, DataLoader
from monai.utils import first
import matplotlib.pyplot as plt

Path dictionary

Two ways to load: images and masks as two lists, or one Python list of dicts with image and label keys. The dict is the one you want. Every transform takes a keys= argument, so intensity hits the CT only and spacing hits both.

Four folders: TrainData, TrainLabels, ValData, ValLabels. Pair by sorted filename.

Build train_files / val_files dictionaries from TrainData and TrainLabels NIfTI
data_dir = 'D:/3_Stage/ALL_THE_DATA/fixed_data_03_august/all_together'

train_images = sorted(glob(os.path.join(data_dir, 'TrainData', '*.nii.gz')))
train_labels = sorted(glob(os.path.join(data_dir, 'TrainLabels', '*.nii.gz')))

val_images = sorted(glob(os.path.join(data_dir, 'ValData', '*.nii.gz')))
val_labels = sorted(glob(os.path.join(data_dir, 'ValLabels', '*.nii.gz')))

train_files = [{"image": image_name, 'label': label_name} for image_name, label_name in zip(train_images, train_labels)]
val_files = [{"image": image_name, 'label': label_name} for image_name, label_name in zip(val_images, val_labels)]

train_files[0] is one patient: path to the volume and path to the mask.

The transforms

Compose stacks dictionary transforms. Required: LoadImaged (read the NIfTI) and ToTensord (torch, so training can start). The rest is what actually makes CT volumes trainable.

Transform Keys What it does here
LoadImaged image, label Read .nii / .nii.gz.
AddChanneld image, label Insert the channel dim the network expects (2021 name).
Spacingd image, label Resample to pixdim=(1.5, 1.5, 2) mm. Image bilinear, label nearest (or default if you omit mode).
ScaleIntensityRanged image only HU window -200..2000..1, clip=True. Do not scale the mask.
CropForegroundd image, label Drop empty air. source_key='image'.
Resized image, label spatial_size=[128, 128, 128]. Needed after crop, or every patient has a different shape.
ToTensord image, label Torch tensors.
2021 MONAI Compose: LoadImaged, AddChanneld, Spacingd, ScaleIntensityRanged, CropForegroundd, Resized, ToTensord

The trailing d means dictionary. Drop it if you are not using a dict. keys picks image, label, or both. Intensity is image-only so the 0/1 mask stays 0/1.

Three composes from the notebook: orig_ (load only, for the before plot), train_ (full stack), val_ (spacing + window, no crop / resize — that is how the 2021 file was written).

orig_transforms = Compose(
    [
        LoadImaged(keys=['image', 'label']),
        AddChanneld(keys=['image', 'label']),
        ToTensord(keys=['image', 'label'])
    ]
)

train_transforms = Compose(
    [
        LoadImaged(keys=['image', 'label']),
        AddChanneld(keys=['image', 'label']),
        Spacingd(keys=['image', 'label'], pixdim=(1.5, 1.5, 2)),
        ScaleIntensityRanged(keys='image', a_min=-200, a_max=200, b_min=0.0, b_max=1.0, clip=True),
        CropForegroundd(keys=['image', 'label'], source_key='image'),
        Resized(keys=['image', 'label'], spatial_size=[128,128,128]),
        ToTensord(keys=['image', 'label'])
    ]
)

val_transforms = Compose(
    [
        LoadImaged(keys=['image', 'label']),
        AddChanneld(keys=['image', 'label']),
        Spacingd(keys=['image', 'label'], pixdim=(1.5, 1.5, 2)),
        ScaleIntensityRanged(keys='image', a_min=-200, a_max=200, b_min=0.0, b_max=1.0, clip=True),
        ToTensord(keys=['image', 'label'])
    ]
)

DataLoader

Dataset binds the file list to a compose. DataLoader batches it (here batch_size=1). Two of each if you have train and val.

MONAI Dataset and DataLoader for original, train, and val transforms
orig_ds = Dataset(data=train_files, transform=orig_transforms)
orig_loader = DataLoader(orig_ds, batch_size=1)

train_ds = Dataset(data=train_files, transform=train_transforms)
train_loader = DataLoader(train_ds, batch_size=1)

val_ds = Dataset(data=val_files, transform=val_transforms)
val_loader = DataLoader(val_ds, batch_size=1)

Plot one patient

first(loader) is the first batch. Slice index 30 in the notebook.

Matplotlib: original slice, preprocessed slice, and label at index 30
test_patient = first(train_loader)
orig_patient = first(orig_loader)

plt.figure('test', (12, 6))

plt.subplot(1, 3, 1)
plt.title('Orig patient')
plt.imshow(orig_patient['image'][0, 0, :, :, 30], cmap="gray")

plt.subplot(1, 3, 2)
plt.title('Slice of a patient')
plt.imshow(test_patient['image'][0, 0, :, :, 30], cmap="gray")

plt.subplot(1, 3, 3)
plt.title('Label of a patient')
plt.imshow(test_patient['label'][0, 0, :, :, 30])
plt.show()
Left: raw CT slice. Middle: after spacing, HU window, crop, resize. Right: label

Left is the raw slice. Middle is after spacing / window / crop / resize. Right is the mask. Random affine, rotate, and Gaussian noise are the next step — 3D volume augmentation in MONAI — not this page.

Notebook: amine0110/preporcess-volume-medical-imaging.

PYCAD builds custom segmentation pipelines and web DICOM viewers when the mask has to live next to the series in an app. Case studies.

Keep Reading

Related Articles

Explore the full Segmentation HubAll services, tools, and guides on this topic — in one place.

Visit Hub →

We build custom medical imaging platforms — advanced DICOM viewers, AI segmentation, and the clinical systems around them.

Get in Touch

Copyright © 2026 PYCAD. All Rights Reserved.