torch_em.data.datasets.medical.pddca

The PDDCA dataset contains annotations for organs-at-risk segmentation in head and neck CT.

The dataset (Public Domain Database for Computational Anatomy, version 1.4.1) consists of 48 CT scans from the RTOG 0522 clinical trial with manual segmentations of nine organs at risk. It was used for the MICCAI 2015 Head and Neck Auto-Segmentation Challenge.

NOTE: The per-structure binary masks are combined into one semantic label volume with the following ids:

  • background: 0
  • brainstem: 1
  • optic chiasm: 2
  • mandible: 3
  • left optic nerve: 4
  • right optic nerve: 5
  • left parotid gland: 6
  • right parotid gland: 7
  • left submandibular gland: 8
  • right submandibular gland: 9 A few structures overlap by a handful of voxels. In this case, the smaller structure takes priority: the masks are written in the order mandible, parotids, submandibular glands, brainstem, optic nerves, chiasm, so that a later structure overwrites an earlier one. Not all structures are annotated for every patient (e.g. some patients lack the mandible or the right submandibular gland); missing structures are left as background.

The official challenge sub-packages are exposed as splits: 'train' (25 scans, 0522c0001 to 0522c0328), 'train_additional' (8 scans, 0522c0329 to 0522c0479), 'test_offsite' (10 scans, 0522c0555 to 0522c0746) and 'test_onsite' (5 scans, 0522c0788 to 0522c0878).

The dataset is located at https://www.imagenglab.com/newsite/pddca/.

This dataset is from the publication https://doi.org/10.1002/mp.12197. Please cite it if you use this dataset in your research.

  1"""The PDDCA dataset contains annotations for organs-at-risk segmentation in head and neck CT.
  2
  3The dataset (Public Domain Database for Computational Anatomy, version 1.4.1) consists of 48 CT scans from the
  4RTOG 0522 clinical trial with manual segmentations of nine organs at risk. It was used for the
  5MICCAI 2015 Head and Neck Auto-Segmentation Challenge.
  6
  7NOTE: The per-structure binary masks are combined into one semantic label volume with the following ids:
  8- background: 0
  9- brainstem: 1
 10- optic chiasm: 2
 11- mandible: 3
 12- left optic nerve: 4
 13- right optic nerve: 5
 14- left parotid gland: 6
 15- right parotid gland: 7
 16- left submandibular gland: 8
 17- right submandibular gland: 9
 18A few structures overlap by a handful of voxels. In this case, the smaller structure takes priority:
 19the masks are written in the order mandible, parotids, submandibular glands, brainstem, optic nerves, chiasm,
 20so that a later structure overwrites an earlier one. Not all structures are annotated for every patient
 21(e.g. some patients lack the mandible or the right submandibular gland); missing structures are left as background.
 22
 23The official challenge sub-packages are exposed as splits: 'train' (25 scans, 0522c0001 to 0522c0328),
 24'train_additional' (8 scans, 0522c0329 to 0522c0479), 'test_offsite' (10 scans, 0522c0555 to 0522c0746)
 25and 'test_onsite' (5 scans, 0522c0788 to 0522c0878).
 26
 27The dataset is located at https://www.imagenglab.com/newsite/pddca/.
 28
 29This dataset is from the publication https://doi.org/10.1002/mp.12197.
 30Please cite it if you use this dataset in your research.
 31"""
 32
 33import os
 34from glob import glob
 35from tqdm import tqdm
 36from natsort import natsorted
 37from typing import Union, Tuple, Literal, List, Optional
 38
 39import numpy as np
 40
 41from torch.utils.data import Dataset, DataLoader
 42
 43import torch_em
 44
 45from .. import util
 46
 47
 48URLS = [
 49    "https://www.imagenglab.com/data/pddca/PDDCA-1.4.1_part1.zip",
 50    "https://www.imagenglab.com/data/pddca/PDDCA-1.4.1_part2.zip",
 51    "https://www.imagenglab.com/data/pddca/PDDCA-1.4.1_part3.zip",
 52]
 53
 54CHECKSUMS = [
 55    "5b47b94f1e6aaad0a10c694906bd40ee8bcfc701e6d3bcbe3f12005815b721ef",
 56    "a5354e5373050f958e2696088a24f25f72361844d8b0d6988c16a3c989fd5659",
 57    "58bc70db301d3f0fd2e856a33e7d2be929859bc5b1a37d7c83a47f2bbe6a7fb6",
 58]
 59
 60LABEL_IDS = {
 61    "BrainStem": 1,
 62    "Chiasm": 2,
 63    "Mandible": 3,
 64    "OpticNerve_L": 4,
 65    "OpticNerve_R": 5,
 66    "Parotid_L": 6,
 67    "Parotid_R": 7,
 68    "Submandibular_L": 8,
 69    "Submandibular_R": 9,
 70}
 71
 72# The order in which the structures are written to the label volume. Later structures overwrite earlier ones.
 73WRITE_ORDER = [
 74    "Mandible", "Parotid_L", "Parotid_R", "Submandibular_L", "Submandibular_R",
 75    "BrainStem", "OpticNerve_L", "OpticNerve_R", "Chiasm",
 76]
 77
 78SPLITS = {
 79    "train": (1, 328),
 80    "train_additional": (329, 479),
 81    "test_offsite": (555, 746),
 82    "test_onsite": (788, 878),
 83}
 84
 85
 86def _convert_case(case_dir, out_path):
 87    import nrrd
 88    import h5py
 89
 90    raw, header = nrrd.read(os.path.join(case_dir, "img.nrrd"))
 91    labels = np.zeros(raw.shape, dtype="uint8")
 92    for name in WRITE_ORDER:
 93        mask_path = os.path.join(case_dir, "structures", f"{name}.nrrd")
 94        if not os.path.exists(mask_path):
 95            continue
 96        mask, _ = nrrd.read(mask_path)
 97        assert mask.shape == raw.shape, f"Shape mismatch for {mask_path}."
 98        labels[mask > 0] = LABEL_IDS[name]
 99
