torch_em.data.datasets.medical.rumc_kidney

The RUMC Kidney dataset contains annotations for kidney and kidney abnormality segmentation in contrast-enhanced thorax-abdomen CT scans.

The scans were collected at the Radboud University Medical Center (RUMC), Nijmegen.

The label ids are - kidney: 1, abnormality: 2

NOTE: The abnormality class merges the five types the authors annotated (tumors, cysts, masses, lesions and metastases) into one id, i.e. tumors cannot be told apart from cysts. Use torch_em.data.datasets.medical.kits if you need labels that separate tumors from cysts.

The dataset is located at https://doi.org/10.5281/zenodo.8014290. This dataset is from the publication https://doi.org/10.48550/arXiv.2309.03383. Please cite it if you use this dataset for your research.

  1"""The RUMC Kidney dataset contains annotations for kidney and kidney abnormality
  2segmentation in contrast-enhanced thorax-abdomen CT scans.
  3
  4The scans were collected at the Radboud University Medical Center (RUMC), Nijmegen.
  5
  6The label ids are - kidney: 1, abnormality: 2
  7
  8NOTE: The abnormality class merges the five types the authors annotated (tumors, cysts, masses,
  9lesions and metastases) into one id, i.e. tumors cannot be told apart from cysts. Use
 10`torch_em.data.datasets.medical.kits` if you need labels that separate tumors from cysts.
 11
 12The dataset is located at https://doi.org/10.5281/zenodo.8014290.
 13This dataset is from the publication https://doi.org/10.48550/arXiv.2309.03383.
 14Please cite it if you use this dataset for your research.
 15"""
 16
 17import os
 18import json
 19from glob import glob
 20from tqdm import tqdm
 21from natsort import natsorted
 22from typing import Union, Tuple, List, Literal
 23
 24import numpy as np
 25
 26from sklearn.model_selection import train_test_split
 27
 28from torch.utils.data import Dataset, DataLoader
 29
 30import torch_em
 31
 32from .. import util
 33
 34
 35URL = "https://zenodo.org/records/8014290/files/{}?download=1"
 36
 37CHECKSUMS = {
 38    "images1.zip": "5b7af2786d0b2281c73a6e0c69eb4982998786a964dc9256fa7fee931a035d7a",
 39    "images2.zip": "2f4e3ebfb1f0aeeffd85c84bccbcae33cd8623193e839cc12a5cd58abb6e8a9e",
 40    "images3.zip": "c7df9dc1cb017e2ca77f74c7f7d90f5f1e2dbb4a16eb7c7b49393bdc65107eed",
 41    "images4.zip": "73aa389b54791a5abd722051b9a0ea2337aa1098f6b87b59e2aa1486a89157f1",
 42    "segmentations.zip": "7d711e306e52fe37216c126c3b282935f916db594193a2d1e3fdf13a2757c163",
 43}
 44
 45IMAGE_DIRS = ("images1", "images2", "images3", "images4")
 46LABEL_IDS = {"kidney": 1, "abnormality": 2}
 47VALID_SPLITS = ("train", "val", "test")
 48SUFFIX = ".nii.gz"
 49METAIMAGE_EXTS = (".mha", ".mhd")
 50
 51# The NIfTI extension code for free-form text, which we use to store the original header as json.
 52NIFTI_COMMENT_ECODE = 6
 53
 54
 55def _read_metaimage_header(path):
 56    header, is_mha = {}, path.endswith(".mha")
 57    with open(path, "rb") as f:
 58        for line in f:
 59            if b"=" not in line:
 60                break
 61            key, value = line.decode("latin-1").split("=", 1)
 62            key = key.strip()
 63            header[key] = value.strip()
 64            # For '.mha' the binary data directly follows the header, so we must not read any further.
 65            if is_mha and key == "ElementDataFile":
 66                break
 67    return header
 68
 69
 70def _itk_geometry_to_ras_affine(spacing, origin, direction):
 71    affine = np.eye(4, dtype="float64")
 72    direction = np.asarray(direction, dtype="float64").reshape(3, 3)
 73    affine[:3, :3] = direction @ np.diag(np.asarray(spacing, dtype="float64"))
 74    affine[:3, 3] = np.asarray(origin, dtype="float64")
 75    # ITK stores the geometry in LPS, whereas NIfTI expects RAS, so the first two axes flip sign.
 76    return np.diag([-1.0, -1.0, 1.0, 1.0]) @ affine
 77
 78
 79def _convert_mha_to_nifti(path: str, output_path: str, keep_metadata: bool = True) -> str:
 80    """Convert a MetaImage file to the NIfTI format.
 81
 82    The voxel spacing, origin and direction are mapped to the NIfTI affine, so that the converted
 83    volume keeps its physical geometry. The original MetaImage header is stored verbatim in a NIfTI
 84    header extension, so that the metadata stays inside the converted file.
 85
 86    Requires the SimpleITK python library.
 87
 88    Args:
 89        path: Filepath to the MetaImage file ('.mha' or '.mhd').
 90        output_path: Filepath for the converted file. Use a '.nii' suffix to write an uncompressed
 91            volume, which can be memory-mapped for lazy loading, or '.nii.gz' to write a compressed one.
 92        keep_metadata: Whether to store the original header in a NIfTI header extension.
 93
 94    Returns:
 95        The filepath to the converted file.
 96    """
 97    import nibabel as nib
 98    import SimpleITK as sitk
 99
