torch_em.data.datasets.medical.vs_mc_rc

The Vestibular-Schwannoma-MC-RC dataset contains annotations for the vestibular schwannoma tumor in MRI.

It consists of routine clinical T1-weighted and T2-weighted MRI series of patients with a single sporadic vestibular schwannoma, acquired at 10 sites in the United Kingdom. The tumor is annotated as a binary mask (label id 1) in 301 series of 124 patients, each series belongs to one timepoint of a patient. Version 2 of the collection also provides the images of 126 further timepoints, which belong to the validation and test set of a challenge and have no public annotations, so they are not used here.

The collection has no official data splits. A split is derived from the patient ids (sorted, first 80% train, next 10% val, last 10% test), so that all timepoints of a patient are in the same split.

The dataset is located at https://doi.org/10.7937/HRZH-2N82 and is distributed under the CC BY 4.0 license. This dataset is from the publication https://doi.org/10.3389/fncom.2024.1365727. Please cite it if you use this dataset in your research.

  1"""The Vestibular-Schwannoma-MC-RC dataset contains annotations for the vestibular schwannoma tumor in MRI.
  2
  3It consists of routine clinical T1-weighted and T2-weighted MRI series of patients with a single sporadic
  4vestibular schwannoma, acquired at 10 sites in the United Kingdom. The tumor is annotated as a binary mask
  5(label id 1) in 301 series of 124 patients, each series belongs to one timepoint of a patient. Version 2 of the
  6collection also provides the images of 126 further timepoints, which belong to the validation and test set of a
  7challenge and have no public annotations, so they are not used here.
  8
  9The collection has no official data splits. A split is derived from the patient ids (sorted, first 80% train,
 10next 10% val, last 10% test), so that all timepoints of a patient are in the same split.
 11
 12The dataset is located at https://doi.org/10.7937/HRZH-2N82 and is distributed under the CC BY 4.0 license.
 13This dataset is from the publication https://doi.org/10.3389/fncom.2024.1365727.
 14Please cite it if you use this dataset in your research.
 15"""
 16
 17import os
 18from glob import glob
 19from tqdm import tqdm
 20from natsort import natsorted
 21from typing import Union, Tuple, List, Literal
 22
 23from torch.utils.data import Dataset, DataLoader
 24
 25import torch_em
 26
 27from .. import util
 28
 29
 30URL = (
 31    "https://www.cancerimagingarchive.net/wp-content/uploads/"
 32    "VS-MC-RC-Segmentations_with_pathname_spreadsheet-Release2023.zip"
 33)
 34CHECKSUM = "6cbbbd10552398795bdac868385aceac70f47817bcf012d126424831ecaf198e"
 35
 36
 37def _get_labeled_series(segmentation_dir):
 38    import pandas as pd
 39
 40    info_path = glob(os.path.join(segmentation_dir, "*", "*segmentation-info*.csv"))[0]
 41    label_dir = glob(os.path.join(segmentation_dir, "*", "VS-MC-RC segmentations*"))[0]
 42    series = pd.read_csv(info_path)
 43    series = series[series["SegmentationPath"].notna()]
 44    # The segmentations of the timepoints of the challenge validation and test set are not public.
 45    series = series[[os.path.exists(os.path.join(label_dir, p)) for p in series["SegmentationPath"]]]
 46    return series, label_dir
 47
 48
 49def _resample_labels(nifti, geometry, shape):
 50    """Resample the labels onto the voxel grid of the DICOM series, as their axes can be ordered differently."""
 51    import numpy as np
 52    from scipy.ndimage import map_coordinates
 53
 54    labels = (np.asarray(nifti.dataobj) > 0).astype("float32")
 55    ras_to_voxel = np.linalg.inv(nifti.affine)
 56    row_step = geometry["spacing"][1] * geometry["row_direction"]
 57    column_step = geometry["spacing"][0] * geometry["column_direction"]
 58
 59    ys, xs = np.meshgrid(np.arange(shape[1]), np.arange(shape[2]), indexing="ij")
 60    in_plane = ys[..., None] * column_step + xs[..., None] * row_step  # (y, x, 3)
 61    resampled = np.zeros(shape, dtype="uint8")
 62    for z, origin in enumerate(geometry["origin"]):
 63        lps = in_plane + origin
 64        ras = lps * np.array([-1.0, -1.0, 1.0])
 65        voxel = ras @ ras_to_voxel[:3, :3].T + ras_to_voxel[:3, 3]
 66        resampled[z] = map_coordinates(labels, voxel.transpose(2, 0, 1), order=0, mode="constant") > 0.5
 67    return resampled
 68
 69
 70def _preprocess_vs_mc_rc(series, label_dir, dicom_dir, preprocessed_dir):
 71    import h5py
 72    import nibabel as nib
 73
 74    os.makedirs(preprocessed_dir, exist_ok=True)
 75    for _, row in tqdm(series.iterrows(), total=len(series), desc="Preprocess Vestibular-Schwannoma-MC-RC"):
 76        patient_id, date, label_name = row["SegmentationPath"].split("/")
 77        modality = label_name.split("_")[-1].split(".")[0]
 78        out_path = os.path.join(preprocessed_dir, f"{patient_id}_{date}_{modality}.h5")
 79        if os.path.exists(out_path):
 80            continue
 81
 82        volume, geometry = util.load_dicom_series(os.path.join(dicom_dir, row["series_instance_uid"]))
 83        labels = _resample_labels(nib.load(os.path.join(label_dir, row["SegmentationPath"])), geometry, volume.shape)
 84
 85        # The file is written to a temporary path first, so an interrupted run is not mistaken for a finished one.
 86        tmp_path = f"{out_path}.incomplete"
 87        with h5py.File(tmp_path, "w") as f:
 88            f.create_dataset("raw", data=volume, compression="gzip")
 89            f.create_dataset("labels", data=labels, compression="gzip")
 90        os.replace(tmp_path, out_path)
 91
 92
 93def get_vs_mc_rc_data(path: Union[os.PathLike, str], download: bool = False) -> str:
 94    """Download the Vestibular-Schwannoma-MC-RC dataset.
 95
 96    NOTE: This requires the pydicom and nibabel python packages. The DICOM series of the annotated timepoints are
 97    about 9 GB.
 98
 99    Args:
100        path: Filepath to a folder where the data is downloaded for further processing.
101        download: Whether to download the data if it is not present.
102
103    Returns:
104        Filepath where the preprocessed data is stored.
105    """
106    os.makedirs(path, exist_ok=True)
107
108    segmentation_dir = os.path.join(path, "segmentations")
109    if not os.path.exists(segmentation_dir):
110        zip_path = os.path.join(path, "segmentations.zip")
111        util.download_source(path=zip_path, url=URL, download=download, checksum=CHECKSUM)
112        util.unzip(zip_path=zip_path, dst=segmentation_dir, remove=False)
113
114    series, label_dir = _get_labeled_series(segmentation_dir)
115
116    dicom_dir = os.path.join(path, "dicom")
117    uids = sorted(series["series_instance_uid"].unique())
118    if download:
119        util.download_tcia_series(uids, dst=dicom_dir, csv_filename=os.path.join(path, "vs_mc_rc_series"))
120    elif not all(glob(os.path.join(dicom_dir, uid, "*.dcm")) for uid in uids):
121        raise RuntimeError(f"Cannot find the data at {path}, but download was set to False.")
122
123    preprocessed_dir = os.path.join(path, "preprocessed")
124    _preprocess_vs_mc_rc(series, label_dir, dicom_dir, preprocessed_dir)
125    return preprocessed_dir
126
127
128def get_vs_mc_rc_paths(
129    path: Union[os.PathLike, str], split: Literal["train", "val", "test"], download: bool = False,
130) -> List[str]:
131    """Get paths to the Vestibular-Schwannoma-MC-RC data.
132
133    Args:
134        path: Filepath to a folder where the data is downloaded for further processing.
135        split: The data split to use. Either 'train', 'val' or 'test'.
136        download: Whether to download the data if it is not present.
137
138    Returns:
139        List of filepaths for the stored data.
140    """
141    assert split in ("train", "val", "test"), f"'{split}' is not a valid data split."
142
143    preprocessed_dir = get_vs_mc_rc_data(path, download)
144    volume_paths = natsorted(glob(os.path.join(preprocessed_dir, "*.h5")))
145    assert len(volume_paths) > 0, f"Could not find any preprocessed volumes in '{preprocessed_dir}'."
146
147    patient_ids = sorted({os.path.basename(p).split("_")[0] for p in volume_paths})
148    n_train, n_val = int(0.8 * len(patient_ids)), int(0.1 * len(patient_ids))
149    split_ids = {
150        "train": patient_ids[:n_train],
151        "val": patient_ids[n_train:n_train + n_val],
152        "test": patient_ids[n_train + n_val:],
153    }[split]
154    return [p for p in volume_paths if os.path.basename(p).split("_")[0] in split_ids]
155
156
157def get_vs_mc_rc_dataset(
158    path: Union[os.PathLike, str],
159    patch_shape: Tuple[int, ...],
160    split: Literal["train", "val", "test"],
161    resize_inputs: bool = False,
162    download: bool = False,
163    **kwargs
164) -> Dataset:
165    """Get the Vestibular-Schwannoma-MC-RC dataset for tumor segmentation.
166
167    Args:
168        path: Filepath to a folder where the data is downloaded for further processing.
169        patch_shape: The patch shape to use for training.
170        split: The data split to use. Either 'train', 'val' or 'test'.
171        resize_inputs: Whether to resize inputs to the desired patch shape.
172        download: Whether to download the data if it is not present.
173        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset`.
174
175    Returns:
176        The segmentation dataset.
177    """
178    volume_paths = get_vs_mc_rc_paths(path, split, download)
179
180    if resize_inputs:
181        resize_kwargs = {"patch_shape": patch_shape, "is_rgb": False}
182        kwargs, patch_shape = util.update_kwargs_for_resize_trafo(
183            kwargs=kwargs, patch_shape=patch_shape, resize_inputs=resize_inputs, resize_kwargs=resize_kwargs
184        )
185
186    return torch_em.default_segmentation_dataset(
187        raw_paths=volume_paths,
188        raw_key="raw",
189        label_paths=volume_paths,
190        label_key="labels",
191        patch_shape=patch_shape,
192        is_seg_dataset=True,
193        **kwargs
194    )
195
196
197def get_vs_mc_rc_loader(
198    path: Union[os.PathLike, str],
199    batch_size: int,
200    patch_shape: Tuple[int, ...],
201    split: Literal["train", "val", "test"],
202    resize_inputs: bool = False,
203    download: bool = False,
204    **kwargs
205) -> DataLoader:
206    """Get the Vestibular-Schwannoma-MC-RC dataloader for tumor segmentation.
207
208    Args:
209        path: Filepath to a folder where the data is downloaded for further processing.
210        batch_size: The batch size for training.
211        patch_shape: The patch shape to use for training.
212        split: The data split to use. Either 'train', 'val' or 'test'.
213        resize_inputs: Whether to resize inputs to the desired patch shape.
214        download: Whether to download the data if it is not present.
215        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset` or for the PyTorch DataLoader.
216
217    Returns:
218        The DataLoader.
219    """
220    ds_kwargs, loader_kwargs = util.split_kwargs(torch_em.default_segmentation_dataset, **kwargs)
221    dataset = get_vs_mc_rc_dataset(path, patch_shape, split, resize_inputs, download, **ds_kwargs)
222    return torch_em.get_data_loader(dataset, batch_size, **loader_kwargs)
URL = 'https://www.cancerimagingarchive.net/wp-content/uploads/VS-MC-RC-Segmentations_with_pathname_spreadsheet-Release2023.zip'
CHECKSUM = '6cbbbd10552398795bdac868385aceac70f47817bcf012d126424831ecaf198e'
def get_vs_mc_rc_data(path: Union[os.PathLike, str], download: bool = False) -> str:
 94def get_vs_mc_rc_data(path: Union[os.PathLike, str], download: bool = False) -> str:
 95    """Download the Vestibular-Schwannoma-MC-RC dataset.
 96
 97    NOTE: This requires the pydicom and nibabel python packages. The DICOM series of the annotated timepoints are
 98    about 9 GB.
 99
100    Args:
101        path: Filepath to a folder where the data is downloaded for further processing.
102        download: Whether to download the data if it is not present.
103
104    Returns:
105        Filepath where the preprocessed data is stored.
106    """
107    os.makedirs(path, exist_ok=True)
108
109    segmentation_dir = os.path.join(path, "segmentations")
110    if not os.path.exists(segmentation_dir):
111        zip_path = os.path.join(path, "segmentations.zip")
112        util.download_source(path=zip_path, url=URL, download=download, checksum=CHECKSUM)
113        util.unzip(zip_path=zip_path, dst=segmentation_dir, remove=False)
114
115    series, label_dir = _get_labeled_series(segmentation_dir)
116
117    dicom_dir = os.path.join(path, "dicom")
118    uids = sorted(series["series_instance_uid"].unique())
119    if download:
120        util.download_tcia_series(uids, dst=dicom_dir, csv_filename=os.path.join(path, "vs_mc_rc_series"))
121    elif not all(glob(os.path.join(dicom_dir, uid, "*.dcm")) for uid in uids):
122        raise RuntimeError(f"Cannot find the data at {path}, but download was set to False.")
123
124    preprocessed_dir = os.path.join(path, "preprocessed")
125    _preprocess_vs_mc_rc(series, label_dir, dicom_dir, preprocessed_dir)
126    return preprocessed_dir

