torch_em.data.datasets.medical.openkbp

The OpenKBP dataset contains annotations for organ-at-risk (OAR) and planning target volume (PTV) segmentation in radiotherapy planning CT scans of head-and-neck cancer patients.

The data was curated for the OpenKBP Grand Challenge (https://github.com/ababier/open-kbp), which was held at AAPM 2020 and targets models for knowledge-based dose prediction. The public release consists of 340 patients treated with intensity modulated radiation therapy, split into the official 200 training, 40 validation and 100 test studies (selected with the 'split' argument). Each patient has a 128x128x128 CT volume together with up to 10 structure masks: 7 OARs ('Brainstem', 'SpinalCord', 'RightParotid', 'LeftParotid', 'Esophagus', 'Larynx', 'Mandible') and 3 PTVs ('PTV56', 'PTV63', 'PTV70'), see LABEL_IDS. Not every patient has every structure delineated; missing structures are simply not present in that patient's label volume.

NOTE: The raw data also provides a per-voxel dose distribution and a feasible dose mask for the OpenKBP dose prediction challenge. This module only exposes the OAR/PTV structure masks as segmentation targets; dose regression is out of scope for this library.

The data is hosted directly (as plain csv files, not through git-lfs) in the challenge repository at https://github.com/ababier/open-kbp. This module downloads it from a pinned commit of that repository, so that the checksum of the downloaded archive stays reproducible. It is distributed for research use; see the repository for the exact license terms.

The CT and structure files each store a flattened (raveled, C order) sparse representation of the 128x128x128 patient volume: only the non-background voxel indices (and, for the CT scan, their values) are listed. This module reconstructs the dense CT volume and a single-channel structure label volume (using the priority order of LABEL_IDS, so that a PTV overwrites an OAR at an overlapping voxel) and stores them as hdf5 volumes (keys 'raw' and 'labels') for efficient access.

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

  1"""The OpenKBP dataset contains annotations for organ-at-risk (OAR) and planning target volume (PTV)
  2segmentation in radiotherapy planning CT scans of head-and-neck cancer patients.
  3
  4The data was curated for the OpenKBP Grand Challenge (https://github.com/ababier/open-kbp), which was held
  5at AAPM 2020 and targets models for knowledge-based dose prediction. The public release consists of 340
  6patients treated with intensity modulated radiation therapy, split into the official 200 training, 40
  7validation and 100 test studies (selected with the 'split' argument). Each patient has a 128x128x128 CT
  8volume together with up to 10 structure masks: 7 OARs ('Brainstem', 'SpinalCord', 'RightParotid',
  9'LeftParotid', 'Esophagus', 'Larynx', 'Mandible') and 3 PTVs ('PTV56', 'PTV63', 'PTV70'), see `LABEL_IDS`.
 10Not every patient has every structure delineated; missing structures are simply not present in that
 11patient's label volume.
 12
 13NOTE: The raw data also provides a per-voxel dose distribution and a feasible dose mask for the OpenKBP
 14dose prediction challenge. This module only exposes the OAR/PTV structure masks as segmentation targets;
 15dose regression is out of scope for this library.
 16
 17The data is hosted directly (as plain csv files, not through git-lfs) in the challenge repository at
 18https://github.com/ababier/open-kbp. This module downloads it from a pinned commit of that repository, so
 19that the checksum of the downloaded archive stays reproducible. It is distributed for research use; see the
 20repository for the exact license terms.
 21
 22The CT and structure files each store a flattened (raveled, C order) sparse representation of the
 23128x128x128 patient volume: only the non-background voxel indices (and, for the CT scan, their values) are
 24listed. This module reconstructs the dense CT volume and a single-channel structure label volume (using the
 25priority order of `LABEL_IDS`, so that a PTV overwrites an OAR at an overlapping voxel) and stores them as
 26hdf5 volumes (keys 'raw' and 'labels') for efficient access.
 27
 28This dataset is from the publication https://doi.org/10.1002/mp.14845.
 29Please cite it if you use this dataset in your research.
 30"""
 31
 32import os
 33from glob import glob
 34from tqdm import tqdm
 35from natsort import natsorted
 36from typing import Union, Tuple, List, Literal
 37
 38import numpy as np
 39
 40from torch.utils.data import Dataset, DataLoader
 41
 42import torch_em
 43
 44from .. import util
 45
 46
 47URL = "https://codeload.github.com/ababier/open-kbp/zip/ce625e62f3b04203f22bd9d1634f3e8fb0245e46"
 48CHECKSUM = "4074858f3f0d9d7f639342349e2799c1109f3018e3bd487827f334b591ebf697"
 49
 50COMMIT = "ce625e62f3b04203f22bd9d1634f3e8fb0245e46"
 51
 52SPLITS = {"train": "train-pats", "val": "validation-pats", "test": "test-pats"}
 53
 54N_PATIENTS = {"train": 200, "val": 40, "test": 100}
 55
 56PATIENT_SHAPE = (128, 128, 128)
 57
 58OARS = ["Brainstem", "SpinalCord", "RightParotid", "LeftParotid", "Esophagus", "Larynx", "Mandible"]
 59TARGETS = ["PTV56", "PTV63", "PTV70"]
 60STRUCTURES = OARS + TARGETS
 61
 62LABEL_IDS = {"background": 0}
 63LABEL_IDS.update({name: i + 1 for i, name in enumerate(STRUCTURES)})
 64
 65
 66def _load_sparse_csv(csv_path):
 67    import pandas as pd
 68
 69    df = pd.read_csv(csv_path, index_col=0)
 70    indices = df.index.to_numpy()
 71    if df["data"].isna().all():  # This is a binary structure mask, so it only stores the indices.
 72        return indices, None
 73    else:  # This is a sparse volume (eg. the CT scan), so it also stores a value per index.
 74        return indices, df["data"].to_numpy()
 75
 76
 77def _preprocess_patient(patient_dir, volume_path):
 78    import h5py
 79
 80    raw = np.zeros(np.prod(PATIENT_SHAPE), dtype="int16")
 81    indices, values = _load_sparse_csv(os.path.join(patient_dir, "ct.csv"))
 82    raw[indices] = values
 83    raw = raw.reshape(PATIENT_SHAPE)
 84
 85    labels = np.zeros(np.prod(PATIENT_SHAPE), dtype="uint8")
 86    for structure in STRUCTURES:
 87        structure_path = os.path.join(patient_dir, f"{structure}.csv")
 88        if not os.path.exists(structure_path):
 89            continue
 90        indices, _ = _load_sparse_csv(structure_path)
 91        labels[indices] = LABEL_IDS[structure]
 92    labels = labels.reshape(PATIENT_SHAPE)
 93
 94    with h5py.File(f"{volume_path}.tmp", "w") as f:
 95        f.create_dataset("raw", data=raw, compression="gzip")
 96        f.create_dataset("labels", data=labels, compression="gzip")
 97
 98    os.rename(f"{volume_path}.tmp", volume_path)
 99
100
101def _preprocess_inputs(data_dir, split, preprocessed_dir):
102    patient_dirs = natsorted(glob(os.path.join(data_dir, SPLITS[split], "pt_*")))
103    os.makedirs(preprocessed_dir, exist_ok=True)
104    for patient_dir in tqdm(patient_dirs, desc=f"Preprocessing the OpenKBP '{split}' patients"):
105        volume_path = os.path.join(preprocessed_dir, f"{os.path.basename(patient_dir)}.h5")
106        if os.path.exists(volume_path):
107            continue
108        _preprocess_patient(patient_dir, volume_path)
109
110
111def get_openkbp_data(path: Union[os.PathLike, str], download: bool = False) -> str:
112    """Download the OpenKBP dataset.
113
114    Args:
115        path: Filepath to a folder where the data is downloaded for further processing.
116        download: Whether to download the data if it is not present.
117
118    Returns:
119        Filepath where the provided data is stored.
120    """
121    data_dir = os.path.join(path, f"open-kbp-{COMMIT}", "provided-data")
122    if os.path.exists(data_dir):
123        return data_dir
124
125    os.makedirs(path, exist_ok=True)
126
127    zip_path = os.path.join(path, "open-kbp.zip")
128    util.download_source(path=zip_path, url=URL, download=download, checksum=CHECKSUM)
129    util.unzip(zip_path=zip_path, dst=path)
130
131    return data_dir
132
133
134def get_openkbp_paths(
135    path: Union[os.PathLike, str], split: Literal["train", "val", "test"], download: bool = False
136) -> List[str]:
137    """Get paths to the OpenKBP data.
138
139    Args:
140        path: Filepath to a folder where the data is downloaded for further processing.
141        split: The choice of data split. Either 'train', 'val' or 'test'.
142        download: Whether to download the data if it is not present.
143
144    Returns:
145        List of filepaths for the hdf5 files, which contain the image data ('raw') and the label data ('labels').
146    """
147    if split not in SPLITS:
148        raise ValueError(f"'{split}' is not a valid split. Please choose one of {list(SPLITS.keys())}.")
149
150    preprocessed_dir = os.path.join(path, "preprocessed", split)
151    if len(glob(os.path.join(preprocessed_dir, "*.h5"))) != N_PATIENTS[split]:
152        data_dir = get_openkbp_data(path, download)
153        _preprocess_inputs(data_dir, split, preprocessed_dir)
154
155    volume_paths = natsorted(glob(os.path.join(preprocessed_dir, "*.h5")))
156    assert len(volume_paths) == N_PATIENTS[split]
157
158    return volume_paths
159
160
161def get_openkbp_dataset(
162    path: Union[os.PathLike, str],
163    patch_shape: Tuple[int, ...],
164    split: Literal["train", "val", "test"],
165    resize_inputs: bool = False,
166    download: bool = False,
167    **kwargs
168) -> Dataset:
169    """Get the OpenKBP dataset for organ-at-risk and planning target volume segmentation.
170
171    Args:
172        path: Filepath to a folder where the data is downloaded for further processing.
173        patch_shape: The patch shape to use for training.
174        split: The choice of data split. Either 'train', 'val' or 'test'.
175        resize_inputs: Whether to resize inputs to the desired patch shape.
176        download: Whether to download the data if it is not present.
177        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset`.
178
179    Returns:
180        The segmentation dataset.
181    """
182    volume_paths = get_openkbp_paths(path, split, download)
183
184    if resize_inputs:
185        resize_kwargs = {"patch_shape": patch_shape, "is_rgb": False}
186        kwargs, patch_shape = util.update_kwargs_for_resize_trafo(
187            kwargs=kwargs, patch_shape=patch_shape, resize_inputs=resize_inputs, resize_kwargs=resize_kwargs
188        )
189
190    return torch_em.default_segmentation_dataset(
191        raw_paths=volume_paths,
192        raw_key="raw",
193        label_paths=volume_paths,
194        label_key="labels",
195        patch_shape=patch_shape,
196        is_seg_dataset=True,
197        **kwargs
198    )
199
200
201def get_openkbp_loader(
202    path: Union[os.PathLike, str],
203    batch_size: int,
204    patch_shape: Tuple[int, ...],
205    split: Literal["train", "val", "test"],
206    resize_inputs: bool = False,
207    download: bool = False,
208    **kwargs
209) -> DataLoader:
210    """Get the OpenKBP dataloader for organ-at-risk and planning target volume segmentation.
211
212    Args:
213        path: Filepath to a folder where the data is downloaded for further processing.
214        batch_size: The batch size for training.
215        patch_shape: The patch shape to use for training.
216        split: The choice of data split. Either 'train', 'val' or 'test'.
217        resize_inputs: Whether to resize inputs to the desired patch shape.
218        download: Whether to download the data if it is not present.
219        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset` or for the PyTorch DataLoader.
220
221    Returns:
222        The DataLoader.
223    """
224    ds_kwargs, loader_kwargs = util.split_kwargs(torch_em.default_segmentation_dataset, **kwargs)
225    dataset = get_openkbp_dataset(path, patch_shape, split, resize_inputs, download, **ds_kwargs)
226    return torch_em.get_data_loader(dataset, batch_size, **loader_kwargs)
URL = 'https://codeload.github.com/ababier/open-kbp/zip/ce625e62f3b04203f22bd9d1634f3e8fb0245e46'
CHECKSUM = '4074858f3f0d9d7f639342349e2799c1109f3018e3bd487827f334b591ebf697'
COMMIT = 'ce625e62f3b04203f22bd9d1634f3e8fb0245e46'
SPLITS = {'train': 'train-pats', 'val': 'validation-pats', 'test': 'test-pats'}
N_PATIENTS = {'train': 200, 'val': 40, 'test': 100}
PATIENT_SHAPE = (128, 128, 128)
OARS = ['Brainstem', 'SpinalCord', 'RightParotid', 'LeftParotid', 'Esophagus', 'Larynx', 'Mandible']
TARGETS = ['PTV56', 'PTV63', 'PTV70']
STRUCTURES = ['Brainstem', 'SpinalCord', 'RightParotid', 'LeftParotid', 'Esophagus', 'Larynx', 'Mandible', 'PTV56', 'PTV63', 'PTV70']
LABEL_IDS = {'background': 0, 'Brainstem': 1, 'SpinalCord': 2, 'RightParotid': 3, 'LeftParotid': 4, 'Esophagus': 5, 'Larynx': 6, 'Mandible': 7, 'PTV56': 8, 'PTV63': 9, 'PTV70': 10}
def get_openkbp_data(path: Union[os.PathLike, str], download: bool = False) -> str:
112def get_openkbp_data(path: Union[os.PathLike, str], download: bool = False) -> str:
113    """Download the OpenKBP dataset.
114
115    Args:
116        path: Filepath to a folder where the data is downloaded for further processing.
117        download: Whether to download the data if it is not present.
118
119    Returns:
120        Filepath where the provided data is stored.
121    """
122    data_dir = os.path.join(path, f"open-kbp-{COMMIT}", "provided-data")
123    if os.path.exists(data_dir):
124        return data_dir
125
126    os.makedirs(path, exist_ok=True)
127
128    zip_path = os.path.join(path, "open-kbp.zip")
129    util.download_source(path=zip_path, url=URL, download=download, checksum=CHECKSUM)
130    util.unzip(zip_path=zip_path, dst=path)
131
132    return data_dir

Download the OpenKBP dataset.

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 where the provided data is stored.

def get_openkbp_paths( path: Union[os.PathLike, str], split: Literal['train', 'val', 'test'], download: bool = False) -> List[str]:
135def get_openkbp_paths(
136    path: Union[os.PathLike, str], split: Literal["train", "val", "test"], download: bool = False
137) -> List[str]:
138    """Get paths to the OpenKBP data.
139
140    Args:
141        path: Filepath to a folder where the data is downloaded for further processing.
142        split: The choice of data split. Either 'train', 'val' or 'test'.
143        download: Whether to download the data if it is not present.
144
145    Returns:
146        List of filepaths for the hdf5 files, which contain the image data ('raw') and the label data ('labels').
147    """
148    if split not in SPLITS:
149        raise ValueError(f"'{split}' is not a valid split. Please choose one of {list(SPLITS.keys())}.")
150
151    preprocessed_dir = os.path.join(path, "preprocessed", split)
152    if len(glob(os.path.join(preprocessed_dir, "*.h5"))) != N_PATIENTS[split]:
153        data_dir = get_openkbp_data(path, download)
154        _preprocess_inputs(data_dir, split, preprocessed_dir)
155
156    volume_paths = natsorted(glob(os.path.join(preprocessed_dir, "*.h5")))
157    assert len(volume_paths) == N_PATIENTS[split]
158
159    return volume_paths

Get paths to the OpenKBP data.

Arguments:
  • path: Filepath to a folder where the data is downloaded for further processing.
  • split: The choice of data split. Either 'train', 'val' or 'test'.
  • download: Whether to download the data if it is not present.
Returns:

List of filepaths for the hdf5 files, which contain the image data ('raw') and the label data ('labels').

def get_openkbp_dataset( path: Union[os.PathLike, str], patch_shape: Tuple[int, ...], split: Literal['train', 'val', 'test'], resize_inputs: bool = False, download: bool = False, **kwargs) -> torch.utils.data.dataset.Dataset:
162def get_openkbp_dataset(
163    path: Union[os.PathLike, str],
164    patch_shape: Tuple[int, ...],
165    split: Literal["train", "val", "test"],
166    resize_inputs: bool = False,
167    download: bool = False,
168    **kwargs
169) -> Dataset:
170    """Get the OpenKBP dataset for organ-at-risk and planning target volume segmentation.
171
172    Args:
173        path: Filepath to a folder where the data is downloaded for further processing.
174        patch_shape: The patch shape to use for training.
175        split: The choice of data split. Either 'train', 'val' or 'test'.
176        resize_inputs: Whether to resize inputs to the desired patch shape.
177        download: Whether to download the data if it is not present.
178        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset`.
179
180    Returns:
181        The segmentation dataset.
182    """
183    volume_paths = get_openkbp_paths(path, split, download)
184
185    if resize_inputs:
186        resize_kwargs = {"patch_shape": patch_shape, "is_rgb": False}
187        kwargs, patch_shape = util.update_kwargs_for_resize_trafo(
188            kwargs=kwargs, patch_shape=patch_shape, resize_inputs=resize_inputs, resize_kwargs=resize_kwargs
189        )
190
191    return torch_em.default_segmentation_dataset(
192        raw_paths=volume_paths,
193        raw_key="raw",
194        label_paths=volume_paths,
195        label_key="labels",
196        patch_shape=patch_shape,
197        is_seg_dataset=True,
198        **kwargs
199    )

Get the OpenKBP dataset for organ-at-risk and planning target volume segmentation.

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. Either 'train', 'val' or 'test'.
  • 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_openkbp_loader( path: Union[os.PathLike, str], batch_size: int, patch_shape: Tuple[int, ...], split: Literal['train', 'val', 'test'], resize_inputs: bool = False, download: bool = False, **kwargs) -> torch.utils.data.dataloader.DataLoader:
202def get_openkbp_loader(
203    path: Union[os.PathLike, str],
204    batch_size: int,
205    patch_shape: Tuple[int, ...],
206    split: Literal["train", "val", "test"],
207    resize_inputs: bool = False,
208    download: bool = False,
209    **kwargs
210) -> DataLoader:
211    """Get the OpenKBP dataloader for organ-at-risk and planning target volume segmentation.
212
213    Args:
214        path: Filepath to a folder where the data is downloaded for further processing.
215        batch_size: The batch size for training.
216        patch_shape: The patch shape to use for training.
217        split: The choice of data split. Either 'train', 'val' or 'test'.
218        resize_inputs: Whether to resize inputs to the desired patch shape.
219        download: Whether to download the data if it is not present.
220        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset` or for the PyTorch DataLoader.
221
222    Returns:
223        The DataLoader.
224    """
225    ds_kwargs, loader_kwargs = util.split_kwargs(torch_em.default_segmentation_dataset, **kwargs)
226    dataset = get_openkbp_dataset(path, patch_shape, split, resize_inputs, download, **ds_kwargs)
227    return torch_em.get_data_loader(dataset, batch_size, **loader_kwargs)

Get the OpenKBP dataloader for organ-at-risk and planning target volume segmentation.

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. Either 'train', 'val' or 'test'.
  • 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.