100    if not path.endswith(METAIMAGE_EXTS):
101        raise ValueError(f"The provided file ({path}) isn't in MetaImage format.")
102
103    image = sitk.ReadImage(path)
104    affine = _itk_geometry_to_ras_affine(image.GetSpacing(), image.GetOrigin(), image.GetDirection())
105
106    # SimpleITK returns the array in 'zyx' order, whereas the NIfTI affine refers to 'xyz'.
107    data = sitk.GetArrayFromImage(image).transpose(2, 1, 0)
108
109    nifti = nib.Nifti1Image(data, affine)
110    nifti.set_data_dtype(data.dtype)
111    nifti.header.set_xyzt_units(xyz="mm")
112
113    if keep_metadata:
114        metadata = {
115            "metaimage_header": _read_metaimage_header(path),
116            "itk_metadata": {k: image.GetMetaData(k) for k in image.GetMetaDataKeys()},
117        }
118        payload = json.dumps(metadata).encode("utf-8")
119        nifti.header.extensions.append(nib.nifti1.Nifti1Extension(NIFTI_COMMENT_ECODE, payload))
120
121    os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
122    nib.save(nifti, output_path)
123    return output_path
124
125
126def _read_nifti_metadata(path: str) -> dict:
127    """Read the metadata that `_convert_mha_to_nifti` stored in a NIfTI header extension.
128
129    Args:
130        path: Filepath to the NIfTI file.
131
132    Returns:
133        The original metadata. Empty if the file does not carry any.
134    """
135    import nibabel as nib
136
137    for extension in nib.load(path).header.extensions:
138        if extension.get_code() != NIFTI_COMMENT_ECODE:
139            continue
140        try:
141            return json.loads(extension.get_content())
142        except (ValueError, UnicodeDecodeError):  # Some other tool wrote a plain text comment.
143            continue
144    return {}
145
146
147class _SelectLabel:
148    def __init__(self, label_id):
149        self.label_id = label_id
150
151    def __call__(self, labels):
152        return (labels == self.label_id).astype("uint8")
153
154
155def _find_image_path(path, case_id):
156    for image_dir in IMAGE_DIRS:
157        image_path = os.path.join(path, image_dir, f"{case_id}.mha")
158        if os.path.exists(image_path):
159            return image_path
160
161    raise FileNotFoundError(f"Could not find the image for case '{case_id}'.")
162
163
164def _preprocess_inputs(path):
165    # The volumes are converted to nifti, because elf has no file wrapper for MetaImage and would
166    # have to load each volume into memory as a whole.
167    image_dir = os.path.join(path, "preprocessed", "images")
168    label_dir = os.path.join(path, "preprocessed", "labels")
169    os.makedirs(image_dir, exist_ok=True)
170    os.makedirs(label_dir, exist_ok=True)
171
172    label_paths = natsorted(glob(os.path.join(path, "segmentations", "*.mha")))
173    if not label_paths:
174        raise RuntimeError(f"Could not find the segmentations in '{path}'.")
175
176    for label_path in tqdm(label_paths, desc="Preprocessing inputs"):
177        case_id = os.path.basename(label_path)[:-len("_segmentations.mha")]
178
179        target_image_path = os.path.join(image_dir, f"{case_id}{SUFFIX}")
180        target_label_path = os.path.join(label_dir, f"{case_id}{SUFFIX}")
181        if os.path.exists(target_image_path) and os.path.exists(target_label_path):
182            continue
183
184        _convert_mha_to_nifti(_find_image_path(path, case_id), target_image_path)
185        _convert_mha_to_nifti(label_path, target_label_path)
186
187
188def get_rumc_kidney_data(path: Union[os.PathLike, str], download: bool = False) -> str:
189    """Download the RUMC Kidney dataset.
190
191    The download is roughly 38 GB, and the volumes converted to nifti take up roughly 40 GB more.
192
193    Args:
194        path: Filepath to a folder where the data is downloaded for further processing.
195        download: Whether to download the data if it is not present.
196
197    Returns:
198        The folder where the dataset is downloaded and preprocessed.
199    """
200    data_dir = os.path.join(path, "preprocessed")
201    if os.path.exists(data_dir):
202        return data_dir
203
204    os.makedirs(path, exist_ok=True)
205
206    for fname, checksum in CHECKSUMS.items():
207        zip_path = os.path.join(path, fname)
208        util.download_source(path=zip_path, url=URL.format(fname), download=download, checksum=checksum)
209        util.unzip(zip_path=zip_path, dst=path)
210
211    _preprocess_inputs(path)
212
213    return data_dir
214
215
216def _get_split_map(path, data_dir):
217    split_path = os.path.join(path, "splits_rumc_kidney.json")
218    if os.path.exists(split_path):
219        with open(split_path) as f:
220            return json.load(f)
221
222    case_ids = [os.path.basename(p).split(".")[0] for p in glob(os.path.join(data_dir, "images", f"*{SUFFIX}"))]
223    train_ids, test_ids = train_test_split(natsorted(case_ids), test_size=0.25, random_state=42)
224    train_ids, val_ids = train_test_split(train_ids, test_size=0.1, random_state=42)
225
226    split_map = {"train": train_ids, "val": val_ids, "test": test_ids}
227    with open(split_path, "w") as f:
228        json.dump(split_map, f, indent=2)
229
230    return split_map
231
232
233def get_rumc_kidney_paths(
234    path: Union[os.PathLike, str],
235    split: Literal["train", "val", "test"],
236    download: bool = False,
237) -> Tuple[List[str], List[str]]:
238    """Get paths to the RUMC Kidney data.
239
240    Args:
241        path: Filepath to a folder where the data is downloaded for further processing.
242        split: Which data split to use.
243        download: Whether to download the data if it is not present.
244
245    Returns:
246        List of filepaths for the image data.
247        List of filepaths for the label data.
248    """
249    if split not in VALID_SPLITS:
250        raise ValueError(f"Invalid split '{split}'. Must be one of {VALID_SPLITS}.")
251
252    data_dir = get_rumc_kidney_data(path, download)
253    split_map = _get_split_map(path, data_dir)
254
255    raw_paths = [os.path.join(data_dir, "images", f"{case_id}{SUFFIX}") for case_id in split_map[split]]
256    label_paths = [os.path.join(data_dir, "labels", f"{case_id}{SUFFIX}") for case_id in split_map[split]]
257
258    missing = [p for p in raw_paths + label_paths if not os.path.exists(p)]
259    if missing:
260        raise RuntimeError(f"Could not find {len(missing)} files, e.g. '{missing[0]}'.")
261
262    return raw_paths, label_paths
263
264
265def get_rumc_kidney_dataset(
266    path: Union[os.PathLike, str],
267    patch_shape: Tuple[int, ...],
268    split: Literal["train", "val", "test"],
269    label_choice: Literal["all", "kidney", "abnormality"] = "all",
270    resize_inputs: bool = False,
271    download: bool = False,
272    **kwargs
273) -> Dataset:
274    """Get the RUMC Kidney dataset for kidney and kidney abnormality segmentation.
275
276    Args:
277        path: Filepath to a folder where the data is downloaded for further processing.
278        patch_shape: The patch shape to use for training.
279        split: Which data split to use.
280        label_choice: Which labels to use. 'all' keeps both classes, the other choices return a
281            binary mask for that class alone.
282        resize_inputs: Whether to resize inputs to the desired patch shape.
283        download: Whether to download the data if it is not present.
284        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset`.
285
286    Returns:
287        The segmentation dataset.
288    """
289    raw_paths, label_paths = get_rumc_kidney_paths(path, split, download)
290
291    if label_choice != "all":
292        if label_choice not in LABEL_IDS:
293            raise ValueError(f"Invalid label choice '{label_choice}'. Must be 'all' or one of {tuple(LABEL_IDS)}.")
294
295        kwargs = util.update_kwargs(kwargs, "label_transform", _SelectLabel(LABEL_IDS[label_choice]))
296
297    if resize_inputs:
298        resize_kwargs = {"patch_shape": patch_shape, "is_rgb": False}
299        kwargs, patch_shape = util.update_kwargs_for_resize_trafo(
300            kwargs=kwargs, patch_shape=patch_shape, resize_inputs=resize_inputs, resize_kwargs=resize_kwargs
301        )
302
303    return torch_em.default_segmentation_dataset(
304        raw_paths=raw_paths, raw_key="data", label_paths=label_paths, label_key="data",
305        patch_shape=patch_shape, **kwargs
306    )
307
308
309def get_rumc_kidney_loader(
310    path: Union[os.PathLike, str],
311    batch_size: int,
312    patch_shape: Tuple[int, ...],
313    split: Literal["train", "val", "test"],
314    label_choice: Literal["all", "kidney", "abnormality"] = "all",
315    resize_inputs: bool = False,
316    download: bool = False,
317    **kwargs
318) -> DataLoader:
319    """Get the RUMC Kidney dataloader for kidney and kidney abnormality segmentation.
320
321    Args:
322        path: Filepath to a folder where the data is downloaded for further processing.
323        batch_size: The batch size for training.
324        patch_shape: The patch shape to use for training.
325        split: Which data split to use.
326        label_choice: Which labels to use. 'all' keeps both classes, the other choices return a
327            binary mask for that class alone.
328        resize_inputs: Whether to resize inputs to the desired patch shape.
329        download: Whether to download the data if it is not present.
330        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset` or for the PyTorch DataLoader.
331
332    Returns:
333        The DataLoader.
334    """
335    ds_kwargs, loader_kwargs = util.split_kwargs(torch_em.default_segmentation_dataset, **kwargs)
336    dataset = get_rumc_kidney_dataset(
337        path, patch_shape, split, label_choice, resize_inputs, download, **ds_kwargs
338    )
339    return torch_em.get_data_loader(dataset, batch_size, **loader_kwargs)
URL = 'https://zenodo.org/records/8014290/files/{}?download=1'
CHECKSUMS = {'images1.zip': '5b7af2786d0b2281c73a6e0c69eb4982998786a964dc9256fa7fee931a035d7a', 'images2.zip': '2f4e3ebfb1f0aeeffd85c84bccbcae33cd8623193e839cc12a5cd58abb6e8a9e', 'images3.zip': 'c7df9dc1cb017e2ca77f74c7f7d90f5f1e2dbb4a16eb7c7b49393bdc65107eed', 'images4.zip': '73aa389b54791a5abd722051b9a0ea2337aa1098f6b87b59e2aa1486a89157f1', 'segmentations.zip': '7d711e306e52fe37216c126c3b282935f916db594193a2d1e3fdf13a2757c163'}
IMAGE_DIRS = ('images1', 'images2', 'images3', 'images4')
LABEL_IDS = {'kidney': 1, 'abnormality': 2}
VALID_SPLITS = ('train', 'val', 'test')
SUFFIX = '.nii.gz'
METAIMAGE_EXTS = ('.mha', '.mhd')
NIFTI_COMMENT_ECODE = 6
def get_rumc_kidney_data(path: Union[os.PathLike, str], download: bool = False) -> str:
189def get_rumc_kidney_data(path: Union[os.PathLike, str], download: bool = False) -> str:
190    """Download the RUMC Kidney dataset.
191
192    The download is roughly 38 GB, and the volumes converted to nifti take up roughly 40 GB more.
193
194    Args:
195        path: Filepath to a folder where the data is downloaded for further processing.
196        download: Whether to download the data if it is not present.
197
198    Returns:
199        The folder where the dataset is downloaded and preprocessed.
200    """
201    data_dir = os.path.join(path, "preprocessed")
202    if os.path.exists(data_dir):
203        return data_dir
204
205    os.makedirs(path, exist_ok=True)
206
207    for fname, checksum in CHECKSUMS.items():
208        zip_path = os.path.join(path, fname)
209        util.download_source(path=zip_path, url=URL.format(fname), download=download, checksum=checksum)
210        util.unzip(zip_path=zip_path, dst=path)
211
212    _preprocess_inputs(path)
213
214    return data_dir