100    # The nrrd arrays are stored in (x, y, z) order, we transpose them to (z, y, x).
101    raw, labels = raw.transpose(2, 1, 0), labels.transpose(2, 1, 0)
102    with h5py.File(out_path, "w") as f:
103        f.create_dataset("raw", data=raw, compression="gzip")
104        f.create_dataset("labels", data=labels, compression="gzip")
105        f.attrs["spacing"] = np.diag(header["space directions"])[::-1].tolist()
106
107
108def get_pddca_data(path: Union[os.PathLike, str], download: bool = False) -> str:
109    """Download the PDDCA dataset and convert it to hdf5 volumes.
110
111    Args:
112        path: Filepath to a folder where the data is downloaded for further processing.
113        download: Whether to download the data if it is not present.
114
115    Returns:
116        Filepath to the folder with the preprocessed hdf5 volumes.
117    """
118    data_dir = os.path.join(path, "preprocessed")
119    if os.path.exists(data_dir):
120        return data_dir
121
122    os.makedirs(path, exist_ok=True)
123    raw_dir = os.path.join(path, "raw")
124    for url, checksum in zip(URLS, CHECKSUMS):
125        zip_path = os.path.join(path, os.path.basename(url))
126        util.download_source(path=zip_path, url=url, download=download, checksum=checksum)
127        util.unzip(zip_path=zip_path, dst=raw_dir)
128
129    case_dirs = natsorted(glob(os.path.join(raw_dir, "0522c*")))
130    assert len(case_dirs) == 48, f"Expected 48 cases, found {len(case_dirs)}."
131
132    os.makedirs(data_dir, exist_ok=True)
133    for case_dir in tqdm(case_dirs, desc="Converting PDDCA volumes to hdf5"):
134        _convert_case(case_dir, os.path.join(data_dir, f"{os.path.basename(case_dir)}.h5"))
135
136    return data_dir
137
138
139def get_pddca_paths(
140    path: Union[os.PathLike, str],
141    split: Optional[Literal["train", "train_additional", "test_offsite", "test_onsite"]] = None,
142    download: bool = False,
143) -> List[str]:
144    """Get paths to the PDDCA data.
145
146    Args:
147        path: Filepath to a folder where the data is downloaded for further processing.
148        split: The choice of data split. One of 'train', 'train_additional', 'test_offsite' or 'test_onsite'.
149            By default, all volumes are returned.
150        download: Whether to download the data if it is not present.
151
152    Returns:
153        List of filepaths for the hdf5 volumes with image and label data.
154    """
155    data_dir = get_pddca_data(path, download)
156    volume_paths = natsorted(glob(os.path.join(data_dir, "0522c*.h5")))
157
158    if split is not None:
159        if split not in SPLITS:
160            raise ValueError(f"'{split}' is not a valid split. Choose one of {list(SPLITS)}.")
161        lower, upper = SPLITS[split]
162        case_ids = [int(os.path.basename(p).replace("0522c", "").replace(".h5", "")) for p in volume_paths]
163        volume_paths = [p for p, case_id in zip(volume_paths, case_ids) if lower <= case_id <= upper]
164
165    assert len(volume_paths) > 0
166    return volume_paths
167
168
169def get_pddca_dataset(
170    path: Union[os.PathLike, str],
171    patch_shape: Tuple[int, ...],
172    split: Optional[Literal["train", "train_additional", "test_offsite", "test_onsite"]] = None,
173    resize_inputs: bool = False,
174    download: bool = False,
175    **kwargs
176) -> Dataset:
177    """Get the PDDCA dataset for organs-at-risk segmentation in head and neck CT.
178
179    Args:
180        path: Filepath to a folder where the data is downloaded for further processing.
181        patch_shape: The patch shape to use for training.
182        split: The choice of data split. One of 'train', 'train_additional', 'test_offsite' or 'test_onsite'.
183            By default, all volumes are returned.
184        resize_inputs: Whether to resize inputs to the desired patch shape.
185        download: Whether to download the data if it is not present.
186        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset`.
187
188    Returns:
189        The segmentation dataset.
190    """
191    volume_paths = get_pddca_paths(path, split, download)
192
193    if resize_inputs:
194        resize_kwargs = {"patch_shape": patch_shape, "is_rgb": False}
195        kwargs, patch_shape = util.update_kwargs_for_resize_trafo(
196            kwargs=kwargs, patch_shape=patch_shape, resize_inputs=resize_inputs, resize_kwargs=resize_kwargs
197        )
198
199    return torch_em.default_segmentation_dataset(
200        raw_paths=volume_paths,
201        raw_key="raw",
202        label_paths=volume_paths,
203        label_key="labels",
204        patch_shape=patch_shape,
205        is_seg_dataset=True,
206        **kwargs
207    )
208
209
210def get_pddca_loader(
211    path: Union[os.PathLike, str],
212    batch_size: int,
213    patch_shape: Tuple[int, ...],
214    split: Optional[Literal["train", "train_additional", "test_offsite", "test_onsite"]] = None,
215    resize_inputs: bool = False,
216    download: bool = False,
217    **kwargs
218) -> DataLoader:
219    """Get the PDDCA dataloader for organs-at-risk segmentation in head and neck CT.
220
221    Args:
222        path: Filepath to a folder where the data is downloaded for further processing.
223        batch_size: The batch size for training.
224        patch_shape: The patch shape to use for training.
225        split: The choice of data split. One of 'train', 'train_additional', 'test_offsite' or 'test_onsite'.
226            By default, all volumes are returned.
227        resize_inputs: Whether to resize inputs to the desired patch shape.
228        download: Whether to download the data if it is not present.
229        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset` or for the PyTorch DataLoader.
230
231    Returns:
232        The DataLoader.
233    """
234    ds_kwargs, loader_kwargs = util.split_kwargs(torch_em.default_segmentation_dataset, **kwargs)
235    dataset = get_pddca_dataset(path, patch_shape, split, resize_inputs, download, **ds_kwargs)
236    return torch_em.get_data_loader(dataset, batch_size, **loader_kwargs)
URLS = ['https://www.imagenglab.com/data/pddca/PDDCA-1.4.1_part1.zip', 'https://www.imagenglab.com/data/pddca/PDDCA-1.4.1_part2.zip', 'https://www.imagenglab.com/data/pddca/PDDCA-1.4.1_part3.zip']
CHECKSUMS = ['5b47b94f1e6aaad0a10c694906bd40ee8bcfc701e6d3bcbe3f12005815b721ef', 'a5354e5373050f958e2696088a24f25f72361844d8b0d6988c16a3c989fd5659', '58bc70db301d3f0fd2e856a33e7d2be929859bc5b1a37d7c83a47f2bbe6a7fb6']
LABEL_IDS = {'BrainStem': 1, 'Chiasm': 2, 'Mandible': 3, 'OpticNerve_L': 4, 'OpticNerve_R': 5, 'Parotid_L': 6, 'Parotid_R': 7, 'Submandibular_L': 8, 'Submandibular_R': 9}
WRITE_ORDER = ['Mandible', 'Parotid_L', 'Parotid_R', 'Submandibular_L', 'Submandibular_R', 'BrainStem', 'OpticNerve_L', 'OpticNerve_R', 'Chiasm']
SPLITS = {'train': (1, 328), 'train_additional': (329, 479), 'test_offsite': (555, 746), 'test_onsite': (788, 878)}
def get_pddca_data(path: Union[os.PathLike, str], download: bool = False) -> str:
109def get_pddca_data(path: Union[os.PathLike, str], download: bool = False) -> str:
110    """Download the PDDCA dataset and convert it to hdf5 volumes.
111
112    Args:
113        path: Filepath to a folder where the data is downloaded for further processing.
114        download: Whether to download the data if it is not present.
115
116    Returns:
117        Filepath to the folder with the preprocessed hdf5 volumes.
118    """
119    data_dir = os.path.join(path, "preprocessed")
120    if os.path.exists(data_dir):
121        return data_dir
122
123    os.makedirs(path, exist_ok=True)
124    raw_dir = os.path.join(path, "raw")
125    for url, checksum in zip(URLS, CHECKSUMS):
126        zip_path = os.path.join(path, os.path.basename(url))
127        util.download_source(path=zip_path, url=url, download=download, checksum=checksum)
128        util.unzip(zip_path=zip_path, dst=raw_dir)
129
130    case_dirs = natsorted(glob(os.path.join(raw_dir, "0522c*")))
131    assert len(case_dirs) == 48, f"Expected 48 cases, found {len(case_dirs)}."
132
133    os.makedirs(data_dir, exist_ok=True)
134    for case_dir in tqdm(case_dirs, desc="Converting PDDCA volumes to hdf5"):
135        _convert_case(case_dir, os.path.join(data_dir, f"{os.path.basename(case_dir)}.h5"))
136
137    return data_dir