Download the Vestibular-Schwannoma-MC-RC dataset.

NOTE: This requires the pydicom and nibabel python packages. The DICOM series of the annotated timepoints are about 9 GB.

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 preprocessed data is stored.

def get_vs_mc_rc_paths( path: Union[os.PathLike, str], split: Literal['train', 'val', 'test'], download: bool = False) -> List[str]:
129def get_vs_mc_rc_paths(
130    path: Union[os.PathLike, str], split: Literal["train", "val", "test"], download: bool = False,
131) -> List[str]:
132    """Get paths to the Vestibular-Schwannoma-MC-RC data.
133
134    Args:
135        path: Filepath to a folder where the data is downloaded for further processing.
136        split: The data split to use. Either 'train', 'val' or 'test'.
137        download: Whether to download the data if it is not present.
138
139    Returns:
140        List of filepaths for the stored data.
141    """
142    assert split in ("train", "val", "test"), f"'{split}' is not a valid data split."
143
144    preprocessed_dir = get_vs_mc_rc_data(path, download)
145    volume_paths = natsorted(glob(os.path.join(preprocessed_dir, "*.h5")))
146    assert len(volume_paths) > 0, f"Could not find any preprocessed volumes in '{preprocessed_dir}'."
147
148    patient_ids = sorted({os.path.basename(p).split("_")[0] for p in volume_paths})
149    n_train, n_val = int(0.8 * len(patient_ids)), int(0.1 * len(patient_ids))
150    split_ids = {
151        "train": patient_ids[:n_train],
152        "val": patient_ids[n_train:n_train + n_val],
153        "test": patient_ids[n_train + n_val:],
154    }[split]
155    return [p for p in volume_paths if os.path.basename(p).split("_")[0] in split_ids]