Download the RUMC Kidney dataset.

The download is roughly 38 GB, and the volumes converted to nifti take up roughly 40 GB more.

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:

The folder where the dataset is downloaded and preprocessed.

def get_rumc_kidney_paths( path: Union[os.PathLike, str], split: Literal['train', 'val', 'test'], download: bool = False) -> Tuple[List[str], List[str]]:
234def get_rumc_kidney_paths(
235    path: Union[os.PathLike, str],
236    split: Literal["train", "val", "test"],
237    download: bool = False,
238) -> Tuple[List[str], List[str]]:
239    """Get paths to the RUMC Kidney data.
240
241    Args:
242        path: Filepath to a folder where the data is downloaded for further processing.
243        split: Which data split to use.
244        download: Whether to download the data if it is not present.
245
246    Returns:
247        List of filepaths for the image data.
248        List of filepaths for the label data.
249    """
250    if split not in VALID_SPLITS:
251        raise ValueError(f"Invalid split '{split}'. Must be one of {VALID_SPLITS}.")
252
253    data_dir = get_rumc_kidney_data(path, download)
254    split_map = _get_split_map(path, data_dir)
255
256    raw_paths = [os.path.join(data_dir, "images", f"{case_id}{SUFFIX}") for case_id in split_map[split]]
257    label_paths = [os.path.join(data_dir, "labels", f"{case_id}{SUFFIX}") for case_id in split_map[split]]
258
259    missing = [p for p in raw_paths + label_paths if not os.path.exists(p)]
260    if missing:
261        raise RuntimeError(f"Could not find {len(missing)} files, e.g. '{missing[0]}'.")
262
263    return raw_paths, label_paths