Download the PDDCA dataset and convert it to hdf5 volumes.

Arguments:
  • path: Filepath to a folder where the data is downloaded for further processing.
  • download: Whether to download the data if it is not present.
Returns:

Filepath to the folder with the preprocessed hdf5 volumes.

def get_pddca_paths( path: Union[os.PathLike, str], split: Optional[Literal['train', 'train_additional', 'test_offsite', 'test_onsite']] = None, download: bool = False) -> List[str]:
140def get_pddca_paths(
141    path: Union[os.PathLike, str],
142    split: Optional[Literal["train", "train_additional", "test_offsite", "test_onsite"]] = None,
143    download: bool = False,
144) -> List[str]:
145    """Get paths to the PDDCA data.
146
147    Args:
148        path: Filepath to a folder where the data is downloaded for further processing.
149        split: The choice of data split. One of 'train', 'train_additional', 'test_offsite' or 'test_onsite'.
150            By default, all volumes are returned.
151        download: Whether to download the data if it is not present.
152
153    Returns:
154        List of filepaths for the hdf5 volumes with image and label data.
155    """
156    data_dir = get_pddca_data(path, download)
157    volume_paths = natsorted(glob(os.path.join(data_dir, "0522c*.h5")))
158
159    if split is not None:
160        if split not in SPLITS:
161            raise ValueError(f"'{split}' is not a valid split. Choose one of {list(SPLITS)}.")
162        lower, upper = SPLITS[split]
163        case_ids = [int(os.path.basename(p).replace("0522c", "").replace(".h5", "")) for p in volume_paths]
164        volume_paths = [p for p, case_id in zip(volume_paths, case_ids) if lower <= case_id <= upper]
165
166    assert len(volume_paths) > 0
167    return volume_paths