Get paths to the Vestibular-Schwannoma-MC-RC data.

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

List of filepaths for the stored data.

def get_vs_mc_rc_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:
158def get_vs_mc_rc_dataset(
159    path: Union[os.PathLike, str],
160    patch_shape: Tuple[int, ...],
161    split: Literal["train", "val", "test"],
162    resize_inputs: bool = False,
163    download: bool = False,
164    **kwargs
165) -> Dataset:
166    """Get the Vestibular-Schwannoma-MC-RC dataset for tumor segmentation.
167
168    Args:
169        path: Filepath to a folder where the data is downloaded for further processing.
170        patch_shape: The patch shape to use for training.
171        split: The data split to use. Either 'train', 'val' or 'test'.
172        resize_inputs: Whether to resize inputs to the desired patch shape.
173        download: Whether to download the data if it is not present.
174        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset`.
175
176    Returns:
177        The segmentation dataset.
178    """
179    volume_paths = get_vs_mc_rc_paths(path, split, download)
180
181    if resize_inputs:
182        resize_kwargs = {"patch_shape": patch_shape, "is_rgb": False}
183        kwargs, patch_shape = util.update_kwargs_for_resize_trafo(
184            kwargs=kwargs, patch_shape=patch_shape, resize_inputs=resize_inputs, resize_kwargs=resize_kwargs
185        )
186
187    return torch_em.default_segmentation_dataset(
188        raw_paths=volume_paths,
189        raw_key="raw",
190        label_paths=volume_paths,
191        label_key="labels",
192        patch_shape=patch_shape,
193        is_seg_dataset=True,
194        **kwargs
195    )