Get paths to the RUMC Kidney data.

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

List of filepaths for the image data. List of filepaths for the label data.

def get_rumc_kidney_dataset( path: Union[os.PathLike, str], patch_shape: Tuple[int, ...], split: Literal['train', 'val', 'test'], label_choice: Literal['all', 'kidney', 'abnormality'] = 'all', resize_inputs: bool = False, download: bool = False, **kwargs) -> torch.utils.data.dataset.Dataset:
266def get_rumc_kidney_dataset(
267    path: Union[os.PathLike, str],
268    patch_shape: Tuple[int, ...],
269    split: Literal["train", "val", "test"],
270    label_choice: Literal["all", "kidney", "abnormality"] = "all",
271    resize_inputs: bool = False,
272    download: bool = False,
273    **kwargs
274) -> Dataset:
275    """Get the RUMC Kidney dataset for kidney and kidney abnormality segmentation.
276
277    Args:
278        path: Filepath to a folder where the data is downloaded for further processing.
279        patch_shape: The patch shape to use for training.
280        split: Which data split to use.
281        label_choice: Which labels to use. 'all' keeps both classes, the other choices return a
282            binary mask for that class alone.
283        resize_inputs: Whether to resize inputs to the desired patch shape.
284        download: Whether to download the data if it is not present.
285        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset`.
286
287    Returns:
288        The segmentation dataset.
289    """
290    raw_paths, label_paths = get_rumc_kidney_paths(path, split, download)
291
292    if label_choice != "all":
293        if label_choice not in LABEL_IDS:
294            raise ValueError(f"Invalid label choice '{label_choice}'. Must be 'all' or one of {tuple(LABEL_IDS)}.")
295
296        kwargs = util.update_kwargs(kwargs, "label_transform", _SelectLabel(LABEL_IDS[label_choice]))
297
298    if resize_inputs:
299        resize_kwargs = {"patch_shape": patch_shape, "is_rgb": False}
300        kwargs, patch_shape = util.update_kwargs_for_resize_trafo(
301            kwargs=kwargs, patch_shape=patch_shape, resize_inputs=resize_inputs, resize_kwargs=resize_kwargs
302        )
303
304    return torch_em.default_segmentation_dataset(
305        raw_paths=raw_paths, raw_key="data", label_paths=label_paths, label_key="data",
306        patch_shape=patch_shape, **kwargs
307    )