Get paths to the PDDCA data.

Arguments:
  • path: Filepath to a folder where the data is downloaded for further processing.
  • split: The choice of data split. One of 'train', 'train_additional', 'test_offsite' or 'test_onsite'. By default, all volumes are returned.
  • download: Whether to download the data if it is not present.
Returns:

List of filepaths for the hdf5 volumes with image and label data.

def get_pddca_dataset( path: Union[os.PathLike, str], patch_shape: Tuple[int, ...], split: Optional[Literal['train', 'train_additional', 'test_offsite', 'test_onsite']] = None, resize_inputs: bool = False, download: bool = False, **kwargs) -> torch.utils.data.dataset.Dataset:
170def get_pddca_dataset(
171    path: Union[os.PathLike, str],
172    patch_shape: Tuple[int, ...],
173    split: Optional[Literal["train", "train_additional", "test_offsite", "test_onsite"]] = None,
174    resize_inputs: bool = False,
175    download: bool = False,
176    **kwargs
177) -> Dataset:
178    """Get the PDDCA dataset for organs-at-risk segmentation in head and neck CT.
179
180    Args:
181        path: Filepath to a folder where the data is downloaded for further processing.
182        patch_shape: The patch shape to use for training.
183        split: The choice of data split. One of 'train', 'train_additional', 'test_offsite' or 'test_onsite'.
184            By default, all volumes are returned.
185        resize_inputs: Whether to resize inputs to the desired patch shape.
186        download: Whether to download the data if it is not present.
187        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset`.
188
189    Returns:
190        The segmentation dataset.
191    """
192    volume_paths = get_pddca_paths(path, split, download)
193
194    if resize_inputs:
195        resize_kwargs = {"patch_shape": patch_shape, "is_rgb": False}
196        kwargs, patch_shape = util.update_kwargs_for_resize_trafo(
197            kwargs=kwargs, patch_shape=patch_shape, resize_inputs=resize_inputs, resize_kwargs=resize_kwargs
198        )
199
200    return torch_em.default_segmentation_dataset(
201        raw_paths=volume_paths,
202        raw_key="raw",
203        label_paths=volume_paths,
204        label_key="labels",
205        patch_shape=patch_shape,
206        is_seg_dataset=True,
207        **kwargs
208    )