Get the Vestibular-Schwannoma-MC-RC dataset for tumor 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 data split to use. 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_vs_mc_rc_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:
198def get_vs_mc_rc_loader(
199    path: Union[os.PathLike, str],
200    batch_size: int,
201    patch_shape: Tuple[int, ...],
202    split: Literal["train", "val", "test"],
203    resize_inputs: bool = False,
204    download: bool = False,
205    **kwargs
206) -> DataLoader:
207    """Get the Vestibular-Schwannoma-MC-RC dataloader for tumor segmentation.
208
209    Args:
210        path: Filepath to a folder where the data is downloaded for further processing.
211        batch_size: The batch size for training.
212        patch_shape: The patch shape to use for training.
213        split: The data split to use. Either 'train', 'val' or 'test'.
214        resize_inputs: Whether to resize inputs to the desired patch shape.
215        download: Whether to download the data if it is not present.
216        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset` or for the PyTorch DataLoader.
217
218    Returns:
219        The DataLoader.
220    """
221    ds_kwargs, loader_kwargs = util.split_kwargs(torch_em.default_segmentation_dataset, **kwargs)
222    dataset = get_vs_mc_rc_dataset(path, patch_shape, split, resize_inputs, download, **ds_kwargs)
223    return torch_em.get_data_loader(dataset, batch_size, **loader_kwargs)

Get the Vestibular-Schwannoma-MC-RC dataloader for tumor 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 data split to use. 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.