Get the RUMC Kidney dataset for kidney and kidney abnormality 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: Which data split to use.
  • label_choice: Which labels to use. 'all' keeps both classes, the other choices return a binary mask for that class alone.
  • 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_rumc_kidney_loader( path: Union[os.PathLike, str], batch_size: int, patch_shape: Tuple[int, ...], split: Literal['train', 'val', 'test'], label_choice: Literal['all', 'kidney', 'abnormality'] = 'all', resize_inputs: bool = False, download: bool = False, **kwargs) -> torch.utils.data.dataloader.DataLoader:
310def get_rumc_kidney_loader(
311    path: Union[os.PathLike, str],
312    batch_size: int,
313    patch_shape: Tuple[int, ...],
314    split: Literal["train", "val", "test"],
315    label_choice: Literal["all", "kidney", "abnormality"] = "all",
316    resize_inputs: bool = False,
317    download: bool = False,
318    **kwargs
319) -> DataLoader:
320    """Get the RUMC Kidney dataloader for kidney and kidney abnormality segmentation.
321
322    Args:
323        path: Filepath to a folder where the data is downloaded for further processing.
324        batch_size: The batch size for training.
325        patch_shape: The patch shape to use for training.
326        split: Which data split to use.
327        label_choice: Which labels to use. 'all' keeps both classes, the other choices return a
328            binary mask for that class alone.
329        resize_inputs: Whether to resize inputs to the desired patch shape.
330        download: Whether to download the data if it is not present.
331        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset` or for the PyTorch DataLoader.
332
333    Returns:
334        The DataLoader.
335    """
336    ds_kwargs, loader_kwargs = util.split_kwargs(torch_em.default_segmentation_dataset, **kwargs)
337    dataset = get_rumc_kidney_dataset(
338        path, patch_shape, split, label_choice, resize_inputs, download, **ds_kwargs
339    )
340    return torch_em.get_data_loader(dataset, batch_size, **loader_kwargs)

Get the RUMC Kidney dataloader for kidney and kidney abnormality 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: Which data split to use.
  • label_choice: Which labels to use. 'all' keeps both classes, the other choices return a binary mask for that class alone.
  • 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.