Get the PDDCA dataset for organs-at-risk segmentation in head and neck CT.

Arguments:
  • path: Filepath to a folder where the data is downloaded for further processing.
  • patch_shape: The patch shape to use for training.
  • split: The choice of data split. One of 'train', 'train_additional', 'test_offsite' or 'test_onsite'. By default, all volumes are returned.
  • resize_inputs: Whether to resize inputs to the desired patch shape.
  • download: Whether to download the data if it is not present.
  • kwargs: Additional keyword arguments for torch_em.default_segmentation_dataset.
Returns:

The segmentation dataset.

def get_pddca_loader( path: Union[os.PathLike, str], batch_size: int, patch_shape: Tuple[int, ...], split: Optional[Literal['train', 'train_additional', 'test_offsite', 'test_onsite']] = None, resize_inputs: bool = False, download: bool = False, **kwargs) -> torch.utils.data.dataloader.DataLoader:
211def get_pddca_loader(
212    path: Union[os.PathLike, str],
213    batch_size: int,
214    patch_shape: Tuple[int, ...],
215    split: Optional[Literal["train", "train_additional", "test_offsite", "test_onsite"]] = None,
216    resize_inputs: bool = False,
217    download: bool = False,
218    **kwargs
219) -> DataLoader:
220    """Get the PDDCA dataloader for organs-at-risk segmentation in head and neck CT.
221
222    Args:
223        path: Filepath to a folder where the data is downloaded for further processing.
224        batch_size: The batch size for training.
225        patch_shape: The patch shape to use for training.
226        split: The choice of data split. One of 'train', 'train_additional', 'test_offsite' or 'test_onsite'.
227            By default, all volumes are returned.
228        resize_inputs: Whether to resize inputs to the desired patch shape.
229        download: Whether to download the data if it is not present.
230        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset` or for the PyTorch DataLoader.
231
232    Returns:
233        The DataLoader.
234    """
235    ds_kwargs, loader_kwargs = util.split_kwargs(torch_em.default_segmentation_dataset, **kwargs)
236    dataset = get_pddca_dataset(path, patch_shape, split, resize_inputs, download, **ds_kwargs)
237    return torch_em.get_data_loader(dataset, batch_size, **loader_kwargs)

Get the PDDCA dataloader for organs-at-risk segmentation in head and neck CT.

Arguments:
  • path: Filepath to a folder where the data is downloaded for further processing.
  • batch_size: The batch size for training.
  • patch_shape: The patch shape to use for training.
  • split: The choice of data split. One of 'train', 'train_additional', 'test_offsite' or 'test_onsite'. By default, all volumes are returned.
  • resize_inputs: Whether to resize inputs to the desired patch shape.
  • download: Whether to download the data if it is not present.
  • kwargs: Additional keyword arguments for torch_em.default_segmentation_dataset or for the PyTorch DataLoader.
Returns:

The DataLoader.